AzureResourceStuff.psm1

function Copy-AzureAutomationRuntime {
    <#
    .SYNOPSIS
    Make a copy of existing Azure Automation Runtime Environment.
 
    .DESCRIPTION
    Make a copy of existing Azure Automation Runtime Environment.
 
    Copy will have:
    - same default and custom modules
    - same language, version and description
 
    Copy is by default created in the same Automation Account, but can be placed in different one too.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER runtimeName
    Name of the runtime to copy.
 
    .PARAMETER newResourceGroupName
    Destination Resource group name.
    If not specified, source one will be used.
 
    .PARAMETER newAutomationAccountName
    Destination Automation account name.
 
    If not specified, source one will be used.
 
    .PARAMETER newRuntimeName
    Name of the new runtime.
 
    If not specified, it will be "copy_<sourceRuntimeName>".
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Copy-AzureAutomationRuntime
 
    Creates a copy of the selected Runtime in the same Automation Account. It will be named like "copy_<sourceRuntimeName>".
 
    Missing function arguments like $runtimeName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Copy-AzureAutomationRuntime -runtimeName "Runtime51" -newRuntimeName "Runtime51_v2" -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging"
 
    Creates a copy of the selected Runtime in the same Automation Account.
    #>


    [CmdletBinding()]
    param (
        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string] $runtimeName,

        [string] $newResourceGroupName,

        [string] $newAutomationAccountName,

        [string] $newRuntimeName,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    if (($newResourceGroupName -and !$newAutomationAccountName) -or (!$newResourceGroupName -and $newAutomationAccountName)) {
        throw "Either both 'newResourceGroupName' and 'newAutomationAccountName' parameters have to be set or neither of them"
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -programmingLanguage PowerShell -runtimeSource Custom -header $header | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select environment you want to process"
    }
    #endregion get missing arguments

    # get all custom modules
    Write-Verbose "Get Runtime '$runtimeName' custom modules"
    $customModule = Get-AzureAutomationRuntimeCustomModule -runtimeName $runtimeName -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -ErrorAction Stop

    # get all default modules
    Write-Verbose "Get Runtime '$runtimeName' default modules"
    $defaultPackageObj = Get-AzureAutomationRuntimeSelectedDefaultModule -runtimeName $runtimeName -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -ErrorAction Stop

    # get runtime language, version, description
    Write-Verbose "Get Runtime '$runtimeName' information"
    $runtime = Get-AzureAutomationRuntime -runtimeName $runtimeName -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -ErrorAction Stop

    $runtimeLanguage = $runtime.properties.runtime.language
    $runtimeVersion = $runtime.properties.runtime.version
    $runtimeDescription = $runtime.properties.description

    #region create new runtime with language, version and default modules
    if (!$newResourceGroupName) {
        $newResourceGroupName = $resourceGroupName
    }
    if (!$newAutomationAccountName) {
        $newAutomationAccountName = $automationAccountName
    }
    if (!$newRuntimeName) {
        $newRuntimeName = "copy_$runtimeName"
    }

    if ($defaultPackageObj) {
        # transform $defaultPackageObj to hashtable
        $defaultPackage = @{}

        $defaultPackageObj | % {
            $defaultPackage.($_.Name) = $_.Version
        }
    } else {
        # no default modules needed
        $defaultPackage = @{}
    }

    "Creating new runtime '$newRuntimeName'"
    $null = New-AzureAutomationRuntime -runtimeName $newRuntimeName -resourceGroupName $newResourceGroupName -automationAccountName $newAutomationAccountName -runtimeLanguage $runtimeLanguage -runtimeVersion $runtimeVersion -defaultPackage $defaultPackage -description $runtimeDescription -header $header
    #region create new runtime with language, version and default modules

    # add custom modules
    foreach ($custModule in $customModule) {
        $name = $custModule.name
        $version = $custModule.properties.version
        $provisioningState = $custModule.properties.provisioningState

        if ($provisioningState -ne 'Succeeded') {
            Write-Verbose "Skipping adding custom module '$name', because it is in '$provisioningState' provisioning state"
            continue
        }

        "Adding custom module '$name' $version"
        New-AzureAutomationRuntimeModule -runtimeName $newRuntimeName -resourceGroupName $newResourceGroupName -automationAccountName $newAutomationAccountName -moduleName $name -moduleVersion $version -header $header
    }
}

function Export-VariableToStorage {
    <#
    .SYNOPSIS
    Function for saving PowerShell variable as XML file in Azure Blob storage.
    That way you can easily later download & convert it back to original state using Import-VariableFromStorage.
 
    .DESCRIPTION
    Function for saving PowerShell variable as XML file in Azure Blob storage.
    That way you can easily later download & convert it back to original state using Import-VariableFromStorage.
 
    Uses native Export-CliXml to convert variable to a XML.
 
    .PARAMETER value
    Variable you want to save to blob storage.
 
    .PARAMETER fileName
    Name that will be used for uploaded file.
    To place file to the folder structure, give name like "folder\file".
    '.xml' will be appended automatically.
 
    .PARAMETER resourceGroupName
    Name of the Resource Group Name.
 
    By default 'PersistentRunbookVariables'
 
    .PARAMETER storageAccount
    Name of the Storage Account.
 
    It is case sensitive!
 
    By default 'persistentvariablesstore'.
 
    .PARAMETER containerName
    Name of the Storage Account Container.
 
    By default 'variables'.
 
    .PARAMETER standardBlobTier
    Tier type.
 
    By default 'Hot'.
 
    .PARAMETER showProgress
    Switch for showing upload progress.
    Can slow down the upload!
 
    .EXAMPLE
    Connect-AzAccount
 
    $processes = Get-Process
 
    Export-VariableToStorage -value $processes -fileName "processes"
 
    Converts $processes to XML (using Export-CliXml) and saves it to the default Storage Account and default container as a file "processes.xml".
 
    .EXAMPLE
    Connect-AzAccount
 
    $processes = Get-Process
 
    Export-VariableToStorage -value $processes -fileName "variables\processes"
 
    Converts $processes to XML (using Export-CliXml) and saves it to the default Storage Account and default container to folder "variables" as a file "processes.xml".
 
    .NOTES
    Required permissions: Role 'Storage Account Contributor' has to be granted to the used Storage account
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $value,

        [Parameter(Mandatory = $true)]
        [ValidateScript( {
                if ($_ -match "\.|/") {
                    throw "$_ is not a valid variable name. Don't use ., / chars."
                } else {
                    $true
                }
            })]
        $fileName,

        $resourceGroupName = "PersistentRunbookVariables",

        [ValidateScript( {
                if ($_ -cmatch '^[a-z0-9]+$') {
                    $true
                } else {
                    throw "$_ is not a valid storage account name (does not match expected pattern '^[a-z0-9]+$')."
                }
            })]
        $storageAccount = "persistentvariablesstore",

        $containerName = "variables",

        [ValidateSet('Hot', 'Cold')]
        [string] $standardBlobTier = "Hot",

        [switch] $showProgress
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    try {
        Write-Verbose "Set Storage Account"
        $null = Set-AzCurrentStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccount -ErrorAction Stop
    } catch {
        if ($_ -like "*does not have authorization to perform action 'Microsoft.Storage/storageAccounts/read' over scope*" -or $_ -like "*'this.Client.SubscriptionId' cannot be null*") {
            throw "Access denied. Role 'Storage Account Contributor' has to be granted to the '$storageAccount' Storage account"
        } else {
            throw $_
        }
    }

    # create temp file
    $cliXmlFile = New-TemporaryFile
    $value | Export-Clixml $CliXmlFile.FullName

    if (!$showProgress) {
        $ProgressPreference = "silentlycontinue"
    }

    # upload the file
    $param = @{
        File             = $cliXmlFile
        Container        = $containerName
        Blob             = "$fileName.xml"
        StandardBlobTier = $standardBlobTier
        Force            = $true
        ErrorAction      = "Stop"
    }
    Write-Verbose "Upload variable xml representation to the '$($fileName.xml)' file"
    $null = Set-AzStorageBlobContent @param

    # remove temp file
    Remove-Item $cliXmlFile -Force
}

function Get-AutomationVariable2 {
    <#
    .SYNOPSIS
    Function for getting Azure RunBook variable exported using Set-AutomationVariable2 function (a.k.a. using Export-CliXml).
 
    .DESCRIPTION
    Function for getting Azure RunBook variable exported using Set-AutomationVariable2 function (a.k.a. using Export-CliXml).
    Compared to original Get-AutomationVariable this one is able to get original PSObjects as they were and not as Newtonsoft.Json.Linq.
 
    As original Get-AutomationVariable can be used only inside RunBook!
 
    .PARAMETER name
    Name of the RunBook variable you want to retrieve.
 
    (such variable had to be set using Set-AutomationVariable2!)
 
    .EXAMPLE
    # save given hashtable to variable myVar
    #Set-AutomationVariable2 -name myVar -value @{name = 'John'; surname = 'Doe'}
 
    Get-AutomationVariable2 myVar
 
    Get variable myVar.
 
    .NOTES
    Same as original Get-AutomationVariable command, can be used only inside a Runbook!
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "Authentication needed. Please call 'Connect-AzAccount -Identity'."
    }

    try {
        [string] $xml = Get-AutomationVariable -Name $name -ErrorAction Stop
    } catch {
        Write-Error $_
        return
    }

    if ($xml) {
        # in-memory import of CliXml string (similar to Import-Clixml)
        [System.Management.Automation.PSSerializer]::Deserialize($xml)
    } else {
        return
    }
}

function Get-AzureAutomationRunbookContent {
    <#
    .SYNOPSIS
    Function gets Automation Runbook code content.
 
    .DESCRIPTION
    Function gets Automation Runbook code content.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER runbookName
    Runbook name.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Get-AzureAutomationRunbookContent -runbookName someRunbook -ResourceGroupName Automations -AutomationAccountName someAutomationAccount
 
    Gets code set in the specified runbook.
    #>


    [CmdletBinding()]
    param (
        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string] $runbookName,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    $subscriptionId = (Get-AzContext).Subscription.Id

    # create auth token
    $accessToken = Get-AzAccessToken -ResourceTypeName "Arm"
    if ($accessToken.Token) {
        $header = @{
            'Content-Type'  = 'application/json'
            'Authorization' = "Bearer {0}" -f $accessToken.Token
        }
    }

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runbookName) {
        $runbookName = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runbook you want to process"
    }
    #endregion get missing arguments

    Write-Verbose "Getting runbook code content"

    try {
        Invoke-RestMethod "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName/content?api-version=2015-10-31" -Method GET -Headers $header
    } catch {
        if ($_.Exception.StatusCode -eq 'NotFound') {
            Write-Verbose "There is no code set in the runbook"
        } else {
            throw $_
        }
    }
}

function Get-AzureAutomationRunbookRuntime {
    <#
    .SYNOPSIS
    Get Runtime Environment name of the selected Azure Automation Account Runbook.
 
    .DESCRIPTION
    Get Runtime Environment name of the selected Azure Automation Account Runbook.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER runbookName
    Runbook name.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRunbookRuntime
 
    Get name of the Runtime Environment used in selected Runbook.
    Missing function arguments like $resourceGroupName, $automationAccountName or $runbookName will be interactively gathered through Out-GridView GUI.
    #>


    [CmdletBinding()]
    param (
        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string] $runbookName,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runbookName) {
        $runbookName = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runbook you want to process"
    }
    #endregion get missing arguments

    Invoke-RestMethod2 "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName`?api-version=2023-05-15-preview" -headers $header | select -ExpandProperty properties | select -ExpandProperty runtimeEnvironment
}

