GraphRunbook.psm1

#region Show-GraphRunbookActivityTraces

function ExpectEvent($GraphTraceRecord, $ExpectedEventType, $ExpectedActivityName) {
    $ActualEventType = $GraphTraceRecord.Event
    $ActualActivityName = $GraphTraceRecord.Activity

    if (($ActualEventType -ne $ExpectedEventType) -or
            (($null -ne $ExpectedActivityName) -and ($ActualActivityName -ne $ExpectedActivityName))) {
        throw "Unexpected event $ActualEventType/$ActualActivityName (expected $ExpectedEventType/$ExpectedActivityName)"
    }
}

function GetGraphTraces($ResourceGroupName, $AutomationAccountName, $JobId) {
    Write-Verbose "Retrieving traces for job $JobId..."

    $GraphTracePrefix = "GraphTrace:"

    Get-AzureRmAutomationJobOutput `
            -ResourceGroupName $ResourceGroupName `
            -AutomationAccountName $AutomationAccountName `
            -Id $JobId `
            -Stream Verbose |
        Get-AzureRmAutomationJobOutputRecord |
        ForEach-Object Value |
        ForEach-Object Message |
        Where-Object { $_.StartsWith($GraphTracePrefix) } |
        ForEach-Object { $_.Substring($GraphTracePrefix.Length) } |
        ConvertFrom-Json
}

function GetActivityExecutionInstances($GraphTraces) {
    $GraphTracePos = 0

    while ($GraphTracePos -lt $GraphTraces.Count) {
        ExpectEvent $GraphTraces[$GraphTracePos] 'ActivityStart'
        $Activity = $GraphTraces[$GraphTracePos].Activity
        $Start = $GraphTraces[$GraphTracePos].Time
        $GraphTracePos += 1

        $Input = $null
        if ($GraphTraces[$GraphTracePos].Event -eq 'ActivityInput') {
            ExpectEvent $GraphTraces[$GraphTracePos] 'ActivityInput' $Activity
            $Input = $GraphTraces[$GraphTracePos].Values.Data
            $GraphTracePos += 1
        }

        ExpectEvent $GraphTraces[$GraphTracePos] 'ActivityOutput' $Activity
        $Output = $GraphTraces[$GraphTracePos].Values.Data
        $GraphTracePos += 1

        ExpectEvent $GraphTraces[$GraphTracePos] 'ActivityEnd' $Activity
        $End = $GraphTraces[$GraphTracePos].Time
        $DurationSeconds = $GraphTraces[$GraphTracePos].DurationSeconds
        $GraphTracePos += 1

        $ActivityExecution = New-Object -TypeName PsObject
        Add-Member -InputObject $ActivityExecution -MemberType NoteProperty -Name Activity -Value $Activity
        Add-Member -InputObject $ActivityExecution -MemberType NoteProperty -Name Start -Value (Get-Date $Start)
        Add-Member -InputObject $ActivityExecution -MemberType NoteProperty -Name End -Value (Get-Date $End)
        Add-Member -InputObject $ActivityExecution -MemberType NoteProperty -Name Duration -Value ([System.TimeSpan]::FromSeconds($DurationSeconds))
        if ($Input) {
            Add-Member -InputObject $ActivityExecution -MemberType NoteProperty -Name Input -Value $Input
        }
        Add-Member -InputObject $ActivityExecution -MemberType NoteProperty -Name Output -Value $Output

        $ActivityExecution
    }
}

