PureStorage.RunCommandLauncher.ps1

Import-Module PureStorage.RunCommandWrapper
function Get-DefaultAzureSubscriptionId {
    # Azure Resource Manager cmdlets use this settings by default when making Azure Resource Manager requests.
    # To list all of subscription, it would be Get-AzContext -ListAvailable
    # In our case, getting default account is sufficient
    $DefaultAzContext = Get-AzContext
    if ($DefaultAzContext.Subscription) {
        return $DefaultAzContext.Subscription.Id
    }
    else {
        throw "Could not find default Azure subscription. Please make sure you are logging in to Azure using 'Connect-AzAccount'. If you have multiple subscriptions, please use 'Select-AzSubscription' to select the subscription you want to use."
    }
}

function Invoke-AvsScriptExecution {
    param (
        [string]$AVSCloudName,
        [string]$SubscriptionId,
        [string]$AVSResourceGroup,
        [string]$RunCommandId,
        [string]$RunCommandExecName,
        [int]$TimeoutInMinutes = 10,
        [string]$ProductName,
        [string]$ProductVersion,
        [hashtable]$CommandParameters,
        [string]$ErrorVariable = "ExecutionError"
    )

    # Convert Timeout to ISO 8601 duration format
    $Timeout = "P0Y0M0DT0H$TimeoutInMinutes`M0S"

    # Construct JSON payload for script execution
    $Payload = @{
        properties = @{
            scriptCmdletId = $RunCommandId
            timeout = $Timeout
            parameters = @()
        }
    }

    foreach ($key in $CommandParameters.Keys) {
        $value = $CommandParameters[$key]

        # Convert large numbers (above 2^53) to string to avoid 'Number ...M' issue
        if ($value -is [UInt64] -or ($value -is [int64] -and $value -ge [math]::Pow(2,53))) {
            $value = "$value"
        }

        $Payload.properties.parameters += @{
            name  = $key
            type  = "Value"
            value = $value
        }
    }
    $JsonPayload = $Payload | ConvertTo-Json -Depth 10

    # REST API call to create script execution
    $ExecutionUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$AVSResourceGroup/providers/Microsoft.AVS/privateClouds/$AVSCloudName/scriptExecutions/$($RunCommandExecName)?api-version=2022-05-01"
    Write-Verbose "Invoking script execution: $ExecutionUri`nRun Command ID: $RunCommandId`nPayload: $($JsonPayload)"

    $ExecutionResponse = Invoke-AzRestMethod -Uri $ExecutionUri -Method Put -Payload $JsonPayload
    if ($ExecutionResponse.StatusCode -lt 200 -or $ExecutionResponse.StatusCode -gt 299) {
        throw "Failed to create script execution. Status code: $($ExecutionResponse.StatusCode), Response: $($ExecutionResponse.Content)"
    }

    $ExecutionId = $ExecutionResponse.Content.id
    Write-Debug "Script Execution ID: $ExecutionId"

    # Poll for completion with a timeout
    $StartTime = Get-Date
    $TimeoutLimit = New-TimeSpan -Minutes $TimeoutInMinutes
    $LastStatusLogTime = $StartTime
    $Status = "Unknown"
    $ExecutionOutput = $null

    do {
        Start-Sleep -Seconds 5  # Poll every 5 seconds
        $Elapsed = (Get-Date) - $StartTime

        # Fetch script execution status and output in a single call
        $StatusResponse = Invoke-AzRestMethod -Uri $ExecutionUri -Method Get
        if ($StatusResponse.StatusCode -eq 200) {
            $ResponseData = $StatusResponse.Content | ConvertFrom-Json -Depth 10
            $Status = $ResponseData.properties.provisioningState
            $ExecutionOutput = $ResponseData.properties.output
        }

        # Log status every 60 seconds to avoid excessive logging
        if ((Get-Date) - $LastStatusLogTime -ge (New-TimeSpan -Seconds 60)) {
            Write-Verbose "Current Status: $Status"
            $LastStatusLogTime = Get-Date
        }

        if ($Status -eq "Succeeded") {
            Write-Debug "Script executed successfully!"
            break
        } elseif ($Status -eq "Failed") {
            throw "Script execution failed! Execution ID: $ExecutionId, URI: $ExecutionUri, Details: $ExecutionOutput"
        }
    } while ($Elapsed -lt $TimeoutLimit)

    # If timeout is reached, terminate
    if ($Elapsed -ge $TimeoutLimit) {
        throw "Timeout reached! Script execution did not complete in $TimeoutInMinutes minutes. Execution ID: $ExecutionId, URI: $ExecutionUri, Last known status: $Status"
    }

    Write-Host "Script Execution Completed. Execution ID: $ExecutionId, URI: $ExecutionUri"
    if ($ExecutionOutput) {
        Write-Host "Script Output: $ExecutionOutput"
    }

    return $ExecutionOutput
}

