From edc24f7b661f35964cfb1736507ecf74d32960b8 Mon Sep 17 00:00:00 2001 From: SrikanthMyakam Date: Sat, 25 Jan 2025 15:32:56 +0530 Subject: [PATCH] HyperV platform port forwarding for internal switches Virtual machines with an internal switch type cannot be accessed from outside the host. To enable external access, you need to use NAT and port mapping. --- lisa/sut_orchestrator/hyperv/platform_.py | 12 +++- lisa/tools/hyperv.py | 88 +++++++++++++++++++++-- 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/lisa/sut_orchestrator/hyperv/platform_.py b/lisa/sut_orchestrator/hyperv/platform_.py index 80b2333141..404c14f67f 100644 --- a/lisa/sut_orchestrator/hyperv/platform_.py +++ b/lisa/sut_orchestrator/hyperv/platform_.py @@ -10,6 +10,7 @@ from lisa.node import RemoteNode from lisa.platform_ import Platform from lisa.tools import Cp, HyperV, Mkdir, Mount, PowerShell +from lisa.tools.hyperv import HypervSwitchType from lisa.util import LisaException, constants from lisa.util.logger import Logger, get_logger from lisa.util.parallel import run_in_parallel @@ -309,10 +310,19 @@ def _deploy_environment(self, environment: Environment, log: Logger) -> None: hv.start_vm(name=vm_name, extra_args=extra_args) ip_addr = hv.get_ip_address(vm_name) + port = 22 + # If the switch type is internal, we need to add a NAT mapping to access the + # VM from the outside of HyperV host. + if default_switch.type == HypervSwitchType.INTERNAL: + port = hv.add_nat_mapping( + nat_name=default_switch.name, + internal_ip=ip_addr, + ) + ip_addr = node_context.host.public_address username = self.runbook.admin_username password = self.runbook.admin_password node.set_connection_info( - address=ip_addr, username=username, password=password + address=ip_addr, username=username, password=password, public_port=port ) # In some cases, we observe that resize vhd resizes the entire disk # but fails to expand the partition size. diff --git a/lisa/tools/hyperv.py b/lisa/tools/hyperv.py index d328aa72f5..c2f42c5a82 100644 --- a/lisa/tools/hyperv.py +++ b/lisa/tools/hyperv.py @@ -5,7 +5,7 @@ import time from dataclasses import dataclass from enum import Enum -from typing import Dict, Optional +from typing import Any, Dict, Optional, Set from assertpy import assert_that from dataclasses_json import dataclass_json @@ -32,9 +32,18 @@ class VMSwitch: class HyperV(Tool): + # Internal NAT network configuration + INTERNAL_NAT_ROUTER = "192.168.5.1" + INTERNAL_NAT_SUBNET = "192.168.5.0/24" + INTERNAL_NAT_DHCP_IP_START = "192.168.5.50" + INTERNAL_NAT_DHCP_IP_END = "192.168.5.100" + # Azure DNS server + AZURE_DNS_SERVER = "168.63.129.16" # 192.168.5.12 IP_REGEX = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" _default_switch: Optional[VMSwitch] = None + _external_forwarding_port_start = 50000 + _assigned_nat_ports: Set[int] = set() @property def command(self) -> str: @@ -58,6 +67,10 @@ def delete_vm_async(self, name: str) -> Optional[Process]: if not self.exists_vm(name): return None + # delete port mapping for internal IP address of the VM + internal_ip = self.get_ip_address(name) + self.delete_nat_mapping(internal_ip=internal_ip) + # stop and delete vm self.stop_vm(name=name) powershell = self.node.tools[PowerShell] @@ -293,6 +306,50 @@ def create_nat(self, name: str, ip_range: str) -> None: force_run=True, ) + def add_nat_mapping(self, nat_name: str, internal_ip: str) -> int: + # get next available NAT port + external_port = self._get_next_available_nat_port() + # create a new NAT + self.node.tools[PowerShell].run_cmdlet( + f"Add-NetNatStaticMapping -NatName {nat_name} -Protocol TCP " + f"-ExternalIPAddress 0.0.0.0 -InternalIPAddress {internal_ip} " + f"-InternalPort 22 -ExternalPort {external_port}", + force_run=True, + ) + return external_port + + def get_nat_mapping_id(self, external_port: int) -> Any: + return self.node.tools[PowerShell].run_cmdlet( + f"Get-NetNatStaticMapping | " + f"Where-Object {{$_.ExternalPort -eq {external_port}}}" + f" | Select-Object -ExpandProperty StaticMappingID", + force_run=True, + ) + + # delete NAT mapping for a port or internal IP + def delete_nat_mapping( + self, external_port: Optional[int] = None, internal_ip: Optional[str] = None + ) -> None: + if external_port is None: + external_port = self.node.tools[PowerShell].run_cmdlet( + f"Get-NetNatStaticMapping | " + f"Where-Object {{$_.InternalIPAddress -eq '{internal_ip}'}}" + f" | Select-Object -ExpandProperty ExternalPort", + force_run=True, + ) + mapping_id = self.get_nat_mapping_id(external_port) + if mapping_id: + # delete the NAT mapping if it exists + self.node.tools[PowerShell].run_cmdlet( + f"Remove-NetNatStaticMapping -StaticMappingID {mapping_id} " + "-Confirm:$false", + force_run=True, + ) + if external_port: + self._release_nat_port(int(external_port)) + else: + self._log.debug(f"Mapping for port {external_port} does not exist") + def delete_nat_networking(self, switch_name: str, nat_name: str) -> None: # Delete switch self.delete_switch(switch_name) @@ -313,13 +370,13 @@ def setup_nat_networking(self, switch_name: str, nat_name: str) -> None: # set switch interface as gateway for NAT self.node.tools[PowerShell].run_cmdlet( - "New-NetIPAddress -IPAddress 192.168.5.1 " + f"New-NetIPAddress -IPAddress {self.INTERNAL_NAT_ROUTER} " f"-InterfaceIndex {interface_index} -PrefixLength 24", force_run=True, ) # create a new NAT - self.create_nat(nat_name, "192.168.5.0/24") + self.create_nat(nat_name, self.INTERNAL_NAT_SUBNET) def get_ip_address(self, name: str) -> str: # verify vm is running @@ -432,13 +489,18 @@ def enable_internal_dhcp(self, dhcp_scope_name: str = "DHCPInternalNAT") -> None # Configure the DHCP server to use the internal NAT network powershell.run_cmdlet( - f'Add-DhcpServerV4Scope -Name "{dhcp_scope_name}" -StartRange 192.168.0.50 -EndRange 192.168.0.100 -SubnetMask 255.255.255.0', # noqa: E501 + f'Add-DhcpServerV4Scope -Name "{dhcp_scope_name}" ' + f"-StartRange {self.INTERNAL_NAT_DHCP_IP_START} " + f"-EndRange {self.INTERNAL_NAT_DHCP_IP_END} " + f"-SubnetMask 255.255.255.0", force_run=True, ) # Set the DHCP server options powershell.run_cmdlet( - "Set-DhcpServerV4OptionValue -Router 192.168.0.1 -DnsServer 168.63.129.16", + f"Set-DhcpServerV4OptionValue " + f"-Router {self.INTERNAL_NAT_ROUTER} " + f"-DnsServer {self.AZURE_DNS_SERVER}", force_run=True, ) @@ -482,3 +544,19 @@ def _run_hyperv_cmdlet( def _check_exists(self) -> bool: return self.node.tools[WindowsFeatureManagement].is_installed("Hyper-V") + + def _get_next_available_nat_port(self) -> int: + # Start checking from the external forwarding port start and + # find the first available one + port = self._external_forwarding_port_start + while self.get_nat_mapping_id(port): + port += 1 + self._assigned_nat_ports.add(port) # Assign this port + return port + + def _release_nat_port(self, port: int) -> None: + # Release a port if used + if port in self._assigned_nat_ports: + self._assigned_nat_ports.remove(port) + else: + print(f"Port {port} was not assigned.")