function Get-AzureAutomationRunbookTestJobOutput {
    <#
    .SYNOPSIS
    Get output from last Runbook test run.
 
    .DESCRIPTION
    Get output from last Runbook test run.
 
    .PARAMETER runbookName
    Runbook name.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER justText
    Instead of object return just outputted messages of selected type(s).
 
    Possible values: 'Output', 'Warning', 'Error', 'Exception'
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRunbookTestJobOutput
 
    Get output of selected Runbook last test run. Output will be returned via array of objects where beside returned text also other properties like type of the output or output time are returned.
 
    Missing function arguments like $runbookName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRunbookTestJobOutput -justText Output
 
    Get just common (no warnings or errors) output of selected Runbook last test run. Output will be returned as array of strings.
 
    Missing function arguments like $runbookName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
    #>


    [CmdletBinding()]
    param (
        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string] $runbookName,

        [ValidateSet('Output', 'Warning', 'Error', 'Exception')]
        [string[]] $justText,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runbookName) {
        $runbookName = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runbook you want to change"
    }
    #endregion get missing arguments

    # get ordinary output, warnings, errors
    $result = Invoke-RestMethod2 -method get -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName/draft/testJob/streams?&api-version=2019-06-01" -headers $header -ErrorAction Stop

    # how the returned output looks like can vary
    if ('Value' -in ($result | Get-Member -MemberType NoteProperty | select -ExpandProperty Name)) {
        $result = $result.value.properties
    } else {
        $result = $result.properties
    }

    # fix for empty summary problem
    # sometimes it happens that primary api call returns empty summary property
    # and direct api calls agains job stream id has to be made to get the actual data
    foreach ($item in $result) {
        $output = $item.summary
        $jobStreamId = $item.jobStreamId

        if (!$output) {
            Write-Verbose "Getting missing output of the job stream $jobStreamId"

            $jobStreamResult = Invoke-RestMethod2 -method get -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName/draft/testJob/streams/$jobStreamId`?&api-version=2019-06-01" -headers $header

            if ($jobStreamResult.properties.streamText) {
                Write-Verbose "Found it"
                $item.summary = $jobStreamResult.properties.streamText
            }
        }
    }

    # get exceptions
    $testJobStatus = Get-AzureAutomationRunbookTestJobStatus -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -runbookName $runbookName -header $header

    if ($justText) {
        # output specified type of messages (ordinary output, warnings and errors)
        $result | ? streamType -In $justText | select -ExpandProperty Summary

        # output exception message if requested
        if ($justText -contains 'Exception' -and $testJobStatus.exception) {
            $testJobStatus.exception
        }
    } else {
        # output ordinary output, warnings and errors
        $result

        # output exception message
        if ($testJobStatus.exception) {
            [PSCustomObject]@{
                jobStreamId = $null
                summary     = $testJobStatus.exception
                time        = $testJobStatus.endTime
                streamType  = 'Exception'
            }
        }
    }
}

function Get-AzureAutomationRunbookTestJobStatus {
    <#
    .SYNOPSIS
    Get status of the last Runbook test run.
 
    .DESCRIPTION
    Get status of the last Runbook test run.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER runbookName
    Runbook name.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRunbookTestJobStatus
 
    Get status of selected Runbook last test run.
 
    Missing function arguments like $runbookName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
    #>


    [CmdletBinding()]
    param (
        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string] $runbookName,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runbookName) {
        $runbookName = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runbook you want to change"
    }
    #endregion get missing arguments

    Invoke-RestMethod2 -method get -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName/draft/testJob?api-version=2019-06-01" -headers $header
}

function Get-AzureAutomationRuntime {
    <#
    .SYNOPSIS
    Function returns selected/all Azure Automation runtime environment/s.
 
    .DESCRIPTION
    Function returns selected/all Azure Automation runtime environment/s.
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to retrieve.
 
    If not provided, all runtimes will be returned.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER programmingLanguage
    Filter runtimes to just ones using selected language.
 
    Possible values: All, PowerShell, Python.
 
    By default: All
 
    .PARAMETER runtimeSource
    Filter runtimes by source of creation.
 
    Possible values: All, Default, Custom.
 
    By default: All
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRuntime -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging"
 
    Get all Automation Runtimes in given Automation Account.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRuntime -programmingLanguage PowerShell -runtimeSource Custom
 
    Get just PowerShell based manually created Automation Runtimes in given Automation Account.
 
    Missing function arguments like $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .NOTES
    https://learn.microsoft.com/en-us/rest/api/automation/runtime-environments/get?view=rest-automation-2023-05-15-preview&tabs=HTTP
    #>


    [CmdletBinding()]
    param (
        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [ValidateSet('PowerShell', 'Python', 'All')]
        [string] $programmingLanguage = 'All',

        [ValidateSet('Default', 'Custom', 'All')]
        [string] $runtimeSource = 'All',

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }
    #endregion get missing arguments

    $result = Invoke-RestMethod2 -method Get -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtimeName/?api-version=2023-05-15-preview" -headers $header -ErrorAction $ErrorActionPreference

    #region filter results
    if ($result -and $programmingLanguage -ne 'All') {
        $result = $result | ? { $_.Properties.Runtime.language -eq $programmingLanguage }
    }

    if ($result -and $runtimeSource -ne 'All') {
        switch ($runtimeSource) {
            'Default' {
                $result = $result | ? { $_.Properties.Description -like "System-generated Runtime Environment for your Automation account with Runtime language:*" }
            }

            'Custom' {
                $result = $result | ? { $_.Properties.Description -notlike "System-generated Runtime Environment for your Automation account with Runtime language:*" }
            }

            default {
                throw "Undefined runtimeSource ($runtimeSource)"
            }
        }
    }
    #endregion filter results

    $result
}

function Get-AzureAutomationRuntimeAvailableDefaultModule {
    <#
    .SYNOPSIS
    Function returns default modules (Az) available to select in selected/all PSH runtime(s).
 
    .DESCRIPTION
    Function returns default modules (Az) available to select in selected/all PSH runtime(s).
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER runtimeName
    (optional) runtime name you want to get default modules for.
 
    If not provided, all default modules for all PSH runtimes in given automation account will be outputted.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRuntimeAvailableDefaultModule
 
    You will get list of all resource groups and automation accounts (in current subscription) to pick the one you are interested in.
    And the output will be all default modules (Az) that are available to select there.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRuntimeAvailableDefaultModule -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging" -runtimeName "PSH51_Custom"
 
    And the output will be default modules (Az) that are available to select in given Runtime Environment.
    #>


    [CmdletBinding()]
    param (
        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string] $runtimeName,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }
    #endregion get missing arguments

    if ($runtimeName) {
        # get available default modules for this specific runtime
        # for this we need to get used PowerShell version
        $runtime = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -runtimeName $runtimeName -programmingLanguage PowerShell -ErrorAction Stop

        if (!$runtime) {
            throw "Runtime Environment wasn't found. Name is misspelled or it is not a PSH runtime"
        }

        $runtimeVersion = $runtime.properties.runtime.version

        if ($runtimeVersion -eq '5.1') {
            $runtimeLanguageVersion = 'powershell'
        } elseif ($runtimeVersion -eq '7.1') {
            $runtimeLanguageVersion = 'powershell7'
        } else {
            # hopefully MS will stick with this format
            $runtimeLanguageVersion = ('powershell' + ($runtimeVersion -replace '\.'))
        }

        Write-Verbose "Available default modules will be limited to $runtimeLanguageVersion runtime language"
    } else {
        $runtimeLanguageVersion = '*'
    }

    $result = Invoke-RestMethod2 -method Post -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/listbuiltinmodules?api-version=2023-05-15-preview" -headers $header

    if ($result) {
        # instead of one object containing all runtimes return one object per runtime
        $result | Get-Member -MemberType NoteProperty | select -ExpandProperty Name | ? { $_ -like $runtimeLanguageVersion } | % {
            $runtimeLanguage = $_
            $result.$runtimeLanguage | select @{n = 'RuntimeLanguage'; e = { $runtimeLanguage } }, *
        }
    }
}

function Get-AzureAutomationRuntimeCustomModule {
    <#
    .SYNOPSIS
    Function gets all (or just selected) custom modules (packages) that are imported in the specified PowerShell Azure Automation runtime.
 
    .DESCRIPTION
    Function gets all (or just selected) custom modules (packages) that are imported in the specified PowerShell Azure Automation runtime.
 
    Custom modules are added by user, default ones are built-in (Az) and user just select version to use.
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to retrieve.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER moduleName
    Name of the custom module you want to get.
 
    If not provided, all custom modules will be returned.
 
    .PARAMETER simplified
    Switch to return only name and version of successfully imported modules.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRuntimeCustomModule
 
    You will get list of all (in current subscription) resource groups, automation accounts and runtimes to pick the one you are interested in.
    And the output will be all custom modules imported in the specified Automation runtime.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRuntimeCustomModule -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging" -runtimeName Custom_PSH_51 -moduleName CommonStuff
 
    Get custom module CommonStuff imported in the specified Automation runtime.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRuntimeCustomModule -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging" -runtimeName Custom_PSH_51
 
    Get all custom modules imported in the specified Automation runtime.
    #>


    [CmdletBinding()]
    param (
        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string] $moduleName,

        [switch] $simplified,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -programmingLanguage PowerShell | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runtime you want to process"
    }
    #endregion get missing arguments

    $result = Invoke-RestMethod2 -method Get -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtimeName/packages/$moduleName`?api-version=2023-05-15-preview" -headers $header

    if ($simplified) {
        $result | ? { $_.properties.provisioningState -eq 'Succeeded' } | select @{n = 'Name'; e = { $_.Name } }, @{n = 'Version'; e = { $version = $_.properties.version; if ($version -eq 'Unknown') { $null } else { $version } } }
    } else {
        $result
    }
}

function Get-AzureAutomationRuntimeSelectedDefaultModule {
    <#
    .SYNOPSIS
    Function get default module (Az) that is selected in the specified Azure Automation runtime.
 
    .DESCRIPTION
    Function get default module (Az) that is selected in the specified Azure Automation runtime.
 
    Custom modules are added by user, default ones are built-in (Az) and user just select version to use.
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to retrieve.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRuntimeSelectedDefaultModule
 
    You will get list of all (in current subscription) resource groups, automation accounts and runtimes to pick the one you are interested in.
    And you will get default module name (AZ) and its version that is selected in the specified Automation runtime.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Get-AzureAutomationRuntimeSelectedDefaultModule -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging" -runtimeName Custom_PSH_51
 
    Get default module (Az) version in the specified Automation runtime.
    #>


    [CmdletBinding()]
    [Alias("Get-AzureAutomationRuntimeAzModule")]
    param (
        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -programmingLanguage PowerShell | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select environment you want to process"
    }
    #endregion get missing arguments

    Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -runtimeName $runtimeName -header $header | select -ExpandProperty properties | select -ExpandProperty defaultPackages | % {
        $module = $_
        $moduleName = $_ | Get-Member -MemberType NoteProperty | select -ExpandProperty Name
        $moduleVersion = $module.$moduleName

        [PSCustomObject]@{
            Name    = $moduleName
            Version = $moduleVersion
        }
    }
}

function Get-AzureResource {
    <#
    .SYNOPSIS
    Returns resources for all or just selected Azure subscription(s).
 
    .DESCRIPTION
    Returns resources for all or just selected Azure subscription(s).
 
    .PARAMETER subscriptionId
    ID of subscription you want to get resources for.
 
    .PARAMETER selectCurrentSubscription
    Switch for getting data just for currently set subscription.
 
    .EXAMPLE
    Get-AzureResource
 
    Returns resources for all subscriptions.
 
    .EXAMPLE
    Get-AzureResource -subscriptionId 1234-1234-1234-1234
 
    Returns resources for subscription with ID 1234-1234-1234-1234.
 
    .EXAMPLE
    Get-AzureResource -selectCurrentSubscription
 
    Returns resources just for current subscription.
    #>


    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(ParameterSetName = "subscriptionId")]
        [string] $subscriptionId,

        [Parameter(ParameterSetName = "currentSubscription")]
        [switch] $selectCurrentSubscription
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    # get Current Context
    $currentContext = Get-AzContext

    # get Azure Subscriptions
    if ($selectCurrentSubscription) {
        Write-Verbose "Only running for current subscription $($currentContext.Subscription.Name)"
        $subscriptions = Get-AzSubscription -SubscriptionId $currentContext.Subscription.Id -TenantId $currentContext.Tenant.Id
    } elseif ($subscriptionId) {
        Write-Verbose "Only running for selected subscription $subscriptionId"
        $subscriptions = Get-AzSubscription -SubscriptionId $subscriptionId -TenantId $currentContext.Tenant.Id
    } else {
        Write-Verbose "Running for all subscriptions in tenant"
        $subscriptions = Get-AzSubscription -TenantId $currentContext.Tenant.Id
    }

    Write-Verbose "Getting information about Role Definitions..."
    $allRoleDefinition = Get-AzRoleDefinition

    foreach ($subscription in $subscriptions) {
        Write-Verbose "Changing to Subscription $($subscription.Name)"

        $Context = Set-AzContext -TenantId $subscription.TenantId -SubscriptionId $subscription.Id -Force

        # getting information about Role Assignments for chosen subscription
        Write-Verbose "Getting information about Role Assignments..."
        $allRoleAssignment = Get-AzRoleAssignment

        Write-Verbose "Getting information about Resources..."

        Get-AzResource | % {
            $resourceId = $_.ResourceId
            Write-Verbose "Processing $resourceId"

            $roleAssignment = $allRoleAssignment | ? { $resourceId -match [regex]::escape($_.scope) -or $_.scope -like "/providers/Microsoft.Authorization/roleAssignments/*" -or $_.scope -like "/providers/Microsoft.Management/managementGroups/*" } | select RoleDefinitionName, DisplayName, Scope, SignInName, ObjectType, ObjectId, @{n = 'CustomRole'; e = { ($allRoleDefinition | ? Name -EQ $_.RoleDefinitionName).IsCustom } }, @{n = 'Inherited'; e = { if ($_.scope -eq $resourceId) { $false } else { $true } } }

            $_ | select *, @{n = "SubscriptionName"; e = { $subscription.Name } }, @{n = "SubscriptionId"; e = { $subscription.SubscriptionId } }, @{n = 'IAM'; e = { $roleAssignment } } -ExcludeProperty SubscriptionId, ResourceId, ResourceType
        }
    }
}

