Obs/bin/ObsDep/content/Powershell/Common/NetworkHelpers.psm1

<###################################################
 # #
 # Copyright (c) Microsoft. All rights reserved. #
 # #
 ##################################################>


function Test-NetworkIPv4Address
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$true)]
        [System.String] $IPv4Address
    )

    $byte = "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"

    # composing a pattern (IPv4 address):
    $IPv4Template = "^($byte\.){3}$byte$"

    return $IPv4Address -match $IPv4Template
}

function Get-NetworkMgmtIPv4FromECEForAllHosts
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters
    )

    [System.Collections.Hashtable] $retValArray = @{}

    $physicalMachinesPublicConfig = $Parameters.Roles["BareMetal"].PublicConfiguration
    $allHostNodesInfo = $physicalMachinesPublicConfig.Nodes.Node

    foreach ($node in $allHostNodesInfo.Name)
    {
        $nodeIP = Get-NetworkMgmtIPv4FromECEForHost -Parameters $Parameters -HostName $node
        $retValArray.Add($node, $nodeIP)
    }

    return $retValArray
}

function Get-NetworkMgmtIPv4FromECEForHost
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters,

        [Parameter(Mandatory = $true)]
        [System.String] $HostName
    )

    [System.String] $retVal = $null

    $physicalMachinesPublicConfig = $Parameters.Roles["BareMetal"].PublicConfiguration
    $physicalNodesHostNameMgmtIPInfo = $physicalMachinesPublicConfig.PublicInfo.PhysicalNodes.HostNameMgmtIPInfo
    $physicalNodesV2NicInfo = $physicalMachinesPublicConfig.PublicInfo.PhysicalNodesV2.NetworkAdaptersInformation

    # Depending on whether this function is called at PreDeploy stage or Deploy stage, and whether the V3 answer file or the older answer file is used,
    # there are 3 possible ways to get the management IP for a node.
    # At PreDeploy stage:
    # If V3 answer file is used, PhysicalNodesV2.NetworkAdaptersInformation should have the data and should be used.
    # If older answer file is used, PhysicalNodesV2.NetworkAdaptersInformation is empty, PhysicalNodes.HostNameMgmtIPInfo is also empty, so HostNIC is used for back-compat reason.
    # At Deploy stage,
    # If V3 answer file is used, PhysicalNodes.HostNameMgmtIPInfo should have the data and should be used. (PhysicalNodesV2.NetworkAdaptersInformation is empty.)
    # If older answer file is used, PhysicalNodesV2.NetworkAdaptersInformation is empty, PhysicalNodes.HostNameMgmtIPInfo is also empty, so HostNIC is used for back-compat reason.
    #
    # Note: PhysicalNodesV2.NetworkAdaptersInformation and PhysicalNodes.HostNameMgmtIPInfo should have same IP info for same hosts. If not, it is the answer file problem.
    # If DHCP is enabled, return HostName

    if (IsDHCPEnabled -Parameters $Parameters)
    {
        Trace-Execution "[Get-NetworkMgmtIPv4FromECEForHost]: No IP saved in ECE because DHCP is enabled. Use HostName [ $HostName ] for current node."
    }
    else
    {
        # PhysicalNodes.HostNameMgmtIPInfo takes priority
        if (-not [System.String]::IsNullOrWhiteSpace($physicalNodesHostNameMgmtIPInfo))
        {
            $currentHostNameIPInfo = ($physicalNodesHostNameMgmtIPInfo | ConvertFrom-Json) | Where-Object { $_.Name -eq $HostName }
            $retVal = $currentHostNameIPInfo.IPv4Address
            Trace-Execution "[Get-NetworkMgmtIPv4FromECEForHost]: retrieved IP [ $retVal ] for node $($HostName) via PhysicalNodes.HostNameMgmtIPInfo."
        }

        # Then PhysicalNodesV2.NetworkAdaptersInformation takes 2nd priority
        if ([System.String]::IsNullOrEmpty($retVal) -or (-not (Test-NetworkIPv4Address -IPv4Address $retVal)))
        {
            if (-not [System.String]::IsNullOrWhiteSpace($physicalNodesV2NicInfo))
            {
                $currentHostNameIPInfo = ($physicalNodesV2NicInfo | ConvertFrom-Json) | Where-Object { $_.Name -eq $HostName }
                $retVal = $currentHostNameIPInfo.IPv4Address
                Trace-Execution "[Get-NetworkMgmtIPv4FromECEForHost]: retrieved IP [ $retVal ] for node $($HostName) via PhysicalNodesV2.NetworkAdaptersInformation."
            }
        }

        # Nothing found above, fall into Hub HostNIC field for legacy support
        if ([System.String]::IsNullOrEmpty($retVal) -or (-not (Test-NetworkIPv4Address -IPv4Address $retVal)))
        {
            $allHostNodesInfo = $physicalMachinesPublicConfig.Nodes.Node
            [System.Xml.XmlElement[]] $currentHostInfo = $allHostNodesInfo | Where-Object { $_.Name -like $HostName }
            [System.Xml.XmlElement[]] $adapterInfo = $currentHostInfo.NICs.NIC | Where-Object { $_.Name -eq 'HostNIC' }

            # Expecting only 1 HostNIC item in ECE config
            if ($adapterInfo.Count -eq 1)
            {
                $retVal = $adapterInfo[0].IPv4Address.Split('/')[0]
            }
            Trace-Execution "[Get-NetworkMgmtIPv4FromECEForHost]: retrieved IP [ $retVal ] for node $($HostName) via HostNIC info."
        }
    }

    if ([System.String]::IsNullOrEmpty($retVal) -or (-not (Test-NetworkIPv4Address -IPv4Address $retVal)))
    {
        # Fall back to host name in case there is no such IP defined in ECE configuration
        Trace-Execution "[Get-NetworkMgmtIPv4FromECEForHost]: No IP Assigned. Setting Value to [ $HostName ] for current node."
        $retVal = $HostName
    }

    return $retVal
}

function Get-ExternalManagementVMSwitch
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters
    )
    # Retrieve managment intent name
    $mgmtIntentFound = $false
    $MgmtIntentName = $null
    # Method 1: grab the management intent name from Get-NetIntent
    $networkATCIntents = Get-NetIntent
    foreach ($networkATCIntent in $networkATCIntents)
    {
        if ($networkATCIntent.IsManagementIntentSet)
        {
            $MgmtIntentName = $networkATCIntent.IntentName
            $mgmtIntentFound = $true
            break
        }
    }
    # Method 2: grab the management intent name from ECE
    if (!$mgmtIntentFound)
    {
        if (-not $PSBoundParameters.ContainsKey('Parameters')) {
            Trace-Error "Unable to locate the management intent name"
        }
        $mgmtIntentNameNicMapping = (Get-MgmtNICNamesFromECEIntent -Parameters $Parameters).GetEnumerator() | Select-Object -Property Key, Value
        [System.String[]] $MgmtIntentName = $mgmtIntentNameNicMapping[0].Key
    }
    Trace-Execution "Management Intent Name: $MgmtIntentName"

    $externalVMSwitchCount = 0
    try
    {
        Trace-Execution "Getting external VMSwitch(es) if any exist"
        [PSObject[]] $existingVMSwitch = Get-VMSwitch -SwitchType External
        $externalVMSwitchCount = $existingVMSwitch.Count
        Trace-Execution "Found [$($externalVMSwitchCount)] external VMSwitch(es)."
    }
    catch
    {
        Trace-Error "Cannot run Get-VMSwitch. Hyper-V might not installed."
    }

    if ($externalVMSwitchCount -eq 0)
    {
        # No pre-defined external VMSwitch
        Trace-Execution "No pre-defined external VMSwitch exists."
        return $null
    }
    else
    {
        foreach ($externalVMSwitch in $existingVMSwitch) {
            if ($externalVMSwitch.Name -eq "ConvergedSwitch($($MgmtIntentName))")
            {
                Trace-Execution "Found external management VM Switch: $($externalVMSwitch.Name)"
                return $externalVMSwitch
            }
        }

        Trace-Execution "Did not locate external management VM Switch"
        return $null
    }
}

