AZ Show machine network information

Get and display network info for specified Azure machine
Version 3.9.16
Created on 2021-12-10
Modified on 2022-10-23
Created by Guy Leech
Downloads: 94

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

<#
.SYNOPSIS
    Get and display network info for specified VM

.DESCRIPTION
    Using REST API calls

.PARAMETER AZid
    The relative URI of the Azure VM

.PARAMETER AZtenantId
    The tenanit id containing the Azure VM
    
.NOTES
    Version:        0.1
    Author:         Guy Leech, BSc based on code from Esther Barthel, MSc
    Creation Date:  2021-10-30
    Updated:        2022-01-17  Guy Leech    Fix for tenant id handling
                    2022-01-17  Guy Leech    Fix for errors from missing public IP address properties when VM deallocated
#>

[CmdletBinding()]

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

$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]$computeApiVersion = '2021-07-01'
[string]$networkApiVersion = '2021-05-01'
[string]$baseURL = 'https://management.azure.com'
[string]$credentialType = 'Azure'
[int]$rdpPort = 3389

Write-Verbose -Message "AZid is $AZid in tenant $AZtenantId"

#region AzureFunctions
function Get-AzSPStoredCredentials {
    <#
    .SYNOPSIS
        Retrieve the Azure Service Principal Stored Credentials.
    .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
    #>
    [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 $AZtenantId 
    }
    
    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.
    .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
    #>
    [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
    )
    
    ## https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
    [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','DELETE')] ## add others as necessary
        [string]$method = 'GET' ,
        $body , ## not typed because could be hashtable or pscustomobject
        [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 -Depth 20))
    }

    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

#region OtherFunctions

<#
.SYNOPSIS
    Test if an IPv4 address is within the given CIDR range

.PARAMETER cidr
    The CIDR to test

.PARAMETER address
    The IP address to test against the CIDR specified

.EXAMPLE
    Test-IPRangeFromCIDR -cidr "192.168.2.1/28" -address 192.168.2.10

    Test if the specified IP address is contained within the given CIDR range

.NOTES

    Modification History:

    2021/11/03  @guyrleech  Initial Release
#>

Function Test-IPRangeFromCIDR
{
    [cmdletbinding()]

    Param
    (
        [Parameter(Mandatory=$true,HelpMessage='IP address range as CIDR')]
        [string]$cidr ,
        [Parameter(Mandatory=$true,HelpMessage='IP address to check in range')]
        [ipaddress]$address
    )

    [ipaddress]$startAddress = [ipaddress]::Any
    [ipaddress]$endAddress   = [ipaddress]::Any

    if( Get-IPRangeFromCIDR -cidr $cidr -startAddress ([ref]$startAddress) -endAddress ([ref]$endAddress) )
    {
        [byte[]]$bytes = $address.GetAddressBytes()
        [uint64]$addressToCompare =  ( ( [uint64]$bytes[0] -shl 24) -bor ( [uint64]$bytes[1] -shl 16) -bor ( [uint64]$bytes[2] -shl 8) -bor  [uint64]$bytes[3])
        $bytes = $startAddress.GetAddressBytes()
        [uint64]$startAddressToCompare =  ( ( [uint64]$bytes[0] -shl 24) -bor ( [uint64]$bytes[1] -shl 16) -bor ( [uint64]$bytes[2] -shl 8) -bor  [uint64]$bytes[3])
        $bytes = $endAddress.GetAddressBytes()
        [uint64]$endAddressToCompare =  ( ( [uint64]$bytes[0] -shl 24) -bor ( [uint64]$bytes[1] -shl 16) -bor ( [uint64]$bytes[2] -shl 8) -bor  [uint64]$bytes[3])

        $addressToCompare -ge $startAddressToCompare -and $addressToCompare -le $endAddressToCompare ## return
    }
}

<#
.SYNOPSIS
    Take a CIDR (Classless Inter-Domain Routing) notation IP v4 range and returns the first and last IPv4 addresses in the range

.PARAMETER cidr
    The CIDR to convert

.PARAMETER startAddress
    Will be set to the start address of the range if the CIDR is valid
    
.PARAMETER endAddress
    Will be set to the end address of the range if the CIDR is valid

.EXAMPLE
    Get-IPRangeFromCIDR -cidr "192.168.2.1/28" -Verbose -startAddress ([ref]$start) -endAddress ([ref]$end)

    Get the starting and ending IPv4 addresses of the specified CIDR range