function Import-VariableFromStorage {
    <#
    .SYNOPSIS
    Function for downloading Azure Blob storage XML file and converting it back to original PowerShell object.
 
    .DESCRIPTION
    Function for downloading Azure Blob storage XML file and converting it back to original PowerShell object.
 
    Uses native Import-CliXml command for converting XML back to an object hence expects that such PowerShell object was previously saved using Export-VariableToStorage.
 
    .PARAMETER fileName
    Name of the file you want to download and convert back to the original variable.
    '.xml' will be appended automatically.
 
    .PARAMETER resourceGroupName
    Name of the Resource Group Name.
 
    By default 'PersistentRunbookVariables'
 
    .PARAMETER storageAccount
    Name of the Storage Account.
 
    It is case sensitive!
 
    By default 'persistentvariablesstore'.
 
    .PARAMETER containerName
    Name of the Storage Account Container.
 
    By default 'variables'.
 
    .PARAMETER showProgress
    Switch for showing upload progress.
    Can slow down the upload!
 
    .EXAMPLE
    Connect-AzAccount
 
    $processes = Import-VariableFromStorage -fileName "processes"
 
    .NOTES
    Required permissions: Role 'Storage Account Contributor' has to be granted to the used Storage account
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript( {
                if ($_ -match "\.|\\|/") {
                    throw "$_ is not a valid variable name. Don't use ., \, / chars."
                } else {
                    $true
                }
            })]
        $fileName,

        $resourceGroupName = "PersistentRunbookVariables",


        [ValidateScript( {
                if ($_ -cmatch '^[a-z0-9]+$') {
                    $true
                } else {
                    throw "$_ is not a valid storage account name (does not match expected pattern '^[a-z0-9]+$')."
                }
            })]
        $storageAccount = "persistentvariablesstore",

        $containerName = "variables",

        [switch] $showProgress
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    if (!$showProgress) {
        $ProgressPreference = "silentlycontinue"
    }

    try {
        Write-Verbose "Set Storage Account"
        $null = Set-AzCurrentStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccount -ErrorAction Stop
    } catch {
        if ($_ -like "*does not have authorization to perform action 'Microsoft.Storage/storageAccounts/read' over scope*" -or $_ -like "*'this.Client.SubscriptionId' cannot be null*") {
            throw "Access denied. Role 'Storage Account Contributor' has to be granted to the '$storageAccount' Storage account"
        } else {
            throw $_
        }
    }

    # create temp file
    $cliXmlFile = New-TemporaryFile

    # download blob
    $param = @{
        Blob        = "$fileName.xml"
        Container   = $containerName
        Destination = $cliXmlFile
        Force       = $true
        ErrorAction = "Stop"
    }
    try {
        $null = Get-AzStorageBlobContent @param
    } catch {
        if ($_ -like "*Can not find blob*" ) {
            # probably file is just not yet created (Export-VariableToStorage wasn't run yet)
            Write-Warning $_

            # remove temp file
            $null = Remove-Item $cliXmlFile -Force

            return
        } else {
            throw $_
        }
    }

    # convert xml back to original object
    $cliXmlFile | Import-Clixml

    # remove temp file
    $null = Remove-Item $cliXmlFile -Force
}

