StartStop-VirtualMachine.ps1

# Function to handle power state assertion for resource manager VM
function AssertResourceManagerVirtualMachinePowerState {
    param(
        [Object]$VirtualMachine,
        [string]$DesiredState,
        [bool]$Simulate
    )
    # Get VM with current status
    $resourceManagerVM = Get-AzVM -ResourceGroupName $VirtualMachine.ResourceGroupName -Name $VirtualMachine.Name -Status
    $currentStatus = $resourceManagerVM.Statuses | Where-Object Code -Like 'PowerState*'
    $currentStatus = $currentStatus.Code -replace 'PowerState/', ''

    # If should be started and isn't, start VM
    if ($DesiredState -eq 'Started' -and $currentStatus -notmatch 'running') {
        if ($Simulate) {
            Write-Host "Az[$($VirtualMachine.Name)]: SIMULATION -- Would have started VM. (No action taken)"
        }
        else {
            Write-Output "[$($VirtualMachine.Name)]: Starting VM"
            $status = $resourceManagerVM | Start-AzVM -ErrorAction Continue;
            $status
            if ($status.Status -match 'Succ') {
                Write-Host "Starting VM: [$AzureVM] - Succeeded!"
            }
            else {
                Write-Host "##[error]Starting VM: [$AzureVM] - Failed!"
            }
        }
    }

    # If should be stopped and isn't, stop VM
    elseif ($DesiredState -eq 'StoppedDeallocated' -and $currentStatus -ne 'deallocated') {
        if ($Simulate) {
            Write-Host "Az[$($VirtualMachine.Name)]: SIMULATION -- Would have stopped VM. (No action taken)"
        }
        else {
            Write-Output "[$($VirtualMachine.Name)]: Stopping VM"
            $status = $resourceManagerVM | Stop-AzVM -Force -ErrorAction Continue;
            $status
            if ($status.Status -match 'Succ') {
                Write-Host "Stopping VM: [$AzureVM] - Succeeded!"
            }
            else {
                Write-Host "##[error]Stopping VM: [$AzureVM] - Failed!"
            }
        }
    }
    # Otherwise, current power state is correct
    else {
        Write-Host "[$($VirtualMachine.Name)]: Current power state [$currentStatus] is correct."
    }
}
function StartStop-AzureVMs {
    Param
    (
        # Action to perform (startup or shutdown)
        [string][Parameter(Mandatory = $true)]$Action,
        # Resource group where the vm belongs to
        [string][Parameter(Mandatory = $false)]$ResourceGroup,
        # The list of the VM's to perform action to (separate by ',')
        # [string][Parameter(Mandatory = $false)]$VMList,
        [string][Parameter(Mandatory = $false)]$vmsExcludedFromAction,
        [bool]$Simulate

    )
    # Suppress Warnings
    Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings 'true'
    # Authenticate with your Automation Account
    # $Conn = Get-AutomationConnection -Name AzureRunAsConnection
    # Add-AzAccount -ServicePrincipal -Tenant $Conn.TenantID -ApplicationID $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint
    $AzureVMsToHandle = @()
    $AzureVMs = @()
    $AzureVMs = (Get-AzVM $ResourceGroup).Name
    $AzureVMsToHandle = $AzureVMs

    # Get the existing VM's
    [Object]$VMs = (Get-AzVM $ResourceGroup)
    #Shut down or Start up VM's
    ('Exceution initiated...')
    Write-Host "List of VMs to handle: $($AzureVMsToHandle)"
    Write-Host "Performing $($Action)"
    if ($Action -eq 'Start') {
        foreach ($AzureVM in $AzureVMsToHandle) {
            if ($VMs | Where-Object { $_.Name -eq $AzureVM }) {
                if ($vmsExcludedFromAction -match ($AzureVM)) {
                    #for skipping array of VMs 'VM1','VM2' etc
                    Write-Host " ---> Skipping VM: [$AzureVM]"
                }
                else {
                    # Readiness parameters
                    Write-Host "Performing [$($Action)] for [$($AzureVM)] resource under [$($ResourceGroup)] RG"
                    $instance = ($VMs | Where-Object { ($_.Name -eq $AzureVM) })
                    $status = AssertResourceManagerVirtualMachinePowerState -VirtualMachine $instance -DesiredState 'Started' -Simulate $Simulate
                    $status
                    # $status = $VMs | Where-Object { ($_.Name -eq $AzureVM) } | Start-AzVM -ErrorAction Continue;
                }
            }
            else {
                Write-Host "##[error]AzureVM : [$AzureVM] - Does not exist! - Check your inputs!"
            }
        }
    }
    elseif ($Action -eq 'Stop') {
        foreach ($AzureVM in $AzureVMsToHandle) {
            if ($vmsExcludedFromAction -match ($AzureVM)) {
                #for skipping array of VMs 'VM1','VM2' etc
                Write-Host " ---> Skipping VM: [$AzureVM]"
            }
            else {
                # $status = $VMs | Where-Object { ($_.Name -eq $AzureVM) } | Stop-AzVM -Force -ErrorAction Continue;
                # Readiness parameters
                Write-Host "Performing [$($Action)] for [$($AzureVM)] resource under [$($ResourceGroup)] RG"
                $instance = ($VMs | Where-Object { ($_.Name -eq $AzureVM) })
                $status = AssertResourceManagerVirtualMachinePowerState -VirtualMachine $instance -DesiredState 'StoppedDeallocated' -Simulate $Simulate
                $status
            }
        }
    }
    elseif ($Action -eq 'restart') {
        foreach ($AzureVM in $AzureVMsToHandle) {
            if ($vmsExcludedFromAction -match ($AzureVM)) {
                #for skipping array of VMs 'VM1','VM2' etc
                Write-Host " ---> Skipping VM: [$AzureVM]"
            }
            else {
                # Readiness parameters
                Write-Host "Performing [$($Action)] for [$($AzureVM)] resource under [$($ResourceGroup)] RG"
                $status = $VMs | Where-Object { ($_.Name -eq $AzureVM) } | Restart-AzVM -ErrorAction Continue;
                $instance = ($VMs | Where-Object { ($_.Name -eq $AzureVM) })
                if ($status.Status -match 'Succ') {
                    Write-Host "Retarting VM: [$AzureVM] - Succeeded!"
                }
                else {
                    Write-Host "##[error]Retarting VM: [$AzureVM] - Failed!"
                }
            }
        }
    }
    else {
        Write-Host "##[error]Action: [$Action] - Input Error! - Check your inputs! (Only 'start', 'stop' and 'restart' are allowed!)" -TargetObject $_
    }
}
Function StartStop-VirtualMachine {
    <#
        .SYNOPSIS
        .\StartStop-VirtualMachine.ps1.
 
        .DESCRIPTION
        You can START or STOP VM(s) with this command.
        It takes ResourceGroup or array of VMs are arguments.
        You can exclude list of VMs from the RG using vmsExcludedFromAction parameter.
 
        .PARAMETER Action
        Specifies the action (Start or Stop) to perform on VM(s).
 
        .PARAMETER ResourceGroupName
        Specifies the name of ResourceGroup. "RG1" or "RG1,RG2,RGn".
 
        .PARAMETER VMList
        Specifies the name of virtual machine(s). "VM1" or "VM1,VM2,VMn".
 
        .PARAMETER vmsExcludedFromAction
        Specifies the name of virtual machine(s) that you want to SKIP from action.
        "VM1" or "VM1,VM2,VMn".
 
        .PARAMETER Simulate
        Use this switch parameter to perform WhatIf action.
 
        .INPUTS
        None. You cannot pipe objects to command.
 
        .OUTPUTS
        System.String. command returns a string with the status of action performed on workload.
 
        .EXAMPLE
        PS> StartStop-VirtualMachine -Action Stop -ResourceGroupName mytestrg1984 -Simulate
 
        # Command simulate the action on the RG mytestrg1984 for all VMs.
        *** Running in SIMULATE mode. No actions will be performed. ***
        Performing [stop] for resource group [mytestrg1984]
        ---------------------------------------------------------------------------
        Exceution initiated...
        List of VMs to handle: mytestvm1 mytestvm2
        Performing stop
        Performing [stop] for [mytestvm1] resource under [mytestrg1984] RG
        Az[mytestvm1]: SIMULATION -- Would have stopped VM. (No action taken)
        Performing [stop] for [mytestvm2] resource under [mytestrg1984] RG
        Az[mytestvm2]: SIMULATION -- Would have stopped VM. (No action taken)
        ---------------------------------------------------------------------------
        Validating all VM status
        ---------------------------------------------------------------------------
        mytestvm1 | PowerState/running
        ---------------------------------------------------------------------------
        ---------------------------------------------------------------------------
        mytestvm2 | PowerState/running
        ---------------------------------------------------------------------------
 
        .EXAMPLE
        PS> StartStop-VirtualMachine -Action Stop -ResourceGroupName mytestrg1984
        # Command perform the action on the RG mytestrg1984 for all VMs.
        *** Running in LIVE mode. Operation will be enforced. ***
        Performing [stop] for resource group [mytestrg1984]
        ---------------------------------------------------------------------------
        Exceution initiated...
        List of VMs to handle: mytestvm1 mytestvm2
        Performing stop
        Performing [stop] for [mytestvm1] resource under [mytestrg1984] RG
        Stopping VM: [mytestvm1] - Succeeded!
        [mytestvm1]: Stopping VM
 
        OperationId : bfc3844a-c88e-47cd-98e9-fd31cbb5ee5c
        Status : Succeeded
        StartTime : 1/19/2023 2:16:17 PM
        EndTime : 1/19/2023 2:17:06 PM
        Error :
 
        Performing [stop] for [mytestvm2] resource under [mytestrg1984] RG
        Stopping VM: [mytestvm2] - Succeeded!
        [mytestvm2]: Stopping VM
        OperationId : c050e8d2-3749-4fc1-a5bd-4550154f1e81
        Status : Succeeded
        StartTime : 1/19/2023 2:17:07 PM
        EndTime : 1/19/2023 2:17:54 PM
        Error :
 
        ---------------------------------------------------------------------------
        Validating all VM status
        ---------------------------------------------------------------------------
        mytestvm1 | PowerState/deallocated
        ---------------------------------------------------------------------------
        ---------------------------------------------------------------------------
        mytestvm2 | PowerState/deallocated
        ---------------------------------------------------------------------------
        .EXAMPLE
        PS> StartStop-VirtualMachine -Action Stop -ResourceGroupName mytestrg1984 -vmsExcludedFromAction mytestvm1
 
        # Excluding the VM from the RG. In the example we are excluding the operation on mytestvm1
 
        *** Running in LIVE mode. Operation will be enforced. ***
        Performing [start] for resource group [mytestrg1984]
        ---------------------------------------------------------------------------
        Exceution initiated...
        List of VMs to handle: mytestvm1 mytestvm2
        Performing start
         ---> Skipping VM: [mytestvm1]
        Performing [start] for [mytestvm2] resource under [mytestrg1984] RG
        Starting VM: [mytestvm2] - Succeeded!
        [mytestvm2]: Starting VM
 
        OperationId : 57528881-9450-4098-a7a4-ef13ccd17e8b
        Status : Succeeded
        StartTime : 1/19/2023 2:29:53 PM
        EndTime : 1/19/2023 2:30:03 PM
        Error :
 
        ---------------------------------------------------------------------------
        Validating all VM status
        ---------------------------------------------------------------------------
        mytestvm1 | PowerState/deallocated
        ---------------------------------------------------------------------------
        ---------------------------------------------------------------------------
        mytestvm2 | PowerState/running
        ---------------------------------------------------------------------------
 
        .LINK
        Donation link : https://www.paypal.com/paypalme/aammirmirza/7
    #>


    Param
    (
        # Action to perform (startup or shutdown)
        [string][Parameter(Mandatory = $true)]$Action,
        # Resource group where the vm belongs to
        [string][Parameter(Mandatory = $false)]$ResourceGroupName,
        # The list of the VM's to perform action to (separate by ',')
        [string][Parameter(Mandatory = $false)]$VMList,
        [string][Parameter(Mandatory = $false)]$vmsExcludedFromAction,
        [switch]$Simulate
    )
    # Connect using a Managed Service Identity
    try {
        $AzureContext = (Connect-AzAccount -Identity).context
    }
    catch {
        Write-Output 'There is no system-assigned user identity. Aborting. Setu the same or try using RunAs account automation method.';
    }
    ## Signature
    $t = @"
Extension designed and managed
     _ _ _
    | |__ _ _ /_\ __ _ _ __ (_) _ _
    | '_ \| || | / _ \ / _` || ' \ | || '_|
    |_.__/ \_, | /_/ \_\\__,_||_|_|_||_||_|
           |__/
            Azure DevOps extensions for cost savings.
            Optimize virtual machine spend by
            shutting down underutilized instances.
 
            Please suggest improvements at aammir.mirza@hotmail.com
 
"@

    for ($i = 0; $i -lt $t.length; $i++) {
        if ($i % 2) {
            $c = 'blue'
        }
        elseif ($i % 5) {
            $c = 'blue'
        }
        elseif ($i % 7) {
            $c = 'blue'
        }
        else {
            $c = 'blue'
        }
        Write-Host $t[$i] -NoNewline -ForegroundColor $c
    }
    ################################################################

    # Main script content
    try {
        if ($Simulate) {
            Write-Host 'Az*** Running in SIMULATE mode. No actions will be performed. ***'
        }
        else {
            Write-Host 'Az*** Running in LIVE mode. Operation will be enforced. ***'
        }
        ###########################################################
        # if ($VMList) {
        # if (!$ResourceGroupName) {
        # Write-Host "##[error]Missing Argument : ResourceGroup name must be provided. Listed Vms should be part of single RG."
        # }
        # # StartStop-AzureVMs -Action $Action -resourceGroup $ResourceGroupName -VMList $VMList -vmsExcludedFromAction $vmsExcludedFromAction
        # Write-Host "AzPerforming [$($Action)] for resource group [$($ResourceGroupName)]"
        # ('-' * 75)
        # StartStop-AzureVMs -Action $Action -resourceGroup $ResourceGroupName -VMList $VMList -vmsExcludedFromAction $vmsExcludedFromAction -Simulate $Simulate
        # ('-' * 75)
        # }
        # elseif ($ResourceGroupName) {
        if ($ResourceGroupName) {
            $AzureRGs = $ResourceGroupName.Split(',')
            $AzureRGsToHandle = @($AzureRGs.replace(' ' , '').replace("'" , '').replace('"' , ''))
            foreach ($rg in $AzureRGsToHandle) {
                Write-Host "Performing [$($Action)] for resource group [$($rg)]" -ForegroundColor Blue
            ('-' * 75)
                StartStop-AzureVMs -Action $Action -ResourceGroup $rg -vmsExcludedFromAction $vmsExcludedFromAction -Simulate $Simulate
            ('-' * 75)
                try {
                    Write-Host 'Validating all VM status' -ForegroundColor Blue
                    $VMs = (Get-AzVM -ResourceGroupName $rg)
                    foreach ($currentVM in $VMs) {
                    ('-' * 75)
                        $vmStatus = (Get-AzVM -ResourceGroupName $rg -Name $currentVM.Name -status)
                        $vmStatus.Name + ' | ' + (($vmStatus).Statuses[1].Code)
                    ('-' * 75)
                    }
                }
                catch {
                    Write-Warning 'Issues with Get-AzVM command to fetch the current status of all VMs for validation'
                }
            }
        }
        else {
            Write-Warning 'You can provide list of RGs (array) or single RG. E.g: "RG1,RG2,RGn"'
            Write-Error 'Mandatory ResourceGroup parameter missing.'
        }
    }
    catch {
        $errorMessage = $_.Exception.Message
        throw "Unexpected exception: $errorMessage"
    }
}