AZ Get console screenshot

Displays the console of the selected machine or stores the console image into a folder defined in the script action arguments
Version 2.10.29
Created on 2021-09-17
Modified on 2022-03-17
Created by Guy Leech
Downloads: 31

The Script Copy Script Copied to clipboard
#require -version 3.0

<#
.SYNOPSIS
    Get Azure VM console screenshots

.DESCRIPTION
    Using REST API calls

.PARAMETER azid
    The relative URI of the Azure VM

.PARAMETER AZtenantId
    Optional Azure tenant id. Specify when there is a need to access multiple tenants with different credentials.

.NOTES
    Version:        0.1
    Author:         Guy Leech, BSc based on code from Esther Barthel, MSc
    Creation Date:  2021-09-15
    Updated:        2021-10-13 Guy Leech  Added support for credential files with tenant id in the file name rather than in the file
                    2021-10-13 Guy Leech  Workaround for VM name in Azid being passed as lowercase which breaks the blob URL
                    2022-02-22 Guy Leech  Look for _AZ_ credentials file if _Azure_ not found. Checks on AZ id and tenant id validity
#>

[CmdletBinding()]

Param
(
    [string]$AZid , ## passed by CU as the URL to the VM minus the FQDN
    [string]$AZtenantId ,
    [string]$screenshotFolder ,
    [string]$screenShotFile
)    

$VerbosePreference = $(if( $PSBoundParameters[ 'verbose' ] ) { $VerbosePreference } else { 'SilentlyContinue' })
$DebugPreference = $(if( $PSBoundParameters[ 'debug' ] ) { $DebugPreference } else { 'SilentlyContinue' })
$ErrorActionPreference = $(if( $PSBoundParameters[ 'erroraction' ] ) { $ErrorActionPreference } else { 'Stop' })
$ProgressPreference = 'SilentlyContinue'

[int]$outputWidth = 250
if( ( $PSWindow = (Get-Host).UI.RawUI ) -and ( $WideDimensions = $PSWindow.BufferSize ) )
{
    $WideDimensions.Width = $outputWidth
    $PSWindow.BufferSize = $WideDimensions
}

[string]$apiversion = '2021-04-01'
[string]$computeApiVersion = '2021-07-01'
[string]$baseURL = 'https://management.azure.com'
[string]$credentialType = 'Azure'

Write-Verbose -Message "AZid is $AZid"

Function Save-ScreenShot
{
    Param
    (
        [Parameter(Mandatory=$true,HelpMessage='Existing screenshot file')]
        [string]$imageFile ,
        [Parameter(Mandatory=$true,HelpMessage='Name of th VM for the screenshot file')]
        [string]$vmname  ,
        [Parameter(Mandatory=$true,HelpMessage='Folder to copy screenshot file to')]
        [string]$folder ,
        [string]$file ,
        [Parameter(Mandatory=$true,HelpMessage='Extension of the screenshot file')]
        [string]$extension ,
        [switch]$report
    )

    $folder = [System.Environment]::ExpandEnvironmentVariables( $folder ) -replace '%vmname%' , $vmname -replace '%month%' , (Get-Date -Format 'MM')  -replace '%year%' , (Get-Date -Format 'yyyy') `
                -replace '%day%' , (Get-Date -Format 'dd') -replace '%hour%' , (Get-Date -Format 'HH')  -replace '%minute%' , (Get-Date -Format 'mm') -replace '%second%' , (Get-Date -Format 'ss')

    if( -Not [string]::IsNullOrEmpty( $file ) )
    {
        $file = [System.Environment]::ExpandEnvironmentVariables( $file ) -replace '%vmname%' , $vmname -replace '%month%' , (Get-Date -Format 'MM')  -replace '%year%' , (Get-Date -Format 'yyyy') `
                -replace '%day%' , (Get-Date -Format 'dd') -replace '%hour%' , (Get-Date -Format 'HH')  -replace '%minute%' , (Get-Date -Format 'mm') -replace '%second%' , (Get-Date -Format 'ss')
    }
    else
    {
        $file = "$vmname.$((Get-Date -Format s) -replace ':' , '-').$extension"
    }
    if( -Not ( Test-Path -Path $folder -ErrorAction SilentlyContinue ) )
    {
        $null = New-Item -Path $folder -ItemType Directory -Force
    }
    if( Copy-Item -Path $imageFile -Destination (Join-Path -Path $folder -ChildPath $file) -PassThru )
    {
        if( $report )
        {
            Write-Output -InputObject "Screenshot file $file saved ok to $folder"
        }
    }
}