function New-AzureAutomationGraphToken {
    <#
    .SYNOPSIS
    Generating auth header for Azure Automation.
 
    .DESCRIPTION
    Generating auth header for Azure Automation.
 
    Expects that you are already connected to Azure using Connect-AzAccount command.
 
    .EXAMPLE
    Connect-AzAccount
 
    $header = New-AzureAutomationGraphToken
 
    $body = @{
        "properties" = @{
            "contentLink" = @{
                "uri" = $modulePkgUri
            }
            "version" = $moduleVersion
        }
    }
 
    $body = $body | ConvertTo-Json
 
    Invoke-RestMethod2 -method Put -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtimeEnvironmentName/packages/$moduleName`?api-version=2023-05-15-preview" -body $body -headers $header
 
    #>


    $accessToken = Get-AzAccessToken -ResourceUrl "https://management.azure.com" -ErrorAction Stop
    if ($accessToken.Token) {
        $header = @{
            'Content-Type'  = 'application/json'
            'Authorization' = "Bearer {0}" -f $accessToken.Token
        }

        return $header
    } else {
        throw "Unable to obtain token. Are you connected using Connect-AzAccount?"
    }
}

function New-AzureAutomationModule {
    <#
    .SYNOPSIS
    Function for importing new (or updating existing) Azure Automation PSH module.
 
    Any module dependencies will be automatically installed too.
 
    .DESCRIPTION
    Function for importing new (or updating existing) Azure Automation PSH module.
 
    Any module dependencies will be automatically installed too.
 
    By default newest supported version is imported (if 'moduleVersion' is not set). If module exists, but with different version, it will be replaced (including its dependencies).
 
    According the dependencies. If version that can be used exist, it is not updated to the newest possible one, but is used at it is. Reason for this is to avoid unnecessary updates that can lead to unstable/untested environment.
 
    Supported version means, version that support given runtime ('runtimeVersion' parameter).
 
    .PARAMETER moduleName
    Name of the PSH module.
 
    .PARAMETER moduleVersion
    (optional) version of the PSH module.
    If not specified, newest supported version for given runtime will be gathered from PSGallery.
 
    .PARAMETER moduleVersionType
    Type of the specified module version.
 
    Possible values are: 'RequiredVersion', 'MinimumVersion', 'MaximumVersion'.
 
    By default 'RequiredVersion'.
 
    .PARAMETER resourceGroupName
    Name of the Azure Resource Group.
 
    .PARAMETER automationAccountName
    Name of the Azure Automation Account.
 
    .PARAMETER runtimeVersion
    PSH runtime version.
 
    Possible values: 5.1, 7.2.
 
    By default 5.1.
 
    .PARAMETER overridePSGalleryModuleVersion
    Hashtable of hashtables where you can specify what module version should be used for given runtime if no specific version is required.
 
    This is needed in cases, where module newest available PSGallery version isn't compatible with your runtime because of incorrect manifest.
 
    By default:
 
    $overridePSGalleryModuleVersion = @{
        # 2.x.x PnP.PowerShell versions (2.1.1, 2.2.0) requires PSH 7.2 even though manifest doesn't say it
        # so the wrong module version would be picked up which would cause an error when trying to import
        "PnP.PowerShell" = @{
            "5.1" = "1.12.0"
        }
    }
 
    .EXAMPLE
    Connect-AzAccount -Tenant "contoso.onmicrosoft.com" -SubscriptionName "AutomationSubscription"
 
    New-AzureAutomationModule -resourceGroupName test -automationAccountName test -moduleName "Microsoft.Graph.Groups"
 
    Imports newest supported version (for given runtime) of the "Microsoft.Graph.Groups" module including all its dependencies.
    In case module "Microsoft.Graph.Groups" with such version is already imported, nothing will happens.
    Otherwise module will be imported/replaced (including all dependencies that are required for this specific version).
 
    .EXAMPLE
    Connect-AzAccount -Tenant "contoso.onmicrosoft.com" -SubscriptionName "AutomationSubscription"
 
    New-AzureAutomationModule -resourceGroupName test -automationAccountName test -moduleName "Microsoft.Graph.Groups" -moduleVersion "2.11.1"
 
    Imports "2.11.1" version of the "Microsoft.Graph.Groups" module including all its dependencies.
    In case module "Microsoft.Graph.Groups" with version "2.11.1" is already imported, nothing will happens.
    Otherwise module will be imported/replaced (including all dependencies that are required for this specific version).
 
    .NOTES
    1. Because this function depends on Find-Module command heavily, it needs to have communication with the PSGallery enabled. To automate this, you can use following code:
 
    "Install a package manager"
    $null = Install-PackageProvider -Name nuget -Force -ForceBootstrap -Scope allusers
 
    "Set PSGallery as a trusted repository"
    Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
 
    'PackageManagement', 'PowerShellGet', 'PSReadline', 'PSScriptAnalyzer' | % {
        "Install module $_"
        Install-Module $_ -Repository PSGallery -Force -AllowClobber
    }
 
    "Uninstall old version of PowerShellGet"
    Get-Module PowerShellGet -ListAvailable | ? version -lt 2.0.0 | select -exp ModuleBase | % { Remove-Item -Path $_ -Recurse -Force }
 
    2. Modules saved in Azure Automation Account have only "main" version saved and suffixes like "beta", "rc" etc are always cut off!
    A.k.a. if you import module with version "1.0.0-rc4". Version that will be shown in the GUI will be just "1.0.0" hence if you try to import such module again, it won't be correctly detected hence will be imported once again.
    #>


    [CmdletBinding()]
    [Alias("New-AzAutomationModule2", "Set-AzureAutomationModule")]
    param (
        [Parameter(Mandatory = $true)]
        [string] $moduleName,

        [string] $moduleVersion,

        [ValidateSet('RequiredVersion', 'MinimumVersion', 'MaximumVersion')]
        [string] $moduleVersionType = 'RequiredVersion',

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

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

        [ValidateSet('5.1', '7.2')]
        [string] $runtimeVersion = '5.1',

        [int] $indent = 0,

        [hashtable[]] $overridePSGalleryModuleVersion = @{
            # 2.x.x PnP.PowerShell versions (2.1.1, 2.2.0) requires PSH 7.2 even though manifest doesn't say it
            # so the wrong module version would be picked up which would cause an error when trying to import
            "PnP.PowerShell" = @{
                "5.1" = "1.12.0"
            }
        }
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    $indentString = " " * $indent

    #region helper functions
    function _write {
        param ($string, $color, [switch] $noNewLine, [switch] $noIndent)

        $param = @{}
        if ($noIndent) {
            $param.Object = $string
        } else {
            $param.Object = ($indentString + $string)
        }
        if ($color) {
            $param.ForegroundColor = $color
        }
        if ($noNewLine) {
            $param.noNewLine = $true
        }

        Write-Host @param
    }

    function Compare-VersionString {
        # module version can be like "1.0.0", but also like "2.0.0-preview8", "2.0.0-rc3"
        # hence this comparison function
        param (
            [Parameter(Mandatory = $true)]
            $version1,

            [Parameter(Mandatory = $true)]
            $version2,

            [Parameter(Mandatory = $true)]
            [ValidateSet('equal', 'notEqual', 'greaterThan', 'lessThan')]
            $operator
        )

        function _convertResultToBoolean {
            # function that converts 0,1,-1 to true/false based on comparison operator
            param (
                [ValidateSet('equal', 'notEqual', 'greaterThan', 'lessThan')]
                $operator,

                $result
            )

            switch ($operator) {
                "equal" {
                    if ($result -eq 0) {
                        return $true
                    }
                }

                "notEqual" {
                    if ($result -ne 0) {
                        return $true
                    }
                }

                "greaterThan" {
                    if ($result -eq 1) {
                        return $true
                    }
                }

                "lessThan" {
                    if ($result -eq -1) {
                        return $true
                    }
                }

                default { throw "Undefined operator" }
            }

            return $false
        }

        # Split version and suffix
        $v1, $suffix1 = $version1 -split '-', 2
        $v2, $suffix2 = $version2 -split '-', 2

        # Compare versions
        $versionComparison = ([version]$v1).CompareTo([version]$v2)
        if ($versionComparison -ne 0) {
            return (_convertResultToBoolean -operator $operator -result $versionComparison)
        }

        # If versions are equal, compare suffixes
        if ($suffix1 -and !$suffix2) {
            return (_convertResultToBoolean -operator $operator -result -1)
        } elseif (!$suffix1 -and $suffix2) {
            return (_convertResultToBoolean -operator $operator -result 1)
        } elseif (!$suffix1 -and !$suffix2) {
            return (_convertResultToBoolean -operator $operator -result 0)
        } else {
            return (_convertResultToBoolean -operator $operator -result ([string]::Compare($suffix1, $suffix2)))
        }
    }
    #endregion helper functions

    if ($moduleVersion) {
        $moduleVersionString = "($moduleVersion)"
    } else {
        $moduleVersionString = ""
    }

    _write "Processing module $moduleName $moduleVersionString" "Magenta"

    #region get PSGallery module data
    $param = @{
        # IncludeDependencies = $true # cannot be used, because always returns newest usable module version, I want to use existing modules if possible (to minimize the runtime & risk that something will stop working)
        Name        = $moduleName
        ErrorAction = "Stop"
    }
    if ($moduleVersion) {
        $param.$moduleVersionType = $moduleVersion
        if (!($moduleVersion -as [version])) {
            # version is something like "2.2.0.rc4" a.k.a. pre-release version
            $param.AllowPrerelease = $true
        }
    } elseif ($runtimeVersion -eq '5.1') {
        $param.AllVersions = $true
    }

    $moduleGalleryInfo = Find-Module @param
    #endregion get PSGallery module data

    # get newest usable module version for given runtime
    if (!$moduleVersion -and $runtimeVersion -eq '5.1') {
        # no specific version was selected and older PSH version is used, make sure module that supports it, will be found
        # for example (currently newest) pnp.powershell 2.3.0 supports only PSH 7.2
        $moduleGalleryInfo = $moduleGalleryInfo | ? { $_.AdditionalMetadata.PowerShellVersion -le $runtimeVersion } | select -First 1
    }

    if (!$moduleGalleryInfo) {
        Write-Error "No supported $moduleName module was found in PSGallery"
        return
    }

    #region override module version
    # range instead of specific module version was specified
    if ($moduleVersion -and $moduleVersionType -ne 'RequiredVersion' -and $moduleVersion -ne $moduleGalleryInfo.Version) {
        _write " (version $($moduleGalleryInfo.Version) will be used instead of $moduleVersionType $moduleVersion)"
        $moduleVersion = $moduleGalleryInfo.Version
    }

    # no version was specified and module is in override list
    if (!$moduleVersion -and $moduleName -in $overridePSGalleryModuleVersion.Keys -and $overridePSGalleryModuleVersion.$moduleName.$runtimeVersion) {
        $overriddenModule = $overridePSGalleryModuleVersion.$moduleName
        $overriddenModuleVersion = $overriddenModule.$runtimeVersion
        if ($overriddenModuleVersion) {
            _write " (no version specified and override for version exists, hence will be used ($overriddenModuleVersion))"
            $moduleVersion = $overriddenModuleVersion
        }
    }

    # no version was specified, use the newest one
    if (!$moduleVersion) {
        $moduleVersion = $moduleGalleryInfo.Version
        _write " (no version specified, newest supported version from PSGallery will be used ($moduleVersion))"
    }
    #endregion override module version

    Write-Verbose "Getting current Automation modules"
    $currentAutomationModules = Get-AzAutomationModule -AutomationAccountName $automationAccountName -ResourceGroup $resourceGroupName -RuntimeVersion $runtimeVersion -ErrorAction Stop

    # check whether required module is present
    # there can be module in Failed state, just because update of such module failed, but if it has SizeInBytes set, it means its in working state
    $moduleExists = $currentAutomationModules | ? { $_.Name -eq $moduleName -and ($_.ProvisioningState -eq "Succeeded" -or $_.SizeInBytes) }
    if ($moduleExists) {
        $moduleExistsVersion = $moduleExists.Version
        if ($moduleVersion -and $moduleVersion -ne $moduleExistsVersion) {
            $moduleExists = $null
        }

        if ($moduleExists) {
            return ($indentString + "Module $moduleName ($moduleExistsVersion) is already present")
        } elseif (!$moduleExists -and $indent -eq 0) {
            # some module with that name exists, but not in the correct version and this is not a recursive call (because of dependency processing) hence user was not yet warned about replacing the module
            _write " - Existing module $moduleName ($moduleExistsVersion) will be replaced" "Yellow"
        }
    }

    _write " - Getting module $moduleName dependencies"
    $moduleDependency = $moduleGalleryInfo.Dependencies | Sort-Object { $_.name }

    # dependency must be installed first
    if ($moduleDependency) {
        #TODO znacit si jake moduly jsou required (at uz tam jsou nebo musim doinstalovat) a kontrolovat, ze jeden neni required s ruznymi verzemi == konflikt protoze nainstalovana muze byt jen jedna
        _write " - Depends on: $($moduleDependency.Name -join ', ')"
        foreach ($module in $moduleDependency) {
            $requiredModuleName = $module.Name
            $requiredModuleMinVersion = $module.MinimumVersion -replace "\[|]" # for some reason version can be like '[2.0.0-preview6]'
            $requiredModuleMaxVersion = $module.MaximumVersion -replace "\[|]"
            $requiredModuleReqVersion = $module.RequiredVersion -replace "\[|]"
            $notInCorrectVersion = $false

            _write " - Checking module $requiredModuleName (minVer: $requiredModuleMinVersion maxVer: $requiredModuleMaxVersion reqVer: $requiredModuleReqVersion)"

            # there can be module in Failed state, just because update of such module failed, but if it has SizeInBytes set, it means its in working state
            $existingRequiredModule = $currentAutomationModules | ? { $_.Name -eq $requiredModuleName -and ($_.ProvisioningState -eq "Succeeded" -or $_.SizeInBytes) }
            $existingRequiredModuleVersion = $existingRequiredModule.Version # version always looks like n.n.n. suffixes like rc, beta etc are always cut off!

            # check that existing module version fits
            if ($existingRequiredModule -and ($requiredModuleMinVersion -or $requiredModuleMaxVersion -or $requiredModuleReqVersion)) {
                #TODO pokud nahrazuji existujici modul, tak bych se mel podivat, jestli jsou vsechny ostatni ok s jeho novou verzi
                if ($requiredModuleReqVersion -and (Compare-VersionString $requiredModuleReqVersion $existingRequiredModuleVersion "notEqual")) {
                    $notInCorrectVersion = $true
                    _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be: $requiredModuleReqVersion). Will be replaced" "Yellow"
                } elseif ($requiredModuleMinVersion -and $requiredModuleMaxVersion -and ((Compare-VersionString $existingRequiredModuleVersion $requiredModuleMinVersion "lessThan") -or (Compare-VersionString $existingRequiredModuleVersion $requiredModuleMaxVersion "greaterThan"))) {
                    $notInCorrectVersion = $true
                    _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be: $requiredModuleMinVersion .. $requiredModuleMaxVersion). Will be replaced" "Yellow"
                } elseif ($requiredModuleMinVersion -and (Compare-VersionString $existingRequiredModuleVersion $requiredModuleMinVersion "lessThan")) {
                    $notInCorrectVersion = $true
                    _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be > $requiredModuleMinVersion). Will be replaced" "Yellow"
                } elseif ($requiredModuleMaxVersion -and (Compare-VersionString $existingRequiredModuleVersion $requiredModuleMaxVersion "greaterThan")) {
                    $notInCorrectVersion = $true
                    _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be < $requiredModuleMaxVersion). Will be replaced" "Yellow"
                }
            }

            if (!$existingRequiredModule -or $notInCorrectVersion) {
                if (!$existingRequiredModule) {
                    _write " - module is missing" "Yellow"
                }

                if ($notInCorrectVersion) {
                    #TODO kontrola, ze jina verze modulu nerozbije zavislost nejakeho jineho existujiciho modulu
                }

                #region install required module first
                $param = @{
                    moduleName            = $requiredModuleName
                    resourceGroupName     = $resourceGroupName
                    automationAccountName = $automationAccountName
                    runtimeVersion        = $runtimeVersion
                    indent                = $indent + 1
                }
                if ($requiredModuleMinVersion) {
                    $param.moduleVersion = $requiredModuleMinVersion
                    $param.moduleVersionType = 'MinimumVersion'
                }
                if ($requiredModuleMaxVersion) {
                    $param.moduleVersion = $requiredModuleMaxVersion
                    $param.moduleVersionType = 'MaximumVersion'
                }
                if ($requiredModuleReqVersion) {
                    $param.moduleVersion = $requiredModuleReqVersion
                    $param.moduleVersionType = 'RequiredVersion'
                }

                New-AzureAutomationModule @param
                #endregion install required module first
            } else {
                if ($existingRequiredModuleVersion) {
                    _write " - module (ver. $existingRequiredModuleVersion) is already present"
                } else {
                    _write " - module is already present"
                }
            }
        }
    } else {
        _write " - No dependency found"
    }

    $uri = "https://www.powershellgallery.com/api/v2/package/$moduleName/$moduleVersion"
    _write " - Uploading module $moduleName ($moduleVersion)" "Yellow"
    $status = New-AzAutomationModule -AutomationAccountName $automationAccountName -ResourceGroup $resourceGroupName -Name $moduleName -ContentLinkUri $uri -RuntimeVersion $runtimeVersion

    #region output dots while waiting on import to finish
    $i = 0
    _write " ." -noNewLine
    do {
        Start-Sleep 5

        if ($i % 3 -eq 0) {
            _write "." -noNewLine -noIndent
        }

        ++$i
    } while (!($requiredModule = Get-AzAutomationModule -AutomationAccountName $automationAccountName -ResourceGroup $resourceGroupName -RuntimeVersion $runtimeVersion -ErrorAction Stop | ? { $_.Name -eq $moduleName -and $_.ProvisioningState -in "Succeeded", "Failed" }))

    ""
    #endregion output dots while waiting on import to finish

    if ($requiredModule.ProvisioningState -ne "Succeeded") {
        Write-Error "Import failed. Check Azure Portal >> Automation Account >> Modules >> $moduleName details to get the reason."
    } else {
        _write " - Success" "Green"
    }
}

function New-AzureAutomationRuntime {
    <#
    .SYNOPSIS
    Function creates a new custom Azure Automation Account Runtime.
 
    .DESCRIPTION
    Function creates a new custom Azure Automation Account Runtime.
 
    Both Powershell nad Python runtimes are supported. Powershell one supports specifying Az module version.
 
    .PARAMETER runtimeName
    Name of the created runtime.
 
    .PARAMETER runtimeLanguage
    Language that will be used in created runtime.
 
    Possible values are PowerShell, Python.
 
    .PARAMETER runtimeVersion
    Version of the runtimeLanguage.
 
    For Python it is 3.8, 3.10, for PowerShell '5.1', '7.1', '7.2', but this will likely change in the future.
 
    .PARAMETER defaultPackage
    Only use for PowerShell runtimeLanguage!
 
    Hashtable where keys are default module names ('az' (both PSHs), 'azure cli' (only in PSH Core)) and values are module versions.
 
    If no defaultPackage hashtable is provided, no default modules will be enabled in created runtime.
 
    .PARAMETER description
    Runtime description.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    $defaultPackage = @{
        az = '8.0.0'
    }
 
    New-AzureAutomationRuntime -runtimeName 'CustomPSH51' -runtimeLanguage 'PowerShell' -runtimeVersion 5.1 -defaultPackage $defaultPackage -description 'PSH 5.1 for testing purposes' -resourceGroupName 'AdvancedLoggingRG' -automationAccountName 'EnableO365AdvancedLogging'
 
    Create new custom Powershell 5.1 runtime with Az module 8.0.0 enabled.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    New-AzureAutomationRuntime -runtimeName 'CustomPSH51' -runtimeLanguage 'PowerShell' -runtimeVersion 5.1 -description 'PSH 5.1 for testing purposes' -resourceGroupName 'AdvancedLoggingRG' -automationAccountName 'EnableO365AdvancedLogging'
 
    Create a new custom Powershell 5.1 runtime without Az module enabled.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    $defaultPackage = @{
        'az' = '8.0.0'
        'azure cli' = '2.56.0'
    }
 
    New-AzureAutomationRuntime -runtimeName 'CustomPSH72' -runtimeLanguage 'PowerShell' -runtimeVersion 7.2 -defaultPackage $defaultPackage -description 'PSH 7.2 for testing purposes' -resourceGroupName 'AdvancedLoggingRG' -automationAccountName 'EnableO365AdvancedLogging'
 
    Create a new custom Powershell 7.2 runtime with 'Az module 8.0.0' and 'azure cli 2.56.0' enabled.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    New-AzureAutomationRuntime -runtimeName 'CustomPython310' -runtimeLanguage 'Python' -runtimeVersion 3.10 -description 'Python 3.10 for testing purposes' -resourceGroupName 'AdvancedLoggingRG' -automationAccountName 'EnableO365AdvancedLogging'
 
    Create a new custom Python 3.10 runtime.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $runtimeName,

        [Parameter(Mandatory = $true)]
        [ValidateSet('PowerShell', 'Python')]
        [string] $runtimeLanguage,

        [ArgumentCompleter( {
                param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)

                if ($runtimeLanguage = $FakeBoundParams.runtimeLanguage) {
                    switch ($runtimeLanguage) {
                        'PowerShell' {
                            '5.1', '7.1', '7.2' | ? { $_ -like "*$WordToComplete*" }
                        }

                        'Python' {
                            '3.8', '3.10' | ? { $_ -like "*$WordToComplete*" }
                        }
                    }
                }
            })]
        [string] $runtimeVersion,

        [hashtable] $defaultPackage,

        [string] $description,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [hashtable] $header
    )

    #region checks
    if ($defaultPackage -and $runtimeLanguage -ne 'PowerShell') {
        Write-Warning "Parameter 'defaultModuleData' can be defined only for 'PowerShell' runtime language. Will be ignored."
        $defaultPackage = @{}
    }

    if (!$defaultPackage -and $runtimeLanguage -eq 'PowerShell') {
        $defaultPackage = @{}
    }
    #endregion checks

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }
    #endregion get missing arguments

    #region checks
    try {
        $runtime = $null
        $runtime = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -runtimeName $runtimeName -ErrorAction Stop
    } catch {
        if ($_.exception.StatusCode -ne 'NotFound') {
            throw $_
        }
    }

    if ($runtime) {
        # prevent accidental replacing of the existing runtime
        throw "Runtime with given name '$runtimeName' already exist"
    }
    #endregion checks

    #region send web request
    $body = @{
        properties = @{
            runtime         = @{
                language = $runtimeLanguage
                version  = $runtimeVersion
            }
            defaultPackages = $defaultPackage
            description     = $description
        }
    }

    $body = $body | ConvertTo-Json

    Write-Verbose $body

    Invoke-RestMethod2 -method Put -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtimeName`?api-version=2023-05-15-preview" -body $body -headers $header
    #endregion send web request
}