function Get-MgmtNICNamesFromECEIntent
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters
    )

    # Trying to get Management NIC adapter name from ECE, with assumption that the name is same across all nodes
    Trace-Execution "Getting management adapter name from ECE intent..."

    $stampIntentsInfo = Get-ECENetworkATCIntentsInfo -Parameters $Parameters
    [PSObject[]] $mgmtIntent = $stampIntentsInfo | Where-Object { $_.TrafficType.Contains("Management") }

    if ($mgmtIntent.Count -le 0)
    {
        Trace-Error "Cannot find correct management adapter name info from intent definition from ECE."
    }

    [System.String[]] $mgmtNICNames = $mgmtIntent[0].Adapter

    if ($mgmtNICNames.Count -le 0)
    {
        Trace-Error "No adapter defined in the management intent. Expecting at least 1."
    }

    return @{$mgmtIntent[0].Name.ToLower() = $mgmtNICNames}
}


function Get-ECENetworkATCIntentsInfo
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $hostNetworkRole = $Parameters.Roles["HostNetwork"].PublicConfiguration
    $atcHostNetworkConfiguration = $hostNetworkRole.PublicInfo.ATCHostNetwork.Configuration | ConvertFrom-JSON

    [PSObject[]] $atcHostIntents = $atcHostNetworkConfiguration.Intents

    if ($atcHostIntents.Count -eq 0) {
        Trace-Execution "No intent defined from end user input. Will default to fully converged scenario"

        # In case we don't have any intent info defined in the system, default to fully converged scenario
        # And we will try to get all NIC info from the current running node
        $intentinfo = @{}

        $intentInfo.Name = "mgmt-storage-compute"
        Trace-Execution "Intent name: [ $($intentInfo.Name) ]"

        $intentInfo.TrafficType = @("Management", "Compute", "Storage")
        Trace-Execution "Intent Type: [ $($intentInfo.TrafficType | Out-String) ]"

        $nics = Get-NetAdapter -Physical | Where-Object status -eq 'Up'

        [System.String[]] $nicNames = @()
        if (Get-IsMachineVirtual) {
            Trace-Execution "Finding the physical NICs that has DHCP or Static IP assigned"
            $nicNames = Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias $nics.Name -PrefixOrigin @("Dhcp", "Manual") -ErrorAction SilentlyContinue | Foreach-object InterfaceAlias

            Trace-Execution "Finding the physical NICs that is WellKnown, which is APIPA or Loopback"
            $nicNames += (Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias $nics.Name -PrefixOrigin "WellKnown" -ErrorAction SilentlyContinue | Foreach-object InterfaceAlias)

            if ($null -eq $nicNames -or $nicNames.Count -eq 0) {
                # In case the physical NIC does not have IP assigned, we just use all physical NIC.
                $nicNames = [string[]] ($nics | ForEach-Object Name)
            }

            Trace-Execution "Found physical NICs that are UP: $nicNames ."
        }
        else {
            # For physical env, we cannot blindly use all NIC that are UP in the system for fully converged scenario
            # as some NIC in the system might not working propertly even it showed as "Up" in OS.
            # The workaround is to use only RDMA supported NIC for fully converged scenario on physical environment.
            Trace-Execution "Getting the physical NICs that are UP and also support RDMA on the physical host"
            $adapterNames = Get-NetAdapterAdvancedProperty -RegistryKeyword '*NetworkDirect' | ForEach-Object Name

            if ($adapterNames) {
                $nics = Get-NetAdapter -Physical -Name $adapterNames -ErrorAction SilentlyContinue | Where-Object status -eq 'Up'
            }

            $nicNames = [string[]] ($nics | ForEach-Object Name)
            Trace-Execution "Found physical NICs that are UP and support RDMA on physical host: $nicNames ."
        }

        $intentInfo.Adapter = $nicNames
        $intentInfo.OverrideVirtualSwitchConfiguration = "False"
        $intentInfo.OverrideQoSPolicy = "False"
        $intentInfo.OverrideAdapterProperty = "False"

        $atcHostIntents += $intentInfo
    }

    [System.String[]] $allAdapters = @()

    foreach ($intent in $atcHostIntents) {
        Get-MgmtVMSwitchIntentAdapterTest -AllUsedAdapters ([ref] $allAdapters) -Intent $intent
    }

    return $atcHostIntents
}

function Get-IsMachineVirtual
{
    $oemModel = (Get-WmiObject -Class:Win32_ComputerSystem).Model

    return ($oemModel -ieq "Virtual Machine")
}

function Get-MgmtVMSwitchIntentAdapterTest
{
    Param(
        [ref] $AllUsedAdapters,
        [PSObject] $Intent
    )

    # Intent Name field should have valid value
    if ([System.String]::IsNullOrEmpty($Intent.Name)) {
        Trace-Error "Intent name either is empty or not defined!"
    }

    Trace-Execution "Intent Name $($Intent.Name) is valid."

    foreach ($nic in $Intent.Adapter) {
        # adpater should NOT be used by any other intent
        if ($AllUsedAdapters.Value.Contains($nic)) {
            Trace-Error "Adapter [ $($nic) ] already used by another intent!"
        }

        $AllUsedAdapters.Value += $nic
        Trace-Execution "Added $($nic) into all intents adapter list."

        # One adapter should exists in the system
        [PSObject[]] $tmp = Get-NetAdapter -Physical | Where-Object { $_.Status -eq "Up" -and $_.Name -eq $nic }

        if ($tmp.Count -ne 1) {
            Trace-Error "Wrong number ($($tmp.Count)) adapter found in the system for adapter [ $($nic) ] defined in intent [ $($Intent.Name) ]"
        }

        Trace-Execution "Adapter $($nic) is valid in the system."
    }
}

