Install-VMInsights.ps1

<#PSScriptInfo
 
.VERSION 1.10.4
 
.GUID 76a487ef-47bf-4537-8942-600a66a547b1
 
.AUTHOR mithomps@microsoft.com
 
.COMPANYNAME Microsoft Corporation
 
.COPYRIGHT Microsoft Corporation. All rights reserved.
 
.TAGS
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
Remove Legacy Agent (LA/MMA/OMS) support. AMA is now the only supported onboarding path.
Remove Dependency Agent (DA) support. -ProcessAndDependencies now throws a retirement error.
 
#>
 

#Requires -Module @{ ModuleName = 'Az.Compute'; ModuleVersion = '7.1.0' }
#Requires -Module @{ ModuleName = 'Az.Resources'; ModuleVersion = '6.12.1' }
#Requires -Module @{ ModuleName = 'Az.Accounts'; ModuleVersion = '2.13.2' }
#Requires -Module @{ ModuleName = 'Az.PolicyInsights'; ModuleVersion = '1.6.4' }
#Requires -Module @{ ModuleName = 'Az.Monitor'; ModuleVersion = '5.0.0' }
#Requires -Module @{ ModuleName = 'Az.ManagedServiceIdentity'; ModuleVersion = '1.2.0' }



<#
.SYNOPSIS
This script installs VM extensions for Azure Monitor Agent (AMA) for VM Insights.
A Data Collection Rule (DCR) and a User Assigned Managed Identity (UAMI) are associated with the VM's and VMSS.
 
.DESCRIPTION
This script installs or re-configures the following on VM's and VMSS under a Subscription.
Azure Monitor Agent along with Data Collection Rule association and User Assigned Managed Identity.
 
 
Scope can further narrowed down to :
- Resource Group
- Specific VM/VMSS
- Policy Assignment Scope
 
Script will show you list of VM's/VMSS that will apply to and let you confirm to continue.
Use -Approve Switch to run without prompting, if all required parameters are provided.
 
Use -WhatIf to get info about expected effect of the commands in the script.
 
.PARAMETER SubscriptionId
Subscription identifier for the VMs or VMSS
If using PolicyAssignmentName parameter, VMs part of the parameter SubscriptionId are considered.
 
.PARAMETER ResourceGroup
<Optional> Name of the Resource Group of VMs or VMSS. It accepts wildcard characters. The default behavior is to match all.
 
.PARAMETER PolicyAssignmentName
<Optional> Only include VMs associated with this policy.
 
 
.PARAMETER Name
<Optional> Name qualifier to match on VM/VMSS's name in the scope. It accepts wildcard characters. The default behavior is to match all.
 
.PARAMETER TriggerVmssManualVMUpdate
<Optional> Set this flag to trigger update of VM instances in a scale set whose upgrade policy is set to Manual.
 
.PARAMETER Approve
<Optional> Set this flag to provide the approval for the installation to start with no confirmation prompt for the listed VM's/VMSS.
 
.PARAMETER Whatif
<Optional> Set this flag to get info about expected effect of the commands in the script.
 
.PARAMETER Confirm
<Optional> Set this flag to confirm each VM / VMSS.
 
 
 
.PARAMETER DcrResourceId
Data Collection Rule (DCR) azure resource ID identifier.
 
.PARAMETER UserAssignedManagedIdentityResourceGroup
Name of User Assigned Managed Identity (UAMI) resource group.
 
.PARAMETER UserAssignedManagedIdentityName
Name of User Assigned Managed Identity (UAMI).
 
.PARAMETER ProcessAndDependencies
<Retired> This flag is no longer functional. VM Insights Map and the Dependency Agent will be retired on
June 30th, 2028. Using this flag results in an error.
See: https://aka.ms/DependencyAgentRetirement
 
.PARAMETER WorkspaceId
<Retired> The Log Analytics agent was retired on August 31, 2024. Using this parameter results in an error.
See: https://aka.ms/MigrateToAMA
 
.PARAMETER WorkspaceKey
<Retired> The Log Analytics agent was retired on August 31, 2024. Using this parameter results in an error.
See: https://aka.ms/MigrateToAMA
 
.PARAMETER ReInstall
<Retired> The Log Analytics agent was retired on August 31, 2024. Using this parameter results in an error.
See: https://aka.ms/MigrateToAMA
 
.EXAMPLE
Install-VMInsights.ps1 -SubscriptionId <SubscriptionId> -ResourceGroup <ResourceGroup> -DcrResourceId <DataCollectionRuleResourceId> -UserAssignedManagedIdentityName <UserAssignedIdentityName> -UserAssignedManagedIdentityResourceGroup <UserAssignedIdentityResourceGroup>
Onboard VMI for all VM's or VMSS in a Resource Group in a subscription with Azure Monitoring Agent (AMA).
Specify DcrResourceId to associate a data collection rule with VM or VMSS
Specify UserAssignedManagedIdentityName and UserAssignedManagedIdentityResourceGroup for AMA authentication purposes.
 
.EXAMPLE
Install-VMInsights.ps1 -SubscriptionId <SubscriptionId> -ResourceGroup <ResourceGroup> -ProcessAndDependencies -DcrResourceId <DataCollectionRuleResourceId> -UserAssignedManagedIdentityName <UserAssignedIdentityName> -UserAssignedManagedIdentityResourceGroup <UserAssignedIdentityResourceGroup>
NOTE: -ProcessAndDependencies is no longer supported. Dependency Agent onboarding has been retired.
See: https://aka.ms/DependencyAgentRetirement
 
.EXAMPLE
Install-VMInsights.ps1 -SubscriptionId <SubscriptionId> -PolicyAssignmentName a4f79f8ce891455198c08736 -DcrResourceId <DataCollectionRuleResourceId> -UserAssignedManagedIdentityName <UserAssignedIdentityName> -UserAssignedManagedIdentityResourceGroup <UserAssignedIdentityResourceGroup>
Specify PolicyAssignmentName to onboard VMI for VMs part of the policy assignment scope with Azure Monitoring Agent (AMA).
 
.LINK
This script is posted to and further documented at the following location :
http://aka.ms/OnBoardVMInsights
#>


<#CmdletBinding ConfirmImpact level Info : High - Customer data-flow disruptive action.
                                           Medium - Everything else that changes the system configuration.
#>