function GetLatestJobByRunbookName($ResourceGroupName, $AutomationAccountName, $RunbookName) {
    Write-Verbose "Looking for the latest job for runbook $RunbookName..."

    Get-AzureRmAutomationJob `
                -RunbookName $RunbookName `
                -ResourceGroupName $ResourceGroupName `
                -AutomationAccountName $AutomationAccountName |
        Sort-Object StartTime -Descending |
        Select-Object -First 1
}

function Show-GraphRunbookActivityTraces {
<#
.SYNOPSIS
Shows graphical runbook activity traces for an Azure Automation job

.DESCRIPTION
Activity tracing data is extremely helpful when testing and troubleshooting graphical runbooks in Azure Automation: it shows the execution order of activities, activity start and finish time, activity input and output data, and more. Azure Automation saves this data encoded in JSON in the job Verbose stream. Even though this data is very valuable, the raw JSON format may be hard to read, especially when activities input and output large and complex objects. Show-GraphRunbookActivityTraces command retrieves activity tracing data and displays it in a user-friendly tree structure:

    - Activity execution instance 1
        - Activity name, start time, end time, duration, etc.
        - Input
            - <parameter name> : <object>
            - <parameter name> : <object>
            ...
        - Output
            - <output object 1>
            - <output object 2>
            ...
    - Activity execution instance 2
    ...

Prerequisites
=============

1. Make sure you add an authenticated Azure account (for example, use Add-AzureRmAcccount cmdlet) before invoking Show-GraphRunbookActivityTraces.

2. In the Azure Portal, enable activity-level tracing *and* verbose logging for a graphical runbook:
    - Runbook Settings -> Logging and tracing
        - Logging verbose records: *On*
        - Trace level: *Basic* or *Detailed*

3. Start the runbook and take note of the job ID.

.PARAMETER ResourceGroupName
Azure Resource Group name

.PARAMETER AutomationAccountName
Azure Automation Account name

.PARAMETER JobId
Azure Automation graphical runbook job ID

.PARAMETER RunbookName
Runbook name

.EXAMPLE
Show-GraphRunbookActivityTraces -ResourceGroupName myresourcegroup -AutomationAccountName myautomationaccount -JobId b15d38a1-ddea-49d1-bd90-407f66f282ef

.LINK
Source code: https://github.com/azureautomation/graphical-runbook-tools

.LINK
Azure Automation: https://azure.microsoft.com/services/automation
#>

    [CmdletBinding()]

    param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = "ByJobId")]
        [Alias('Id')]
        [guid]
        $JobId,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = "ByRunbookName")]
        [string]
        $RunbookName,

        [Parameter(Mandatory = $true)]
        [string]
        $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [string]
        $AutomationAccountName
    )

    process {
        switch ($PSCmdlet.ParameterSetName) {
            "ByJobId" {
                $GraphTraces = GetGraphTraces $ResourceGroupName $AutomationAccountName $JobId
            }

            "ByRunbookName" {
                $Job = GetLatestJobByRunbookName `
                            -RunbookName $RunbookName `
                            -ResourceGroupName $ResourceGroupName `
                            -AutomationAccountName $AutomationAccountName

                if ($Job) {
                    $JobId = $Job.JobId
                    $GraphTraces = GetGraphTraces $ResourceGroupName $AutomationAccountName $JobId
                }
                else {
                    Write-Error -Message "No job found for runbook $RunbookName."
                }
            }
        }

        $ActivityExecutionInstances = GetActivityExecutionInstances $GraphTraces
        if ($ActivityExecutionInstances) {
            $ObjectToShow = New-Object PsObject -Property @{
                'Job ID' = $JobId
                'Activity execution instances' = $ActivityExecutionInstances
            }

            Show-Object -InputObject @($ObjectToShow)
        }
        else {
            Write-Error -Message ('No activity traces found. Make sure activity tracing and ' +
                                  'logging Verbose stream are enabled in the runbook configuration.')
        }
    }
}

#endregion

#region Convert-GraphRunbookToPowerShellData

function Get-ActivityById([Orchestrator.GraphRunbook.Model.GraphRunbook]$Runbook, $ActivityId) {
    $Result = $Runbook.Activities | ForEach-Object { $_ } | Where-Object { $_.EntityId -eq $ActivityId }
    if (-not $Result) {
        throw "Cannot find activity by entity ID: $ActivityId"
    }
    $Result
}

function Get-Indent($IndentLevel) {
    ' ' * $IndentLevel * 4
}

function NullIfPositionZeroZero([Orchestrator.GraphRunbook.Model.IPositionedEntity]$Value) {
    if (($Value.PositionX -eq 0) -and ($Value.PositionY -eq 0)) {
        $null
    }
    else {
        [System.Tuple]::Create($Value.PositionX, $Value.PositionY)
    }
}

function NullIfEmptyString($Value) {
    if ([string]::IsNullOrEmpty($Value)) {
        $null
    }
    else {
        $Value
    }
}

function NullIfEmptyDictionary($Value) {
    if (($null -eq $Value) -or ($Value.Count -eq 0)) {
        $null
    }
    else {
        $Value
    }
}

function IsDefaultValue($Value) {
    ($null -eq $Value) -or
    (($Value -is [bool]) -and ($Value -eq $false)) -or
    (($Value -is [Orchestrator.GraphRunbook.Model.Condition]) -and
        ($Value.Mode -eq [Orchestrator.GraphRunbook.Model.ConditionMode]::Disabled) -and
        ([string]::IsNullOrEmpty($Value.Expression))) -or
    (($Value -is [Orchestrator.GraphRunbook.Model.ExecutableView.LinkStreamType]) -and
        ($Value -eq [Orchestrator.GraphRunbook.Model.ExecutableView.LinkStreamType]::Output))
}

function CreateScriptBlockIfNotEmpty($Value)
{
    if ($Value) {
        [scriptblock]::Create($Value)
    }
    else {
        $null
    }
}

function ConvertListToPsd($IndentLevel, [System.Collections.IList]$Value) {
    if ($Value.Count -eq 0) {
        '@()'
    }
    else {
        $Result = "@(`r`n"
        $NextIndentLevel = $IndentLevel + 1
        foreach ($Item in $Value) {
            $Result += "$(Get-Indent $NextIndentLevel)$(ConvertValueToPsd -IndentLevel $NextIndentLevel -Value $Item)`r`n"
        }
        $Result += "$(Get-Indent $IndentLevel))"
        $Result
    }
}

function ConvertDictionaryToPsd($IndentLevel, [System.Collections.IDictionary]$Value) {
    $Result = "@{`r`n"
    $NextIndentLevel = $IndentLevel + 1
    foreach ($Entry in $Value.GetEnumerator()) {
        if (-not (IsDefaultValue $Entry.Value)) {
            $Result += "$(ConvertNamedValueToPsd -IndentLevel $NextIndentLevel -Name $Entry.Key -Value $Entry.Value)`r`n"
        }
    }
    $Result += "$(Get-Indent $IndentLevel)}"
    $Result
}