function Get-ReservedIpAddress
{
    param(
        [Parameter(Mandatory = $true)]
        [string] $IpReservationName
    )

    $eceClient = Create-ECEClusterServiceClient
    $EceXml = [XML]($eceClient.GetCloudParameters().getAwaiter().GetResult().CloudDefinitionAsXmlString)

    $subnetRangesCategory = $EceXml.Parameters.Category | Where-Object {$_.Name -ieq "Subnet Ranges"}
    Trace-Execution "SubnetRangeCategory = $($subnetRangesCategory | Out-String)" -Verbose

    # Currently there is only one "Management Subnet", a new feature to support multiple disjoint subnet ranges will be added
    # These subnet names are expected to be of the fashion "Management Subnet1", "Management Subnet2", "Management Subnet3"
    $managementSubnets = @()
    $managementSubnets += $subnetRangesCategory.parameter | Where-Object {$_.name -match "Management Subnet"}
    Trace-Execution "Management Subnets Count = $($managementSubnets.count)"
    Trace-Execution "Management Subnet Name = $($managementSubnets.Name | Out-String)"
    $reservationNameWithPrefix = "(-IPReservation-$IpReservationName)"
    Trace-Execution "Looking for IP reservation name = $reservationNameWithPrefix" -Verbose

    foreach ($subnet in $managementSubnets)
    {
        $token = $subnet.Mapping | Where-Object {$_.Token -match $reservationNameWithPrefix }
        if ($token)
        {
            break
        }
    }

    if ($token)
    {
        Trace-Execution "Found management subnet with $IpReservationName returning $($token.IPAddress)" -Verbose
    } else
    {
        $err = "No management subnet found with $IpReservationName"
        Trace-Execution $err -Verbose
        throw $err
    }

    return $token.IPAddress
}

function Test-IPConnection {
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory = $True)]
        [string]
        $IP,

        [int]
        $TimeoutInSeconds = 1
    )

    try {
        return Test-NetConnection -ComputerName $IP -WarningAction SilentlyContinue -InformationLevel Quiet
    } catch {
        Trace-Warning "Pinging $IP failed with the following error: $_.ToString()"
        return $false
    }
}

function ConvertTo-SubnetMask {
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory = $True)]
        [ValidateRange(0, 32)]
        [UInt32]
        $PrefixLength
    )

    $byteMask = ([Convert]::ToUInt32($(("1" * $PrefixLength).PadRight(32, "0")), 2))
    $bytes = [BitConverter]::GetBytes($byteMask)
    [Array]::Reverse($bytes)
    $ipAddress = New-Object System.Net.IPAddress -ArgumentList (, $bytes)
    return $ipAddress.IPAddressToString
}

function ConvertTo-PrefixLength {
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory = $True)]
        [System.Net.IPAddress]
        $SubnetMask
    )

    $Bits = "$($SubnetMask.GetAddressBytes() | ForEach-Object {[Convert]::ToString($_, 2)})" -Replace '[\s0]'
    $Bits.Length
}

# Convert IP address to UInt32 to use for IP transformation (compare, increment, mask, etc.).
# Note that an existing 'Address' property of [System.Net.IPAddress] is unusable as it has byte order reversed.
function ConvertFrom-IPAddress {
    param (
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress]
        $IPAddress
    )

    $bytes = $IPAddress.GetAddressBytes()
    [Array]::Reverse($bytes)

    return [BitConverter]::ToUInt32($bytes, 0)
}

# Note that this function returns IPAdrress string representation, not [System.Net.IPAddress].
# String representation is more usable for validation as it is more easy to compare.
function ConvertTo-IPAddress {
    param (
        [Parameter(Mandatory=$true)]
        [UInt32]
        $Value
    )

    $bytes = [BitConverter]::GetBytes($Value)
    [Array]::Reverse($bytes)

    # Construct new IPAddress object from byte array.
    # ', ' construct is used to wrap $bytes array into another array to prevent treating each byte as a separate argument.
    $ipAddress = New-Object System.Net.IPAddress -ArgumentList (, $bytes)

    return $ipAddress.IPAddressToString
}

function Get-NetworkAddress {
    param (
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress]
        $IPAddress,

        [Parameter(Mandatory=$true)]
        [UInt32]
        $PrefixLength
    )

    $value = ConvertFrom-IPAddress $IPAddress

    $networkMask = [Convert]::ToUInt32(("1" * $PrefixLength).PadRight(32, "0"), 2)
    $transformedValue = $value -band $networkMask

    return (ConvertTo-IPAddress $transformedValue)
}

function Get-BroadcastAddress {
    param (
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress]
        $IPAddress,

        [Parameter(Mandatory=$true)]
        [UInt32]
        $PrefixLength
    )

    $value = ConvertFrom-IPAddress $IPAddress

    $hostMask = [Convert]::ToUInt32("1" * (32 - $PrefixLength), 2)
    $transformedValue = $value -bor $hostMask

    return (ConvertTo-IPAddress $transformedValue)
}

function Get-RangeEndAddress {
    param (
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress]
        $IPAddress,

        [Parameter(Mandatory=$true)]
        [UInt32]
        $PrefixLength
    )

    $value = ConvertFrom-IPAddress $IPAddress

    $hostMask = [Convert]::ToUInt32("1" * (32 - $PrefixLength), 2)
    $transformedValue = $value -bor $hostMask
    $transformedValue--

    return (ConvertTo-IPAddress $transformedValue)
}

function Add-IPAddress {
    param (
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress]
        $IPAddress,

        [Parameter(Mandatory=$true)]
        [Int]
        $Addend
    )

    $value = ConvertFrom-IPAddress $IPAddress

    $transformedValue = $value + $Addend

    return (ConvertTo-IPAddress $transformedValue)
}

function Get-GatewayAddress {
    param (
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress]
        $IPAddress,

        [Parameter(Mandatory=$true)]
        [UInt32]
        $PrefixLength
    )

    $networkAddress = Get-NetworkAddress $IPAddress $PrefixLength
    return (Add-IPAddress $networkAddress 1)
}

function Get-MgmtNetworkGatewayAddress
{
    param
    (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters
    )

    [System.String] $retVal = $null

    $networkDefinition = Get-NetworkDefinitions -Parameters $Parameters
    [PSObject[]] $mgmtNetwork = $networkDefinition.Networks.Network | Where-Object { $_.Name -eq "Management" }
    $retVal = $mgmtNetwork[0].IPv4.DefaultGateWay

    if ([System.String]::IsNullOrEmpty($retVal) -or (-not (Test-NetworkIPv4Address -IPv4Address $retVal)))
    {
        $retVal = $null
    }

    return $retVal
}

# Returns two IP addresses delimiting the addressable part of the scope and the prefix length, e.g. 10.0.0.1, 10.0.0.254, 24 for 10.0.0.0/24.
function Get-ScopeRange {
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Scope
    )

    $scopeIP, $prefixLength = $Scope -split '/'
    $networkAddress = Get-NetworkAddress $scopeIP $prefixLength
    $scopeStart = Add-IPAddress $networkAddress 1
    $broadcastAddress = Get-BroadcastAddress $scopeIP $prefixLength
    $scopeEnd = Add-IPAddress $broadcastAddress -1
    return $scopeStart, $scopeEnd, $prefixLength
}