function New-AzureAutomationRuntimeModule {
    <#
    .SYNOPSIS
    Function add/replace selected module in specified Azure Automation runtime by importing it from the PowerShell Gallery.
    If module has some dependencies, that are currently missing (or have incorrect version), they will be imported automatically.
 
    .DESCRIPTION
    Function add/replace selected module in specified Azure Automation runtime by importing it from the PowerShell Gallery.
 
    If module exists, it will be replaced by selected version, if it is not, it will be added.
 
    If module has some dependencies, that are currently missing (or have incorrect version), they will be imported automatically.
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to retrieve.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER moduleName
    Name of the module you want to add/(replace by other version).
 
    .PARAMETER moduleVersion
    Module version.
    If not specified, newest supported version for given runtime will be gathered from PSGallery.
 
    .PARAMETER moduleVersionType
    Type of the specified module version.
 
    Possible values are: 'RequiredVersion', 'MinimumVersion', 'MaximumVersion'.
 
    By default 'RequiredVersion'.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .PARAMETER overridePSGalleryModuleVersion
    Hashtable of hashtables where you can specify what module version should be used for given runtime if no specific version is required.
 
    This is needed in cases, where newest module version available in PSGallery isn't compatible with your runtime because of incorrect module manifest.
 
    By default:
 
    $overridePSGalleryModuleVersion = @{
        # 2.x.x PnP.PowerShell versions (2.1.1, 2.2.0) requires PSH 7.2 even though manifest doesn't say it
        # so the wrong module version would be picked up which would cause an error when trying to import
        "PnP.PowerShell" = @{
            "5.1" = "1.12.0"
        }
    }
 
    .PARAMETER dontWait
    Switch for not waiting on module import to finish.
    Will be ignored if:
     - importing found module dependency (otherwise the "main" module import would fail)
     - function detects that requested module is currently being imported (I expect it to be some explicitly imported dependency)
    Beware that in case you explicitly import module A in version X.X.X and than some other module that depends on module A, but requires version Y.Y.Y, version X.X.X will be still imported. Because during the import process, you cannot tell which version is being imported a.k.a. you cannot check&fix it.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    New-AzureAutomationRuntimeModule -moduleName CommonStuff -moduleVersion 1.0.18
 
    Add module CommonStuff 1.0.18 to the specified Automation runtime.
    If module exists, it will be replaced by selected version, if it is not, it will be added.
    If module has some dependencies, that are currently missing (or have incorrect version), they will be imported automatically.
 
    Missing function arguments like $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    New-AzureAutomationRuntimeModule -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging" -runtimeName Custom_PSH_51 -moduleName CommonStuff -moduleVersion 1.0.18
 
    Add module CommonStuff 1.0.18 to specified Automation runtime.
    If module exists, it will be replaced by selected version, if it is not, it will be added.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    New-AzureAutomationRuntimeModule -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging" -runtimeName Custom_PSH_51 -moduleName CommonStuff -moduleVersion 1.0.18 -dontWait
 
    Add module CommonStuff 1.0.18 to specified Automation runtime.
    If module exists, it will be replaced by selected version, if it is not, it will be added.
    Function will not wait for import of the module to finish!
    #>


    [CmdletBinding()]
    [Alias("Set-AzureAutomationRuntimeModule")]
    param (
        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

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

        [string] $moduleVersion,

        [ValidateSet('RequiredVersion', 'MinimumVersion', 'MaximumVersion')]
        [string] $moduleVersionType = 'RequiredVersion',

        [hashtable] $header,

        [int] $indent = 0,

        [hashtable[]] $overridePSGalleryModuleVersion = @{
            # 2.x.x PnP.PowerShell versions (2.1.1, 2.2.0) requires PSH 7.2 even though manifest doesn't say it
            # so the wrong module version would be picked up which would cause an error when trying to import
            "PnP.PowerShell" = @{
                "5.1" = "1.12.0"
            }
        },

        [switch] $dontWait
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -programmingLanguage PowerShell -runtimeSource Custom -header $header | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select environment you want to process"
    }
    #endregion get missing arguments

    try {
        $runtime = Get-AzureAutomationRuntime -automationAccountName $automationAccountName -ResourceGroup $resourceGroupName -runtimeName $runtimeName -programmingLanguage PowerShell -runtimeSource Custom -header $header -ErrorAction Stop
    } catch {
        throw "Runtime '$runtimeName' doesn't exist or it isn't custom created PowerShell Runtime"
    }
    $runtimeVersion = $runtime.properties.runtime.version

    $indentString = " " * $indent

    #region helper functions
    function _write {
        param ($string, $color, [switch] $noNewLine, [switch] $noIndent)

        $param = @{}
        if ($noIndent) {
            $param.Object = $string
        } else {
            $param.Object = ($indentString + $string)
        }
        if ($color) {
            $param.ForegroundColor = $color
        }
        if ($noNewLine) {
            $param.noNewLine = $true
        }

        Write-Host @param
    }

    function Compare-VersionString {
        # module version can be like "1.0.0", but also like "2.0.0-preview8", "2.0.0-rc3"
        # hence this comparison function
        param (
            [Parameter(Mandatory = $true)]
            $version1,

            [Parameter(Mandatory = $true)]
            $version2,

            [Parameter(Mandatory = $true)]
            [ValidateSet('equal', 'notEqual', 'greaterThan', 'lessThan')]
            $operator
        )

        function _convertResultToBoolean {
            # function that converts 0,1,-1 to true/false based on comparison operator
            param (
                [ValidateSet('equal', 'notEqual', 'greaterThan', 'lessThan')]
                $operator,

                $result
            )

            switch ($operator) {
                "equal" {
                    if ($result -eq 0) {
                        return $true
                    }
                }

                "notEqual" {
                    if ($result -ne 0) {
                        return $true
                    }
                }

                "greaterThan" {
                    if ($result -eq 1) {
                        return $true
                    }
                }

                "lessThan" {
                    if ($result -eq -1) {
                        return $true
                    }
                }

                default { throw "Undefined operator" }
            }

            return $false
        }

        # Split version and suffix
        $v1, $suffix1 = $version1 -split '-', 2
        $v2, $suffix2 = $version2 -split '-', 2

        # Compare versions
        $versionComparison = ([version]$v1).CompareTo([version]$v2)
        if ($versionComparison -ne 0) {
            return (_convertResultToBoolean -operator $operator -result $versionComparison)
        }

        # If versions are equal, compare suffixes
        if ($suffix1 -and !$suffix2) {
            return (_convertResultToBoolean -operator $operator -result -1)
        } elseif (!$suffix1 -and $suffix2) {
            return (_convertResultToBoolean -operator $operator -result 1)
        } elseif (!$suffix1 -and !$suffix2) {
            return (_convertResultToBoolean -operator $operator -result 0)
        } else {
            return (_convertResultToBoolean -operator $operator -result ([string]::Compare($suffix1, $suffix2)))
        }
    }
    #endregion helper functions

    if ($moduleVersion) {
        $moduleVersionString = "($moduleVersion)"
    } else {
        $moduleVersionString = ""
    }

    _write "Processing module $moduleName $moduleVersionString" "Magenta"

    #region get PSGallery module data
    $param = @{
        # IncludeDependencies = $true # cannot be used, because always returns newest usable module version, I want to use existing modules if possible (to minimize the runtime & risk that something will stop working)
        Name        = $moduleName
        ErrorAction = "Stop"
    }
    if ($moduleVersion) {
        $param.$moduleVersionType = $moduleVersion
        if (!($moduleVersion -as [version])) {
            # version is something like "2.2.0.rc4" a.k.a. pre-release version
            $param.AllowPrerelease = $true
        }
    } elseif ($runtimeVersion -eq '5.1') {
        $param.AllVersions = $true
    }

    $moduleGalleryInfo = Find-Module @param
    #endregion get PSGallery module data

    # get newest usable module version for given runtime
    if (!$moduleVersion -and $runtimeVersion -eq '5.1') {
        # no specific version was selected and older PSH version is used, make sure module that supports it, will be found
        # for example (currently newest) pnp.powershell 2.3.0 supports only PSH 7.2
        $moduleGalleryInfo = $moduleGalleryInfo | ? { $_.AdditionalMetadata.PowerShellVersion -le $runtimeVersion } | select -First 1
    }

    if (!$moduleGalleryInfo) {
        Write-Error "No supported $moduleName module was found in PSGallery"
        return
    }

    #region override module version
    # range instead of specific module version was specified
    if ($moduleVersion -and $moduleVersionType -ne 'RequiredVersion' -and $moduleVersion -ne $moduleGalleryInfo.Version) {
        _write " (version $($moduleGalleryInfo.Version) will be used instead of $moduleVersionType $moduleVersion)"
        $moduleVersion = $moduleGalleryInfo.Version
    }

    # no version was specified and module is in override list
    if (!$moduleVersion -and $moduleName -in $overridePSGalleryModuleVersion.Keys -and $overridePSGalleryModuleVersion.$moduleName.$runtimeVersion) {
        $overriddenModule = $overridePSGalleryModuleVersion.$moduleName
        $overriddenModuleVersion = $overriddenModule.$runtimeVersion
        if ($overriddenModuleVersion) {
            _write " (no version specified and override for version exists, hence will be used ($overriddenModuleVersion))"
            $moduleVersion = $overriddenModuleVersion
        }
    }

    # no version was specified, use the newest one
    if (!$moduleVersion) {
        $moduleVersion = $moduleGalleryInfo.Version
        _write " (no version specified, newest supported version from PSGallery will be used ($moduleVersion))"
    }
    #endregion override module version

    Write-Verbose "Getting current Automation modules"
    $currentAutomationModules = Get-AzureAutomationRuntimeCustomModule -automationAccountName $automationAccountName -ResourceGroup $resourceGroupName -runtimeName $runtimeName -header $header -ErrorAction Stop

    # check whether required module is present
    # there can be module in Failed state, just because update of such module failed, but if it has SizeInBytes set, it means its in working state
    $moduleExists = $currentAutomationModules | ? { $_.Name -eq $moduleName -and ($_.Properties.ProvisioningState -eq "Succeeded" -or $_.Properties.SizeInBytes) }

    if ($moduleExists) {
        $moduleExistsVersion = $moduleExists.Properties.Version
        if ($moduleVersion -and $moduleVersion -ne $moduleExistsVersion) {
            $moduleExists = $null
        }

        if ($moduleExists) {
            return ($indentString + "Module $moduleName ($moduleExistsVersion) is already present")
        } elseif (!$moduleExists -and $indent -eq 0) {
            # some module with that name exists, but not in the correct version and this is not a recursive call (because of dependency processing) hence user was not yet warned about replacing the module
            _write " - Existing module $moduleName ($moduleExistsVersion) will be replaced" "Yellow"
        }
    }

    $moduleIsBeingImported = $currentAutomationModules | ? { $_.Name -eq $moduleName -and ($_.Properties.ProvisioningState -eq "Creating") }

    if ($moduleIsBeingImported) {
        # I expect this to be dependency explicitly imported with dontWait switch
        # therefore I wait for it to finish and at the same time I expect it to has the correct version
        _write " - Module $moduleName is being imported already. Wait for it to finish"
    } else {
        # module doesn't exist or has incorrect version a.k.a. it has to be imported

        _write " - Getting module $moduleName dependencies"
        $moduleDependency = $moduleGalleryInfo.Dependencies | Sort-Object { $_.name }

        # dependency must be installed first
        if ($moduleDependency) {
            #TODO znacit si jake moduly jsou required (at uz tam jsou nebo musim doinstalovat) a kontrolovat, ze jeden neni required s ruznymi verzemi == konflikt protoze nainstalovana muze byt jen jedna
            _write " - Depends on: $($moduleDependency.Name -join ', ')"
            foreach ($module in $moduleDependency) {
                $requiredModuleName = $module.Name
                $requiredModuleMinVersion = $module.MinimumVersion -replace "\[|]" # for some reason version can be like '[2.0.0-preview6]'
                $requiredModuleMaxVersion = $module.MaximumVersion -replace "\[|]"
                $requiredModuleReqVersion = $module.RequiredVersion -replace "\[|]"
                $notInCorrectVersion = $false

                _write " - Checking module $requiredModuleName (minVer: $requiredModuleMinVersion maxVer: $requiredModuleMaxVersion reqVer: $requiredModuleReqVersion)"

                # there can be module in Failed state, just because update of such module failed, but if it has SizeInBytes set, it means its in working state
                $existingRequiredModule = $currentAutomationModules | ? { $_.Name -eq $requiredModuleName -and ($_.Properties.ProvisioningState -eq "Succeeded" -or $_.Properties.SizeInBytes) }
                $existingRequiredModuleVersion = $existingRequiredModule.Properties.Version # version always looks like n.n.n. suffixes like rc, beta etc are always cut off!

                # check that existing module version fits
                if ($existingRequiredModule -and ($requiredModuleMinVersion -or $requiredModuleMaxVersion -or $requiredModuleReqVersion)) {
                    #TODO pokud nahrazuji existujici modul, tak bych se mel podivat, jestli jsou vsechny ostatni ok s jeho novou verzi
                    if ($requiredModuleReqVersion -and (Compare-VersionString $requiredModuleReqVersion $existingRequiredModuleVersion "notEqual")) {
                        $notInCorrectVersion = $true
                        _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be: $requiredModuleReqVersion). Will be replaced" "Yellow"
                    } elseif ($requiredModuleMinVersion -and $requiredModuleMaxVersion -and ((Compare-VersionString $existingRequiredModuleVersion $requiredModuleMinVersion "lessThan") -or (Compare-VersionString $existingRequiredModuleVersion $requiredModuleMaxVersion "greaterThan"))) {
                        $notInCorrectVersion = $true
                        _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be: $requiredModuleMinVersion .. $requiredModuleMaxVersion). Will be replaced" "Yellow"
                    } elseif ($requiredModuleMinVersion -and (Compare-VersionString $existingRequiredModuleVersion $requiredModuleMinVersion "lessThan")) {
                        $notInCorrectVersion = $true
                        _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be > $requiredModuleMinVersion). Will be replaced" "Yellow"
                    } elseif ($requiredModuleMaxVersion -and (Compare-VersionString $existingRequiredModuleVersion $requiredModuleMaxVersion "greaterThan")) {
                        $notInCorrectVersion = $true
                        _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be < $requiredModuleMaxVersion). Will be replaced" "Yellow"
                    }
                }

                if (!$existingRequiredModule -or $notInCorrectVersion) {
                    if (!$existingRequiredModule) {
                        _write " - module is missing" "Yellow"
                    }

                    if ($notInCorrectVersion) {
                        #TODO kontrola, ze jina verze modulu nerozbije zavislost nejakeho jineho existujiciho modulu
                    }

                    #region install required module first
                    $param = @{
                        moduleName            = $requiredModuleName
                        resourceGroupName     = $resourceGroupName
                        automationAccountName = $automationAccountName
                        runtimeName           = $runtimeName
                        indent                = $indent + 1
                    }
                    if ($requiredModuleMinVersion) {
                        $param.moduleVersion = $requiredModuleMinVersion
                        $param.moduleVersionType = 'MinimumVersion'
                    }
                    if ($requiredModuleMaxVersion) {
                        $param.moduleVersion = $requiredModuleMaxVersion
                        $param.moduleVersionType = 'MaximumVersion'
                    }
                    if ($requiredModuleReqVersion) {
                        $param.moduleVersion = $requiredModuleReqVersion
                        $param.moduleVersionType = 'RequiredVersion'
                    }

                    New-AzureAutomationRuntimeModule @param
                    #endregion install required module first
                } else {
                    if ($existingRequiredModuleVersion) {
                        _write " - module (ver. $existingRequiredModuleVersion) is already present"
                    } else {
                        _write " - module is already present"
                    }
                }
            }
        } else {
            _write " - No dependency found"
        }

        _write " - Uploading module $moduleName ($moduleVersion)" "Yellow"
        $modulePkgUri = "https://devopsgallerystorage.blob.core.windows.net/packages/$($moduleName.ToLower()).$moduleVersion.nupkg"

        $pkgStatus = Invoke-WebRequest -Uri $modulePkgUri -SkipHttpErrorCheck
        if ($pkgStatus.StatusCode -ne 200) {
            # don't exit the invocation, module can have as dependency module that doesn't exist in PSH Gallery
            Write-Error "Module $moduleName (version $moduleVersion) doesn't exist in PSGallery. Error was $($pkgStatus.StatusDescription)"
            return
        }

        #region send web request
        $body = @{
            "properties" = @{
                "contentLink" = @{
                    "uri" = $modulePkgUri
                }
                "version"     = $moduleVersion
            }
        }

        $body = $body | ConvertTo-Json

        Write-Verbose $body

        $null = Invoke-RestMethod2 -method Put -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtimeName/packages/$moduleName`?api-version=2023-05-15-preview" -body $body -headers $header
        #endregion send web request
    }

    #region output dots while waiting on import to finish
    $importingDependency = $false
    if ((Get-PSCallStack)[1].Command -eq $MyInvocation.MyCommand) {
        # recursive New-AzureAutomationRuntimeModule invocation
        Write-Verbose "$($MyInvocation.MyCommand) was called by itself a.k.a dependency module is being imported a.k.a. if I skip it, dependant module will fail on import"
        $importingDependency = $true
    }

    if ($dontWait -and !$moduleIsBeingImported -and !$importingDependency) {
        _write " - Don't wait for the upload to finish" "Yellow"
        return
    } else {
        $i = 0
        _write " ." -noNewLine
        do {
            Start-Sleep 5

            if ($i % 3 -eq 0) {
                _write "." -noNewLine -noIndent
            }

            ++$i
        } while (!($requiredModule = Get-AzureAutomationRuntimeCustomModule -automationAccountName $automationAccountName -ResourceGroup $resourceGroupName -runtimeName $runtimeName -moduleName $moduleName -header $header -ErrorAction Stop | ? { $_.Properties.ProvisioningState -in "Succeeded", "Failed" }))

        ""
    }
    #endregion output dots while waiting on import to finish

    # output import result
    if ($requiredModule.Properties.ProvisioningState -ne "Succeeded") {
        Write-Error "Import failed. Check Azure Portal >> Automation Account >> Runtime Environments >> $runtimeName >> $moduleName details to get the reason."
    } else {
        _write " - Success" "Green"
    }
}

function New-AzureAutomationRuntimeZIPModule {
    <#
    .SYNOPSIS
    Function imports given archived PowerShell module (as a ZIP file) to the given Automation Runtime Environment.
 
    .DESCRIPTION
    Function imports given archived PowerShell module (as a ZIP file) to the given Automation Runtime Environment.
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to retrieve.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER moduleZIPPath
    Path to the ZIP file containing archived module folder.
 
    Name of the ZIP will be used as name of the imported module!
 
    If the module folder contains psd1 manifest file, specified version will be set automatically as a module version in Runtime modules list. Otherwise the version will be 'unknown'.
 
    .PARAMETER dontWait
    Switch for not waiting on module import to finish.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    New-AzureAutomationRuntimeZIPModule -moduleZIPPath "C:\DATA\helperFunctions.zip"
 
    Imports module 'helperFunctions' to the specified Automation runtime.
 
    If module exists, it will be replaced, if it is not, it will be added.
 
    If module contains psd1 manifest file with specified version, such version will be set as module version in the Runtime module list. Otherwise the version will be 'unknown'.
 
    Missing function arguments like $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
    #>


    [CmdletBinding()]
    [Alias("Set-AzureAutomationRuntimeZIPModule")]
    param (
        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

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

        [switch] $dontWait
    )

    $ErrorActionPreference = "Stop"
    $InformationPreference = "Continue"

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    $moduleName = (Split-Path $moduleZIPPath -Leaf) -replace "\.zip$"

    $subscriptionId = (Get-AzContext).Subscription.Id

    # create auth token
    $accessToken = Get-AzAccessToken -ResourceTypeName "Arm"
    if ($accessToken.Token) {
        $header = @{
            'Content-Type'  = 'application/json'
            'Authorization' = "Bearer {0}" -f $accessToken.Token
        }
    }

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -programmingLanguage PowerShell -runtimeSource Custom -header $header | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select environment you want to process"
    }
    #endregion get missing arguments

    #region get upload URL
    Write-Verbose "Getting upload URL"
    $uploadUrl = Invoke-RestMethod -Method GET "https://s2.automation.ext.azure.com/api/Orchestrator/GenerateSasLinkUri?accountId=/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName&assetType=Module" -Headers $header
    Write-Verbose $uploadUrl
    #endregion get upload URL

    #region upload module (ZIP) using upload URL
    $uploadHeader = @{
        'X-Ms-Blob-Type'                = "BlockBlob"
        'X-Ms-Blob-Content-Disposition' = "filename=`"$(Split-Path $moduleZIPPath -Leaf)`""
        'X-Ms-Blob-Content-Type'        = 'application/x-gzip'
    }

    Write-Information "Uploading ZIP file"
    Invoke-RestMethod -Method PUT $uploadUrl -InFile $moduleZIPPath -Headers $uploadHeader
    #endregion upload module (ZIP) using upload URL

    #region importing uploaded module to the runtime
    $body = @{
        "properties" = @{
            "contentLink" = @{
                "uri" = $uploadUrl
            }
            "version"     = "" # ignored when uploading a ZIP
        }
    }
    $body = $body | ConvertTo-Json

    Write-Information "Importing uploaded module (ZIP) to the Runtime"
    Invoke-RestMethod -Method PUT "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtimeName/packages/$moduleName`?api-version=2023-05-15-preview" -Body $body -Headers $header
    #endregion importing uploaded module to the runtime

    #region output dots while waiting on import to finish
    if ($dontWait) {
        Write-Information "Skipping waiting on the ZIP file import to finish"
        return
    } else {
        $i = 0
        Write-Verbose "."
        do {
            Start-Sleep 5

            if ($i % 3 -eq 0) {
                Write-Verbose "."
            }

            ++$i
        } while (!($requiredModule = Get-AzureAutomationRuntimeCustomModule -automationAccountName $automationAccountName -ResourceGroup $resourceGroupName -runtimeName $runtimeName -moduleName $moduleName -header $header | ? { $_.Properties.ProvisioningState -in "Succeeded", "Failed" }))
    }
    #endregion output dots while waiting on import to finish

    if ($requiredModule.Properties.ProvisioningState -ne "Succeeded") {
        Write-Error "Import failed. Check Azure Portal >> Automation Account >> Runtime Environments >> $runtimeName >> $moduleName details to get the reason."
    } else {
        Write-Information "DONE"
    }
}