#Assumption - The script assumes the entity running the script has access to all VMs/VMSS in the script.
[CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High', DefaultParameterSetName = 'AzureMonitoringAgent')]
param(
    [Parameter(mandatory = $True)][String]$SubscriptionId,
    [Parameter(mandatory = $False)][Switch]$TriggerVmssManualVMUpdate,
    [Parameter(mandatory = $False)][Switch]$Approve,
    [Parameter(mandatory = $False)][String]$ResourceGroup = "*",
    [Parameter(mandatory = $False)][String]$Name = "*",
    [Parameter(mandatory = $False)][String]$PolicyAssignmentName,

    [Parameter(mandatory = $False, ParameterSetName = 'AzureMonitoringAgent')][Switch]$ProcessAndDependencies,
    [Parameter(mandatory = $True,  ParameterSetName = 'AzureMonitoringAgent')][String]$DcrResourceId,
    [Parameter(mandatory = $True,  ParameterSetName = 'AzureMonitoringAgent')][String]$UserAssignedManagedIdentityResourceGroup,
    [Parameter(mandatory = $True,  ParameterSetName = 'AzureMonitoringAgent')][String]$UserAssignedManagedIdentityName,

    [Parameter(mandatory = $True,  ParameterSetName = 'LogAnalyticsAgent')][String]$WorkspaceId,
    [Parameter(mandatory = $True,  ParameterSetName = 'LogAnalyticsAgent')][String]$WorkspaceKey,
    [Parameter(mandatory = $False, ParameterSetName = 'LogAnalyticsAgent')][Switch]$ReInstall
)

$ErrorActionPreference = "Stop"

class CustomerSkip : System.Exception {
    CustomerSkip() : base () {}
}

class FatalException : System.Exception {
    FatalException([String]$errorMessage, [Exception]$innerException) : base($errorMessage, $innerException) {}
}

class VirtualMachineException : System.Exception {
    VirtualMachineException(<#[Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]#>$vmObject,
                            [String]$errorMessage,
                            [Exception]$innerException) : base("$(FormatVmIdentifier -VMObject $vmObject) : $errorMessage", $innerException) {}
}

class VirtualMachineScaleSetException : System.Exception {
    VirtualMachineScaleSetException(<#[Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]#>$vmssObject,
                                    [String]$errorMessage,
                                    [Exception]$innerException) : base("$(FormatVmssIdentifier -VMssObject $vmssObject) : $errorMessage", $innerException) {}
}

class ResourceGroupDoesNotExist : System.Exception {
    ResourceGroupDoesNotExist ([String]$rgName, [Exception]$innerException) : base("$rgName : Resource Group does not exist", $innerException) {}
}

class VirtualMachineUnknownException : VirtualMachineException {
    VirtualMachineUnknownException(<#[Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]#>$vmObject,
                                   [String]$errorMessage,
                                   [Exception]$innerException) : base($vmObject, $errorMessage, $innerException) {}
}

class VirtualMachineDoesNotExist : VirtualMachineException {
    VirtualMachineDoesNotExist (<#[Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]#>$vmObject,
                                [Exception]$innerException) : base($vmObject, "VM does not exist", $innerException) {}
}

class VirtualMachineOperationFailed : VirtualMachineException {
    VirtualMachineOperationFailed(<#[Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]#>$vmObject,
                                  [String]$errorMessage) : base($vmObject, $errorMessage, $null) {}
}

class VirtualMachinePoweredDown : VirtualMachineException {
    VirtualMachinePoweredDown(<#[Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]#>$vmObject,
                              [Exception]$innerException) : base($vmObject, "VM is powered down", $innerException) {}
}

class VirtualMachineExtensionError : VirtualMachineException {
    VirtualMachineExtensionError (<#[Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]#>$vmObject,
                                  [String]$extension,
                                  [Exception]$innerException) : base($vmObject, "VM extension error for $extension`n$($innerException.Message)", $innerException) {}
}

class VirtualMachineScaleSetUnknownException : VirtualMachineScaleSetException {
    VirtualMachineScaleSetUnknownException(<#[Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]#>$vmssObject,
                                           [String]$errorMessage,
                                           [Exception]$innerException) : base($vmssObject, $errorMessage, $innerException) {}
}

class VirtualMachineScaleSetDoesNotExist : VirtualMachineScaleSetException {
    VirtualMachineScaleSetDoesNotExist (<#[Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]#>$vmssObject,
                                        [Exception]$innerException) : base($vmssObject, "VMSS does not exist", $innerException) {}
}

class VirtualMachineScaleSetOperationFailed : VirtualMachineScaleSetException {
    VirtualMachineScaleSetOperationFailed(<#[Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]#>$vmssObject,
                                          [String]$errorMessage) : base($vmssObject, $errorMessage, $null) {}
}

class VirtualMachineScaleSetExtensionError : VirtualMachineScaleSetException {
    VirtualMachineScaleSetExtensionError (<#[Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]#>$vmssObject,
                                          [Exception]$innerException) : base($vmssObject, "VMSS extension error`n$($innerException.Message)", $innerException) {}
}

class DataCollectionRuleForbidden : FatalException {
    DataCollectionRuleForbidden([String]$dcrResourceId,
                                [Exception]$innerException) : base("$dcrResourceId : Access to Data Collection Rule is forbidden", $innerException) {}
}

class DataCollectionRuleDoesNotExist : FatalException {
    DataCollectionRuleDoesNotExist([String]$dcrResourceId,
                                   [Exception]$innerException) : base("$dcrResourceId : Data Collection Rule does not exist.", $innerException) {}
}

class DataCollectionRuleIncorrect : FatalException {
    DataCollectionRuleIncorrect([String]$dcrResourceId,
                                [Exception]$innerException) : base("$dcrResourceId : Data Collection Rule incorrect format.", $innerException) {}
}

class UserAssignedManagedIdentityDoesNotExist : FatalException {
    UserAssignedManagedIdentityDoesNotExist($uamiName,
                                            [Exception]$innerException) : base("$uamiName : User Assigned Managed Identity does not exist.", $innerException) {}
}

class UserAssignedManagedIdentityResourceGroupDoesNotExist : FatalException {
    UserAssignedManagedIdentityResourceGroupDoesNotExist($uamiResourceGroup,
                                                         [Exception]$innerException) : base("$uamiResourceGroup : User Assigned Managed Identity Resource Group does not exist.", $innerException) {}
}

class UserAssignedManagedIdentityUnknownException : FatalException {
    UserAssignedManagedIdentityUnknownException([String]$errorMessage,
                                                [Exception]$innerException) : base($errorMessage, $innerException) {}
}

class ResourceGroupTableElement {
    [System.Collections.ArrayList] $VirtualMachineList = [System.Collections.ArrayList]::new()
    [System.Collections.ArrayList] $VirtualMachineScaleSetList = [System.Collections.ArrayList]::new()
}

class OnboardingCounters {
    [Decimal]$Succeeded = 0
    [Decimal]$Total = 0
    [Decimal]$Skipped = 0
    [Decimal]$VMSSInstanceUpgradeFailure = 0
}

# Azure Monitoring Agent Extension constants
Set-Variable -WhatIf:$False -Confirm:$False -Name amaExtensionConstantMap -Option Constant -Value @{ 
       "Windows" = @{  ExtensionType = "AzureMonitorWindowsAgent"
                       TypeHandlerVersion = "1.16"
                       Publisher = "Microsoft.Azure.Monitor" 
                    }
       "Linux" =   @{  ExtensionType = "AzureMonitorLinuxAgent" 
                       TypeHandlerVersion = "1.16"
                       Publisher = "Microsoft.Azure.Monitor"
                    }
}
Set-Variable -WhatIf:$False -Confirm:$False -Name amaDefaultExtensionName -Option Constant -Value "AzureMonitoringAgent"

$extensionVmDefaultUpgradeSettings = @{
    EnableAutomaticUpgrade = $True
    DisableAutoUpgradeMinorVersion = $False
}

Set-Variable -WhatIf:$False -Confirm:$False -Name unknownExceptionVirtualMachineConsequentCounterLimit -Option Constant -Value 3
Set-Variable -WhatIf:$False -Confirm:$False -Name unknownExceptionVirtualMachineScaleSetConsequentCounterLimit -Option Constant -Value 3
Set-Variable -WhatIf:$False -Confirm:$False -Name unknownExceptionTotalCounterLimit -Option Constant -Value 6

#
# FUNCTIONS
#
function PrintSummaryMessage {
    <#
    .SYNOPSIS
    Print the Total of eligible VM/VMSS, number of succeeded and failed.
    #>

    param (
        [Parameter(mandatory = $True)]
        [OnboardingCounters]
        $OnboardingCounters
    )
    Write-Host ""
    Write-Host "Summary :"
    Write-Host "Total VM/VMSS to be processed : $($OnboardingCounters.Total)"
    Write-Host "Succeeded : $($OnboardingCounters.Succeeded)"
    Write-Host "Skipped : $($OnboardingCounters.Skipped)"
    Write-Host "Failed : $($OnboardingCounters.Total - $OnboardingCounters.Skipped - $OnboardingCounters.Succeeded)"
    Write-Host "VMSS Instance Upgrade Failures : $($OnboardingCounters.VMSSInstanceUpgradeFailure)"
}

function ExtractCloudExceptionErrorMessage {
    <#
    .SYNOPSIS
    Extract error code from the Cloud Exception.
    #>

    param
    (
        [Parameter(Mandatory=$True)]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord
    )

    if ($ErrorRecord.Exception.Message -match 'ErrorMessage *: *(.*)') {
        return $matches[1]
    }

    return $null
}

function ExtractExceptionErrorCode {
    <#
    .SYNOPSIS
    Extract error code from the Cloud Exception.
    #>

    param
    (
        [Parameter(Mandatory=$True)]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord
    )

    if ($ErrorRecord.Exception.Message -match 'ErrorCode *: *([^\s]+)') {
        return $matches[1]
    }

    return $null
}

function ExtractExceptionPrefixErrCode {
    <#
    .SYNOPSIS
    Extract error code from the Cloud Exception.
    #>

    param
    (
        [Parameter(Mandatory=$True)]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord
    )

    if ($ErrorRecord.Exception.Message -match '^[[]([^]]+)[]] *:') {
        return $matches[1]
    }

    return $null
}

function FormatVmIdentifier {
    <#
    .SYNOPSIS
    Format VM Information for messages
    #>

    param (
        [Parameter(Mandatory=$True)] 
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject
    )
    return "($($VMObject.ResourceGroupName)) $($VMObject.Name)"
}

function FormatVmssIdentifier {
    <#
    .SYNOPSIS
    Format VMSS Information for messages
    #>

    param (
        [Parameter(Mandatory=$True)] 
        [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
        $VMssObject
    )
    
    return "($($VMssObject.ResourceGroupName)) $($VMssObject.Name)"
}

function IsVmAndGuestAgentUpAndHealthy {
    <#
    .SYNOPSIS
    Checks whether a VM is up and Guest agent is in a healthy state.
    #>

    param (
        [Parameter(Mandatory=$True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject
    )
  
    try {
        $vmResourceGroupName = $VMObject.ResourceGroupName
        $vmlogheader = FormatVmIdentifier -VMObject $VMObject
        
        $vmWithStatus = Get-AzVm -ResourceGroupName $vmResourceGroupName -Name $VMObject.Name -Status
        
        $vmhealthStatusCode = $vmWithStatus.Statuses.Code
        if (!($vmhealthStatusCode -contains 'ProvisioningState/succeeded' -and $vmhealthStatusCode -contains 'PowerState/running')) {
            Write-Host "$vmlogheader : Virtual Machine is not operational - $($vmWithStatus.Statuses.DisplayStatus)"
            return $False
        }

        $guestAgentHealthCode = $vmWithStatus.VMAgent.Statuses.Code
        if (!($guestAgentHealthCode -contains 'ProvisioningState/succeeded')) {
            Write-Host "$vmlogheader : Guest Agent is not healthy - $($vmWithStatus.VMAgent.Statuses.DisplayStatus)"
            return $False
        }

        return $True
    } catch [Microsoft.Azure.Commands.Compute.Common.ComputeCloudException] {
        $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
        if ($errorCode -eq "ResourceNotFound") { 
            throw [VirtualMachineDoesNotExist]::new($VMObject, $_.Exception)
        }

        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [ResourceGroupDoesNotExist]::new($vmResourceGroupName, $_.Exception)  
        }

        throw [VirtualMachineUnknownException]::new($VMObject, "Failed to get status of virtual machine", $_.Exception)
    }
}

function VerboseDisplayException {
    <#
    .SYNOPSIS
    Renders customer actionable message, inner exception and stack trace to verbose stream.
    Does not throw any exceptions.
    #>

    param (
        [Parameter(Mandatory=$True)]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord
    )

    try {
        $ex = $ErrorRecord.Exception
        Write-Host "ExceptionMessage : $($ex.Message)"
        Write-Verbose "StackTrace :"
        Write-Verbose "$($ex.StackTrace)"
        while ($ex = $ex.InnerException) {
            Write-Verbose "InnerExceptionMessage : $($ex.Message)"
            Write-Verbose "StackTrace :"
            Write-Verbose "$($ex.StackTrace)"
        }
        Write-Verbose "ScriptStackTrace :" $ErrorRecord.ScriptStackTrace
    }
    catch {
        # silently ignore
    }
}

function DisplayException {
    <#
    .SYNOPSIS
    Renders customer actionable message, inner exception and stack trace to console.
    Does not throw any exceptions.
    #>

    param (
        [Parameter(Mandatory=$True)]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord
    )

    try {
        $ex = $ErrorRecord.Exception
        Write-Host "ExceptionMessage : $($ex.Message)"
        Write-Host "StackTrace :"
        Write-Host "$($ex.StackTrace)"
        while ($ex = $ex.InnerException) {
            Write-Host "InnerExceptionMessage : $($ex.Message)"
            Write-Host "StackTrace :"
            Write-Host "$($ex.StackTrace)"
        }
        Write-Host "ScriptStackTrace :" $ErrorRecord.ScriptStackTrace
    }
    catch {
        # silently ignore
    }
}

function GetRgObject {

    <#
    .SYNOPSIS
    PopulateRgHashTableVMs/VMss util function
    #>

    param(
        [Parameter(Mandatory=$True)]
        [Hashtable]
        $Rghashtable,
        [Parameter(Mandatory=$True)]
        [String]
        $ResourceGroupName
    )

    $rgTableElemObject = $Rghashtable[$ResourceGroupName]
    if ($null -eq $rgTableElemObject) {
        $rgTableElemObject = [ResourceGroupTableElement]::new()
        $Rghashtable.Add($ResourceGroupName,$rgTableElemObject)
    }

    return $rgTableElemObject
}

function PopulateRgHashTableVm {
    <#
    .SYNOPSIS
    Populate Resource Group hash table for VMs
    #>

    param(
        [Parameter(Mandatory=$True)]
        [Hashtable]
        $Rghashtable,
        [Parameter(Mandatory=$True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject
    )

    $rgTableElemObject = GetRgObject -Rghashtable $Rghashtable -ResourceGroupName $VMObject.ResourceGroupName
    $rgTableElemObject.VirtualMachineList.Add($VMObject)  > $null
}

function PopulateRgHashTableVmss {
    <#
    .SYNOPSIS
    Populate Resource Group hash table for VMSS
    #>

    param(
        [Parameter(Mandatory=$True)]
        [Hashtable]
        $Rghashtable,
        [Parameter(Mandatory=$True)]
        [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
        $VMssObject
    )

    $rgTableElemObject = GetRgObject -Rghashtable $Rghashtable -ResourceGroupName $VMssObject.ResourceGroupName
    $rgTableElemObject.VirtualMachineScaleSetList.Add($VMssObject)  > $null
}

function GetVMExtension {
    <#
    .SYNOPSIS
    Return the VM extension of specified Type and Publisher.
    #>

    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject,
        [Parameter(mandatory = $True)]
        [Hashtable]
        $ExtensionProperties
    )

    $extensionType = $ExtensionProperties.ExtensionType
    $publisher = $ExtensionProperties.Publisher

    try {
        $extensions = Get-AzVMExtension -ResourceGroupName $VMObject.ResourceGroupName -VMName $VMObject.Name
        foreach ($extension in $extensions) {
            if ($extension.ExtensionType -eq $extensionType -and $extension.Publisher -eq $publisher) {
                return $extension
            }
        }
    } catch [Microsoft.Azure.Commands.Compute.Common.ComputeCloudException] {
        $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
        if ($errorCode -eq "ParentResourceNotFound") {
            throw [VirtualMachineDoesNotExist]::new($VMObject, $_.Exception)
        }
        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [ResourceGroupDoesNotExist]::new($($VMObject.ResourceGroupName),$_.Exception)   
        }    
        throw [VirtualMachineUnknownException]::new($VMObject, "Failed to locate extension with type = $($publisher).$($extensionType)", $_.Exception)
    }
    
    return $null
}

function GetVMssExtension {
    <#
    .SYNOPSIS
    Return the VMSS extension of specified ExtensionType and Publisher.
    #>

    param
    (
        [Parameter(Mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
        $VMssObject,
        [Parameter(mandatory = $True)]
        [String]
        $ExtensionType,
        [Parameter(mandatory = $True)]
        [String]
        $Publisher
    )

    $extensions = $VMssObject.VirtualMachineProfile.ExtensionProfile.Extensions

    foreach ($extension in $extensions) {
        if ($extension.Type -eq $ExtensionType -and $extension.Publisher -eq $Publisher) {
            return $extension
        }
    }

    return $null
}

function RemoveVMExtension {
    <#
    .SYNOPSIS
    Remove a VM Extension.
    #>

    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High')]
    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject,
        [Parameter(mandatory = $True)]
        [String]
        $ExtensionName,
        [Parameter(mandatory = $True)]
        [hashtable]
        $ExtensionProperties
    )

    $vmlogheader = FormatVmIdentifier -VMObject $VMObject
    $extensionType = $ExtensionProperties.ExtensionType
    $extensionPublisher = $ExtensionProperties.Publisher
    
    if (!$PSCmdlet.ShouldProcess($vmlogheader, "Remove $ExtensionName, type $($extensionPublisher).$($extensionType)")) {
        #-WhatIf skip processing here, return to the caller as we have completed our work.
        if ($WhatIfPreference) {
            return
        }
        throw [CustomerSkip]::new()
    }

    try {
        #Remove operation on non existent VM, extension still return a success
        $removeResult = Remove-AzVMExtension -ResourceGroupName $VMObject.ResourceGroupName `
                                             -VMName $VMObject.Name `
                                             -Name $ExtensionName -Confirm:$false -Force
    } catch [Microsoft.Azure.Commands.Compute.Common.ComputeCloudException] {
        $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [ResourceGroupDoesNotExist]::new($VMObject.ResourceGroupName, $_.Exception)       
        } 
        
        throw [VirtualMachineUnknownException]::new($VMObject, "Failed to remove extension $ExtensionName, type $($extensionPublisher).$($extensionType)", $_.Exception)
    }
    
    if ($removeResult.IsSuccessStatusCode) {
        Write-Host "$vmlogheader : Successfully removed extension $ExtensionName, type $($extensionPublisher).$($extensionType)"
        return
    }

    throw [VirtualMachineOperationFailed]::new($VMObject, 
            "Failed to remove extension $ExtensionName, type $($extensionPublisher).$($extensionType). StatusCode = $($removeResult.StatusCode). ReasonPhrase = $($removeResult.ReasonPhrase)")
}

#VMI supports Customers onboarding DCR from different subscription to which it has access to.
#Cannot validate DCRResourceId parameter Get-AzDataCollectionRule -ResourceId is getting deprecated.
function NewDCRAssociationVm {
    <#
    .SYNOPSIS
    Create a new DCRAssociation with VMs.
    #>

    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject
    )

    $vmId = $VMObject.Id
    $vmlogheader = FormatVmIdentifier -VMObject $VMObject
    
    try {
        # A VM may have zero or more Data Collection Rule Associations.
        $dcrAssociationList = Get-AzDataCollectionRuleAssociation -TargetResourceId $vmId 
        foreach ($dcra in $dcrAssociationList) {
            if ($dcra.DataCollectionRuleId -eq $DcrResourceId) {
                Write-Host "$vmlogheader : Data Collection Rule Id $DcrResourceId already associated with the VM."
                return
            }
        }
    } catch [System.Management.Automation.ParameterBindingException] {
        throw [VirtualMachineDoesNotExist]::new($VMObject, $_.Exception)
    }

    if (!($PSCmdlet.ShouldProcess($vmlogheader, "Install Data Collection Rule Association"))) {
        #-WhatIf skip processing here, return to the caller as we have completed our work.
        if ($WhatIfPreference) {
            return
        }
        throw [CustomerSkip]::new()
    }

    #Customer can associate multiple DCRs to a VM.
    $dcrAssociationName = "VM-Insights-DCR-Association-$(New-Guid)"
    Write-Host "$vmlogheader : Deploying Data Collection Rule Association $dcrAssociationName"
    try {
        #BUG! To be fixed by AMCS team - New-AzDataCollectionRuleAssociation allows creating multiple DCRAs for the same {DCR,VMSS} combination.
        $dcrAssociation = New-AzDataCollectionRuleAssociation -TargetResourceId $vmId `
                                                              -AssociationName $dcrAssociationName `
                                                              -RuleId $DcrResourceId `
                                                              -Confirm:$false
    } catch [System.Management.Automation.PSInvalidOperationException] {
        $exceptionMessage = $_.Exception.Message
        
        if ($exceptionMessage.Contains('Invalid format of the resource identifier')) {
            throw [DataCollectionRuleIncorrect]::new($DcrResourceId, $_.Exception)
        } 

        if (!($exceptionMessage -match "status code '([^\s]+)'")) {
            throw [VirtualMachineUnknownException]::new($VMObject, "Failed to create Data Collection Rule Association with $DcrResourceId", $_.Exception)
        }

        $statusCode = $matches[1]
        if ($statusCode -eq 'BadRequest') {
            throw [DataCollectionRuleDoesNotExist]::new($DcrResourceId, $_.Exception)
        }
        if ($statusCode -eq 'NotFound') {
            throw [VirtualMachineDoesNotExist]::new($VMObject, $_.Exception)
        }
        if ($statusCode -eq 'Forbidden') {
            throw [DataCollectionRuleForbidden]::new($DcrResourceId, $_.Exception)     
        }

        throw [VirtualMachineUnknownException]::new($VMObject, "Failed to create Data Collection Rule Association with with $DcrResourceId. StatusCode = $statusCode", $_.Exception)
    }

    #Tmp fix task :- 21191002
    if (($null -eq $dcrAssociation) -or ($dcrAssociation -is [Microsoft.Azure.Management.Monitor.Models.ErrorResponseCommonV2Exception])) {
        throw [VirtualMachineUnknownException]::new($VMObject, "Unable to create Data Collection Rule Association with $DcrResourceId", $_.Exception)
    }

    Write-Host "$vmlogheader : Successfully created Data Collection Rule Association"
}

function NewDCRAssociationVmss {
    <#
    .SYNOPSIS
    Create a new DCR Association with VMSS.
    #>

    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
        $VMssObject
    )

    $vmsslogheader = FormatVmssIdentifier -VMssObject $VMssObject
    $vmssId = $VMssObject.Id

    try {
        # A VMSS may have zero or more Data Collection Rule Associations.
        $dcrAssociationList = Get-AzDataCollectionRuleAssociation -TargetResourceId $vmssId 
        foreach ($dcra in $dcrAssociationList) {
            if ($dcra.DataCollectionRuleId -eq $DcrResourceId) {
                Write-Host "$vmsslogheader : Data Collection Rule Id $DcrResourceId already associated with the VMSS."
                return
            }
        }
    } catch [System.Management.Automation.ParameterBindingException] {
        throw [VirtualMachineScaleSetDoesNotExist]::new($VMssObject, $_.Exception)
    }

    if (!($PSCmdlet.ShouldProcess($vmsslogheader, "Install Data Collection Rule Association"))) {
        #-WhatIf skip processing here, return to the caller as we have completed our work.
        if ($WhatIfPreference) {
            return
        }
        throw [CustomerSkip]::new()
    }

    #Customer can associate multiple DCRs to a VMSS.
    $dcrAssociationName = "VM-Insights-DCR-Association-$(New-Guid)"
    Write-Host "$vmsslogheader : Deploying Data Collection Rule Association $dcrAssociationName"
    try {
        #BUG! To be fixed by AMCS team - New-AzDataCollectionRuleAssociation allows creating multiple DCRAs for the same {DCR,VMSS} combination.
        $dcrAssociation = New-AzDataCollectionRuleAssociation -TargetResourceId $vmssId `
                                                              -AssociationName $dcrAssociationName `
                                                              -RuleId $DcrResourceId `
                                                              -Confirm:$false
    } catch [System.Management.Automation.PSInvalidOperationException] {
        $exceptionMessage = $_.Exception.Message
        
        if ($exceptionMessage.Contains('Invalid format of the resource identifier')) {
            throw [DataCollectionRuleIncorrect]::new($DcrResourceId, $_.Exception)
        } 
        
        if (!($exceptionMessage -match "status code '([^\s]+)'")) {
            throw [VirtualMachineScaleSetUnknownException]::new($VMssObject, "Failed to create Data Collection Rule Association with $DcrResourceId", $_.Exception)
        }

        $statusCode = $matches[1]
        if ($statusCode -eq 'BadRequest') {
            throw [DataCollectionRuleDoesNotExist]::new($DcrResourceId, $_.Exception)
        }
        if ($statusCode -eq 'NotFound') {
            throw [VirtualMachineScaleSetDoesNotExist]::new($VMssObject, $_.Exception)
        }
        if ($statusCode -eq 'Forbidden') {
            throw [DataCollectionRuleForbidden]::new($DcrResourceId, $_.Exception)     
        }

        throw [VirtualMachineUnknownException]::new($VMssObject, "Failed to create Data Collection Rule Association with with $DcrResourceId. StatusCode = $statusCode", $_.Exception)
    }
    #Tmp fix task :- 21191002
    if (($null -eq $dcrAssociation) -or ($dcrAssociation -is [Microsoft.Azure.Management.Monitor.Models.ErrorResponseCommonV2Exception])) {
        throw [VirtualMachineScaleSetDoesNotExist]::new($VMssObject, "Unable to create Data Collection Rule Association with $DcrResourceId", $_.Exception)
    }
}

function RetainExtensionUpgradeSettings {
    <#
    .SYNOPSIS
    Retain extension upgrade settings
    #>

    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachineExtension]
        $Extension,
        [Parameter(mandatory = $True)]
        [hashtable]
        $ExtUpgradeSettings
    )

    $ExtUpgradeSettings.DisableAutoUpgradeMinorVersion = !($Extension.AutoUpgradeMinorVersion)
    $ExtUpgradeSettings.EnableAutomaticUpgrade = $Extension.EnableAutomaticUpgrade
}

function OnboardAmaVm {
    <#
    .SYNOPSIS
    Onboard AMA on VM, handling if already installed.
    #>

    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject,
        [Parameter(mandatory = $True)]
        [hashtable]
        $ExtensionSettings
    )
    
    $amaExtensionConstantProperties = $amaExtensionConstantMap[$VMObject.StorageProfile.OsDisk.OsType.ToString()]
    $extensionName = $amaDefaultExtensionName
    $extUpgradeSettings = $extensionVmDefaultUpgradeSettings.clone()
    $extension = GetVMExtension -VMObject $VMObject -ExtensionProperties $amaExtensionConstantProperties
    # Use supplied name unless already deployed, use same name.
    if ($extension) {
        $extensionName = $extension.Name
        RetainExtensionUpgradeSettings -Extension $extension -ExtUpgradeSettings $extUpgradeSettings
        Write-Host "$(FormatVmIdentifier -VMObject $VMObject) : Extension $extensionName, type = $($amaExtensionConstantProperties.Publisher).$($amaExtensionConstantProperties.ExtensionType) already installed. Provisioning State : $($extension.ProvisioningState)"
    }
    
    return SetVMExtension -VMObject $VMObject `
                          -ExtensionName $extensionName `
                          @amaExtensionConstantProperties `
                          -ExtensionUpgradeSettings $extUpgradeSettings `
                          -ExtensionSettings $ExtensionSettings
}

