modules/NatPoolToNatRuleMigration/NatPoolToNatRuleMigration.psm1

Import-Module ((Split-Path $PSScriptRoot -Parent) + "/Log/Log.psd1")
Import-Module ((Split-Path $PSScriptRoot -Parent) + "/UpdateVmssInstances/UpdateVmssInstances.psd1")
Function Start-NatPoolToNatRuleMigration {
    <#
.SYNOPSIS
    Migrates an Azure Standard Load Balancer's Inbound NAT Pools to Inbound NAT Rules
.DESCRIPTION
    This script creates a new NAT Rule for each NAT Pool, then adds a new Backend Pool with membership corresponding to the NAT Pool's original membership.
 
    For every NAT Pool, a new NAT Rule and backend pool will be created on the Load Balancer. Names will follow these patterns:
        natrule_migrated_<inboundNATPool Name>
        be_migrated_<inboundNATPool Name>
 
    The script reassociated NAT Pool VMSSes with the new NAT Rules, requiring multiple updates to the VMSS model and instance upgrades, which may cause service disruption during the migration.
 
    Backend port mapping for pool members will not necessarily be the same for NAT Pools with multiple associated VMSSes.
.NOTES
    Please report issues at: https://github.com/Azure/AzLoadBalancerMigration/issues
 
.LINK
    https://github.com/Azure/AzLoadBalancerMigration
     
.EXAMPLE
    Import-Module AzureLoadBalancerNATPoolMigration
    Start-NatPoolToNatRuleMigration -LoadBalancerName lb-standard-01 -verbose -ResourceGroupName rg-natpoollb
     
    # Migrates all NAT Pools on Load Balance 'lb-standard-01' to new NAT Rules.
 
.EXAMPLE
    Import-Module AzureLoadBalancerNATPoolMigration
    $lb = Get-AzLoadBalancer | ? name -eq 'my-standard-lb-01'
    $lb | Start-NatPoolToNatRuleMigration -LoadBalancerName lb-standard-01 -verbose -ResourceGroupName rg-natpoollb
     
    # Migrates all NAT Pools on Load Balance 'lb-standard-01' to new NAT Rules, passing the Load Balancer to the function through the pipeline.
#>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $True, ValueFromPipeline, ParameterSetName = 'ByObject')][Microsoft.Azure.Commands.Network.Models.PSLoadBalancer] $basicLoadBalancer,
        [Parameter(Mandatory = $True, ValueFromPipeline, ParameterSetName = 'ByObject')][Microsoft.Azure.Commands.Network.Models.PSLoadBalancer] $standardLoadBalancer
    )

    Function Wait-VMSSInstanceUpdate {
        [CmdletBinding()]
        param (
            [Parameter()]
            [Microsoft.Azure.Commands.Compute.Automation.Models.PSVirtualMachineScaleSet]
            $vmss
        )

        $vmssInstances = Get-AzVmssVM -ResourceGroupName $vmss.ResourceGroupName -VMScaleSetName $vmss.Name

        If ($vmssInstances.LatestModelApplied -contains $false) {
            log -message "[Start-NatPoolToNatRuleMigration] `tWaiting for VMSS '$($vmss.Name)' to update all instances..."
            Start-Sleep -Seconds 15
            Wait-VMSSInstanceUpdate -vmss $vmss
        }
    }

    $ErrorActionPreference = 'Stop'

    log -message "[Start-NatPoolToNatRuleMigration] Starting NAT Pool to NAT Rule migration..."

    # check load balancer sku
    If ($standardLoadBalancer.sku.name -ne 'Standard') {
        log -Severity Error -terminateOnError -message "[Start-NatPoolToNatRuleMigration] In order to migrate to NAT Rules, the Load Balancer must be a Standard SKU. Upgrade the Load Balancer first. See: https://learn.microsoft.com/azure/load-balancer/load-balancer-basic-upgrade-guidance"
    }

    # check load balancer has inbound nat pools
    If ($basicLoadBalancer.InboundNatPools.count -lt 1) {
        log -message "[Start-NatPoolToNatRuleMigration] Load Balancer '$($basicLoadBalancer.Name)' does not have any Inbound NAT Pools to migrate"
        return
    }

    # create a hard copy of NAT Pool configs for later reference
    $inboundNatPoolConfigs = $basicLoadBalancer.InboundNatPools | ConvertTo-Json | ConvertFrom-Json

    try {
        $ErrorActionPreference = 'Stop'

        # get add virtual machine scale sets associated with the LB NAT Pools (via NAT Pool-create NAT Rules)
        $vmssIds = $basicLoadBalancer.InboundNatRules.BackendIPConfiguration.Id | Foreach-Object { ($_ -split '/virtualMachines/')[0].ToLower() } | Select-Object -Unique

        If (![string]::IsNullOrEmpty($vmssIds)) {
            log -message "[Start-NatPoolToNatRuleMigration] The following VMSSes are associated with the NAT Pools: $($vmssIds -join ', ')"

            $vmssObjects = $vmssIds | ForEach-Object { Get-AzResource -ResourceId $_ | Get-AzVmss }

            # build vmss table
            $vmsses = @()
            ForEach ($vmss in $vmssObjects) {
                $vmsses += @{
                    vmss           = $vmss
                    updateRequired = $false
                }
            }
        }
    }
    catch {
        log -Severity Error -message "[Start-NatPoolToNatRuleMigration] An error occured while getting the VMSSes associated with the NAT Pools. Migration will continue. If VMSSes were associated with NAT Pools, they will need to be manually reassociated post-migration!: $_"
    }

    # check that vmsses use Manual or Automatic upgrade policy
    $incompatibleUpgradePolicyVMSSes = $vmsses.vmss | Where-Object { $_.UpgradePolicy.Mode -notIn 'Manual', 'Automatic' }
    If ($incompatibleUpgradePolicyVMSSes.count -gt 0) {
        log -Severity Error -terminateOnError -message "[Start-NatPoolToNatRuleMigration] The following VMSSes have upgrade policies which are not Manual or Automatic: $($incompatibleUpgradePolicyVMSSes.id)"
    }

    try {
        log -Message "[Start-NatPoolToNatRuleMigration] Starting adding new NAT Rules and Backend Pools to load balancer..."

        $ErrorActionPreference = 'Stop'

        # update load balancer with nat rule configurations
        $natPoolToBEPMap = @{} # { natPoolId = backendPoolId, ... }
        ForEach ($inboundNATPool in $inboundNatPoolConfigs) {

            # add a new backend pool for the NAT rule
            $newBackendPoolName = "be_migrated_$($inboundNATPool.Name)"

            log -message "[Start-NatPoolToNatRuleMigration] Adding new Backend Pool '$newBackendPoolName' to LB for NAT Pool '$($inboundNATPool.Name)'"
            $standardLoadBalancer = $standardLoadBalancer | Add-AzLoadBalancerBackendAddressPoolConfig -Name $newBackendPoolName
            $natPoolToBEPMap[$inboundNATPool.Id] = '{0}/backendAddressPools/{1}' -f $standardLoadBalancer.Id, $newBackendPoolName

            # update the load balancer config
            $standardLoadBalancer = $standardLoadBalancer | Set-AzLoadBalancer 

            # add a NAT rule config
            $frontendIPConfiguration = New-Object Microsoft.Azure.Commands.Network.Models.PSFrontendIPConfiguration
            $frontendIPConfiguration.id = $inboundNATPool.FrontendIPConfiguration.Id -replace $basicLoadBalancer.Id, $standardLoadBalancer.Id

            $backendAddressPool = $standardLoadBalancer.BackendAddressPools | Where-Object { $_.name -eq $newBackendPoolName }

            $newNatRuleName = "natrule_migrated_$($inboundNATPool.Name)"

            log -message "[Start-NatPoolToNatRuleMigration] Adding new NAT Rule '$newNatRuleName' to LB..."
            $standardLoadBalancer = $standardLoadBalancer | Add-AzLoadBalancerInboundNatRuleConfig -Name $newNatRuleName `
                -Protocol $inboundNATPool.Protocol `
                -FrontendPortRangeStart $inboundNATPool.FrontendPortRangeStart `
                -FrontendPortRangeEnd $inboundNATPool.FrontendPortRangeEnd `
                -BackendPort $inboundNATPool.BackendPort `
                -FrontendIpConfiguration $frontendIPConfiguration `
                -BackendAddressPool $backendAddressPool

            # update the load balancer config
            $standardLoadBalancer = $standardLoadBalancer | Set-AzLoadBalancer
        }

        log -Message "[Start-NatPoolToNatRuleMigration] Finished adding new NAT Rules and Backend Pools to load balancer."
    }
    catch {
        log -Severity Error -Message "[Start-NatPoolToNatRuleMigration] An error occured while updating the Load Balancer with new NAT Rules and additional Backend Pools. To recover, address the cause of the following error, then follow the steps at https://aka.ms/basiclbupgradefailure to retry the migration. Error: $_" -terminateOnError
    }

    If ($vmsses) {
        # add vmss model ip configs to new backend pools
        log -message "[Start-NatPoolToNatRuleMigration] Adding new backend pools to VMSS model ipConfigs..."

        try {
            $ErrorActionPreference = 'Stop'
            ForEach ($vmssItem in $vmsses) {
                ForEach ($nicConfig in $vmssItem.vmss.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations) {
                    ForEach ($ipConfig in $nicConfig.ipConfigurations) {
                
                        # if there is an existing ipconfig to nat pool association, add the ipconfig to the new backend pool for the nat rule
                        If ($ipconfigRecord = $ipConfigNatPoolMap | Where-Object {
                                $_.vmssId -eq $vmssItem.vmss.id -and
                                $_.nicName -eq $nicConfig.Name -and
                                $_.ipConfigName -eq $ipConfig.Name
                            }) {

                            #$backendPoolList = New-Object System.Collections.Generic.List[Microsoft.Azure.Commands.Network.Models.PSBackendAddressPool]
                            $backendPoolList = New-Object System.Collections.Generic.List[Microsoft.Azure.Management.Compute.Models.SubResource]

                            # add existing backend pools to pool list to maintain existing membership
                            ForEach ($existingBackendPoolId in $ipConfig.LoadBalancerBackendAddressPools.id) {
                                $backendPoolObj = new-object Microsoft.Azure.Management.Compute.Models.SubResource
                                $backendPoolObj.id = $existingBackendPoolId

                                $backendPoolList.Add($backendPoolObj)
                            }
                            #$backendPoolObj = new-object Microsoft.Azure.Commands.Network.Models.PSBackendAddressPool
                            $backendPoolObj = New-Object Microsoft.Azure.Management.Compute.Models.SubResource
                            $backendPoolObj.id = $natPoolToBEPMap[$ipconfigRecord.inboundNatPoolId]

                            log -message "[Start-NatPoolToNatRuleMigration] Adding VMSS '$($vmssItem.vmss.Name)' NIC '$($nicConfig.Name)' ipConfig '$($ipConfig.Name)' to new backend pool '$($backendPoolObj.id)'"
                            $backendPoolList.Add($backendPoolObj)

                            $ipConfig.LoadBalancerBackendAddressPools = $backendPoolList

                            $vmssItem.updateRequired = $true
                        }
                    }
                }
            }

            # update each vmss to add the backend pool membership to the model
            $vmssModelUpdateAddBackendPoolJobs = @()
            ForEach ($vmssItem in ($vmsses | Where-Object { $_.updateRequired })) {
                $vmss = $vmssItem.vmss
                $job = $vmss | Update-AzVmss -AsJob
                $job.Name = $vmss.vmss.Name + '_modelUpdateAddBackendPool'
                $vmssModelUpdateAddBackendPoolJobs += $job
            }

            log -message "[Start-NatPoolToNatRuleMigration] Waiting for VMSS model to update to include the new Backend Pools..."
            While ($vmssModelUpdateAddBackendPoolJobs.State -contains 'Running') {
                Start-Sleep -Seconds 15
                log -message "[Start-NatPoolToNatRuleMigration] `t[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:sszz')]Waiting for VMSS model update jobs to complete..."
            } 
    
            $vmssModelUpdateAddBackendPoolJobs | Foreach-Object {
                $job = $_
                If ($job.Error -or $job.State -eq 'Failed') {
                    log -Severity Error -terminateOnError -message "[Start-NatPoolToNatRuleMigration] An error occured while updating the VMSS model to add the NAT Rules: $($job.error; $job | Receive-Job)."
                }
            }
 
            # update all vmss instances to include the backend pool
            log -message "[Start-NatPoolToNatRuleMigration] Waiting for VMSS instances to update to include the new Backend Pools..."
            $vmssInstanceUpdateAddBackendPoolJobs = @()
            ForEach ($vmssItem in ($vmsses | Where-Object { $_.updateRequired })) {

                If ($vmss.UpgradePolicy.Mode -eq 'Automatic') {
                    Wait-VMSSInstanceUpdate -vmss $vmss
                }
                Else {
                    $vmss = $vmssItem.vmss
                    $vmssInstances = Get-AzVmssVM -ResourceGroupName $vmss.ResourceGroupName -VMScaleSetName $vmss.Name

                    $job = Update-AzVmssInstance -ResourceGroupName $vmss.ResourceGroupName -VMScaleSetName $vmss.Name -InstanceId $vmssInstances.InstanceId -AsJob
                    $job.Name = $vmss.vmss.Name + '_instanceUpdateAddBackendPool'
                    $vmssInstanceUpdateAddBackendPoolJobs += $job
                }
            }

            # for manual update vmsses, wait for the instance update jobs to complete
            If ($vmssInstanceUpdateAddBackendPoolJobs.count -gt 0) {
                $vmssInstanceUpdateAddBackendPoolJobs | Wait-Job | Foreach-Object {
                    $job = $_
                    If ($job.Error -or $job.State -eq 'Failed') {
                        log -Severity Error -terminateOnError -message "[Start-NatPoolToNatRuleMigration] An error occured while updating the VMSS instanaces to add the NAT Rules: $($job.error; $job | Receive-Job)."
                    }
                }
            }
        }
        catch {
            log -Severity Error -message "[Start-NatPoolToNatRuleMigration] An error occured while updating the VMSS model to add the new Backend Pools. Migration will attempt to continue; if successful, VMSSes will need to be manually associated with new NAT Rules. If continuing fails, address the cause of the following error, then follow the steps at https://aka.ms/basiclbupgradefailure to retry the migration.: $_"
        }
    }
    Else {
        log -message "[Start-NatPoolToNatRuleMigration] No VMSSes are associated with the NAT Pools, migrated as empty"
    }

    log -Message "[Start-NatPoolToNatRuleMigration] NAT Pool to NAT Rule migration complete."
}

Export-ModuleMember -Function Start-NatPoolToNatRuleMigration