function ConvertTuple2IntToPsd($IndentLevel, [System.Tuple`2[[int], [int]]]$Value) {
    "$($Value.Item1), $($Value.Item2)"
}

function ConvertScriptBlockToPsd($IndentLevel, [scriptblock]$Value) {
    $NextIndentLevel = $IndentLevel + 1
    "{`r`n$(Get-Indent $NextIndentLevel)$Value`r`n$(Get-Indent $IndentLevel)}"
}

function GetActivityTypeName([Orchestrator.GraphRunbook.Model.ExecutableView.IActivity]$Activity)
{
    if ($Activity -is [Orchestrator.GraphRunbook.Model.WorkflowScriptActivity]) {
        'Code'
    }
    elseif ($Activity -is [Orchestrator.GraphRunbook.Model.CommandActivity]) {
        'Command'
    }
    elseif ($Activity -is [Orchestrator.GraphRunbook.Model.InvokeRunbookActivity]) {
        'InvokeRunbook'
    }
    elseif ($Activity -is [Orchestrator.GraphRunbook.Model.JunctionActivity]) {
        'Junction'
    }
    else {
        throw "Activity '$($Activity.Name)' is of unknown type: $($Activity.GetType().FullName)"
    }
}

function CreateRetry($ExitCondition, $Delay) {
    $IsExitConditionDataPresent =
        ($null -ne $ExitCondition) -and
        (($ExitCondition.Mode -eq [Orchestrator.GraphRunbook.Model.ConditionMode]::Enabled) -or
            (-not [string]::IsNullOrEmpty($ExitCondition.Expression)))

    if ($IsExitConditionDataPresent) {
        [ordered]@{
            ExitCondition = $ExitCondition
            Delay = $Delay
        }
    }
    else {
        $null
    }
}

function ConvertActivityToPsd($IndentLevel, [Orchestrator.GraphRunbook.Model.ExecutableView.IActivity]$Value) {
    $Properties = [ordered]@{ }
    
    $Properties.Add('Name', $Value.Name)
    $Properties.Add('Description', (NullIfEmptyString $Value.Description))
    $Properties.Add('Type', (GetActivityTypeName $Value))

    $Properties.Add('Begin', (CreateScriptBlockIfNotEmpty $Value.Begin))
    $Properties.Add('Process', (CreateScriptBlockIfNotEmpty $Value.Process))
    $Properties.Add('End', (CreateScriptBlockIfNotEmpty $Value.End))

    $Properties.Add('ModuleName', (NullIfEmptyString $Value.CommandType.ModuleName))
    $Properties.Add('CommandName', $Value.InvocationActivityType.CommandName)
    $Properties.Add('Parameters', (NullIfEmptyDictionary $Value.Parameters))
    $Properties.Add('CustomParameters', (NullIfEmptyString $Value.CustomParameters))
    
    $Properties.Add('CheckpointAfter', $Value.CheckpointAfter)
    $Properties.Add('ExceptionsToErrors', $Value.ExceptionsToErrors)
    $Properties.Add('Retry', (CreateRetry -ExitCondition $Value.LoopExitCondition -Delay $Value.LoopDelay))

    $Properties.Add('Position', (NullIfPositionZeroZero $Value))

    ConvertDictionaryToPsd -IndentLevel $IndentLevel -Value $Properties
}

function ConvertNamedReferenceToPsd($IndentLevel, $SourceType, $Name) {
    ConvertDictionaryToPsd -IndentLevel $IndentLevel -Value ([ordered]@{
        SourceType = $SourceType
        Name = $Name
    })
}

function ConvertValueDescriptorToPsd($IndentLevel, [Orchestrator.GraphRunbook.Model.ExecutableView.IValueDescriptor]$Value) {
    if ($Value -is [Orchestrator.GraphRunbook.Model.ExecutableView.IConstantValueDescriptor]) {
        ConvertValueToPsd -IndentLevel $IndentLevel -Value $Value.Value
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.ActivityOutputValueDescriptor]) {
        ConvertDictionaryToPsd -IndentLevel $IndentLevel -Value ([ordered]@{
            SourceType = 'ActivityOutput'
            Activity = $Value.ActivityName
            FieldPath = $Value.FieldPath
        })
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.PowerShellExpressionValueDescriptor]) {
        ConvertScriptBlockToPsd -IndentLevel $IndentLevel -Value (CreateScriptBlockIfNotEmpty $Value.Expression)
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.RunbookParameterValueDescriptor]) {
        ConvertNamedReferenceToPsd -IndentLevel $IndentLevel -SourceType 'RunbookParameter' -Name $Value.ParameterName
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.AutomationCertificateValueDescriptor]) {
        ConvertNamedReferenceToPsd -IndentLevel $IndentLevel -SourceType 'AutomationCertificate' -Name $Value.CertificateName
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.AutomationCredentialValueDescriptor]) {
        ConvertNamedReferenceToPsd -IndentLevel $IndentLevel -SourceType 'AutomationCredential' -Name $Value.CredentialName
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.AutomationConnectionValueDescriptor]) {
        ConvertNamedReferenceToPsd -IndentLevel $IndentLevel -SourceType 'AutomationConnection' -Name $Value.ConnectionName
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.AutomationVariableValueDescriptor]) {
        ConvertNamedReferenceToPsd -IndentLevel $IndentLevel -SourceType 'AutomationVariable' -Name $Value.VariableName
    }
    else {
        throw "Unknown value descriptor type: $($Value.GetType().FullName)"
    }
}

function ConvertLinkToPsd($IndentLevel, [Orchestrator.GraphRunbook.Model.Link]$Value) {
    $FromActivity = Get-ActivityById $Runbook $Value.SourceActivityEntityId
    $ToActivity = Get-ActivityById $Runbook $Value.DestinationActivityEntityId

    ConvertDictionaryToPsd -IndentLevel $IndentLevel -Value ([ordered]@{
        From = $FromActivity.Name
        To = $ToActivity.Name
        Description = (NullIfEmptyString $Value.Description)
        Stream = $Value.LinkStreamType
        Type = $Value.LinkType
        Condition = $Value.Condition
    })
}

function ConvertConditionToPsd($IndentLevel, [Orchestrator.GraphRunbook.Model.Condition]$Value) {
    if ($Value.Mode -eq [Orchestrator.GraphRunbook.Model.ConditionMode]::Enabled) {
        ConvertValueToPsd -IndentLevel $IndentLevel -Value (CreateScriptBlockIfNotEmpty $Value.Expression)
    }
    else {
        ConvertDictionaryToPsd -IndentLevel $IndentLevel -Value ([ordered]@{
            Mode = $Value.Mode
            Expression = CreateScriptBlockIfNotEmpty $Value.Expression
        })
    }
}

function ConvertCommentToPsd($IndentLevel, [Orchestrator.GraphRunbook.Model.Comment]$Value) {
    ConvertDictionaryToPsd -IndentLevel $IndentLevel -Value ([ordered]@{
        Name = $Value.Name
        Text = $Value.Text
        Position = NullIfPositionZeroZero $Value
    })
}

function ConvertParameterToPsd($IndentLevel, [Orchestrator.GraphRunbook.Model.Parameter]$Value) {
    ConvertDictionaryToPsd -IndentLevel $IndentLevel -Value ([ordered]@{
        Name = $Value.Name
        Description = (NullIfEmptyString $Value.Description)
        Mandatory = -not $Value.Optional
        DefaultValue = $Value.DefaultValue
    })
}

function ConvertValueToPsd($IndentLevel, $Value) {
    if ($null -eq $Value) {
        '$null'
    }
    elseif ($Value -is [bool]) {
        if ($Value) {
            '$true'
        }
        else {
            '$false'
        }
    }
    elseif ($Value -is [int]) {
        "$Value"
    }
    elseif ($Value -is [scriptblock]) {
        ConvertScriptBlockToPsd -IndentLevel $IndentLevel -Value $Value
    }
    elseif ($Value -is [System.TimeSpan]) {
        $Value.Ticks
    }
    elseif ($Value -is [System.Collections.IList]) {
        ConvertListToPsd -IndentLevel $IndentLevel -Value $Value
    }
    elseif ($Value -is [System.Collections.IDictionary]) {
        ConvertDictionaryToPsd -IndentLevel $IndentLevel -Value $Value
    }
    elseif ($Value -is [System.Tuple`2[[int], [int]]]) {
        ConvertTuple2IntToPsd -IndentLevel $IndentLevel -Value $Value
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.ExecutableView.IActivity]) {
        ConvertActivityToPsd -IndentLevel $IndentLevel -Value $Value
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.ExecutableView.IValueDescriptor]) {
        ConvertValueDescriptorToPsd -IndentLevel $IndentLevel -Value $Value
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.Link]) {
        ConvertLinkToPsd -IndentLevel $IndentLevel -Value $Value
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.Condition]) {
        ConvertConditionToPsd -IndentLevel $IndentLevel -Value $Value
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.Comment]) {
        ConvertCommentToPsd -IndentLevel $IndentLevel -Value $Value
    }
    elseif ($Value -is [Orchestrator.GraphRunbook.Model.Parameter]) {
        ConvertParameterToPsd -IndentLevel $IndentLevel -Value $Value
    }
    else {
        # PowerShell v.5+ required!
        "'$([Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($Value.ToString()))'"
    }
}