function Get-MacAddressString {
    param (
        [System.Net.NetworkInformation.PhysicalAddress]
        $MacAddress
    )

    $originalOfs = $ofs
    $ofs = '-'
    $macAddressString = "$($MacAddress.GetAddressBytes() | ForEach-Object {'{0:X2}' -f $_})"
    $ofs = $originalOfs
    return $macAddressString
}

function NormalizeIPv4Subnet
{
    param(
        [Parameter(Mandatory=$true)][string]$cidrSubnet
        )
    # $cidrSubnet is IPv4 subnet in CIDR format, such as 192.168.10.0/24
    $subnet, $prefixLength = $cidrSubnet.Split('/')

    $addr = $null
    if (([System.Net.IPAddress]::TryParse($subnet, [ref]$addr) -ne $true) -or ($addr.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork)) {
        throw "$subnet is not a valid IPv4 address."
    }

    if ($prefixLength -lt 0 -or $prefixLength -gt 32) {
        throw "$prefixLength is not a valid IPv4 subnet prefix-length."
    }

    $networkAddress = Get-NetworkAddress $subnet $prefixLength

    return $networkAddress.ToString() + '/' + $prefixLength
}

function Get-NetworkNameForCluster
{
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ClusterName,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $NetworkName
    )

    if (($NetworkName -eq 'External') -or ($NetworkName -eq 'InternalVip'))
    {
        return $NetworkName
    }

    # AzS, single cluster only, it always has clusterId == 's-cluster', while cluster name is provided at deployment time.
    # In order to keep backward compatibility, we don't change the function interface for now.
    return "s-cluster-$NetworkName"
}

function Get-NetworkDefinitionForCluster
{
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ClusterName,

        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $clusterRole = $Parameters.Roles["Cluster"].PublicConfiguration
    $clusterId = ($clusterRole.Clusters.Node | ? Name -eq $ClusterName).Id

    $networkRole = $Parameters.Roles["Network"].PublicConfiguration
    return $networkRole.NetworkDefinitions.Node | Where-Object { $_.RefClusterId -ieq $clusterId }
}

function Get-NetworkDefinitions
{
    param(
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $networkRole = $Parameters.Roles["Network"].PublicConfiguration
    return $networkRole.NetworkDefinitions.Node
}

function Get-NetworkSchemaVersion
{
    param (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
   )

    $cloudRole = $Parameters.Roles["Cloud"].PublicConfiguration
    return ($cloudRole.PublicInfo.NetworkConfiguration.Version).Id
}

function IsNetworkSchemaVersion2021
{
    param (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
   )

   return (Get-NetworkSchemaVersion($Parameters)) -eq "2021"
}

function IsDHCPEnabled
{
    param (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $hostNetworkRole = $Parameters.Roles["HostNetwork"].PublicConfiguration
    $DHCPConfiguration = $hostNetworkRole.PublicInfo.DHCP.Configuration

    return ($DHCPConfiguration -ieq "True")
}

function Check-IPAddressFormat
{
    param(
        [Parameter(Mandatory=$true)]
        [string] $IPAddress
    )

    $addr = $null
    return ([System.Net.IPAddress]::TryParse($IPAddress, [ref] $addr)) -and
        ($addr.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork)
}

function Check-PortFormat
{
    param(
        [Parameter(Mandatory=$true)]
        [string] $Port
    )

    return ([bool]($Port -as [int]) -and ($Port -In 0..65535))
}

function Check-ProxyParameters
{
    param(
        [Parameter(Mandatory=$true)]
        [AllowNull()]
        [AllowEmptyString()]
        [string] $IPAddress1,
        [Parameter(Mandatory=$true)]
        [AllowNull()]
        [AllowEmptyString()]
        [string] $IPAddress2,
        [Parameter(Mandatory=$true)]
        [AllowNull()]
        [AllowEmptyString()]
        [string] $Port
    )

    return ($IPAddress1 -and $IPAddress2 -and $Port -and (Check-IPAddressFormat -IPAddress $IPAddress1) -and (Check-IPAddressFormat -IPAddress $IPAddress2) -and (Check-PortFormat -Port $Port))
}

function Get-ASProxySettings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters
    )
    
    $proxyServerHTTP = [System.Environment]::GetEnvironmentVariable("HTTP_PROXY", "Machine")
    $proxyServerHTTPS = [System.Environment]::GetEnvironmentVariable("HTTPS_PROXY", "Machine")
    $proxyServerNoProxy = [System.Environment]::GetEnvironmentVariable("NO_PROXY", "Machine")
    # TODO: figure out where the certificate path is stored

    # validate proxy bypass list includes the required values
    $domainRole = $Parameters.Roles["Domain"].PublicConfiguration
    $customerdomain = $domainRole.PublicInfo.DomainConfiguration.FQDN
    $requiredBypasses = @("localhost", "127.0.0.1", ".svc", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", ".$customerdomain")
    # convert the comma-separated string into an array to make search easier below
    $bypasses = $proxyServerNoProxy.split(',')
    foreach ($bypass in $requiredBypasses)
    {
        if ($bypasses -notcontains $bypass)
        {
            Trace-Execution "Adding $bypass to bypass list"
            $proxyServerNoProxy += ", $bypass"
        }
    }

    # Construct the proxy settings object to return
    $ProxySettings = @{
        HTTP            = $proxyServerHTTP
        HTTPS           = $proxyServerHTTPS
        ByPass          = $proxyServerNoProxy
        # TODO: return certificate path
    }
    return $ProxySettings
}

# Below functions are for networking usage during pre-deploy phase only
function Get-NetworkAllHostIPForBareMetalStage
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters
    )

    [System.Collections.Hashtable] $retValArray = @{}

    $physicalMachinesPublicConfig = $Parameters.Roles["BareMetal"].PublicConfiguration
    $allHostNodesInfo = $physicalMachinesPublicConfig.Nodes.Node

    foreach ($node in $allHostNodesInfo.Name)
    {
        $nodeIP = Get-NetworkHostIPForBareMetalStage -Parameters $Parameters -HostName $node
        $retValArray.Add($node, $nodeIP)
    }

    return $retValArray
}

function Get-NetworkHostIPForBareMetalStage
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters,

        [Parameter(Mandatory=$true)]
        [System.String] $HostName
    )

    [System.String] $retVal = ""

    $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration
    $hostNodeInfo = $physicalMachinesRole.Nodes.Node | Where-Object { $_.Name -eq $HostName }

    $configurationFile = Join-Path "$env:SystemDrive\CloudDeployment\Configuration\Roles\Infrastructure\DeploymentMachine" "NetworkBootServer.json"
    $configFileContent = Get-Content -Path $configurationFile -Raw

    if ($configFileContent)
    {
        $pxeConfigJson = ConvertFrom-Json $configFileContent
    }

    if ($pxeConfigJson -and $pxeConfigJson.DHCP)
    {
        $hostIPReservation = $pxeConfigJson.DHCP.Reservations

        if ($hostIPReservation)
        {
            $retVal = $hostIPReservation.$($hostNodeInfo.MacAddress)
        }
    }

    # Fall back to HostNIC if above bootserver JSON is not there yet.
    if ([System.String]::IsNullOrEmpty($retVal))
    {
        $allHostNodesInfo = $physicalMachinesRole.Nodes.Node
        [System.Xml.XmlElement[]] $currentHostInfo = $allHostNodesInfo | Where-Object { $_.Name -like $HostName }
        [System.Xml.XmlElement[]] $adapterInfo = $currentHostInfo.NICs.NIC | Where-Object { $_.Name -eq 'HostNIC' }

        # Expecting only 1 HostNIC item in ECE config
        if ($adapterInfo.Count -eq 1)
        {
            $retVal = $adapterInfo[0].IPv4Address.Split('/')[0]
        }
    }

    return $retVal
}