#region GUI
Function New-Form
{
    Param
    (
        [Parameter(Mandatory=$true)]
        $inputXaml
    )

    $form = $null
    $inputXML = $inputXaml -replace 'mc:Ignorable="d"' , '' -replace 'x:N' ,'N'  -replace '^<Win.*' , '<Window'
 
    if( [xml]$xaml = $inputXML )
    {
        $reader = New-Object -TypeName Xml.XmlNodeReader -ArgumentList $xaml

        try
        {
            if( $form = [Windows.Markup.XamlReader]::Load( $reader ) )
            {
                $xaml.SelectNodes( '//*[@Name]' ) | ForEach-Object `
                {
                    Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name) -Scope Global
                }
            }
        }
        catch
        {
            Write-Error -Message "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .NET is installed.`n$_"
        }
 
    }
    else
    {
        Write-Error -Message 'Failed to convert input XAML to WPF XML'
    }

    $form ## return
}

Function Show-ImageInWindow
{
    Param
    (
        [Parameter(Mandatory=$true)]
        [string]$filename ,
        [string]$windowTitle
    )
    
    [string]$screenshotXAML = @'
<Window x:Class="VMWare_GUI.Screenshot"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:VMWare_GUI"
        mc:Ignorable="d"
        Title="Screenshot" Height="700" Width="950">
    <Grid>
        <Image x:Name="imgScreenshot" Margin="20,20,20,83"/>
    </Grid>
</Window>
'@
    Add-Type -AssemblyName PresentationCore,PresentationFramework #,System.Windows.Forms

    if( $screenshotWindow = New-Form -inputXaml $screenshotXAML )
    {
        if( $filestream = New-Object System.IO.FileStream -ArgumentList $filename , Open , Read )
        {
            $bitmap = New-Object -Typename System.Windows.Media.Imaging.BitmapImage
            $bitmap.BeginInit()
            $bitmap.StreamSource = $filestream
            $bitmap.EndInit()
            $wpfimgScreenshot.Source = $bitmap
            $screenshotWindow.Title = $windowTitle

            $null = $screenshotWindow.ShowDialog()
            $bitmap.StreamSource = $null
            $filestream.Close()
            $filestream = $null
            $bitmap = $null
        }
        else
        {
            Write-Error -Message "Failed to open image file $filename"
        }
    }
}
#endregion GUI

#region AzureFunctions

function Get-AzSPStoredCredentials {
    <#
    .SYNOPSIS
        Retrieve the Azure Service Principal Stored Credentials.
    .DESCRIPTION
        Retrieve the Azure Service Principal Stored Credentials from a stored credentials file.
    .EXAMPLE
        Get-AzSPStoredCredentials
    .CONTEXT
        Azure
    .NOTES
        Version:        0.1
        Author:         Esther Barthel, MSc
        Creation Date:  2020-08-03
        Purpose:        WVD Administration, through REST API calls
        
        Copyright (c) cognition IT. All rights reserved.
    #>
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$true)]
        [string]$system ,
        [string]$tenantId
    )

    $strAzSPCredFolder = [System.IO.Path]::Combine( [environment]::GetFolderPath('CommonApplicationData') , 'ControlUp' , 'ScriptSupport' )
    $AzSPCredentials = $null

    Write-Verbose -Message "Get-AzSPStoredCredentials $system"

    [string]$credentialsFile = $(if( -Not [string]::IsNullOrEmpty( $tenantId ) )
        {
            [System.IO.Path]::Combine( $strAzSPCredFolder , "$($env:USERNAME)_$($tenantId)_$($System)_Cred.xml" )
        }
        else
        {
            [System.IO.Path]::Combine( $strAzSPCredFolder , "$($env:USERNAME)_$($System)_Cred.xml" )
        })

    Write-Verbose -Message "`tCredentials file is $credentialsFile"

    If (Test-Path -Path $credentialsFile)
    {
        try 
        {
            if( ( $AzSPCredentials = Import-Clixml -Path $credentialsFile ) -and -Not [string]::IsNullOrEmpty( $tenantId ) -and -Not $AzSPCredentials.ContainsKey( 'tenantid' ) )
            {
                $AzSPCredentials.Add(  'tenantID' , $tenantId )
            }
        }
        catch 
        {
            Write-Error -Message "The required PSCredential object could not be loaded from $credentialsFile : $_"
        }
    }
    Elseif( $system -eq 'Azure' )
    {
        ## try old Azure file name 
        $azSPCredentials = Get-AzSPStoredCredentials -system 'AZ' -tenantId $tenantId 
    }

    if( -not $AzSPCredentials )
    {
        Write-Error -Message "The Azure Service Principal Credentials file stored for this user ($($env:USERNAME)) cannot be found at $credentialsFile.`nCreate the file with the Set-AzSPCredentials script action (prerequisite)."
    }
    return $AzSPCredentials
}