function OnboardVmiWithAmaVm {
    <#
    .SYNOPSIS
    Onboard VMI with AMA on VM.
    #>

    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject,
        [Parameter(mandatory = $True)]
        [hashtable]
        $ExtensionSettings
    )

    $VMObject = AssignVmUserManagedIdentity -VMObject $VMObject
    NewDCRAssociationVm -VMObject $VMObject
    return OnboardAmaVm -VMObject $VMObject -ExtensionSettings $ExtensionSettings
}

function OnboardVmiWithAmaVmss {
    <#
    .SYNOPSIS
    Onboard VMI with AMA on VMSS
    #>

    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
        $VMssObject,
        [Parameter(mandatory = $True)]
        [hashtable]
        $ExtensionSettings
    )
            
    $VMssObject = AssignVmssUserManagedIdentity -VMssObject $VMssObject
    NewDCRAssociationVmss -VMssObject $VMssObject
    return OnboardVMssExtension -VMssObject $VMssObject `
                                -ExtensionName $amaDefaultExtensionName `
                                -ExtensionConstantMap $amaExtensionConstantMap `
                                -ExtensionSettings $ExtensionSettings
}

function SetManagedIdentityRoles {
    <#
    .SYNOPSIS
    Set roles to User Assigned Managed Identity.
    #>

    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(Mandatory = $True)]
        [String]
        $ResourceGroupId,
        [Parameter(Mandatory = $True)]
        [object]
        $UserAssignedManagedIdentity,
        [Parameter(Mandatory = $True)]
        [String[]]
        $Roles
    )

    $uamiName = $UserAssignedManagedIdentity.Name
    if (!($PSCmdlet.ShouldProcess($ResourceGroupId, "Assign $Roles to User Assigned Managed Identity $uamiName"))) {
        #-WhatIf skip processing here, return to the caller as we have completed our work.
        if ($WhatIfPreference) {
            return
        }
        throw [CustomerSkip]::new()
    }

    foreach ($role in $Roles) {
        Write-Verbose "Assigning role $role"
        try {
            New-AzRoleAssignment -ObjectId $($UserAssignedManagedIdentity.principalId) `
                                 -RoleDefinitionName $role `
                                 -Scope $ResourceGroupId `
                                 -Confirm:$false
            Write-Verbose "$ResourceGroupId : $role has been successfully assigned to $uamiName"
        } catch {
            $excepMessage = $_.Exception.Message
            if ($excepMessage.Contains('Conflict')) {
                Write-Verbose "$uamiName : Role $role has been assigned already"
            }
            if ($excepMessage.Contains('BadRequest')) {
                throw [FatalException]::new("$uamiName : User Assigned Managed Identity doesn't exist", $_.Exception)
            }
            if ($excepMessage.Contains('NotFound')) {
                throw [ResourceGroupDoesNotExist]::new($($VMObject.ResourceGroupName), $_.Exception)
            }
        }
        
    }
}