.NOTES
    Results compared with https://mxtoolbox.com/SubnetCalculator.aspx

    Modification History:

    2021/11/03  @guyrleech  Initial Release
#>

Function Get-IPRangeFromCIDR
{
    [cmdletbinding()]

    Param
    (
        [Parameter(Mandatory=$true,HelpMessage='IP address range as CIDR')]
        [string]$cidr ,
        [Parameter(Mandatory=$true,HelpMessage='IP address range start result')]
        [ref]$startAddress ,
        [Parameter(Mandatory=$true,HelpMessage='IP address range end result')]
        [ref]$endAddress
    )

    [string]$ipaddressPart , [int]$bitsPart = $cidr -split '/'

    if( $bitsPart -eq $null -or $bitsPart -le 0 -or $bitsPart -gt 32 )
    {
        Write-Error -Message "/$bitsPart is invalid"
        return $false
    }

    if( -Not ( $ipaddress = $ipaddressPart -as [ipaddress] ))
    {
        Write-Error -Message "IP address $ipaddressPart is invalid"
        return $false
    }

    [uint64]$mask = ([int64][System.Math]::Pow( 2 , (32 - $bitsPart) ) - 1)
    [byte[]]$bytes = $ipaddress.GetAddressBytes()
    [uint64]$octets =  ( ( [uint64]$bytes[0] -shl 24) -bor ( [uint64]$bytes[1] -shl 16) -bor ( [uint64]$bytes[2] -shl 8) -bor  [uint64]$bytes[3])
    [uint64]$start = $octets -band ($mask -bxor 0xffffffff)
    [uint64]$end = $octets -bor $mask

    $startAddress.Value = [ipaddress]$start
    $endAddress.Value   = [ipaddress]$end

    return $true
}

Function Wait-ProvisioningComplete
{
    [CmdletBinding()]

    Param
    (
        [Parameter(Mandatory=$true)]
        [string]$bearerToken ,
        [Parameter(Mandatory=$true)]
        [string]$uri ,
        [int]$sleepMilliseconds = 3000 ,
        [int]$waitForSeconds = 60 
    )

    [datetime]$start = [datetime]::Now
    [datetime]$end = $start.AddSeconds( $waitForSeconds )

    do
    {
        if( ( $state = Invoke-AzureRestMethod -BearerToken $bearerToken -uri $uri -property 'properties') )
        {
            if( -Not $state.PSObject.properties[ 'provisioningState' ] )
            {
                Write-Warning -Message "No state property on response from $uri - $state"
            }
            elseif( $state.provisioningState -eq 'Succeeded' )
            {
                break
            }
            elseif( $state.provisioningState -eq 'Failed' )
            {
                Write-Error -Message "Provisioning failed for $uri"
                break
            }
        }
        else
        {
            Write-Warning -Message "Failed call to $uri"
        }
        Write-Verbose -Message "$(Get-Date -Format G) : provisioning state of $uri is $($state | Select-Object -ExpandProperty provisioningState -ErrorAction SilentlyContinue) so waiting $sleepMilliseconds ms"
        Start-Sleep -Milliseconds $sleepMilliseconds
    } while( [datetime]::Now -le $end )

    if( $state -and $state.PSObject.properties[ 'provisioningState' ] -and $state.provisioningState -eq 'Succeeded' )
    {
        $state ## return
    }
    ## else not succeeded so implicitly return $null
}

