public/Invoke-AzToolsVmRunCommand.ps1

function Invoke-AzToolsVmRunCommand  {
    <#
    .SYNOPSIS
        Invoke the "Run Command" feature on an Azure Virtual Machine using PowerShell
    .DESCRIPTION
        Invoke the Azure VM "Run Command" to submit PowerShell code to remote machines and
        return the result. Script code can be provided in-line or from a script file.
    .PARAMETER ScriptContent
        Optional. PowerShell statement to run on the remote machine.
        Example: Get-Process | Sort-Object WorkingSet -Desc | Select-Object -First 3
        Note: Either -ScriptContent or -ScriptFile must be provided, not both
    .PARAMETER ScriptFile
        Optional. File containing PowerShell script code to run on the remote machine.
        Note: Either -ScriptContent or -ScriptFile must be provided, not both
    .PARAMETER SelectContext
        Optional. Prompt to select the Azure context (tenant/subscription)
    .PARAMETER SelectSubscription
        Optional. Prompt to select Subscriptions to query machines.
        Default is to query the current subscription context only.
    .PARAMETER RunCommandName
        Optional. Name of RunCommand. Default is "AzToolsRunCommand"
    .PARAMETER WaitSeconds
        Optional. Number of seconds to wait for job completion on each VM between polling cycles
        Default is 10 (seconds)
    .PARAMETER TryCount
        Optional. Number of times to poll for job completion
        Default is 10 (retries)
    .EXAMPLE
        Invoke-AzToolsVmRunCommand -ScriptContent "Get-Service BITS"
        Prompts user to select VM's to run the command on from within the current subscription context.
    .EXAMPLE
        Invoke-AzToolsVmRunCommand -ScriptContent "Get-Service BITS" -SelectSubscription
        Prompts user to select the Subscriptions to query VM's and then prompts to select the VM's to run the command on.
    .LINK
        https://github.com/Skatterbrainz/aztools/tree/main/docs/Invoke-AzToolsVmRunCommand.md
    #>

    [CmdletBinding()]
    param (
        [parameter(Mandatory=$False,HelpMessage="Script Content to execute on remote machine")]
            [string]$ScriptContent,
        [parameter(Mandatory=$False,HelpMessage="Script file to import an excute on remote machine")]
            [string]$ScriptFile,
        [parameter(Mandatory=$False,HelpMessage="Select Azure Context")]
            [switch]$SelectContext,
        [parameter(Mandatory=$False,HelpMessage="Select Subscription to query for remote machines")]
            [switch]$SelectSubscription,
        [parameter(Mandatory=$False,HelpMessage="Name of Run Command. Default is AzToolsRunCommand")]
            [string]$RunCommandName = "AzToolsRunCommand",
        [parameter(Mandatory=$False,HelpMessage="Number of seconds to pause per wait cycle")]
            [int32]$WaitSeconds = 10,
        [parameter(Mandatory=$False,HelpMessage="Number of wait cycles to return result. Total time is (TryCount * WaitSeconds)")]
            [int32]$TryCount = 10
    )
    # $tryCount * $waitSeconds = totalTime to wait if job doesn't finish
    if ($SelectContext) { Switch-AzToolsContext }
    if ([string]::IsNullOrWhiteSpace($VMName)) {
        [array]$vms = Get-AzToolsMachines -SelectSubscription:$SelectSubscription
        $vms = $vms | Select-Object Name,ResourceGroupName,PowerState,OsName,Location,SubscriptionId,Id |
            Sort-Object Name | Out-GridView -Title "Select Machines to Send Command to" -OutputMode Multiple
        $vmcount = $vms.Count
        $vmnum   = 1
    }
    if ($vmcount -eq 0) { break }
    if (![string]::IsNullOrEmpty($ScriptContent)) {
        $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent))
    } elseif (![string]::IsNullOrEmpty($ScriptFile)) {
        if (Test-Path -Path $ScriptFile) {
            $ScriptContent = Get-Content -Path $ScriptFile -Raw
            $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent))
        } else {
            throw "File not found: $ScriptFile"
        }
    } else {
        throw "Either -ScriptContent or -FilePath must be provided"
    }
    $PSSource = "$($prefix)powershell.exe -EncodedCommand $EncodedCommand"
    foreach ($vm in $vms) {
        Write-Host "Machine $vmnum of $vmcount - $($vm.Name)" -ForegroundColor Cyan
        try {
            if ($vm.StorageProfile.OsDisk.OsType -eq 'Windows') {
                $prefix = '. '
            } else {
                $prefix = ''
            }
            $AzVMRunCommand = @{
                ResourceGroupName = $VM.ResourceGroupName
                VMName            = $VM.Name
                RunCommandName    = $RunCommandName
                SourceScript      = $PSSource
                Location          = $VM.Location
                AsJob             = $true
                ErrorAction       = 'Stop'
            }
            Write-Host "Submitting runcommand to machine: $Name" -ForegroundColor Cyan
            $SetCmd = Set-AzVMRunCommand @AzVMRunCommand
            [pscustomobject]@{
                ResourceId        = $VM.Id
                ResourceGroupName = $VM.ResourceGroupName
                Name              = $VM.Name
                CommandName       = $RunCommandName
                State             = $SetCmd.State
                Type              = 'VM'
            }
            Write-Host ""
            $jobstate = $($setcmd.JobStateInfo).State
            $counter = 0
            while (($counter -lt $tryCount) -and ($jobstate -eq 'Running')) {
                Write-Host "JobState: $jobstate - (pause 10 seconds)..."
                Start-Sleep -Seconds $waitSeconds
                $jobstate = $($setcmd.JobStateInfo).State
                $counter++
            }
            Write-Host "JobState: $jobstate"
            if ($jobstate -eq 'Completed') {
                Write-Host "Getting job output..."
                $rest = Invoke-AzRestMethod -Path "$($VM.Id)/runCommands/$($RunCommandName)?`$expand=instanceView&api-version=2022-11-01" -Method GET
                if ($rest.StatusCode -eq 200) {
                    if (($rest.Content | ConvertFrom-Json).Properties.provisioningState -eq 'Succeeded') {
                        Write-Host "Returning output..." -ForegroundColor Cyan
                        $iview = Get-AzVMRunCommand -ResourceGroupName $vm.ResourceGroupName -VMName $vm.name -RunCommandName $runcommandname -Expand InstanceView
                        $iview.InstanceView.Output
                    }
                } else {
                    $msg = "StatusCode: $($rest.StatusCode)"
                    if ($rest.StatusCode -eq 404) {
                        $msg += " - verify you have access (Contributor/Owner) and check if your PIM session is still active"
                    }
                    throw $msg
                }
            }
        } catch {
            [pscustomobject]@{
                Status   = 'Error'
                Activity = $($_.CategoryInfo.Activity -join (";"))
                Message  = $($_.Exception.Message -join (";"))
                Trace    = $($_.ScriptStackTrace -join (";"))
                RunAs    = $($env:USERNAME)
                RunOn    = $($env:COMPUTERNAME)
            }
        }
        $vmnum++
    }
}