function OnboardVMssExtension {
    <#
    .SYNOPSIS
    Install/Update Extension VMSS, handling if already installed
    #>

    #check if this supposed to be all the stack. that is functioing call it as well.
    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(Mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
        $VMssObject,
        [Parameter(mandatory = $True)]
        [String]
        $ExtensionName,
        [Parameter(mandatory = $True)]
        [Hashtable]
        $ExtensionConstantMap,
        [Parameter(mandatory = $True)]
        [Hashtable]
        $ExtensionSettings
    )

    $extensionConstantProperties = $ExtensionConstantMap[$VMssObject.VirtualMachineProfile.StorageProfile.OsDisk.OsType.ToString()]
    $extensionType = $extensionConstantProperties.ExtensionType
    $publisher = $extensionConstantProperties.Publisher
    $typeHandlerVersion = $extensionConstantProperties.TypeHandlerVersion
    $extensionUpgradeSettings = @{
        AutoUpgradeMinorVersion = $True
        EnableAutomaticUpgrade = $True 
    }
    $extension = GetVMssExtension -VMssObject $VMssObject -ExtensionType $extensionType -Publisher $publisher
    $vmsslogheader = FormatVmssIdentifier -VMssObject $VMssObject

    if ($extension) {
        if (!($PSCmdlet.ShouldProcess($vmsslogheader, "Update extension $($extension.Name), type = $($publisher).$($extensionType)"))) {
            #-WhatIf skip processing here, return to the caller as we have completed our work.
            if ($WhatIfPreference) {
                return $VMssObject
            }
            throw [CustomerSkip]::new()
        }
        Write-Host "$vmsslogheader : Extension $($extension.Name), type = $($publisher).$($extensionType) already installed."
        #VMSS extension has field "Settings" but Add-AzVmssExtension supports 'Setting'
        if ($ExtensionSettings.ContainsKey("Setting")) {
            $extension.Settings = $ExtensionSettings.Setting
        }
        #VMSS extension has field "ProtectedSettings" but Add-AzVmssExtension supports 'ProtectedSetting'
        if ($ExtensionSettings.ContainsKey("ProtectedSetting")) {
            $extension.ProtectedSettings = $ExtensionSettings.ProtectedSetting
        }
        $extensionUpgradeSettings.GetEnumerator() | ForEach-Object { $extension.($_.Key) = $_.Value }
        $extension.TypeHandlerVersion = $typeHandlerVersion
        return $VMssObject
    }

    if (!($PSCmdlet.ShouldProcess($vmsslogheader, "Install extension $ExtensionName, type = $($publisher).$($extensionType)"))) {
        #-WhatIf skip processing here, return to the caller as we have completed our work.
        if ($WhatIfPreference) {
            return $VMssObject
        }
        throw [CustomerSkip]::new()
    }
    
    $VMssObject = Add-AzVmssExtension -VirtualMachineScaleSet $VMssObject `
                                      -Name $ExtensionName `
                                      -Type $extensionType `
                                      -Publisher $publisher `
                                      -TypeHandlerVersion $typeHandlerVersion `
                                      @extensionUpgradeSettings `
                                      @ExtensionSettings `
                                      -Confirm:$false


    Write-Host "$vmsslogheader : Extension $ExtensionName, type = $($publisher).$($extensionType) added."
    return $VMssObject
}

function SetVMExtension {
    <#
    .SYNOPSIS
    Install/Update VM Extension, handling if already installed
    #>

    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject,
        [Parameter(mandatory = $True)]
        [String]
        $ExtensionName,
        [Parameter(mandatory = $True)]
        [String]
        $ExtensionType,
        [Parameter(mandatory = $True)]
        [String]
        $Publisher,
        [Parameter(mandatory = $True)]
        [String]
        $TypeHandlerVersion,
        [Parameter(mandatory = $True)]
        [Hashtable]
        $ExtensionUpgradeSettings,
        [Parameter(mandatory = $True)]
        [Hashtable]
        $ExtensionSettings
    )
    
    $vmlogheader = $(FormatVmIdentifier -VMObject $VMObject)
    
    if (!($PSCmdlet.ShouldProcess($vmlogheader, "Install/Update extension $ExtensionName, type = $($Publisher).$($ExtensionType)"))) {
        #-WhatIf skip processing here, return to the caller as we have completed our work.
        if ($WhatIfPreference) {
            return $VMObject
        }
        throw [CustomerSkip]::new()
    }

    Write-Host "$vmlogheader : Installing/Updating extension $ExtensionName, type = $($Publisher).$($ExtensionType)"
    
    try {
        $result = Set-AzVMExtension -ResourceGroupName $($VMObject.ResourceGroupName) `
                                    -VMName $($VMObject.Name) `
                                    -Name $ExtensionName `
                                    -ExtensionType $ExtensionType `
                                    -Publisher $Publisher `
                                    -TypeHandlerVersion $TypeHandlerVersion `
                                    @ExtensionUpgradeSettings `
                                    @ExtensionSettings -ForceRerun $True `
                                    -Confirm:$false

        if (!$result.IsSuccessStatusCode) {
            throw [VirtualMachineOperationFailed]::new($VMObject, "Failed to update extension. StatusCode = $($result.StatusCode). ReasonPhrase = $($result.ReasonPhrase)")
        }
    
        Write-Host "$vmlogheader : Successfully installed/updated extension $ExtensionName, type = $($Publisher).$($ExtensionType)"
        return $VMObject
    } catch [Microsoft.Azure.Commands.Compute.Common.ComputeCloudException] {
        $errorMessage = ExtractCloudExceptionErrorMessage -ErrorRecord $_
        $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
        #Found by experimentation.
        if ($errorCode -eq "OperationNotAllowed" -and $errorMessage -eq "Cannot modify extensions in the VM when the VM is not running.") {
            throw [VirtualMachinePoweredDown]::new($VMObject, $_.Exception)
        }
        
        if ($errorCode -eq "ParentResourceNotFound") {
            throw [VirtualMachineDoesNotExist]::new($VMObject, $_.Exception)
        } 
        
        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [ResourceGroupDoesNotExist]::new($VMObject.ResourceGroupName, $_.Exception)       
        } 
        
        if ($errorCode -eq "VMExtensionHandlerNonTransientError") {
            throw [VirtualMachineExtensionError]::new($VMObject, "$($Publisher).$($ExtensionType)", $_.Exception)       
        }

        throw [VirtualMachineUnknownException]::new($VMObject, "Failed to install/update extension $ExtensionName, type = $($Publisher).$($ExtensionType)", $_.Exception)
    }
}

function UpgradeVmssExtensionManualUpdateEnabled {
    <#
    .SYNOPSIS
    Upgrade VMss Extension with manual update.
    #>

    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
        $VMssObject,
        [Parameter(mandatory = $True)]
        [ref]
        $InstanceUpgradeFailCounter
    )
    
    $vmssResourceGroupName = $VMssObject.ResourceGroupName
    $vmssName = $VMssObject.Name
    $vmsslogheader = FormatVmssIdentifier -VMssObject $VMssObject
    
    $scaleSetInstances = @()
    try {
        $scaleSetInstances = Get-AzVmssVm -ResourceGroupName $vmssResourceGroupName -VMScaleSetName $vmssName -InstanceView
    } catch [Microsoft.Azure.Commands.Compute.Common.ComputeCloudException] {
        $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
        if ($errorCode -eq "ParentResourceNotFound") {
            throw [VirtualMachineScaleSetDoesNotExist]::new($VMssObject, $_.Exception)
        }
        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [ResourceGroupDoesNotExist]::new($VMssObject.ResourceGroupName, $_.Exception)       
        } 
        
        throw [VirtualMachineScaleSetUnknownException]::new($VMssObject, "Failed to upgrade VMSS", $_.Exception)
    }

    $i = 0
    $instanceCount = $scaleSetInstances.Count
    $unexpectedUpgradeExceptionCounter = 0
    $unexpectedUpgradeExceptionLimit = 5 
    
    Foreach ($scaleSetInstance in $scaleSetInstances) {
        $i++
        $healthstatus = $scaleSetInstance.InstanceView.Statuses.Code

        if (!($healthstatus -contains 'ProvisioningState/succeeded' -and $healthstatus -contains 'PowerState/running')) {
            Write-Host "VMSS instance $scaleSetInstanceName, $i of $instanceCount is not operational. State = $($scaleSetInstance.InstanceView.Statuses.DisplayStatus)"
            Write-Host "Continuing ..."
            $InstanceUpgradeFailCounter.Value += 1
            continue
        }

        $scaleSetInstanceName = $($scaleSetInstance.Name)
        Write-Host "$vmsslogheader : Upgrading $scaleSetInstanceName, $i of $instanceCount"

        if ($scaleSetInstance.LatestModelApplied) {
            Write-Verbose "$vmsslogheader : Latest model already applied for $scaleSetInstanceName, $i of $instanceCount"
            continue
        }
        
        Write-Verbose "$vmsslogheader : Upgrading VMSS instance name $scaleSetInstanceName, $i of $instanceCount"
        try {
            $result = Update-AzVmssInstance -ResourceGroupName $vmssResourceGroupName `
                                            -VMScaleSetName $vmssName `
                                            -InstanceId $scaleSetInstance.InstanceId `
                                            -Confirm:$false
            if ($result.Status -ne "Succeeded") {
                Write-Host "$vmsslogheader : Failed to upgrade VMSS instance name $scaleSetInstanceName, $i of $instanceCount. $($result.Status)"
                $InstanceUpgradeFailCounter.Value += 1
            } else {
                Write-Verbose "$vmsslogheader : Upgrade VMSS instance name $scaleSetInstanceName, $i of $instanceCount"
            }
        } catch [Microsoft.Azure.Commands.Compute.Common.ComputeCloudException] {
            $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
            if ($errorCode -eq "ResourceNotFound") { 
                throw [VirtualMachineScaleSetDoesNotExist]::new($VMssObject, $_.Exception)
            }

            if ($errorCode -eq "ResourceGroupNotFound") {
                throw [ResourceGroupDoesNotExist]::new($VMssObject.ResourceGroupName ,$_.Exception)  
            }

            if ($unexpectedUpgradeExceptionCounter -gt $unexpectedUpgradeExceptionLimit) {
                throw [VirtualMachineScaleSetUnknownException]::new($VMssObject, "More than $unexpectedUpgradeExceptionLimit unexpected exceptions encountered", $_.Exception)
            }

            if ($errorCode -eq "OperationNotAllowed") {
                Write-Host "$vmsslogheader : Failed to locate VMSS instance name $scaleSetInstanceName. Continuing ..."
            } elseif ($errorCode -eq "VMExtensionHandlerNonTransientError") {
                Write-Host "$vmsslogheader : Failed to upgrade extension for VMSS instance name $scaleSetInstanceName, $i of $instanceCount."
                VerboseDisplayException -ErrorRecord $_
                Write-Host "Continuing ..."
            } else {
                Write-Host "$vmsslogheader : Failed to upgrade VMSS instance name $scaleSetInstanceName, $i of $instanceCount. ErrorCode $errorCode." 
                DisplayException -ErrorRecord $_
                Write-Host "Continuing ..."
                $unexpectedUpgradeExceptionCounter += 1
            }
            
            $InstanceUpgradeFailCounter.Value += 1
        }
    }
}