function Remove-AzureAutomationRuntime {
    <#
    .SYNOPSIS
    Removes selected Azure Automation Account Runtime(s).
 
    .DESCRIPTION
    Removes selected Azure Automation Account Runtime(s).
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to remove.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Remove-AzureAutomationRuntime
 
    Removes selected Automation Runtime.
 
    Missing function arguments like $runtimeName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Remove-AzureAutomationRuntime -runtimeName "PSH51Custom" -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging"
 
    Removes "PSH51Custom" Automation Runtime from given Automation Account.
    #>


    [CmdletBinding()]
    param (
        [string[]] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    if ($runtimeName) {
        foreach ($runtName in $runtimeName) {
            Write-Verbose "Checking existence of $runtName runtime"
            try {
                $runtime = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -runtimeName $runtName -ErrorAction Stop
            } catch {
                if ($_.exception.StatusCode -eq 'NotFound') {
                    throw "Runtime '$runtName' doesn't exist"
                } else {
                    throw $_
                }
            }
        }
    } else {
        while (!$runtimeName) {
            $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -programmingLanguage PowerShell -runtimeSource Custom | select -ExpandProperty Name | Out-GridView -OutputMode Multiple -Title "Select runtime you want to process"
        }
    }
    #endregion get missing arguments

    foreach ($runtName in $runtimeName) {
        Write-Verbose "Removing $runtName runtime"

        Invoke-RestMethod2 -method delete -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtName`?api-version=2023-05-15-preview" -body $body -headers $header
    }
}

function Remove-AzureAutomationRuntimeModule {
    <#
    .SYNOPSIS
    Function remove selected module from specified Azure Automation runtime.
 
    .DESCRIPTION
    Function remove selected module from specified Azure Automation runtime.
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to retrieve.
 
    If not provided, all runtimes will be returned.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER moduleName
    Name of the module(s) you want to remove.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Remove-AzureAutomationRuntimeModule
 
    Remove selected module(s) from the specified Automation runtime.
    Missing function arguments like $resourceGroupName or $moduleName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Remove-AzureAutomationRuntimeModule -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging" -runtimeName Custom_PSH_51 -moduleName CommonStuff
 
    Remove module CommonStuff from the specified Automation runtime.
    #>


    [CmdletBinding()]
    param (
        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string[]] $moduleName,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -programmingLanguage PowerShell -runtimeSource Custom | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runtime you want to process"
    }

    if (!$moduleName) {
        while (!$moduleName) {
            $moduleName = Get-AzureAutomationRuntimeCustomModule -runtimeName $runtimeName -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -moduleName $moduleName -header $header | select -ExpandProperty Name | Out-GridView -OutputMode Multiple -Title "Select module(s) you want to remove"
        }
    } else {
        $moduleExists = Get-AzureAutomationRuntimeCustomModule -runtimeName $runtimeName -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -moduleName $moduleName -header $header
        if (!$moduleExists) {
            throw "Module $moduleName doesn't exist in specified Automation environment"
        }
    }
    #endregion get missing arguments

    foreach ($modName in $moduleName) {
        Write-Verbose "Removing module $modName"

        Invoke-RestMethod2 -method Delete -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtimeName/packages/$modName`?api-version=2023-05-15-preview" -headers $header
    }
}

function Set-AutomationVariable2 {
    <#
    .SYNOPSIS
    Function for setting Azure RunBook variable value by exporting given value using Export-CliXml and saving the text result.
 
    .DESCRIPTION
    Function for setting Azure RunBook variable value by exporting given value using Export-CliXml and saving the text result.
    Compared to original Set-AutomationVariable this one is able to save original PSObjects as they were and not as Newtonsoft.Json.Linq.
    Variable set using this function has to be read using Get-AutomationVariable2!
 
    As original Set-AutomationVariable can be used only inside RunBook!
 
    .PARAMETER name
    Name of the RunBook variable you want to set.
 
    (to later retrieve such variable, use Get-AutomationVariable2!)
 
    .PARAMETER value
    Value you want to export to RunBook variable.
    Can be of any type.
 
    .EXAMPLE
    Set-AutomationVariable2 -name myVar -value @{name = 'John'; surname = 'Doe'}
 
    # to retrieve the variable
    #$hashTable = Get-AutomationVariable2 -name myVar
 
    Save given hashtable to variable myVar.
 
    .NOTES
    Same as original Get-AutomationVariable command, can be used only inside a Runbook!
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,

        $value
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "Authentication needed. Please call 'Connect-AzAccount -Identity'."
    }

    if ($value) {
        # in-memory export to CliXml (similar to Export-Clixml)
        $processedValue = [string]([System.Management.Automation.PSSerializer]::Serialize($value, 2))
    } else {
        $processedValue = ''
    }

    try {
        Set-AutomationVariable -Name $name -Value $processedValue -ErrorAction Stop
    } catch {
        throw "Unable to set automation variable $name. Set value is probably too big. Error was: $_"
    }
}