function ConvertNamedValueToPsd($IndentLevel, $Name, $Value) {
    "$(Get-Indent $IndentLevel)$Name = $(ConvertValueToPsd -IndentLevel $IndentLevel -Value $Value)"
}

function ConvertOptionalSectionToPsd($Name, $Data) {
    if ($Data) {
        "$(ConvertNamedValueToPsd -IndentLevel 0 -Name $Name -Value $Data)`r`n`r`n"
    }
    else {
        ''
    }
}

function Get-GraphicalAuthoringSdkDirectoryFromRegistry {
    Get-ItemPropertyValue -Path HKLM:\SOFTWARE\WOW6432Node\Microsoft\AzureAutomation\GraphicalAuthoringSDK -Name InstallPath
}

function Add-GraphRunbookModelAssembly($GraphicalAuthoringSdkDirectory) {
    if (-not $GraphicalAuthoringSdkDirectory) {
        $GraphicalAuthoringSdkDirectory = Get-GraphicalAuthoringSdkDirectoryFromRegistry
    }

    $ModelAssemblyPath = Join-Path $GraphicalAuthoringSdkDirectory 'Orchestrator.GraphRunbook.Model.dll'

    if (Test-Path $ModelAssemblyPath -PathType Leaf) {
        Add-Type -Path $ModelAssemblyPath
    }
    else {
        Write-Warning ("Assembly not found: $ModelAssemblyPath. Install Microsoft Azure Automation Graphical Authoring SDK " +
            "(https://www.microsoft.com/en-us/download/details.aspx?id=50734) and provide the installation directory path " +
            "in the GraphicalAuthoringSdkDirectory parameter.")
    }
}