function UpdateVMssExtension {
    <#
    .SYNOPSIS
    Update VMss Extension
    #>

    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
        $VMssObject
    )

    $vmsslogheader = FormatVMssIdentifier -VMssObject $VMssObject

    if (!($PSCmdlet.ShouldProcess($vmsslogheader, "Update VMSS"))) {
        #-WhatIf skip processing here, return to the caller as we have completed our work.
        if ($WhatIfPreference) {
            return $VMssObject
        }
        throw [CustomerSkip]::new()
    }

    Write-Host "$vmsslogheader : Updating VMSS"
    try {
        $VMssObject = Update-AzVmss -VMScaleSetName $VMssObject.Name `
                                    -ResourceGroupName $VMssObject.ResourceGroupName `
                                    -VirtualMachineScaleSet $VMssObject `
                                    -Confirm:$false
        if ($VMssObject.ProvisioningState -ne "Succeeded") {
            throw [VirtualMachineScaleSetOperationFailed]::new($VMssObject, "Failed to update VMSS")
        }
        Write-Host "$vmsslogheader : Successfully updated scale set with extension"
        return $VMssObject                           
    } catch [Microsoft.Azure.Commands.Compute.Common.ComputeCloudException] {
        $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
        if ($errorCode -eq "ParentResourceNotFound") {
            throw [VirtualMachineScaleSetDoesNotExist]::new($VMssObject, $_.Exception)
        } 
        
        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [ResourceGroupDoesNotExist]::new($($VMssObject.ResourceGroupName), $_.Exception)       
        }
        
        if ($errorCode -eq "VMExtensionHandlerNonTransientError") {
            throw [VirtualMachineScaleSetExtensionError]::new($VMssObject, $_.Exception)   
        }

        throw [VirtualMachineScaleSetUnknownException]::new($VMssObject, "Failed to update VMSS", $_.Exception)
    }
}

function Get-MissingUserAssignedIdentities {
    <#
    .SYNOPSIS
    Validates that all User Assigned Managed Identities in the list still exist.
    Returns resource IDs of UAMIs that are referenced but have been deleted.
    #>


    [CmdletBinding()]

    param(
        [Parameter(Mandatory = $True)]
        [string[]]$IdentityIds
    )
    
    $missingUamis = @()

    foreach ($uamiId in $IdentityIds) {
        try {
            $uamiParts = $uamiId -split '/'
            if ($uamiParts.Count -lt 9) {
                Write-Verbose "Invalid UAMI resource ID format: $uamiId"
                continue
            }
            
            $uamiResourceGroup = $uamiParts[4]
            $uamiName = $uamiParts[8]
            
            # Try to get the UAMI - if it doesn't exist, this will throw
            $null = Get-AzUserAssignedIdentity -ResourceGroupName $uamiResourceGroup -Name $uamiName -ErrorAction Stop
        } catch {
            $errorCode = ExtractExceptionPrefixErrCode -ErrorRecord $_
            if ($errorCode -eq "ResourceNotFound") {
                Write-Verbose "UAMI no longer exists: $uamiId"
                $missingUamis += $uamiId
            } else {
                # For other errors (throttling, network, permission), log and skip validation for this UAMI
                # Re-throwing would fail the entire operation; skipping allows the Update call to fail with its own error
                Write-Verbose "Unable to validate UAMI $uamiId (error: $errorCode), skipping validation"
            }
        }

    }
    
    return $missingUamis
}

function Format-UamiForDisplay {
    <#
    .SYNOPSIS
    Formats a UAMI resource ID for user-friendly display.
    Returns format: "name (RG: resourceGroup)" which is unique and readable.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $True)]
        [string]$UamiId
    )
    
    try {
        $parts = $UamiId -split '/'
        if ($parts.Count -ge 9) {
            $resourceGroup = $parts[4]
            $uamiName = $parts[8]
            return "$uamiName (RG: $resourceGroup)"
        }
    } catch {
        Write-Verbose "Failed to parse UAMI ID for display: $UamiId"
    }
    
    # Fallback: return full ID if parsing fails
    return $UamiId
}

function Handle-FailedIdentityOperation {
    <#
    .SYNOPSIS
    Handles FailedIdentityOperation errors by validating which UAMIs are missing.
    Returns user-friendly error details for VMSS or VM operations.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $True)]
        [string]$ResourceType,  # "VMSS" or "VM"
        
        [Parameter(Mandatory = $True)]
        [string]$LogHeader,
        
        [Parameter(Mandatory = $True)]
        [string]$NewUamiId,
        
        [Parameter(Mandatory = $True)]
        [string]$NewUamiName,
        
        [Parameter(Mandatory = $False)]
        [System.Collections.Generic.Dictionary[string,Microsoft.Azure.Management.Compute.Models.UserAssignedIdentitiesValue]]$ExistingUamis,
        
        [Parameter(Mandatory = $True)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )
    
    # First, check if the NEW UAMI being added has been deleted
    $missingNewUami = Get-MissingUserAssignedIdentities -IdentityIds @($NewUamiId)
    if ($missingNewUami.Count -gt 0) {
        Write-Host "$LogHeader : Cannot assign User Assigned Managed Identity '$NewUamiName' because it no longer exists. The UAMI may have been deleted after the script started. Please verify the UAMI exists and try again."
        throw [UserAssignedManagedIdentityDoesNotExist]::new($NewUamiName, $ErrorRecord.Exception)
    }
    
    # Then, check if any EXISTING UAMIs on the resource have been deleted
    if ($ExistingUamis) {
        $missingExistingUamis = Get-MissingUserAssignedIdentities -IdentityIds $ExistingUamis.Keys
        
        if ($missingExistingUamis.Count -gt 0) {
            # Format UAMIs for readable display: "name (RG: resourceGroup)"
            $formattedUamis = $missingExistingUamis | ForEach-Object { Format-UamiForDisplay $_ }
            $missingUamisList = $formattedUamis -join ', '
            
            # Resource-specific documentation links
            $docLink = if ($ResourceType -eq "VMSS") {
                "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/how-to-configure-managed-identities-scale-sets"
            } else {
                "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/how-manage-user-assigned-managed-identities"
            }
            
            Write-Host "$LogHeader : Cannot assign User Assigned Managed Identity because the $ResourceType has invalid UAMI associations. The following UAMIs are associated with this $ResourceType but no longer exist: $missingUamisList. Please disassociate the deleted UAMIs from the $ResourceType before onboarding. See: $docLink"
            
            # Format exception message for plural UAMIs to avoid awkward "uami1, uami2 : User Assigned Managed Identity does not exist."
            $exceptionUamiName = if ($missingExistingUamis.Count -eq 1) { $missingExistingUamis[0] } else { "multiple UAMIs ($missingUamisList)" }
            throw [UserAssignedManagedIdentityDoesNotExist]::new($exceptionUamiName, $ErrorRecord.Exception)
        }
    }
    
    # If we get here, the FailedIdentityOperation error occurred for an unknown reason
    # This could be a transient issue or a permission problem we couldn't diagnose
    throw [UserAssignedManagedIdentityUnknownException]::new("$NewUamiName : FailedIdentityOperation error occurred for an unknown reason. This may be a transient issue or a permission problem. Please try again or verify that the script has sufficient permissions to assign identities on this $ResourceType.", $ErrorRecord.Exception)
}

function AssignVmssUserManagedIdentity {
    <#
    .SYNOPSIS
    Checking if User Assigned Managed Identity is already assigned to VMSS, if not assigning it
    #>

    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(Mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
        $VMssObject
    )

    $userAssignedManagedIdentityName = $UserAssignedManagedIdentityObject.Name
    $vmsslogheader = FormatVmssIdentifier -VMssObject $VmssObject
    $vmssResourceGroupName = $VMssObject.ResourceGroupName
                                  
    if ($VMssObject.Identity.UserAssignedIdentities.Keys -contains $UserAssignedManagedIdentityObject.Id) {
        Write-Host "$vmsslogheader : User Assigned Managed Identity $userAssignedManagedIdentityName already assigned."
        return $VMssObject
    }

    if (!($PSCmdlet.ShouldProcess($vmsslogheader, "Assign User Assigned Managed Identity $userAssignedManagedIdentityName"))) {
        #-WhatIf skip processing here, return to the caller as we have completed our work.
        if ($WhatIfPreference) {
            return $VMssObject
        }
        throw [CustomerSkip]::new()
    }

    $identityType = if ("$($VMssObject.Identity.Type)".StartsWith("SystemAssigned")) { "SystemAssignedUserAssigned" } else { "UserAssigned" }
    $identityList = [System.Collections.Generic.List[string]]@($UserAssignedManagedIdentityObject.Id)
    if ($VMssObject.Identity.UserAssignedIdentities) {
        $identityList += $VMssObject.Identity.UserAssignedIdentities.Keys
    }

    Write-Host "$vmsslogheader : Assigning User Assigned Managed Identity $userAssignedManagedIdentityName"
    try {
        $VMssObject = Update-AzVmss -VMScaleSetName $VMssObject.Name `
                                -ResourceGroupName  $vmssResourceGroupName `
                                -VirtualMachineScaleSet $VMssObject `
                                -IdentityType $identityType `
                                -IdentityID $identityList `
                                -Confirm:$false
        if ($VMssObject.ProvisioningState -ne "Succeeded") {
            throw [VirtualMachineOperationScaleSetFailed]::new($VMssObject, "Failed to assign User Assigned Managed Identity $userAssignedManagedIdentityName")
        }
        
        Write-Host "$vmsslogheader : Successfully assigned User Assigned Managed Identity $userAssignedManagedIdentityName"
        return $VMssObject
    } catch [Microsoft.Azure.Commands.Compute.Common.ComputeCloudException] {
        $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
        if ($errorCode -eq "FailedIdentityOperation") {
            Handle-FailedIdentityOperation `
                -ResourceType "VMSS" `
                -LogHeader $vmsslogheader `
                -NewUamiId $UserAssignedManagedIdentityObject.Id `
                -NewUamiName $userAssignedManagedIdentityName `
                -ExistingUamis $VMssObject.Identity.UserAssignedIdentities `
                -ErrorRecord $_
        }
        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [ResourceGroupDoesNotExist]::new($vmssResourceGroupName, $_.Exception)       
        } 
        if ($errorCode -eq "InvalidParameter") {
            throw [VirtualMachineScaleSetDoesNotExist]::new($VMssObject, $_.Exception) 
        } 
            
        throw [VirtualMachineScaleSetUnknownException]::new($VMssObject, "Failed to assign User Assigned Managed Identity $userAssignedManagedIdentityName", $_.Exception)
    }
}