function Set-AzureAutomationRunbookContent {
    <#
    .SYNOPSIS
    Function sets Automation Runbook code content.
 
    .DESCRIPTION
    Function sets Automation Runbook code content.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER runbookName
    Runbook name.
 
    .PARAMETER content
    String that should be set as a new runbook code.
 
    .PARAMETER publish
    Switch to publish the newly set content.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    $content = @'
        Get-process notepad
        restart-service spooler
    '@
 
    Set-AzureAutomationRunbookContent -runbookName someRunbook -ResourceGroupName Automations -AutomationAccountName someAutomationAccount -content $content
 
    Sets given code as the new runbook content.
    #>


    [CmdletBinding()]
    param (
        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string] $runbookName,

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

        [switch] $publish,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    $subscriptionId = (Get-AzContext).Subscription.Id

    # create auth token
    $accessToken = Get-AzAccessToken -ResourceTypeName "Arm"
    if ($accessToken.Token) {
        $header = @{
            'Content-Type'  = 'application/json'
            'Authorization' = "Bearer {0}" -f $accessToken.Token
        }
    }

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runbookName) {
        $runbookName = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runbook you want to process"
    }
    #endregion get missing arguments

    Write-Verbose "Setting new runbook code content"
    Invoke-RestMethod2 "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName/draft/content?api-version=2015-10-31" -method PUT -headers $header -body $content

    if ($publish) {
        Write-Verbose "Publishing"
        $null = Publish-AzAutomationRunbook -Name $runbookName -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName
    }
}

function Set-AzureAutomationRunbookRuntime {
    <#
    .SYNOPSIS
    Set Runtime Environment in the selected Azure Automation Account Runbook.
 
    .DESCRIPTION
    Set Runtime Environment in the selected Azure Automation Account Runbook.
 
    .PARAMETER runtimeName
    Runtime name you want to use.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER runbookName
    Runbook name.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Set-AzureAutomationRunbookRuntime
 
    Set selected Runtime Environment in selected Runbook.
    Missing function arguments like $runtimeName, $resourceGroupName, $automationAccountName or $runbookName will be interactively gathered through Out-GridView GUI.
    #>


    [CmdletBinding()]
    param (
        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string] $runbookName,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runbookName) {
        $runbookName = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runbook you want to change"
    }
    #endregion get missing arguments

    $runbookType = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -Name $runbookName | select -ExpandProperty RunbookType

    if ($runbookType -eq 'python2') {
        $programmingLanguage = 'Python'
    } else {
        $programmingLanguage = $runbookType
    }

    $currentRuntimeName = Get-AzureAutomationRunbookRuntime -automationAccountName $automationAccountName -resourceGroupName $resourceGroupName -runbookName $runbookName -header $header

    if ($runtimeName -and $runtimeName -eq $currentRuntimeName) {
        Write-Warning "Runtime '$runtimeName' is already set. Skipping."
        return
    } else {
        while (!$runtimeName) {
            $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -programmingLanguage $programmingLanguage | select -ExpandProperty Name | ? { $_ -notin $currentRuntimeName } | Out-GridView -OutputMode Single -Title "Select runtime you want to set (current is '$currentRuntimeName')"
        }
    }

    #region send web request
    $body = @{
        "properties" = @{
            runtimeEnvironment = $runtimeName
        }
    }
    if ($programmingLanguage -eq 'Python') {
        # fix for bug? "The property runtimeEnvironment cannot be configured for runbookType Python2. Either use runbookType Python or remove runtimeEnvironment from input."
        $body.properties.runbookType = 'Python'
    }
    $body = $body | ConvertTo-Json

    Write-Verbose $body

    Invoke-RestMethod2 -method PATCH -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName`?api-version=2023-05-15-preview" -body $body -headers $header
    #endregion send web request
}

function Set-AzureAutomationRuntimeDefaultModule {
    <#
    .SYNOPSIS
    Function sets Runtime Default Module(s) to given version(s) in selected Azure Automation Account PowerShell Runbook.
    Default modules are currently 'az' and 'azure cli' (just in PowerShell 7.2).
 
    .DESCRIPTION
    Function sets Runtime Default Module(s) to given version(s) in selected Azure Automation Account PowerShell Runbook.
    Default modules are currently 'az' and 'azure cli' (just in PowerShell 7.2).
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to update.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER defaultPackage
    Hashtable where keys are default module names ('az' (both PSHs), 'azure cli' (only in PSH 7.2)) and values are module versions.
 
    If empty hashtable is provided, currently set default module(s) will be removed (set to 'not configured' in GUI terms).
 
    .PARAMETER replace
    Switch for replacing current default modules with the ones in 'defaultPackage' parameter.
    Hence what is not defined, will be removed.
 
    By default default modules not specified in the 'defaultPackage' parameter are ignored.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    $defaultPackage = @{
        'az' = '8.3.0'
        'azure cli' = '2.56.0'
    }
 
    Set-AzureAutomationRuntimeDefaultModule -defaultPackage $defaultPackage
 
    Set default modules to versions specified in $defaultPackage.
    Missing function arguments like $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    $defaultPackage = @{
        'azure cli' = '2.56.0'
    }
 
    Set-AzureAutomationRuntimeDefaultModule -defaultPackage $defaultPackage
 
    Set default module 'azure cli' to version '2.56.0'.
    In case that any other default module ('az') is set in the modified Runtime too, it will stay intact.
    Missing function arguments like $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    $defaultPackage = @{
        'azure cli' = '2.56.0'
    }
 
    Set-AzureAutomationRuntimeDefaultModule -defaultPackage $defaultPackage -replace
 
    Set default module 'azure cli' to version '2.56.0'.
    In case that any other default module ('az') is set in the modified Runtime too, it will be removed.
    Missing function arguments like $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Set-AzureAutomationRuntimeDefaultModule -defaultPackage @{}
 
    All default modules in selected Runtime will be removed.
    Missing function arguments like $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
    #>


    [CmdletBinding()]
    param (
        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [Parameter(Mandatory = $true)]
        [hashtable] $defaultPackage,

        [switch] $replace,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -programmingLanguage PowerShell | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select environment you want to process"
    }
    #endregion get missing arguments

    #region checks
    $runtime = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -runtimeName $runtimeName -header $header

    if (!$runtime) {
        throw "Runtime '$runtimeName' wasn't found"
    }

    # what default modules are currently set
    $currentDefaultModule = Get-AzureAutomationRuntimeSelectedDefaultModule -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -runtimeName $runtimeName -header $header

    # check default modules defined in given hashtable vs allowed/currently set ones
    $defaultPackage.GetEnumerator() | % {
        $defaultModuleName = $_.Key
        $defaultModuleVersion = $_.Value

        $currentDefaultModuleVersion = $currentDefaultModule | ? Name -EQ $defaultModuleName | select -ExpandProperty Version

        if ($defaultModuleVersion -eq $currentDefaultModuleVersion) {
            Write-Warning "Module '$defaultModuleName' already has version $defaultModuleVersion"
        }
    }
    #endregion checks

    #region send web request
    if ($defaultPackage.Count -eq 0) {
        # remove all default modules

        Write-Verbose "Removing all default modules"

        $body = @{
            properties = @{
                runtime         = @{
                    language = $runtime.properties.runtime.language
                    version  = $runtime.properties.runtime.version
                }
                defaultPackages = @{}
            }
        }

        $method = "Put"
    } else {
        # modify current default modules

        Write-Verbose "Replacing current default modules with the defined ones"

        if ($replace) {
            # replace

            $body = @{
                properties = @{
                    runtime         = @{
                        language = $runtime.properties.runtime.language
                        version  = $runtime.properties.runtime.version
                    }
                    defaultPackages = $defaultPackage
                }
            }

            $method = "Put"
        } else {
            # modify

            Write-Verbose "Updating defined default modules"

            $body = @{
                properties = @{
                    defaultPackages = $defaultPackage
                }
            }

            $method = "Patch"
        }
    }

    $body = $body | ConvertTo-Json

    Write-Verbose $body

    Invoke-RestMethod2 -method $method -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtimeName`?api-version=2023-05-15-preview" -body $body -headers $header
    #endregion send web request
}

function Set-AzureAutomationRuntimeDescription {
    <#
    .SYNOPSIS
    Function set Azure Automation Account Runtime description.
 
    .DESCRIPTION
    Function set Azure Automation Account Runtime description.
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to update.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER description
    Runtime description.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Set-AzureAutomationRuntimeDescription -description "testing runtime"
 
    Set given description in given Automation Runtime.
    Missing function arguments like $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Set-AzureAutomationRuntimeDescription -resourceGroupName "AdvancedLoggingRG" -automationAccountName "EnableO365AdvancedLogging" -description "testing runtime"
 
    Set given description in given Automation Runtime.
    #>


    [CmdletBinding()]
    param (
        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

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

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -header $header -programmingLanguage PowerShell | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select environment you want to process"
    }
    #endregion get missing arguments

    #region send web request
    $body = @{
        "properties" = @{
            "description" = $description
        }
    }
    $body = $body | ConvertTo-Json

    Write-Verbose $body

    Invoke-RestMethod2 -method Patch -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runtimeEnvironments/$runtimeName`?api-version=2023-05-15-preview" -body $body -headers $header
    #endregion send web request
}

function Start-AzureAutomationRunbookTestJob {
    <#
    .SYNOPSIS
    Start selected Runbook test job using selected Runtime.
 
    .DESCRIPTION
    Start selected Runbook test job using selected Runtime.
 
    Runtime will be used only for this test job, no permanent change to the Runbook will be made.
 
    To get the test job results use Get-AzureAutomationRunbookTestJobOutput, to get overall status use Get-AzureAutomationRunbookTestJobStatus.
 
    .PARAMETER runbookName
    Runbook name you want to run.
 
    .PARAMETER runtimeName
    Runtime name you want to use for a test job.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER wait
    Switch for waiting the Runbook test job to end and returning the job status.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Start-AzureAutomationRunbookTestJob
 
    Start selected Runbook test job using selected Runtime.
 
    Missing function arguments like $runbookName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    To get the test job results use Get-AzureAutomationRunbookTestJobOutput, to get overall status use Get-AzureAutomationRunbookTestJobStatus.
    #>


    [CmdletBinding()]
    [Alias("Invoke-AzureAutomationRunbookTestJob")]
    param (
        [string] $runbookName,

        [string] $runtimeName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [switch] $wait,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    $InformationPreference = 'continue'

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runbookName) {
        $runbookName = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runbook you want to start"
    }

    #region get runbook language
    $runbook = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -Name $runbookName -ErrorAction Stop

    $runbookType = $runbook.RunbookType
    if ($runbookType -eq 'python2') {
        $programmingLanguage = 'Python'
    } else {
        $programmingLanguage = $runbookType
    }
    #endregion get runbook language

    $currentRuntimeName = Get-AzureAutomationRunbookRuntime -automationAccountName $automationAccountName -resourceGroupName $resourceGroupName -runbookName $runbookName -header $header -ErrorAction Stop

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -programmingLanguage $programmingLanguage -header $header | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select environment you want to test (currently used '$currentRuntimeName')"
    }
    #endregion get missing arguments

    #region send web request
    $body = @{
        properties = @{
            "runtimeEnvironment" = $runtimeName
            "runOn"              = ""
            "parameters"         = @{}
        }
    }

    $body = $body | ConvertTo-Json

    Write-Verbose $body

    Write-Information "Starting Runbook '$runbookName' test job using Runtime '$runtimeName'"

    try {
        $null = Invoke-RestMethod2 -method Put -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName/draft/testJob?api-version=2023-05-15-preview" -headers $header -body $body -ErrorAction Stop
    } catch {
        if ($_.ErrorDetails.Message -like "*Test job is already running.*") {
            throw "Test job is currently running. Unable to start a new one."
        } else {
            throw $_
        }
    }
    #endregion send web request

    Write-Verbose "To get the test job results use Get-AzureAutomationRunbookTestJobOutput, to get overall status use Get-AzureAutomationRunbookTestJobStatus."

    if ($wait) {
        Write-Information "Waiting for the Runbook '$runbookName' to finish"
        Write-Information "Job status:"

        $processedStatus = @()

        do {
            $testRunStatus = Get-AzureAutomationRunbookTestJobStatus -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -runbookName $runbookName -header $header

            if ($testRunStatus.Status -notin $processedStatus) {
                $processedStatus += $testRunStatus.Status
                Write-Information "`t$($testRunStatus.Status)"
            }

            Start-Sleep 2
        } while ($testRunStatus.Status -notin "Stopped", "Completed", "Failed")

        $testRunStatus
    }
}

function Stop-AzureAutomationRunbookTestJob {
    <#
    .SYNOPSIS
    Invoke test run of the selected Runbook using selected Runtime.
 
    .DESCRIPTION
    Invoke test run of the selected Runbook using selected Runtime.
 
    Runtime will be used only for test run, no permanent change to the Runbook will be made.
 
    .PARAMETER runbookName
    Runbook name you want to run.
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Stop-AzureAutomationRunbookTestJob
 
    Stop test run of the selected Runbook.
 
    Missing function arguments like $runbookName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
    #>


    [CmdletBinding()]
    param (
        [string] $runbookName,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [switch] $wait,

        [hashtable] $header
    )

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    $InformationPreference = 'continue'

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runbookName) {
        $runbookName = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName | select -ExpandProperty Name | Out-GridView -OutputMode Single -Title "Select runbook you want to stop"
    }
    #endregion get missing arguments

    $testRunStatus = Get-AzureAutomationRunbookTestJobStatus -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -runbookName $runbookName -header $header

    if ($testRunStatus.Status -in "Stopped", "Completed", "Failed") {
        Write-Warning "Runbook '$runbookName' test job isn't running"
        return
    }

    Write-Information "Stopping Runbook '$runbookName' test job"

    Invoke-RestMethod2 -method Post -uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName/draft/testJob/stop?api-version=2019-06-01" -headers $header

    if ($wait) {
        Write-Information -MessageData "Waiting for the Runbook '$runbookName' test job to stop"

        do {
            $testRunStatus = Get-AzureAutomationRunbookTestJobStatus -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -runbookName $runbookName -header $header
            Start-Sleep 5
        } while ($testRunStatus.Status -ne "Stopped")

        Write-Information -MessageData "Runbook '$runbookName' test job was stopped"
    }
}