function Get-AzBearerToken {
    <#
    .SYNOPSIS
        Retrieve the Azure Bearer Token for an authentication session.
    .DESCRIPTION
        Retrieve the Azure Bearer Token for an authentication session, using a REST API call.
    .EXAMPLE
        Get-AzBearerToken -SPCredentials <PSCredentialObject> -TenantID <string>
    .CONTEXT
        Azure
    .NOTES
        Version:        0.1
        Author:         Esther Barthel, MSc
        Creation Date:  2020-03-22
        Updated:        2020-05-08
                        Created a separate Azure Credentials function to support ARM architecture and REST API scripted actions
        Purpose:        WVD Administration, through REST API calls
        
        Copyright (c) cognition IT. All rights reserved.
    #>
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true, HelpMessage='Azure Service Principal credentials' )]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential] $SPCredentials,

        [Parameter(Mandatory=$true, HelpMessage='Azure Tenant ID' )]
        [ValidateNotNullOrEmpty()]
        [string] $TenantID
    )

    [string]$uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token"
        
    [hashtable]$body = @{
        grant_type    = 'client_credentials'
        client_Id     = $SPCredentials.UserName
        client_Secret = $SPCredentials.GetNetworkCredential().Password
        scope         = "$baseURL/.default"
    }

    [hashtable]$invokeRestMethodParams = @{
        Uri             = $uri
        Body            = $body
        Method          = 'POST'
        ContentType     = 'application/x-www-form-urlencoded'
    }

    Invoke-RestMethod @invokeRestMethodParams | Select-Object -ExpandProperty access_token -ErrorAction SilentlyContinue
}

function Invoke-AzureRestMethod {
   
    [CmdletBinding()]
    Param(
        [Parameter( Mandatory=$true, HelpMessage='A valid Azure bearer token' )]
        [ValidateNotNullOrEmpty()]
        [string]$BearerToken ,
        [string]$uri ,
        [ValidateSet('GET','POST','PUT')] ## add others as necessary
        [string]$method = 'GET' ,
        [hashtable]$body ,
        [string]$property = 'value' ,
        [string]$contentType = 'application/json'
    )

    [hashtable]$header = @{
        'Authorization' = "Bearer $BearerToken"
    }

    if( ! [string]::IsNullOrEmpty( $contentType ) )
    {
        $header.Add( 'Content-Type'  , $contentType )
    }

    [hashtable]$invokeRestMethodParams = @{
        Uri             = $uri
        Method          = $method
        Headers         = $header
    }

    if( $PSBoundParameters[ 'body' ] )
    {
        $invokeRestMethodParams.Add( 'Body' , ( $body | ConvertTo-Json ))
    }
    
    if( -not [String]::IsNullOrEmpty( $property ) )
    {
        Invoke-RestMethod @invokeRestMethodParams | Select-Object -ErrorAction SilentlyContinue -ExpandProperty $property
    }
    else
    {
        Invoke-RestMethod @invokeRestMethodParams ## don't pipe through select as will slow script down for large result sets if processed again after rreturn
    }
}