#endregion OtherFunctions

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"
    }

    [string]$vmName = ($AZid -split '/')[-1]

    if( -Not ( $vm = Invoke-AzureRestMethod -BearerToken $azBearerToken -uri "$baseURL/$azid/?api-version=$computeApiVersion" -property $null ) )
    {
        Throw "Failed to get VM for $azid"
    }

    if( ! [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/instance-view
    if( -Not ( $virtualMachineStatus = Invoke-AzureRestMethod -BearerToken $azBearerToken -uri "$baseURL/$azid/instanceView`?api-version=$computeApiVersion" -property 'statuses' ) )
    {
        Write-Warning "Failed to get VM instance view for $azid"
    }
        
    ## get its networking so we can see if it already has a public IP
    if( -Not ( [array]$networkInterfaces = @( $vm.properties | Select-Object -ExpandProperty networkProfile | Select-Object -ExpandProperty networkInterfaces | Select-Object -ExpandProperty Id ) ))
    {
        Throw "VM $vmName has no network interfaces"
    }
    
    Write-Verbose -Message "Got $($networkInterfaces.Count) network interfaces"

    [array]$networkInfo = @( ForEach( $networkInterface in $networkInterfaces )
    {
        $result = [pscustomobject]@{ 'Interface' = Split-Path -Path $networkInterface -Leaf }

        if( ( $thisNetworkInterface = Invoke-AzureRestMethod -BearerToken $azBearerToken -uri "$baseURL/$networkInterface/?api-version=$networkApiVersion" -property 'properties' ) )
        {
            if( $IPproperties = $thisNetworkInterface.ipConfigurations | Select-Object -ExpandProperty properties )
            {
                if( $IPproperties.provisioningState -ne 'Succeeded' )
                {
                    Write-Warning -Message "Provisioning state of $networkInterface is $($IPproperties.provisioningState)"
                }
                
                ## don't break so we can get all application security groups for all the VMs NICs as they may appear in network security group rules which we check later for port 3389 access
                $IPproperties | Select-Object -ExpandProperty applicationSecurityGroups -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id | Select-Object -Unique | ForEach-Object `
                {
                    [string]$applicationSecurityGroupName = ($_ -split '/')[-1]
                    try
                    {
                        $applicationSecurityGroups.Add( $applicationSecurityGroupName , $true )
                    }
                    catch
                    {
                        ## already have it which doesn't matter
                    }
                }
                Add-Member -InputObject $result -NotePropertyMembers @{
                    'Nic Type' = $thisNetworkInterface.nicType
     'Accelerated Networking' = $thisNetworkInterface.enableAcceleratedNetworking
                    'Subnet' = (( $IPproperties |Select-Object -ExpandProperty subnet -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id -ErrorAction SilentlyContinue) -split '/') | Select-Object -Last 1
                    'Private IP' = $IPproperties | Select-Object -ExpandProperty privateIPAddress
                    'Private IP Allocation' = $IPproperties | Select-Object -ExpandProperty privateIPAllocationMethod
                    'Private IP Type' = $IPproperties | Select-Object -ExpandProperty privateIPAddressVersion
                    'MAC Address' = $thisNetworkInterface.macAddress
                    'Network Security Group' = (( $thisNetworkInterface | Select-Object -ExpandProperty networkSecurityGroup -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id -ErrorAction SilentlyContinue) -split '/') | Select-Object -Last 1
                }
                
                if( $IPproperties.PSObject.properties[ 'publicIPAddress' ] )
                {
                    if( ( $thisPublicIpAddressDetail = Invoke-AzureRestMethod -BearerToken $azBearerToken -uri "$baseURL$($IPproperties.publicIPAddress.Id)`?api-version=$networkApiVersion" -property $null ) )
                    {
                        Add-Member -InputObject $result -NotePropertyMembers @{
                            'Public IP' = $thisPublicIpAddressDetail.properties | Select-Object -ExpandProperty ipAddress -ErrorAction SilentlyContinue
                            'Public IP Allocation' = $thisPublicIpAddressDetail.properties.publicIPAllocationMethod
                            'Public IP Type' = $thisPublicIpAddressDetail.properties.publicIPAddressVersion
                            'Public IP Location' = $thisPublicIpAddressDetail.location
                            'Public IP SKU' = $thisPublicIpAddressDetail.sku.name
                            'Public IP Tier' = $thisPublicIpAddressDetail.sku.tier
                            'Public IP Timeout' = "$($thisPublicIpAddressDetail.properties.idleTimeoutInMinutes) minutes"
                            'Public IP Tags' = $thisPublicIpAddressDetail | Select-Object -ExpandProperty Tags -ErrorAction SilentlyContinue
                        }
                    }
                    else
                    {
                        Write-Warning -Message "Failed to get details of public IP address $($thisPublicIpAddress.Id)"
                    }
                }
            }
        }
        else
        {
            Write-Warning -Message "Failed to get properties for network interface $networkInterface"
        }
        $result
    })

    Write-Output "Details for $($networkInfo.Count) network interfaces, $(($virtualMachineStatus | Where-Object code -match '^PowerState/' | Select-Object -ExpandProperty code) -replace '/' , ' is ') :"

    $networkInfo | Format-List
}