function Update-AzureAutomationModule {
    [CmdletBinding()]
    param (
        [string[]] $moduleName,

        [string] $moduleVersion,

        [switch] $allModule,

        [switch] $allCustomModule,

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

        [string[]] $automationAccountName,

        [ValidateSet('5.1', '7.2')]
        [string] $runtimeVersion = '5.1'
    )

    if ($allCustomModule -and $moduleName) {
        throw "Choose moduleName or allCustomModule"
    }
    if ($allCustomModule -and $allModule) {
        throw "Choose allModule or allCustomModule"
    }

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    $subscription = $((Get-AzContext).Subscription.Name)

    $automationAccount = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName

    if (!$automationAccount) {
        throw "No Automation account found in the current Subscription '$subscription' and Resource group '$resourceGroupName'"
    }

    if ($automationAccountName) {
        $automationAccount = $automationAccount | ? AutomationAccountName -EQ $automationAccountName
    }

    if (!$automationAccount) {
        throw "No Automation account match the selected criteria"
    }

    foreach ($atmAccount in $automationAccount) {
        $atmAccountName = $atmAccount.AutomationAccountName
        $atmAccountResourceGroup = $atmAccount.ResourceGroupName

        "Processing Automation account '$atmAccountName' (ResourceGroup: '$atmAccountResourceGroup' Subscription: '$subscription')"

        $currentAutomationModules = Get-AzAutomationModule -AutomationAccountName $atmAccountName -ResourceGroup $atmAccountResourceGroup -RuntimeVersion $runtimeVersion

        if ($allCustomModule) {
            $automationModulesToUpdate = $currentAutomationModules | ? IsGlobal -EQ $false
        } elseif ($moduleName) {
            $automationModulesToUpdate = $currentAutomationModules | ? Name -In $moduleName
            if ($moduleVersion -and $automationModulesToUpdate) {
                Write-Verbose "Selecting only module(s) with version $moduleVersion or lower"
                $automationModulesToUpdate = $automationModulesToUpdate | ? { [version]$_.Version -lt [version] $moduleVersion }
            }
        } elseif ($allModule) {
            $automationModulesToUpdate = $currentAutomationModules
        } else {
            $automationModulesToUpdate = $currentAutomationModules | Out-GridView -PassThru
            if ($moduleVersion -and $automationModulesToUpdate) {
                Write-Verbose "Selecting only module(s) with version $moduleVersion or lower"
                $automationModulesToUpdate = $automationModulesToUpdate | ? { [version]$_.Version -lt [version] $moduleVersion }
            }
        }

        if (!$automationModulesToUpdate) {
            Write-Warning "No module match the selected update criteria. Skipping"
            continue
        }

        foreach ($module in $automationModulesToUpdate) {
            $moduleName = $module.Name
            $requiredModuleVersion = $moduleVersion

            #region get PSGallery module data
            $param = @{
                # IncludeDependencies = $true # cannot be used, because always returns newest available modules, I want to use existing modules if possible (to minimize risk that something will stop working)
                Name        = $moduleName
                ErrorAction = "Stop"
            }
            if ($requiredModuleVersion) {
                $param.RequiredVersion = $requiredModuleVersion
            } else {
                $param.AllVersions = $true
            }

            $moduleGalleryInfo = Find-Module @param
            #endregion get PSGallery module data

            # get newest usable module version for given runtime
            if (!$requiredModuleVersion -and $runtimeVersion -eq '5.1') {
                # no specific version was selected and older PSH version is used, make sure module that supports it, will be found
                # for example (currently newest) pnp.powershell 2.3.0 supports only PSH 7.2
                $moduleGalleryInfo = $moduleGalleryInfo | ? { $_.AdditionalMetadata.PowerShellVersion -le $runtimeVersion } | select -First 1
            }

            if (!$moduleGalleryInfo) {
                Write-Error "No supported $moduleName module was found in PSGallery"
                continue
            }

            if (!$requiredModuleVersion) {
                # no version specified, newest version from PSGallery will be used"
                $requiredModuleVersion = $moduleGalleryInfo.Version | select -First 1

                if ($requiredModuleVersion -eq $module.Version) {
                    Write-Warning "Module $moduleName already has newest available version $requiredModuleVersion. Skipping"
                    continue
                }
            }

            $param = @{
                resourceGroupName     = $module.ResourceGroupName
                automationAccountName = $module.AutomationAccountName
                moduleName            = $module.Name
                runtimeVersion        = $runtimeVersion
                moduleVersion         = $requiredModuleVersion
            }

            "Updating module $($module.Name) $($module.Version) >> $requiredModuleVersion"
            New-AzureAutomationModule @param
        }
    }
}

function Update-AzureAutomationRunbookModule {
    <#
    .SYNOPSIS
    Function updates all/selected custom modules in given Azure Automation Account Environment Runtime.
 
    Custom module means module you have to explicitly import (not 'Az' or 'azure cli').
 
    .DESCRIPTION
    Function updates all/selected custom modules in given Azure Automation Account Environment Runtime.
 
    Custom module means module you have to explicitly import (not 'Az' or 'azure cli').
 
    .PARAMETER moduleName
    Name of the module you want to add/(replace by other version).
 
    .PARAMETER moduleVersion
    Target module version you want to update to.
 
    Applies to all updated modules!
 
    If not specified, newest supported version for used runtime language version will be gathered from PSGallery.
 
    .PARAMETER allCustomModule
    Parameter description
 
    .PARAMETER resourceGroupName
    Resource group name.
 
    .PARAMETER automationAccountName
    Automation account name.
 
    .PARAMETER runtimeName
    Name of the runtime environment you want to retrieve.
 
    .PARAMETER header
    Authentication header that can be created via New-AzureAutomationGraphToken.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Update-AzureAutomationRunbookModule -moduleName CommonStuff -moduleVersion 1.0.18
 
    Updates module CommonStuff to the version 1.0.18 in the specified Automation runtime(s).
    If module has some dependencies, that are currently missing (or have incorrect version), they will be imported automatically.
 
    Missing function arguments like $runtimeName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Update-AzureAutomationRunbookModule -moduleName CommonStuff
 
    Updates module CommonStuff to the newest available version in the specified Automation runtime(s).
    If module has some dependencies, that are currently missing (or have incorrect version), they will be imported automatically.
 
    Missing function arguments like $runtimeName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
 
    .EXAMPLE
    Connect-AzAccount
 
    Set-AzContext -Subscription "IT_Testing"
 
    Update-AzureAutomationRunbookModule -allCustomModule
 
    Updates all custom modules to the newest available version in the specified Automation runtime(s).
    If module(s) have some dependencies, that are currently missing (or have incorrect version), they will be imported automatically.
 
    Missing function arguments like $runtimeName, $resourceGroupName or $automationAccountName will be interactively gathered through Out-GridView GUI.
    #>


    [CmdletBinding()]
    param (
        [string[]] $moduleName,

        [string] $moduleVersion,

        [switch] $allCustomModule,

        [string] $resourceGroupName,

        [string] $automationAccountName,

        [string[]] $runtimeName,

        [hashtable] $header
    )

    if ($allCustomModule -and $moduleName) {
        throw "Choose moduleName or allCustomModule"
    }

    if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) {
        throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount."
    }

    #region get missing arguments
    if (!$header) {
        $header = New-AzureAutomationGraphToken
    }

    $subscriptionId = (Get-AzContext).Subscription.Id
    $subscription = $((Get-AzContext).Subscription.Name)

    while (!$resourceGroupName) {
        $resourceGroupName = Get-AzResourceGroup | select -ExpandProperty ResourceGroupName | Out-GridView -OutputMode Single -Title "Select resource group you want to process"
    }

    while (!$automationAccountName) {
        $automationAccountName = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName | select -ExpandProperty AutomationAccountName | Out-GridView -OutputMode Single -Title "Select automation account you want to process"
    }

    while (!$runtimeName) {
        $runtimeName = Get-AzureAutomationRuntime -resourceGroupName $resourceGroupName -automationAccountName $automationAccountName -programmingLanguage PowerShell -runtimeSource Custom -header $header | select -ExpandProperty Name | Out-GridView -OutputMode Multiple -Title "Select environment you want to process"
    }

    $runtimeVersion = $runtime.properties.runtime.version
    #endregion get missing arguments

    foreach ($runtName in $runtimeName) {
        "Processing Runtime '$runtName' (ResourceGroup: '$resourceGroupName' Subscription: '$subscription')"

        $currentAutomationCustomModules = Get-AzureAutomationRuntimeCustomModule -automationAccountName $automationAccountName -ResourceGroup $resourceGroupName -runtimeName $runtName -header $header -ErrorAction Stop

        if ($allCustomModule) {
            $automationModulesToUpdate = $currentAutomationCustomModules
        } elseif ($moduleName) {
            $automationModulesToUpdate = $currentAutomationCustomModules | ? Name -In $moduleName

            if ($moduleVersion -and $automationModulesToUpdate) {
                Write-Verbose "Selecting only module(s) with version $moduleVersion or lower"
                $automationModulesToUpdate = $automationModulesToUpdate | ? { [version]$_.Version -lt [version] $moduleVersion }
            }
        } else {
            $automationModulesToUpdate = $currentAutomationCustomModules | Out-GridView -PassThru -Title "Select module(s) to update"

            if ($moduleVersion -and $automationModulesToUpdate) {
                Write-Verbose "Selecting only module(s) with version $moduleVersion or lower"
                $automationModulesToUpdate = $automationModulesToUpdate | ? { [version]$_.Version -lt [version] $moduleVersion }
            }
        }

        if (!$automationModulesToUpdate) {
            Write-Warning "No module match the selected update criteria. Skipping"
            continue
        }

        foreach ($module in $automationModulesToUpdate) {
            $moduleName = $module.Name
            $requiredModuleVersion = $moduleVersion

            #region get PSGallery module data
            $param = @{
                # IncludeDependencies = $true # cannot be used, because always returns newest available modules, I want to use existing modules if possible (to minimize risk that something will stop working)
                Name        = $moduleName
                ErrorAction = "Stop"
            }
            if ($requiredModuleVersion) {
                $param.RequiredVersion = $requiredModuleVersion
            } else {
                $param.AllVersions = $true
            }

            $moduleGalleryInfo = Find-Module @param
            #endregion get PSGallery module data

            # get newest usable module version for given runtime
            if (!$requiredModuleVersion -and $runtimeVersion -eq '5.1') {
                # no specific version was selected and older PSH version is used, make sure module that supports it, will be found
                # for example (currently newest) pnp.powershell 2.3.0 supports only PSH 7.2
                $moduleGalleryInfo = $moduleGalleryInfo | ? { $_.AdditionalMetadata.PowerShellVersion -le $runtimeVersion } | select -First 1
            }

            if (!$moduleGalleryInfo) {
                Write-Error "No supported $moduleName module was found in PSGallery"
                continue
            }

            if (!$requiredModuleVersion) {
                # no version specified, newest version from PSGallery will be used"
                $requiredModuleVersion = $moduleGalleryInfo.Version | select -First 1

                if ($requiredModuleVersion -eq $module.Version) {
                    Write-Warning "Module $moduleName already has newest available version $requiredModuleVersion. Skipping"
                    continue
                }
            }

            $param = @{
                resourceGroupName     = $resourceGroupName
                automationAccountName = $automationAccountName
                runtimeName           = $runtName
                moduleName            = $module.Name
                moduleVersion         = $requiredModuleVersion
                header                = $header
            }

            "Updating module $($module.Name) $($module.Version) >> $requiredModuleVersion"
            New-AzureAutomationRuntimeModule @param
        }
    }
}

Export-ModuleMember -function Copy-AzureAutomationRuntime, Export-VariableToStorage, Get-AutomationVariable2, Get-AzureAutomationRunbookContent, Get-AzureAutomationRunbookRuntime, Get-AzureAutomationRunbookTestJobOutput, Get-AzureAutomationRunbookTestJobStatus, Get-AzureAutomationRuntime, Get-AzureAutomationRuntimeAvailableDefaultModule, Get-AzureAutomationRuntimeCustomModule, Get-AzureAutomationRuntimeSelectedDefaultModule, Get-AzureResource, Import-VariableFromStorage, New-AzureAutomationGraphToken, New-AzureAutomationModule, New-AzureAutomationRuntime, New-AzureAutomationRuntimeModule, New-AzureAutomationRuntimeZIPModule, Remove-AzureAutomationRuntime, Remove-AzureAutomationRuntimeModule, Set-AutomationVariable2, Set-AzureAutomationRunbookContent, Set-AzureAutomationRunbookRuntime, Set-AzureAutomationRuntimeDefaultModule, Set-AzureAutomationRuntimeDescription, Start-AzureAutomationRunbookTestJob, Stop-AzureAutomationRunbookTestJob, Update-AzureAutomationModule, Update-AzureAutomationRunbookModule

Export-ModuleMember -alias Get-AzureAutomationRuntimeAzModule, Invoke-AzureAutomationRunbookTestJob, New-AzAutomationModule2, Set-AzureAutomationModule, Set-AzureAutomationRuntimeModule, Set-AzureAutomationRuntimeZIPModule