function Test-NetworkAtcIntentStatus
{
    <#
    .SYNOPSIS
    Checks the intent status for the given intent on all the hosts in the cluster (or single host in standalone case)
 
    .DESCRIPTION
    Checks the intent status for the given intent on all the hosts in the cluster (or individual host in standalone deployment case)
    It throws error if there is any problem while getting the intent statuses.
 
    .EXAMPLE
    Test-NetworkAtcIntentStatus -IntentName $IntentName -$timeoutInSec 60*10 ClusterMode $true
 
    .PARAMETER IntentName
    Name of the intent on the seed node
 
    .PARAMETER timeoutInSec
    Time to wait in seconds for checking the status before timing out
 
    .PARAMETER ClusterMode
    If true runs in a cluster mode.
 
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [PSObject] $IntentName,
        [Parameter(Mandatory=$false)]
        [System.Int32] $TimeoutInSec = 60*20,
        [Parameter(Mandatory=$false)]
        [System.Boolean] $ClusterMode = $false
    )

    try
    {
        $stopWatch = [diagnostics.stopwatch]::StartNew()
        Write-Host "Starting function call $($MyInvocation.MyCommand.Name) on [ $($Env:COMPUTERNAME) ]"

        [string] $TimeString = Get-Date -Format "yyyyMMdd-HHmmss"

        $getIntentStatusParameters = @{}
        $getIntentStatusParameters["Name"] = $IntentName

        if ($ClusterMode)
        {
            $RemoteLogFileRelativePath = "C:\MASLogs\$($MyInvocation.MyCommand.Name)_Cluster_$TimeString.log"
            Start-Transcript -Append -Path $RemoteLogFileRelativePath

            $clusterName = Get-Cluster
            $getIntentStatusParameters["ClusterName"] = $clusterName.Name

            # Get Intent Statuses on all the hosts
            $nodes = Get-ClusterNode
            Write-Host "Will check intent status for cluster [ $($clusterName.Name) ] from computer [ $($Env:COMPUTERNAME) ]"
        }
        else
        {
            $RemoteLogFileRelativePath = "C:\MASLogs\$($MyInvocation.MyCommand.Name)_Standalone_$TimeString.log"
            Start-Transcript -Append -Path $RemoteLogFileRelativePath
            $getIntentStatusParameters["ComputerName"] = $env:COMPUTERNAME
            $nodes = @($env:COMPUTERNAME)

            Write-Host "Will check intent status for computer [ $($Env:COMPUTERNAME) ]"
        }

        while (($intentProvisionedNodeCount -lt $nodes.Count) -and ($stopWatch.Elapsed.TotalSeconds -lt $TimeoutInSec))
        {
            $intentProvisionedNodeCount = 0

            try
            {
                if ($ClusterMode)
                {
                    # Need to make sure cluster service itself is running and cluster IP Address resource is Online
                    $clus = Get-Service -Name clussvc
                    Write-Host "Cluster Service Status: [ $($clus.Status) ]"

                    Write-Host "Try to clear quarantine state on all cluster nodes by running `"Start-ClusterNode -ClearQuarantine -Name $($nodes.Name)`""
                    Start-ClusterNode -ClearQuarantine -Name $nodes.Name

                    # Wait for 5 seconds so quarantine state could be cleaned correctly
                    Start-Sleep -seconds 5

                    Write-Host "Make sure Cluster Name and Cluster IP Address resource is Online by running `"Start-ClusterResource -Name `"Cluster Name`"`""
                    Start-ClusterResource -Name "Cluster Name" -Wait 5
                    Write-Host "Call `"Start-ClusterResource -Name `"Cluster Name`"`" finished!"
                }

                Write-Host "Call `"Get-NetIntentStatus`" with below parameters"
                Write-Host ($getIntentStatusParameters | Out-String)
                $intentStatuses = Get-NetIntentStatus @getIntentStatusParameters
                Write-Host "Call `"Get-NetIntentStatus`" finished!"
            }
            catch
            {
                $intentStatuses = $null
                Write-Warning "$($_.ScriptStackTrace)"
                Write-Host "Cannot get intent status of [ $($IntentName) ]. Will retry again..."
            }

            if ($intentStatuses)
            {
                Write-Host "Got intent status!"
                foreach ($node in $nodes)
                {
                    $status = $intentStatuses | Where-Object {$_.Host -eq $node}
                    Write-Host "Intent $($IntentName) Host: $($status.Host) Provision Status:"
                    Write-Host "$($status | ConvertTo-Json)"

                    if ($status.ProvisioningStatus -eq "Completed")
                    {
                        if($status.ConfigurationStatus -eq "Success")
                        {
                            Write-Host "Intent $($IntentName) has been applied successfully on the host $($node)."

                            # Check the VMSwitch allocation for management on each node (This is especially for the FRU scenario where its possible that ATC doesn't
                            # do a proper cleanup of the intent status for the FRU'ed node).
                            if ($status.IsManagementIntentSet -ieq "True")
                            {
                                try
                                {
                                    # [Host1]: PS C:\> Get-VMSwitch
                                    # Name SwitchType NetAdapterInterfaceDescription
                                    # ---- ---------- ------------------------------
                                    # ConvergedSwitch(mgmt-storage-compute) External Teamed-Interface
                                    # or
                                    # ConvergedSwitch(managementcompute) External Teamed-Interface

                                    $ManagementSwitch = Get-VMSwitch -ComputerName $node
                                    $ManagementNIC = Get-NetAdapter -Name "vManagement*"

                                    if($null -ne $ManagementNIC -and $null -ne $ManagementSwitch -and $ManagementSwitch.Name -like "ConvergedSwitch*")
                                    {
                                        Write-Host "Host $($node) has Management VMSwitch and vNIC set"
                                        $intentProvisionedNodeCount += 1
                                    }
                                }
                                catch
                                {
                                    $formatstring = "{0} : {1}`n{2}`n" +
                                            " + CategoryInfo : {3}`n" +
                                            " + FullyQualifiedErrorId : {4}`n"

                                    $fields = $_.InvocationInfo.MyCommand.Name,
                                            $_.ErrorDetails.Message,
                                            $_.InvocationInfo.PositionMessage,
                                            $_.CategoryInfo.ToString(),
                                            $_.FullyQualifiedErrorId
                                    Trace-Warning $_
                                    Trace-Warning ($formatstring -f $fields)
                                    Write-Host "Error getting Management VMSwitch or vNIC on the host $($node). Will try again."
                                }
                            }
                            else
                            {
                                $intentProvisionedNodeCount += 1
                            }
                        }
                        else
                        {
                            # stop execution
                            Write-Host "$($status)"
                            throw "Stopping execution after failing to apply intent $($IntentName) on Host $($node)."
                        }
                    }
                }
            }

            Start-Sleep -seconds 5
        }

        # check the intent provision condition again as the above loop could have exited because of a timeout as well.
        if($intentProvisionedNodeCount -ne $nodes.Count)
        {
            throw "Intent validation timed out. Stopping execution after failing to apply intent $($IntentName)."
        }
    }
    catch
    {
        Write-Host "[$($MyInvocation.MyCommand.Name)] failed with exception: $_"
        Write-Host "$($_.ScriptStackTrace)"
        throw $_
    }
    finally
    {
        Write-Host "End function call $($MyInvocation.MyCommand.Name) on [ $($Env:COMPUTERNAME) ]"
        $stopWatch.Stop()
        Stop-Transcript -ErrorAction Ignore
    }
}