function Invoke-RunScript {
    Param (
        [Parameter(Mandatory=$true)]
        [String] $RunCommandName,

        [Parameter(Mandatory=$true)]
        [ValidateSet("Microsoft.AVS.VMFS", "Microsoft.AVS.VVOLS")]
        [String] $RunCommandModule,

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

        [Parameter(Mandatory=$false)]
        [String]$AVSCloudName,

        [Parameter(Mandatory=$false)]
        [String]$AVSResourceGroup,

        [Parameter(Mandatory=$false)]
        [switch]$GetNamedOutputs,

        [Parameter(Mandatory=$false)]
        [int] $TimeoutInMinutes = 10
    )

    $SubscriptionId = Get-DefaultAzureSubscriptionId
    Write-Host "Using Azure default subscription: $SubscriptionId"

    # Determine which RunCommand version to use
    $UseAvsClient = $env:CBS_AVS_SCRIPT_EXECUTION_VIA_AVS_CLIENT -eq "true"

    # Microsoft recommends using the stable version
    if ($RunCommandModule -eq "Microsoft.AVS.VMFS") {
        $RunCommandPackageVersion = "1.0.154"
    } elseif($RunCommandModule -eq "Microsoft.AVS.VVOLS") {
        $RunCommandPackageVersion = "1.0.110-dev"
    } else {
        throw "Unknown RunCommandModule $RunCommandModule"
    }

    # Allow overriding the version via an environment variable
    $EnvVariableName = ($RunCommandModule -replace "\\.", "_") + '_Version'
    $UpdatedVersion = [Environment]::GetEnvironmentVariable($EnvVariableName)
    if ($UpdatedVersion) {
        Write-Warning "Using customized AVS Run Command version $UpdatedVersion for Module ($RunCommandModule). Please consult with Pure Storage Support before using this version"
        $RunCommandPackageVersion = $UpdatedVersion
    }

    # Test RunCommand availability
    Test-RunCommandPackageAvailability -SubscriptionId $SubscriptionId -RunCommandModule $RunCommandModule -RunCommandPackageVersion $RunCommandPackageVersion -AVSCloudName $AVSCloudName -AVSResourceGroup $AVSResourceGroup

    # Construct command execution details
    $CmdletId = "/subscriptions/$SubscriptionId/resourceGroups/$AVSResourceGroup/providers/Microsoft.AVS/privateClouds/$AVSCloudName/scriptPackages/$RunCommandModule@$RunCommandPackageVersion/scriptCmdlets/$RunCommandName"
    $RandomID = [System.Guid]::NewGuid().ToString().Substring(0,8)
    $RunCmdExecutionName = "$RunCommandName-PureStorage.RunCommandWrapper-$RandomID"
    $ProductName = "PureStorage.CBS.AVS"
    $ProductVersion = (Get-Module $ProductName).Version.ToString()

    Write-Host "Invoking RunCommand $RunCmdExecutionName with ID $CmdletId (use AvsClient: $UseAvsClient) ..."

    if ($UseAvsClient) {
        # Use the original AvsClient-based function
        Invoke-AvsScript -AVSCloudName $AVSCloudName -SubscriptionId  $SubscriptionId -AVSResourceGroup $AVSResourceGroup `
        -RunCommandId $CmdletId -RunCommandExecName $RunCmdExecutionName -TimeoutInMinutes $TimeoutInMinutes `
        -ProductName $ProductName -ProductVersion $ProductVersion `
        -CommandParameters $Parameters -ErrorVariable stopError
    } else {
        # Use the new REST API-based function
        Invoke-AvsScriptExecution -AVSCloudName $AVSCloudName -SubscriptionId  $SubscriptionId -AVSResourceGroup $AVSResourceGroup `
        -RunCommandId $CmdletId -RunCommandExecName $RunCmdExecutionName -TimeoutInMinutes $TimeoutInMinutes `
        -ProductName $ProductName -ProductVersion $ProductVersion `
        -CommandParameters $Parameters -ErrorVariable stopError
    }

    if ($stopError) {
        Write-Host "RunCommand $RunCmdExecutionName failed with error:"
        throw $stopError
    }

    if ($GetNamedOutputs) {
        $Output = Get-RunCommandNamedOutput -RunCmdExecutionName $RunCmdExecutionName -AvsPrivateCloudName $AVSCloudName -AvsResourceGroupName $AVSResourceGroup
        Set-Variable -Name NamedOutputs -Value $Output -Scope Script
    }
}

Export-ModuleMember -Function Invoke-RunScript