function AssignVmUserManagedIdentity {
     <#
    .SYNOPSIS
    Checking if User Assigned Managed Identity is already assigned to VM, if not assigning it
    #>

    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Medium')]
    param
    (
        [Parameter(mandatory = $True)]
        [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]
        $VMObject
    )

    $userAssignedManagedIdentityName = $UserAssignedManagedIdentityObject.Name
    $vmlogheader = FormatVmIdentifier -VMObject $VMObject
    $vmResourceGroupName = $VMObject.ResourceGroupName 
    
    if ($VMObject.Identity.UserAssignedIdentities.Keys -contains $UserAssignedManagedIdentityObject.Id) {
        Write-Host "$vmlogheader : User Assigned Managed Identity $userAssignedManagedIdentityName already assigned."
        return $VMObject
    }

    if (!($PSCmdlet.ShouldProcess($vmlogheader, "Assign User Assigned Managed Identity $userAssignedManagedIdentityName"))) {
        #-WhatIf skip processing here, return to the caller as we have completed our work.
        if ($WhatIfPreference) {
            return $VMObject
        } 
        throw [CustomerSkip]::new()
    }

    $identityType = if ("$($VMObject.Identity.Type)".StartsWith("SystemAssigned")) { "SystemAssignedUserAssigned" } else { "UserAssigned" }
    $identityList = [System.Collections.Generic.List[string]]@($UserAssignedManagedIdentityObject.Id)
    if ($VMObject.Identity.UserAssignedIdentities) {
        $identityList += $VMObject.Identity.UserAssignedIdentities.Keys
    }

    Write-Host "$vmlogheader : Assigning User Assigned Managed Identity $userAssignedManagedIdentityName"

    try {
        $result = Update-AzVM -VM $VMObject `
                                -ResourceGroupName $vmResourceGroupName `
                                -IdentityType $identityType `
                                -IdentityID $identityList `
                                -Confirm:$false
    } catch [Microsoft.Azure.Commands.Compute.Common.ComputeCloudException] {
        $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
        if ($errorCode -eq "FailedIdentityOperation") {
            Handle-FailedIdentityOperation `
                -ResourceType "VM" `
                -LogHeader $vmlogheader `
                -NewUamiId $UserAssignedManagedIdentityObject.Id `
                -NewUamiName $userAssignedManagedIdentityName `
                -ExistingUamis $VMObject.Identity.UserAssignedIdentities `
                -ErrorRecord $_
        }
        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [ResourceGroupDoesNotExist]::new($vmResourceGroupName, $_.Exception)       
        }
        if ($errorCode -eq "InvalidParameter") {
            throw [VirtualMachineDoesNotExist]::new($VMObject, $_.Exception)
        }
        
        throw [VirtualMachineUnknownException]::new($VMObject, "Failed to update VM with $userAssignedManagedIdentityName", $_.Exception)
    }

    if (!($result.IsSuccessStatusCode)) {
        throw [VirtualMachineOperationFailed]::new($VMObject, "Failed to assign User Assigned Managed Identity $userAssignedManagedIdentityName. StatusCode : $($result.StatusCode). ReasonPhrase : $($result.ReasonPhrase)")
    }
    
    Write-Host "$vmlogheader : Successfully assigned User Assigned Managed Identity $userAssignedManagedIdentityName"
    return $VMObject
}