function EnableOrDisableDHCPClientEvent
{
    param (
        [Parameter(Mandatory=$false)]
        [System.Boolean]
        $Enable = $true
    )

    # Enable Windows DHCP client events. Following events should be enabled on hosts.
    $logNames = @('Microsoft-Windows-Dhcp-Client/Admin', 'Microsoft-Windows-Dhcp-Client/Operational')

    foreach($logName in $logNames)
    {
        Write-Host  "Checking $($logName) event"
        $out = Get-WinEvent -ListLog $logName | Select-Object IsEnabled
        if($out.IsEnabled -eq $Enable)
        {
            Write-Host  "Try to set $($logName) to enablement:$($Enable), but the event is already set to $($out.IsEnabled)"
        }
        else
        {
            Write-Host  "Setting $logName to enablement:$($Enable)"
            $log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $logName
            $log.IsEnabled = $Enable
            $log.SaveChanges()
            Write-Host  "Event $logName is enablement:$($Enable)"
        }
    }
}

function GetSystemVlanIdFromVirtualAdapter
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false)]
        [System.String] $MgmtIntentName
    )

    $retVal = New-Object PSObject -Property @{
        VlanID = 0
        Message = [string]::Empty
    }

    $existingMgmtVNic = Get-VMNetworkAdapter -Name "vManagement($($MgmtIntentName))" -ManagementOS -ErrorAction SilentlyContinue

    if ($existingMgmtVNic -and ($existingMgmtVNic.Count -eq 1))
    {
        # using Get-VMNetworkAdapterIsolation to find the VlanID used for a valid connection
        $vNicIsolation = Get-VMNetworkAdapterIsolation -VMNetworkAdapter $existingMgmtVNic[0] -ErrorAction SilentlyContinue

        if ($vNicIsolation)
        {
            $retVal.VlanID = $vNicIsolation.DefaultIsolationID
            $retVal.Message = "Management VlanID $($retVal.VlanID) retrieved from VM Network Adapter $($existingMgmtVNic[0].Name)"
        }
        else
        {
            # This should not be hit (otherwise we have a bigger issue in the Get-VMNetworkAdapterIsolation call) but keep it here for error handling
            $retVal.VlanID = -1
            $retVal.Message = "Cannot get valid VM isolation data from VM Network Adapter $($existingMgmtVNic[0].Name)"
        }
    }
    else
    {
        $retVal.VlanID = -1
        $retVal.Message = "Found $($existingMgmtVNic.Count) management VMNetworkAdapters with name like `"vManagement(*)`". Expecting 1."
    }

    return $retVal
}

function GetSystemVlanIdFromPhysicalAdapter
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false)]
        [PSObject[]] $MgmtIntentInfoInEce
    )

    $retVal = New-Object PSObject -Property @{
        VlanID = 0
        Message = [string]::Empty
    }

    $allMgmtAdapters = $MgmtIntentInfoInEce[0].Adapter
    $mgmtIntentName = $mgmtIntentInfoInEce[0].Name.ToLower()

    $retrievedPNICVlanId = 0
    $firstPNICVlanIdInfo = Get-NetAdapterAdvancedProperty -RegistryKeyword VlanID -Name $allMgmtAdapters[0] -ErrorAction SilentlyContinue
    $retrievedPNICVlanId = $firstPNICVlanIdInfo.RegistryValue[0]

    foreach ($mgmtAdapter in $allMgmtAdapters)
    {
        $currentPNICVlanIdInfo = Get-NetAdapterAdvancedProperty -RegistryKeyword VlanID -Name $mgmtAdapter -ErrorAction SilentlyContinue
        $currentPNICVlanId = $currentPNICVlanIdInfo.RegistryValue[0]

        if ($currentPNICVlanId -ne 0)
        {
            if ($retrievedPNICVlanId -eq 0)
            {
                $retrievedPNICVlanId = $currentPNICVlanId
            }

            if ($currentPNICVlanId -ne $retrievedPNICVlanId)
            {
                $retrievedPNICVlanId = -1
                $retVal.Message = "Found multiple VlanIDs on different physical adapters for management intent $($mgmtIntentName)."
                break;
            }
        }
    }

    if ($retrievedPNICVlanId -ne -1)
    {
        $retVal.Message = "Management VlanID $($retrievedPNICVlanId) retrieved from physical adapter."
    }

    $retVal.VlanID = $retrievedPNICVlanId

    return $retVal
}

function Get-MgmtVlanIDForAzureStackHciCluster
{
    <#
        .SYNOPSIS
        Get the management VlanID used in the system for Azure Stack HCI cluster
 
        .DESCRIPTION
        Returns the management VlanID used in the system for Azure Stack HCI cluster by reading system information.
 
        - If the system already have NetworkATC management intent provisioned on it, read the VlanID info from the management intent.
        - Otherwise,
            > If the system has VMSwitch created in advance, we will need to read the VMSwitch/VNIC to get the VlanID
            > Otherwise, we will need to read the physical adapter to get the VlanID
 
        .PARAMETER Parameters
        Optional. ECE parameters object.
        This is needed if the system doesn't have NetworkATC management intent provisioned on it yet.
 
        .OUTPUTS
        PSObject. Returns a PSObject with VlanID and Message properties.
        Valid VlanID is 0 or a positive integer.
        If VlanID is -1, it means we failed to get the VlanID from the system. "Message" property will have the error message.
 
        .EXAMPLE
        PS> Get-MgmtVlanIDForAzureStackHciCluster -Parameters $parameters
    #>


    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters
    )

    # By default we just return VlanID 0
    $retVal = New-Object PSObject -Property @{
        VlanID = 0
        Message = [string]::Empty
    }

    [PSObject] $mgmtIntent = $null

    try
    {
        $mgmtIntent = Get-NetIntent | Where-Object { $_.IsManagementIntentSet -eq $true } | Select-Object -First 1
    }
    catch
    {
        # The only possibility this path be hit is NetworkATC feature/module is not installed on the system.
        # In such case, we will continue to check Vlan ID from VMSwitch/VNIC or physical adapter.
        # So keep this catch here and it won't fail this library call (even the scenario is not valid in HCI LCM context).
    }

    if ($mgmtIntent)
    {
        if ($mgmtIntent.Count -eq 1)
        {
            # System has a mgmt intent provisioned, we can just read the VlanID from the intent
            if ($mgmtIntent.ManagementVLAN)
            {
                $retVal.VlanID = $mgmtIntent.ManagementVLAN
                $retVal.Message = "Management VlanID $($retVal.VlanID) retrieved from NetworkATC intent $($mgmtIntent.IntentName)"
            }
            else
            {
                $retVal.VlanID = 0
                $retVal.Message = "NetworkATC intent $($mgmtIntent.IntentName) is using default ManagementVLAN."
            }
        }
        else
        {
            # This path should not be hit based on NetworkATC requirement (can have only 1 mgmt exist in the system)
            # Keep the code here for error handling and easy maintenance
            $retVal.VlanID = -1
            $retVal.Message = "Found $($mgmtIntent.Count) management intents, expecting only 1."
        }
    }
    else
    {
        # System don't have mgmt intent provisioned, we need to get VlanID from VMSwitch/VNIC or physical adapter

        if ($PSBoundParameters.ContainsKey("Parameters"))
        {
            [PSObject[]] $stampIntentsInfo = Get-ECENetworkATCIntentsInfo -Parameters $Parameters
            [PSObject[]] $mgmtIntentInfoInEce = $stampIntentsInfo | Where-Object { $_.TrafficType.Contains("Management") }

            if ($mgmtIntentInfoInEce.Count -eq 1)
            {
                # Only return valid mgmt intent name when there is only 1 mgmt intent defined in ECE config
                $mgmtVSwitch = Get-ExternalManagementVMSwitch -Parameters $Parameters

                if ($mgmtVSwitch)
                {
                    if ($mgmtVSwitch.Count -eq 1)
                    {
                        $retVal = GetSystemVlanIdFromVirtualAdapter -MgmtIntentName $mgmtIntentInfoInEce[0].Name.ToLower()
                    }
                    else
                    {
                        $retval.VlanID = -1
                        $retVal.Message = "Found $($mgmtVSwitch.Count) external management VMSwitches while trying to retrieve VLAN ID: expecting only 1 external VMSwitches in the system."
                    }
                }
                else
                {
                    $retVal = GetSystemVlanIdFromPhysicalAdapter -MgmtIntentInfoInEce $mgmtIntentInfoInEce
                }
            }
            else
            {
                # This path should not be hit considering we control the ECE config. But keep it here just in case end user messed ECE config somehow.
                $retVal.VlanID = -1
                $retVal.Message = "Found $($mgmtIntentInfoInEce.Count) management intents in Azure Stack HCI LCM configuration. Expecting 1."
            }
        }
        else
        {
            $retVal.VlanID = -1
            $retVal.Message = "Missing parameter `"-Parameters`" for Get-MgmtVlanIDForAzureStackHciCluster"
        }
    }

    return $retVal
}