function Get-GraphRunbookFromFile($FileName) {
    $SerializedRunbook = Get-Content -Path $FileName | Out-String
    $RunbookContainer = [Orchestrator.GraphRunbook.Model.Serialization.RunbookSerializer]::DeserializeRunbookContainer($SerializedRunbook)
    if ($RunbookContainer.SchemaVersion.Major -gt 1) {
        Write-Warning ("Runbook $FileName is serialized using schema version $($RunbookContainer.SchemaVersion). " +
            "Schema versions higher than 1.* may not be supported.")
    }

    [Orchestrator.GraphRunbook.Model.Serialization.RunbookSerializer]::GetRunbook($RunbookContainer)
}

function New-TemporaryDirectory {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Scope="Function")]
    param()

    $parent = [System.IO.Path]::GetTempPath()
    [string]$name = [System.Guid]::NewGuid()
    New-Item -ItemType Directory -Path (Join-Path $parent $name)
}

function Convert-GraphRunbookObjectToPowerShellData(
    [Parameter(Mandatory = $true)]
    [Orchestrator.GraphRunbook.Model.GraphRunbook]
    $Runbook) {

    $Result = "@{`r`n`r`n"
    $Result += ConvertOptionalSectionToPsd -Name Parameters -Data $Runbook.Parameters
    $Result += ConvertOptionalSectionToPsd -Name Comments -Data $Runbook.Comments
    $Result += ConvertOptionalSectionToPsd -Name OutputTypes -Data $Runbook.OutputTypes
    $Result += ConvertOptionalSectionToPsd -Name Activities -Data $Runbook.Activities
    $Result += ConvertOptionalSectionToPsd -Name Links -Data $Runbook.Links
    $Result += "}`r`n"

    $Result
}

function Convert-GraphRunbookFileToPowerShellData($RunbookFileName) {
    Write-Verbose "Converting runbook from file $RunbookFileName"
    $Runbook = Get-GraphRunbookFromFile -FileName $RunbookFileName
    Convert-GraphRunbookObjectToPowerShellData $Runbook
}

