<< Back to Script Library

AZ Resize machine

Change the size of the Azure VM. Specify the name, or partial name, of a VM size such as B2s but note that the name of the new size must be unique.
IF THE TARGET MACHINE IS ON THIS SCRIPT CAN TRIGGER A RESTART!

Note that costs of this VM once updated could be very different.
Version: 2.3.13
Created: 2021-11-09
Modified: 2022-03-17
Creator: Guy Leech
Downloads: 11
Tags: azure machine resize
The Script Copy Script Copied to clipboard
#require -version 3.0

<#
.SYNOPSIS
    Change the size of the Azure VM passed

.DESCRIPTION
    Using REST API calls

.PARAMETER azid
    The relative URI of the Azure VM
    
.PARAMETER newSize
    The SKU to change the VM to use

.NOTES
    Version:        0.1
    Author:         Guy Leech, BSc based on code from Esther Barthel, MSc
    Creation Date:  2021-11-09
    Updated:
#>

[CmdletBinding()]

Param
(
    [string]$AZid ,## passed by CU as the URL to the VM minus the FQDN
    [string]$AZtenantId ,
    [ValidateSet('Yes','No','True','False')]
    [string]$confirmAction = 'No' , 
    [string]$resizeTo
)

$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]$insightsApiVersion = '2015-04-01'
[string]$baseURL = 'https://management.azure.com'
[string]$credentialType = 'Azure'

Write-Verbose -Message "AZid is $AZid"

#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

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]
    
    [string]$subscriptionId = $null
    [string]$resourceGroupName = $null

    ## subscriptions/58ffa3cb-2f63-4f2e-a06d-369c1fcebbf5/resourceGroups/WVD/providers/Microsoft.Compute/virtualMachines/GLMW10WVD-0
    if( $AZid -match '\bsubscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\.' )
    {
        $subscriptionId = $Matches[1]
        $resourceGroupName = $Matches[2]
    }
    else
    {
        Throw "Failed to parse subscription id and resource group from $AZid"
    }

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

    if( $availableSizes = Invoke-AzureRestMethod -BearerToken $azBearerToken -uri "$baseURL/$azid/vmSizes?api-version=$computeApiVersion" )
    {
        Write-Verbose -Message "$($availableSizes.Count) available VM sizes for $($virtualMachine.Name), current size is $($virtualMachine.properties.hardwareProfile.vmSize)"
        if( -Not ( $newSize = $availableSizes.Where( { $_.name -match $resizeTo } ) ) )
        {
            Throw "Unable to find VM size $resizeTo"
        }
        elseif( $newSize.Count -gt 1 )
        {
            Throw -Message "Found $($newSize.Count) VM sizes matching $resizeTo - $(($newSize | Select-Object -ExpandProperty name) -join ',')"
        }
        elseif( $virtualMachine.properties.hardwareProfile.vmSize -eq $newSize.name )
        {
            Throw "VM $($virtualMachine.name) is already size $($virtualMachine.properties.hardwareProfile.vmSize)"
        }
        else
        {
            if( $confirmAction -ieq 'yes' -or $confirmAction -ieq 'true' )
            {
                ## if we are in session 0 then do not prompt
                [int]$thisSession = Get-Process -Id $pid -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SessionId
                if( $thisSession -eq 0 )
                {
                    Write-Warning -Message "Action not confirmed by user - runnning in session 0 so unable to prompt"
                    Exit 2
                }
                else
                {
                    Add-Type -AssemblyName PresentationCore,PresentationFramework
                    ## script must be running on console for this to work otherwise will hang indefinitely
                    $answer = [Windows.MessageBox]::Show( "Are you sure you want to change $vmName from $($virtualMachine.properties.hardwareProfile.vmSize) to $($newSize.Name)" , "Confirm Resize Operation" , 'YesNo' ,'Question' )
                    if( $answer -ine 'Yes' )
                    {
                        Write-Warning -Message "Action not confirmed by user - aborting"
                        Exit 1
                    }
                }
            }
            ## https://docs.microsoft.com/en-us/rest/api/compute/virtual-machines/create-or-update
            [hashtable]$body = @{
                'location' = $virtualMachine.location
                'properties' = @{
                    'hardwareprofile' = @{
                        'vmSize' = $newSize.Name
                    }
                }
            }
            if( -Not ( $resized = Invoke-AzureRestMethod -BearerToken $azBearerToken -uri "$baseURL/$azid/?api-version=$computeApiVersion" -property $null -body $body -method PUT ) )
            {
                Write-Error -Message "Error when changing $($virtualMachine.Name) from $($virtualMachine.properties.hardwareProfile.vmSize) to $($newSize.name)"
            }
            elseif( $resized.properties.hardwareProfile.vmSize -ne $newSize.name )
            {
                Write-Error -Message "Failed to change $($virtualMachine.Name) from $($virtualMachine.properties.hardwareProfile.vmSize) to $($newSize.name), it is now $($resized.properties.hardwareProfile.vmSize)"
            }
            else
            {
                Write-Output -InputObject "Changed from $($virtualMachine.properties.hardwareProfile.vmSize) to $($resized.properties.hardwareProfile.vmSize) ok"
            }
        }
    }
    else
    {
        Write-Warning -Message "Failed to get available VM sizes for $vmname"
    }
}