diff --git a/framework/python/src/net_orc/ip_control.py b/framework/python/src/net_orc/ip_control.py new file mode 100644 index 000000000..eb683c46b --- /dev/null +++ b/framework/python/src/net_orc/ip_control.py @@ -0,0 +1,220 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""IP Control Module""" +from common import logger +from common import util +import re + +LOGGER = logger.get_logger('ip_ctrl') + + +class IPControl: + """IP Control""" + + def __init__(self): + """Initialize the IPControl object""" + + def add_link(self, interface_name, peer_name): + """Create an ip link with a peer""" + success = util.run_command('ip link add ' + interface_name + + ' type veth peer name ' + peer_name) + return success + + def add_namespace(self, namespace): + """Add a network namespace""" + exists = self.namespace_exists(namespace) + LOGGER.info("Namespace exists: " + str(exists)) + if exists: + return True + else: + success = util.run_command('ip netns add ' + namespace) + return success + + def delete_link(self, interface_name): + """Delete an ip link""" + success = util.run_command('ip link delete ' + interface_name) + return success + + def delete_namespace(self, interface_name): + """Delete an ip namespace""" + success = util.run_command('ip netns delete ' + interface_name) + return success + + def link_exists(self, link_name): + links = self.get_links() + return link_name in links + + def namespace_exists(self, namespace): + """Check if a namespace already exists""" + namespaces = self.get_namespaces() + if namespace in namespaces: + return True + else: + return False + + def get_links(self): + stdout, stderr = util.run_command('ip link list') + links = stdout.strip().split('\n') + netns_links = [] + for link in links: + match = re.search(r'\d+:\s+(\S+)', link) + if match: + interface_name = match.group(1) + name_match = re.search(r'(.*)@', interface_name) + if name_match: + interface_name = name_match.group(1) + netns_links.append(interface_name.strip()) + return netns_links + + def get_namespaces(self): + stdout, stderr = util.run_command('ip netns list') + #Strip ID's from the namespace results + namespaces = re.findall(r'(\S+)(?:\s+\(id: \d+\))?', stdout) + return namespaces + + def set_namespace(self, interface_name, namespace): + """Attach an interface to a network namespace""" + success = util.run_command('ip link set ' + interface_name + ' netns ' + + namespace) + return success + + def rename_interface(self, interface_name, namespace, new_name): + """Rename an interface""" + success = util.run_command('ip netns exec ' + namespace + + ' ip link set dev ' + interface_name + ' name ' + + new_name) + return success + + def set_interface_mac(self, interface_name, namespace, mac_addr): + """Set MAC address of an interface""" + success = util.run_command('ip netns exec ' + namespace + + ' ip link set dev ' + interface_name + + ' address ' + mac_addr) + return success + + def set_interface_ip(self, interface_name, namespace, ipaddr): + """Set IP address of an interface""" + success = util.run_command('ip netns exec ' + namespace + ' ip addr add ' + + ipaddr + ' dev ' + interface_name) + return success + + def set_interface_up(self, interface_name, namespace=None): + """Set the interface to the up state""" + if namespace is None: + success = util.run_command('ip link set dev ' + interface_name + ' up') + else: + success = util.run_command('ip netns exec ' + namespace + + ' ip link set dev ' + interface_name + ' up') + return success + + def clean_all(self): + """Cleanup all existing test run interfaces and namespaces""" + + # Delete all namesapces that start with tr + namespaces = self.get_namespaces() + for ns in namespaces: + if 'tr' in ns: + self.delete_namespace(ns) + + # Delete all namespaces that start with tr + links = self.get_links() + for link in links: + if 'tr' in link: + self.delete_link(link) + + def cleanup(self, interface=None, namespace=None): + """Cleanup existing link and namespace if they still exist""" + + link_clean = True + if interface is not None: + if self.link_exists(interface): + link_clean = self.delete_link(interface) + + ns_clean = True + if namespace is not None: + if self.namespace_exists(namespace): + ns_clean = self.delete_namespace + return link_clean and ns_clean + + def configure_container_interface(self, + bridge_intf, + container_intf, + namespace_intf, + namespace, + mac_addr, + container_name=None, + ipv4_addr=None, + ipv6_addr=None): + + # Cleanup old interface and namespaces + self.cleanup(bridge_intf, namespace) + + # Create interface pair + self.add_link(bridge_intf, container_intf) + + if container_name is not None: + # Get PID for running container + # TODO: Some error checking around missing PIDs might be required + container_pid = util.run_command('docker inspect -f {{.State.Pid}} ' + + container_name)[0] + if not container_pid.isdigit(): + LOGGER.error(f'Failed to resolve pid for {container_name}') + return False + + # Create symlink for container network namespace + if not util.run_command('ln -sf /proc/' + container_pid + + '/ns/net /var/run/netns/' + namespace, + output=False): + LOGGER.error( + f'Failed to link {container_name} to namespace {namespace_intf}') + return False + + # Attach container interface to container network namespace + if not self.set_namespace(container_intf, namespace): + LOGGER.error(f'Failed to set namespace {namespace} for {container_intf}') + return False + + # Rename container interface name + if not self.rename_interface(container_intf, namespace, namespace_intf): + LOGGER.error( + f'Failed to rename container interface {container_intf} to {namespace_intf}' + ) + return False + + # Set MAC address of container interface + if not self.set_interface_mac(namespace_intf, namespace, mac_addr): + LOGGER.error( + f'Failed to set MAC address for {namespace_intf} to {mac_addr}') + return False + + # Set IP address of container interface + if ipv4_addr is not None: + if not self.set_interface_ip(namespace_intf, namespace, ipv4_addr): + LOGGER.error( + f'Failed to set IPv4 address for {namespace_intf} to {ipv4_addr}') + return False + if ipv6_addr is not None: + if not self.set_interface_ip(namespace_intf, namespace, ipv6_addr): + LOGGER.error( + f'Failed to set IPv6 address for {namespace_intf} to {ipv6_addr}') + return False + + # Set interfaces up + if not self.set_interface_up(bridge_intf): + LOGGER.error(f'Failed to set interface up {bridge_intf}') + return False + if not self.set_interface_up(namespace_intf, namespace): + LOGGER.error(f'Failed to set interface up {namespace_intf}') + return False + return True diff --git a/framework/python/src/net_orc/network_orchestrator.py b/framework/python/src/net_orc/network_orchestrator.py index f1f479742..f3c07e8e4 100644 --- a/framework/python/src/net_orc/network_orchestrator.py +++ b/framework/python/src/net_orc/network_orchestrator.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Network orchestrator is responsible for managing all of the virtual network services""" import getpass @@ -34,6 +33,7 @@ from net_orc.network_event import NetworkEvent from net_orc.network_validator import NetworkValidator from net_orc.ovs_control import OVSControl +from net_orc.ip_control import IPControl LOGGER = logger.get_logger('net_orc') CONFIG_FILE = 'conf/system.json' @@ -83,15 +83,17 @@ def __init__(self, self.validate = validate self.async_monitor = async_monitor - self._path = os.path.dirname(os.path.dirname( - os.path.dirname( - os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))) + self._path = os.path.dirname( + os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))) self.validator = NetworkValidator() shutil.rmtree(os.path.join(os.getcwd(), NET_DIR), ignore_errors=True) self.network_config = NetworkConfig() self.load_config(config_file) self._ovs = OVSControl() + self._ip_ctrl = IPControl() def start(self): """Start the network orchestrator.""" @@ -181,9 +183,8 @@ def _device_discovered(self, mac_addr): f'Discovered device {mac_addr}. Waiting for device to obtain IP') device = self._get_device(mac_addr=mac_addr) - device_runtime_dir = os.path.join(RUNTIME_DIR, - TEST_DIR, - device.mac_addr.replace(':', '')) + device_runtime_dir = os.path.join(RUNTIME_DIR, TEST_DIR, + device.mac_addr.replace(':', '')) os.makedirs(device_runtime_dir) util.run_command(f'chown -R {self._host_user} {device_runtime_dir}') @@ -201,7 +202,7 @@ def _device_discovered(self, mac_addr): LOGGER.info( f'Device with mac addr {device.mac_addr} has obtained IP address ' f'{device.ip_addr}') - + self._start_device_monitor(device) def _device_has_ip(self, packet): @@ -418,8 +419,7 @@ def _load_network_module(self, module_dir): # Determine if this is a template if 'template' in net_module_json['config']['docker']: - net_module.template = net_module_json['config']['docker'][ - 'template'] + net_module.template = net_module_json['config']['docker']['template'] # Load network service networking configuration if net_module.enable_container: @@ -493,7 +493,7 @@ def _start_network_service(self, net_module): def _get_host_user(self): user = self._get_os_user() - + # If primary method failed, try secondary if user is None: user = self._get_user() @@ -510,7 +510,7 @@ def _get_os_user(self): LOGGER.error("An OS error occurred while retrieving the login name.") except Exception as e: # Catch any other unexpected exceptions - LOGGER.error("An exception occurred:", e) + LOGGER.error("An exception occurred:", e) return user def _get_user(self): @@ -520,15 +520,15 @@ def _get_user(self): except (KeyError, ImportError, ModuleNotFoundError, OSError) as e: # Handle specific exceptions individually if isinstance(e, KeyError): - LOGGER.error("USER environment variable not set or unavailable.") + LOGGER.error("USER environment variable not set or unavailable.") elif isinstance(e, ImportError): - LOGGER.error("Unable to import the getpass module.") + LOGGER.error("Unable to import the getpass module.") elif isinstance(e, ModuleNotFoundError): - LOGGER.error("The getpass module was not found.") + LOGGER.error("The getpass module was not found.") elif isinstance(e, OSError): - LOGGER.error("An OS error occurred while retrieving the username.") + LOGGER.error("An OS error occurred while retrieving the username.") else: - LOGGER.error("An exception occurred:", e) + LOGGER.error("An exception occurred:", e) return user def _stop_service_module(self, net_module, kill=False): @@ -666,9 +666,18 @@ def _attach_service_to_network(self, net_module): # Container network namespace name container_net_ns = 'tr-ctns-' + net_module.dir_name - # Create interface pair - util.run_command('ip link add ' + bridge_intf + ' type veth peer name ' + - container_intf) + # Resolve the interface information + mac_addr = '9a:02:57:1e:8f:' + str(net_module.net_config.ip_index) + ipv4_addr = net_module.net_config.get_ipv4_addr_with_prefix() + ipv6_addr = net_module.net_config.get_ipv6_addr_with_prefix() + + # Add and configure the interface container + if not self._ip_ctrl.configure_container_interface( + bridge_intf, container_intf, "veth0", container_net_ns, mac_addr, + net_module.container_name, ipv4_addr, ipv6_addr): + LOGGER.error('Failed to configure local networking for ' + + net_module.name + '. Exiting.') + sys.exit(1) # Add bridge interface to device bridge if self._ovs.add_port(port=bridge_intf, bridge_name=DEVICE_BRIDGE): @@ -677,42 +686,6 @@ def _attach_service_to_network(self, net_module): DEVICE_BRIDGE + '. Exiting.') sys.exit(1) - # Get PID for running container - # TODO: Some error checking around missing PIDs might be required - container_pid = util.run_command('docker inspect -f {{.State.Pid}} ' + - net_module.container_name)[0] - - # Create symlink for container network namespace - util.run_command('ln -sf /proc/' + container_pid + - '/ns/net /var/run/netns/' + container_net_ns) - - # Attach container interface to container network namespace - util.run_command('ip link set ' + container_intf + ' netns ' + - container_net_ns) - - # Rename container interface name to veth0 - util.run_command('ip netns exec ' + container_net_ns + ' ip link set dev ' + - container_intf + ' name veth0') - - # Set MAC address of container interface - util.run_command('ip netns exec ' + container_net_ns + - ' ip link set dev veth0 address 9a:02:57:1e:8f:' + - str(net_module.net_config.ip_index)) - - # Set IP address of container interface - util.run_command('ip netns exec ' + container_net_ns + ' ip addr add ' + - net_module.net_config.get_ipv4_addr_with_prefix() + - ' dev veth0') - - util.run_command('ip netns exec ' + container_net_ns + ' ip addr add ' + - net_module.net_config.get_ipv6_addr_with_prefix() + - ' dev veth0') - - # Set interfaces up - util.run_command('ip link set dev ' + bridge_intf + ' up') - util.run_command('ip netns exec ' + container_net_ns + - ' ip link set dev veth0 up') - if net_module.net_config.enable_wan: LOGGER.debug('Attaching net service ' + net_module.display_name + ' to internet bridge') @@ -725,9 +698,11 @@ def _attach_service_to_network(self, net_module): # tr-cti-dhcp (Test Run Container Interface for DHCP container) container_intf = 'tr-cti-' + net_module.dir_name - # Create interface pair - util.run_command('ip link add ' + bridge_intf + ' type veth peer name ' + - container_intf) + if not self._ip_ctrl.configure_container_interface( + bridge_intf, container_intf, "eth1", container_net_ns, mac_addr): + LOGGER.error('Failed to configure internet networking for ' + + net_module.name + '. Exiting.') + sys.exit(1) # Attach bridge interface to internet bridge if self._ovs.add_port(port=bridge_intf, bridge_name=INTERNET_BRIDGE): @@ -737,24 +712,6 @@ def _attach_service_to_network(self, net_module): ' to internet bridge ' + DEVICE_BRIDGE + '. Exiting.') sys.exit(1) - # Attach container interface to container network namespace - util.run_command('ip link set ' + container_intf + ' netns ' + - container_net_ns) - - # Rename container interface name to eth1 - util.run_command('ip netns exec ' + container_net_ns + - ' ip link set dev ' + container_intf + ' name eth1') - - # Set MAC address of container interface - util.run_command('ip netns exec ' + container_net_ns + - ' ip link set dev eth1 address 9a:02:57:1e:8f:0' + - str(net_module.net_config.ip_index)) - - # Set interfaces up - util.run_command('ip link set dev ' + bridge_intf + ' up') - util.run_command('ip netns exec ' + container_net_ns + - ' ip link set dev eth1 up') - def restore_net(self): LOGGER.info('Clearing baseline network') @@ -776,6 +733,9 @@ def restore_net(self): # Clear the virtual network self._ovs.restore_net() + # Clean up any existing network artifacts + self._ip_ctrl.clean_all() + # Restart internet interface if util.interface_exists(self._int_intf): util.run_command('ip link set ' + self._int_intf + ' down')