AzSHCI.CloudDeploymentTool.psm1

<#############################################################
 # #
 # Copyright (C) Microsoft Corporation. All rights reserved. #
 # #
 #############################################################>

Import-Module $PSScriptRoot\Classes\reporting.psm1 -Force -DisableNameChecking -Global
$global:RPAPIVersion="2024-04-01"
function Invoke-AzStackHCIEnvironmentPreparator {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,
          
        # AzureCloud , AzureUSGovernment , AzureChinaCloud
        [Parameter(Mandatory = $true, HelpMessage = "Azure Cloud type used for HCI Cluster Deployment. Valid values are : AzureCloud , AzureUSGovernment , AzureChinaCloud")]
        [string] 
        $Cloud,
        
        [Parameter(Mandatory = $true, HelpMessage = "Azure Region used for HCI Cluster Deployment")]
        [string] 
        $Region,
 
        [Parameter(Mandatory = $false, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,
 
        [Parameter(Mandatory = $true, HelpMessage = "Local Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential]
        $LocalAdminCredentials,
         
        [Parameter(Mandatory = $true, HelpMessage = "Cloud Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential] 
        $DomainAdminCredentials,
 
        [Parameter(Mandatory = $true, HelpMessage = "Arc Node ids required for cloud based deployment")]
        [string[]] 
        $ArcNodeIds,
 
        [Parameter(Mandatory = $false, HelpMessage = "Return PSObject result.")]
        [System.Collections.Hashtable] $Tag,
 
        [Parameter(Mandatory = $false, HelpMessage = "Directory path for log and report output")]
        [string]$OutputPath,
 
        [Parameter(Mandatory = $false)]
        [Switch] $Force,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix,

        [Parameter(Mandatory = $false)]
        [System.Collections.Hashtable] $SBESecrets
    )
    try {
        $script:ErrorActionPreference = 'Stop'
        $ProgressPreference = 'SilentlyContinue'
        $DebugPreference = "Continue"
        Set-AzStackHciOutputPath -Path $OutputPath
        if(CheckIfScriptIsRunByAdministrator){
            Log-Info -Message "Script is run as administrator, so enabling" -ConsoleOut
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072;
        }
        else{
            throw "This script should be executed in administrator mode or above"
        }

        $contextStatus = CheckIfAzContextIsSetOrNot
        if($contextStatus)
        {
            Log-Info -Message "Az Context is set, so proceeding" -ConsoleOut
        }
        else
        {
            throw "Az Context is not set , so cannot proceed with environment preparation, please run Connect-AzAccount and retry"
        }

        if ($null -eq $ClusterName)
        {
            Log-Info -Message "Obtained cluster name is null, so getting the cluster Name from the answer file" -ConsoleOut
            $ClusterName = GetClusterNameFromAnswerFile -AnswerFilePath $AnswerFilePath
            Log-Info -Message "Obtained cluster name from answer file is $ClusterName" -ConsoleOut
        }

        Log-Info -Message "Starting AzStackHci Deployment Initialization" -ConsoleOut

        CreateResourceGroupIfNotExists -ResourceGroupName $ResourceGroup -Region $Region

        Log-Info -Message "Registering Resource providers step" -ConsoleOut
        RegisterRequiredResourceProviders

        Log-Info -Message "Creating cluster and assigning permissions for ARC machines" -ConsoleOut
        CreateClusterAndAssignRoles -SubscriptionID $SubscriptionID -ResourceGroup $ResourceGroup -Region $Region -ClusterName $ClusterName

        Log-Info -Message "Creating storage cloud for witness" -ConsoleOut
        CreateStorageAccountForCloudDeployment -ResourceGroup $ResourceGroup -Region $Region -ClusterName $ClusterName -Prefix $Prefix

        Log-Info -Message "Creating key vault and adding the secrets" -ConsoleOut
        if ($null -ne $SBESecrets)
        {
            Log-Info -Message "SBE Secrets is not null" -ConsoleOut
        }
        CreateKeyVaultAndAddSecrets -SubscriptionID $SubscriptionID -ResourceGroup $ResourceGroup -Region $Region -LocalAdminCredentials $LocalAdminCredentials -DomainAdminCredentials $DomainAdminCredentials -ClusterName $ClusterName -Prefix $Prefix -SBESecrets $SBESecrets
    
        Log-Info -Message "Trying to assign the rbac permissions on the Arc Machines" -ConsoleOut
        AssignPermissionsToArcMachines -ArcMachineIds $ArcNodeIds -ResourceGroup $ResourceGroup
        Log-Info -Message "Successfully assigned the rbac permission on the Arc Machines" -ConsoleOut      
        
        Log-Info -Message "Successfully prepared the environment with cluster, storage account and kv" -ConsoleOut
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        $cmdletFailed = $true
        throw $_
    }
    finally {
        $Script:ErrorActionPreference = 'SilentlyContinue'
        Write-AzStackHciArcInstallerFooter -invocation $MyInvocation -Failed:$cmdletFailed -PassThru:$PassThru
        $DebugPreference = "Stop"
    }
}

function Invoke-AzStackHCIEnvironmentPreparatorForClusterUpgrade {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,
          
        # AzureCloud , AzureUSGovernment , AzureChinaCloud
        [Parameter(Mandatory = $true, HelpMessage = "Azure Cloud type used for HCI Cluster Deployment. Valid values are : AzureCloud , AzureUSGovernment , AzureChinaCloud")]
        [string] 
        $Cloud,
        
        [Parameter(Mandatory = $true, HelpMessage = "Azure Region used for HCI Cluster Deployment")]
        [string] 
        $Region,
 
        [Parameter(Mandatory = $false, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,
 
        [Parameter(Mandatory = $true, HelpMessage = "Local Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential]
        $LocalAdminCredentials,
         
        [Parameter(Mandatory = $true, HelpMessage = "Cloud Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential] 
        $DomainAdminCredentials,
 
        [Parameter(Mandatory = $true, HelpMessage = "Arc Node ids required for cloud based deployment")]
        [string[]] 
        $ArcNodeIds,
 
        [Parameter(Mandatory = $false, HelpMessage = "Return PSObject result.")]
        [System.Collections.Hashtable] $Tag,
 
        [Parameter(Mandatory = $false, HelpMessage = "Directory path for log and report output")]
        [string]$OutputPath,
 
        [Parameter(Mandatory = $false)]
        [Switch] $Force,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix,

        [Parameter(Mandatory = $false)]
        [System.Collections.Hashtable] $SBESecrets
    )
    try {
        $script:ErrorActionPreference = 'Stop'
        $ProgressPreference = 'SilentlyContinue'
        $DebugPreference = "Continue"
        Set-AzStackHciOutputPath -Path $OutputPath
        if(CheckIfScriptIsRunByAdministrator){
            Log-Info -Message "Script is run as administrator, so enabling" -ConsoleOut
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072;
        }
        else{
            throw "This script should be executed in administrator mode or above"
        }

        $contextStatus = CheckIfAzContextIsSetOrNot
        if($contextStatus)
        {
            Log-Info -Message "Az Context is set, so proceeding" -ConsoleOut
        }
        else
        {
            throw "Az Context is not set , so cannot proceed with environment preparation, please run Connect-AzAccount and retry"
        }

        if ($null -eq $ClusterName)
        {
            Log-Info -Message "Obtained cluster name is null, so getting the cluster Name from the answer file" -ConsoleOut
            $ClusterName = GetClusterNameFromAnswerFile -AnswerFilePath $AnswerFilePath
            Log-Info -Message "Obtained cluster name from answer file is $ClusterName" -ConsoleOut
        }

        Log-Info -Message "Starting AzStackHci Deployment Initialization" -ConsoleOut

        CreateResourceGroupIfNotExists -ResourceGroupName $ResourceGroup -Region $Region

        Log-Info -Message "Registering Resource providers step" -ConsoleOut
        RegisterRequiredResourceProviders

        Log-Info -Message "Creating key vault and adding the secrets" -ConsoleOut
        if ($null -ne $SBESecrets)
        {
            Log-Info -Message "SBE Secrets is not null" -ConsoleOut
        }
        CreateKeyVaultAndAddSecretsForClusterUpgrade -SubscriptionID $SubscriptionID -ResourceGroup $ResourceGroup -Region $Region -LocalAdminCredentials $LocalAdminCredentials -DomainAdminCredentials $DomainAdminCredentials -ClusterName $ClusterName -Prefix $Prefix -SBESecrets $SBESecrets
    
        Log-Info -Message "Trying to assign the rbac permissions on the Arc Machines" -ConsoleOut
        AssignPermissionsToArcMachines -ArcMachineIds $ArcNodeIds -ResourceGroup $ResourceGroup
        Log-Info -Message "Successfully assigned the rbac permission on the Arc Machines" -ConsoleOut      
        
        Log-Info -Message "Successfully prepared the environment with cluster, storage account and kv" -ConsoleOut
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        $cmdletFailed = $true
        throw $_
    }
    finally {
        $Script:ErrorActionPreference = 'SilentlyContinue'
        Write-AzStackHciArcInstallerFooter -invocation $MyInvocation -Failed:$cmdletFailed -PassThru:$PassThru
        $DebugPreference = "Stop"
    }
}

function Invoke-AzStackHCIEnvironmentValidator {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,
 
        [Parameter(Mandatory = $false, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,
 
        [Parameter(Mandatory = $true, HelpMessage = "Arc Node ids required for cloud based deployment")]
        [string[]] 
        $ArcNodeIds,
 
        [Parameter(Mandatory = $true, HelpMessage = "Answer file path required for deployment")]
        [string] 
        $AnswerFilePath,
 
        [Parameter(Mandatory = $false, HelpMessage = "Return PSObject result.")]
        [System.Collections.Hashtable] $Tag,
 
        [Parameter(Mandatory = $false, HelpMessage = "Directory path for log and report output")]
        [string]$OutputPath,
 
        [Parameter(Mandatory = $false)]
        [Switch] $Force,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix,

        [Parameter(Mandatory = $false)]
        [System.Collections.Hashtable] $SBESecrets,

        [Parameter(Mandatory = $false, HelpMessage = "For testing preview features enable this flag")]
        [bool] $preview
    )
    try {
        $script:ErrorActionPreference = 'Stop'
        $ProgressPreference = 'SilentlyContinue'
        $DebugPreference = "Continue"
        Set-AzStackHciOutputPath -Path $OutputPath
        if(CheckIfScriptIsRunByAdministrator){
            Log-Info -Message "Script is run as administrator, so enabling" -ConsoleOut
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072;
        }
        else{
            throw "This script should be executed in administrator mode or above"
        }
        
        $contextStatus = CheckIfAzContextIsSetOrNot
        if($contextStatus)
        {
            Log-Info -Message "Az Context is set, so proceeding" -ConsoleOut
        }
        else
        {
            throw "Az Context is not set , so cannot proceed with environment validation, please run Connect-AzAccount and retry"
        }

        if ($null -eq $ClusterName)
        {
            Log-Info -Message "Obtained cluster name is null, so getting the cluster Name from the answer file" -ConsoleOut
            $ClusterName = GetClusterNameFromAnswerFile -AnswerFilePath $AnswerFilePath
            Log-Info -Message "Obtained cluster name from answer file is $ClusterName" -ConsoleOut
        }

        Log-Info -Message "Starting Deployment Settings Validation Operation" -ConsoleOut

        $storageAccountName = GetStorageAccountName -ClusterName $ClusterName -Prefix $Prefix
        $KVName = GetKeyVaultName -ClusterName $ClusterName -Prefix $Prefix
        $deploymentSettingsObject = Get-Content $AnswerFilePath | ConvertFrom-Json
        if ($null -eq $deploymentSettingsObject){
            throw "Deployment Settings Object cannot be null"
        }
        Log-Info -Message "Deployment Settings Object obtained is $deploymentSettingsObject" -ConsoleOut

        $kvResource = Get-AzResource -Name $KVName -ResourceType "Microsoft.KeyVault/vaults" -ResourceGroupName $ResourceGroup
        $kvVaultUri = $kvResource.Properties.vaultUri
        Log-Info -Message "Key Vault Uri obtained is $kvVaultUri" -ConsoleOut
        
        if ($null -ne $SBESecrets)
        {
            $sbeCredetailsList = CreateSBECredentialsList -SBESecrets $SBESecrets -kvVaultUri $kvVaultUri
        }
        # Will Trigger Validate first
        $deploymentSettingsParameters = ReplaceDeploymentSettingsParametersTemplateWithActualValues -deploymentSettingsObject $deploymentSettingsObject -clusterName $ClusterName -arcNodeResourceIds $ArcNodeIds -storageAccountName $storageAccountName -secretsLocation $kvVaultUri -sbeCredentialsList $sbeCredetailsList -preview $preview -Prefix $Prefix
        if ($null -eq $deploymentSettingsParameters){
            throw "Deployment Settings Parameters cannot be null"
        }
        $deploymentSettingsParameters.parameters.deploymentMode.value = "Validate"

        Log-Info -Message "Deployment settings parameters obtained is $deploymentSettingsParameters" -ConsoleOut
        
        $deploymentSettingsParametersJson = $deploymentSettingsParameters | ConvertTo-Json -Depth 100
        Log-Info -Message "Deployment Settings Parameters to json is $deploymentSettingsParametersJson" -ConsoleOut

        $updatedDeploymentSettingsParametersFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\DeploymentSettingsReportedPropertiesValidate.json")
        Log-Info -Message "Updated Deployment Settings Parameters File Path $updatedDeploymentSettingsParametersFilePath" -ConsoleOut
        Set-Content -Path $updatedDeploymentSettingsParametersFilePath -Value $deploymentSettingsParametersJson | Out-Null

        $deploymentSettingsTemplateFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Templates\DeploymentSettingsTemplate.json")
        if ($preview)
        {
            Log-Info -Message "The preview flag is enabled so picked up the preview deployment settings template" -ConsoleOut
            $deploymentSettingsTemplateFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Templates\DeploymentSettingsTemplatePreview.json")
        }
        $deploymentIdentifier = [guid]::NewGuid().ToString().Split("-")[0]
        $deploymentSettingsValidationName = $ResourceGroup + "-DSValidate" + $deploymentIdentifier
        Log-Info -Message "Deployment Settings Template File Path $deploymentSettingsTemplateFilePath and Deployment Name $deploymentSettingsDeploymentName" -ConsoleOut

        $currentDeploymentNameFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\DeploymentName.txt")
        Log-Info -Message "Current Deployment File Path where the deployment name is stored is $currentDeploymentNameFilePath" -ConsoleOut
        Set-Content -Path $currentDeploymentNameFilePath -Value $deploymentSettingsValidationName | Out-Null

        $resourceGroupDeploymentStatus = New-AzResourceGroupDeployment -Name $deploymentSettingsValidationName -ResourceGroupName $ResourceGroup -TemplateFile $deploymentSettingsTemplateFilePath -TemplateParameterFile $updatedDeploymentSettingsParametersFilePath -Force -Verbose   -AsJob
        $deploystatusString = $resourceGroupDeploymentStatus | Out-String
        Log-Info -Message "Triggered Validated the deployment Settings Resource $deploystatusString" -ConsoleOut

        Start-Sleep -Seconds 120
        $entireDeploymentStatus = Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroup -Name $deploymentSettingsValidationName
        $deploymentStatus = $entireDeploymentStatus | Format-Table ResourceGroupName, DeploymentName, ProvisioningState
        $deploystatusString = $deploymentStatus | Out-String 
        Log-Info -Message "Triggered Validated the deployment Settings Resource $deploystatusString" -ConsoleOut

        $validationOperation = Get-AzResourceGroupDeploymentOperation -DeploymentName $deploymentSettingsValidationName -ResourceGroup $ResourceGroup | ConvertTo-Json
        Log-Info -Message "Current Deployment Settings Validation Operation is $validationOperation" -ConsoleOut

        if ($deploymentStatus.ProvisioningState -eq "Failed")
        {
            Log-Info -Message "The deployment status is already in a failed state , Entire deployment Status : $entireDeploymentStatus" -ConsoleOut
            throw "The deployment status is in a failed state, status = $deploystatusString"
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    finally {
        $Script:ErrorActionPreference = 'SilentlyContinue'
        Write-AzStackHciArcInstallerFooter -invocation $MyInvocation -Failed:$cmdletFailed -PassThru:$PassThru
        $DebugPreference = "Stop"
    }
}

function Invoke-AzStackHCIEnvironmentValidatorForClusterUpgrade {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,
 
        [Parameter(Mandatory = $false, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,
 
        [Parameter(Mandatory = $true, HelpMessage = "Arc Node ids required for cloud based deployment")]
        [string[]] 
        $ArcNodeIds,
 
        [Parameter(Mandatory = $true, HelpMessage = "Answer file path required for deployment")]
        [string] 
        $AnswerFilePath,
 
        [Parameter(Mandatory = $false, HelpMessage = "Return PSObject result.")]
        [System.Collections.Hashtable] $Tag,
 
        [Parameter(Mandatory = $false, HelpMessage = "Directory path for log and report output")]
        [string]$OutputPath,
 
        [Parameter(Mandatory = $false)]
        [Switch] $Force,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix,

        [Parameter(Mandatory = $false)]
        [System.Collections.Hashtable] $SBESecrets
        
    )
    try {
        $script:ErrorActionPreference = 'Stop'
        $ProgressPreference = 'SilentlyContinue'
        $DebugPreference = "Continue"
        Set-AzStackHciOutputPath -Path $OutputPath
        if(CheckIfScriptIsRunByAdministrator){
            Log-Info -Message "Script is run as administrator, so enabling" -ConsoleOut
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072;
        }
        else{
            throw "This script should be executed in administrator mode or above"
        }
        
        $contextStatus = CheckIfAzContextIsSetOrNot
        if($contextStatus)
        {
            Log-Info -Message "Az Context is set, so proceeding" -ConsoleOut
        }
        else
        {
            throw "Az Context is not set , so cannot proceed with environment validation, please run Connect-AzAccount and retry"
        }

        if ($null -eq $ClusterName)
        {
            Log-Info -Message "Obtained cluster name is null, so getting the cluster Name from the answer file" -ConsoleOut
            $ClusterName = GetClusterNameFromAnswerFile -AnswerFilePath $AnswerFilePath
            Log-Info -Message "Obtained cluster name from answer file is $ClusterName" -ConsoleOut
        }

        Log-Info -Message "Starting Deployment Settings Validation Operation" -ConsoleOut

        $KVName = GetKeyVaultName -ClusterName $ClusterName -Prefix $Prefix
        $deploymentSettingsObject = Get-Content $AnswerFilePath | ConvertFrom-Json
        if ($null -eq $deploymentSettingsObject){
            throw "Deployment Settings Object cannot be null"
        }
        Log-Info -Message "Deployment Settings Object obtained is $deploymentSettingsObject" -ConsoleOut

        $kvResource = Get-AzResource -Name $KVName -ResourceType "Microsoft.KeyVault/vaults" -ResourceGroupName $ResourceGroup
        $kvVaultUri = $kvResource.Properties.vaultUri
        Log-Info -Message "Key Vault Uri obtained is $kvVaultUri" -ConsoleOut
        
        if ($null -ne $SBESecrets)
        {
            $sbeCredetailsList = CreateSBECredentialsList -SBESecrets $SBESecrets -kvVaultUri $kvVaultUri
        }
        # Will Trigger Validate first
        $deploymentSettingsParameters = ReplaceDeploymentSettingsParametersTemplateWithActualValuesForClusterUpgrade -deploymentSettingsObject $deploymentSettingsObject -clusterName $ClusterName -arcNodeResourceIds $ArcNodeIds -secretsLocation $kvVaultUri -sbeCredentialsList $sbeCredetailsList -Prefix $Prefix
        if ($null -eq $deploymentSettingsParameters){
            throw "Deployment Settings Parameters cannot be null"
        }
        $deploymentSettingsParameters.parameters.deploymentMode.value = "Validate"
        $deploymentSettingsParameters.parameters.operationType.value = "ClusterUpgrade"

        Log-Info -Message "Deployment settings parameters obtained is $deploymentSettingsParameters" -ConsoleOut
        
        $deploymentSettingsParametersJson = $deploymentSettingsParameters | ConvertTo-Json -Depth 100
        Log-Info -Message "Deployment Settings Parameters to json is $deploymentSettingsParametersJson" -ConsoleOut

        $updatedDeploymentSettingsParametersFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\DeploymentSettingsReportedPropertiesValidate.json")
        Log-Info -Message "Updated Deployment Settings Parameters File Path $updatedDeploymentSettingsParametersFilePath" -ConsoleOut
        Set-Content -Path $updatedDeploymentSettingsParametersFilePath -Value $deploymentSettingsParametersJson | Out-Null

        $deploymentSettingsTemplateFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Templates\DeploymentSettingsTemplateForClusterUpgrade.json")
        $deploymentIdentifier = [guid]::NewGuid().ToString().Split("-")[0]
        $deploymentSettingsValidationName = $ResourceGroup + "-DSValidate" + $deploymentIdentifier
        Log-Info -Message "Deployment Settings Template File Path $deploymentSettingsTemplateFilePath and Deployment Name $deploymentSettingsDeploymentName" -ConsoleOut

        $currentDeploymentNameFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\DeploymentName.txt")
        Log-Info -Message "Current Deployment File Path where the deployment name is stored is $currentDeploymentNameFilePath" -ConsoleOut
        Set-Content -Path $currentDeploymentNameFilePath -Value $deploymentSettingsValidationName | Out-Null

        $resourceGroupDeploymentStatus = New-AzResourceGroupDeployment -Name $deploymentSettingsValidationName -ResourceGroupName $ResourceGroup -TemplateFile $deploymentSettingsTemplateFilePath -TemplateParameterFile $updatedDeploymentSettingsParametersFilePath -Force -Verbose   -AsJob
        $deploystatusString = $resourceGroupDeploymentStatus | Out-String
        Log-Info -Message "Triggered Validated the deployment Settings Resource $deploystatusString" -ConsoleOut

        Start-Sleep -Seconds 120
        $entireDeploymentStatus = Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroup -Name $deploymentSettingsValidationName
        $deploymentStatus = $entireDeploymentStatus | Format-Table ResourceGroupName, DeploymentName, ProvisioningState
        $deploystatusString = $deploymentStatus | Out-String 
        Log-Info -Message "Triggered Validated the deployment Settings Resource $deploystatusString" -ConsoleOut

        $validationOperation = Get-AzResourceGroupDeploymentOperation -DeploymentName $deploymentSettingsValidationName -ResourceGroup $ResourceGroup | ConvertTo-Json
        Log-Info -Message "Current Deployment Settings Validation Operation is $validationOperation" -ConsoleOut

        if ($deploymentStatus.ProvisioningState -eq "Failed")
        {
            Log-Info -Message "The deployment status is already in a failed state , Entire deployment Status : $entireDeploymentStatus" -ConsoleOut
            throw "The deployment status is in a failed state, status = $deploystatusString"
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    finally {
        $Script:ErrorActionPreference = 'SilentlyContinue'
        Write-AzStackHciArcInstallerFooter -invocation $MyInvocation -Failed:$cmdletFailed -PassThru:$PassThru
        $DebugPreference = "Stop"
    }
}

function Invoke-AzStackHCIDeployment {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,
 
        [Parameter(Mandatory = $false, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,
 
        [Parameter(Mandatory = $true, HelpMessage = "Arc Node ids required for cloud based deployment")]
        [string[]] 
        $ArcNodeIds,
 
        [Parameter(Mandatory = $true, HelpMessage = "Answer file path required for deployment")]
        [string] 
        $AnswerFilePath,
 
        [Parameter(Mandatory = $false, HelpMessage = "Return PSObject result.")]
        [System.Collections.Hashtable] $Tag,
 
        [Parameter(Mandatory = $false, HelpMessage = "Directory path for log and report output")]
        [string]$OutputPath,
 
        [Parameter(Mandatory = $false)]
        [Switch] $Force,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix,

        [Parameter(Mandatory = $false, HelpMessage = "For testing preview features enable this flag")]
        [bool] $preview
    )
    try {
        $script:ErrorActionPreference = 'Stop'
        $ProgressPreference = 'SilentlyContinue'
        $DebugPreference = "Continue"
        Set-AzStackHciOutputPath -Path $OutputPath
        if(CheckIfScriptIsRunByAdministrator){
            Log-Info -Message "Script is run as administrator, so enabling" -ConsoleOut
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072;
        }
        else{
            throw "This script should be executed in administrator mode or above"
        }
        
        $contextStatus = CheckIfAzContextIsSetOrNot
        if($contextStatus)
        {
            Log-Info -Message "Az Context is set, so proceeding" -ConsoleOut
        }
        else
        {
            throw "Az Context is not set , so cannot proceed with deployment, please run Connect-AzAccount and retry"
        }

        if ($null -eq $ClusterName)
        {
            Log-Info -Message "Obtained cluster name is null, so getting the cluster Name from the answer file" -ConsoleOut
            $ClusterName = GetClusterNameFromAnswerFile -AnswerFilePath $AnswerFilePath
            Log-Info -Message "Obtained cluster name from answer file is $ClusterName" -ConsoleOut
        }

        Log-Info -Message "Starting Deployment Settings Validation Operation" -ConsoleOut

        $storageAccountName = GetStorageAccountName -ClusterName $ClusterName -Prefix $Prefix
        $KVName = GetKeyVaultName -ClusterName $ClusterName -Prefix $Prefix
        $deploymentSettingsObject = Get-Content $AnswerFilePath | ConvertFrom-Json
        if ($null -eq $deploymentSettingsObject){
            throw "Deployment Settings Object cannot be null"
        }
        Log-Info -Message "Deployment Settings Object obtained is $deploymentSettingsObject" -ConsoleOut

        $kvResource = Get-AzResource -Name $KVName -ResourceType "Microsoft.KeyVault/vaults" -ResourceGroupName $ResourceGroup
        $kvVaultUri = $kvResource.Properties.vaultUri
        Log-Info -Message "Key Vault Uri obtained is $kvVaultUri" -ConsoleOut

        # Will Trigger Deployment
        $deploymentSettingsParameters = ReplaceDeploymentSettingsParametersTemplateWithActualValues -deploymentSettingsObject $deploymentSettingsObject -clusterName $ClusterName -arcNodeResourceIds $ArcNodeIds -storageAccountName $storageAccountName -secretsLocation $kvVaultUri -preview $preview -Prefix $Prefix
        if ($null -eq $deploymentSettingsParameters){
            throw "Deployment Settings Parameters cannot be null"
        }
        $deploymentSettingsParameters.parameters.deploymentMode.value = "Deploy"

        Log-Info -Message "Deployment settings parameters obtained is $deploymentSettingsParameters" -ConsoleOut
        
        $deploymentSettingsParametersJson = $deploymentSettingsParameters | ConvertTo-Json -Depth 100
        Log-Info -Message "Deployment Settings Parameters to json is $deploymentSettingsParametersJson" -ConsoleOut

        $updatedDeploymentSettingsParametersFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\DeploymentSettingsReportedPropertiesDeploy.json")
        Log-Info -Message "Updated Deployment Settings Parameters File Path $updatedDeploymentSettingsParametersFilePath" -ConsoleOut
        Set-Content -Path $updatedDeploymentSettingsParametersFilePath -Value $deploymentSettingsParametersJson | Out-Null

        $deploymentSettingsTemplateFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Templates\DeploymentSettingsTemplate.json")
        if ($preview)
        {
            Log-Info -Message "The preview flag is enabled so picked up the preview deployment settings template" -ConsoleOut
            $deploymentSettingsTemplateFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Templates\DeploymentSettingsTemplatePreview.json")
        }
        $deploymentIdentifier = [guid]::NewGuid().ToString().Split("-")[0]
        $deploymentSettingsValidationName = $ResourceGroup + "-DSDeploy" + $deploymentIdentifier
        Log-Info -Message "Deployment Settings Template File Path $deploymentSettingsTemplateFilePath and Deployment Name $deploymentSettingsValidationName" -ConsoleOut

        $currentDeploymentNameFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\DeploymentName.txt")
        Log-Info -Message "Current Deployment File Path where the deployment name is stored is $currentDeploymentNameFilePath" -ConsoleOut
        Set-Content -Path $currentDeploymentNameFilePath -Value $deploymentSettingsValidationName | Out-Null

        New-AzResourceGroupDeployment -Name $deploymentSettingsValidationName -ResourceGroupName $ResourceGroup -TemplateFile $deploymentSettingsTemplateFilePath -TemplateParameterFile $updatedDeploymentSettingsParametersFilePath -Force -Verbose  -AsJob
        Start-Sleep -Seconds 120
        $deploymentStatus = Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroup -Name $deploymentSettingsValidationName  
        $deploystatusString = $deploymentStatus | Out-String 
        Log-Info -Message "Triggered the deployment Settings Resource in deploy mode: $deploystatusString " -ConsoleOut

        $deploymentOperation = Get-AzResourceGroupDeploymentOperation -DeploymentName $deploymentSettingsValidationName -ResourceGroup $ResourceGroup | ConvertTo-Json
        Log-Info -Message "Current Deployment Settings Validation Operation is $deploymentOperation" -ConsoleOut
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    finally {
        $Script:ErrorActionPreference = 'SilentlyContinue'
        Write-AzStackHciArcInstallerFooter -invocation $MyInvocation -Failed:$cmdletFailed -PassThru:$PassThru
        $DebugPreference = "Stop"
    }
}

function Invoke-AzStackHCIUpgrade {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,
 
        [Parameter(Mandatory = $false, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,
 
        [Parameter(Mandatory = $true, HelpMessage = "Arc Node ids required for cloud based deployment")]
        [string[]] 
        $ArcNodeIds,
 
        [Parameter(Mandatory = $true, HelpMessage = "Answer file path required for deployment")]
        [string] 
        $AnswerFilePath,
 
        [Parameter(Mandatory = $false, HelpMessage = "Return PSObject result.")]
        [System.Collections.Hashtable] $Tag,
 
        [Parameter(Mandatory = $false, HelpMessage = "Directory path for log and report output")]
        [string]$OutputPath,
 
        [Parameter(Mandatory = $false)]
        [Switch] $Force,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix
    )
    try {
        $script:ErrorActionPreference = 'Stop'
        $ProgressPreference = 'SilentlyContinue'
        $DebugPreference = "Continue"
        Set-AzStackHciOutputPath -Path $OutputPath
        if(CheckIfScriptIsRunByAdministrator){
            Log-Info -Message "Script is run as administrator, so enabling" -ConsoleOut
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072;
        }
        else{
            throw "This script should be executed in administrator mode or above"
        }
        
        $contextStatus = CheckIfAzContextIsSetOrNot
        if($contextStatus)
        {
            Log-Info -Message "Az Context is set, so proceeding" -ConsoleOut
        }
        else
        {
            throw "Az Context is not set , so cannot proceed with deployment, please run Connect-AzAccount and retry"
        }

        if ($null -eq $ClusterName)
        {
            Log-Info -Message "Obtained cluster name is null, so getting the cluster Name from the answer file" -ConsoleOut
            $ClusterName = GetClusterNameFromAnswerFile -AnswerFilePath $AnswerFilePath
            Log-Info -Message "Obtained cluster name from answer file is $ClusterName" -ConsoleOut
        }

        Log-Info -Message "Starting Deployment Settings Validation Operation" -ConsoleOut

        $storageAccountName = GetStorageAccountName -ClusterName $ClusterName -Prefix $Prefix
        $KVName = GetKeyVaultName -ClusterName $ClusterName -Prefix $Prefix
        $deploymentSettingsObject = Get-Content $AnswerFilePath | ConvertFrom-Json
        if ($null -eq $deploymentSettingsObject){
            throw "Deployment Settings Object cannot be null"
        }
        Log-Info -Message "Deployment Settings Object obtained is $deploymentSettingsObject" -ConsoleOut

        $kvResource = Get-AzResource -Name $KVName -ResourceType "Microsoft.KeyVault/vaults" -ResourceGroupName $ResourceGroup
        $kvVaultUri = $kvResource.Properties.vaultUri
        Log-Info -Message "Key Vault Uri obtained is $kvVaultUri" -ConsoleOut

        # Will Trigger Upgrade
        $deploymentSettingsParameters = ReplaceDeploymentSettingsParametersTemplateWithActualValuesForClusterUpgrade -deploymentSettingsObject $deploymentSettingsObject -clusterName $ClusterName -arcNodeResourceIds $ArcNodeIds -secretsLocation $kvVaultUri -Prefix $Prefix
        if ($null -eq $deploymentSettingsParameters){
            throw "Deployment Settings Parameters cannot be null"
        }
        $deploymentSettingsParameters.parameters.deploymentMode.value = "Deploy"
        $deploymentSettingsParameters.parameters.operationType.value = "ClusterUpgrade"

        Log-Info -Message "Deployment settings parameters obtained is $deploymentSettingsParameters" -ConsoleOut
        
        $deploymentSettingsParametersJson = $deploymentSettingsParameters | ConvertTo-Json -Depth 100
        Log-Info -Message "Deployment Settings Parameters to json is $deploymentSettingsParametersJson" -ConsoleOut

        $updatedDeploymentSettingsParametersFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\DeploymentSettingsReportedPropertiesDeploy.json")
        Log-Info -Message "Updated Deployment Settings Parameters File Path $updatedDeploymentSettingsParametersFilePath" -ConsoleOut
        Set-Content -Path $updatedDeploymentSettingsParametersFilePath -Value $deploymentSettingsParametersJson | Out-Null

        $deploymentSettingsTemplateFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Templates\DeploymentSettingsTemplateForClusterUpgrade.json")
        $deploymentIdentifier = [guid]::NewGuid().ToString().Split("-")[0]
        $deploymentSettingsValidationName = $ResourceGroup + "-DSDeploy" + $deploymentIdentifier
        Log-Info -Message "Deployment Settings Template File Path $deploymentSettingsTemplateFilePath and Deployment Name $deploymentSettingsDeploymentName" -ConsoleOut

        $currentDeploymentNameFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\DeploymentName.txt")
        Log-Info -Message "Current Deployment File Path where the deployment name is stored is $currentDeploymentNameFilePath" -ConsoleOut
        Set-Content -Path $currentDeploymentNameFilePath -Value $deploymentSettingsValidationName | Out-Null

        New-AzResourceGroupDeployment -Name $deploymentSettingsValidationName -ResourceGroupName $ResourceGroup -TemplateFile $deploymentSettingsTemplateFilePath -TemplateParameterFile $updatedDeploymentSettingsParametersFilePath -Force -Verbose  -AsJob
        Start-Sleep -Seconds 120
        $deploymentStatus = Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroup -Name $deploymentSettingsValidationName  
        $deploystatusString = $deploymentStatus | Out-String 
        Log-Info -Message "Triggered the deployment Settings Resource in deploy mode: $deploystatusString " -ConsoleOut

        $deploymentOperation = Get-AzResourceGroupDeploymentOperation -DeploymentName $deploymentSettingsValidationName -ResourceGroup $ResourceGroup | ConvertTo-Json
        Log-Info -Message "Current Deployment Settings Validation Operation is $deploymentOperation" -ConsoleOut
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    finally {
        $Script:ErrorActionPreference = 'SilentlyContinue'
        Write-AzStackHciArcInstallerFooter -invocation $MyInvocation -Failed:$cmdletFailed -PassThru:$PassThru
        $DebugPreference = "Stop"
    }
}

function Invoke-AzStackHCIFullDeployment {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,
          
        # AzureCloud , AzureUSGovernment , AzureChinaCloud
        [Parameter(Mandatory = $true, HelpMessage = "Azure Cloud type used for HCI Cluster Deployment. Valid values are : AzureCloud , AzureUSGovernment , AzureChinaCloud")]
        [string] 
        $Cloud,
         
        [Parameter(Mandatory = $true, HelpMessage = "Azure Region used for HCI Cluster Deployment")]
        [string] 
        $Region,
 
        [Parameter(Mandatory = $false, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,
 
        [Parameter(Mandatory = $true, HelpMessage = "Local Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential]
        $LocalAdminCredentials,
         
        [Parameter(Mandatory = $true, HelpMessage = "Cloud Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential] 
        $DomainAdminCredentials,
 
        [Parameter(Mandatory = $true, HelpMessage = "Arc Node ids required for cloud based deployment")]
        [string[]] 
        $ArcNodeIds,
 
        [Parameter(Mandatory = $true, HelpMessage = "Answer file path required for deployment")]
        [string] 
        $AnswerFilePath,
 
        [Parameter(Mandatory = $false, HelpMessage = "Return PSObject result.")]
        [System.Collections.Hashtable] $Tag,
 
        [Parameter(Mandatory = $false, HelpMessage = "Directory path for log and report output")]
        [string]$OutputPath,
 
        [Parameter(Mandatory = $false)]
        [Switch] $Force,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix
    )
    try {
        $script:ErrorActionPreference = 'Stop'
        $ProgressPreference = 'SilentlyContinue'
        $DebugPreference = "Continue"
        Set-AzStackHciOutputPath -Path $OutputPath
        if(CheckIfScriptIsRunByAdministrator){
            Log-Info -Message "Script is run as administrator, so enabling"
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072;
        }
        else{
            throw "This script should be executed in administrator mode or above"
        }

        $contextStatus = CheckIfAzContextIsSetOrNot
        if($contextStatus)
        {
            Log-Info -Message "Az Context is set, so proceeding" -ConsoleOut
        }
        else
        {
            throw "Az Context is not set , so cannot proceed with environment preparation, please run Connect-AzAccount and retry"
        }

        if ($null -eq $ClusterName)
        {
            Log-Info -Message "Obtained cluster name is null, so getting the cluster Name from the answer file" -ConsoleOut
            $ClusterName = GetClusterNameFromAnswerFile -AnswerFilePath $AnswerFilePath
            Log-Info -Message "Obtained cluster name from answer file is $ClusterName" -ConsoleOut
        }
        Log-Info -Message "Starting AzStackHci Full Deployment" -ConsoleOut

        $environmentPreparationParameters = @{
            SubscriptionID = $SubscriptionID
            ResourceGroup = $ResourceGroup
            Region = $Region
            ClusterName = $ClusterName
            LocalAdminCredentials = $LocalAdminCredentials
            DomainAdminCredentials = $DomainAdminCredentials
            ArcNodeIds = $ArcNodeIds
            Tag = $Tag
            OutputPath = $OutputPath
            Force = $Force
            Prefix = $Prefix
        }
        Log-Info -Message "Successfully got the parameters for environment validation" -ConsoleOut
        Invoke-AzStackHCIEnvironmentPreparator @environmentPreparationParameters

        Log-Info -Message "Successfully prepared the environment for cloud deployment, triggering validation"

        $deploymentSettingsParameters = @{
            SubscriptionID = $SubscriptionID
            ResourceGroup = $ResourceGroup
            Region = $Region
            ClusterName = $ClusterName
            ArcNodeIds = $ArcNodeIds
            AnswerFilePath = $AnswerFilePath
            Tag = $Tag
            OutputPath = $OutputPath
            Force = $Force
            Prefix = $Prefix
        }
        Log-Info -Message "Successfully got the parameters for deployment settings validation" -ConsoleOut
        Invoke-AzStackHCIEnvironmentValidator @deploymentSettingsParameters

        Log-Info -Message "Started polling on the environment validation status"
        PollDeploymentSettingsStatus -SubscriptionID $SubscriptionID -ResourceGroup $ResourceGroup -ClusterName $ClusterName
        Log-Info -Message "Environment Validation succeeded , so moving to the deployment stage" -ConsoleOut
        
        Invoke-AzStackHCIDeployment @deploymentSettingsParameters
        Log-Info -Message "Starting polling on the deployment action plan"
        PollDeploymentSettingsStatus -SubscriptionID $SubscriptionID -ResourceGroup $ResourceGroup -ClusterName $ClusterName
        Log-Info -Message "Congrats, the Azure Stack HCI cluster has been deployed successfully"
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        $cmdletFailed = $true
        Log-Info -Message "Clearing the resource group since deployment failed"
        Remove-AzResourceGroup -Name $ResourceGroup -Force -Verbose
        throw $_
    }
    finally {
        $Script:ErrorActionPreference = 'SilentlyContinue'
        Write-AzStackHciArcInstallerFooter -invocation $MyInvocation -Failed:$cmdletFailed -PassThru:$PassThru
        $DebugPreference = "Stop"
    }
     
}

enum EdgeDeviceKind {
    HCI
}

function New-EdgeDevices
{
    param (
        # AzureCloud , AzureUSGovernment , AzureChinaCloud
        [Parameter(Mandatory = $true, HelpMessage = "Azure Cloud type used for HCI Cluster Deployment. Valid values are : AzureCloud , AzureUSGovernment , AzureChinaCloud")]
        [string] 
        $Cloud,
         
        [Parameter(Mandatory = $true, HelpMessage = "Azure Region used for HCI Cluster Deployment")]
        [string] 
        $Region,        

        [Parameter(Mandatory = $true, HelpMessage = "Arc Node for which EdgeDevice resource needs to be created")]
        [string] 
        $ArcNodeId,

        [Parameter(Mandatory = $true, HelpMessage = "Edge Device Kind that needs to be created")]
        [EdgeDeviceKind] 
        $kind,

        [Parameter(Mandatory = $false, HelpMessage = "Directory path for log and report output")]
        [string]$OutputPath
    )
    try
    {
        $extensionInstallationStatus = $true
        $script:ErrorActionPreference = 'Stop'
        $ProgressPreference = 'SilentlyContinue'
        $DebugPreference = "Continue"
        Set-AzStackHciOutputPath -Path $OutputPath
        $contextStatus = CheckIfAzContextIsSetOrNot
        if($contextStatus)
        {
            Log-Info -Message "Az Context is set, so proceeding" -ConsoleOut
        }
        else
        {
            throw "Az Context is not set , so cannot proceed, please run Connect-AzAccount and retry"
        }

        
        $edgeDeviceNodeId = "$($ArcNodeId)/providers/Microsoft.AzureStackHCI/edgeDevices/default" 
        $edgeDevicesValidateEndpointWithAPI = "{0}?api-version={1}" -f $edgeDeviceNodeId,  $global:RPAPIVersion

        Log-Info -Message "EdgeDevices Endpoint URI : $edgeDevicesValidateEndpointWithAPI" -ConsoleOut
    
        $edgeDeviceResource = [PSCustomObject]@{
            kind = "HCI"
            properties = @{}
        }
        $edgeDeviceResourcePayload = $edgeDeviceResource | ConvertTo-Json -Depth 100
        Log-Info -Message "EdgeDevices payload : $($jsonString) " -ConsoleOut
        $response = Invoke-AzRestMethod -Path $edgeDevicesValidateEndpointWithAPI -Method PUT -Payload $edgeDeviceResourcePayload
        if ($response.StatusCode -eq 200)
        {
            Log-Info -Message "Feature is not enabled yet in this cloud region " -ConsoleOut
            throw "Extension installation from cloud is not enabled yet in this cloud region "
        }
        elseif($response.StatusCode -ge 300)
        {
            Log-Info -Message "Failure in creating EdgeDevices resource " -ConsoleOut
            throw "Failure while creating EdgeDevices resource statuscode: $($response.StatusCode) , edgeDeviceID: $($edgeDeviceNodeId) "
        }
        Log-Info -Message "EdgeDevices Creation response : $($response.StatusCode) " -ConsoleOut
        $asyncURL = $response.Headers.GetValues("Azure-AsyncOperation")[0]
        $stopLoop = $false
        $status = $false
        do 
        {
            Log-Info -Message "Querying EdgeDevice creation status using : $asyncURL " -ConsoleOut
            $response = Invoke-AzRestMethod -URI $asyncURL -Method GET
            Log-Info -Message "EdgeDevice creation status: $response " -ConsoleOut
            $validationResponse = $response.Content | ConvertFrom-Json
            $prettyResponse = $validationResponse | ConvertTo-Json -Depth 100
            Log-Info -Message "Validation status $prettyResponse" -ConsoleOut
            if( $validationResponse.status.Equals("Provisioning") -or $validationResponse.status.Equals("Accepted") )
            {
                Start-Sleep -Seconds 25
            }
            else
            {
                $stopLoop = $true
                Log-Info -Message "EdgeDevice creation has completed, checking whether extension installation has succeeded" -ConsoleOut
                $status = $validationResponse.status.Equals("Succeeded")
                Log-Info -Message "Extension installation completed with status $status" -ConsoleOut
            }        
        }
        While (-Not $stopLoop)

        $response = Invoke-AzRestMethod -Path $edgeDevicesValidateEndpointWithAPI -Method GET
        $edgeDevicesJson = $response.Content | ConvertFrom-Json
        $edgeDeviceExtensionProfile = $edgeDevicesJson.properties.reportedProperties.extensionProfile | ConvertTo-Json -Depth 100
        Log-Info -Message " EdgeDevice $edgeDevicesJson.id extension profile: $edgeDeviceExtensionProfile" -ConsoleOut
        
        if ($edgeDevicesJson.properties.reportedProperties.extensionProfile.extensions.Count -ne 4 ) 
        {
            $extensionInstallationStatus = $false
            Log-Info -Message "Count of Extension [ $edgeDevicesJson.properties.reportedProperties.extensionProfile.extensions.Count ] status does not match the expected Extensions [4]" -ConsoleOut
        }
        foreach ($extensionState in $edgeDevicesJson.properties.reportedProperties.extensionProfile.extensions) {
            if ($extensionState.state -ne "Succeeded") {
                $extensionInstallationStatus = $false
                Log-Info -Message "$extensionState.extensionName is in $extensionState.state which is not expected state [Succeeded] " -ConsoleOut
                
            }
        }

    }
    catch
    {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        $status = $false
        throw $_
    }
    finally
    {
        $Script:ErrorActionPreference = 'SilentlyContinue'
        Write-AzStackHciArcInstallerFooter -invocation $MyInvocation -Failed:$cmdletFailed -PassThru:$PassThru
        $DebugPreference = "Stop"
    }
    if ($extensionInstallationStatus)
    {
        Log-Info -Message "All mandatory extensions were successfully installed" -ConsoleOut
        return $extensionInstallationStatus
    }
    else
    {
        throw "Extension installation status was not successful, check the logs to debug the issue"
    }

}
function Invoke-validateNodesForDeployment
{
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,

        # AzureCloud , AzureUSGovernment , AzureChinaCloud
        [Parameter(Mandatory = $true, HelpMessage = "Azure Cloud type used for HCI Cluster Deployment. Valid values are : AzureCloud , AzureUSGovernment , AzureChinaCloud")]
        [string] 
        $Cloud,
         
        [Parameter(Mandatory = $true, HelpMessage = "Azure Region used for HCI Cluster Deployment")]
        [string] 
        $Region,        

        [Parameter(Mandatory = $true, HelpMessage = "Arc Node ids required for cloud based deployment")]
        [string[]] 
        $ArcNodeIds,

        [Parameter(Mandatory = $false, HelpMessage = "Directory path for log and report output")]
        [string]$OutputPath
    )
    try
    {
        $script:ErrorActionPreference = 'Stop'
        $ProgressPreference = 'SilentlyContinue'
        $DebugPreference = "Continue"
        Set-AzStackHciOutputPath -Path $OutputPath
        $contextStatus = CheckIfAzContextIsSetOrNot
        if($contextStatus)
        {
            Log-Info -Message "Az Context is set, so proceeding" -ConsoleOut
        }
        else
        {
            throw "Az Context is not set , so cannot proceed with environment preparation, please run Connect-AzAccount and retry"
        }

        $edgeDeviceNodeIds=@()
        foreach ($arcResourceID in $ArcNodeIds) 
        {
            $edgeDeviceNodeIds += "$($arcResourceID)/providers/Microsoft.AzureStackHCI/edgeDevices/default"
        }

        $edgeDevicesValidateEndpointWithAPI = "{0}/validate?api-version={1}" -f $edgeDeviceNodeIds[0],  $global:RPAPIVersion
        Log-Info -Message "Validation Endpoint Uri : $edgeDevicesValidateEndpointWithAPI" -ConsoleOut
    
        $parameters = @{EdgeDeviceIds=$edgeDeviceNodeIds}
        $jsonString = $parameters | ConvertTo-Json
        Log-Info -Message "Validation action payload : $($jsonString) " -ConsoleOut
        $response = Invoke-AzRestMethod -Path $edgeDevicesValidateEndpointWithAPI -Method POST -Payload $jsonString
        Log-Info -Message "Validation action response : $($response.StatusCode) " -ConsoleOut
        $asyncURL = $response.Headers.GetValues("Azure-AsyncOperation")
        $stopLoop = $false
        $status = $false
        do 
        {
            Log-Info -Message "Querying validation status using : $asyncURL[0] " -ConsoleOut
            $response = Invoke-AzRestMethod -URI $asyncURL[0] -Method GET
            Log-Info -Message "validation Response: $response " -ConsoleOut
            $validationResponse = $response.Content | ConvertFrom-Json
            $prettyResponse = $validationResponse | ConvertTo-Json -Depth 100
            Log-Info -Message "Validation status $prettyResponse" -ConsoleOut
            if( $validationResponse.status.Equals("Inprogress") -or $validationResponse.status.Equals("Accepted") )
            {
                Start-Sleep -Seconds 10
            }
            else
            {
                $stopLoop = $true
                Log-Info -Message "Validation has completed, checking whether validation has succeeded" -ConsoleOut
                $status = $validationResponse.status.Equals("Succeeded")
                Log-Info -Message "Validation Response succeeded status $status" -ConsoleOut
            }        
        }
        While (-Not $stopLoop)
    }
    catch
    {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        $status = $false
        throw $_
    }
    finally
    {
        $Script:ErrorActionPreference = 'SilentlyContinue'
        Write-AzStackHciArcInstallerFooter -invocation $MyInvocation -Failed:$cmdletFailed -PassThru:$PassThru
        $DebugPreference = "Stop"
    }
    if ($status)
    {
        Log-Info -Message "The status of node validation is successful" -ConsoleOut
        return $status
    }
    else
    {
        throw "Node validation was unsuccessful, check the logs to debug the issue"
    }
}

function CheckIfAzContextIsSetOrNot {
    try {
        $context = Get-AzContext
        if ([string]::IsNullOrEmpty($context)){
            Log-Info -Message "Az Context is Not Set, so cannot run the operation" -ConsoleOut
            return $false
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        return $false
    }
    return $true
}
function GetClusterNameFromAnswerFile {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Answer File Path")]
        [string] 
        $AnswerFilePath
    )
    try {
        $deploymentSettingsObject = Get-Content $AnswerFilePath | ConvertFrom-Json
        if ($null -eq $deploymentSettingsObject){
            throw "Deployment Settings Object cannot be null"
        }
        $deploymentDataFromAnswerFile = $deploymentSettingsObject.ScaleUnits[0].DeploymentData
        $clusterName = $deploymentDataFromAnswerFile.Cluster.Name
        Log-Info -Message "Cluster Name obtained in answer file is $clusterName" -ConsoleOut
        if ($null -ne $clusterName)
        {
            Log-Info -Message "Cluster Name is not null, so returning clustername $clusterName" -ConsoleOut
            return $clusterName
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    return $null
}

function CreateKeyVaultAndAddSecretsForClusterUpgrade {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true, HelpMessage = "Azure Region used for HCI Cluster Deployment")]
        [string] 
        $Region,
 
        [Parameter(Mandatory = $true, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,

        [Parameter(Mandatory = $true, HelpMessage = "Local Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential]
        $LocalAdminCredentials,
         
        [Parameter(Mandatory = $true, HelpMessage = "Cloud Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential] 
        $DomainAdminCredentials,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix,

        [Parameter(Mandatory = $false)]
        [System.Collections.Hashtable] $SBESecrets
    )
    try {
        Log-Info -Message "Initializing the flow where the kv for cluster upgrade creation starts" -ConsoleOut
        # $storageAccountName = GetStorageAccountName -ClusterName $ClusterName -Prefix $Prefix
        $KVName = GetKeyVaultName -ClusterName $ClusterName -Prefix $Prefix
        # $storageWitnessKey = GetStorageWitnessKey -SubscriptionId $SubscriptionID -ResourceGroup $ResourceGroup -StorageAccountName $storageAccountName
        # if ($null -eq $storageWitnessKey){
        # throw "Storage Witness Key is null, so cannot proceed with deployment"
        # }
        # Log-Info -Message "Successfully received the storage witness key for storage account $storageAccountName" -ConsoleOut
        # $storageWitnessKeyB64Encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($storageWitnessKey))
        #Starting to create the spn for ARB Deployment
        $spnDisplayName = GetSpnName -ClusterName $ClusterName -Prefix $Prefix
        $servicePrincialCreds = CreateServicePrincipalForCloudDeployment -DisplayName $spnDisplayName -ResourceGroup $ResourceGroup
        if ($null -eq $servicePrincialCreds){
            throw "Service Principal Credentials are null, so cannot proceed with deployment"
        }
        Log-Info -Message "Successfully created the service principal and the corresponding credentials to put in the kv" -ConsoleOut

        Log-Info -Message "Starting Key Vault Creation...." -ConsoleOut

        $localAdminSecret = ExtractUsernameAndPasswordFromCredential -Credential $LocalAdminCredentials
        if ($null -eq $localAdminSecret){
            throw "Local Admin secret cannot be null, so cannot proceed with deployment"
        }
        Log-Info -Message "Successfully extracted and encoded the Local Admin Credentials"

        $domainAdminSecret = ExtractUsernameAndPasswordFromCredential -Credential $DomainAdminCredentials
        if ($null -eq $domainAdminSecret){
            throw "Domain Admin secret cannot be null, so cannot proceed with deployment"
        }
        Log-Info -Message "Successfully extracted and encoded the Domain Admin Credentials"

        $updatedSBESecretsList = ExtractSBECredentialsToKeyVaulepairs -Credentials $SBESecrets

        $keyVaultParameters = ReplaceKeyVaultTemplateWithActualValuesForClusterUpgrade -KVName $KVName -Region $Region -DomainAdminSecret $domainAdminSecret -ArbDeploymentSpnSecret $servicePrincialCreds -ClusterName $ClusterName -SBESecrets $updatedSBESecretsList
        if ($null -eq $keyVaultParameters){
            throw "Key Vault parameters file could not be updated with actual values"
        }
        $deploymentIdentifier = [guid]::NewGuid().ToString().Split("-")[0]
        $KVDeploymentName = $KVName + "-KVDeploy" + $deploymentIdentifier
        $kvTemplateFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Templates\KeyVaultTemplate.json")
        Log-Info -Message "Key Vault Template file path $kvTemplateFilePath" -ConsoleOut
        $keyVaultParametersJson = $keyVaultParameters | ConvertTo-Json -Depth 4
        Log-Info -Message "Json value of key vault parameters $keyVaultParametersJson" -ConsoleOut
        $updatedKVParametersFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\KeyVaultReportedParameters.json")
        Set-Content -Path $updatedKVParametersFilePath -Value $keyVaultParametersJson | Out-Null
        New-AzResourceGroupDeployment -Name $KVDeploymentName -ResourceGroupName $ResourceGroup -TemplateFile $kvTemplateFilePath -TemplateParameterFile $updatedKVParametersFilePath -Force
        $kvDeploymentStatus = Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroup -DeploymentName $KVDeploymentName
        if ($kvDeploymentStatus.ProvisioningState -eq "Succeeded"){
            Log-Info -Message "Successfully deployed the KV with name $KVName" -ConsoleOut
        }
        else{
            throw "KV Deployment Failed so not proceeding with the deployment"
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
}

function CreateKeyVaultAndAddSecrets {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true, HelpMessage = "Azure Region used for HCI Cluster Deployment")]
        [string] 
        $Region,
 
        [Parameter(Mandatory = $true, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,

        [Parameter(Mandatory = $true, HelpMessage = "Local Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential]
        $LocalAdminCredentials,
         
        [Parameter(Mandatory = $true, HelpMessage = "Cloud Admin Credentials Required for deployment")]
        [System.Management.Automation.PSCredential] 
        $DomainAdminCredentials,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix,

        [Parameter(Mandatory = $false)]
        [System.Collections.Hashtable] $SBESecrets
    )
    try {
        Log-Info -Message "Initializing the flow where the kv creation starts" -ConsoleOut
        $storageAccountName = GetStorageAccountName -ClusterName $ClusterName -Prefix $Prefix
        $KVName = GetKeyVaultName -ClusterName $ClusterName -Prefix $Prefix
        $storageWitnessKey = GetStorageWitnessKey -SubscriptionId $SubscriptionID -ResourceGroup $ResourceGroup -StorageAccountName $storageAccountName
        if ($null -eq $storageWitnessKey){
            throw "Storage Witness Key is null, so cannot proceed with deployment"
        }
        Log-Info -Message "Successfully received the storage witness key for storage account $storageAccountName" -ConsoleOut
        $storageWitnessKeyB64Encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($storageWitnessKey))
        #Starting to create the spn for ARB Deployment
        $spnDisplayName = GetSpnName -ClusterName $ClusterName -Prefix $Prefix
        $servicePrincialCreds = CreateServicePrincipalForCloudDeployment -DisplayName $spnDisplayName -ResourceGroup $ResourceGroup
        if ($null -eq $servicePrincialCreds){
            throw "Service Principal Credentials are null, so cannot proceed with deployment"
        }
        Log-Info -Message "Successfully created the service principal and the corresponding credentials to put in the kv" -ConsoleOut

        Log-Info -Message "Starting Key Vault Creation...." -ConsoleOut

        $localAdminSecret = ExtractUsernameAndPasswordFromCredential -Credential $LocalAdminCredentials
        if ($null -eq $localAdminSecret){
            throw "Local Admin secret cannot be null, so cannot proceed with deployment"
        }
        Log-Info -Message "Successfully extracted and encoded the Local Admin Credentials"

        $domainAdminSecret = ExtractUsernameAndPasswordFromCredential -Credential $DomainAdminCredentials
        if ($null -eq $domainAdminSecret){
            throw "Domain Admin secret cannot be null, so cannot proceed with deployment"
        }
        Log-Info -Message "Successfully extracted and encoded the Domain Admin Credentials"

        $updatedSBESecretsList = ExtractSBECredentialsToKeyVaulepairs -Credentials $SBESecrets

        $keyVaultParameters = ReplaceKeyVaultTemplateWithActualValues -KVName $KVName -Region $Region -LocalAdminSecret $localAdminSecret -DomainAdminSecret $domainAdminSecret -ArbDeploymentSpnSecret $servicePrincialCreds -StorageWitnessKey $storageWitnessKeyB64Encoded -ClusterName $ClusterName  -SBESecrets $updatedSBESecretsList
        if ($null -eq $keyVaultParameters){
            throw "Key Vault parameters file could not be updated with actual values"
        }
        $deploymentIdentifier = [guid]::NewGuid().ToString().Split("-")[0]
        $KVDeploymentName = $KVName + "-KVDeploy" + $deploymentIdentifier
        $kvTemplateFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Templates\KeyVaultTemplate.json")
        Log-Info -Message "Key Vault Template file path $kvTemplateFilePath" -ConsoleOut
        $keyVaultParametersJson = $keyVaultParameters | ConvertTo-Json -Depth 4
        Log-Info -Message "Json value of key vault parameters $keyVaultParametersJson" -ConsoleOut
        $updatedKVParametersFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\KeyVaultReportedParameters.json")
        Set-Content -Path $updatedKVParametersFilePath -Value $keyVaultParametersJson | Out-Null
        New-AzResourceGroupDeployment -Name $KVDeploymentName -ResourceGroupName $ResourceGroup -TemplateFile $kvTemplateFilePath -TemplateParameterFile $updatedKVParametersFilePath -Force
        $kvDeploymentStatus = Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroup -DeploymentName $KVDeploymentName
        if ($kvDeploymentStatus.ProvisioningState -eq "Succeeded"){
            Log-Info -Message "Successfully deployed the KV with name $KVName" -ConsoleOut
        }
        else{
            throw "KV Deployment Failed so not proceeding with the deployment"
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
}

function CreateStorageAccountForCloudDeployment {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,
 
        [Parameter(Mandatory = $true, HelpMessage = "Azure Region used for HCI Cluster Deployment")]
        [string] 
        $Region,
 
        [Parameter(Mandatory = $true, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix
    )
    try {
        Log-Info -Message "Starting to create the storage account for deployment" -ConsoleOut
        #Perform Storage Account Deployment here

        $storageAccountName = GetStorageAccountName -ClusterName $ClusterName -Prefix $Prefix
        $deploymentIdentifier = [guid]::NewGuid().ToString().Split("-")[0]
        $storageAccountDeploymentName = $storageAccountName + "sadeployment" + $deploymentIdentifier
        Log-Info -Message "Trying to create storage account with name $storageAccountName and Deployment Name $storageAccountDeploymentName" -ConsoleOut
        $storageAccountParameters = ReplaceStorageAccountTemplateWithActualValues -StorageAccountName $storageAccountName -Location $Region
        if ($null -ne $storageAccountParameters){
            $storageAccountTemplateFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Templates\StorageAccountTemplate.json")
            Log-Info -Message "Storage Account Template File Path $storageAccountTemplateFilePath"
            $storageAccountParametersJson = $storageAccountParameters | ConvertTo-Json
            Log-Info -Message "Storage Account Parameters Converted to JSON is $storageAccountParametersJson" -ConsoleOut
            $updatedStorageAccountParametersFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\StorageAccountReportedParameters.json")
            Log-Info -Message "Updated Storage Account Parameters File Path is $updatedStorageAccountParametersFilePath" -ConsoleOut
            Set-Content -Path $updatedStorageAccountParametersFilePath -Value $storageAccountParametersJson | Out-Null
            New-AzResourceGroupDeployment -Name $storageAccountDeploymentName -ResourceGroupName $ResourceGroup -TemplateFile $storageAccountTemplateFilePath -TemplateParameterFile $updatedStorageAccountParametersFilePath -Force
            $statusOfStorageAccountDeployment = Get-AzResourceGroupDeployment -ResourceGroupName $ResourceGroup -DeploymentName $storageAccountDeploymentName
            if ($statusOfStorageAccountDeployment.ProvisioningState -eq "Succeeded"){
                Log-Info -Message "Storage Account $storageAccountName is created successfully" -ConsoleOut
            }
            else{
                throw "Storage account deployment with name $storageAccountName and deploymentName $storageAccountDeploymentName failed"
            }
        }
        else{
            throw "Could not replace storage account parameter template with the parameter values"
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
}

function CreateClusterAndAssignRoles {
    [CmdletBinding(DefaultParametersetName = 'AZContext')]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,

        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,
 
        [Parameter(Mandatory = $true, HelpMessage = "Azure Region used for HCI Cluster Deployment")]
        [string] 
        $Region,
 
        [Parameter(Mandatory = $true, HelpMessage = "Azure Stack HCI Cluster Name for Registration")]
        [string] 
        $ClusterName
    )
    try {
        # Checking if cluster is already deployed
        DeleteClusterResourceIfAlreadyExists -ClusterName $ClusterName -SubscriptionID $SubscriptionID -ResourceGroupName $ResourceGroup

        # Trying to create the cluster object
        $properties = [ResourceProperties]::new($Region, @{})
        $payload = ConvertTo-Json -InputObject $properties
        Log-Info -Message "Payload for cluster creation is $payload" -ConsoleOut
        $resourceId = "/subscriptions/$SubscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.AzureStackHCI/clusters/$ClusterName"
        $resourceIdApiVersion = "{0}?api-version={1}" -f $resourceId, $global:RPAPIVersion
        Log-Info -Message "Resource Id is $resourceId" -ConsoleOut
        $clusterResult = New-ClusterWithRetries -ResourceIdWithAPI $resourceIdApiVersion -Payload $payload
        if ($clusterResult -eq $false) {
            throw "Cluster creation with name $ClusterName failed in $Region with Resource Group $ResourceGroup"
        }

        $clusterResource = Get-AzResource -ResourceId $resourceId -ApiVersion $global:RPAPIVersion -ErrorAction SilentlyContinue
        if ($null -ne $clusterResource) {
            Log-Info -Message "Successfully created the cluster resource $clusterResource" -ConsoleOut

            #Assigning permission to the HCI first party object id on the resource group level
            AssignRolesToHCIResourceProvider -ResourceGroup $ResourceGroup -hciObjectId $clusterResource.Properties.resourceProviderObjectId
        }
        else {
            throw "Cluster creation with name $ClusterName failed in $Region with Resource Group $ResourceGroup"
        }

    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
}

function PollDeploymentSettingsStatus {
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Azure Subscription Id for HCI Cluster Deployment")]
        [string]
        $SubscriptionID,
          
        [Parameter(Mandatory = $true, HelpMessage = "Azure Resource group used for HCI Cluster Deployment")]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true)]
        [string] $ClusterName
    )
    $deploymentSettingsResourceUri = "/subscriptions/$SubscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.AzureStackHCI/clusters/$ClusterName/deploymentSettings/default"
    Log-Info -Message "Deployment Settings Resource Uri is $deploymentSettingsResourceUri" -ConsoleOut
    $currentDeploymentNameFilePath = (Join-Path  -Path $env:TEMP -ChildPath "\DeploymentName.txt")
    Log-Info -Message "Current Deployment Name stored file path is $currentDeploymentNameFilePath" -ConsoleOut
    $currentDeploymentName = Get-Content -Path $currentDeploymentNameFilePath
    $currentDeploymentName = $currentDeploymentName.Trim()
    Log-Info -Message "Current Deployment Name obtained is $currentDeploymentName" -ConsoleOut
    $stopLoop = $false
    $status = $false
    $currentDeploymentSettingsResource = $null
    $currentDeploymentOperationStatus = $null
    do {
        $deploymentSettingsResource = Get-AzResource -ResourceId $deploymentSettingsResourceUri -ApiVersion $global:RPAPIVersion -Verbose
        $currentDeploymentSettingsResource = $deploymentSettingsResource | ConvertTo-Json
        Log-Info -Message "Deployment Settings Resource obtained is $currentDeploymentSettingsResource" -ConsoleOut
        $provisioningState = $deploymentSettingsResource.properties.provisioningState
        if (("Succeeded" -eq $provisioningState) -or ("Failed" -eq $provisioningState) -or ("Cancelled" -eq $provisioningState)){
            $stopLoop = $true
            if (("Succeeded" -eq $provisioningState)){
                $status = $true
            }
            Log-Info -Message "Provisioning State has reached a terminal state, so closing the operation" -ConsoleOut
        }
        if (-Not [string]::IsNullOrEmpty($currentDeploymentName))
        {
            $currentDeploymentOperationStatus = Get-AzResourceGroupDeploymentOperation -DeploymentName $currentDeploymentName -ResourceGroupName $ResourceGroup | ConvertTo-Json
            Log-Info -Message "Current Deployment Operation status is $currentDeploymentOperationStatus" -ConsoleOut
        }
        $reportedProperties = $deploymentSettingsResource.properties.reportedProperties
        $reportedPropertiesJson = $reportedProperties | ConvertTo-Json
        Log-Info -Message "Reported Properties obtained is $reportedPropertiesJson" -ConsoleOut
        Start-Sleep -Seconds 120
    }
    While (-Not $stopLoop)
    if (-not $status)
    {
        Log-Info -Message "The current deployment settings resource is in failed state, so throwing an exception, deployment settings resource = $currentDeploymentSettingsResource" -ConsoleOut
        throw "Deployment Settings resource is in Failed state , current deployment operation = $currentDeploymentOperationStatus"
    }
}

function RegisterRequiredResourceProviders {
    try {
        Log-Info -Message "Registering required resource providers" -ConsoleOut
        Register-RPIfRequired -ProviderNamespace "Microsoft.HybridCompute"
        Register-RPIfRequired -ProviderNamespace "Microsoft.GuestConfiguration"
        Register-RPIfRequired -ProviderNamespace "Microsoft.HybridConnectivity"
        Register-RPIfRequired -ProviderNamespace "Microsoft.AzureStackHCI"
        Register-RPIfRequired -ProviderNamespace "Microsoft.Storage"
        Register-RPIfRequired -ProviderNamespace "Microsoft.KeyVault"
        Register-RPIfRequired -ProviderNamespace "Microsoft.ResourceConnector"
        Register-RPIfRequired -ProviderNamespace "Microsoft.HybridContainerService"
        Register-RPIfRequired -ProviderNamespace "Microsoft.Attestation"
        Log-Info -Message "Successfully registered Resource Providers" -ConsoleOut
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    
}

function Register-RPIfRequired{
    param(
        [string] $ProviderNamespace
    )
    $rpState = Get-AzResourceProvider -ProviderNamespace $ProviderNamespace
    $notRegisteredResourcesForRP = ($rpState.Where({$_.RegistrationState  -ne "Registered"}) | Measure-Object ).Count
    if ($notRegisteredResourcesForRP -eq 0 )
    { 
        Log-Info -Message "$ProviderNamespace RP already registered, skipping registration" -ConsoleOut
    } 
    else
    {
        try
        {
            Register-AzResourceProvider -ProviderNamespace $ProviderNamespace | Out-Null
            Log-Info -Message "registered Resource Provider: $ProviderNamespace " -ConsoleOut
        }
        catch
        {
            Log-Info -Message  -Message "Exception occured while registering $ProviderNamespace RP, $_" -ConsoleOut   
            throw 
        }
    }
}

function GetStorageAccountName {
    param (
        [Parameter(Mandatory = $true)]
        [string] $ClusterName,

        [Parameter(Mandatory = $false)]
        [string] $Prefix
    )
    try {
        $storageAccountName = $ClusterName + "sa"
        if ([string]::IsNullOrEmpty($Prefix)) {
            Log-Info -Message "Storage account name with null prefix is $storageAccountName" -ConsoleOut
        }
        else {
            $storageAccountName = $storageAccountName + $Prefix
            Log-Info -Message "Storage account name appended with prefix is $storageAccountName" -ConsoleOut
        }
        $storageAccountName = $storageAccountName -replace "[^a-zA-Z0-9]", ""
        $storageAccountName = $storageAccountName.ToLower()
        if ($storageAccountName.Length -gt 24) {
            $storageAccountName = $storageAccountName.Substring(0, 24)
        }
        Log-Info -Message "Storage account name is $storageAccountName" -ConsoleOut
        return $storageAccountName
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
}

function GetKeyVaultName {
    param (
        [Parameter(Mandatory = $true)]
        [string] $ClusterName,

        [Parameter(Mandatory = $false)]
        [string] $Prefix
    )
    try {
        $KVName = $ClusterName + "-KV"
        if ([string]::IsNullOrEmpty($Prefix)) {
            Log-Info -Message "KV Name with without prefix is $KVName" -ConsoleOut
        }
        else {
            $KVName = $KVName + $Prefix
            Log-Info -Message "KV Name with unique prefix provided by user is $KVName" -ConsoleOut
        }
        $KVName = $KVName -replace "[^a-zA-Z0-9]", ""
        $KVName = $KVName.ToLower()
        if ($KVName.Length -gt 24) {
            $KVName = $KVName.Substring(0, 24)
        }
        Log-Info -Message "Key Vault name is $KVName" -ConsoleOut
        return $KVName
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
}

function GetSpnName {
    param (
        [Parameter(Mandatory = $true)]
        [string] $ClusterName,

        [Parameter(Mandatory = $false)]
        [string] $Prefix
    )
    try {
        $spnDisplayName = $ClusterName + "-SPN"
        if ([string]::IsNullOrEmpty($Prefix)) 
        {
            Log-Info -Message "Spn display name without prefix is $spnDisplayName" -ConsoleOut
        }
        else
        {
            $spnDisplayName = $ClusterName + "-SPN" + $Prefix
            Log-Info -Message "Spn display name with prefix is $spnDisplayName" -ConsoleOut
        }
        return $spnDisplayName
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
}

function CheckIfKVAlreadyExists {
    param (
        [Parameter(Mandatory = $true)]
        [string] $KVName,

        [Parameter(Mandatory = $true)]
        [string] $ResourceGroupName
    )
    try {
        $kvAccount = Get-AzResource -Name $KVName -ResourceType "Microsoft.KeyVault/vaults" -ResourceGroupName $ResourceGroupName -ErrorAction SilentlyContinue
        if (($null -ne $kvAccount) -and ($null -ne $kvAccount.properties.ProvisioningState)){
            $status = $kvAccount.properties.ProvisioningState
            if (($status -eq "Succeeded")){
                Log-Info -Message "Key Vault with the same name $kvAccount exists in the Resource Group $ResourceGroupName" -ConsoleOut
                return [ErrorDetail]::KeyVaultAlreadyExists
            }
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return [ErrorDetail]::NotFound
}

function CheckIfStorageAccountAlreadyExists {
    param (
        [Parameter(Mandatory = $true)]
        [string] $StorageAccountName,

        [Parameter(Mandatory = $true)]
        [string] $ResourceGroupName
    )
    try {
        $storageAccount = Get-AzResource -Name $StorageAccountName -ResourceType "Microsoft.Storage/storageAccounts" -ResourceGroupName $ResourceGroupName -ErrorAction SilentlyContinue
        if (($null -ne $storageAccount) -and ($null -ne $storageAccount.properties.ProvisioningState)){
            $status = $storageAccount.properties.ProvisioningState
            if (($status -eq "Succeeded")){
                Log-Info -Message "Storage Account with the same name $StorageAccountName exists in the Resource Group $ResourceGroupName" -ConsoleOut
                return [ErrorDetail]::StorageAccountAlreadyExists
            }
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return [ErrorDetail]::NotFound
}

function DeleteClusterResourceIfAlreadyExists {
    param (
        [Parameter(Mandatory = $true)]
        [string] $ClusterName,

        [Parameter(Mandatory = $true)]
        [string] $SubscriptionID,

        [Parameter(Mandatory = $true)]
        [string] $ResourceGroupName
    )
    try {
        $clusterResourceUri = "/subscriptions/$SubscriptionID/resourceGroups/$ResourceGroupName/providers/Microsoft.AzureStackHCI/clusters/$ClusterName"
        Log-Info -Message "Cluster Resource Uri obtained is $clusterResourceUri" -ConsoleOut
        $stopLoop = $false
        do{
            $clusterResource = Get-AzResource -ResourceId $clusterResourceUri -ApiVersion $global:RPAPIVersion -ErrorAction SilentlyContinue
            Log-Info -Message "Current cluster resource obtained is $clusterResource" -ConsoleOut
            if ($null -ne $clusterResource)
            {
                Log-Info -Message "Current cluster resource is not null, so deleting the cluster resource" -ConsoleOut
                Remove-AzResource -ResourceId $clusterResourceUri -Force
                Log-Info -Message "Successfully triggered delete of the cluster resource $clusterResourceUri" -ConsoleOut
            }
            else
            {
                Log-Info -Message "Current cluster resource is deleted, so returning" -ConsoleOut
                $stopLoop = true
            }
            Start-Sleep -Seconds 120
        }
        While (-Not $stopLoop)

        Log-Info -Message "Triggering a force delete of the cluster even though it is null" -ConsoleOut
        Remove-AzResource -ResourceId $clusterResourceUri -Force -ErrorAction SilentlyContinue
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
}

function GetStorageWitnessKey {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $SubscriptionId,

        [Parameter(Mandatory = $true)]
        [string] $ResourceGroup,

        [Parameter(Mandatory = $true)]
        [string] $StorageAccountName
    )

    try {
        $resourceId = "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Storage/storageAccounts/{2}" -f $SubscriptionId, $ResourceGroup, $StorageAccountName
        Log-Info -Message "Resource id of storage account is $resourceId" -ConsoleOut
        $res = Invoke-AzResourceAction -ResourceId $resourceId -Action "listKeys" -ApiVersion "2023-01-01" -Force
        Log-Info -Message "Successfully got the keys for the storage account $StorageAccountName" -ConsoleOut
        if (($null -ne $res) -and ($res.keys.Count -gt 0)){
            return $res.keys[0].value
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return $null
}
function ReplaceDeploymentSettingsParametersTemplateWithActualValues {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $deploymentSettingsObject,

        [Parameter(Mandatory = $true)]
        [string] $clusterName,

        [Parameter(Mandatory = $true)]
        [string[]] $arcNodeResourceIds,

        [Parameter(Mandatory = $true)]
        [string] $storageAccountName,

        [Parameter(Mandatory = $true)]
        [string] $secretsLocation,

        [Parameter(Mandatory = $false)]
        [PSCustomObject[]]  $sbeCredentialsList,

        [Parameter(Mandatory = $false, HelpMessage = "For testing preview features enable this flag")]
        [bool] $preview,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix
    )
    try {
        $customLocationName = $clusterName + "-customlocation"
        $KVName = GetKeyVaultName -ClusterName $ClusterName -Prefix $Prefix
        $deploymentDataFromAnswerFile = $deploymentSettingsObject.ScaleUnits[0].DeploymentData
        $deploymentSettingsParameterFilePath = ""
        if ($preview)
        {
            $deploymentSettingsParameterFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Parameters\DeploymentSettingsParametersPreview.json")
        }
        else
        {
            $deploymentSettingsParameterFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Parameters\DeploymentSettingsParameters.json")
        }
        $deploymentSettingsParameters = Get-Content $deploymentSettingsParameterFilePath | ConvertFrom-Json
        $deploymentSettingsParameters.parameters.name.value = $clusterName
        $deploymentSettingsParameters.parameters.arcNodeResourceIds.value = $arcNodeResourceIds
        $deploymentSettingsParameters.parameters.domainFqdn.value = $deploymentDataFromAnswerFile.DomainFQDN
        $deploymentSettingsParameters.parameters.namingPrefix.value = $deploymentDataFromAnswerFile.NamingPrefix
        $deploymentSettingsParameters.parameters.adouPath.value = $deploymentDataFromAnswerFile.ADOUPath
        $deploymentSettingsParameters.parameters.driftControlEnforced.value = $deploymentDataFromAnswerFile.SecuritySettings.DriftControlEnforced
        $deploymentSettingsParameters.parameters.credentialGuardEnforced.value = $deploymentDataFromAnswerFile.SecuritySettings.CredentialGuardEnforced
        $deploymentSettingsParameters.parameters.smbSigningEnforced.value = $deploymentDataFromAnswerFile.SecuritySettings.SMBSigningEnforced
        $deploymentSettingsParameters.parameters.smbClusterEncryption.value = $deploymentDataFromAnswerFile.SecuritySettings.SMBClusterEncryption
        $deploymentSettingsParameters.parameters.bitlockerBootVolume.value = $deploymentDataFromAnswerFile.SecuritySettings.BitlockerBootVolume
        $deploymentSettingsParameters.parameters.bitlockerDataVolumes.value = $deploymentDataFromAnswerFile.SecuritySettings.BitlockerDataVolumes
        $deploymentSettingsParameters.parameters.wdacEnforced.value = $deploymentDataFromAnswerFile.SecuritySettings.WDACEnforced
        $deploymentSettingsParameters.parameters.streamingDataClient.value = $deploymentDataFromAnswerFile.Observability.StreamingDataClient
        $deploymentSettingsParameters.parameters.euLocation.value = $deploymentDataFromAnswerFile.Observability.EULocation
        $deploymentSettingsParameters.parameters.episodicDataUpload.value = $deploymentDataFromAnswerFile.Observability.EpisodicDataUpload
        $deploymentSettingsParameters.parameters.clusterName.value =  $clusterName    
        $deploymentSettingsParameters.parameters.storageWitnessSecretName.value =  $clusterName + "-WitnessStorageKey"
        $deploymentSettingsParameters.parameters.LocalAdminCredentialSecretName.value = $clusterName + "-LocalAdminCredential"
        $deploymentSettingsParameters.parameters.domainAdminSecretName.value = $clusterName + "-AzureStackLCMUserCredential"
        $deploymentSettingsParameters.parameters.arbDeploymentSpnSecretName.value = $clusterName + "-DefaultARBApplication"
        $deploymentSettingsParameters.parameters.keyVaultName.value = $KVName
        $deploymentSettingsParameters.parameters.cloudAccountName.value = $storageAccountName
        $deploymentSettingsParameters.parameters.configurationMode.value = $deploymentDataFromAnswerFile.Storage.ConfigurationMode
        $deploymentSettingsParameters.parameters.subnetMask.value = $deploymentDataFromAnswerFile.InfrastructureNetwork.SubnetMask
        $deploymentSettingsParameters.parameters.defaultGateway.value = $deploymentDataFromAnswerFile.InfrastructureNetwork.Gateway
        $deploymentSettingsParameters.parameters.infrastructureIpPoolSettings.value = @(GetInfrastructureNetworkIPPoolSettingsFromAnswerFile -deploymentData $deploymentDataFromAnswerFile)
        $deploymentSettingsParameters.parameters.dnsServers.value = @($deploymentDataFromAnswerFile.InfrastructureNetwork.DNSServers)
        $deploymentSettingsParameters.parameters.physicalNodesSettings.value = @(GetPhysicalNodesSettingsFromAnswerFile -deploymentData $deploymentDataFromAnswerFile -preview $preview)
        $deploymentSettingsParameters.parameters.storageNetworkList.value = @(GetStorageNetworkListFromDeploymentData -deploymentData $deploymentDataFromAnswerFile)
        $deploymentSettingsParameters.parameters.intentList.value = @(GetNetworkIntents -deploymentData $deploymentDataFromAnswerFile)
        $deploymentSettingsParameters.parameters.customLocation.value = $customLocationName
        $deploymentSettingsParameters.parameters.secretsLocation.value = $secretsLocation
        # TODO: useDHCP in answerfile needs to be verified.
        if (-Not [string]::IsNullOrEmpty($deploymentDataFromAnswerFile.InfrastructureNetwork.UseDhcp))
        {
            $deploymentSettingsParameters.parameters.useDhcp.value = $deploymentDataFromAnswerFile.InfrastructureNetwork.UseDhcp
        }
        if ($preview -and (-Not [string]::IsNullOrEmpty($deploymentDataFromAnswerFile.Cluster.ClusterType)))
        {
            $deploymentSettingsParameters.parameters.clusterType.value = $deploymentDataFromAnswerFile.Cluster.ClusterType
        }
        if ($preview -and (-Not [string]::IsNullOrEmpty($deploymentDataFromAnswerFile.Cluster.ReplicationMode)))
        {
            $deploymentSettingsParameters.parameters.replicationMode.value = $deploymentDataFromAnswerFile.Cluster.ReplicationMode
        }
        if (-Not [string]::IsNullOrEmpty($deploymentDataFromAnswerFile.HostNetwork.EnableStorageAutoIP))
        {
            $deploymentSettingsParameters.parameters.enableStorageAutoIp.value = $deploymentDataFromAnswerFile.HostNetwork.EnableStorageAutoIP
        }
        else
        {
            # IF EnableStorageAutoIP is not available in Lab Answerfile, assume true as default.
            $deploymentSettingsParameters.parameters.enableStorageAutoIp.value = $true
        }
        if (-Not [string]::IsNullOrEmpty($deploymentDataFromAnswerFile.HostNetwork.StorageConnectivitySwitchless))
        {
            $deploymentSettingsParameters.parameters.storageConnectivitySwitchless.value = $deploymentDataFromAnswerFile.HostNetwork.StorageConnectivitySwitchless
        }
        if ($null -ne $deploymentDataFromAnswerFile.SdnIntegration -and $null -ne $deploymentDataFromAnswerFile.SdnIntegration.NetworkController )
        {
            $deploymentSettingsParameters.parameters.sdnIntegration.value = GetSDNIntegrationFromDeploymentData -deploymentData $deploymentDataFromAnswerFile
        }
        $sbePartnerInfo = $deploymentSettingsObject.ScaleUnits[0].sbePartnerInfo
        if ($null -ne $sbePartnerInfo)
        {
            $deploymentSettingsParameters.parameters.sbeVersion.value = $sbePartnerInfo.sbeDeploymentInfo.version
            $deploymentSettingsParameters.parameters.sbeFamily.value = $sbePartnerInfo.sbeDeploymentInfo.family
            $deploymentSettingsParameters.parameters.sbePublisher.value = $sbePartnerInfo.sbeDeploymentInfo.publisher
            $deploymentSettingsParameters.parameters.sbeManifestSource.value = $sbePartnerInfo.sbeDeploymentInfo.sbeManifestSource
            $deploymentSettingsParameters.parameters.sbeManifestCreationDate.value = $sbePartnerInfo.sbeDeploymentInfo.sbeManifestCreationDate
            $deploymentSettingsParameters.parameters.partnerProperties.value = $sbePartnerInfo.partnerProperties
            $deploymentSettingsParameters.parameters.credentiallist.value = $sbeCredentialsList
        }
        
        Log-Info -Message "Deployment Settings Parameters Object $deploymentSettingsParameters" -ConsoleOut
        return $deploymentSettingsParameters
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return $null
}

function ReplaceDeploymentSettingsParametersTemplateWithActualValuesForClusterUpgrade {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $deploymentSettingsObject,

        [Parameter(Mandatory = $true)]
        [string] $clusterName,

        [Parameter(Mandatory = $true)]
        [string[]] $arcNodeResourceIds,

        [Parameter(Mandatory = $true)]
        [string] $secretsLocation,

        [Parameter(Mandatory = $false)]
        [PSCustomObject[]]  $sbeCredentialsList,

        [Parameter(Mandatory = $false, HelpMessage = "Prefix to uniquely identify a storage account and a keyvault")]
        [string] $Prefix
    )
    try {
        $customLocationName = $clusterName + "-customlocation"
        $KVName = GetKeyVaultName -ClusterName $ClusterName -Prefix $Prefix
        $deploymentDataFromAnswerFile = $deploymentSettingsObject.ScaleUnits[0].DeploymentData
        $deploymentSettingsParameterFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Parameters\DeploymentSettingsParametersForClusterUpgrade.json")
        $deploymentSettingsParameters = Get-Content $deploymentSettingsParameterFilePath | ConvertFrom-Json
        $deploymentSettingsParameters.parameters.name.value = $clusterName
        $deploymentSettingsParameters.parameters.arcNodeResourceIds.value = $arcNodeResourceIds
        $deploymentSettingsParameters.parameters.domainFqdn.value = $deploymentDataFromAnswerFile.DomainFQDN
        $deploymentSettingsParameters.parameters.namingPrefix.value = $deploymentDataFromAnswerFile.NamingPrefix
        $deploymentSettingsParameters.parameters.streamingDataClient.value = $true
        $deploymentSettingsParameters.parameters.euLocation.value = $true
        $deploymentSettingsParameters.parameters.episodicDataUpload.value = $true
        $deploymentSettingsParameters.parameters.clusterName.value =  $deploymentDataFromAnswerFile.Cluster.Name # This is Failover Cluster name
        $deploymentSettingsParameters.parameters.configurationMode.value = "InfraOnly"  
        $deploymentSettingsParameters.parameters.domainAdminSecretName.value = $clusterName + "-AzureStackLCMUserCredential"
        $deploymentSettingsParameters.parameters.arbDeploymentSpnSecretName.value = $clusterName + "-DefaultARBApplication"
        $deploymentSettingsParameters.parameters.keyVaultName.value = $KVName
        $deploymentSettingsParameters.parameters.subnetMask.value = $deploymentDataFromAnswerFile.InfrastructureNetwork.SubnetMask
        $deploymentSettingsParameters.parameters.defaultGateway.value = $deploymentDataFromAnswerFile.InfrastructureNetwork.Gateway
        $deploymentSettingsParameters.parameters.infrastructureIpPoolSettings.value = @(GetInfrastructureNetworkIPPoolSettingsFromAnswerFile -deploymentData $deploymentDataFromAnswerFile)
        $deploymentSettingsParameters.parameters.dnsServers.value = @($deploymentDataFromAnswerFile.InfrastructureNetwork.DNSServers)
        $deploymentSettingsParameters.parameters.physicalNodesSettings.value = @(GetPhysicalNodesSettingsFromAnswerFileForClusterUpgrade -deploymentData $deploymentDataFromAnswerFile)
        $deploymentSettingsParameters.parameters.adouPath.value = $deploymentDataFromAnswerFile.ADOUPath
        $deploymentSettingsParameters.parameters.customLocation.value = $customLocationName
        $deploymentSettingsParameters.parameters.secretsLocation.value = $secretsLocation
        
        Log-Info -Message "Deployment Settings Parameters Object $deploymentSettingsParameters" -ConsoleOut
        return $deploymentSettingsParameters
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return $null
}

function GetNetworkIntents {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $deploymentData
    )
    $networkIntents = @()
    try {
        $networkIntentList = $deploymentData.HostNetwork.Intents
        foreach ($intent in $networkIntentList) {
            $networkIntentInfo = New-Object -TypeName PSObject
            $networkIntentInfo | Add-Member -Name 'name' -MemberType Noteproperty -Value $intent.Name
            $networkIntentInfo | Add-Member -Name 'trafficType' -MemberType Noteproperty -Value @($intent.TrafficType)
            $networkIntentInfo | Add-Member -Name 'adapter' -MemberType Noteproperty -Value @($intent.Adapter)
            $networkIntentInfo | Add-Member -Name 'overrideVirtualSwitchConfiguration' -MemberType Noteproperty -Value $intent.OverrideVirtualSwitchConfiguration        
            $networkIntentInfo | Add-Member -Name 'overrideQosPolicy' -MemberType Noteproperty -Value $intent.OverrideQosPolicy      
            $networkIntentInfo | Add-Member -Name 'overrideAdapterProperty' -MemberType Noteproperty -Value $intent.overrideAdapterProperty
        
            $virtualSwitchConfigurationOverrides = New-Object -TypeName PSObject
            $virtualSwitchConfigurationOverrides | Add-Member -Name 'enableIov' -MemberType Noteproperty -Value $intent.VirtualSwitchConfigurationOverrides.EnableIov
            $virtualSwitchConfigurationOverrides | Add-Member -Name 'loadBalancingAlgorithm' -MemberType Noteproperty -Value $intent.VirtualSwitchConfigurationOverrides.LoadBalancingAlgorithm
            $networkIntentInfo | Add-Member -Name 'virtualSwitchConfigurationOverrides' -MemberType Noteproperty -Value $virtualSwitchConfigurationOverrides

            $qosPolicyOverrides = New-Object -TypeName PSObject
            $qosPolicyOverrides | Add-Member -Name 'priorityValue8021Action_Cluster' -MemberType Noteproperty -Value $intent.QosPolicyOverrides.PriorityValue8021Action_Cluster
            $qosPolicyOverrides | Add-Member -Name 'priorityValue8021Action_SMB' -MemberType Noteproperty -Value $intent.QosPolicyOverrides.PriorityValue8021Action_Cluster
            $qosPolicyOverrides | Add-Member -Name 'bandwidthPercentage_SMB' -MemberType Noteproperty -Value $intent.QosPolicyOverrides.BandwidthPercentage_SMB
            $networkIntentInfo | Add-Member -Name 'qosPolicyOverrides' -MemberType Noteproperty -Value $qosPolicyOverrides

            $adapterPropertyOverrides = New-Object -TypeName PSObject
            $adapterPropertyOverrides | Add-Member -Name 'jumboPacket' -MemberType Noteproperty -Value $intent.AdapterPropertyOverrides.JumboPacket
            if( ([string]::IsNullOrEmpty($intent.AdapterPropertyOverrides.NetworkDirect)))
            {
                $adapterPropertyOverrides | Add-Member -Name 'networkDirect' -MemberType Noteproperty -Value "Disabled"
            }else
            {
                $adapterPropertyOverrides | Add-Member -Name 'networkDirect' -MemberType Noteproperty -Value $intent.AdapterPropertyOverrides.NetworkDirect
            }

            $adapterPropertyOverrides | Add-Member -Name 'networkDirectTechnology' -MemberType Noteproperty -Value $intent.AdapterPropertyOverrides.NetworkDirectTechnology
            $networkIntentInfo | Add-Member -Name 'adapterPropertyOverrides' -MemberType Noteproperty -Value $adapterPropertyOverrides
        
            $networkIntents += $networkIntentInfo
            Log-Info -Message "Network Intent Info obtained is $networkIntentInfo" -ConsoleOut
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    
    Log-Info -Message "Network Intents obtained is $networkIntents" -ConsoleOut
    return $networkIntents
}


function GetSDNIntegrationFromDeploymentData {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $deploymentData
    )
    $sdnIntegration = New-Object -TypeName psobject
    try {
        $networkControllerFromDeploymentData = $deploymentData.SDNIntegration.NetworkController

        $networkController = New-Object -TypeName psobject
        $networkController | Add-Member -Name 'macAddressPoolStart' -MemberType Noteproperty -Value $networkControllerFromDeploymentData.MacAddressPoolStart
        $networkController | Add-Member -Name 'macAddressPoolStop' -MemberType Noteproperty -Value $networkControllerFromDeploymentData.MacAddressPoolStop
        $networkController | Add-Member -Name 'networkVirtualizationEnabled' -MemberType Noteproperty -Value $networkControllerFromDeploymentData.NetworkVirtualizationEnabled

        
        $sdnIntegration  | Add-Member -Name 'networkController' -MemberType Noteproperty -Value $networkController
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    Log-Info -Message "SDN integration parsed: $sdnIntegration" -ConsoleOut
    return $sdnIntegration
}

function GetStorageNetworkListFromDeploymentData {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $deploymentData
    )
    $storageNetworks = @()
    try {
        $storageNetworksList = $deploymentData.HostNetwork.StorageNetworks
        foreach ($network in $storageNetworksList) {
            $storageNetworkInfo = New-Object -TypeName psobject
            $storageNetworkInfo | Add-Member -Name 'name' -MemberType Noteproperty -Value $network.Name
            $storageNetworkInfo | Add-Member -Name 'networkAdapterName' -MemberType Noteproperty -Value $network.NetworkAdapterName
            $storageNetworkInfo | Add-Member -Name 'vlanId' -MemberType Noteproperty -Value $network.VlanId.ToString()

            
            if ($null -ne $network.StorageAdapterIPInfo)
            {
                $storageAdapterInfos = @()
                foreach ($storageAdapterIPInformation in $network.StorageAdapterIPInfo) {
                    $storageAdapterIPInfo = New-Object -TypeName psobject
                    $storageAdapterIPInfo | Add-Member -Name 'physicalNode' -MemberType Noteproperty -Value $storageAdapterIPInformation.PhysicalNode
                    $storageAdapterIPInfo | Add-Member -Name 'ipv4Address' -MemberType Noteproperty -Value $storageAdapterIPInformation.IPv4Address
                    $storageAdapterIPInfo | Add-Member -Name 'subnetMask' -MemberType Noteproperty -Value $storageAdapterIPInformation.SubnetMask
                    $storageAdapterInfos += $storageAdapterIPInfo
                }
                $storageNetworkInfo | Add-Member -Name 'storageAdapterIPInfo' -MemberType Noteproperty -Value $storageAdapterInfos
            }

            $storageNetworks += $storageNetworkInfo
            Log-Info -Message "Storage Network Setting Info is $storageNetworkInfo" -ConsoleOut
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    Log-Info -Message "Storage Network Settings Obtained is $storageNetworks" -ConsoleOut
    return $storageNetworks
}

function GetInfrastructureNetworkIPPoolSettingsFromAnswerFile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $deploymentData
    )
    $ipPoolSettings = @()
    try {
        $infrastructureNetworkIpPools = $deploymentData.InfrastructureNetwork.IPPools
        foreach ($ippool in $infrastructureNetworkIpPools) {
            $ippoolInfo = New-Object -TypeName psobject
            $ippoolInfo | Add-Member -Name 'startingAddress' -MemberType Noteproperty -Value $ippool.StartingAddress
            $ippoolInfo | Add-Member -Name 'endingAddress' -MemberType Noteproperty -Value $ippool.EndingAddress
           
            $ipPoolSettings += $ippoolInfo
            Log-Info -Message "Infrastructure Ip Pool info is $ippoolInfo" -ConsoleOut
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    Log-Info -Message "Infrastructure Ip Pool Settings obtained is $ipPoolSettings" -ConsoleOut
    return $ipPoolSettings
}

function GetPhysicalNodesSettingsFromAnswerFile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $deploymentData,

        [Parameter(Mandatory = $false, HelpMessage = "For testing preview features enable this flag")]
        [bool] $preview
    )
    $physicalNodeSettings = @()
    try {
        $physicalNodesData = $deploymentData.PhysicalNodes
        foreach ($settings in $physicalNodesData) {
            $physicalNodeInfo = New-Object -TypeName psobject
            $physicalNodeInfo | Add-Member -Name 'name' -MemberType Noteproperty -Value $settings.Name
            $physicalNodeInfo | Add-Member -Name 'ipv4Address' -MemberType Noteproperty -Value $settings.Ipv4Address
            if ($preview -and (-Not [string]::IsNullOrEmpty($settings.FaultDomainName)))
            {
                $physicalNodeInfo | Add-Member -Name 'faultDomainName' -MemberType Noteproperty -Value $settings.FaultDomainName
            }
            if ($preview -and (-Not [string]::IsNullOrEmpty($settings.FaultDomainMode)))
            {
                $physicalNodeInfo | Add-Member -Name 'faultDomainMode' -MemberType Noteproperty -Value $settings.faultDomainMode
            }
           
            $physicalNodeSettings += $physicalNodeInfo
            Log-Info -Message "Physical Node Ip info is $physicalNodeInfo" -ConsoleOut
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    Log-Info -Message "Physical Node Settings obtained is $physicalNodeSettings" -ConsoleOut
    return $physicalNodeSettings
}

function GetPhysicalNodesSettingsFromAnswerFileForClusterUpgrade {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $deploymentData,

        [Parameter(Mandatory = $false, HelpMessage = "For testing preview features enable this flag")]
        [bool] $preview
    )
    $physicalNodeSettings = @()
    try {
        $physicalNodesData = $deploymentData.PhysicalNodes
        foreach ($settings in $physicalNodesData) {
            $physicalNodeInfo = New-Object -TypeName psobject
            $physicalNodeInfo | Add-Member -Name 'name' -MemberType Noteproperty -Value $settings.Name                       
            $physicalNodeSettings += $physicalNodeInfo
            Log-Info -Message "Physical Node Ip info is $physicalNodeInfo" -ConsoleOut
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    Log-Info -Message "Physical Node Settings obtained is $physicalNodeSettings" -ConsoleOut
    return $physicalNodeSettings
}

function AssignPermissionsToArcMachines {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]] $ArcMachineIds,

        [Parameter(Mandatory = $true)]
        [string] $ResourceGroup
    )
    try {
        ForEach ($arcMachineUri in $ArcMachineIds) {
            $objectId = GetArcMachineObjectId -ArcMachineUri $arcMachineUri
            if ($null -ne $objectId) {
                $setHCIRegistrationRoleResult = PerformObjectRoleAssignmentWithRetries -ObjectId $objectId -RoleName "Azure Stack HCI Device Management Role" -ResourceGroup $ResourceGroup -Verbose
                if ($setHCIRegistrationRoleResult -ne [ErrorDetail]::Success) {
                    Log-Info -Message "Failed to assign the Azure Stack HCI Device Management Role on the resource group" -ConsoleOut -Type Error
                }
                else {
                    Log-Info -Message "Successfully assigned the Azure Stack HCI Device Management Role on the resource group" -ConsoleOut
                }

                # Start: Temp role assignment for TVM action plan to work
                $tvmRoleAssignerRoleExists = $null -ne (Get-AzRoleDefinition -Name "TVM Attestation Role Assigner (Custom)")
                $tvmRoleExists = $null -ne (Get-AzRoleDefinition -Name "Azure Stack HCI TVM Attestation Role (Custom)")
                
                if ($tvmRoleAssignerRoleExists -and $tvmRoleExists) {
                    $setTvmAttestationRoleResult = PerformObjectRoleAssignmentWithRetries -ObjectId $objectId -RoleName "Azure Stack HCI TVM Attestation Role (Custom)" -ResourceGroup $ResourceGroup -Verbose
                    if ($setTvmAttestationRoleResult -ne [ErrorDetail]::Success) {
                        Log-Info -Message "Failed to assign the Azure Stack HCI TVM Attestation Role (Custom) on the resource group" -ConsoleOut -Type Error
                    }
                    else {
                        Log-Info -Message "Successfully assigned the Azure Stack HCI TVM Attestation Role (Custom) on the resource group" -ConsoleOut
                    }
                }
                else {
                    Log-Info -Message "TVM Attestation Role Assigner (Custom) or Azure Stack HCI TVM Attestation Role (Custom) role definition does not exist" -ConsoleOut
                }
                # End: Temp role assignment for TVM action plan to work

                $keyVaultSecretsUserRoleResult = PerformObjectRoleAssignmentWithRetries -ObjectId $objectId -RoleName "Key Vault Secrets User" -ResourceGroup $ResourceGroup -Verbose
                if ($keyVaultSecretsUserRoleResult -ne [ErrorDetail]::Success) {
                    Log-Info -Message "Failed to assign the Key Vault Secrets User role on the resource group" -ConsoleOut -Type Error
                }
                else {
                    Log-Info -Message "Successfully assigned the Key Vault Secrets User role on the resource group" -ConsoleOut
                }

                $connectedInfraVMsRoleResult = PerformObjectRoleAssignmentWithRetries -ObjectId $objectId -RoleName "Azure Stack HCI Connected InfraVMs" -ResourceGroup $ResourceGroup -Verbose
                if ($connectedInfraVMsRoleResult -ne [ErrorDetail]::Success) {
                    Log-Info -Message "Failed to assign the Azure Stack HCI Connected InfraVMs role on the resource group" -ConsoleOut -Type Error
                }
                else {
                    Log-Info -Message "Successfully assigned the Azure Stack HCI Connected InfraVMs role on the resource group" -ConsoleOut
                }
            }
            else{
                Log-Info -Message "HCI Object Id is null, so could not assign the required permissions the HCI RP on the RG" -Type Error -ConsoleOut
            }
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
}

function GetArcMachineObjectId {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $ArcMachineUri
    )
    try {
        Log-Info -Message "Arc Machine Uri $ArcMachineUri" -ConsoleOut
        $arcResource = Get-AzResource -ResourceId $ArcMachineUri
        $objectId = $arcResource.Identity.PrincipalId
        Log-Info -Message "Successfully got Object Id for Arc Installation $objectId" -ConsoleOut
        return $objectId
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
        throw $_
    }
    return $null
}

function ReplaceKeyVaultTemplateWithActualValues {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $KVName,

        [Parameter(Mandatory = $true)]
        [string] $Region,

        [Parameter(Mandatory = $true)]
        [string] $LocalAdminSecret,

        [Parameter(Mandatory = $true)]
        [string] $DomainAdminSecret,

        [Parameter(Mandatory = $true)]
        [string] $ArbDeploymentSpnSecret,

        [Parameter(Mandatory = $true)]
        [string] $StorageWitnessKey,

        [Parameter(Mandatory = $true)]
        [string] $ClusterName,

        [Parameter(Mandatory = $false)]
        [PSCustomObject[]] $SBESecrets
    )
    try {
        Log-Info -Message "Starting to change the parameters of the key vault parameyters template" -ConsoleOut
        $keyVaultParameterFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Parameters\KeyVaultParameters.json")
        $keyVaultParameters = Get-Content $keyVaultParameterFilePath | ConvertFrom-Json
        Log-Info -Message "Successfully got the template file for the key vault parameters" -ConsoleOut
        $keyVaultParameters.parameters.keyVaultName.value = $KVName
        $keyVaultParameters.parameters.location.value = $Region
        $keyVaultParameters.parameters.localAdminSecretName.value = $ClusterName + "-LocalAdminCredential"
        $keyVaultParameters.parameters.localAdminSecretValue.value = $LocalAdminSecret
        $keyVaultParameters.parameters.domainAdminSecretName.value = $ClusterName + "-AzureStackLCMUserCredential"
        $keyVaultParameters.parameters.domainAdminSecretValue.value = $DomainAdminSecret
        $keyVaultParameters.parameters.arbDeploymentSpnName.value = $ClusterName + "-DefaultARBApplication"
        $keyVaultParameters.parameters.arbDeploymentSpnValue.value = $ArbDeploymentSpnSecret
        $keyVaultParameters.parameters.storageWitnessName.value = $ClusterName + "-WitnessStorageKey"
        $keyVaultParameters.parameters.storageWitnessValue.value = $StorageWitnessKey
        Log-Info -Message "Successfully updated the key vault parameters file with the actual values" -ConsoleOut
        return $keyVaultParameters
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return $null
}

function ReplaceKeyVaultTemplateWithActualValuesForClusterUpgrade {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $KVName,

        [Parameter(Mandatory = $true)]
        [string] $Region,

        [Parameter(Mandatory = $true)]
        [string] $DomainAdminSecret,

        [Parameter(Mandatory = $true)]
        [string] $ArbDeploymentSpnSecret,

        [Parameter(Mandatory = $true)]
        [string] $ClusterName,

        [Parameter(Mandatory = $false)]
        [PSCustomObject[]] $SBESecrets
    )
    try {
        Log-Info -Message "Starting to change the parameters of the key vault parameyters template" -ConsoleOut
        $keyVaultParameterFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Parameters\KeyVaultParameters.json")
        $keyVaultParameters = Get-Content $keyVaultParameterFilePath | ConvertFrom-Json
        Log-Info -Message "Successfully got the template file for the key vault parameters" -ConsoleOut
        $keyVaultParameters.parameters.keyVaultName.value = $KVName
        $keyVaultParameters.parameters.location.value = $Region
        $keyVaultParameters.parameters.domainAdminSecretName.value = $ClusterName + "-AzureStackLCMUserCredential"
        $keyVaultParameters.parameters.domainAdminSecretValue.value = $DomainAdminSecret
        $keyVaultParameters.parameters.arbDeploymentSpnName.value = $ClusterName + "-DefaultARBApplication"
        $keyVaultParameters.parameters.arbDeploymentSpnValue.value = $ArbDeploymentSpnSecret
        Log-Info -Message "Successfully updated the key vault parameters file with the actual values" -ConsoleOut
        return $keyVaultParameters
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return $null
}

function CreateServicePrincipalForCloudDeployment {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DisplayName,

        [Parameter(Mandatory = $true)]
        [string] $ResourceGroup
    )
    try {
        $SPNAndAppArray = CreateAndCheckAadApp -DisplayName $DisplayName
        $servicePrincipal = $SPNAndAppArray[0] 
        $AADApp = $SPNAndAppArray[1]
        if ($null -eq $AADApp)
        {
            Log-Info -Message "Could not create AAD app successfully for creating SPN, so returning null" -ConsoleOut
            return $null
        }
        Log-Info -Message "Service principal obtained is $servicePrincipal" -ConsoleOut
        
        Log-Info -Message "Created a spn with the appId $AADApp" -ConsoleOut
        $PasswordCedentials = @{
            StartDateTime = Get-Date
            EndDateTime   = (Get-Date).AddDays(90)
            DisplayName   = ("Secret auto-rotated on: " + (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd"))
        }
        $servicePrincipalSecret = New-AzADAppCredential -ApplicationObject $AADApp -PasswordCredentials $PasswordCedentials
        $servicePrincipalSecretTest = $servicePrincipalSecret.SecretText
        Log-Info -Message "Successfully created a service principal secret for the app $AADApp" -ConsoleOut

        $spnCredentialForArb = $servicePrincipal.AppId + ":" + $servicePrincipalSecretTest
        $base64EncodedSpnCredential = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($spnCredentialForArb))

        Log-Info -Message "The base 64 encoded spn credential for deployment is created successfully" -ConsoleOut

        Log-Info -Message "Trying to assign permission to the SPN" -ConsoleOut
        AssignPermissionToSPN -spnObjectId $servicePrincipal.Id -ResourceGroup $ResourceGroup

        return $base64EncodedSpnCredential
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return $null
}

function CreateAndCheckAadApp {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DisplayName
    )

    $App = $null
    $servicePrincipal = $null
    try {
        $servicePrincipal = New-AzADServicePrincipal -DisplayName $DisplayName
        Log-Info -Message "Initialized creation of an aad app with display name $DisplayName" -ConsoleOut
        $stopLoop = $false
        $count = 0;
        do {
            $count++;
            Log-Info -Message "Current retry count is $count" -ConsoleOut
            try {
                $AADApp = Get-AzADApplication -ApplicationId $servicePrincipal.AppId
                if ($null -ne $AADApp) {
                    Log-Info -Message "Successfully fetched an aad app using the created spn $($servicePrincipal.AppId)" -ConsoleOut
                    $stopLoop = $true;
                    $App = $AADApp;
                }
                else
                {
                    Log-Info -Message "AAD app obtained is null, so retrying" -ConsoleOut
                }
            }
            catch {
                Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
                Log-Info -Message "Error while getting the aad app for $DisplayName so retrying" -ConsoleOut
            }
            finally {
                Start-Sleep -Seconds 30
                if ($count -ge 30) {
                    Log-Info -Message "AAD application could not be created within 15 mins, so stopping retrying" -ConsoleOut
                    $stopLoop = $true;
                }
            }
        }
        while (-Not $stopLoop)
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    Log-Info -Message "Returning AAD app $App" -ConsoleOut
    return @($servicePrincipal,$App);
}

function ReplaceStorageAccountTemplateWithActualValues {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $StorageAccountName,

        [Parameter(Mandatory = $true)]
        [string] $Location
    )
    try {
        $storageAccountParameterFilePath = (Join-Path  -Path $PSScriptRoot -ChildPath "Parameters\StorageAccountParameters.json")
        Log-Info -Message "Storage Account Parameters File Path $storageAccountParameterFilePath" -ConsoleOut
        $storageAccountParameters = Get-Content $storageAccountParameterFilePath | ConvertFrom-Json
        $storageAccountParameters.parameters.cloudDeployStorageAccountName.value = $StorageAccountName
        $storageAccountParameters.parameters.location.value = $Location
        Log-Info -Message "Successfully replaced the storage account name in the parameters file" -ConsoleOut
        return $storageAccountParameters
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return $null
}

function CreateSBECredentialsList{
    [CmdletBinding()]
    param (
        [System.Collections.Hashtable] $SBESecrets,
        [string] $kvVaultUri
    )

    $objectsArray = @()
    if( $null -ne $SBESecrets)
    {
        foreach ($key in $SBESecrets.Keys) {
            $object = [PSCustomObject]@{
                Key = $key
                Value = $SBESecrets[$key]
            }
            $object = [PSCustomObject]@{
                secretName = $key
                eceSecretName = $key
                secretLocation= $kvVaultUri+"secrets/"+$key
            }
            Log-Info -Message "Added SBE secret Name $username and the secret Value to deploymentsettings $($object.secretName), $($object.eceSecretName), $($object.secretLocation) " -ConsoleOut
            $objectsArray += $object
        }
    }
    return $objectsArray

}
function ExtractSBECredentialsToKeyVaulepairs {
    [CmdletBinding()]
    param (
        [System.Collections.Hashtable] $Credentials
    )

    if( $null -ne $Credentials)
    {
        $objectsArray = @()
        
        foreach ($key in $Credentials.Keys) {
            $object = [PSCustomObject]@{
                Key = $key
                Value = ExtractUsernameAndPasswordFromCredential -Credential $Credentials[$key] 
            }
            Log-Info -Message "Added SBE secret Name $($key) and the secret Value to hashtable" -ConsoleOut
            $objectsArray += $object
        }
    }
    return $objectsArray
    
}

function ExtractUsernameAndPasswordFromCredential {
    [CmdletBinding()]
    param (
        [System.Management.Automation.PSCredential] $Credential
    )
    try {
        $secretName = $Credential.GetNetworkCredential().UserName
        $secretValue = $Credential.GetNetworkCredential().Password
        Log-Info -Message "Successfully extracted the secret Name $secretName and the secret Value from the Credential Object" -ConsoleOut
        $KVSecret = $secretName + ":" + $secretValue
        $base64EncodedKVSecret = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($KVSecret))
        Log-Info -Message "Successfully base 64 encoded the secret $secretName "
        return $base64EncodedKVSecret
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
    return $null
}

function AssignPermissionToSPN {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $ResourceGroup,
 
        [Parameter(Mandatory = $true)]
        [string] $spnObjectId
    )
    try {
        $rolesToAssign = @("Azure Resource Bridge Deployment Role", "Azure Connected Machine Resource Administrator")
        if ($null -ne $spnObjectId) {
            foreach ($role in $rolesToAssign) {
                # Assign Azure Connected Machine Resource Administrator role at Resource Group level
                if ($role -eq "Azure Connected Machine Resource Administrator") {
                    $roleStatus = PerformObjectRoleAssignmentWithRetries -ObjectId $spnObjectId -RoleName $role -ResourceGroup $ResourceGroup
                }
                else  {
                    $roleStatus = PerformObjectRoleAssignmentWithRetries -ObjectId $spnObjectId -RoleName $role
                }

                if ($roleStatus -ne [ErrorDetail]::Success) {
                    Log-Info -Message "Failed to assign the role $role" -ConsoleOut -Type Error
                }
                else {
                    Log-Info -Message "Successfully assigned the role $role" -ConsoleOut
                }
            }
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
}


function AssignRolesToHCIResourceProvider {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $ResourceGroup,
 
        [Parameter(Mandatory = $true)]
        [string] $hciObjectId
    )
    try {
        if ($null -ne $hciObjectId) {
            $arcManagerRoleStatus = PerformObjectRoleAssignmentWithRetries -ObjectId $hciObjectId -RoleName "Azure Connected Machine Resource Manager" -ResourceGroup $ResourceGroup
            if ($arcManagerRoleStatus -ne [ErrorDetail]::Success) {
                Log-Info -Message "Failed to assign the Azure Connected Machine Resource Nanager role on the resource group" -ConsoleOut -Type Error
            }
            else {
                Log-Info -Message "Successfully assigned the Azure Connected Machine Resource Nanager role on the resource group" -ConsoleOut
            }
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
}
 
function PerformObjectRoleAssignmentWithRetries {
    param(
        [String] $ObjectId,
        [String] $ResourceGroup,
        [string] $RoleName
    )
    $stopLoop = $false
    [int]$retryCount = "0"
    [int]$maxRetryCount = "5"
 
    Log-Info -Message $"Checking if $RoleName is assigned already for SPN with Object ID: $ObjectId" -ConsoleOut
    if( [string]::IsNullOrEmpty($ResourceGroup))
    {
        $arcSPNRbacRoles = Get-AzRoleAssignment -ObjectId $ObjectId
    }
    else
    {
        $arcSPNRbacRoles = Get-AzRoleAssignment -ObjectId $ObjectId -ResourceGroupName $ResourceGroup
    }
    
    $alreadyFoundRole = $false
    $arcSPNRbacRoles | ForEach-Object {
        $roleFound = $_.RoleDefinitionName
        if ($roleFound -eq $RoleName) {
            $alreadyFoundRole = $true
            Log-Info -Message $"Already Found $RoleName Not Assigning" -ConsoleOut
        }
    }
    if ( -not $alreadyFoundRole) {
        Log-Info -Message "Assigning $RoleName to Object : $ObjectId" -ConsoleOut
        do {
            try {
                if( [string]::IsNullOrEmpty($ResourceGroup))
                {
                    New-AzRoleAssignment -ObjectId $ObjectId -RoleDefinitionName $RoleName | Out-Null                    
                }
                else
                {
                    New-AzRoleAssignment -ObjectId $ObjectId -ResourceGroupName $ResourceGroup -RoleDefinitionName $RoleName | Out-Null
                }

                Log-Info -Message $"Sucessfully assigned $RoleName to Object Id $ObjectId" -ConsoleOut
                $stopLoop = $true
            }
            catch {
                # 'Conflict' can happen when either the RoleAssignment already exists or the limit for number of role assignments has been reached.
                if ($_.Exception.Response.StatusCode -eq 'Conflict') {
                    if( [string]::IsNullOrEmpty($ResourceGroup))
                    {
                        $roleAssignment = Get-AzRoleAssignment -ObjectId $ObjectId -RoleDefinitionName $RoleName                        
                    }
                    else
                    {
                        $roleAssignment = Get-AzRoleAssignment -ObjectId $ObjectId -ResourceGroupName $ResourceGroup -RoleDefinitionName $RoleName
                    }

                    if ($null -ne $roleAssignment) {
                        Log-Info -Message $"Sucessfully assigned $RoleName to Object Id $ObjectId" -ConsoleOut
                        return [ErrorDetail]::Success
                    }
                    Log-Info -Message $"Failed to assign roles to service principal with object Id $($ObjectId). ErrorMessage: " + $_.Exception.Message + " PositionalMessage: " + $_.InvocationInfo.PositionMessage -ConsoleOut -Type Error
                    return [ErrorDetail]::PermissionsMissing
                }
                if ($retryCount -ge $maxRetryCount) {
                    # Timed out.
                    Log-Info -Message $"Failed to assign roles to service principal with object Id $($ObjectId). ErrorMessage: " + $_.Exception.Message + " PositionalMessage: " + $_.InvocationInfo.PositionMessage -ConsoleOut -Type Error
                    return [ErrorDetail]::PermissionsMissing
                }
                Log-Info -Message $"Could not assign roles to service principal with Object Id $($ObjectId). Retrying in 10 seconds..." -ConsoleOut
                Start-Sleep -Seconds 10
                $retryCount = $retryCount + 1
            }
        }
        While (-Not $stopLoop)
    }
    return [ErrorDetail]::Success
}

function CreateResourceGroupIfNotExists {
    param (
        [Parameter(Mandatory = $true)]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [string] $Region
    )
    try {
        # Check if the resource group exists
        $existingResourceGroup = Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue

        if (([string]::IsNullOrEmpty($existingResourceGroup)) -or ([string]::IsNullOrEmpty($existingResourceGroup.ResourceGroupName))) {
            # Resource group doesn't exist, create it
            Log-Info -Message "$ResourceGroupName does not exist, creating it" -ConsoleOut
            New-AzResourceGroup -Name $ResourceGroupName -Location $Region -Force | Out-Null
            Log-info -Message "Created the resource group $ResourceGroupName" -ConsoleOut
        }
        else {
            # Resource group already exists
            Log-Info -Message "The resource group '$ResourceGroupName' already exists." -ConsoleOut
        }
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
}

function CheckIfScriptIsRunByAdministrator {
    try {
        $user = [System.Security.Principal.WindowsIdentity]::GetCurrent()

        # Get the Windows Principal for the current user
        $principal = New-Object System.Security.Principal.WindowsPrincipal($user)

        # Check if the user is in the Administrator role
        $is_admin = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)

        if ($is_admin) {
            Log-Info -Message "User has administrator access" -ConsoleOut
            return $is_admin
        }
        Log-Info -Message "User is not running the script in administrator mode" -ConsoleOut
        return $is_admin
    }
    catch {
        Log-Info -Message "" -ConsoleOut
        Log-Info -Message "$($_.Exception.Message)" -ConsoleOut -Type Error
        Log-Info -Message "$($_.ScriptStackTrace)" -ConsoleOut -Type Error
    }
}

function New-ClusterWithRetries {
    param(
        [String] $ResourceIdWithAPI,
        [String] $Payload
    )
    $stopLoop = $false
    [int]$retryCount = "0"
    [int]$maxRetryCount = "10"
    do {

        $response = Invoke-AzRestMethod -Path $ResourceIdWithAPI -Method PUT -Payload $Payload

        if (($response.StatusCode -ge 200) -and ($response.StatusCode -lt 300)) {
            $stopLoop = $true
            return $true
        }
        if ($retryCount -ge $maxRetryCount) {
            # Timed out.
            Log-Info -Message "Failed to create ARM resource representing the cluster. StatusCode: {0}, ErrorCode: {1}, Details: {2}" -f $response.StatusCode, $response.ErrorCode, $response.Content -Type Error -ConsoleOut
            return $false
        }
        Log-Info -Message "Failed to create ARM resource representing the cluster. Retrying in 10 seconds..." -Type Error -ConsoleOut
        Start-Sleep -Seconds 10
        $retryCount = $retryCount + 1

    }
    While (-Not $stopLoop)
    return $true
}

class Identity {
    [string] $type = "SystemAssigned"
}

class ResourceProperties {
    [string] $location
    [object] $properties
    [Identity] $identity = [Identity]::new()

    ResourceProperties (
        [string] $location,
        [object] $properties
    )
    {
        $this.location = $location
        $this.properties = $properties
    }
}

enum ErrorDetail
{
    Unused;
    PermissionsMissing;
    Success;
    NodeAlreadyArcEnabled;
    NotFound;
    ClusterAlreadyExists;
    ConnectedRecently;
    DeploymentSuccess;
    StorageAccountAlreadyExists;
    KeyVaultAlreadyExists;
    EnvironmentValidationFailed
}
 
Export-ModuleMember -Function Invoke-AzStackHCIDeployment
Export-ModuleMember -Function Invoke-AzStackHCIEnvironmentValidator
Export-ModuleMember -Function Invoke-AzStackHCIEnvironmentPreparator
Export-ModuleMember -Function Invoke-AzStackHCIUpgrade
Export-ModuleMember -Function Invoke-AzStackHCIEnvironmentValidatorForClusterUpgrade
Export-ModuleMember -Function Invoke-AzStackHCIEnvironmentPreparatorForClusterUpgrade
Export-ModuleMember -Function Invoke-AzStackHCIFullDeployment
Export-ModuleMember -Function PollDeploymentSettingsStatus
Export-ModuleMember -Function Invoke-validateNodesForDeployment
Export-ModuleMember -Function New-EdgeDevices
# SIG # Begin signature block
# MIInvwYJKoZIhvcNAQcCoIInsDCCJ6wCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCANdQlZJ/cIah34
# Frg+6/Y/mJ9sk1YZdOdXx4RoVUmNa6CCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA
# hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG
# 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN
# xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL
# go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB
# tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd
# mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ
# 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY
# 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp
# XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn
# TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT
# e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG
# OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O
# PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk
# ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx
# HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt
# CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIFt6gH+PetCkTfjTCOxnXm79
# sJ5Se2qvAWCoU3JgiMGTMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEALjaFO3pZ7r4TufIloy7JOEtK98AZAOFV5hqzJVYIcGOLOTL1k4e19q9z
# MYOROSbPobWsuhrTN6Z4uxMVB14FZ4PwXvjjyt94O6voTcqw1DSEDnQocd6clAkG
# ZWOyH0C9OFhn/d1I4CXFTmf3u26ShTUTQqXHQuP2OrJa4b6ENCU5sNIWvCnZyXMp
# F7L6qQJTodM73/12WerBAfKTf1pEU1+WuZR0ZXRaC+LxYyDx/kGL8yWLvCTKkOZM
# y0EGuNa0B/PjaIuR7sXLPJiO8qnCDRQRezuSWzwNwaUBcqtzsL071n3SOI9v4FRU
# eJ3dd3LSQeE5ZnpGfrvUdcmei5kCS6GCFykwghclBgorBgEEAYI3AwMBMYIXFTCC
# FxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq
# hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCC3+JMlYc1kkL2uvITJ2I5qK7suAMIhNfL08p0Q3zKQBgIGZr4UXznF
# GBMyMDI0MDgxNjE4MTQ1MS45NzFaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO
# OjNCRDQtNEI4MC02OUMzMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAHlj2rA8z20C6MAAQAAAeUwDQYJ
# KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx
# MDEyMTkwNzM1WhcNMjUwMTEwMTkwNzM1WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl
# cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozQkQ0LTRC
# ODAtNjlDMzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKl74Drau2O6LLrJO3HyTvO9
# aXai//eNyP5MLWZrmUGNOJMPwMI08V9zBfRPNcucreIYSyJHjkMIUGmuh0rPV5/2
# +UCLGrN1P77n9fq/mdzXMN1FzqaPHdKElKneJQ8R6cP4dru2Gymmt1rrGcNe800C
# cD6d/Ndoommkd196VqOtjZFA1XWu+GsFBeWHiez/PllqcM/eWntkQMs0lK0zmCfH
# +Bu7i1h+FDRR8F7WzUr/7M3jhVdPpAfq2zYCA8ZVLNgEizY+vFmgx+zDuuU/GChD
# K7klDcCw+/gVoEuSOl5clQsydWQjJJX7Z2yV+1KC6G1JVqpP3dpKPAP/4udNqpR5
# HIeb8Ta1JfjRUzSv3qSje5y9RYT/AjWNYQ7gsezuDWM/8cZ11kco1JvUyOQ8x/JD
# kMFqSRwj1v+mc6LKKlj//dWCG/Hw9ppdlWJX6psDesQuQR7FV7eCqV/lfajoLpPN
# x/9zF1dv8yXBdzmWJPeCie2XaQnrAKDqlG3zXux9tNQmz2L96TdxnIO2OGmYxBAA
# ZAWoKbmtYI+Ciz4CYyO0Fm5Z3T40a5d7KJuftF6CToccc/Up/jpFfQitLfjd71cS
# +cLCeoQ+q0n0IALvV+acbENouSOrjv/QtY4FIjHlI5zdJzJnGskVJ5ozhji0YRsc
# v1WwJFAuyyCMQvLdmPddAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQU3/+fh7tNczEi
# fEXlCQgFOXgMh6owHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD
# VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG
# CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw
# MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBADP6whOFjD1ad8Gk
# EJ9oLBuvfjndMyGQ9R4HgBKSlPt3pa0XVLcimrJlDnKGgFBiWwI6XOgw82hdolDi
# MDBLLWRMTJHWVeUY1gU4XB8OOIxBc9/Q83zb1c0RWEupgC48I+b+2x2VNgGJUsQI
# yPR2PiXQhT5PyerMgag9OSodQjFwpNdGirna2rpV23EUwFeO5+3oSX4JeCNZvgyU
# OzKpyMvqVaubo+Glf/psfW5tIcMjZVt0elswfq0qJNQgoYipbaTvv7xmixUJGTbi
# xYifTwAivPcKNdeisZmtts7OHbAM795ZvKLSEqXiRUjDYZyeHyAysMEALbIhdXgH
# Eh60KoZyzlBXz3VxEirE7nhucNwM2tViOlwI7EkeU5hudctnXCG55JuMw/wb7c71
# RKimZA/KXlWpmBvkJkB0BZES8OCGDd+zY/T9BnTp8si36Tql84VfpYe9iHmy7Pqq
# xqMF2Cn4q2a0mEMnpBruDGE/gR9c8SVJ2ntkARy5SfluuJ/MB61yRvT1mUx3lypp
# O22ePjBjnwoEvVxbDjT1jhdMNdevOuDeJGzRLK9HNmTDC+TdZQlj+VMgIm8ZeEIR
# NF0oaviF+QZcUZLWzWbYq6yDok8EZKFiRR5otBoGLvaYFpxBZUE8mnLKuDlYobjr
# xh7lnwrxV/fMy0F9fSo2JxFmtLgtMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ
# mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh
# dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1
# WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB
# BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK
# NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg
# fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp
# rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d
# vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9
# 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR
# Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu
# qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO
# ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb
# oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6
# bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t
# AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW
# BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb
# UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku
# aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA
# QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2
# VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu
# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw
# LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt
# MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q
# XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6
# U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt
# I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis
# 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp
# kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0
# sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e
# W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ
# sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7
# Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0
# dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ
# tB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh
# bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjoz
# QkQ0LTRCODAtNjlDMzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUA942iGuYFrsE4wzWDd85EpM6RiwqggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOpp5EYwIhgPMjAyNDA4MTYyMjQ0MjJaGA8yMDI0MDgxNzIyNDQyMlowdDA6Bgor
# BgEEAYRZCgQBMSwwKjAKAgUA6mnkRgIBADAHAgEAAgIanTAHAgEAAgIQnzAKAgUA
# 6ms1xgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID
# B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAFOAB3xFQFBbpfTtgiZd
# VvNlMChZ8GUESMj9jsynLgtF59wEk8+hEAOubjoCfCBEo1acAwG5gkAIKVxSTarU
# c+4pa9HD0hozkc+7pchpyASMdU2HItRR3jemnUzZGsq91idhVUQfinx0ZOJSY5/D
# S7bBvr66GuzgFnhySrQXUx5UMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgUENBIDIwMTACEzMAAAHlj2rA8z20C6MAAQAAAeUwDQYJYIZIAWUDBAIB
# BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx
# IgQgc5BjOPl/IrP+Lm5qv9g6Ijl3mMkY1Q4f5KrgOuPFaTAwgfoGCyqGSIb3DQEJ
# EAIvMYHqMIHnMIHkMIG9BCAVqdP//qjxGFhe2YboEXeb8I/pAof01CwhbxUH9U69
# 7TCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB5Y9q
# wPM9tAujAAEAAAHlMCIEIAMSCXskvA17T6w7gaJP046HqQZxldDZ6fVCD/TDdeVl
# MA0GCSqGSIb3DQEBCwUABIICADaZSmXaDIoBC8Rt3bRZ2mBPno8+nyiuTAE0ycMY
# GOAE1assnNlbuRor+wDlqHZDDYpIH3M0v2xKPATjKGCYr3DbuD8A5g4vaTsQe5CT
# vKxXIERyjL1vYSUefSECtl5MrW8VARJAamXc46Tz1qwUfMK13GlkUgRwH/QEtrwW
# 8EVeOv+6k6NPhwriaaNMhE0CeeFbu2I81HNmjaOBEngoEkZRAR4TglS04OBB4OG1
# v/Qz4WYMPYeKmSo6knj4nj6k1jKvNFdkRvrqlZobSvC28YQY/cK0HUynSA2kA5yN
# ZKU9TYe3GreFU7fmRv+S0J5/dVODfsObQJtAxA3D8QOuD+GQp9Zo1SAPrBQ9vHNK
# txu8W7NCvQUHHudC0P7w4aMyuvgQ3CugwEn07LKWJ1DIjixLnKMfBNPLyYK0YNc2
# FEA11JPzyhApYKrFWsx/6PPYi1nfBuiS7ASzwViqH0NpMdzPhlEhckBak+6WtOzS
# dwMkG6ckuHDIrTqvBB3gfN2Pp0ORnfDOB7GbqKWMcqy8JPZtF87gjTSu9Hqf6BPe
# B/OGUNCIEWeFWMbzuEhyvPSWzTCWZQGi7V89lpM+0d2Fyf+t5F4TM//OinL0PRrB
# fh3q4CEXMVeESixpk8rEal12OnuHMmWiWTB9bgDljkBJbS9ykYYflOxcb7SfV3/f
# f/iC
# SIG # End signature block