diff --git a/src/modules/SdnDiag.NetworkController.psm1 b/src/modules/SdnDiag.NetworkController.psm1 index f34cdc8d..26229c34 100644 --- a/src/modules/SdnDiag.NetworkController.psm1 +++ b/src/modules/SdnDiag.NetworkController.psm1 @@ -234,7 +234,10 @@ function Get-PublicIpReference { # with the ipconfiguration and return back to calling function if ($IpConfiguration.properties.publicIPAddress) { "Located {0} associated with {1}" -f $IpConfiguration.properties.publicIPAddress.resourceRef, $IpConfiguration.resourceRef | Trace-Output -Level:Verbose - return ($IpConfiguration.properties.publicIPAddress.resourceRef) + return [PSCustomObject]@{ + ResourceRef = $IpConfiguration.properties.publicIPAddress.resourceRef + IPAddress = $null + } } else { "Unable to locate an instance-level public IP address associated with {0}" -f $IpConfiguration.resourceRef | Trace-Output -Level:Verbose @@ -269,8 +272,26 @@ function Get-PublicIpReference { $natRule = $loadBalancers.properties.outboundNatRules | Where-Object { $_.resourceRef -eq $obRuleRef } $frontendConfig = $loadBalancers.properties.frontendIPConfigurations | Where-Object { $_.resourceRef -eq $natRule.properties.frontendIPConfigurations[0].resourceRef } - "Located {0} associated with {0}" -f $frontendConfig.resourceRef, $natRule.resourceRef | Trace-Output -Level:Verbose - return ($frontendConfig.properties.publicIPAddress.resourceRef) + "Located {0} associated with {1}" -f $frontendConfig.resourceRef, $natRule.resourceRef | Trace-Output -Level:Verbose + + # the public IP can be referenced via a publicIPAddress resource or by a static privateIPAddress + # from the public VIP logical network; check both and return the appropriate value + if ($frontendConfig.properties.publicIPAddress) { + return [PSCustomObject]@{ + ResourceRef = $frontendConfig.properties.publicIPAddress.resourceRef + IPAddress = $null + } + } + elseif (![string]::IsNullOrEmpty($frontendConfig.properties.privateIPAddress)) { + "Located static privateIPAddress {0} on frontend {1}" -f $frontendConfig.properties.privateIPAddress, $frontendConfig.resourceRef | Trace-Output -Level:Verbose + return [PSCustomObject]@{ + ResourceRef = $null + IPAddress = $frontendConfig.properties.privateIPAddress + } + } + else { + "Unable to locate publicIPAddress or privateIPAddress on frontend {0}" -f $frontendConfig.resourceRef | Trace-Output -Level:Verbose + } } else { "Unable to locate outboundNatRules associated with {0}" -f $IpConfiguration.properties.loadBalancerBackendAddressPools.resourceRef | Trace-Output -Level:Verbose @@ -1832,14 +1853,28 @@ function Get-SdnNetworkInterfaceOutboundPublicIPAddress { foreach ($ipConfig in $networkInterface.properties.ipConfigurations) { $publicIpRef = Get-PublicIpReference @ncRestParams -IpConfiguration $ipConfig if ($publicIpRef) { - $publicIpAddress = Get-SdnResource @ncRestParams -ResourceRef $publicIpRef - if ($publicIpAddress) { + if ($publicIpRef.ResourceRef) { + # public IP is referenced via a publicIPAddress resource; look it up to get the IP address + $publicIpAddress = Get-SdnResource @ncRestParams -ResourceRef $publicIpRef.ResourceRef + if ($publicIpAddress) { + [void]$arrayList.Add( + [PSCustomObject]@{ + IPConfigResourceRef = $ipConfig.resourceRef + IPConfigPrivateIPAddress = $ipConfig.properties.privateIPAddress + PublicIPResourceRef = $publicIpAddress.resourceRef + PublicIPAddress = $publicIpAddress.properties.ipAddress + } + ) + } + } + elseif ($publicIpRef.IPAddress) { + # public IP is a static privateIPAddress on the LB frontend from the public VIP logical network [void]$arrayList.Add( [PSCustomObject]@{ IPConfigResourceRef = $ipConfig.resourceRef IPConfigPrivateIPAddress = $ipConfig.properties.privateIPAddress - PublicIPResourceRef = $publicIpAddress.resourceRef - PublicIPAddress = $publicIpAddress.properties.ipAddress + PublicIPResourceRef = $null + PublicIPAddress = $publicIpRef.IPAddress } ) } diff --git a/tests/offline/NetworkController.Tests.ps1 b/tests/offline/NetworkController.Tests.ps1 index 321849f4..d2d0412e 100644 --- a/tests/offline/NetworkController.Tests.ps1 +++ b/tests/offline/NetworkController.Tests.ps1 @@ -145,3 +145,83 @@ Describe 'NetworkController - Get-SdnResource' { } } } + +Describe 'NetworkController - Get-SdnNetworkInterfaceOutboundPublicIPAddress' { + It "Returns public IP for NIC with a direct instance-level publicIPAddress (tenantvm1)" { + InModuleScope SdnDiag.NetworkController { + Mock Invoke-RestMethodWithRetry { + $path = ([Uri]$Uri).AbsolutePath + if ($path -match '/networking/v1/(.+)$') { + $resourceType = ($Matches[1] -split '/')[0] + $refKey = "/$($Matches[1])" + if ($Global:PesterOfflineTests.SdnApiResourcesByRef.ContainsKey($refKey)) { + return $Global:PesterOfflineTests.SdnApiResourcesByRef[$refKey] + } + return [PSCustomObject]@{ value = $Global:PesterOfflineTests.SdnApiResources[$resourceType] } + } + } + $result = Get-SdnNetworkInterfaceOutboundPublicIPAddress -NcUri "https://dvlab-nc.dvlab.contoso.local" -ResourceId "tenantvm1" + $result | Should -Not -BeNullOrEmpty + $result[0].PublicIPAddress | Should -Be "40.40.40.5" + $result[0].PublicIPResourceRef | Should -Be "/publicIPAddresses/pip-tenant-0001" + } + } + + It "Returns public IP for NIC using LB outbound NAT with publicIPAddress on frontend (tenantvm2)" { + InModuleScope SdnDiag.NetworkController { + Mock Invoke-RestMethodWithRetry { + $path = ([Uri]$Uri).AbsolutePath + if ($path -match '/networking/v1/(.+)$') { + $resourceType = ($Matches[1] -split '/')[0] + $refKey = "/$($Matches[1])" + if ($Global:PesterOfflineTests.SdnApiResourcesByRef.ContainsKey($refKey)) { + return $Global:PesterOfflineTests.SdnApiResourcesByRef[$refKey] + } + return [PSCustomObject]@{ value = $Global:PesterOfflineTests.SdnApiResources[$resourceType] } + } + } + $result = Get-SdnNetworkInterfaceOutboundPublicIPAddress -NcUri "https://dvlab-nc.dvlab.contoso.local" -ResourceId "tenantvm2" + $result | Should -Not -BeNullOrEmpty + $result[0].PublicIPAddress | Should -Be "40.40.40.4" + $result[0].PublicIPResourceRef | Should -Be "/publicIPAddresses/pip-outbound-0001" + } + } + + It "Returns public IP for NIC using LB outbound NAT with static privateIPAddress on frontend (tenantvm3)" { + InModuleScope SdnDiag.NetworkController { + Mock Invoke-RestMethodWithRetry { + $path = ([Uri]$Uri).AbsolutePath + if ($path -match '/networking/v1/(.+)$') { + $resourceType = ($Matches[1] -split '/')[0] + $refKey = "/$($Matches[1])" + if ($Global:PesterOfflineTests.SdnApiResourcesByRef.ContainsKey($refKey)) { + return $Global:PesterOfflineTests.SdnApiResourcesByRef[$refKey] + } + return [PSCustomObject]@{ value = $Global:PesterOfflineTests.SdnApiResources[$resourceType] } + } + } + $result = Get-SdnNetworkInterfaceOutboundPublicIPAddress -NcUri "https://dvlab-nc.dvlab.contoso.local" -ResourceId "tenantvm3" + $result | Should -Not -BeNullOrEmpty + $result[0].PublicIPAddress | Should -Be "40.40.40.6" + $result[0].PublicIPResourceRef | Should -BeNullOrEmpty + } + } + + It "Returns empty result for NIC with no public IP association (nic-vm01-0001)" { + InModuleScope SdnDiag.NetworkController { + Mock Invoke-RestMethodWithRetry { + $path = ([Uri]$Uri).AbsolutePath + if ($path -match '/networking/v1/(.+)$') { + $resourceType = ($Matches[1] -split '/')[0] + $refKey = "/$($Matches[1])" + if ($Global:PesterOfflineTests.SdnApiResourcesByRef.ContainsKey($refKey)) { + return $Global:PesterOfflineTests.SdnApiResourcesByRef[$refKey] + } + return [PSCustomObject]@{ value = $Global:PesterOfflineTests.SdnApiResources[$resourceType] } + } + } + $result = Get-SdnNetworkInterfaceOutboundPublicIPAddress -NcUri "https://dvlab-nc.dvlab.contoso.local" -ResourceId "nic-vm01-0001" + $result.Count | Should -Be 0 + } + } +} diff --git a/tests/offline/data/SdnApiResources/loadBalancers.json b/tests/offline/data/SdnApiResources/loadBalancers.json index b2eefb72..f6dbafcd 100644 --- a/tests/offline/data/SdnApiResources/loadBalancers.json +++ b/tests/offline/data/SdnApiResources/loadBalancers.json @@ -76,6 +76,80 @@ "tenantId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" }, "etag": "W/\"lb-etag-0001\"" + }, + { + "resourceId": "lb-outbound-0002", + "properties": { + "frontendIPConfigurations": [ + { + "etag": "W/\"lb-etag-0002\"", + "resourceRef": "/loadBalancers/lb-outbound-0002/frontendIPConfigurations/feip-0002", + "resourceId": "feip-0002", + "properties": { + "subnet": { + "resourceRef": "/logicalNetworks/PublicVIP/subnets/PublicVIP-subnet-0001" + }, + "provisioningState": "Succeeded", + "privateIPAllocationMethod": "Static", + "privateIPAddress": "40.40.40.6" + }, + "instanceId": "feip-inst-0002" + } + ], + "probes": [], + "backendAddressPools": [ + { + "etag": "W/\"lb-etag-0002\"", + "resourceRef": "/loadBalancers/lb-outbound-0002/backendAddressPools/OutboundNatPool", + "resourceId": "OutboundNatPool", + "properties": { + "backendIPConfigurations": [ + { + "resourceRef": "/networkInterfaces/tenantvm3/ipConfigurations/ipconfig1" + } + ], + "provisioningState": "Succeeded", + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/lb-outbound-0002/outboundNatRules/outbound-nat-0002" + } + ] + }, + "instanceId": "bepool-inst-0002" + } + ], + "outboundNatRules": [ + { + "etag": "W/\"lb-etag-0002\"", + "resourceRef": "/loadBalancers/lb-outbound-0002/outboundNatRules/outbound-nat-0002", + "resourceId": "outbound-nat-0002", + "properties": { + "protocol": "All", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/lb-outbound-0002/frontendIPConfigurations/feip-0002" + } + ], + "provisioningState": "Succeeded", + "backendAddressPool": { + "resourceRef": "/loadBalancers/lb-outbound-0002/backendAddressPools/OutboundNatPool" + } + }, + "instanceId": "outnat-inst-0002" + } + ], + "provisioningState": "Succeeded", + "loadBalancingRules": [] + }, + "instanceId": "lb-inst-0002-aaaa-bbbb-cccccccccccc", + "resourceRef": "/loadBalancers/lb-outbound-0002", + "resourceMetadata": { + "client": "Network Resource Provider", + "resourceName": "lb-outbound-0002", + "groupId": "TestWorkload-RG", + "tenantId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + }, + "etag": "W/\"lb-etag-0002\"" } ], "nextLink": "" diff --git a/tests/offline/data/SdnApiResources/networkInterfaces.json b/tests/offline/data/SdnApiResources/networkInterfaces.json index 0330cb05..d04ed1ae 100644 --- a/tests/offline/data/SdnApiResources/networkInterfaces.json +++ b/tests/offline/data/SdnApiResources/networkInterfaces.json @@ -119,6 +119,59 @@ "resourceId": "tenantvm2", "etag": "W/\"nic-etag-0002\"" }, + { + "properties": { + "configurationState": { + "lastUpdatedTime": "2024-03-15T10:00:00.0000000-05:00", + "id": "nic-inst-0005-aaaa-bbbb-cccccccccccc", + "status": "Success" + }, + "privateMacAddress": "001DD8070003", + "isPrimary": true, + "isHostVirtualNetworkInterface": false, + "portSettings": { + "provisioningState": "Succeeded", + "stormLimit": 0, + "vmqWeight": 100, + "iovWeight": 0, + "arpGuardEnabled": "Disabled", + "portFlowLimit": 0, + "macSpoofingEnabled": "Disabled", + "dhcpGuardEnabled": "Disabled" + }, + "ipConfigurations": [ + { + "properties": { + "privateIPAllocationMethod": "Static", + "subnet": { + "resourceRef": "/virtualNetworks/vnet-0001/subnets/subnet-0001" + }, + "loadBalancerInboundNatRules": [], + "loadBalancerBackendAddressPools": [ + { + "resourceRef": "/loadBalancers/lb-outbound-0002/backendAddressPools/OutboundNatPool" + } + ], + "provisioningState": "Succeeded", + "privateIPAddress": "192.168.33.6" + }, + "resourceRef": "/networkInterfaces/tenantvm3/ipConfigurations/ipconfig1", + "resourceId": "ipconfig1", + "etag": "W/\"nic-etag-0005\"", + "instanceId": "ipconf-inst-0005" + } + ], + "server": { + "resourceRef": "/servers/DVLAB-S1-N02" + }, + "provisioningState": "Succeeded", + "privateMacAllocationMethod": "Dynamic" + }, + "instanceId": "nic-inst-0005-aaaa-bbbb-cccccccccccc", + "resourceId": "tenantvm3", + "resourceRef": "/networkInterfaces/tenantvm3", + "etag": "W/\"nic-etag-0005\"" + }, { "properties": { "configurationState": {