function WithExportedRunbook($RunbookName, $Slot, $ResourceGroupName, $AutomationAccountName, [scriptblock]$Action) {
    $OutputFolder = New-TemporaryDirectory
    Write-Verbose "Created temporary directory: $OutputFolder"
    try {
        Write-Verbose "Exporting runbook '$RunbookName' to temporary directory '$OutputFolder'"
        $RunbookFile = Export-AzureRMAutomationRunbook `
            -Name $RunbookName `
            -OutputFolder $OutputFolder `
            -Slot $Slot `
            -ResourceGroupName $ResourceGroupName `
            -AutomationAccountName $AutomationAccountName
        
        $FullFileName = Join-Path $OutputFolder $RunbookFile.Name
        Write-Verbose "Exported runbook '$RunbookName' to file '$FullFileName'"

        $Action.Invoke($FullFileName)
    }
    finally {
        Remove-Item $OutputFolder -Recurse -Force
    }
}

function Convert-GraphRunbookInAzureToPowerShellData($RunbookName, $Slot, $ResourceGroupName, $AutomationAccountName) {
    WithExportedRunbook -RunbookName $RunbookName -Slot $Slot -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Action {
        param($FullFileName)
        Convert-GraphRunbookFileToPowerShellData $FullFileName
    }
}

function Convert-GraphRunbookToPowerShellData {
<#
.SYNOPSIS
Converts a graphical runbook to PowerShell data

.DESCRIPTION
Converts a graphical runbook to PowerShell data. The resulting representation contains the entire runbook definition in a human-readable and PowerShell-readable text format. It can be used for inspecting and documenting runbooks, storing them in a source control system, comparing different versions, etc. Furthermore, the resulting representation is valid PowerShell code that constructs a data structure with all the runbook content, so you can save it in a .psd1 file, open it in any PowerShell editing tool, parse it with PowerShell, etc.

IMPORTANT NOTES
===============

1. The resulting PowerShell code is not an executable runbook. If this code is executed, it builds a data structure describing the original graphical runbook, but does not run the runbook.

2. Even though the resulting representation contains all the data from the original runbook, and it can theoretically be used to construct a runbook equivalent to the original one, there is no automated conversion back to .graphrunbook implemented yet. If you intend to use this runbook in Azure Automation later, do *not* discard the original .graphrunbook file after conversion.

Prerequisites
=============

1. Install Microsoft Azure Automation Graphical Authoring SDK (https://www.microsoft.com/en-us/download/details.aspx?id=50734).

2. Before invoking Convert-GraphRunbookToPowerShellData with RunbookName, ResourceGroupName, and AutomationAccountName parameters, make sure you add an authenticated Azure account (for example, use Add-AzureRmAcccount cmdlet).

.PARAMETER Runbook
An instance of Orchestrator.GraphRunbook.Model.GraphRunbook type

.PARAMETER GraphicalAuthoringSdkDirectory
Microsoft Azure Automation Graphical Authoring SDK installation directory

.PARAMETER RunbookFileName
Runbook file name (.graphrunbook)

.PARAMETER RunbookName
Runbook name

.PARAMETER Slot
Specifies whether this cmdlet converts the draft or published content of the runbook. Valid values are:
        -- Published
        -- Draft

.PARAMETER ResourceGroupName
Azure Resource Group name

.PARAMETER AutomationAccountName
Azure Automation Account name

.EXAMPLE
Convert-GraphRunbookToPowerShellData -RunbookFileName ./MyRunbook.graphrunbook
Convert a graphical runbook from .graphrunbook file to PowerShell data.

.EXAMPLE
Convert-GraphRunbookToPowerShellData -RunbookFileName ./MyRunbook.graphrunbook | Out-File ./MyRunbook.psd1
Save a graphical runbook converted to PowerShell data as a .psd1 file.

.EXAMPLE
Convert-GraphRunbookToPowerShellData -RunbookName MyRunbook -ResourceGroupName myresourcegroup -AutomationAccountName myautomationaccount
Convert a graphical runbook from an Azure Automation account to PowerShell data.

.EXAMPLE
Convert-GraphRunbookToPowerShellData -RunbookFileName ./MyRunbook.graphrunbook -GraphicalAuthoringSdkDirectory 'C:\Program Files (x86)\Microsoft Azure Automation Graphical Authoring SDK'
Specify the Microsoft Azure Automation Graphical Authoring SDK installation directory.

.EXAMPLE
Get-AzureRmAutomationRunbook -ResourceGroupName myresourcegroup -AutomationAccountName myautomationaccount -PipelineVariable Runbook | ?{ ($_.RunbookType -match '^Graph') -and ($_.State -eq 'Published') } | Convert-GraphRunbookToPowerShellData -Verbose | %{ $_ | Out-File "$HOME\Desktop\AllRunbooks\$($Runbook.Name).psd1" }
Retrieve all published graphical runbooks from a specified Azure Automation account, convert them to PowerShell data, and save the results to .psd1 files.

.LINK
Source code: https://github.com/azureautomation/graphical-runbook-tools

.LINK
Azure Automation: https://azure.microsoft.com/services/automation

.LINK
Microsoft Azure Automation Graphical Authoring SDK: https://www.microsoft.com/en-us/download/details.aspx?id=50734
#>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'ByGraphRunbook')]
        # Should be [Orchestrator.GraphRunbook.Model.GraphRunbook], but declaring this type here would require
        # the Model assembly to be pre-loaded even before accessing module metadata
        $Runbook,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'ByRunbookFileName')]
        [string]
        $RunbookFileName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'ByRunbookName')]
        [Alias('Name')]
        [string]
        $RunbookName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'ByRunbookName')]
        [string]
        $ResourceGroupName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'ByRunbookName')]
        [string]
        $AutomationAccountName,

        [Parameter(
            ParameterSetName = 'ByRunbookName')]
        [ValidateSet('Published', 'Draft')]
        [string]
        $Slot = 'Published',

        [string]
        $GraphicalAuthoringSdkDirectory
    )

    begin {
        Add-GraphRunbookModelAssembly $GraphicalAuthoringSdkDirectory
    }

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'ByGraphRunbook' {
                Convert-GraphRunbookObjectToPowerShellData $Runbook -ErrorAction Stop
            }

            'ByRunbookFileName' {
                Convert-GraphRunbookFileToPowerShellData $RunbookFileName -ErrorAction Stop
            }

            'ByRunbookName' {
                Convert-GraphRunbookInAzureToPowerShellData `
                    -RunbookName $RunbookName `
                    -Slot $Slot `
                    -ResourceGroupName $ResourceGroupName `
                    -AutomationAccountName $AutomationAccountName `
                    -ErrorAction Stop
            }
        }
    }
}

#endregion

#region Get-GraphRunbookDependency

Add-Type -Language 'CSharp' -TypeDefinition @"
    namespace GraphRunbook
    {
        public class Dependency
        {
            public Dependency(string type, string name)
            {
                this.Type = type;
                this.Name = name;
            }

            public string Type { get; set; }
            public string Name { get; set; }
        }
    }
"@


function Get-RequiredModules([Orchestrator.GraphRunbook.Model.GraphRunbook]$Runbook) {
    $Runbook.Activities | ForEach-Object CommandType | ForEach-Object ModuleName | Where-Object { $_ } | Sort-Object -Unique |
        ForEach-Object { New-Object GraphRunbook.Dependency -ArgumentList 'Module', $_ }
}