#endregion AzureFunctions

[string]$vmName = ($AZid -split '/')[-1]
    
if( [string]::IsNullOrEmpty( $vmName ) )
{
    Throw "Azure id `"$AZid`" does not appear valid - failed to find VM name"
}

if( -Not [string]::IsNullOrEmpty( $AZtenantId ) -and -Not ( $AZtenantId -as [guid] ) )
{
    Throw "Azure tenant id `"$AZtenantId`" is invalid"
}

if( ! ( $webClient = New-Object -TypeName System.Net.WebClient ) )
{
    Throw "Failed to create a System.Net.WebClient object"
}

If ($azSPCredentials = Get-AzSPStoredCredentials -system $credentialType -tenantId $AZtenantId )
{
    # Sign in to Azure with a Service Principal with Contributor Role at Subscription level and retrieve the bearer token
    Write-Verbose -Message "Authenticating to tenant $($azSPCredentials.tenantID) as $($azSPCredentials.spCreds.Username)"
    if( -Not ( $azBearerToken = Get-AzBearerToken -SPCredentials $azSPCredentials.spCreds -TenantID $azSPCredentials.tenantID ) )
    {
        Throw "Failed to get Azure bearer token"
    }

    $timeZone = Get-TimeZone
    [string]$currentTimeZone = $timeZone.StandardName
    if( $timeZone.IsDaylightSavingTime([datetime]::Now) )
    {
        $currentTimeZone = $timeZone.DaylightName
    }

    if( ( $vm = Invoke-AzureRestMethod -BearerToken $azBearerToken -uri "$baseURL/$azid/?api-version=$computeApiVersion" -property $null ) -and ! [string]::IsNullOrEmpty( $vm.id ) )
    {
        ## GRL 2021-10-20 appears to be a CU bug/feature that transmogrifies the VM name to lowercase which produces a blob URL that doesn't work so we replace the Az ID with what is returned here
        $AZid = $vm.id
        $vmName = $vm.Name
    }

    ## https://docs.microsoft.com/en-us/rest/api/compute/virtual-machines/retrieve-boot-diagnostics-data
    if( $bootDiagnostics = Invoke-AzureRestMethod -BearerToken $azBearerToken -uri "$baseURL/$azid/retrieveBootDiagnosticsData?api-version=$computeApiVersion" -property $null -method POST )
    {
        if( [string]$screenshotURL = $bootDiagnostics.PSObject.Properties | Where-Object Name -match screenshot | Select-Object -ExpandProperty Value )
        {
            [string]$extension = $screenshotURL -replace '.*\.(\w+)\?.*$' , '$1'
            [string]$tempFile = Join-Path -Path $env:TEMP -ChildPath "$((New-Guid).Guid).$extension"
            [datetime]$retrievedDate = [datetime]::Now
            try
            {
                Write-Verbose -Message "Downloading $screenshotURL to $tempFile"
                $webClient.DownloadFile( $screenshotURL , $tempFile )
            }
            catch
            {
                Write-Warning -Message "Failed to download screenshot from $screenshotURL to $tempfile for VM $vmName : $_"
            }
            if( Test-Path -Path $tempFile )
            {
                if( $PSBoundParameters[ 'screenshotFolder' ] -or $PSBoundParameters[ 'screenShotFile' ] )
                {
                    Save-ScreenShot -imageFile $tempFile -folder $screenshotFolder -file $screenShotFile -extension $extension -vmname $vmName -report
                }
                else
                {
                    Show-ImageInWindow -filename $tempFile -windowTitle "Screenshot of $vmName retrieved at $(Get-Date -Date $retrievedDate -Format G) $currentTimeZone"
                }
                Remove-Item -Path $tempFile
            }
        }
        else
        {
            Write-Warning -Message "Failed to find screenshot URL for VM $vmName"
        }
    }
    else
    {
        Write-Warning -Message "Failed to get boot diagnostics for $vmName"
    }
}