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 # MIInvwYJKoZIhvcNAQcCoIInsDCCJ6wCAQExDzANBglghkgBZQMEAgEFADB5Bgor # 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 # /Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEILbgPtyRcxRDHPIuPP4CYaHB # Pyz8S1LlDnSAGuvRP2YPMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAbAOdDgQL8C0F2kyea0gszUEPOyoz44/9GvDfj7pRlZZfIxxWLSAoc2Md # B/Y+3/oFEMen/IReWEkbX9SOI1ZahXoupMpiUDTsH/fQDAMSTkUn16TMY2aTC2Q6 # QVnrqIstoH0d8A16dofBAaUXKx7kRAFV2sMB0v6Xt4+CT6K2fL9tydViRr1bfvqP # GuvXyhUAeoeFod+TzP0SfJ1iiFcInzWum8X4kWCzYu8jJ+cFX00WA1CTGvZFZEtS # pHWil9JMUB7yF4VT8nvsDy/HbNT1CGBnE9gXRpBoLpRcJUbdt+ka+eqjS2b6qlVS # NQquoPby21S7cDvtkInmmfz3KcEZgaGCFykwghclBgorBgEEAYI3AwMBMYIXFTCC # FxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq # hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCDx3qVe3QkkO/SH1mmvAIR1qg7AQq5GC5z2bv20RDp0/gIGZjOrHly7 # GBMyMDI0MDUxNjE4NDUyOS42NzVaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO # OjE3OUUtNEJCMC04MjQ2MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT # ZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAHg1PwfExUffl0AAQAAAeAwDQYJ # KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx # MDEyMTkwNzE5WhcNMjUwMTEwMTkwNzE5WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl # cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjoxNzlFLTRC # QjAtODI0NjElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKyHnPOhxbvRATnGjb/6fuBh # h3ZLzotAxAgdLaZ/zkRFUdeSKzyNt3tqorMK7GDvcXdKs+qIMUbvenlH+w53ssPa # 6rYP760ZuFrABrfserf0kFayNXVzwT7jarJOEjnFMBp+yi+uwQ2TnJuxczceG5FD # HrII6sF6F879lP6ydY0BBZkZ9t39e/svNRieA5gUnv/YcM/bIMY/QYmd9F0B+ebF # Yi+PH4AkXahNkFgK85OIaRrDGvhnxOa/5zGL7Oiii7+J9/QHkdJGlfnRfbQ3QXM/ # 5/umBOKG4JoFY1niZ5RVH5PT0+uCjwcqhTbnvUtfK+N+yB2b9rEZvp2Tv4ZwYzEd # 9A9VsYMuZiCSbaFMk77LwVbklpnw4aHWJXJkEYmJvxRbcThE8FQyOoVkSuKc5OWZ # 2+WM/j50oblA0tCU53AauvUOZRoQBh89nHK+m5pOXKXdYMJ+ceuLYF8h5y/cXLQM # OmqLJz5l7MLqGwU0zHV+MEO8L1Fo2zEEQ4iL4BX8YknKXonHGQacSCaLZot2kyJV # RsFSxn0PlPvHVp0YdsCMzdeiw9jAZ7K9s1WxsZGEBrK/obipX6uxjEpyUA9mbVPl # jlb3R4MWI0E2xI/NM6F4Ac8Ceax3YWLT+aWCZeqiIMLxyyWZg+i1KY8ZEzMeNTKC # EI5wF1wxqr6T1/MQo+8tAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQUcF4XP26dV+8S # usoA1XXQ2TDSmdIwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD # VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j # cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG # CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw # MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD # CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAMATzg6R/A0ldO7M # qGxD1VJji5yVA1hHb0Hc0Yjtv7WkxQ8iwfflulX5Us64tD3+3NT1JkphWzaAWf2w # KdAw35RxtQG1iON3HEZ0X23nde4Kg/Wfbx5rEHkZ9bzKnR/2N5A16+w/1pbwJzdf # RcnJT3cLyawr/kYjMWd63OP0Glq70ua4WUE/Po5pU7rQRbWEoQozY24hAqOcwuRc # m6Cb0JBeTOCeRBntEKgjKep4pRaQt7b9vusT97WeJcfaVosmmPtsZsawgnpIjbBa # 55tHfuk0vDkZtbIXjU4mr5dns9dnanBdBS2PY3N3hIfCPEOszquwHLkfkFZ/9bxw # 8/eRJldtoukHo16afE/AqP/smmGJh5ZR0pmgW6QcX+61rdi5kDJTzCFaoMyYzUS0 # SEbyrDZ/p2KOuKAYNngljiOlllct0uJVz2agfczGjjsKi2AS1WaXvOhgZNmGw42S # FB1qaloa8Kaux9Q2HHLE8gee/5rgOnx9zSbfVUc7IcRNodq6R7v+Rz+P6XKtOgyC # qW/+rhPmp/n7Fq2BGTRkcy//hmS32p6qyglr2K4OoJDJXxFs6lwc8D86qlUeGjUy # o7hVy5VvyA+y0mGnEAuA85tsOcUPlzwWF5sv+B5fz35OW3X4Spk5SiNulnLFRPM5 # XCsSHqvcbC8R3qwj2w1evPhZxDuNMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ # 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 # bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjox # NzlFLTRCQjAtODI0NjElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy # dmljZaIjCgEBMAcGBSsOAwIaAxUAbfPR1fBX6HxYfyPx8zYzJU5fIQyggYMwgYCk # fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF # AOnwnUMwIhgPMjAyNDA1MTYyMjU3MDdaGA8yMDI0MDUxNzIyNTcwN1owdDA6Bgor # BgEEAYRZCgQBMSwwKjAKAgUA6fCdQwIBADAHAgEAAgID3zAHAgEAAgIRrTAKAgUA # 6fHuwwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID # B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAGRlJmD4Pm0a2ZXNrC7c # hWy5hXLsafZMqicwyWFUOagBv008UJXV934PwR/AYu6JEfRQNBQKDOYskFLAOnvO # jXU9VhvPALsmy8+mZD2thf4r2HFkYffZGvyfS7G61QPbGd/6gez0O4p8pdFeQq8o # jY4M2biKfSLGPGc+lo2XncRfMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt # U3RhbXAgUENBIDIwMTACEzMAAAHg1PwfExUffl0AAQAAAeAwDQYJYIZIAWUDBAIB # BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx # IgQgmuoMyLCSooQ/4HEgO0ZUfdr0W/GLSOM8xWueHioace8wgfoGCyqGSIb3DQEJ # EAIvMYHqMIHnMIHkMIG9BCDj7lK/8jnlbTjPvc77DCCSb4TZApY9nJm5whsK/2kK # wTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB4NT8 # HxMVH35dAAEAAAHgMCIEIKTJslI+Gq3aHsWJ9P5eKg7d9P5UfEDHlHAwLXSd1TWf # MA0GCSqGSIb3DQEBCwUABIICAEFGnBN5xddwj5gu0LfoUWOenKEmKfErvMinzXFA # J9iCXvzMrrUBjoIZ2ETS65wKRH46NxRqxAVaSPC+uhLS5Bt6G0vexxbTJFatBiTA # Pya5l/Ff3baj2lbslldpGYOd5jbNHzxdcb1+FQ1yfSZJ4cEx2kZuTh1VF0THs4ST # tGCPJxCO/HKVfcmrcYbJenhdF2doFrgjnpYegI83ZH+xUcyL1aHW77fJ5tpFUAKP # ozyyPjAqH9LSlYJ9/wyswWEvK4NU4EJzidzemkMKjfWrxP5VwdnyOct6+MHviKpa # Hu5/P8cV1qg0c9okEZrv+1qfY0ADX8p4X/dFBsQpjBgOk6auEPES3GVzw28F2xXm # IlmLq4Bk2XHSVyLmJDLp5iGBWJvsArvfg5vec+wVuSjDp2tNFE1zD/rrPd7OfPd2 # 2AOY1gyZRHu0e5+2LtCp3rGN+9WvGKfDf9MaXZxHO7isoNIcXXwV4jPGZ9EE+WJr # twr8uxXQFteSA6PmcXFMkmmh7M+l2/5k/EaO7JNFNgNFZj5g56GwozU6PSr6iEZP # 9AITDF3wgHhgZ7qvpTdPzDrW9hlbQoZRfahwrd4YVmvCiknwrAbwCCLSHMrtU+IO # W1osAfOzOXHVnc9mbwVkHB5WSWLDZlr8aZ+eG34BCTP+mk7L8bap/rNm7zuRa4kD # 1ZMw # SIG # End signature block |