function Get-ValueDescriptor([Orchestrator.GraphRunbook.Model.GraphRunbook]$Runbook) {
    $Parameters = $Runbook.Activities | ForEach-Object Parameters
    $Parameters | Where-Object { $_ } | ForEach-Object { foreach ($Entry in $_.GetEnumerator()) { $Entry.Value } }
}

function Get-AutomationAssets(
    [Orchestrator.GraphRunbook.Model.GraphRunbook]$Runbook,
    [string]$ValueDescriptorPropertyName,
    [string[]]$AssetAccessCommandNames,
    [string]$DependencyType) {

    $NamesFromValueDescriptors = Get-ValueDescriptor $Runbook | ForEach-Object -MemberName $ValueDescriptorPropertyName

    $NamesFromAssetAccessCommands += $Runbook.Activities |
        Where-Object { $AssetAccessCommandNames -icontains $_.CommandType.CommandName } |
        ForEach-Object { $_.Parameters['Name'] } |
        Where-Object { $_ -is [Orchestrator.GraphRunbook.Model.ConstantValueDescriptor] } |
        ForEach-Object { $_.Value }

    $AllNames = $NamesFromValueDescriptors + $NamesFromAssetAccessCommands

    $AllNames | Where-Object { $_ } | Sort-Object -Unique |
        ForEach-Object { New-Object GraphRunbook.Dependency -ArgumentList $DependencyType, $_ }
}

function Get-RequiredAutomationAssets([Orchestrator.GraphRunbook.Model.GraphRunbook]$Runbook) {
    Get-AutomationAssets -Runbook $Runbook `
        -ValueDescriptorPropertyName CertificateName `
        -AssetAccessCommandNames ('Get-AutomationCertificate', 'Get-AzureAutomationCertificate', 'Get-AzureRmAutomationCertificate') `
        -DependencyType AutomationCertificate

    Get-AutomationAssets -Runbook $Runbook `
        -ValueDescriptorPropertyName ConnectionName `
        -AssetAccessCommandNames ('Get-AutomationConnection', 'Get-AzureAutomationConnection', 'Get-AzureRmAutomationConnection') `
        -DependencyType AutomationConnection

    Get-AutomationAssets -Runbook $Runbook `
        -ValueDescriptorPropertyName CredentialName `
        -AssetAccessCommandNames ('Get-AutomationPSCredential', 'Get-AzureAutomationCredential', 'Get-AzureRmAutomationCredential') `
        -DependencyType AutomationCredential

    Get-AutomationAssets -Runbook $Runbook `
        -ValueDescriptorPropertyName VariableName `
        -AssetAccessCommandNames (
            'Get-AutomationVariable', 'Set-AutomationVariable',
            'Get-AzureAutomationVariable', 'Get-AzureRmAutomationVariable',
            'Set-AzureAutomationVariable', 'Set-AzureRmAutomationVariable') `
        -DependencyType AutomationVariable
}

function Get-RequiredRunbooks([Orchestrator.GraphRunbook.Model.GraphRunbook]$Runbook) {
    $NamesFromInvokeRunbookActivity = $Runbook.Activities | ForEach-Object RunbookActivityType | ForEach-Object CommandName

    $NamesFromCommandActivity = $Runbook.Activities |
        Where-Object { @('Start-AzureAutomationRunbook', 'Start-AzureRmAutomationRunbook') -icontains $_.CommandType.CommandName } |
        ForEach-Object { $_.Parameters['Name'] } |
        Where-Object { $_ -is [Orchestrator.GraphRunbook.Model.ConstantValueDescriptor] } |
        ForEach-Object { $_.Value }

    $AllNames = $NamesFromInvokeRunbookActivity + $NamesFromCommandActivity

    $AllNames | Where-Object { $_ } | Sort-Object -Unique |
        ForEach-Object { New-Object GraphRunbook.Dependency -ArgumentList 'Runbook', $_ }
}

function Get-GraphRunbookDependencyByGraphRunbook(
    [Orchestrator.GraphRunbook.Model.GraphRunbook]$Runbook,
    [string]$DependencyType) {

    if ($DependencyType -ieq 'Module') {
        Get-RequiredModules -Runbook $Runbook
    }
    elseif ($DependencyType -ieq 'AutomationAsset') {
        Get-RequiredAutomationAssets -Runbook $Runbook
    }
    elseif ($DependencyType -ieq 'Runbook') {
        Get-RequiredRunbooks -Runbook $Runbook
    }
    elseif ($DependencyType -ieq 'All') {
        Get-RequiredModules -Runbook $Runbook
        Get-RequiredAutomationAssets -Runbook $Runbook
        Get-RequiredRunbooks -Runbook $Runbook
    }
}

function Get-GraphRunbookDependencyByRunbookFileName(
    [string]$RunbookFileName,
    [string]$DependencyType) {

    Write-Verbose "Inspecting runbook file $RunbookFileName"
    $Runbook = Get-GraphRunbookFromFile -FileName $RunbookFileName
    Get-GraphRunbookDependencyByGraphRunbook -Runbook $Runbook -DependencyType $DependencyType
}