function SetManagedIdentityRolesAma {
    <#
    .SYNOPSIS
    Set Roles to a User Assigned Managed Identity
    #>

    param(
        [Parameter(Mandatory = $True)]
        [String]
        $ResourceGroupName
    )
    
    try { 
        $rgObj = Get-AzResourceGroup -Name $ResourceGroupName
    } catch { 
        $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [FatalException]::new("$ResourceGroupName : Not found.", $_.Exception)
        }
         
        throw [FatalException]::new("Unable to lookup Resource Group.", $_.Exception)
    }

    $roles = @("Virtual Machine Contributor", "Azure Connected Machine Resource Administrator", "Log Analytics Contributor")
    Write-Host "($ResourceGroupName) : Assigning roles"
    SetManagedIdentityRoles -ResourceGroupId $rgObj.ResourceId `
                            -UserAssignedManagedIdentity $UserAssignedManagedIdentityObject `
                            -Roles $roles
}

#
# Main Script
#
#
#
try {
    # To report on overall status
    $onboardingCounters = [OnboardingCounters]::new()
    $unknownExceptionVirtualMachineScaleSetConsequentCounter = 0
    $unknownExceptionVirtualMachineConsequentCounter = 0
    $unknownExceptionTotalCounter = 0

    if ($ProcessAndDependencies) {
        throw [FatalException]::new("The -ProcessAndDependencies flag is no longer supported. VM Insights Map and the Dependency Agent are being retired on June 30th, 2028. Please see our retirement guidance for more details: https://aka.ms/DependencyAgentRetirement.", $null)
    }

    if ($PSCmdlet.ParameterSetName -eq 'LogAnalyticsAgent') {
        throw [FatalException]::new("The -WorkspaceId, -WorkspaceKey, and -ReInstall parameters are no longer supported. The Log Analytics agent was retired on August 31, 2024. Please use Azure Monitor Agent (AMA) onboarding instead. See: https://aka.ms/MigrateToAMA", $null)
    }

    # First make sure we are authenticed and Select the subscription supplied and input parameters are valid.
    $account =  Get-AzContext
    if ($null -eq $account.Account) {
        Write-Host "Account Context not found, please login"
        Connect-AzAccount -WhatIf:$False -Confirm:$False -subscriptionid $SubscriptionId
    }
    else {
        if ($account.Subscription.Id -eq $SubscriptionId) {
            Write-Verbose "Subscription : $SubscriptionId is already selected."
            $account
        }
        else {
            Write-Host "Current Subscription :"
            $account
            Write-Host "Changing to subscription : $SubscriptionId"
            Select-AzSubscription -WhatIf:$False -Confirm:$False -SubscriptionId $SubscriptionId
        }
    }

    #script block
    Set-Variable -WhatIf:$False -Confirm:$False -Name sb_nop_block_upgrade -Option Constant -Value { `
        param([Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]$vmssObj, [ref]$instanceUpgradeFailCounter)
    }
    
    $Rghashtable = @{}
    
    try {
        Write-Verbose "Validating ($UserAssignedManagedIdentityResourceGroup, $UserAssignedManagedIdentityName)"
        Set-Variable -WhatIf:$False -Confirm:$False -Name UserAssignedManagedIdentityObject -Option Constant -Value `
                        (Get-AzUserAssignedIdentity -Name $UserAssignedManagedIdentityName `
                                                    -ResourceGroupName $UserAssignedManagedIdentityResourceGroup `
                                                    -ErrorAction Stop)
    } catch {
        $errorCode = ExtractExceptionPrefixErrCode -ErrorRecord $_
        if ($errorCode -eq "ResourceNotFound") {
            throw [UserAssignedManagedIdentityDoesNotExist]::new($UserAssignedManagedIdentityName, $_.Exception)
        }
        
        if ($errorCode -eq "ResourceGroupNotFound") {
            throw [UserAssignedManagedIdentityResourceGroupDoesNotExist]::new($UserAssignedManagedIdentityResourceGroup, $_.Exception)
        }

        throw [UserAssignedManagedIdentityUnknownException]::new("($UserAssignedManagedIdentityResourceGroup) $UserAssignedManagedIdentityName : Failed to locate User Assigned Managed Identity.", $_.Exception)
    }
    
    $local:amaSettings = @{
        'authentication' = @{ 
            'managedIdentity' = @{
                'identifier-name' = 'mi_res_id'
                'identifier-value' = $($UserAssignedManagedIdentityObject.Id) 
            }
        }
    }
    
    Set-Variable -WhatIf:$False -Confirm:$False -Name amaExtensionSettingsVm -Option Constant -Value `
    @{
        "Settings" = $local:amaSettings
    }

    Set-Variable -WhatIf:$False -Confirm:$False -Name amaExtensionSettingsVmss -Option Constant -Value `
    @{
        "Setting" = $local:amaSettings
    }
    
    Set-Variable -WhatIf:$False -Confirm:$False -Name sb_vm -Option Constant -Value { `
        param([Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]$vmObj) `
        OnboardVmiWithAmaVm -VMObject $vmObj -ExtensionSettings $amaExtensionSettingsVm
    }
    Set-Variable -WhatIf:$False -Confirm:$False -Name sb_vmss -Option Constant -Value { `
        param([Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]$vmssObj) `
        OnboardVmiWithAmaVmss -VMssObject $vmssObj -ExtensionSettings $amaExtensionSettingsVmss
    }
    
    Set-Variable -WhatIf:$False -Confirm:$False -Name sb_roles -Option Constant -Value { `
        param([String]$rgName) SetManagedIdentityRolesAma -ResourceGroupName $rgName
    }

    if ($TriggerVmssManualVMUpdate) {
        Set-Variable -WhatIf:$False -Confirm:$False -Name sb_upgrade -Option Constant -Value { `
            param([Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]$vmssObj, [ref]$instanceUpgradeFailCounter) `
            UpgradeVmssExtensionManualUpdateEnabled -VMssObject $vmssObj -InstanceUpgradeFailCounter $instanceUpgradeFailCounter
        }
    } else {
        Set-Variable -WhatIf:$False -Confirm:$False -Name sb_upgrade -Option Constant -Value $sb_nop_block_upgrade
    }

    if ($PolicyAssignmentName) {
        #this section is only for VMs
        Write-Host "Looking up Virtual Machines in policy assignment $PolicyAssignmentName"

        $policyAssignmentNameResources = @{}
        Get-AzPolicyState `
            -Filter "PolicyAssignmentName eq '$PolicyAssignmentName' and ResourceType eq 'Microsoft.Compute/virtualMachines'" `
            | ForEach-Object {
                $policyAssignmentNameResources.Add($_.ResourceId, $True)
              }

        try {
            #Virtual Machines part of a VMSS will be skipped.
            Get-AzVM -ResourceGroupName $ResourceGroup -Name $Name `
                | Where-Object {!($_.VirtualMachineScaleSet) -and $policyAssignmentNameResources.ContainsKey($_.Id)} `
                | ForEach-Object {
                    $onboardingCounters.Total +=1 ;
                    PopulateRgHashTableVm -Rghashtable $Rghashtable -VMObject $_
                }
        } catch {
            $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
            if (!($errorCode -eq "ResourceGroupNotFound" -or $errorCode -eq "ResourceNotFound")) {
                throw [FatalException]::new("Unable to lookup VMs", $_.Exception)
            }

            #quietly do nothing.
        } 
    } else {
        Write-Host ""
        Write-Host "Getting list of VMs or VM Scale Sets matching specified criteria."
        #Skipping heath status check as API output throttling noticed at scale and wild-cards are not accepted with Get-AzVM -Status.
        try {
            Get-AzVM -ResourceGroupName $ResourceGroup -Name $Name `
                | Where-Object {!($_.VirtualMachineScaleSet)} `
                | ForEach-Object { 
                    $onboardingCounters.Total +=1 ; 
                    PopulateRgHashTableVm -Rghashtable $Rghashtable -VMObject $_
                }
        } catch {
            $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
            if (!($errorCode -eq "ResourceGroupNotFound" -or $errorCode -eq "ResourceNotFound")) {
                throw [FatalException]::new("Unable to lookup VMs", $_.Exception)
            }

            #quietly do nothing.
        }

        try {
            #VMI does not support VMSS with flexible orchestration.
            Get-AzVmss -ResourceGroupName $ResourceGroup -Name $Name `
                | Where-Object {$_.OrchestrationMode -ne 'Flexible'} `
                | ForEach-Object {
                    $onboardingCounters.Total +=1 ; 
                    PopulateRgHashTableVmss -RgHashTable $Rghashtable -VMssObject $_
                }
        } catch {
            $errorCode = ExtractExceptionErrorCode -ErrorRecord $_
            if (!($errorCode -eq "ResourceGroupNotFound" -or $errorCode -eq "ResourceNotFound")) {
                throw [FatalException]::new("Unable to lookup VMSS", $_.Exception)
            }

            #quietly do nothing.
        } 
    }

    $rgList = $Rghashtable.GetEnumerator() | Sort-Object -Property Key
    Write-Host "VM's and VMSS matching selection criteria :"
    $ManualUpgrade = 0
    Foreach ($entry in $rgList) {
        $rg = $entry.Key
        $rgTableObj = $entry.Value
        $vmList = $rgTableObj.VirtualMachineList
        $vmssList = $rgTableObj.VirtualMachineScaleSetList
        Write-Host ""
        Write-Host "ResourceGroup : $rg"


        if ($vmList.Count -gt 0) {
            $vmList = Sort-Object -Property Name -InputObject $vmList
            $vmList | ForEach-Object { Write-Host " $($_.Name)" }
            $rgTableObj.VirtualMachineList = $vmList
        }
        
        if ($vmssList.Count -gt 0) {
            $vmssList = Sort-Object -Property Name -InputObject $vmssList
            $vmssList | ForEach-Object { Write-Host " $($_.Name) - Upgrade mode $($_.UpgradePolicy.Mode)"; `
                                         if ($_.UpgradePolicy.Mode -eq "Manual") {$ManualUpgrade+=1}
                                       }
            $rgTableObj.VirtualMachineScaleSetList = $vmssList
        }
    }

    if ($ManualUpgrade -gt 0 -and !$TriggerVmssManualVMUpdate) {
        Write-Host "Found $ManualUpgrade VMSS with upgrade mode 'Manual'."
        Write-Host "'-TriggerVmssManualVMUpdate' was not provided. Upgrade will not be performed." 
    }

    Write-Host ""

    # Validate customer wants to continue
    if ($Approve -or $WhatIfPreference -or $PSCmdlet.ShouldContinue("Continue?", "")) {
        Write-Host ""
    } else {
        Write-Host "You selected No - exiting"
        $onboardingCounters.Total = 0
        exit 1
    }
    
    ForEach ($entry in $rgList) {
        $rg = $entry.Key
        $rgTableObj = $entry.Value
        try {
            &$sb_roles -rgName $rg
            
            foreach ($vm in $rgTableObj.VirtualMachineList) {
                try {
                    Write-Host ""
                    #This is on best-effort basis. State can change later.
                    if (!(IsVmAndGuestAgentUpAndHealthy -VMObject $vm)) {
                        Write-Host "Continuing to the next VM ..."
                        $onboardingCounters.Skipped +=1
                        continue
                    }
                    $vm = &$sb_vm -vmObj $vm
                    Write-Host "$(FormatVmIdentifier -VMObject $vm) : Successfully onboarded VM insights"
                    $onboardingCounters.Succeeded +=1
                    $unknownExceptionVirtualMachineConsequentCounter = 0
                } catch [VirtualMachineUnknownException] {
                    if ($unknownExceptionVirtualMachineConsequentCounter -gt $unknownExceptionVirtualMachineConsequentCounterLimit) {
                        throw [FatalException]::new("More than $unknownExceptionVirtualMachineConsequentCounterLimit unexpected exceptions encountered consequtively", $_.Exception)
                    }
                    if ($unknownExceptionTotalCounter -gt $unknownExceptionTotalCounterLimit) {
                        throw [FatalException]::new("More than $unknownExceptionTotalCounterLimit unexpected exceptions encountered", $_.Exception)
                    }
                    Write-Host "Unexpected VM Exception :"
                    $unknownExceptionTotalCounter+=1
                    $unknownExceptionVirtualMachineConsequentCounter+=1
                    DisplayException -ErrorRecord $_
                    Write-Host "Continuing to the next VM ..."
                } catch [VirtualMachineException] {
                    Write-Host "VM Exception :"
                    VerboseDisplayException -ErrorRecord $_
                    Write-Host "Continuing to the next VM ..."
                } catch [CustomerSkip] {
                    Write-Host "Onboarding operation aborted"
                    Write-Host "Continuing to the next VM ..."
                    $onboardingCounters.Skipped +=1
                }
            }

            foreach ($vmss in $rgTableObj.VirtualMachineScaleSetList) {
                try {
                    Write-Host ""
                    $vmsslogheader = FormatVmssIdentifier -VMssObject $vmss
                    $vmss = &$sb_vmss -vmssObj $vmss
                    $vmss = UpdateVMssExtension -VMssObject $vmss
                    $instanceUpgradeFailCounter = 0
                    if ($vmss.UpgradePolicy.Mode -eq 'Manual') {
                        &$sb_upgrade -vmssObj $vmss -InstanceUpgradeFailCounter ([ref]$instanceUpgradeFailCounter)
                    }
                    $onboardingCounters.VMSSInstanceUpgradeFailure += $instanceUpgradeFailCounter
                    if ($instanceUpgradeFailCounter -eq 0) {
                        Write-Host "$vmsslogheader : Successfully onboarded VM insights"
                        $onboardingCounters.Succeeded +=1
                    }
                    $unknownExceptionVirtualMachineScaleSetConsequentCounter = 0
                } catch [VirtualMachineScaleSetUnknownException] {
                    if ($unknownExceptionVirtualMachineScaleSetConsequentCounter -gt $unknownExceptionVirtualMachineScaleSetConsequentCounterLimit) {
                        throw [FatalException]::new("More than $unknownExceptionVirtualMachineScaleSetConsequentCounterLimit unexpected exceptions encountered consequtively", $_.Exception)
                    }
                    if ($unknownExceptionTotalCounter -gt $unknownExceptionTotalCounterLimit) {
                        throw [FatalException]::new("More than $unknownExceptionTotalCounterLimit unexpected exceptions encountered", $_.Exception)
                    }
                    $unknownExceptionTotalCounter+=1
                    $unknownExceptionVirtualMachineScaleSetConsequentCounter+=1
                    Write-Host "Unexpected VMSS Exception :"
                    DisplayException -ErrorRecord $_
                    Write-Host "Continuing to the next VMSS ..."
                } catch [VirtualMachineScaleSetException] {
                    Write-Host "VMSS Exception :"
                    VerboseDisplayException -ErrorRecord $_
                    Write-Host "Continuing to the next VMSS ..."
                } catch [CustomerSkip] {
                    Write-Host "Onboarding operation aborted"
                    Write-Host "Continuing to the next VMSS ..."
                    $onboardingCounters.Skipped +=1
                }
            }
        } catch [ResourceGroupDoesNotExist] {
            Write-Host "Resource Group Exception :"
            VerboseDisplayException -ErrorRecord $_
            Write-Host "Continuing to the next Resource Group ..."
        } catch [CustomerSkip] {
            Write-Host "Onboarding operation aborted"
            $onboardingCounters.Skipped += $rgTableObj.VirtualMachineList.Count + $rgTableObj.VirtualMachineScaleSetList.Count
        }
    }
}
catch [UserAssignedManagedIdentityUnknownException] {
    Write-Host "Fatal Exception :"
    DisplayException -ErrorRecord $_
    Write-Host "Exiting ..."
    exit 2
}
# Customer actionable exceptions
catch [FatalException] {
    Write-Host "Fatal Exception :"
    VerboseDisplayException -ErrorRecord $_
    Write-Host "Exiting ..."
    exit 3
}
catch {
    Write-Host "Unexpected Fatal Exception :"
    DisplayException -ErrorRecord $_
    Write-Host "Exiting ..."
    exit 4
}
finally {
    PrintSummaryMessage  $onboardingCounters
}