Export-ModuleMember -Function Add-IPAddress
Export-ModuleMember -Function Check-IPAddressFormat
Export-ModuleMember -Function Check-PortFormat
Export-ModuleMember -Function Check-ProxyParameters
Export-ModuleMember -Function ConvertFrom-IPAddress
Export-ModuleMember -Function ConvertTo-IPAddress
Export-ModuleMember -Function ConvertTo-PrefixLength
Export-ModuleMember -Function ConvertTo-SubnetMask
Export-ModuleMember -Function EnableOrDisableDHCPClientEvent
Export-ModuleMember -Function Get-BroadcastAddress
Export-ModuleMember -Function Get-ExternalManagementVMSwitch
Export-ModuleMember -Function Get-GatewayAddress
Export-ModuleMember -Function Get-MacAddressString
Export-ModuleMember -Function Get-MgmtNetworkGatewayAddress
Export-ModuleMember -Function Get-MgmtVlanIDForAzureStackHciCluster
Export-ModuleMember -Function Get-NetworkAddress
Export-ModuleMember -Function Get-NetworkAllHostIPForBareMetalStage
Export-ModuleMember -Function Get-NetworkDefinitionForCluster
Export-ModuleMember -Function Get-NetworkDefinitions
Export-ModuleMember -Function Get-NetworkHostIPForBareMetalStage
Export-ModuleMember -Function Get-NetworkMgmtIPv4FromECEForAllHosts
Export-ModuleMember -Function Get-NetworkMgmtIPv4FromECEForHost
Export-ModuleMember -Function Get-NetworkNameForCluster
Export-ModuleMember -Function Get-NetworkSchemaVersion
Export-ModuleMember -Function Get-ASProxySettings
Export-ModuleMember -Function Get-RangeEndAddress
Export-ModuleMember -Function Get-ReservedIpAddress
Export-ModuleMember -Function Get-ScopeRange
Export-ModuleMember -Function IsDHCPEnabled
Export-ModuleMember -Function IsNetworkSchemaVersion2021
Export-ModuleMember -Function NormalizeIPv4Subnet
Export-ModuleMember -Function Test-IPConnection
Export-ModuleMember -Function Test-NetworkAtcIntentStatus
Export-ModuleMember -Function Test-NetworkIPv4Address
Export-ModuleMember -Function Get-IsMachineVirtual
# SIG # Begin signature block
# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBNMlCZhfopoc5P
# C4I8u9AGyUIpcbMB6wuPXC4rDbmvD6CCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEILbgPtyRcxRDHPIuPP4CYaHB
# Pyz8S1LlDnSAGuvRP2YPMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAbAOdDgQL8C0F2kyea0gszUEPOyoz44/9GvDfj7pRlZZfIxxWLSAoc2Md
# B/Y+3/oFEMen/IReWEkbX9SOI1ZahXoupMpiUDTsH/fQDAMSTkUn16TMY2aTC2Q6
# QVnrqIstoH0d8A16dofBAaUXKx7kRAFV2sMB0v6Xt4+CT6K2fL9tydViRr1bfvqP
# GuvXyhUAeoeFod+TzP0SfJ1iiFcInzWum8X4kWCzYu8jJ+cFX00WA1CTGvZFZEtS
# pHWil9JMUB7yF4VT8nvsDy/HbNT1CGBnE9gXRpBoLpRcJUbdt+ka+eqjS2b6qlVS
# NQquoPby21S7cDvtkInmmfz3KcEZgaGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC
# F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCDx3qVe3QkkO/SH1mmvAIR1qg7AQq5GC5z2bv20RDp0/gIGZmrcNzej
# GBMyMDI0MDcwOTA4NTQzMC40NDlaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTkzNS0w
# M0UwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHqMIIHIDCCBQigAwIBAgITMwAAAekPcTB+XfESNgABAAAB6TANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzEyMDYxODQ1
# MjZaFw0yNTAzMDUxODQ1MjZaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTkzNS0wM0UwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCsmowxQRVgp4TSc3nTa6yrAPJnV6A7aZYnTw/yx90u
# 1DSH89nvfQNzb+5fmBK8ppH76TmJzjHUcImd845A/pvZY5O8PCBu7Gq+x5Xe6plQ
# t4xwVUUcQITxklOZ1Rm9fJ5nh8gnxOxaezFMM41sDI7LMpKwIKQMwXDctYKvCyQy
# 6kO2sVLB62kF892ZwcYpiIVx3LT1LPdMt1IeS35KY5MxylRdTS7E1Jocl30NgcBi
# JfqnMce05eEipIsTO4DIn//TtP1Rx57VXfvCO8NSCh9dxsyvng0lUVY+urq/G8QR
# FoOl/7oOI0Rf8Qg+3hyYayHsI9wtvDHGnT30Nr41xzTpw2I6ZWaIhPwMu5DvdkEG
# zV7vYT3tb9tTviY3psul1T5D938/AfNLqanVCJtP4yz0VJBSGV+h66ZcaUJOxpbS
# IjImaOLF18NOjmf1nwDatsBouXWXFK7E5S0VLRyoTqDCxHG4mW3mpNQopM/U1WJn
# jssWQluK8eb+MDKlk9E/hOBYKs2KfeQ4HG7dOcK+wMOamGfwvkIe7dkylzm8BeAU
# QC8LxrAQykhSHy+FaQ93DAlfQYowYDtzGXqE6wOATeKFI30u9YlxDTzAuLDK073c
# ndMV4qaD3euXA6xUNCozg7rihiHUaM43Amb9EGuRl022+yPwclmykssk30a4Rp3v
# 9QIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFJF+M4nFCHYjuIj0Wuv+jcjtB+xOMB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBWsSp+rmsxFLe61AE90Ken2XPgQHJDiS4S
# bLhvzfVjDPDmOdRE75uQohYhFMdGwHKbVmLK0lHV1Apz/HciZooyeoAvkHQaHmLh
# wBGkoyAAVxcaaUnHNIUS9LveL00PwmcSDLgN0V/Fyk20QpHDEukwKR8kfaBEX83A
# yvQzlf/boDNoWKEgpdAsL8SzCzXFLnDozzCJGq0RzwQgeEBr8E4K2wQ2WXI/ZJxZ
# S/+d3FdwG4ErBFzzUiSbV2m3xsMP3cqCRFDtJ1C3/JnjXMChnm9bLDD1waJ7TPp5
# wYdv0Ol9+aN0t1BmOzCj8DmqKuUwzgCK9Tjtw5KUjaO6QjegHzndX/tZrY792dfR
# AXr5dGrKkpssIHq6rrWO4PlL3OS+4ciL/l8pm+oNJXWGXYJL5H6LNnKyXJVEw/1F
# bO4+Gz+U4fFFxs2S8UwvrBbYccVQ9O+Flj7xTAeITJsHptAvREqCc+/YxzhIKkA8
# 8Q8QhJKUDtazatJH7ZOdi0LCKwgqQO4H81KZGDSLktFvNRhh8ZBAenn1pW+5UBGY
# z2GpgcxVXKT1CuUYdlHR9D6NrVhGqdhGTg7Og/d/8oMlPG3YjuqFxidiIsoAw2+M
# hI1zXrIi56t6JkJ75J69F+lkh9myJJpNkx41sSB1XK2jJWgq7VlBuP1BuXjZ3qgy
# m9r1wv0MtTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN
# MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkE5MzUtMDNFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQCr
# aYf1xDk2rMnU/VJo2GGK1nxo8aCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6jb26zAiGA8yMDI0MDcwODIzMzgx
# OVoYDzIwMjQwNzA5MjMzODE5WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDqNvbr
# AgEAMAcCAQACAhL2MAcCAQACAhO0MAoCBQDqOEhrAgEAMDYGCisGAQQBhFkKBAIx
# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI
# hvcNAQELBQADggEBADRILuVrwFuS6aJEs1iIaRQBP8QnFNWuQi2Pg7JHaynWRCg/
# TSdvYnIPdxUpaJvlFx0fity624Z6HF0XKh6L1rZU1tEhaBnHCaA3SR6cX/+oGHb3
# 4ib/uMf3pcl4FsLETIyIbx0yHgJhwD3gI/3epz3HohSB2ah97Ysz7CoSVdnzttb/
# pZipXTbyM0cprlXLc/OXX3SFQ2y/2ZWfdnXw9zrDnCPXVZfBxoDvO/gqG7uKakyk
# NJby39QDnLJsjc2fhIhqafA/RQFZ/wowpUidWW/1KPd7IE/l3/r+1/Wc9YvDCqeT
# yaoyw91iRZ6T767QxZdj1hYGkNygNXlnocOnm1sxggQNMIIECQIBATCBkzB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAekPcTB+XfESNgABAAAB6TAN
# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G
# CSqGSIb3DQEJBDEiBCDkCMFPKaXOXS1FOoIXazbyzIMrG7N1sjhpz8V/qcrCbDCB
# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIKSQkniXaTcmj1TKQWF+x2U4riVo
# rGD8TwmgVbN9qsQlMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTACEzMAAAHpD3Ewfl3xEjYAAQAAAekwIgQgiH7OEnzHN1/LICbhwnEiaMKtHXMQ
# MPTqslgX44TZ/iMwDQYJKoZIhvcNAQELBQAEggIAMj7WFXubNO/IJsRqF0xID9u5
# JkyBlHdN2ULrUDUnC8MdoI+ugBW8w8PNWPtaHz9ueB/a/xQOJXtNiOQricR/Q+/p
# noeEtk2JHnQyTqAN1L2NGfiQD8dsuKJLVB9WxIMeFz17eAz572ys6Q55mYQuERBD
# hAWqIpT8yLBWcKm5v1Cs/ZHdjqJkflDHL5v8fcdguNdtLfwvO6udf6fVG2eRx9/u
# nXrR/sNqJwyPvJZMKgnqMkKlB9wHUFtTniLCFHNiyHSO1WGgkhwdD0A+3s7upKtv
# fhSIW1Xi9OL6sRB8IvjwQ75Z3ZhzSda/yFo6oKgUe4B6bcTwpBJCnUeFm7LE1eyk
# n8lqwg+WwmMWAYcWZn/5CrraBFUL+GgsEn7zjueuZxo5OpnMNrlHUv3Gae4RmSZy
# ZAeCEX7n3UOON02cVf9BOac09sbl0AbsegjOW6mQfkAS9PggOdZTSXoVgSRmMnLj
# IxMGx84BzfbxJnkWCtntHm+YUsezvprhUb5j9pM98utI4n8+ir/T4xUSUB3f01st
# tlFzL5H34273RAc2374KpfHwW3bkC2evZwazOtQzNkLOyUjcOx/pbsYgdLV5H79Y
# ayGmpoyum46kp8Vzl9k0zLWtqWhHURkHhmkqepij4CJ2M5hA82SfRRvCKZ7MRD3H
# 6vTIRndfhVlAabHbiBc=
# SIG # End signature block