function Get-GraphRunbookDependencyByRunbookName($RunbookName, $Slot, $ResourceGroupName, $AutomationAccountName, $DependencyType) {
    WithExportedRunbook -RunbookName $RunbookName -Slot $Slot -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Action {
        param($FullFileName)
        Get-GraphRunbookDependencyByRunbookFileName -RunbookFileName $FullFileName -DependencyType $DependencyType
    }
}

function Get-GraphRunbookDependency {
<#
.SYNOPSIS
Outputs graphical runbook dependencies

.DESCRIPTION
Inspects a graphical runbook and outputs the runbook dependencies: required modules, accessed Automation Assets (Certificates, Connections, Credentials, and Variables), and invoked runbooks.

This command discovers dependencies explicitly specified in the graphical runbook, but it may not accurately determine dependencies of any PowerShell code (such as Code activity body, PowerShell expressions in activity parameters, or Link and Retry conditions).

Prerequisites
=============

1. Install Microsoft Azure Automation Graphical Authoring SDK (https://www.microsoft.com/en-us/download/details.aspx?id=50734).

2. Before invoking Get-GraphRunbookDependency with RunbookName, ResourceGroupName, and AutomationAccountName parameters, make sure you add an authenticated Azure account (for example, use Add-AzureRmAcccount cmdlet).

.PARAMETER Runbook
An instance of Orchestrator.GraphRunbook.Model.GraphRunbook type

.PARAMETER GraphicalAuthoringSdkDirectory
Microsoft Azure Automation Graphical Authoring SDK installation directory

.PARAMETER RunbookFileName
Runbook file name (.graphrunbook)

.PARAMETER RunbookName
Runbook name

.PARAMETER Slot
Specifies whether this cmdlet converts the draft or published content of the runbook. Valid values are:
        -- Published
        -- Draft

.PARAMETER ResourceGroupName
Azure Resource Group name

.PARAMETER AutomationAccountName
Azure Automation Account name

.PARAMETER DependencyType
Dependency type: Module, AutomationAsset, Runbook, or All (default)

.EXAMPLE
Get-GraphRunbookDependency -RunbookFileName ./MyRunbook.graphrunbook -DependencyType Module
Output modules that the specified graphical runbook depends on.

.EXAMPLE
Get-GraphRunbookDependency -RunbookName MyRunbook -ResourceGroupName myresourcegroup -AutomationAccountName myautomationaccount
Output all dependencies of a graphical runbook from an Azure Automation account.

.EXAMPLE
Get-GraphRunbookDependency -RunbookFileName ./MyRunbook.graphrunbook -GraphicalAuthoringSdkDirectory 'C:\Program Files (x86)\Microsoft Azure Automation Graphical Authoring SDK'
Specify the Microsoft Azure Automation Graphical Authoring SDK installation directory.

.LINK
Source code: https://github.com/azureautomation/graphical-runbook-tools

.LINK
Azure Automation: https://azure.microsoft.com/services/automation

.LINK
Microsoft Azure Automation Graphical Authoring SDK: https://www.microsoft.com/en-us/download/details.aspx?id=50734
#>

    [CmdletBinding()]
    [OutputType([GraphRunbook.Dependency])]
    param(
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'ByGraphRunbook')]
        # Should be [Orchestrator.GraphRunbook.Model.GraphRunbook], but declaring this type here would require
        # the Model assembly to be pre-loaded even before accessing module metadata
        $Runbook,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'ByRunbookFileName')]
        [string]
        $RunbookFileName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'ByRunbookName')]
        [Alias('Name')]
        [string]
        $RunbookName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'ByRunbookName')]
        [string]
        $ResourceGroupName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'ByRunbookName')]
        [string]
        $AutomationAccountName,

        [Parameter(
            ParameterSetName = 'ByRunbookName')]
        [ValidateSet('Published', 'Draft')]
        [string]
        $Slot = 'Published',

        [ValidateSet('Module', 'AutomationAsset', 'Runbook', 'All')]
        [string]
        $DependencyType = 'All',

        [string]
        $GraphicalAuthoringSdkDirectory
    )
    
    begin {
        Add-GraphRunbookModelAssembly $GraphicalAuthoringSdkDirectory
    }

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'ByGraphRunbook' {
                Get-GraphRunbookDependencyByGraphRunbook -Runbook $Runbook -DependencyType $DependencyType -ErrorAction Stop
            }

            'ByRunbookFileName' {
                Get-GraphRunbookDependencyByRunbookFileName -RunbookFileName $RunbookFileName -DependencyType $DependencyType -ErrorAction Stop
            }

            'ByRunbookName' {
                Get-GraphRunbookDependencyByRunbookName `
                    -RunbookName $RunbookName `
                    -Slot $Slot `
                    -ResourceGroupName $ResourceGroupName `
                    -AutomationAccountName $AutomationAccountName `
                    -DependencyType $DependencyType `
                    -ErrorAction Stop
            }
        }
    }
}

#endregion

Export-ModuleMember -Function Show-GraphRunbookActivityTraces
Export-ModuleMember -Function Convert-GraphRunbookToPowerShellData
Export-ModuleMember -Function Get-GraphRunbookDependency