diff --git a/.gitignore b/.gitignore index 81ccda05c..172bf5786 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ .tox -.stestr diff --git a/.zuul.yaml b/.zuul.yaml index 09ad56229..585abffbf 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -7,13 +7,11 @@ - stx-puppet-linters - stx-puppet-tox-pep8 - stx-puppet-tox-pylint - - puppet-manifests-tox-py39 gate: jobs: - stx-puppet-linters - stx-puppet-tox-pep8 - stx-puppet-tox-pylint - - puppet-manifests-tox-py39 post: jobs: - stx-stx-puppet-upload-git-mirror @@ -43,19 +41,6 @@ vars: python_version: 3.9 -- job: - name: puppet-manifests-tox-py39 - parent: openstack-tox-py39 - description: | - Run py39 test for puppet-manifests - nodeset: debian-bullseye - files: - - puppet-manifests/* - vars: - tox_envlist: py39 - python_version: 3.9 - tox_extra_args: -c puppet-manifests/tox.ini - - job: name: stx-stx-puppet-upload-git-mirror parent: upload-git-mirror diff --git a/puppet-manifests/.stestr.conf b/puppet-manifests/.stestr.conf deleted file mode 100644 index ea359caed..000000000 --- a/puppet-manifests/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests -top_dir=./ diff --git a/puppet-manifests/src/Makefile b/puppet-manifests/src/Makefile index 2e3f3d818..44226e3e0 100644 --- a/puppet-manifests/src/Makefile +++ b/puppet-manifests/src/Makefile @@ -14,7 +14,6 @@ ifdef ignore_puppet_warnings else install -m 755 -D bin/puppet-manifest-apply.sh $(BINDIR)/puppet-manifest-apply.sh endif - install -m 755 -D bin/apply_network_config.py $(BINDIR)/apply_network_config.py install -m 755 -D bin/apply_network_config.sh $(BINDIR)/apply_network_config.sh install -m 755 -D bin/k8s_wait_for_endpoints_health.py $(BINDIR)/k8s_wait_for_endpoints_health.py install -m 755 -D bin/kube-wait-control-plane-terminated.sh $(BINDIR)/kube-wait-control-plane-terminated.sh diff --git a/puppet-manifests/src/bin/apply_network_config.py b/puppet-manifests/src/bin/apply_network_config.py deleted file mode 100644 index 9ef66ac3c..000000000 --- a/puppet-manifests/src/bin/apply_network_config.py +++ /dev/null @@ -1,1046 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (c) 2025 Wind River Systems, Inc. -# -# SPDX-License-Identifier: Apache-2.0 -# - -import argparse -from datetime import datetime -import errno -import fcntl -import logging as LOG -from netaddr import AddrFormatError -from netaddr import IPAddress -import os -import re -import signal -import shlex -import subprocess -import sys -import time - -LOG_FILE = "/var/log/user.log" -PUPPET_DIR = "/var/run/network-scripts.puppet" -PUPPET_FILE = "/var/run/network-scripts.puppet/interfaces" -PUPPET_ROUTES_FILE = "/var/run/network-scripts.puppet/routes" -PUPPET_ROUTES6_FILE = "/var/run/network-scripts.puppet/routes6" -ETC_ROUTES_FILE = "/etc/network/routes" -ETC_DIR = "/etc/network/interfaces.d" -SYSINV_LOCK_FILE = "/var/run/apply_network_config.lock" -UPGRADE_FILE = "/var/run/.network_upgrade_bootstrap" -SUBCLOUD_ENROLLMENT_FILE = "/var/run/.enroll-init-reconfigure" -CLOUD_INIT_FILE = ETC_DIR + "/50-cloud-init" -IFSTATE_BASE_PATH = "/run/network/ifstate." -DEVLINK_BASE_PATH = "/sys/class/net/" -CFG_PREFIX = "ifcfg-" -TERM_WAIT_TIME = 10 - -# Interface types -ETH = "eth" -VLAN = "vlan" -BONDING = "bonding" -SLAVE = "slave" -LABEL = "label" -LO = "lo" - -# Order for setting interfaces down -DOWN_ORDER = (LABEL, VLAN, BONDING, LO, ETH) -# Order for setting interfaces up -UP_ORDER = (ETH, LO, BONDING, VLAN, LABEL) -# Order for configuring interfaces without down/up operation -ONLINE_ORDER = (ETH, BONDING, VLAN, LABEL) - -# Interface property sort positions -PROPERTY_SORT_POS = { - "iface": 0, - "vlan-raw-device": 1, - "address": 2, - "netmask": 3, - "gateway": 4, - "bond-master": 5, - "bond-miimon": 6, - "bond-mode": 7, - "bond-primary": 8, - "bond-slaves": 9, - "hwaddress": 10, - "mtu": 11, - "pre-up": 12, - "up": 13, - "post-up": 14, - "pre-down": 15, - "down": 16, - "post-down": 17, - "allow-": 19, - # Position DEFAULT_POS holds properties that are not in the list, allow- is put last to not - # break ifupdown parsing, see https://review.opendev.org/c/starlingx/stx-puppet/+/839620 -} - -# Default sort position for properties -DEFAULT_POS = 18 - - -class InvalidNetmaskError(BaseException): - pass - - -class StanzaParser(): - - @staticmethod - def ParseLines(lines): - parser = StanzaParser() - parser.parse_lines(lines) - return parser.get_auto_and_ifaces() - - def __init__(self): - self.auto = [] - self.auto_set = set() - self.ifaces = dict() - self.iface = None - self.state = "none" - - def _proc_state_auto(self, verbs): - for iface in verbs[1:]: - if iface not in self.auto_set: - self.auto.append(iface) - self.auto_set.add(iface) - self.state = "none" - - def _proc_state_start_iface(self, verbs): - self.iface = self.ifaces.setdefault(verbs[1], {verbs[0]: " ".join(verbs[1:])}) - self.state = "continue-iface" - - def _proc_state_continue_iface(self, verbs): - # Special case for allow- property - if "allow-" in verbs[0]: - self.iface["allow-"] = " ".join(verbs) - else: - self.iface[verbs[0]] = " ".join(verbs[1:]) if len(verbs) > 1 else None - - STATES = { - "none": lambda self, line: None, - "start-auto": _proc_state_auto, - "start-iface": _proc_state_start_iface, - "continue-iface": _proc_state_continue_iface, - "standby-iface": lambda self, line: None, - } - - NEXT_STATES = { - "none": {"new-auto": "start-auto", - "new-iface": "start-iface"}, - "continue-iface": {"new-auto": "start-auto", - "new-iface": "start-iface", - "empty": "standby-iface", - "reset": "none"}, - "standby-iface": {"new-auto": "start-auto", - "new-iface": "start-iface", - "continue": "continue-iface", - "reset": "none"} - } - - def _proc_state(self, verbs): - func = self.STATES[self.state] - func(self, verbs) - - def _proc_event(self, event): - self.state = self.NEXT_STATES[self.state].get(event, self.state) - - @staticmethod - def _get_event(verbs): - if len(verbs) == 0 or verbs[0].startswith("#"): - return "empty" - if verbs[0] == "auto": - return "new-auto" - if verbs[0] == "iface": - if len(verbs) > 1: - return "new-iface" - return "reset" - return "continue" - - def _parse_line(self, line): - verbs = line.split() - event = self._get_event(verbs) - self._proc_event(event) - self._proc_state(verbs) - - def parse_lines(self, lines): - for line in lines: - self._parse_line(line.strip()) - self.state = "none" - - def get_auto_and_ifaces(self): - return self.auto, self.ifaces - - -def read_file_lines(path): - with open(path, "r") as f: - lines = f.readlines() - return [line.strip() for line in lines] - - -def read_file_text(path): - with open(path, "r") as f: - return f.read() - - -def is_label(iface): - return ":" in iface - - -def get_base_iface(iface): - return iface.split(":")[0] - - -def execute_system_cmd(cmd, timeout=30): - # When transitioning management network to a VLAN, ifup (for the mgmt interface) does its job - # in configuring the link but blocks sub.communicate() for a long period of time, long enough - # to cause the puppet task to end by timeout. - # If the subprocess is ended via sub.terminate(), sub.communicate() still blocks for an - # indefinite period of time. The only way that was found for the function to work as intended - # was to add start_new_session=True to subprocess.Popen() and to terminate the process group via - # os.killpg(). - - sub = subprocess.Popen(shlex.split(cmd), - start_new_session=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - try: - stdout, _ = sub.communicate(timeout=timeout) - decoded_stdout = stdout.decode('utf-8') - except subprocess.TimeoutExpired: - pgid = os.getpgid(sub.pid) - LOG.warning(f"Execution time exceeded for command '{cmd}', " - f"sending SIGTERM to subprocess (pid={sub.pid}, pgid={pgid})") - os.killpg(pgid, signal.SIGTERM) - try: - stdout, _ = sub.communicate(timeout=TERM_WAIT_TIME) - except subprocess.TimeoutExpired: - LOG.warning(f"Command '{cmd}' has not terminated after {TERM_WAIT_TIME} seconds, " - f"sending SIGKILL to subprocess (pid={sub.pid}, pgid={pgid})") - os.killpg(pgid, signal.SIGKILL) - stdout, _ = sub.communicate() - decoded_stdout = stdout.decode('utf-8') - if sub.returncode == 0: - LOG.info(f"Command '{cmd}' output:{format_stdout(decoded_stdout)}") - return sub.returncode, decoded_stdout - - -def apply_config(routes_only): - if routes_only: - LOG.info("Process Debian route config") - update_routes() - else: - if not os.path.isdir(PUPPET_DIR): - LOG.error("No puppet files? Nothing to do! Aborting...") - sys.exit(1) - LOG.info("Process Debian network config") - log_network_info() - updated_ifaces = update_interfaces() - update_routes(updated_ifaces) - check_enrollment_config() - log_network_info() - LOG.info("Finished") - - -def log_network_info(): - _, links = execute_system_cmd("/usr/sbin/ip addr show") - _, routes_ipv4 = execute_system_cmd("/usr/sbin/ip route show") - _, routes_ipv6 = execute_system_cmd("/usr/sbin/ip -6 route show") - LOG.info("Network info:\n************ Links/addresses ************\n" - f"{links}" - "************ IPv4 routes ****************\n" - f"{routes_ipv4}" - "************ IPv6 routes ****************\n" - f"{routes_ipv6}" - "*****************************************") - - -def get_new_config(): - '''Gets new network config from puppet directory''' - auto, ifaces = parse_interface_stanzas() - return build_config(auto, ifaces, is_from_puppet=True) - - -def parse_interface_stanzas(): - lines = read_file_lines(PUPPET_FILE) - return StanzaParser.ParseLines(lines) - - -def get_current_config(): - '''Gets current network config in etc directory''' - auto = parse_auto_file() - ifaces = parse_ifcfg_files(auto) - return build_config(auto, ifaces, is_from_puppet=False) - - -def parse_auto_list(input_auto, ifaces, is_from_puppet): - valid_auto = [] - invalid_auto = [] - for iface in input_auto: - if iface in ifaces: - valid_auto.append(iface) - else: - invalid_auto.append(iface) - if invalid_auto: - origin = "PUPPET" if is_from_puppet else "ETC DIR" - LOG.error(f"Auto list from {origin} has interfaces that have no or invalid " - f"config: {', '.join(invalid_auto)}") - return valid_auto - - -def build_config(auto, ifaces, is_from_puppet): - valid_auto = parse_auto_list(auto, ifaces, is_from_puppet) - ifaces_types, dependencies = get_types_and_dependencies(ifaces) - return {"auto": set(valid_auto), - "ifaces": ifaces, - "ifaces_types": ifaces_types, - "dependencies": dependencies} - - -def parse_auto_file(): - path = get_auto_path() - if not os.path.isfile(path): - LOG.info(f"Auto file not found: '{path}'") - return [] - lines = read_file_lines(path) - auto, _ = StanzaParser.ParseLines(lines) - return auto - - -def get_auto_path(): - return os.path.join(ETC_DIR, "auto") - - -def get_ifcfg_path(iface): - return os.path.join(ETC_DIR, CFG_PREFIX + iface) - - -def parse_ifcfg_files(ifaces): - iface_configs = dict() - for iface in ifaces: - iface_configs[iface] = parse_ifcfg_file(iface) - return iface_configs - - -def parse_ifcfg_file(iface): - path = get_ifcfg_path(iface) - if not os.path.isfile(path): - LOG.warning(f"Interface config file not found: '{path}'") - return dict() - lines = read_file_lines(path) - _, ifaces = StanzaParser.ParseLines(lines) - if len(ifaces) == 0: - LOG.warning(f"No interface config found in '{path}'") - return dict() - if (ifconfig := ifaces.get(iface, None)) is None: - LOG.warning(f"Config for interface '{iface}' not found in '{path}'. Instead, file has " - f"config(s) for the following interface(s): {' '.join(sorted(ifaces.keys()))}") - return dict() - if len(ifaces) > 1: - LOG.warning(f"Multiple interface configs found in '{path}': " - f"{' '.join(sorted(ifaces.keys()))}") - return ifconfig - - -def get_types_and_dependencies(iface_configs): - ifaces_types = dict() - dependencies = dict() - - def set_type(iface, iftype): - ifaces_types[iface] = iftype - - def add_dependent(iface, dependent): - entry = dependencies.setdefault(iface, set()) - entry.add(dependent) - - for iface, config in iface_configs.items(): - if is_label(iface): - set_type(iface, LABEL) - parent = get_base_iface(iface) - add_dependent(parent, iface) - elif iface == "lo": - set_type(iface, LO) - elif vlan_attribs := get_vlan_attributes(iface, config): - set_type(iface, VLAN) - add_dependent(vlan_attribs[0], iface) - elif slaves := config.get("bond-slaves", None): - set_type(iface, BONDING) - for slave in slaves.split(): - add_dependent(slave, iface) - elif master := config.get("bond-master", None): - set_type(iface, SLAVE) - add_dependent(iface, master) - else: - set_type(iface, ETH) - - return ifaces_types, dependencies - - -def get_vlan_attributes(iface, config): - '''Returns (vlan-raw-device, vlan-id) if iface is VLAN, else None''' - if result := re.search(R"^vlan([0-9]+)$", iface): - if raw_dev := config.get("vlan-raw-device", None): - return raw_dev, int(result.group(1)) - LOG.warning("vlan-raw-device property is empty or not specified for " - f"interface {iface}, so it will not be considered as a valid VLAN") - return None - if result := re.search(R"^(.*)\.([0-9]+)$", iface): - return result.group(1), int(result.group(2)) - if preup := config.get("pre-up", None): - if result := re.search(R"ip\s+link\s+add\s+link\s+(\S+)\s+name\s+\S+\s+type" - R"\s+vlan\s+id\s+(\d+)", preup): - return result.group(1), int(result.group(2)) - return None - - -def compare_configs(new_config, current_config): - added = new_config["auto"].difference(current_config["auto"]) - if added: - LOG.info(f"Added interfaces: {' '.join(sorted(added))}") - removed = current_config["auto"].difference(new_config["auto"]) - if removed: - LOG.info(f"Removed interfaces: {' '.join(sorted(removed))}") - modified = get_modified_ifaces(new_config, current_config) - if modified: - LOG.info(f"Modified interfaces: {' '.join(sorted(modified))}") - return {"added": added, "removed": removed, "modified": modified} - - -def get_modified_ifaces(new_config, current_config): - modified = set() - new_ifaces = new_config["ifaces"] - current_ifaces = current_config["ifaces"] - for iface, new_if_config in new_ifaces.items(): - current_if_config = current_ifaces.get(iface, None) - if not current_if_config: - continue - if is_iface_modified(iface, new_if_config, current_if_config): - modified.add(iface) - return modified - - -def is_iface_modified(iface, new, current): - filtered_new = {p for p in new.keys() if p in PROPERTY_SORT_POS} - filtered_current = {p for p in current.keys() if p in PROPERTY_SORT_POS} - removed_props = filtered_current.difference(filtered_new) - added_props = filtered_new.difference(filtered_current) - modified_props = [p for p in filtered_new.intersection(filtered_current) - if new[p] != current[p]] - if not removed_props and not added_props and not modified_props: - return False - text = f"Differences found for interface {iface}:" - if removed_props: - text += "\n Removed properties:" - for prop in sort_properties(list(removed_props)): - text += f"\n {prop} {current[prop]}" - if added_props: - text += "\n Added properties:" - for prop in sort_properties(list(added_props)): - text += f"\n {prop} {new[prop]}" - if modified_props: - text += "\n Modified properties:" - for prop in sort_properties(list(modified_props)): - text += f"\n '{prop}' went from '{current[prop]}' to '{new[prop]}'" - LOG.info(text) - return True - - -def get_dependent_list(config, ifaces): - auto = config["auto"] - dep_map = config["dependencies"] - covered = set() - - def add_dependent(iface): - if iface in covered or iface not in auto: - return - covered.add(iface) - dependents = dep_map.get(iface, None) - if not dependents: - return - for dependent in dependents: - add_dependent(dependent) - - for iface in ifaces: - add_dependent(iface) - - return covered - - -def get_down_list(current_config, comparison): - base_set = comparison["modified"].union(comparison["removed"]) - dependents = get_dependent_list(current_config, base_set) - return base_set.union(dependents) - - -def get_up_list(new_config, comparison): - base_set = comparison["modified"].union(comparison["added"]) - missing_set = get_missing_list(new_config, base_set) - up_set = base_set.union(missing_set) - dependents = get_dependent_list(new_config, up_set) - return up_set.union(dependents) - - -def get_missing_list(config, base_set): - ifaces_types = config["ifaces_types"] - types = {ETH, BONDING, VLAN} - ifaces = {i for i in config["auto"].difference(base_set) if ifaces_types[i] in types} - out_set = set() - for iface in ifaces: - if is_iface_missing_or_down(iface): - LOG.info(f"Interface {iface} is missing or down, adding to up list") - out_set.add(iface) - return out_set - - -def get_updated_ifaces(new_config, up_list): - ifaces_types = new_config["ifaces_types"] - types = {ETH, VLAN, BONDING, LO} - updated = set() - for iface in up_list: - if ifaces_types[iface] == LABEL: - updated.add(get_base_iface(iface)) - elif ifaces_types[iface] in types: - updated.add(iface) - return updated - - -def sort_ifaces_by_type(config, ifaces, type_order): - ifaces_types = config["ifaces_types"] - ifaces_by_type = dict() - for iface in ifaces: - iftype = ifaces_types[iface] - iface_list = ifaces_by_type.setdefault(iftype, []) - iface_list.append(iface) - sorted_ifaces = [] - for iftype in type_order: - if iface_list := ifaces_by_type.get(iftype, None): - iface_list.sort() - sorted_ifaces.extend(iface_list) - return sorted_ifaces - - -def set_ifaces_down(config, ifaces): - sorted_ifaces = sort_ifaces_by_type(config, ifaces, DOWN_ORDER) - for iface in sorted_ifaces: - set_iface_down(iface) - - -def format_stdout(stdout): - cln_stdout = stdout.strip() - return f"\n{cln_stdout}" if "\n" in cln_stdout else f" '{cln_stdout}'" - - -def set_iface_down(iface): - LOG.info(f"Bringing {iface} down") - - ifstate_path = IFSTATE_BASE_PATH + iface - if os.path.isfile(ifstate_path) and read_file_text(ifstate_path).strip() == iface: - retcode, stdout = execute_system_cmd(f"/sbin/ifdown -v {iface}") - if retcode != 0: - LOG.error(f"Command 'ifdown' failed for interface {iface}:{format_stdout(stdout)}") - - if not is_label(iface): - devlink_path = DEVLINK_BASE_PATH + iface - if os.path.islink(devlink_path): - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip link set down dev {iface}") - if retcode != 0: - LOG.error(f"Command 'ip link set down' failed for " - f"interface {iface}:{format_stdout(stdout)}") - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip addr flush dev {iface}") - if retcode != 0: - LOG.error(f"Command 'ip addr flush' failed for interface {iface}:" - f"{format_stdout(stdout)}") - - -def set_ifaces_up(config, ifaces): - sorted_ifaces = sort_ifaces_by_type(config, ifaces, UP_ORDER) - for iface in sorted_ifaces: - set_iface_up(iface) - - -def set_iface_up(iface): - LOG.info(f"Bringing {iface} up") - retcode, stdout = execute_system_cmd(f"/sbin/ifup -v {iface}") - if retcode != 0: - LOG.error(f"Command 'ifup' failed for interface {iface}: {format_stdout(stdout)}") - return retcode - - -def update_files(new_config): - for iface, iface_config in new_config["ifaces"].items(): - write_iface_config_file(iface, iface_config) - write_auto_file(new_config["auto"]) - - -def remove_iface_config_files(comparison): - for to_remove in comparison["removed"]: - remove_iface_config_file(to_remove) - - -def path_exists(path): - return os.path.exists(path) - - -def remove_iface_config_file(iface): - path = get_ifcfg_path(iface) - if path_exists(path): - LOG.info(f"Removing {path}") - try: - os.remove(path) - except OSError as e: - LOG.error(f"Failed to remove {path}: {e}") - else: - LOG.info(f"File {path} does not exist, no need to remove") - - -def write_iface_config_file(iface, iface_config): - lines = get_ifcfg_lines(iface_config) - path = get_ifcfg_path(iface) - with open(path, "w") as f: - f.write("\n".join(lines) + "\n") - - -def write_auto_file(auto): - contents = get_header() + "\nauto " + " ".join(auto) + "\n" - path = get_auto_path() - with open(path, "w") as f: - f.write(contents) - - -def sort_properties(props): - # Key is position number (from PROPERTY_SORT_POS) with 2 digits followed by property name - def get_sort_key(v): - return f"{PROPERTY_SORT_POS.get(v, DEFAULT_POS):02d}{v}" - props.sort(key=get_sort_key) - return props - - -def get_ifcfg_lines(iface_config): - props = list(iface_config.keys()) - sort_properties(props) - lines = [get_header()] - for prop in props: - lines.append(iface_config[prop] if prop == "allow-" else prop + " " + iface_config[prop]) - return lines - - -def get_header(): - dt = datetime.now().astimezone() - return dt.strftime("# HEADER: Last generated at: %Y-%m-%d %H:%M:%S %z") - - -def get_route_entries(files): - entries = [] - for file in files: - if os.path.isfile(file): - lines = read_file_lines(file) - entries.extend(get_route_entries_from_lines(lines, file)) - return entries - - -def get_route_entries_from_lines(lines, file): - routes = [] - for line in lines: - clean_line = line.strip() - if len(clean_line) > 0 and not clean_line.startswith("#"): - verbs = clean_line.split() - if len(verbs) >= 4: - routes.append(' '.join(verbs)) - else: - LOG.warning(f"Invalid route in file '{file}', must have at least 4 " - f"parameters, {len(verbs)} found: '{clean_line}'") - return routes - - -def get_route_iface(route): - return route.split()[3] - - -def create_route_obj_from_entry(route_entry): - verbs = route_entry.split() - route_obj = {"network": verbs[0], - "netmask": verbs[1], - "nexthop": verbs[2], - "ifname": verbs[3]} - if len(verbs) > 4: - route_obj["metric"] = verbs[5] - return route_obj - - -def get_prefix_length(netmask): - try: - addr = IPAddress(netmask) - if addr.is_netmask(): - return addr.netmask_bits() - except AddrFormatError: - pass - raise InvalidNetmaskError(f"Failed to get prefix length, invalid netmask: '{netmask}'") - - -def get_linux_network(route): - network = route["network"] - if network == "default": - return "default" - prefixlen = get_prefix_length(route["netmask"]) - return f"{network}/{prefixlen}" - - -def remove_route_entry_from_kernel(route_entry): - route = create_route_obj_from_entry(route_entry) - try: - remove_route_from_kernel(route) - except InvalidNetmaskError as e: - LOG.error(f"Failed to remove route entry '{route_entry}' from the kernel: {e}") - - -def remove_route_from_kernel(route): - description = get_route_description(route) - LOG.info(f"Removing route: {description}") - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip route del {description}") - if retcode != 0: - LOG.error(f"Failed removing route {description}:{format_stdout(stdout)}") - - -def add_route_entry_to_kernel(route_entry): - route = create_route_obj_from_entry(route_entry) - try: - add_route_to_kernel(route) - except InvalidNetmaskError as e: - LOG.error(f"Failed to add route entry '{route_entry}' to the kernel: {e}") - - -def get_route_description(route, full=True): - linux_network = get_linux_network(route) - gateway = f" via {route['nexthop']} dev {route['ifname']}" if full else "" - descr = f"{linux_network}{gateway}" - if metric := route.get("metric", None): - descr += f" metric {metric}" - return descr - - -def add_route_to_kernel(route): - prot = "-6 " if ":" in route["nexthop"] else "" - description = get_route_description(route) - LOG.info(f"Adding route: {description}") - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip {prot}route show {description}") - if retcode == 0 and route["network"] in stdout: - LOG.info("Route already exists, skipping") - else: - short_descr = get_route_description(route, full=False) - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip {prot}route show {short_descr}") - if retcode == 0 and route["network"] in stdout: - LOG.info(f"Route to specified network already exists, replacing: {stdout.strip()}") - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip route replace {description}") - if retcode != 0: - LOG.error(f"Failed replacing route {description}:{format_stdout(stdout)}") - else: - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip route add {description}") - if retcode != 0: - LOG.error(f"Failed adding route {description}:{format_stdout(stdout)}") - - -def acquire_sysinv_agent_lock(): - LOG.info("Acquiring lock to synchronize with sysinv-agent audit") - lock_file_fd = os.open(SYSINV_LOCK_FILE, os.O_CREAT | os.O_RDONLY) - return acquire_file_lock(lock_file_fd, fcntl.LOCK_EX | fcntl.LOCK_NB, 5, 5) - - -def release_sysinv_agent_lock(lockfd): - if lockfd: - LOG.info("Releasing lock") - release_file_lock(lockfd) - os.close(lockfd) - - -def acquire_file_lock(lockfd, operation, max_retry, wait_interval): - count = 1 - while count <= max_retry: - try: - fcntl.flock(lockfd, operation) - LOG.info("Successfully acquired lock (fd={})".format(lockfd)) - return lockfd - except IOError as e: - # raise on unrelated IOErrors - if e.errno != errno.EAGAIN: - raise - LOG.info("Could not acquire lock({}): {} ({}/{}), will retry".format( - lockfd, str(e), count, max_retry)) - time.sleep(wait_interval) - count += 1 - LOG.error("Failed to acquire lock (fd={}). Stopped trying.".format(lockfd)) - sys.exit(1) - - -def release_file_lock(lockfd): - if lockfd: - fcntl.flock(lockfd, fcntl.LOCK_UN) - - -def is_upgrade(): - return os.path.isfile(UPGRADE_FILE) - - -def update_interfaces(): - new_config = get_new_config() - - auto = new_config["auto"] - if len(auto) == 0 or (len(auto) == 1 and next(iter(auto)) == "lo"): - LOG.info(f"Generated {PUPPET_FILE} with empty configuration: '{' '.join(auto)}', exiting") - return None - - disable_pxeboot_interface() - - if is_upgrade(): - LOG.info("Upgrade bootstrap is in execution") - return update_ifaces_online(new_config) - - return update_ifaces_ifupdown(new_config) - - -def disable_pxeboot_interface(): - path = get_ifcfg_path("pxeboot") - if not os.path.isfile(path): - return - - lines = read_file_lines(path) - _, ifaces = StanzaParser.ParseLines(lines) - if len(ifaces) == 0: - LOG.info(f"Pxeboot install config file '{path}' has no valid interface config, skipping") - return - - for iface in ifaces.keys(): - LOG.info(f"Turn off pxeboot install config for {iface}, will be turned on later") - set_iface_down(iface) - - LOG.info("Remove ifcfg-pxeboot, left from kickstart install phase") - remove_iface_config_file("pxeboot") - - -def update_ifaces_ifupdown(new_config): - current_config = get_current_config() - comparison = compare_configs(new_config, current_config) - down_list = get_down_list(current_config, comparison) - up_list = get_up_list(new_config, comparison) - - lock = acquire_sysinv_agent_lock() if down_list or up_list else None - try: - set_ifaces_down(current_config, down_list) - remove_iface_config_files(comparison) - update_files(new_config) - set_ifaces_up(new_config, up_list) - finally: - release_sysinv_agent_lock(lock) - - return get_updated_ifaces(new_config, up_list) - - -def update_ifaces_online(config): - sorted_ifaces = sort_ifaces_by_type(config, config["auto"], ONLINE_ORDER) - if not sorted_ifaces: - return set() - update_files(config) - for iface in sorted_ifaces: - LOG.info(f"Configuring interface {iface}") - ensure_iface_configured(iface, config["ifaces"][iface]) - return get_updated_ifaces(config, sorted_ifaces) - - -def is_iface_missing_or_down(iface): - path = f"{DEVLINK_BASE_PATH}{iface}/operstate" - if os.path.isfile(path): - state = read_file_text(path) - if state != "down": - return False - return True - - -def get_iface_address(iface, cfg): - if address := cfg.get("address", None): - if "/" not in address: - if netmask := cfg.get("netmask", None): - if ":" in address: - try: - prefixlen = int(netmask) - except ValueError: - LOG.error(f"Failed to get {iface} interface prefixlen, " - f"invalid value: '{netmask}'") - return None - else: - try: - prefixlen = get_prefix_length(netmask) - except InvalidNetmaskError as e: - LOG.error(f"Failed to get {iface} interface netmask: {e}") - return None - return f"{address}/{prefixlen}" - LOG.error(f"Interface {iface} has address but no netmask") - return None - return address - - -def ensure_iface_configured_label(iface, cfg): - address = get_iface_address(iface, cfg) - if not address: - return - base_iface = get_base_iface(iface) - existing = get_link_addresses(base_iface) - if address in existing: - LOG.info(f"Link already has address '{address}', no need to set label up") - else: - if set_iface_up(iface) == 0: - return - add_ip_to_iface(base_iface, address) - if gateway := cfg.get("gateway", None): - add_default_route(base_iface, gateway) - - -def ensure_iface_configured_non_label(iface, cfg): - if is_iface_missing_or_down(iface): - LOG.info(f"Interface '{iface}' is missing or down, flushing IPs and bringing up") - flush_ips(iface) - if set_iface_up(iface) == 0: - return - address = get_iface_address(iface, cfg) - if not address: - return - existing = get_link_addresses(iface) - if address not in existing: - add_ip_to_iface(iface, address) - if gateway := cfg.get("gateway", None): - add_default_route(iface, gateway) - - -def ensure_iface_configured(iface, cfg): - if is_label(iface): - ensure_iface_configured_label(iface, cfg) - else: - ensure_iface_configured_non_label(iface, cfg) - - -def get_link_addresses(name): - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip -br addr show dev {name}") - if retcode == 0: - verbs = stdout.split() - return verbs[2:] - LOG.error(f"Failed to get IP address list from {name}:{format_stdout(stdout)}") - return None - - -def add_ip_to_iface(iface, ip): - LOG.info(f"Adding IP {ip} to interface {iface}") - existing = get_link_addresses(iface) - if existing is None: - return - if ip in existing: - LOG.info(f"Interface {iface} already has address {ip}, skipping") - return - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip addr add {ip} dev {iface}") - if retcode != 0: - LOG.error(f"Failed to add IP address to interface {iface}:{format_stdout(stdout)}") - - -def add_default_route(iface, gateway): - route = {"network": "default", - "nexthop": gateway, - "ifname": iface} - add_route_to_kernel(route) - - -def flush_ips(iface): - path = DEVLINK_BASE_PATH + iface - if os.path.islink(path): - retcode, stdout = execute_system_cmd(f"/usr/sbin/ip addr flush dev {iface}") - if retcode != 0: - LOG.error(f"Command 'ip addr flush' failed for interface {iface}:" - f"{format_stdout(stdout)}") - - -def write_routes_file(route_entries): - lines = [get_header()] + route_entries - with open(ETC_ROUTES_FILE, "w") as f: - f.write("\n".join(lines) + "\n") - - -def update_routes(updated_ifaces=None): - if updated_ifaces is None: - updated_ifaces = set() - - new_routes = get_route_entries([PUPPET_ROUTES_FILE, PUPPET_ROUTES6_FILE]) - new_routes_set = set(new_routes) - - current_routes = get_route_entries([ETC_ROUTES_FILE]) - current_routes_set = set(current_routes) - - write_routes_file(new_routes) - - if new_routes_set != current_routes_set: - LOG.info(f"Differences found between {PUPPET_ROUTES_FILE} and {ETC_ROUTES_FILE}") - # Remove routes that are currently present and no longer needed, following the order in - # which they appear in the file - for route_entry in current_routes: - if route_entry not in new_routes_set: - remove_route_entry_from_kernel(route_entry) - else: - LOG.info(f"No differences found between {PUPPET_ROUTES_FILE} and {ETC_ROUTES_FILE}") - if not updated_ifaces: - return - - for route_entry in new_routes: - if route_entry not in current_routes_set: - LOG.info(f"Route not previously present in {ETC_ROUTES_FILE}, adding") - elif get_route_iface(route_entry) in updated_ifaces: - LOG.info("Route is associated with and updated interface, adding") - else: - continue - add_route_entry_to_kernel(route_entry) - - -def check_enrollment_config(): - if not os.path.isfile(SUBCLOUD_ENROLLMENT_FILE) or not os.path.isfile(CLOUD_INIT_FILE): - return - LOG.info(f"Enrollment: Parsing file '{CLOUD_INIT_FILE}'") - lines = read_file_lines(CLOUD_INIT_FILE) - _, ifaces = StanzaParser.ParseLines(lines) - ifaces.pop("lo", None) - if len(ifaces) == 0: - LOG.warning(f"Enrollment: Could not find any valid interface config in '{CLOUD_INIT_FILE}'") - return - ifaces_with_gateway = dict() - for iface, cfg in ifaces.items(): - if gateway := cfg.get("gateway", None): - try: - gateway_ip = IPAddress(gateway) - except AddrFormatError: - LOG.warning(f"Enrollment: Invalid gateway address '{gateway}' " - f"for interface '{iface}'") - continue - ifaces_with_gateway.setdefault(gateway_ip.version, dict())[iface] = cfg - if len(ifaces_with_gateway) == 0: - LOG.warning("Enrollment: No interface with gateway address found, skipping") - return - for version, iface_cfgs in ifaces_with_gateway.items(): - if len(iface_cfgs) > 1: - LOG.warning(f"Enrollment: Multiple interfaces with gateway for ipv{version} found: " - f"{', '.join(iface_cfgs.keys())}") - for iface, cfg in iface_cfgs.items(): - LOG.info(f"Enrollment: Configuring interface {iface} with gateway {cfg['gateway']}") - ensure_iface_configured(iface, cfg) - - -def main(): - log_format = ('%(asctime)s: [%(process)s]: %(filename)s(%(lineno)s): ' - '%(levelname)s: %(message)s') - LOG.basicConfig(filename=LOG_FILE, format=log_format, level=LOG.INFO, datefmt="%FT%T") - - parser = argparse.ArgumentParser( - prog='Network Configuration Applier', - description='Applies the network configuration generated by Puppet to the linux kernel' - ) - parser.add_argument("--routes", action='store_true') - args = parser.parse_args() - - apply_config(args.routes) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/puppet-manifests/src/bin/apply_network_config.sh b/puppet-manifests/src/bin/apply_network_config.sh index e478833d3..0feedf633 100755 --- a/puppet-manifests/src/bin/apply_network_config.sh +++ b/puppet-manifests/src/bin/apply_network_config.sh @@ -7,8 +7,6 @@ # ################################################################################ -# WARNING: This file is OBSOLETE, use apply_network_config.py instead - # # Purpose of this script is to copy the puppet-built ifcfg-* network config # files from the PUPPET_DIR to the ETC_DIR. Only files that are detected as diff --git a/puppet-manifests/src/bin/k8s_wait_for_endpoints_health.py b/puppet-manifests/src/bin/k8s_wait_for_endpoints_health.py index 0e235855f..fa391d321 100644 --- a/puppet-manifests/src/bin/k8s_wait_for_endpoints_health.py +++ b/puppet-manifests/src/bin/k8s_wait_for_endpoints_health.py @@ -62,10 +62,10 @@ def k8s_wait_for_endpoints_health(tries=TRIES, try_sleep=TRY_SLEEP, timeout=TIME healthz_endpoints = [APISERVER_READYZ_ENDPOINT, CONTROLLER_MANAGER_HEALTHZ_ENDPOINT, SCHEDULER_HEALTHZ_ENDPOINT, KUBELET_HEALTHZ_ENDPOINT] for endpoint in healthz_endpoints: - is_k8s_endpoint_healthy = kubernetes.k8s_health_check(tries=tries, - try_sleep=try_sleep, - timeout=timeout, - healthz_endpoint=endpoint) + is_k8s_endpoint_healthy = kubernetes.k8s_health_check(tries = tries, + try_sleep = try_sleep, + timeout = timeout, + healthz_endpoint = endpoint) if not is_k8s_endpoint_healthy: LOG.error("Timeout: Kubernetes control-plane endpoints not healthy") return 1 @@ -93,3 +93,4 @@ def main(): if __name__ == "__main__": sys.exit(main()) + diff --git a/puppet-manifests/src/bin/network_ifupdown.sh b/puppet-manifests/src/bin/network_ifupdown.sh index 1a0412e58..88b86f409 100644 --- a/puppet-manifests/src/bin/network_ifupdown.sh +++ b/puppet-manifests/src/bin/network_ifupdown.sh @@ -5,8 +5,6 @@ # ################################################################################ -# WARNING: This file is OBSOLETE - # # This file purpose is to provide helper functions if the system is Debian based # for the apply_network_config.sh script diff --git a/puppet-manifests/src/bin/network_sysconfig.sh b/puppet-manifests/src/bin/network_sysconfig.sh index 003ee70f8..bf6cd890e 100644 --- a/puppet-manifests/src/bin/network_sysconfig.sh +++ b/puppet-manifests/src/bin/network_sysconfig.sh @@ -5,8 +5,6 @@ # ################################################################################ -# WARNING: This file is OBSOLETE - # # This file purpose is to provide helper functions if the system is CentOS based # for the apply_network_config.sh script diff --git a/puppet-manifests/src/bin/puppet-update-grub-env.py b/puppet-manifests/src/bin/puppet-update-grub-env.py index 686859822..18c5d8352 100755 --- a/puppet-manifests/src/bin/puppet-update-grub-env.py +++ b/puppet-manifests/src/bin/puppet-update-grub-env.py @@ -28,7 +28,6 @@ import sys BOOT_ENV = "/boot/efi/EFI/BOOT/boot.env" KERNEL_PARAMS_STRING = "kernel_params" - # Get value of kernel_params from conf def read_kernel_params(conf): """Get value of kernel_params from conf""" @@ -47,7 +46,6 @@ def read_kernel_params(conf): return res - # Write key=value string to conf def write_conf(conf, string): """Write key=value string to conf""" @@ -61,7 +59,6 @@ def write_conf(conf, string): print(err) raise - def set_parser(): """Set command parser""" @@ -113,7 +110,6 @@ def set_parser(): return parser - def convert_dict_to_value(kernel_params_dict): """Dictionary to value""" @@ -132,7 +128,6 @@ def convert_dict_to_value(kernel_params_dict): return f"kernel_params={kernel_params}" - def convert_value_to_dict(value): """Value to dictionary""" @@ -161,6 +156,7 @@ def convert_value_to_dict(value): else: key, val = param, '' + kernel_params_dict[key] = val if hugepage_cache: @@ -181,7 +177,6 @@ def convert_value_to_dict(value): return kernel_params_dict - def edit_boot_env(args): """Edit boot environment""" @@ -217,7 +212,6 @@ def edit_boot_env(args): kernel_params = convert_dict_to_value(kernel_params_dict) write_conf(BOOT_ENV, kernel_params) - def get_kernel_dir(): """Get kernel directory""" @@ -229,12 +223,11 @@ def get_kernel_dir(): return "/boot/1" - def edit_kernel_env(args): """Edit kernel environment""" kernel_dir = get_kernel_dir() - path_all = os.path.join(kernel_dir, "vmlinuz*-amd64") + path_all = os.path.join(kernel_dir,"vmlinuz*-amd64") path_rt = os.path.join(kernel_dir, "vmlinuz*rt*-amd64") glob_all_kernels = [os.path.basename(f) for f in glob.glob(path_all)] @@ -261,7 +254,6 @@ def edit_kernel_env(args): kernel_rollback_env = f"kernel_rollback={kernel}" write_conf(kernel_env, kernel_rollback_env) - def list_kernels(): """List kernels""" @@ -280,7 +272,6 @@ def list_kernels(): print(output) - def list_kernel_params(): """List kernel params""" @@ -297,7 +288,6 @@ def list_kernel_params(): print(line) break - def main(): """Main""" parser = set_parser() @@ -315,6 +305,5 @@ def main(): if args.list_kernel_params: list_kernel_params() - if __name__ == "__main__": main() diff --git a/puppet-manifests/src/modules/platform/files/change_k8s_control_plane_params.py b/puppet-manifests/src/modules/platform/files/change_k8s_control_plane_params.py index 66b595fac..f531088da 100644 --- a/puppet-manifests/src/modules/platform/files/change_k8s_control_plane_params.py +++ b/puppet-manifests/src/modules/platform/files/change_k8s_control_plane_params.py @@ -729,15 +729,14 @@ def get_kubelet_cfg_from_service_parameters(service_params): # map[string]string & []string if value.startswith(('[', '{')) and value.endswith((']', '}')): try: - value = json.loads( - value.replace('True', 'true').replace('False', 'false').replace("'", '"')) + value = json.loads(value.replace('True', 'true').replace('False', 'false').replace("'", '"')) except Exception as e: msg = 'Parsing param: %s / value: %s. [Error: %s]' % (param, value, e) LOG.error(msg) return 3 # bool elif value in ['False', 'false'] or value in ['True', 'true']: - value = True if value in ['True', 'true'] else False # pylint: disable-msg=simplifiable-if-expression # noqa: E501 + value = True if value in ['True', 'true'] else False # pylint: disable-msg=simplifiable-if-expression # float elif '.' in value: try: @@ -1158,8 +1157,7 @@ def main(): parser.add_argument("--kubelet_latest_config_file", default="/var/lib/kubelet/config.yaml") parser.add_argument("--kubelet_bak_config_file", default="/var/lib/kubelet/config.yaml.bak") parser.add_argument("--kubelet_error_log", default="/tmp/kubelet_errors.log") - parser.add_argument("--k8s_configmaps_init_flag", - default="/tmp/.sysinv_k8s_configmaps_initialized") + parser.add_argument("--k8s_configmaps_init_flag", default="/tmp/.sysinv_k8s_configmaps_initialized") parser.add_argument("--automatic_recovery", default=True) parser.add_argument("--timeout", default=RECOVERY_TIMEOUT) diff --git a/puppet-manifests/src/modules/platform/manifests/network.pp b/puppet-manifests/src/modules/platform/manifests/network.pp index c60c32864..57779d85b 100644 --- a/puppet-manifests/src/modules/platform/manifests/network.pp +++ b/puppet-manifests/src/modules/platform/manifests/network.pp @@ -800,7 +800,7 @@ class platform::network::apply { -> Exec['apply-network-config'] exec {'apply-network-config': - command => 'apply_network_config.py', + command => 'apply_network_config.sh', } # Wait for network interface to leave tentative state during ipv6 DAD, if interface is UP @@ -890,7 +890,7 @@ class platform::network::routes::runtime { } exec {'apply-network-config route setup': - command => 'apply_network_config.py --routes', + command => 'apply_network_config.sh --routes', } } diff --git a/puppet-manifests/test-requirements.txt b/puppet-manifests/test-requirements.txt deleted file mode 100644 index 31b716025..000000000 --- a/puppet-manifests/test-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -mock>=2.0.0 -stestr>=1.0.0 -netaddr diff --git a/puppet-manifests/tests/__init__.py b/puppet-manifests/tests/__init__.py deleted file mode 100644 index e4937264c..000000000 --- a/puppet-manifests/tests/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -# Copyright (c) 2025 Wind River Systems, Inc. -# -# SPDX-License-Identifier: Apache-2.0 -# diff --git a/puppet-manifests/tests/filesystem_mock.py b/puppet-manifests/tests/filesystem_mock.py deleted file mode 100644 index 109c4b077..000000000 --- a/puppet-manifests/tests/filesystem_mock.py +++ /dev/null @@ -1,318 +0,0 @@ -# -# Copyright (c) 2025 Wind River Systems, Inc. -# -# SPDX-License-Identifier: Apache-2.0 -# - -import io - -# Keys for filesystem node properties -PARENT = "parent" -TYPE = "type" -FILE = "file" -DIR = "dir" -LINK = "link" -CONTENTS = "contents" -TARGET = "target" -REF = "ref" -LISTENERS = "listeners" - - -class FilesystemMockError(BaseException): - pass - - -class FileMock(): - def __init__(self, fs, entry): - self.fs = fs - self.entry = entry - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - pass - - def readlines(self): - lines = self.entry[CONTENTS].split("\n") - out_lines = [line + "\n" for line in lines[:-1]] - if len(lines[-1]) > 0: - out_lines.append(lines[-1]) - return out_lines - - def read(self): - return self.entry[CONTENTS] - - def write(self, contents): - if REF not in self.entry: - raise io.UnsupportedOperation("not writable") - self.entry[CONTENTS] += contents - - -class ReadOnlyFileContainer(): - def __init__(self, contents=None): - self.next_id = 0 - self.root = self._get_new_dir(None) - if contents: - self.batch_add(contents) - - def batch_add(self, contents): - for path, data in contents.items(): - if data is None: - self._add_dir(path) - elif type(data) == str: - self._add_file(path, data) - elif type(data) == tuple and len(data) == 1 and type(data[0]) == str: - self._add_link(path, data[0]) - else: - raise FilesystemMockError("Invalid entry, must be None for directory, " - "str for file or tuple with 1 str element for link") - - def get_root_node(self): - return self.root - - @staticmethod - def _get_new_dir(parent): - return {PARENT: parent, TYPE: DIR, CONTENTS: dict()} - - @staticmethod - def _get_new_file(parent, contents): - return {PARENT: parent, TYPE: FILE, CONTENTS: contents} - - @staticmethod - def _get_new_link(parent, entry, target_path): - return {PARENT: parent, TYPE: LINK, CONTENTS: entry, TARGET: target_path} - - def _do_add_dir(self, path_pieces): - def add_dir_rec(parent, pieces): - if len(pieces) == 0: - return parent - current = parent[CONTENTS].get(pieces[0], None) - if not current: - current = self._get_new_dir(parent) - parent[CONTENTS][pieces[0]] = current - return add_dir_rec(current, pieces[1:]) - return add_dir_rec(self.root, path_pieces) - - def _get_entry(self, path): - pieces = path.split("/")[1:] - - def get_entry_rec(parent, pieces): - if len(pieces) == 0: - return parent - current = parent[CONTENTS].get(pieces[0], None) - if not current: - raise FilesystemMockError(f"Path not found: '{path}'") - return get_entry_rec(current, pieces[1:]) - - return get_entry_rec(self.root, pieces) - - def _add_dir(self, path): - pieces = path.split("/")[1:] - self._do_add_dir(pieces) - - def _add_file(self, path, contents): - pieces = path.split("/")[1:] - new_dir = self._do_add_dir(pieces[:-1]) - file_entry = self._get_new_file(new_dir, contents) - new_dir[CONTENTS][pieces[-1]] = file_entry - - def _add_link(self, path, ref_path): - pieces = path.split("/")[1:] - new_dir = self._do_add_dir(pieces[:-1]) - ref_entry = self._get_entry(ref_path) - link_entry = self._get_new_link(new_dir, ref_entry, ref_path) - new_dir[CONTENTS][pieces[-1]] = link_entry - - -class FilesystemMock(): - def __init__(self, contents: dict = None, fs: ReadOnlyFileContainer = None): - if fs is not None: - self.fs = fs - add_contents = True - else: - self.fs = ReadOnlyFileContainer(contents) - add_contents = False - - self.root = self._get_new_entry(self.fs.get_root_node(), None) - if add_contents and contents: - self.batch_add(contents) - - def batch_add(self, contents): - for path, data in contents.items(): - if data is None: - self.create_directory(path) - elif type(data) == str: - self.set_file_contents(path, data) - elif type(data) == tuple and len(data) == 1 and type(data[0]) == str: - self.set_link_contents(path, data[0]) - else: - raise FilesystemMockError("Invalid entry, must be None for directory, " - "str for file or tuple with 1 str element for link") - - @staticmethod - def _get_new_entry(ref, parent, node_type=None): - if not node_type: - node_type = ref[TYPE] - entry = {REF: ref, PARENT: parent, TYPE: node_type} - if node_type == DIR: - entry[CONTENTS] = ref[CONTENTS].copy() if ref else dict() - elif node_type == LINK: - entry[CONTENTS] = ref[CONTENTS] if ref else None - entry[TARGET] = ref[TARGET] if ref else None - else: - entry[CONTENTS] = '' - return entry - - def _get_entry(self, path, translate_link=False): - pieces = path.split("/")[1:] - - def get_entry_rec(contents, pieces): - if len(pieces) == 0: - if translate_link and contents[TYPE] == LINK: - return contents[CONTENTS] - return contents - if contents[TYPE] == LINK: - contents = contents[CONTENTS] - if REF in contents and contents[CONTENTS] is None: - child = contents[REF][CONTENTS].get(pieces[0], None) - else: - child = contents[CONTENTS].get(pieces[0], None) - if child is None: - return None - return get_entry_rec(child, pieces[1:]) - - return get_entry_rec(self.root, pieces) - - def _patch_entry(self, path, node_type): - pieces = path.split("/")[1:] - - def translate_link(entry): - target = entry[CONTENTS] - if REF not in target: - target = self._patch_entry(entry[TARGET], target[TYPE]) - entry[CONTENTS] = target - return target - - def patch_entry_rec(level, entry, pieces): - if len(pieces) == 0: - if entry[TYPE] == LINK and node_type != LINK: - entry = translate_link(entry) - if entry[TYPE] != node_type: - if node_type == FILE: - raise IsADirectoryError(f"[Errno 21] Is a directory: '{path}'") - raise NotADirectoryError(f"[Errno 20] Not a directory: '{path}'") - return entry - if entry[TYPE] == LINK: - entry = translate_link(entry) - if entry[TYPE] != DIR: - raise NotADirectoryError(f"[Errno 20] Not a directory: '{path}'") - if entry[CONTENTS] is None: - entry[CONTENTS] = entry[REF][CONTENTS].copy() - child = entry[CONTENTS].get(pieces[0], None) - if child is None or REF not in child: - if child is None: - new_type = node_type if len(pieces) == 1 else DIR - child = self._get_new_entry(None, entry, new_type) - else: - child = self._get_new_entry(child, entry) - entry[CONTENTS][pieces[0]] = child - return patch_entry_rec(level + 1, child, pieces[1:]) - - return patch_entry_rec(0, self.root, pieces) - - def exists(self, path): - entry = self._get_entry(path) - return entry is not None - - def isfile(self, path): - entry = self._get_entry(path) - return entry and entry[TYPE] == FILE - - def isdir(self, path): - entry = self._get_entry(path) - return entry and entry[TYPE] == DIR - - def islink(self, path): - entry = self._get_entry(path) - return entry and entry[TYPE] == LINK - - def open(self, path, mode="r"): - if "w" in mode: - entry = self._patch_entry(path, FILE) - else: - entry = self._get_entry(path, translate_link=True) - if not entry: - raise FileNotFoundError(f"[Errno 2] No such file or directory: '{path}'") - if entry[TYPE] == DIR: - raise IsADirectoryError(f"[Errno 21] Is a directory: '{path}'") - if "w" in mode: - self._call_listeners(entry) - return FileMock(self, entry) - - def _call_listeners(self, entry): - if parent := entry[PARENT]: - self._call_listeners(parent) - if listeners := entry.get(LISTENERS, None): - for listener in listeners: - listener() - - def create_directory(self, path): - entry = self._patch_entry(path, DIR) - self._call_listeners(entry) - - def set_file_contents(self, path, contents): - entry = self._patch_entry(path, FILE) - entry[CONTENTS] = contents - self._call_listeners(entry) - - def get_file_contents(self, path): - entry = self._get_entry(path, translate_link=True) - if entry is None: - raise FilesystemMockError("Path does not exist") - if entry[TYPE] != FILE: - raise FilesystemMockError("Path is not a file") - return entry[CONTENTS] - - def set_link_contents(self, link_path, target_path): - target = self._get_entry(target_path) - if target is None: - raise FilesystemMockError("Target path does not exist") - entry = self._patch_entry(link_path, LINK) - entry[CONTENTS] = target - entry[TARGET] = target_path - self._call_listeners(entry) - - def get_file_list(self, path): - entry = self._get_entry(path, translate_link=True) - if entry is None: - raise FilesystemMockError("Path does not exist") - if entry[TYPE] != DIR: - raise FilesystemMockError("Path is not a directory") - files = [] - for name, child in entry[CONTENTS].items(): - if child[TYPE] == FILE: - files.append(name) - files.sort() - return files - - def add_listener(self, path, callback): - entry = self._get_entry(path, translate_link=True) - if entry is None: - raise FilesystemMockError("Path does not exist") - if REF not in entry: - entry = self._patch_entry(path, entry[TYPE]) - listeners = entry.setdefault(LISTENERS, list()) - listeners.append(callback) - - def delete(self, path): - pieces = path.split("/")[1:] - - entry = self._get_entry(path) - if entry is None: - raise FileNotFoundError(f"[Errno 2] No such file or directory: '{path}'") - - pieces = path.split("/") - patched_entry = self._patch_entry("/".join(pieces[:-1]), DIR) - patched_entry[CONTENTS].pop(pieces[-1]) - self._call_listeners(patched_entry) diff --git a/puppet-manifests/tests/system_cmd_test_script.sh b/puppet-manifests/tests/system_cmd_test_script.sh deleted file mode 100755 index 2932829af..000000000 --- a/puppet-manifests/tests/system_cmd_test_script.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -################################################################################ -# Copyright (c) 2025 Wind River Systems, Inc. -# -# SPDX-License-Identifier: Apache-2.0 -# -################################################################################ - -# -# This script is used by the automated tests -# tests.test_apply_network_config.GeneralTests.test_execute_system_cmd_timeout_*, it simulates a -# command that takes too long to terminate and triggers a timeout. In certain situations, the ifup -# command can exhibit this behavior. -# - -return_code=$1 -extra_sleep=$2 - -terminate() -{ - echo "< SIGTERM RECEIVED >" - - if [[ "$extra_sleep" == "-e" ]]; then - sleep 10 - echo "< AFTER EXTRA SLEEP >" - fi - - exit $return_code -} - -trap terminate 15 - -echo "< BEFORE SLEEP >" -sleep 10 -echo "< AFTER SLEEP >" diff --git a/puppet-manifests/tests/test_apply_network_config.py b/puppet-manifests/tests/test_apply_network_config.py deleted file mode 100644 index 069992545..000000000 --- a/puppet-manifests/tests/test_apply_network_config.py +++ /dev/null @@ -1,3062 +0,0 @@ -# -# Copyright (c) 2025 Wind River Systems, Inc. -# -# SPDX-License-Identifier: Apache-2.0 -# - -import mock -import os -import re -import testtools -from netaddr import IPAddress -from netaddr import IPNetwork -from netaddr import AddrFormatError - -from tests.filesystem_mock import FilesystemMock -from tests.filesystem_mock import ReadOnlyFileContainer -import src.bin.apply_network_config as anc - - -class NetworkingMockError(BaseException): - pass - - -class NetworkingMock(): # pylint: disable=too-many-instance-attributes - def __init__(self, fs: FilesystemMock, ifaces: list): - self._stdout = '' - self._history = [] - self._etc_changed = True - self._fs = fs - self._current_config = None - self._links = dict() - self._routes = dict() - self._next_route_id = 0 - self._allow_multiple_default_gateways = False - self._add_eth_ifaces(ifaces) - self._fs.add_listener(anc.ETC_DIR, self._etc_dir_changed) - - def _etc_dir_changed(self): - self._etc_changed = True - - def _add_eth_ifaces(self, ifaces): - for iface in ifaces: - self._add_eth_iface(iface) - - @staticmethod - def _get_device_path(iface, is_virtual=False): - if is_virtual: - return f"/sys/devices/virtual/net/{iface}" - return f"/sys/devices/pci0000:00/net/{iface}" - - def _add_eth_iface(self, iface): - phys_path = self._get_device_path(iface) - self._fs.set_file_contents(phys_path + "/operstate", "down") - self._fs.set_link_contents(anc.DEVLINK_BASE_PATH + iface, phys_path) - self._links[iface] = {"adm_state": False, "virtual": False, - "addresses": set(), "routes": set()} - - def _parse_etc_interfaces(self): - file_list = self._fs.get_file_list(anc.ETC_DIR) - parser = anc.StanzaParser() - for file in file_list: - file_contents = self._fs.get_file_contents(anc.ETC_DIR + "/" + file) - parser.parse_lines(file_contents.split("\n")) - return parser.get_auto_and_ifaces() - - @staticmethod - def _decode_iface_config(name, config): - if anc.is_label(name): - parent = name.split(":")[0] - props = {"type": anc.LABEL, "parent": parent} - elif name == "lo": - props = {"type": anc.LO} - elif vlan_attribs := anc.get_vlan_attributes(name, config): - preup = config.get("pre-up", None) - add_link_cmd = preup and "ip link add" in preup - props = {"type": anc.VLAN, "raw_dev": vlan_attribs[0], "vlan_id": vlan_attribs[1], - "add_link_cmd": add_link_cmd} - elif slaves := config.get("bond-slaves", None): - props = {"type": anc.BONDING, "slaves": slaves.split()} - elif master := config.get("bond-master", None): - props = {"type": anc.SLAVE, "master": master} - else: - props = {"type": anc.ETH} - mode = config["iface"].split()[2] - props["mode"] = mode - if mode == "static": - if not (address := config.get("address", None)): - raise NetworkingMockError( - f"Interface '{name}' is set to STATIC but has no address specified") - if "/" in address: - props["address"] = IPNetwork(address) - else: - if not (netmask := config.get("netmask", None)): - raise NetworkingMockError( - f"Interface '{name}' is set to STATIC but has no netmask specified") - props["address"] = IPNetwork(f"{address}/{netmask}") - if gateway := config.get("gateway", None): - props["gateway"] = IPAddress(gateway) - return props - - def _decode_config(self): - auto, etc_ifaces = self._parse_etc_interfaces() - decoded_ifaces = dict() - for iface, config in etc_ifaces.items(): - decoded_ifaces[iface] = self._decode_iface_config(iface, config) - return auto, decoded_ifaces - - @staticmethod - def _get_ifaces_by_type(ifaces): - type_ifaces = dict() - for iface, config in ifaces.items(): - iflist = type_ifaces.setdefault(config["type"], list()) - iflist.append(iface) - for iftype in type_ifaces.keys(): - type_ifaces[iftype].sort() - return type_ifaces - - def _update_config(self): - if not self._etc_changed: - return - self._etc_changed = False - auto, ifaces = self._decode_config() - by_type = self._get_ifaces_by_type(ifaces) - self._current_config = {"auto": auto, "ifaces": ifaces, "by_type": by_type} - - @staticmethod - def _sort_auto_by_type(config): - auto = set(config["auto"]) - by_type = config["by_type"] - sorted_ifaces = list() - for iftype in [anc.LO, anc.ETH, anc.BONDING, anc.VLAN, anc.LABEL]: - if ifaces := by_type.get(iftype, None): - for iface in ifaces: - if iface in auto: - sorted_ifaces.append(iface) - return sorted_ifaces - - def _add_route_line(self, line): - pieces = line.split() - if len(pieces) < 4: - raise NetworkingMockError(f"Invalid route in '{anc.ETC_ROUTES_FILE}' file: '{line}'") - netmask_ip = IPAddress(pieces[1]) - prefixlen = netmask_ip.netmask_bits() - network = f"{pieces[0]}/{prefixlen}" - metric = pieces[5] if len(pieces) > 4 else None - self._do_ip_route_add(network, pieces[2], pieces[3], metric) - - def _apply_etc_routes(self): - if not self._fs.isfile(anc.ETC_ROUTES_FILE): - return - file_contents = self._fs.get_file_contents(anc.ETC_ROUTES_FILE) - lines = [line.strip() for line in file_contents.split("\n")] - for line in lines: - clean_line = line.strip() - if clean_line and not clean_line.startswith("#"): - self._add_route_line(line) - - def apply_auto(self): - self._reset_stdout() - self._etc_changed = True - self._update_config() - sorted_auto = self._sort_auto_by_type(self._current_config) - for iface in sorted_auto: - self._do_ifup(iface) - self._apply_etc_routes() - return self._stdout - - def reset_history(self): - self._history = [] - - def get_history(self): - return self._history - - def _add_history(self, command, *args): - self._history.append((command, *args)) - - def _reset_stdout(self): - self._stdout = '' - - def _print_stdout(self, msg): - self._stdout += msg + "\n" - - def _is_up(self, iface): - state_file_path = anc.IFSTATE_BASE_PATH + iface - if self._fs.isfile(state_file_path): - data = self._fs.get_file_contents(state_file_path) - return data == iface - return False - - def _set_link_state(self, iface, link, state): - if link["adm_state"] == state: - return - link["adm_state"] = state - operstate_path = self._get_device_path(iface, link["virtual"]) + "/operstate" - value = "up" if state else "down" - self._fs.set_file_contents(operstate_path, value) - - def _create_virtual_link(self, name): - if link := self._links.get(name, None): - self._print_stdout("RTNETLINK answers: File exists") - return link, 1 - phys_path = self._get_device_path(name, True) - self._fs.set_file_contents(phys_path + "/operstate", "down") - self._fs.set_link_contents(anc.DEVLINK_BASE_PATH + name, phys_path) - link = {"adm_state": False, "virtual": True, "addresses": set(), "routes": set()} - self._links[name] = link - return link, 0 - - def _remove_virtual_link(self, name): - link, retcode = self._get_link(name) - if retcode != 0: - return 1 - for route_id in link["routes"]: - self._routes.pop(route_id) - if deps := link.get("deps", None): - for dep in deps: - self._remove_virtual_link(dep) - del self._links[name] - self._fs.delete(anc.DEVLINK_BASE_PATH + name) - self._fs.delete(self._get_device_path(name, True)) - return 0 - - def _get_link(self, name): - link = self._links.get(name, None) - if link: - return link, 0 - self._print_stdout(f'Cannot find device "{name}"') - return None, 1 - - def _get_link_for_ip_cmd(self, name): - link = self._links.get(name, None) - if link: - return link, 0 - self._print_stdout(f'Device "{name}" does not exist.') - return None, 1 - - def _enslave_iface(self, iface, master, master_failed): - if master_failed or not (link := self._links.get(iface, None)): - self._print_stdout(f"Failed to enslave {iface} to {master}. " - f"Is {master} ready and a bonding interface ?") - return None, 1 - link["master"] = master - self._set_link_state(iface, link, True) - return link, 0 - - def _unenslave_iface(self, iface): - if not (link := self._links.get(iface, None)): - return 1 - link.pop("master", None) - self._set_link_state(iface, link, False) - return 0 - - def _add_address(self, iface, config, link): - mode = config["mode"] - if mode == "static": - address = config["address"] - if address in link["addresses"]: - self._print_stdout(f"Error: ipv{address.version}: Address already assigned.") - return 1 - link["addresses"].add(address) - if gateway := config.get("gateway", None): - self._add_default_gateway(iface, link, gateway) - return 0 - - def _remove_routes_associated_to_address(self, link, address): - to_remove = [] - for route_id in link["routes"]: - route = self._routes[route_id] - if route["via"] in address: - to_remove.append(route_id) - for route_id in to_remove: - self._routes.pop(route_id) - link["routes"].remove(route_id) - - def _remove_address(self, config, link): - mode = config["mode"] - if mode == "static": - address = config["address"] - if address not in link["addresses"]: - self._print_stdout(f"Error: ipv{address.version}: Address not found.") - return 1 - link["addresses"].remove(address) - self._remove_routes_associated_to_address(link, address) - return 0 - - def _add_default_gateway(self, ifname, link, gateway): - net = '0.0.0.0/0' if gateway.version == 4 else '::0/0' - route_filter = self._get_route_filter(net, None, None, None) - existing = self._find_routes(route_filter, True) - if existing: - if not self._allow_multiple_default_gateways: - raise NetworkingMockError("Trying to create default route from ifup for " - f"interface '{ifname}', default route already exists") - route_obj = self._get_route_obj(net, gateway, ifname, None) - retcode = self._check_can_add_route(route_obj, link) - if retcode != 0: - return retcode - for route_id in existing.keys(): - self._remove_route(route_id) - self._add_route(route_obj, link) - return 0 - - def _set_lo_up(self, iface, config): - return self._set_eth_up(iface, config) - - def _set_lo_down(self, iface, config): - return self._set_eth_down(iface, config) - - def _set_eth_up(self, iface, config): - link, retcode = self._get_link(iface) - if retcode != 0: - return 1 - self._set_link_state(iface, link, True) - return self._add_address(iface, config, link) - - def _set_eth_down(self, iface, config): - link, retcode = self._get_link(iface) - if retcode != 0: - return 0 - self._set_link_state(iface, link, False) - self._remove_address(config, link) - return 0 - - def _set_slave_up(self, iface, config): # pylint: disable=no-self-use,unused-argument - raise NetworkingMockError( - f"ifup is not supposed to be called for slave interfaces: {iface}") - - def _set_slave_down(self, iface, config): # pylint: disable=no-self-use,unused-argument - raise NetworkingMockError( - f"ifdown is not supposed to be called for slave interfaces: {iface}") - - def _set_bonding_up(self, iface, config): - link, retcode = self._create_virtual_link(iface) - if retcode != 0: - self._print_stdout("/etc/network/if-pre-up.d/ifenslave: line 39: /sys/class/net/" - f"{iface}/bonding/miimon: No such file or directory") - self._print_stdout("/etc/network/if-pre-up.d/ifenslave: line 39: /sys/class/net/" - f"{iface}/bonding/mode: No such file or directory") - link["slaves"] = config["slaves"] - for slave in config["slaves"]: - self._enslave_iface(slave, iface, retcode != 0) - self._set_link_state(iface, link, True) - return self._add_address(iface, config, link) - - def _set_bonding_down(self, iface, config): - link, retcode = self._get_link(iface) - if retcode != 0: - return 0 - self._remove_address(config, link) - self._set_link_state(iface, link, False) - for slave in config["slaves"]: - self._unenslave_iface(slave) - self._remove_virtual_link(iface) - return 0 - - def _set_vlan_up(self, iface, config): - raw_dev = config["raw_dev"] - if config["add_link_cmd"]: - link, retcode = self._get_link(raw_dev) - if retcode != 0: - return retcode - else: - if raw_dev not in self._links: - self._print_stdout(f'cat: /sys/class/net/{raw_dev}/mtu: No such file or directory') - self._print_stdout(f'Device "{raw_dev}" does not exist.') - self._print_stdout(f'{raw_dev} does not exist, unable to create {iface}') - self._print_stdout('run-parts: /etc/network/if-pre-up.d/vlan exited with ' - 'return code 1') - return 1 - link, retcode = self._create_virtual_link(iface) - if retcode != 0: - return 1 - link["raw_dev"] = raw_dev - link["vlan_id"] = config["vlan_id"] - deps = self._links[raw_dev].setdefault("deps", list()) - deps.append(iface) - self._set_link_state(iface, link, True) - return self._add_address(iface, config, link) - - def _set_vlan_down(self, iface, config): - link, retcode = self._get_link(iface) - if retcode != 0: - return 0 - self._remove_address(config, link) - self._set_link_state(iface, link, False) - self._remove_virtual_link(iface) - return 0 - - def _set_label_up(self, iface, config): # pylint: disable=unused-argument - parent = config["parent"] - link, retcode = self._get_link(parent) - if retcode != 0: - return retcode - return self._add_address(parent, config, link) - - def _set_label_down(self, iface, config): # pylint: disable=unused-argument - parent = config["parent"] - link, retcode = self._get_link(parent) - if retcode == 0: - self._remove_address(config, link) - return 0 - - def _set_ifstate(self, iface, state): - path = anc.IFSTATE_BASE_PATH + iface - contents = iface if state else '' - self._fs.set_file_contents(path, contents) - - _UP_FUNCTIONS = {anc.LO: _set_lo_up, - anc.ETH: _set_eth_up, - anc.SLAVE: _set_slave_up, - anc.BONDING: _set_bonding_up, - anc.VLAN: _set_vlan_up, - anc.LABEL: _set_label_up} - - _DOWN_FUNCTIONS = {anc.LO: _set_lo_down, - anc.ETH: _set_eth_down, - anc.SLAVE: _set_slave_down, - anc.BONDING: _set_bonding_down, - anc.VLAN: _set_vlan_down, - anc.LABEL: _set_label_down} - - def _get_iface_config(self, iface): - self._update_config() - if not (config := self._current_config["ifaces"].get(iface, None)): - self._print_stdout(f"ifup: unknown interface {iface}") - return None, 1 - return config, 0 - - def _run_command(self, fxn, *args, **kwargs): - self._reset_stdout() - retcode = fxn(*args, **kwargs) - return retcode, self._stdout - - def _do_ifup(self, iface): - config, retcode = self._get_iface_config(iface) - if retcode != 0: - return 1 - if self._is_up(iface): - self._print_stdout(f"ifup: interface {iface} already configured") - return 0 - fxn = self._UP_FUNCTIONS[config["type"]] - retcode = fxn(self, iface, config) - if retcode == 0: - self._set_ifstate(iface, True) - else: - self._print_stdout(f"ifup: failed to bring up {iface}") - return retcode - - def ifup(self, iface): - self._add_history("ifup", iface) - return self._run_command(self._do_ifup, iface) - - def _do_ifdown(self, iface): - config, retcode = self._get_iface_config(iface) - if retcode != 0: - return 1 - if not self._is_up(iface): - self._print_stdout(f"ifdown: interface {iface} not configured") - return 0 - fxn = self._DOWN_FUNCTIONS[config["type"]] - retcode = fxn(self, iface, config) - if retcode == 0: - self._set_ifstate(iface, False) - return retcode - - def ifdown(self, iface): - self._add_history("ifdown", iface) - return self._run_command(self._do_ifdown, iface) - - def ip_addr_show(self): - self._add_history("ip_addr_show") - return 0, "< 'ip addr show' output placeholder >\n" - - def _do_ip_addr_show_dev(self, iface): - link, retcode = self._get_link_for_ip_cmd(iface) - if retcode != 0: - return retcode - name = iface - if raw_dev := link.get("raw_dev", None): - name += "@" + raw_dev - state = "UP" if link["adm_state"] else "DOWN" - addresses = [str(addr) for addr in sorted(list(link["addresses"]))] - text = f"{name:<16} {state:<14} {' '.join(addresses)}" - self._print_stdout(text) - return 0 - - def ip_addr_show_dev(self, iface): - self._add_history("ip_addr_show_dev", iface) - return self._run_command(self._do_ip_addr_show_dev, iface) - - def _do_ip_addr_add(self, addr, iface): - link, retcode = self._get_link_for_ip_cmd(iface) - if retcode != 0: - return retcode - try: - ip = IPNetwork(addr) - except AddrFormatError: - self._print_stdout(f'Error: any valid prefix is expected rather than "{addr}".') - return 1 - if ip in link["addresses"]: - self._print_stdout(f"Error: ipv{ip.version}: Address already assigned.") - return 1 - link["addresses"].add(ip) - return 0 - - def ip_addr_add(self, addr, iface): - self._add_history("ip_addr_add", addr, iface) - return self._run_command(self._do_ip_addr_add, addr, iface) - - def _do_ip_addr_flush(self, iface): - link, retcode = self._get_link_for_ip_cmd(iface) - if retcode != 0: - return retcode - for address in link["addresses"]: - self._remove_routes_associated_to_address(link, address) - link["addresses"].clear() - return 0 - - def ip_addr_flush(self, iface): - self._add_history("ip_addr_flush", iface) - return self._run_command(self._do_ip_addr_flush, iface) - - def _do_ip_link_set_updown(self, iface, state): - link, retcode = self._get_link_for_ip_cmd(iface) - if retcode != 0: - return retcode - self._set_link_state(iface, link, state) - return 0 - - def ip_link_set_down(self, iface): - self._add_history("ip_link_set_down", iface) - return self._run_command(self._do_ip_link_set_updown, iface, False) - - def ip_link_set_up(self, iface): - self._add_history("ip_link_set_up", iface) - return self._run_command(self._do_ip_link_set_updown, iface, True) - - def ip_route_show_all(self, prot): - self._add_history("ip_route_show_all", prot) - return 0, "< 'ip route show all' output placeholder >\n" - - @staticmethod - def _sort_routes(routes): - return [routes[k] for k in sorted(routes.keys())] - - def _print_route(self, route, route_filter=None): - net = route["net"] - if net.value == 0 and net.prefixlen == 0: - pieces = ["default"] - else: - pieces = [f"{net.ip}/{net.prefixlen}"] - if not route_filter or not route_filter["via"]: - pieces.append(f'via {route["via"]}') - if not route_filter or not route_filter["dev"]: - pieces.append(f'dev {route["dev"]}') - if not route_filter or not route_filter["metric"]: - if (metric := route["metric"]) != 0 or net.version != 4: - pieces.append(f'metric {metric}') - if net.version == 6: - pieces.append("pref medium") - self._print_stdout(" ".join(pieces)) - - def _do_ip_route_show(self, prot, network, gateway, dev, metric): - # pylint: disable=too-many-arguments - ip_version = 6 if prot == "-6" else 4 - filter_filter = self._get_route_filter(network, gateway, dev, metric, ip_version) - routes = self._find_routes(filter_filter) - for route in self._sort_routes(routes): - self._print_route(route, filter_filter) - return 0 - - def ip_route_show(self, prot, network, gateway, dev, metric): - # pylint: disable=too-many-arguments - self._add_history("ip_route_show", prot, network, gateway, dev, metric) - return self._run_command(self._do_ip_route_show, prot, network, gateway, dev, metric) - - @staticmethod - def _get_route_obj(network, gateway, dev, metric): - gateway_ip = IPAddress(gateway) - if network == "default": - net = IPNetwork('0.0.0.0/0') if gateway_ip.version == 4 else IPNetwork('::0/0') - else: - net = IPNetwork(network) - if metric: - metric_val = int(metric) - if gateway_ip.version == 6 and metric_val == 0: - metric_val = 1024 - else: - metric_val = 0 if gateway_ip.version == 4 else 1024 - return {"net": net, "via": gateway_ip, "dev": dev, "metric": metric_val} - - def _add_route(self, route_obj, link): - route_id = self._next_route_id - self._next_route_id += 1 - self._routes[route_id] = route_obj - link["routes"].add(route_id) - - def _remove_route(self, route_id): - route_obj = self._routes.pop(route_id) - self._links[route_obj["dev"]]["routes"].remove(route_id) - - @staticmethod - def _get_route_filter(network, gateway, dev, metric, version=None): - gateway_ip = IPAddress(gateway) if gateway else None - if network == "default": - if (version and version == 6) or (gateway_ip and gateway_ip.version == 6): - net = IPNetwork('::0/0') - else: - net = IPNetwork('0.0.0.0/0') - else: - net = IPNetwork(network) if network else None - metric_val = int(metric) if metric else None - return {"net": net, "via": gateway_ip, "dev": dev, - "metric": metric_val, "version": version} - - @staticmethod - def _route_matches(route_filter, route): - filter_net = route_filter["net"] - route_net = route["net"] - version = route_filter["version"] - if version and route_net.version != version: - return False - if route_net != filter_net: - return False - for prop in ["via", "dev", "metric"]: - if not (val := route_filter[prop]): - continue - if route[prop] != val: - return False - return True - - def _find_routes(self, route_filter, single=False): - routes = dict() - for route_id, route in self._routes.items(): - if self._route_matches(route_filter, route): - routes[route_id] = route - if single: - break - return routes - - def _route_exists(self, network, gateway, dev, metric): - route_filter = self._get_route_filter(network, gateway, dev, metric) - return bool(self._find_routes(route_filter, True)) - - def _erase_routes_by_filter(self, route_filter): - to_remove = [] - for route_id, route in self._routes.items(): - if self._route_matches(route_filter, route): - to_remove.append(route_id) - for route_id in to_remove: - self._remove_route(route_id) - - def _check_can_add_route(self, route_obj, link): - gateway = route_obj["via"] - for addr in link["addresses"]: - if gateway in addr: - return 0 - self._print_stdout("RTNETLINK answers: No route to host") - return 2 - - def _do_ip_route_add(self, network, gateway, dev, metric): - link, retcode = self._get_link(dev) - if retcode != 0: - return retcode - if self._route_exists(network, gateway, dev, metric): - self._print_stdout("RTNETLINK answers: File exists") - return 2 - route_obj = self._get_route_obj(network, gateway, dev, metric) - retcode = self._check_can_add_route(route_obj, link) - if retcode != 0: - return retcode - self._add_route(route_obj, link) - return 0 - - def ip_route_add(self, network, gateway, dev, metric): - self._add_history("ip_route_add", network, gateway, dev, metric) - return self._run_command(self._do_ip_route_add, network, gateway, dev, metric) - - def _do_ip_route_replace(self, network, gateway, dev, metric): - link, retcode = self._get_link(dev) - if retcode != 0: - return retcode - ip_version = IPAddress(gateway).version - route_obj = self._get_route_obj(network, gateway, dev, metric) - retcode = self._check_can_add_route(route_obj, link) - if retcode != 0: - return retcode - route_filter = self._get_route_filter(network, None, None, metric, ip_version) - self._erase_routes_by_filter(route_filter) - self._add_route(route_obj, link) - return 0 - - def ip_route_replace(self, network, gateway, dev, metric): - self._add_history("ip_route_replace", network, gateway, dev, metric) - return self._run_command(self._do_ip_route_replace, network, gateway, dev, metric) - - def _do_ip_route_del(self, network, gateway, dev, metric): - route_filter = self._get_route_filter(network, gateway, dev, metric) - routes = self._find_routes(route_filter) - if len(routes) == 0: - self._print_stdout("RTNETLINK answers: No such process") - return 2 - for route_id in routes.keys(): - self._remove_route(route_id) - return 0 - - def ip_route_del(self, network, gateway, dev, metric): - self._add_history("ip_route_del", network, gateway, dev, metric) - return self._run_command(self._do_ip_route_del, network, gateway, dev, metric) - - @staticmethod - def _get_link_text(link): - pieces = ["UP" if link["adm_state"] else "DOWN"] - if raw_dev := link.get("raw_dev", None): - pieces.append(f"VLAN({raw_dev},{link['vlan_id']})") - elif master := link.get("master", None): - pieces.append(f"SLAVE({master})") - elif slaves := link.get("slaves", None): - pieces.append(f"BONDING({','.join(slaves)})") - pieces.extend([str(ip) for ip in sorted(link["addresses"])]) - return " ".join(pieces) - - def get_link_status(self, name): - if not (link := self._links.get(name, None)): - raise NetworkingMockError(f"Link does not exist: '{name}'") - return self._get_link_text(link) - - def get_links_status(self): - return [name + " " + self._get_link_text(self._links[name]) - for name in sorted(self._links.keys())] - - @staticmethod - def _get_route_text(route): - net = route["net"] - net_text = "default" if net.value == 0 and net.prefixlen == 0 else str(net) - text = f"{net_text} via {route['via']} dev {route['dev']}" - if metric := route["metric"]: - text += f" metric {metric}" - return text - - def get_routes(self): - return [self._get_route_text(self._routes[id]) for id in sorted(self._routes.keys())] - - def set_allow_multiple_default_gateways(self, allow: bool): - self._allow_multiple_default_gateways = allow - - -class SystemCommandMockError(BaseException): - pass - - -class SystemCommandMock(): # pylint: disable=too-few-public-methods - def __init__(self, nwmock: NetworkingMock): - self._nwmock = nwmock - - def _ip_addr_show(self, _): - return self._nwmock.ip_addr_show() - - def _ip_br_addr_show_dev(self, args): - return self._nwmock.ip_addr_show_dev(args[0]) - - def _ip_addr_add(self, args): - return self._nwmock.ip_addr_add(args[0], args[1]) - - def _ip_addr_flush(self, args): - return self._nwmock.ip_addr_flush(args[0]) - - def _ip_link_set_down(self, args): - return self._nwmock.ip_link_set_down(args[0]) - - def _ip_route_show_all(self, args): - return self._nwmock.ip_route_show_all(args[0]) - - def _ip_route_show(self, args): - return self._nwmock.ip_route_show(args[0], args[1], args[2], args[3], args[4]) - - def _ip_route_add(self, args): - return self._nwmock.ip_route_add(args[0], args[1], args[2], args[3]) - - def _ip_route_replace(self, args): - return self._nwmock.ip_route_replace(args[0], args[1], args[2], args[3]) - - def _ip_route_del(self, args): - return self._nwmock.ip_route_del(args[0], args[1], args[2], args[3]) - - def _ifup(self, args): - return self._nwmock.ifup(args[0]) - - def _ifdown(self, args): - return self._nwmock.ifdown(args[0]) - - _MAPPINGS = ( - (re.compile(R"^/sbin/ifup (?:-v )?(\S+)$"), _ifup), - (re.compile(R"^/sbin/ifdown (?:-v )?(\S+)$"), _ifdown), - (re.compile(R"^/usr/sbin/ip addr show$"), _ip_addr_show), - (re.compile(R"^/usr/sbin/ip -br addr show dev (\S+)$"), _ip_br_addr_show_dev), - (re.compile(R"^/usr/sbin/ip addr add (\S+) dev (\S+)$"), _ip_addr_add), - (re.compile(R"^/usr/sbin/ip addr flush dev (\S+)$"), _ip_addr_flush), - (re.compile(R"^/usr/sbin/ip link set down dev (\S+)$"), _ip_link_set_down), - (re.compile(R"^/usr/sbin/ip (?:(-6) )?route show$"), _ip_route_show_all), - (re.compile(R"^/usr/sbin/ip (?:(-6) )?route show (\S+)(?: via (\S+) " - R"dev (\S+))?(?: metric (\S+))?$"), _ip_route_show), - (re.compile(R"^/usr/sbin/ip route add (\S+) via (\S+) " - R"dev (\S+)(?: metric (\S+))?$"), _ip_route_add), - (re.compile(R"^/usr/sbin/ip route replace (\S+) via (\S+) " - R"dev (\S+)(?: metric (\S+))?$"), _ip_route_replace), - (re.compile(R"^/usr/sbin/ip route del (\S+) via (\S+) " - R"dev (\S+)(?: metric (\S+))?$"), _ip_route_del),) - - def execute_system_cmd(self, cmd): - for mapping in self._MAPPINGS: - if result := mapping[0].search(cmd): - return mapping[1](self, result.groups()) - raise SystemCommandMockError(f"Unrecognized command: '{cmd}'") - - -class LoggerMock(): - DEBUG = "debug" - INFO = "info" - WARNING = "warning" - ERROR = "error" - FATAL = "fatal" - - def __init__(self): - self._entries = list() - - def _log(self, log_type, msg): - self._entries.append((log_type, msg)) - - def get_history(self): - return self._entries - - def reset_history(self): - self._entries.clear() - - def basicConfig(self): - pass - - def debug(self, msg): - self._log(self.DEBUG, msg) - - def info(self, msg): - self._log(self.INFO, msg) - - def warning(self, msg): - self._log(self.WARNING, msg) - - def error(self, msg): - self._log(self.ERROR, msg) - - def fatal(self, msg): - self._log(self.FATAL, msg) - - -class ConfigFileGenerator(): - _SHORT_HEADER = ["# HEADER: Last generated at: 2025-01-01 00:00:00 +0000"] - - _LONG_HEADER = ["# HEADER: This file is being managed by puppet. Changes to", - "# HEADER: interfaces that are not being managed by puppet will persist;", - "# HEADER: however changes to interfaces that are being managed by puppet will", - "# HEADER: be overwritten. In addition, file order is NOT guaranteed.", - "# HEADER: Last generated at: 2025-01-01 00:00:00 +0000", "", ""] - - _AUTOCFG = ("echo 0 > /proc/sys/net/ipv6/conf/{ifname}/autoconf; " - "echo 0 > /proc/sys/net/ipv6/conf/{ifname}/accept_ra; " - "echo 0 > /proc/sys/net/ipv6/conf/{ifname}/accept_redirects") - - _TEMPLATE = { - "iface": "iface {ifname} {inet} {mode}", - "vlan-raw-device": "vlan-raw-device {raw_dev}", - "address": "address {address}", - "netmask": "netmask {netmask}", - "gateway": "{indent}gateway {gateway}", - "bond-master": "{indent}bond-master {master}", - "bond-miimon": "{indent}bond-miimon 100", - "bond-mode": "{indent}bond-mode active-backup", - "bond-primary": "{indent}bond-primary {primary}", - "bond-slaves": "{indent}bond-slaves {slaves}", - "hwaddress": "{indent}hwaddress {hwaddress}", - "mtu": "{indent}mtu {mtu}", - "pre-up-slave": "{indent}pre-up /usr/sbin/ip link set dev {device} promisc on; " + _AUTOCFG, - "pre-up-vlan-ifupdown": "{indent}pre-up /sbin/modprobe -q 8021q", - "pre-up-vlan-manual": "{indent}pre-up /sbin/modprobe -q 8021q; " - "ip link add link {raw_dev} name {device} type vlan id {vlan_id}", - "up": "{indent}up sleep 10", - "post-up": "{indent}post-up " + _AUTOCFG, - "post-up-lo": "{indent}post-up /usr/local/bin/tc_setup.sh " - "lo mgmt 10000 > /dev/null; " + _AUTOCFG, - "post-up-vlan": "{indent}post-up /usr/sbin/ip link set dev {device} mtu {mtu}; " + _AUTOCFG, - "post-down": "{indent}post-down ip link del {device}", - "scope": "{indent}scope host", - "stx-description": "{indent}stx-description ifname:{device},net:None", - "allow-": "{indent}allow-{master} {device}", - } - - _PROPERTY_MAP = { - "lo": ["mtu", "post-up-lo", "scope", "stx-description"], - "eth": ["mtu", "post-up", "stx-description"], - "slave": ["bond-master", "mtu", "pre-up-slave", "stx-description", "allow-"], - "bonding": ["bond-miimon", "bond-mode", "bond-primary", "bond-slaves", "hwaddress", - "mtu", "post-up", "stx-description", "up"], - "vlan-NNN": ["vlan-raw-device", "mtu", "post-up-vlan", "pre-up-vlan-ifupdown", - "stx-description"], - "vlan-dot": ["mtu", "post-up-vlan", "pre-up-vlan-ifupdown", "stx-description"], - "vlan-manual": ["mtu", "post-down", "post-up-vlan", "pre-up-vlan-manual", - "stx-description"], - } - - def __init__(self): - self._hwaddr_seq = 1 - - @staticmethod - def _get_basic_props(config): - props = ["iface"] - if config.get("address", None): - props.append("address") - props.append("netmask") - if config.get("gateway", None): - props.append("gateway") - return props - - def _get_props(self, config): - return self._get_basic_props(config) + self._PROPERTY_MAP[config["type"]] - - def _gen_cfg_lines(self, config): - props = self._get_props(config) - lines = list() - for prop in props: - lines.append(self._TEMPLATE[prop].format(**config)) - return lines - - def _get_new_hw_address(self): - hwaddr = f"08:00:27:f2:66:{self._hwaddr_seq:02d}" - self._hwaddr_seq += 1 - return hwaddr - - def _parse_cfg(self, ifname, input_config, indent=False): - config = input_config.copy() - if ifname == "lo": - iftype = "lo" - elif res := re.search(R"^(\S+)\.(\d+)(?:\:\S+)?$", ifname): - iftype = "vlan-dot" - config["raw_dev"] = res.groups()[0] - config["vlan_id"] = res.groups()[1] - elif res := re.search(R"^vlan(\d+)(?:\:\S+)?$", ifname): - iftype = "vlan-NNN" - config["vlan_id"] = res.groups()[0] - elif "vlan_id" in input_config: - iftype = "vlan-manual" - elif "master" in input_config: - iftype = "slave" - elif slaves := input_config.get("slaves", None): - iftype = "bonding" - config["slaves"] = " ".join(slaves) - config["primary"] = slaves[0] - if "hwaddress" not in input_config: - config["hwaddress"] = self._get_new_hw_address() - else: - iftype = "eth" - config["type"] = iftype - config["ifname"] = ifname - config["device"] = ifname.split(":")[0] if ":" in ifname else ifname - config["mtu"] = input_config.get("mtu", 1500) - config["indent"] = " " if indent else "" - - if address := input_config.get("address", None): - net = IPNetwork(address) - config["address"] = str(net.ip) - config["netmask"] = str(net.netmask) if net.version == 4 else net.prefixlen - config["inet"] = "inet6" if net.version == 6 else "inet" - config["mode"] = "static" - else: - config["inet"] = input_config.get("inet", "inet") - config["mode"] = input_config.get("mode", "manual") - - return config - - @staticmethod - def _gen_auto_lines(auto): - return ["auto " + " ".join(auto)] - - @staticmethod - def _gen_route_line(route): - net = IPNetwork(route["net"]) - line = f"{net.ip} {net.netmask} {route['via']} {route['dev']}" - if metric := route.get("metric", None): - line += f" metric {metric}" - return line - - def _gen_routes_lines(self, routes): - return [self._gen_route_line(route) for route in routes] - - def generate_auto_file(self, auto): - lines = self._gen_auto_lines(auto) - return '\n'.join(lines) + '\n' - - def generate_ifcfg_file(self, ifname, config): - config = self._parse_cfg(ifname, config) - lines = self._SHORT_HEADER + self._gen_cfg_lines(config) - return '\n'.join(lines) + '\n' - - def generate_interfaces_file(self, config): - lines = self._LONG_HEADER.copy() - for ifname, input_cfg in config.items(): - if ifname == "auto": - lines.extend(self._gen_auto_lines(input_cfg)) - else: - output_cfg = self._parse_cfg(ifname, input_cfg, True) - lines.extend(self._gen_cfg_lines(output_cfg)) - lines.append('') - return '\n'.join(lines) + '\n' - - def generate_routes_file(self, routes): - lines = self._LONG_HEADER + self._gen_routes_lines(routes) - return '\n'.join(lines) + '\n' - - def _generate_ifcfg_files(self, tree, contents): - for name, config in contents.items(): - if name == "auto": - tree[anc.ETC_DIR + "/auto"] = self.generate_auto_file(config) - else: - tree[anc.ETC_DIR + "/ifcfg-" + name] = self.generate_ifcfg_file(name, config) - - def generate_file_tree(self, puppet_files=None, etc_files=None): - tree = dict() - - if puppet_files: - if interfaces := puppet_files.get("interfaces", None): - tree[anc.PUPPET_FILE] = self.generate_interfaces_file(interfaces) - if routes := puppet_files.get("routes", None): - tree[anc.PUPPET_ROUTES_FILE] = self.generate_routes_file(routes) - if routes6 := puppet_files.get("routes6", None): - tree[anc.PUPPET_ROUTES6_FILE] = self.generate_routes_file(routes6) - - if etc_files: - if interfaces := etc_files.get("interfaces", None): - self._generate_ifcfg_files(tree, interfaces) - routes = etc_files.get("routes", []) - routes6 = etc_files.get("routes6", []) - if routes or routes6: - tree[anc.ETC_ROUTES_FILE] = self.generate_routes_file(routes + routes6) - - return tree - - -FILE_GEN = ConfigFileGenerator() - - -class BaseTestCase(testtools.TestCase): - def tearDown(self): - self._log = None - self._scmdmock = None - self._nwmock = None - self._fs = None - return super().tearDown() - - def _add_fs_mock(self, contents=None): - self._fs = FilesystemMock(contents) - - def _add_logger_mock(self): - self._log = LoggerMock() - - def _add_nw_mock(self, static_links): - self._nwmock = NetworkingMock(self._fs, static_links) - - def _add_scmd_mock(self): - self._scmdmock = SystemCommandMock(self._nwmock) - - def _mock_fs(self, mocks, fxn, *args, **kwargs): - with ( - mock.patch("src.bin.apply_network_config.path_exists", self._fs.exists), - mock.patch("os.remove", self._fs.delete), - mock.patch("builtins.open", self._fs.open), - mock.patch.multiple("os.path", - isfile=self._fs.isfile, - isdir=self._fs.isdir, - islink=self._fs.islink) - ): - return self._mocked_call(mocks, fxn, *args, **kwargs) - - def _mock_logger(self, mocks, fxn, *args, **kwargs): - with mock.patch.multiple("logging", - basicConfig=self._log.basicConfig, - debug=self._log.debug, - info=self._log.info, - warning=self._log.warning, - error=self._log.error, - fatal=self._log.fatal): - return self._mocked_call(mocks, fxn, *args, **kwargs) - - def _mock_syscmd(self, mocks, fxn, *args, **kwargs): - with mock.patch("src.bin.apply_network_config.execute_system_cmd", - self._scmdmock.execute_system_cmd): - return self._mocked_call(mocks, fxn, *args, **kwargs) - - def _mock_sysinv_lock(self, mocks, fxn, *args, **kwargs): - with mock.patch.multiple("src.bin.apply_network_config", - acquire_sysinv_agent_lock=mock.DEFAULT, - release_sysinv_agent_lock=mock.DEFAULT): - return self._mocked_call(mocks, fxn, *args, **kwargs) - - @staticmethod - def _mocked_call(mocks, fxn, *args, **kwargs): - if len(mocks) == 0: - return fxn(*args, **kwargs) - return mocks[0](mocks[1:], fxn, *args, **kwargs) - - -class GeneralTests(BaseTestCase): # pylint: disable=too-many-public-methods - def test_stanza_parser(self): - parser = anc.StanzaParser() - parser.parse_lines([ - "# HEADER: Last generated at: 2024-11-06 00:54:24 +0000", - "iface enp0s3\tinet manual ", - "# Comment", - " \t # Comment", - "", - "mtu 1500", - "\tpost-up echo 0 > /proc/sys/net/ipv6/conf/enp0s3/autoconf ", - " stx-description ifname:oam0,net:None", - ""]) - parser.parse_lines([ - "# HEADER: Last generated at: 2024-11-06 00:54:24 +0000", - "auto\tlo\tenp0s3 vlan200 ", - "iface vlan200 inet manual", - "vlan-raw-device enp0s3", - " mtu 1500", - " post-up /usr/sbin/ip link set dev vlan200 mtu 1500", - " pre-up /sbin/modprobe -q 8021q", - " stx-description ifname:vlan200,net:None", - "iface ", - " address 10.23.44.11", - " netmask 255.255.255.0", - " mtu 1500", - "iface enp0s8 inet manual", - " mtu 1500", - " post-up echo 0 > /proc/sys/net/ipv6/conf/enp0s8/autoconf", - " stx-description ifname:etc0,net:None", - ""]) - parser.parse_lines([" auto "]) - parser.parse_lines([" auto lo enp0s3 enp0s8"]) - parser.parse_lines(["\tauto \t lo \t enp0s8 enp0s9"]) - - auto, ifaces = parser.get_auto_and_ifaces() - self.assertEqual(["lo", "enp0s3", "vlan200", "enp0s8", "enp0s9"], auto) - self.assertEqual({ - 'enp0s3': { - 'iface': 'enp0s3 inet manual', - 'mtu': '1500', - 'post-up': 'echo 0 > /proc/sys/net/ipv6/conf/enp0s3/autoconf', - 'stx-description': 'ifname:oam0,net:None'}, - 'vlan200': { - 'iface': 'vlan200 inet manual', - 'mtu': '1500', - 'post-up': '/usr/sbin/ip link set dev vlan200 mtu 1500', - 'pre-up': '/sbin/modprobe -q 8021q', - 'stx-description': 'ifname:vlan200,net:None', - 'vlan-raw-device': 'enp0s3'}, - 'enp0s8': { - 'iface': 'enp0s8 inet manual', - 'mtu': '1500', - 'post-up': 'echo 0 > /proc/sys/net/ipv6/conf/enp0s8/autoconf', - 'stx-description': 'ifname:etc0,net:None'}}, - ifaces) - - def test_is_label(self): - self.assertEqual(True, anc.is_label("enp0s8:2-7")) - self.assertEqual(False, anc.is_label("enp0s8")) - - def test_get_base_iface(self): - self.assertEqual("enp0s8", anc.get_base_iface("enp0s8:2-7")) - self.assertEqual("vlan-200", anc.get_base_iface("vlan-200:11")) - - def test_read_file_lines(self): - self._add_fs_mock({"/test-dir/test-file": "0\n1\n2\n"}) - lines = self._mocked_call([self._mock_fs], anc.read_file_lines, "/test-dir/test-file") - self.assertEqual(3, len(lines)) - self.assertEqual("0", lines[0]) - self.assertEqual("1", lines[1]) - self.assertEqual("2", lines[2]) - - _HEADER = "# HEADER: Last generated at: 2025-01-01 00:00:00 +0000" - - _IFACE_CONFIG = {"iface": "enp0s8 inet static", - "mtu": "9000", - "address": "12.12.1.55", - "netmask": "255.255.255.0", - "post-up": "echo # > /proc/sys/net/ipv6/conf/enp0s8/autoconf", - "stx-description": "ifname:etc0,net:None"} - - _IFACE_FILE = (f"{_HEADER}\n" - "iface enp0s8 inet static\n" - "address 12.12.1.55\n" - "netmask 255.255.255.0\n" - "mtu 9000\n" - "post-up echo # > /proc/sys/net/ipv6/conf/enp0s8/autoconf\n" - "stx-description ifname:etc0,net:None\n") - - def test_parse_valid_ifcfg_file(self): - self._add_fs_mock({anc.ETC_DIR + "/ifcfg-enp0s8": self._IFACE_FILE}) - config = self._mocked_call([self._mock_fs], anc.parse_ifcfg_file, "enp0s8") - self.assertEqual(6, len(config)) - self.assertEqual("enp0s8 inet static", config["iface"]) - self.assertEqual("12.12.1.55", config["address"]) - self.assertEqual("255.255.255.0", config["netmask"]) - self.assertEqual("9000", config["mtu"]) - self.assertEqual("echo # > /proc/sys/net/ipv6/conf/enp0s8/autoconf", config["post-up"]) - self.assertEqual("ifname:etc0,net:None", config["stx-description"]) - - def test_parse_missing_ifcfg_file(self): - self._add_fs_mock() - self._add_logger_mock() - config = self._mocked_call([self._mock_fs, self._mock_logger], - anc.parse_ifcfg_file, "enp0s8") - self.assertEqual(0, len(config)) - self.assertEqual(LoggerMock.WARNING, self._log.get_history()[-1][0]) - self.assertEqual(f"Interface config file not found: '{anc.ETC_DIR + '/ifcfg-enp0s8'}'", - self._log.get_history()[-1][1]) - - def test_parse_ifcfg_file_with_multiple_config(self): - path = anc.ETC_DIR + "/ifcfg-enp0s8" - self._add_fs_mock({path: self._IFACE_FILE + - "iface enp0s9 inet static\n" - "mtu 9000\n" - "stx-description ifname:etc1,net:None\n"}) - self._add_logger_mock() - config = self._mocked_call([self._mock_fs, self._mock_logger], - anc.parse_ifcfg_file, "enp0s8") - self.assertEqual(6, len(config)) - self.assertEqual(LoggerMock.WARNING, self._log.get_history()[-1][0]) - self.assertEqual(f"Multiple interface configs found in '{path}': enp0s8 enp0s9", - self._log.get_history()[-1][1]) - - def test_parse_invalid_ifcfg_file(self): - path = anc.ETC_DIR + "/ifcfg-enp0s8" - self._add_fs_mock({path: "invalid content line 1\n" - "invalid content line 2\n" - "invalid content line 3\n"}) - self._add_logger_mock() - config = self._mocked_call([self._mock_fs, self._mock_logger], - anc.parse_ifcfg_file, "enp0s8") - self.assertEqual(0, len(config)) - self.assertEqual(LoggerMock.WARNING, self._log.get_history()[-1][0]) - self.assertEqual(f"No interface config found in '{path}'", self._log.get_history()[-1][1]) - - def test_parse_ifcfg_file_with_unrelated_ifaces(self): - path = anc.ETC_DIR + "/ifcfg-enp0s8" - self._add_fs_mock({path: "iface enp0s9 inet static\n" - "mtu 9000\n" - "stx-description ifname:etc1,net:None\n" - "iface enp0s10 inet static\n" - "mtu 9000\n" - "stx-description ifname:etc2,net:None\n"}) - self._add_logger_mock() - config = self._mocked_call([self._mock_fs, self._mock_logger], - anc.parse_ifcfg_file, "enp0s8") - self.assertEqual(0, len(config)) - self.assertEqual(LoggerMock.WARNING, self._log.get_history()[-1][0]) - self.assertEqual(f"Config for interface 'enp0s8' not found in '{path}'. Instead, " - f"file has config(s) for the following interface(s): enp0s10 enp0s9", - self._log.get_history()[-1][1]) - - def test_parse_auto_file(self): - self._add_fs_mock({anc.ETC_DIR + "/auto": - "auto lo enp0s3\tenp0s3:1-17 enp0s8 vlan100"}) - auto = self._mocked_call([self._mock_fs], anc.parse_auto_file) - self.assertEqual(["lo", "enp0s3", "enp0s3:1-17", "enp0s8", "vlan100"], auto) - - def test_parse_missing_auto_file(self): - self._add_fs_mock() - self._add_logger_mock() - auto = self._mocked_call([self._mock_fs, self._mock_logger], anc.parse_auto_file) - self.assertEqual(0, len(auto)) - self.assertEqual(LoggerMock.INFO, self._log.get_history()[-1][0]) - self.assertEqual(f"Auto file not found: '{anc.ETC_DIR + '/auto'}'", - self._log.get_history()[-1][1]) - - def test_get_vlan_attributes_vlanNNN(self): - dev, vlan_id = anc.get_vlan_attributes("vlan123", {"vlan-raw-device": "enp0s8"}) - self.assertEqual("enp0s8", dev) - self.assertEqual(123, vlan_id) - - def test_get_vlan_attributes_vlanNNN_no_dev(self): - self._add_logger_mock() - attribs = self._mocked_call([self._mock_logger], anc.get_vlan_attributes, - "vlan123", {"iface": "vlan123 inet static"}) - self.assertIsNone(attribs) - self.assertEqual(LoggerMock.WARNING, self._log.get_history()[-1][0]) - self.assertEqual("vlan-raw-device property is empty or not specified for " - "interface vlan123, so it will not be considered as a valid VLAN", - self._log.get_history()[-1][1]) - - def test_get_vlan_attributes_vlan_dot(self): - dev, vlan_id = anc.get_vlan_attributes("enp0s8.123", {"iface": "enp0s8.123 inet static"}) - self.assertEqual("enp0s8", dev) - self.assertEqual(123, vlan_id) - - def test_get_vlan_attributes_vlan_manual(self): - dev, vlan_id = anc.get_vlan_attributes( - "data0", - {"pre-up": "/sbin/modprobe -q 8021q; " - "/usr/sbin/ip link add link\tenp0s8 name data0 type vlan id 123"}) - self.assertEqual("enp0s8", dev) - self.assertEqual(123, vlan_id) - - def test_get_vlan_attributes_not_vlan(self): - attribs = anc.get_vlan_attributes("enp0s8", {"iface": "enp0s8 inet static"}) - self.assertIsNone(attribs) - - def test_get_types_and_dependencies(self): - iface_configs = {"bond0": {"bond-slaves": "enp0s9 enp0s10"}, - "bond0:0-16": {}, - "enp0s10": {"bond-master": "bond0"}, - "enp0s3": {}, - "enp0s3:3-7": {}, - "enp0s4": {}, - "enp0s4:5-17": {}, - "enp0s9": {"bond-master": "bond0"}, - "lo": {}, - "lo:1-2": {}, - "lo:5-14": {}, - "vlan200": {"vlan-raw-device": "bond0"}, - "vlan200:0-17": {}} - - ifaces_types, dependencies = anc.get_types_and_dependencies(iface_configs) - - self.assertEqual({ - "bond0": "bonding", - "bond0:0-16": "label", - "enp0s10": "slave", - "enp0s3": "eth", - "enp0s3:3-7": "label", - "enp0s4": "eth", - "enp0s4:5-17": "label", - "enp0s9": "slave", - "lo": "lo", - "lo:1-2": "label", - "lo:5-14": "label", - "vlan200": "vlan", - "vlan200:0-17": "label" - }, ifaces_types) - - self.assertEqual({ - "bond0": {"vlan200", "bond0:0-16"}, - "enp0s10": {"bond0"}, - "enp0s3": {"enp0s3:3-7"}, - "enp0s4": {"enp0s4:5-17"}, - "enp0s9": {"bond0"}, - "lo": {"lo:1-2", "lo:5-14"}, - "vlan200": {"vlan200:0-17"}}, dependencies) - - def test_is_iface_modified_true(self): - self._add_logger_mock() - - current = {"iface": "enp0s8 inet manual", - "mtu": "1500", - "post-up": "echo 0 > /proc/sys/net/ipv6/conf/enp0s8/autoconf", - "down": "ip addr flush dev enp0s8", - "stx-description": "ifname:etc0,net:None"} - - new = {"iface": "enp0s8 inet static", - "mtu": "9000", - "address": "12.12.1.55", - "netmask": "255.255.255.0", - "post-up": "echo 0 > /proc/sys/net/ipv6/conf/enp0s8/autoconf", - "stx-description": "ifname:data0,net:None"} - - modified = self._mocked_call([self._mock_logger], - anc.is_iface_modified, "enp0s8", new, current) - - self.assertEqual(True, modified) - self.assertEqual(LoggerMock.INFO, self._log.get_history()[-1][0]) - self.assertEqual("Differences found for interface enp0s8:\n" - " Removed properties:\n" - " down ip addr flush dev enp0s8\n" - " Added properties:\n" - " address 12.12.1.55\n" - " netmask 255.255.255.0\n" - " Modified properties:\n" - " 'iface' went from 'enp0s8 inet manual' to 'enp0s8 inet static'\n" - " 'mtu' went from '1500' to '9000'", - self._log.get_history()[-1][1]) - - def test_is_iface_modified_false(self): - current = {"iface": "enp0s8 inet manual", - "mtu": "1500", - "post-up": "echo 0 > /proc/sys/net/ipv6/conf/enp0s8/autoconf", - "down": "ip addr flush dev enp0s8", - "stx-description": "ifname:etc0,net:None", - "random-property": "potato"} - - new = {"iface": "enp0s8 inet manual", - "mtu": "1500", - "post-up": "echo 0 > /proc/sys/net/ipv6/conf/enp0s8/autoconf", - "down": "ip addr flush dev enp0s8", - "stx-description": "ifname:data0,net:None", - "random-property": "banana"} - - modified = anc.is_iface_modified("enp0s8", new, current) - - self.assertEqual(False, modified) - - def test_get_dependent_list(self): - config = {"auto": {"lo", "lo:1-2", "lo:5-14", "enp0s3", "enp0s3:3-7", "enp0s4", - "enp0s4:5-17", "enp0s9", "enp0s10", "bond0", "bond0:0-16", - "vlan200", "vlan200:0-17"}, - "dependencies": {"bond0": {"vlan200", "bond0:0-16"}, - "enp0s10": {"bond0"}, - "enp0s3": {"enp0s3:3-7"}, - "enp0s4": {"enp0s4:5-17"}, - "enp0s9": {"bond0"}, - "lo": {"lo:1-2", "lo:5-14"}, - "vlan200": {"vlan200:0-17"}}} - - dep1 = anc.get_dependent_list(config, {"vlan200"}) - self.assertEqual({"vlan200", "vlan200:0-17"}, dep1) - - dep2 = anc.get_dependent_list(config, {"bond0"}) - self.assertEqual({"bond0", "bond0:0-16", "vlan200", "vlan200:0-17"}, dep2) - - dep3 = anc.get_dependent_list(config, {"enp0s9"}) - self.assertEqual({"enp0s9", "bond0", "bond0:0-16", "vlan200", "vlan200:0-17"}, dep3) - - dep4 = anc.get_dependent_list(config, {"vlan200", "enp0s3"}) - self.assertEqual({"vlan200", "enp0s3", "vlan200:0-17", "enp0s3:3-7"}, dep4) - - dep5 = anc.get_dependent_list(config, {"enp0s4:5-17"}) - self.assertEqual({"enp0s4:5-17"}, dep5) - - def test_is_iface_missing_or_down(self): - dev_path = "/sys/devices/pci0000:00/net/enp0s8" - self._add_fs_mock({dev_path + "/operstate": "up", - anc.DEVLINK_BASE_PATH + "enp0s8": (dev_path, )}) - - def check_result(value): - result = self._mocked_call([self._mock_fs], anc.is_iface_missing_or_down, "enp0s8") - self.assertEqual(value, result) - - check_result(False) - - self._fs.set_file_contents(anc.DEVLINK_BASE_PATH + "enp0s8/operstate", "down") - check_result(True) - - self._fs.delete(anc.DEVLINK_BASE_PATH + "enp0s8") - check_result(True) - - def test_get_updated_ifaces(self): - new_config = {"ifaces_types": {"enp0s3": anc.ETH, - "enp0s8": anc.ETH, - "enp0s9": anc.SLAVE, - "enp0s10": anc.SLAVE, - "bond0": anc.BONDING, - "bond1": anc.BONDING, - "vlan100": anc.VLAN, - "vlan200": anc.VLAN, - "enp0s3:1-1": anc.LABEL, - "enp0s8:2-4": anc.LABEL, - "bond0:5-14": anc.LABEL, - "bond1:6-16": anc.LABEL, - "vlan100:3-9": anc.LABEL, - "vlan200:4-11": anc.LABEL}} - up_list = ["enp0s3", "enp0s9", "enp0s10", "bond0", "vlan100", - "enp0s8:2-4", "bond1:6-16", "vlan200:4-11"] - updated = anc.get_updated_ifaces(new_config, up_list) - self.assertEqual({"enp0s3", "enp0s8", "bond0", "bond1", "vlan100", "vlan200"}, updated) - - def test_sort_ifaces_by_type(self): - config = {"ifaces_types": {"lo": anc.ETH, - "enp0s3": anc.ETH, - "enp0s8": anc.ETH, - "enp0s9": anc.SLAVE, - "enp0s10": anc.SLAVE, - "bond0": anc.BONDING, - "bond1": anc.BONDING, - "vlan100": anc.VLAN, - "vlan200": anc.VLAN, - "enp0s3:1-1": anc.LABEL, - "bond0:5-14": anc.LABEL, - "vlan100:3-9": anc.LABEL}} - ifaces = {"vlan100:3-9", "vlan200", "bond1", "bond0:5-14", "enp0s9", - "enp0s8", "enp0s3", "enp0s3:1-1", "vlan100", "bond0", "enp0s10", "lo"} - sorted_ifaces = anc.sort_ifaces_by_type(config, ifaces, anc.UP_ORDER) - self.assertEqual(["enp0s3", "enp0s8", "lo", "bond0", "bond1", "vlan100", - "vlan200", "bond0:5-14", "enp0s3:1-1", "vlan100:3-9"], sorted_ifaces) - - def _test_set_iface_down(self, delete_ifstate): - etc_files = { - "interfaces": { - "auto": ["enp0s8", "enp0s8:2-3", "enp0s8:2-4"], - "enp0s8": {"address": "169.254.202.2/24"}, - "enp0s8:2-3": {"address": "192.168.204.2/24"}, - "enp0s8:2-4": {"address": "fd01::2/64"}}, - "routes": [ - {"net": "14.15.1.0/24", "via": "169.254.202.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.2.0/24", "via": "192.168.204.111", "dev": "enp0s8", "metric": 1}], - "routes6": [ - {"net": "fa01:2::/64", "via": "fd01::111", "dev": "enp0s8", "metric": 1}], - } - - self._add_fs_mock(FILE_GEN.generate_file_tree(etc_files=etc_files)) - self._add_nw_mock(["enp0s8"]) - self._add_scmd_mock() - self._add_logger_mock() - self._nwmock.apply_auto() - - if delete_ifstate: - self._fs.delete(anc.IFSTATE_BASE_PATH + "enp0s8") - - self.assertEqual(['enp0s8 UP 169.254.202.2/24 192.168.204.2/24 fd01::2/64'], - self._nwmock.get_links_status()) - - self.assertEqual(['14.15.1.0/24 via 169.254.202.111 dev enp0s8 metric 1', - '14.14.2.0/24 via 192.168.204.111 dev enp0s8 metric 1', - 'fa01:2::/64 via fd01::111 dev enp0s8 metric 1'], - self._nwmock.get_routes()) - - self._mocked_call([self._mock_fs, self._mock_syscmd, self._mock_logger], - anc.set_iface_down, "enp0s8") - - self.assertEqual(['enp0s8 DOWN'], self._nwmock.get_links_status()) - self.assertEqual([], self._nwmock.get_routes()) - - def test_set_iface_down_ifstate_up(self): - self._test_set_iface_down(delete_ifstate=False) - self.assertEqual([('ifdown', 'enp0s8'), - ('ip_link_set_down', 'enp0s8'), - ('ip_addr_flush', 'enp0s8')], - self._nwmock.get_history()) - - def test_set_iface_down_ifstate_down(self): - self._test_set_iface_down(delete_ifstate=True) - self.assertEqual([('ip_link_set_down', 'enp0s8'), - ('ip_addr_flush', 'enp0s8')], - self._nwmock.get_history()) - - def test_set_iface_down_error_messages(self): - def exec_sys_cmd(cmd): - if cmd.startswith("/sbin/ifdown"): - return 1, "< IFDOWN ERROR MESSAGE >\n" - if cmd.startswith("/usr/sbin/ip link set down"): - return 1, "< IP LINK SET DOWN ERROR MESSAGE >\n" - if cmd.startswith("/usr/sbin/ip addr flush"): - return 1, ("\n< IP ADDR FLUSH ERROR MESSAGE LINE 1 >\n" - "< IP ADDR FLUSH ERROR MESSAGE LINE 2 >\n\n\n") - raise Exception(f"Unexpected system command: '{cmd}'") - - dev_path = "/sys/devices/pci0000:00/net/enp0s8" - self._add_fs_mock({dev_path + "/operstate": "up", - anc.DEVLINK_BASE_PATH + "enp0s8": (dev_path, ), - anc.IFSTATE_BASE_PATH + "enp0s8": "enp0s8"}) - self._add_logger_mock() - - with mock.patch('src.bin.apply_network_config.execute_system_cmd', exec_sys_cmd): - self._mocked_call([self._mock_fs, self._mock_logger], anc.set_iface_down, "enp0s8") - - self.assertEqual([ - ('info', 'Bringing enp0s8 down'), - ('error', "Command 'ifdown' failed for interface enp0s8: '< IFDOWN ERROR MESSAGE >'"), - ('error', "Command 'ip link set down' failed for interface enp0s8: " - "'< IP LINK SET DOWN ERROR MESSAGE >'"), - ('error', "Command 'ip addr flush' failed for interface enp0s8:\n" - "< IP ADDR FLUSH ERROR MESSAGE LINE 1 >\n" - "< IP ADDR FLUSH ERROR MESSAGE LINE 2 >")], - self._log.get_history()) - - def test_remove_iface_config_file(self): - self._add_logger_mock() - - def run_function(path_exists: bool): - with(mock.patch('src.bin.apply_network_config.path_exists', return_value=path_exists), - mock.patch('os.remove', side_effect=OSError("< OS ERROR >"))): - self._mocked_call([self._mock_logger], anc.remove_iface_config_file, "enp0s8") - - run_function(False) - self.assertEqual([('info', 'File /etc/network/interfaces.d/ifcfg-enp0s8 does not exist, ' - 'no need to remove')], self._log.get_history()) - - self._log.reset_history() - run_function(True) - self.assertEqual([ - ('info', 'Removing /etc/network/interfaces.d/ifcfg-enp0s8'), - ('error', 'Failed to remove /etc/network/interfaces.d/ifcfg-enp0s8: < OS ERROR >')], - self._log.get_history()) - - def _test_write_iface_config_file(self, has_existing_file): - path = anc.ETC_DIR + "/ifcfg-enp0s8" - contents = {path: "EXISTING CONTENTS\n"} if has_existing_file else None - self._add_fs_mock(contents) - with mock.patch('src.bin.apply_network_config.get_header', return_value=self._HEADER): - self._mocked_call([self._mock_fs], - anc.write_iface_config_file, "enp0s8", self._IFACE_CONFIG) - contents = self._fs.get_file_contents(path) - self.assertEqual(self._IFACE_FILE, contents) - - def test_write_iface_config_file_new(self): - self._test_write_iface_config_file(False) # pylint: disable=no-value-for-parameter - - def test_write_iface_config_file_existing(self): - self._test_write_iface_config_file(True) # pylint: disable=no-value-for-parameter - - _AUTO_LIST = ["lo", "enp0s3", "enp0s8", "vlan100"] - - _AUTO_FILE = (f"{_HEADER}\n" - "auto lo enp0s3 enp0s8 vlan100\n") - - def _test_write_auto_file(self, has_existing_file): - path = anc.ETC_DIR + "/auto" - contents = {path: "EXISTING CONTENTS\n"} if has_existing_file else None - self._add_fs_mock(contents) - with mock.patch('src.bin.apply_network_config.get_header', return_value=self._HEADER): - self._mocked_call([self._mock_fs], anc.write_auto_file, self._AUTO_LIST) - contents = self._fs.get_file_contents(path) - self.assertEqual(self._AUTO_FILE, contents) - - def test_write_auto_file_new(self): - self._test_write_auto_file(False) # pylint: disable=no-value-for-parameter - - def test_write_auto_file_existing(self): - self._test_write_auto_file(True) # pylint: disable=no-value-for-parameter - - def test_sort_properties(self): - props = ["other3", "allow-", "gateway", "other1", "mtu", "bond-miimon", "other2", "iface"] - sorted_props = anc.sort_properties(props) - self.assertEqual(["iface", "gateway", "bond-miimon", "mtu", - "other1", "other2", "other3", "allow-"], sorted_props) - - def test_get_route_entries(self): - self._add_fs_mock( - {anc.PUPPET_ROUTES_FILE: - "13.13.1.0 255.255.255.0 12.12.1.65 bond0 metric 1\n" - "13.13.2.0 255.255.255.0 12.12.3.37 enp0s8\n", - anc.PUPPET_ROUTES6_FILE: - "dead:beef:55:: ffff:ffff:ffff:ffff:: dead:beef::aa:1:453 bond0 metric 1\n" - "dead:beef:78:: ffff:ffff:ffff:ffff:: dead:beef:bb::bb:1:172 vlan200"}) - self._add_logger_mock() - - entries = self._mocked_call([self._mock_fs, self._mock_logger], anc.get_route_entries, - [anc.PUPPET_ROUTES_FILE, anc.PUPPET_ROUTES6_FILE]) - - self.assertEqual(['13.13.1.0 255.255.255.0 12.12.1.65 bond0 metric 1', - '13.13.2.0 255.255.255.0 12.12.3.37 enp0s8', - 'dead:beef:55:: ffff:ffff:ffff:ffff:: dead:beef::aa:1:453 bond0 metric 1', - 'dead:beef:78:: ffff:ffff:ffff:ffff:: dead:beef:bb::bb:1:172 vlan200'], - entries) - self.assertEqual([], self._log.get_history()) - - def test_get_route_entries_from_lines(self): - self._add_logger_mock() - - contents = [ - "# Comment 1", - "", - " # Comment 2", - "\t # Comment 3", - "13.13.1.0 255.255.255.0 12.12.1.65 bond0 metric 1", - "\t13.13.2.0\t255.255.255.0\t12.12.3.37\tenp0s8\t\t\t", - " 13.13.3.0 255.255.255.0 12.12.3.113 vlan200 metric 1 ", - " 13.13.4.0 255.255.255.0 12.12.4.16 ", - " \t dead:beef:55:: ffff:ffff:ffff:ffff:: dead:beef::aa:1:453 bond0 metric 1 ", - " dead:beef:78:: ffff:ffff:ffff:ffff:: dead:beef:bb::bb:1:172 vlan200 metric 1\t"] - - entries = self._mocked_call([self._mock_logger], - anc.get_route_entries_from_lines, contents, anc.ETC_ROUTES_FILE) - - self.assertEqual([ - '13.13.1.0 255.255.255.0 12.12.1.65 bond0 metric 1', - '13.13.2.0 255.255.255.0 12.12.3.37 enp0s8', - '13.13.3.0 255.255.255.0 12.12.3.113 vlan200 metric 1', - 'dead:beef:55:: ffff:ffff:ffff:ffff:: dead:beef::aa:1:453 bond0 metric 1', - 'dead:beef:78:: ffff:ffff:ffff:ffff:: dead:beef:bb::bb:1:172 vlan200 metric 1'], - entries) - - self.assertEqual([( - 'warning', - "Invalid route in file '/etc/network/routes', must have at least 4 " - "parameters, 3 found: '13.13.4.0 255.255.255.0 12.12.4.16'")], - self._log.get_history()) - - def test_get_route_iface(self): - self.assertEqual("vlan200", anc.get_route_iface("13.13.3.0 255.255.255.0 12.12.3.113 " - "vlan200 metric 1")) - - def test_create_route_obj_from_entry(self): - self.assertEqual({'ifname': 'enp0s8', - 'network': '13.13.2.0', - 'netmask': '255.255.255.0', - 'nexthop': '12.12.3.37'}, - anc.create_route_obj_from_entry( - "13.13.2.0 255.255.255.0 12.12.3.37 enp0s8")) - self.assertEqual({'ifname': 'bond0', - 'network': '13.13.1.0', - 'netmask': '255.255.255.0', - 'nexthop': '12.12.1.65', - 'metric': '1'}, - anc.create_route_obj_from_entry( - "13.13.1.0 255.255.255.0 12.12.1.65 bond0 metric 1")) - - def test_get_prefix_length(self): - self.assertEqual(0, anc.get_prefix_length('0.0.0.0')) - self.assertEqual(1, anc.get_prefix_length('128.0.0.0')) - self.assertEqual(8, anc.get_prefix_length('255.0.0.0')) - self.assertEqual(31, anc.get_prefix_length('255.255.255.254')) - - self.assertEqual(0, anc.get_prefix_length('0::')) - self.assertEqual(1, anc.get_prefix_length('8000::')) - self.assertEqual(16, anc.get_prefix_length('ffff::')) - self.assertEqual(127, anc.get_prefix_length('ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe')) - - def assert_fails(netmask): - exc = self.assertRaises(anc.InvalidNetmaskError, anc.get_prefix_length, netmask) - self.assertEqual(f"Failed to get prefix length, invalid netmask: '{netmask}'", str(exc)) - - assert_fails("2555.0.0.0") - assert_fails("255.0.255.0") - assert_fails("0.255.0.0") - - assert_fails("fffff:ffff::") - assert_fails("ffff::ffff") - assert_fails("::ffff") - - def test_get_linux_network(self): - self.assertEqual("192.168.1.0/24", anc.get_linux_network({"network": "192.168.1.0", - "netmask": "255.255.255.0"})) - self.assertEqual("default", anc.get_linux_network({"network": "default"})) - - def _test_remove_route_entry_from_kernel(self, entry, return_code=0, stdout=""): - received_cmd = None - - def exec_sys_cmd(cmd): - nonlocal received_cmd - received_cmd = cmd - return return_code, stdout - - with mock.patch('src.bin.apply_network_config.execute_system_cmd', exec_sys_cmd): - self._mocked_call([self._mock_logger], anc.remove_route_entry_from_kernel, entry) - - return received_cmd - - def test_remove_route_entry_from_kernel_invalid_netmask(self): - self._add_logger_mock() - self._test_remove_route_entry_from_kernel("13.13.3.0 2555.255.255.0 12.12.3.113 " - "vlan200 metric 1") - self.assertEqual([( - 'error', - "Failed to remove route entry '13.13.3.0 2555.255.255.0 12.12.3.113 vlan200 " - "metric 1' from the kernel: Failed to get prefix length, invalid netmask: " - "'2555.255.255.0'")], - self._log.get_history()) - - def test_remove_route_entry_from_kernel_fail(self): - self._add_logger_mock() - self._test_remove_route_entry_from_kernel("13.13.3.0 255.255.255.0 12.12.3.113 " - "vlan200 metric 1", 1, "< ERROR >") - self.assertEqual( - [('info', 'Removing route: 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1'), - ('error', "Failed removing route 13.13.3.0/24 via 12.12.3.113 dev vlan200 " - "metric 1: '< ERROR >'")], - self._log.get_history()) - - def test_remove_route_entry_from_kernel_succeed(self): - self._add_logger_mock() - cmd = self._test_remove_route_entry_from_kernel("13.13.3.0 255.255.255.0 12.12.3.113 " - "vlan200 metric 1") - self.assertEqual( - [('info', 'Removing route: 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1')], - self._log.get_history()) - self.assertEqual( - "/usr/sbin/ip route del 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", cmd) - - def test_get_route_description(self): - route_1 = {"network": "13.13.3.0", "netmask": "255.255.255.0", - "nexthop": "12.12.3.113", "ifname": "vlan200"} - self.assertEqual("13.13.3.0/24 via 12.12.3.113 dev vlan200", - anc.get_route_description(route_1)) - self.assertEqual("13.13.3.0/24", anc.get_route_description(route_1, False)) - route_1["metric"] = 1 - self.assertEqual("13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", - anc.get_route_description(route_1)) - self.assertEqual("13.13.3.0/24 metric 1", anc.get_route_description(route_1, False)) - - route_2 = {"network": "default", "nexthop": "12.12.3.113", "ifname": "vlan200"} - self.assertEqual("default via 12.12.3.113 dev vlan200", anc.get_route_description(route_2)) - self.assertEqual("default", anc.get_route_description(route_2, False)) - route_2["metric"] = 1 - self.assertEqual("default via 12.12.3.113 dev vlan200 metric 1", - anc.get_route_description(route_2)) - self.assertEqual("default metric 1", anc.get_route_description(route_2, False)) - - route_3 = {"network": "aabb::", "netmask": "ffff:ffff:ffff:ffff::", - "nexthop": "fe88::1", "ifname": "enp0s9"} - self.assertEqual("aabb::/64 via fe88::1 dev enp0s9", anc.get_route_description(route_3)) - self.assertEqual("aabb::/64", anc.get_route_description(route_3, False)) - route_3["metric"] = 1 - self.assertEqual("aabb::/64 via fe88::1 dev enp0s9 metric 1", - anc.get_route_description(route_3)) - self.assertEqual("aabb::/64 metric 1", anc.get_route_description(route_3, False)) - - route_4 = {"network": "default", "nexthop": "fe88::1", "ifname": "enp0s9"} - self.assertEqual("default via fe88::1 dev enp0s9", anc.get_route_description(route_4)) - self.assertEqual("default", anc.get_route_description(route_4, False)) - route_4["metric"] = 1 - self.assertEqual("default via fe88::1 dev enp0s9 metric 1", - anc.get_route_description(route_4)) - self.assertEqual("default metric 1", anc.get_route_description(route_4, False)) - - def _test_add_route_entry_to_kernel(self, entry, cmd_responses): - position = 0 - self._add_logger_mock() - - def exec_sys_cmd(cmd): - nonlocal position - pos = position - position += 1 - self.assertEqual(cmd_responses[pos][0], cmd) - return cmd_responses[pos][1], cmd_responses[pos][2] - - with mock.patch('src.bin.apply_network_config.execute_system_cmd', exec_sys_cmd): - self._mocked_call([self._mock_logger], anc.add_route_entry_to_kernel, entry) - - def test_add_route_entry_to_kernel_existing(self): - self._test_add_route_entry_to_kernel( - "13.13.3.0 255.255.255.0 12.12.3.113 vlan200 metric 1", - (("/usr/sbin/ip route show 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 0, - "13.13.3.0/24"), )) - self.assertEqual( - [('info', 'Adding route: 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1'), - ('info', 'Route already exists, skipping')], - self._log.get_history()) - - def test_add_route_entry_to_kernel_show_fail(self): - self._test_add_route_entry_to_kernel( - "13.13.3.0 255.255.255.0 12.12.3.113 vlan200 metric 1", - (("/usr/sbin/ip route show 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 1, - "< ERROR 1 >"), - ("/usr/sbin/ip route show 13.13.3.0/24 metric 1", 1, "< ERROR 2 >"), - ("/usr/sbin/ip route add 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 0, ""))) - self.assertEqual( - [('info', 'Adding route: 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1')], - self._log.get_history()) - - def test_add_route_entry_to_kernel_add_fail(self): - self._test_add_route_entry_to_kernel( - "13.13.3.0 255.255.255.0 12.12.3.113 vlan200 metric 1", - (("/usr/sbin/ip route show 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 0, ""), - ("/usr/sbin/ip route show 13.13.3.0/24 metric 1", 0, ""), - ("/usr/sbin/ip route add 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 1, - "< ERROR >"))) - self.assertEqual( - [('info', 'Adding route: 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1'), - ('error', "Failed adding route 13.13.3.0/24 via 12.12.3.113 dev " - "vlan200 metric 1: '< ERROR >'")], - self._log.get_history()) - - def test_add_route_entry_to_kernel_replace_fail(self): - self._test_add_route_entry_to_kernel( - "13.13.3.0 255.255.255.0 12.12.3.113 vlan200 metric 1", - (("/usr/sbin/ip route show 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 0, ""), - ("/usr/sbin/ip route show 13.13.3.0/24 metric 1", 0, - "13.13.3.0/24 via 12.12.3.1 dev vlan200"), - ("/usr/sbin/ip route replace 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 1, - "< ERROR >"))) - self.assertEqual( - [('info', 'Adding route: 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1'), - ('info', 'Route to specified network already exists, replacing: 13.13.3.0/24 via ' - '12.12.3.1 dev vlan200'), - ('error', "Failed replacing route 13.13.3.0/24 via 12.12.3.113 dev " - "vlan200 metric 1: '< ERROR >'")], - self._log.get_history()) - - def test_add_route_entry_to_kernel_add_succeed(self): - self._test_add_route_entry_to_kernel( - "13.13.3.0 255.255.255.0 12.12.3.113 vlan200 metric 1", - (("/usr/sbin/ip route show 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 0, ""), - ("/usr/sbin/ip route show 13.13.3.0/24 metric 1", 0, ""), - ("/usr/sbin/ip route add 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 0, ""))) - self.assertEqual( - [('info', 'Adding route: 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1')], - self._log.get_history()) - - def test_add_route_entry_to_kernel_replace_succeed(self): - self._test_add_route_entry_to_kernel( - "13.13.3.0 255.255.255.0 12.12.3.113 vlan200 metric 1", - (("/usr/sbin/ip route show 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 0, ""), - ("/usr/sbin/ip route show 13.13.3.0/24 metric 1", 0, - "13.13.3.0/24 via 12.12.3.1 dev vlan200"), - ("/usr/sbin/ip route replace 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1", 0, - ""))) - self.assertEqual( - [('info', 'Adding route: 13.13.3.0/24 via 12.12.3.113 dev vlan200 metric 1'), - ('info', 'Route to specified network already exists, replacing: 13.13.3.0/24 via ' - '12.12.3.1 dev vlan200')], - self._log.get_history()) - - def _test_update_routes(self, etc_routes, puppet_routes, updated_ifaces=None): - links = ["enc10", "enc11", "enc12", "enc13"] - self._add_fs_mock(FILE_GEN.generate_file_tree( - puppet_files={ - "routes": [route for route in puppet_routes if ":" not in route["net"]], - "routes6": [route for route in puppet_routes if ":" in route["net"]] - }, - etc_files={ - "interfaces": { - "auto": links, - "enc10": {"address": "10.10.10.3/24"}, - "enc11": {"address": "10.10.11.3/24"}, - "enc12": {"address": "fd12::3/64"}, - "enc13": {"address": "fd13::3/64"}, - }, - "routes": etc_routes, - } - )) - self._add_nw_mock(links) - self._add_scmd_mock() - self._add_logger_mock() - self._nwmock.apply_auto() - - if updated_ifaces: - for iface in updated_ifaces: - self._nwmock.ifdown(iface) - self._nwmock.ifup(iface) - - with mock.patch('src.bin.apply_network_config.get_header', return_value=self._HEADER): - self._mocked_call([self._mock_fs, self._mock_syscmd, self._mock_sysinv_lock, - self._mock_logger], anc.update_routes, updated_ifaces) - - def test_update_routes(self): - self._test_update_routes( - etc_routes=[ - {"net": "10.33.1.0/24", "via": "10.10.10.101", "dev": "enc10", "metric": 1}, - {"net": "10.33.2.0/24", "via": "10.10.10.101", "dev": "enc10", "metric": 1}, - {"net": "10.33.3.0/24", "via": "10.10.10.101", "dev": "enc10", "metric": 1}, - {"net": "fd33:1::/64", "via": "fd12::101", "dev": "enc12", "metric": 1}, - {"net": "fd33:2::/64", "via": "fd12::101", "dev": "enc12", "metric": 1}, - {"net": "fd33:3::/64", "via": "fd12::101", "dev": "enc12", "metric": 1}], - puppet_routes=[ - {"net": "10.33.1.0/24", "via": "10.10.10.101", "dev": "enc10", "metric": 1}, - {"net": "10.33.2.0/24", "via": "10.10.10.202", "dev": "enc10", "metric": 1}, - {"net": "10.33.4.0/24", "via": "10.10.10.101", "dev": "enc10", "metric": 1}, - {"net": "fd33:1::/64", "via": "fd12::101", "dev": "enc12", "metric": 1}, - {"net": "fd33:2::/64", "via": "fd12::202", "dev": "enc12", "metric": 1}, - {"net": "fd33:4::/64", "via": "fd12::101", "dev": "enc12", "metric": 1}]) - - self.assertEqual([ - '10.33.1.0/24 via 10.10.10.101 dev enc10 metric 1', - 'fd33:1::/64 via fd12::101 dev enc12 metric 1', - '10.33.2.0/24 via 10.10.10.202 dev enc10 metric 1', - '10.33.4.0/24 via 10.10.10.101 dev enc10 metric 1', - 'fd33:2::/64 via fd12::202 dev enc12 metric 1', - 'fd33:4::/64 via fd12::101 dev enc12 metric 1'], - self._nwmock.get_routes()) - - self.assertEqual([ - ('info', 'Differences found between /var/run/network-scripts.puppet/routes and ' - '/etc/network/routes'), - ('info', 'Removing route: 10.33.2.0/24 via 10.10.10.101 dev enc10 metric 1'), - ('info', 'Removing route: 10.33.3.0/24 via 10.10.10.101 dev enc10 metric 1'), - ('info', 'Removing route: fd33:2::/64 via fd12::101 dev enc12 metric 1'), - ('info', 'Removing route: fd33:3::/64 via fd12::101 dev enc12 metric 1'), - ('info', 'Route not previously present in /etc/network/routes, adding'), - ('info', 'Adding route: 10.33.2.0/24 via 10.10.10.202 dev enc10 metric 1'), - ('info', 'Route not previously present in /etc/network/routes, adding'), - ('info', 'Adding route: 10.33.4.0/24 via 10.10.10.101 dev enc10 metric 1'), - ('info', 'Route not previously present in /etc/network/routes, adding'), - ('info', 'Adding route: fd33:2::/64 via fd12::202 dev enc12 metric 1'), - ('info', 'Route not previously present in /etc/network/routes, adding'), - ('info', 'Adding route: fd33:4::/64 via fd12::101 dev enc12 metric 1')], - self._log.get_history()) - - self.assertEqual( - self._HEADER + "\n" - "10.33.1.0 255.255.255.0 10.10.10.101 enc10 metric 1\n" - "10.33.2.0 255.255.255.0 10.10.10.202 enc10 metric 1\n" - "10.33.4.0 255.255.255.0 10.10.10.101 enc10 metric 1\n" - "fd33:1:: ffff:ffff:ffff:ffff:: fd12::101 enc12 metric 1\n" - "fd33:2:: ffff:ffff:ffff:ffff:: fd12::202 enc12 metric 1\n" - "fd33:4:: ffff:ffff:ffff:ffff:: fd12::101 enc12 metric 1\n", - self._fs.get_file_contents(anc.ETC_ROUTES_FILE)) - - def test_update_routes_updated_interfaces(self): - routes = [ - {"net": "10.33.1.0/24", "via": "10.10.10.101", "dev": "enc10", "metric": 1}, - {"net": "10.33.2.0/24", "via": "10.10.10.101", "dev": "enc10", "metric": 1}, - {"net": "10.33.3.0/24", "via": "10.10.11.101", "dev": "enc11", "metric": 1}, - {"net": "10.33.4.0/24", "via": "10.10.11.101", "dev": "enc11", "metric": 1}, - {"net": "fd33:1::/64", "via": "fd12::101", "dev": "enc12", "metric": 1}, - {"net": "fd33:2::/64", "via": "fd12::101", "dev": "enc12", "metric": 1}, - {"net": "fd33:3::/64", "via": "fd13::101", "dev": "enc13", "metric": 1}, - {"net": "fd33:4::/64", "via": "fd13::101", "dev": "enc13", "metric": 1}] - self._test_update_routes(routes, routes, ["enc11", "enc13"]) - - self.assertEqual([ - '10.33.1.0/24 via 10.10.10.101 dev enc10 metric 1', - '10.33.2.0/24 via 10.10.10.101 dev enc10 metric 1', - 'fd33:1::/64 via fd12::101 dev enc12 metric 1', - 'fd33:2::/64 via fd12::101 dev enc12 metric 1', - '10.33.3.0/24 via 10.10.11.101 dev enc11 metric 1', - '10.33.4.0/24 via 10.10.11.101 dev enc11 metric 1', - 'fd33:3::/64 via fd13::101 dev enc13 metric 1', - 'fd33:4::/64 via fd13::101 dev enc13 metric 1'], - self._nwmock.get_routes()) - - self.assertEqual([ - ('info', 'No differences found between /var/run/network-scripts.puppet/routes and ' - '/etc/network/routes'), - ('info', 'Route is associated with and updated interface, adding'), - ('info', 'Adding route: 10.33.3.0/24 via 10.10.11.101 dev enc11 metric 1'), - ('info', 'Route is associated with and updated interface, adding'), - ('info', 'Adding route: 10.33.4.0/24 via 10.10.11.101 dev enc11 metric 1'), - ('info', 'Route is associated with and updated interface, adding'), - ('info', 'Adding route: fd33:3::/64 via fd13::101 dev enc13 metric 1'), - ('info', 'Route is associated with and updated interface, adding'), - ('info', 'Adding route: fd33:4::/64 via fd13::101 dev enc13 metric 1')], - self._log.get_history()) - - self.assertEqual( - self._HEADER + "\n" - "10.33.1.0 255.255.255.0 10.10.10.101 enc10 metric 1\n" - "10.33.2.0 255.255.255.0 10.10.10.101 enc10 metric 1\n" - "10.33.3.0 255.255.255.0 10.10.11.101 enc11 metric 1\n" - "10.33.4.0 255.255.255.0 10.10.11.101 enc11 metric 1\n" - "fd33:1:: ffff:ffff:ffff:ffff:: fd12::101 enc12 metric 1\n" - "fd33:2:: ffff:ffff:ffff:ffff:: fd12::101 enc12 metric 1\n" - "fd33:3:: ffff:ffff:ffff:ffff:: fd13::101 enc13 metric 1\n" - "fd33:4:: ffff:ffff:ffff:ffff:: fd13::101 enc13 metric 1\n", - self._fs.get_file_contents(anc.ETC_ROUTES_FILE)) - - def test_check_cloud_init_valid(self): - static_links = ["lo", "ens1f0"] - self._add_fs_mock({ - anc.ETC_DIR + "/auto": FILE_GEN.generate_auto_file(static_links), - anc.ETC_DIR + "/ifcfg-ens1f0": - FILE_GEN.generate_ifcfg_file("ens1f0", {"address": "fd05::2/64", - "gateway": "fd05::111"}), - anc.SUBCLOUD_ENROLLMENT_FILE: '', - anc.CLOUD_INIT_FILE: - "# This file is generated from information provided by the datasource. Changes\n" - "# to it will not persist across an instance reboot. To disable cloud-init's\n" - "# network configuration capabilities, write a file\n" - "# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:\n" - "# network: {config: disabled}\n" - "auto lo\n" - "iface lo inet loopbackauto vlan401\n" - "iface vlan401 inet6 static\n" - " address 2620:10a:a001:d41::163/64\n" - " gateway 2620:10a:a001:d41::1\n" - " vlan-raw-device ens1f0\n" - " vlan_id 401\n"}) - - self._add_nw_mock(static_links) - self._add_scmd_mock() - self._add_logger_mock() - self._nwmock.set_allow_multiple_default_gateways(True) - self._nwmock.apply_auto() - self._nwmock.ifup("vlan401") - self._nwmock.ifdown("ens1f0") - self._nwmock.ifup("ens1f0") - - self._mocked_call([self._mock_fs, self._mock_syscmd, self._mock_logger], - anc.check_enrollment_config) - - self.assertEqual(['default via 2620:10a:a001:d41::1 dev vlan401 metric 1024'], - self._nwmock.get_routes()) - - self.assertEqual([ - ('info', "Enrollment: Parsing file '/etc/network/interfaces.d/50-cloud-init'"), - ('info', 'Enrollment: Configuring interface vlan401 with gateway ' - '2620:10a:a001:d41::1'), - ('info', 'Adding route: default via 2620:10a:a001:d41::1 dev vlan401'), - ('info', 'Route to specified network already exists, replacing: default via fd05::111 ' - 'dev ens1f0 metric 1024 pref medium')], - self._log.get_history()) - - def test_check_cloud_init_multiple_ifaces(self): - static_links = ["lo", "ens1f0"] - self._add_fs_mock({ - anc.ETC_DIR + "/auto": FILE_GEN.generate_auto_file(static_links), - anc.ETC_DIR + "/ifcfg-ens1f0": - FILE_GEN.generate_ifcfg_file("ens1f0", {"address": "fd05::2/64", - "gateway": "fd05::111"}), - anc.SUBCLOUD_ENROLLMENT_FILE: '', - anc.CLOUD_INIT_FILE: - "auto lo\n" - "iface lo inet loopbackauto vlan401\n" - "iface vlan401 inet6 static\n" - " address 2620:10a:a001:d41::163/64\n" - " gateway 2620:10a:a001:d41::1\n" - " vlan-raw-device ens1f0\n" - " vlan_id 401\n" - "iface vlan402 inet6 static\n" - " address eb22:303::55:2/64\n" - " gateway eb22:303::1\n" - " vlan-raw-device ens1f0\n" - " vlan_id 402\n"}) - - self._add_nw_mock(static_links) - self._add_scmd_mock() - self._add_logger_mock() - self._nwmock.set_allow_multiple_default_gateways(True) - self._nwmock.apply_auto() - self._nwmock.ifup("vlan401") - self._nwmock.ifup("vlan402") - self._nwmock.ifdown("ens1f0") - self._nwmock.ifup("ens1f0") - - self._mocked_call([self._mock_fs, self._mock_syscmd, self._mock_logger], - anc.check_enrollment_config) - - self.assertEqual(['default via eb22:303::1 dev vlan402 metric 1024'], - self._nwmock.get_routes()) - - self.assertEqual([ - ('info', "Enrollment: Parsing file '/etc/network/interfaces.d/50-cloud-init'"), - ('warning', 'Enrollment: Multiple interfaces with gateway for ipv6 found: vlan401, ' - 'vlan402'), - ('info', 'Enrollment: Configuring interface vlan401 with gateway ' - '2620:10a:a001:d41::1'), - ('info', 'Adding route: default via 2620:10a:a001:d41::1 dev vlan401'), - ('info', 'Route to specified network already exists, replacing: default via fd05::111 ' - 'dev ens1f0 metric 1024 pref medium'), - ('info', 'Enrollment: Configuring interface vlan402 with gateway eb22:303::1'), - ('info', 'Adding route: default via eb22:303::1 dev vlan402'), - ('info', 'Route to specified network already exists, replacing: default via ' - '2620:10a:a001:d41::1 dev vlan401 metric 1024 pref medium')], - self._log.get_history()) - - def test_check_cloud_init_empty(self): - self._add_fs_mock({ - anc.SUBCLOUD_ENROLLMENT_FILE: '', - anc.CLOUD_INIT_FILE: ''}) - - self._add_logger_mock() - - self._mocked_call([self._mock_fs, self._mock_logger], anc.check_enrollment_config) - - self.assertEqual([ - ('info', "Enrollment: Parsing file '/etc/network/interfaces.d/50-cloud-init'"), - ('warning', 'Enrollment: Could not find any valid interface config in ' - "'/etc/network/interfaces.d/50-cloud-init'")], - self._log.get_history()) - - def test_check_cloud_init_invalid_gateway(self): - self._add_fs_mock({ - anc.SUBCLOUD_ENROLLMENT_FILE: '', - anc.CLOUD_INIT_FILE: - "auto lo\n" - "iface lo inet loopbackauto vlan401\n" - "iface vlan401 inet6 static\n" - " address 2620:10a:a001:d41::163/64\n" - " gateway h620::1\n" - " vlan-raw-device ens1f0\n" - " vlan_id 401\n"}) - - self._add_logger_mock() - - self._mocked_call([self._mock_fs, self._mock_logger], anc.check_enrollment_config) - - self.assertEqual([ - ('info', "Enrollment: Parsing file '/etc/network/interfaces.d/50-cloud-init'"), - ('warning', "Enrollment: Invalid gateway address 'h620::1' for interface 'vlan401'"), - ('warning', 'Enrollment: No interface with gateway address found, skipping')], - self._log.get_history()) - - def test_disable_kickstart_pxeboot(self): - etc_cfg = { - "interfaces": { - "auto": ["lo", "enp0s8"], - "lo": {}, - "enp0s8": {}, }, - } - - puppet_cfg = { - "interfaces": { - "auto": ["lo", "enp0s8", "enp0s8:2-3", "enp0s8:2-4"], - "lo": {}, - "enp0s8": {"address": "169.254.202.2/24"}, - "enp0s8:2-3": {"address": "192.168.204.2/24"}, - "enp0s8:2-4": {"address": "fd01::2/64"}}, - } - - contents = FILE_GEN.generate_file_tree(puppet_files=puppet_cfg, etc_files=etc_cfg) - contents[anc.ETC_DIR + "/ifcfg-pxeboot"] = ( - "auto enp0s8:2\n" - "iface enp0s8:2 inet dhcp\n" - " post-up echo 0 > /proc/sys/net/ipv6/conf/enp0s8/autoconf; " - "echo 0 > /proc/sys/net/ipv6/conf/enp0s8/accept_ra; " # noqa: E131 - "echo 0 > /proc/sys/net/ipv6/conf/enp0s8/accept_redirects\n") - - self._add_fs_mock(contents) - self._add_nw_mock(["lo", "enp0s8"]) - self._add_scmd_mock() - self._add_logger_mock() - self._nwmock.apply_auto() - - self._mocked_call([self._mock_fs, self._mock_syscmd, - self._mock_sysinv_lock, self._mock_logger], anc.update_interfaces) - - self.assertEqual([ - ('info', 'Turn off pxeboot install config for enp0s8:2, will be turned on later'), - ('info', 'Bringing enp0s8:2 down'), - ('info', 'Remove ifcfg-pxeboot, left from kickstart install phase'), - ('info', 'Removing /etc/network/interfaces.d/ifcfg-pxeboot')], - self._log.get_history()[:4]) - - def test_execute_system_cmd(self): - retcode, stdout = anc.execute_system_cmd('echo "test_execute_system_cmd"') - self.assertEqual(0, retcode) - self.assertEqual("test_execute_system_cmd\n", stdout) - - _OS_GETPGID = os.getpgid - - def test_execute_system_cmd_timeout_retcode_15(self): - subproc_pid = None - subproc_pgid = None - - def getpgid(pid): - nonlocal subproc_pid - nonlocal subproc_pgid - subproc_pid = pid - subproc_pgid = self._OS_GETPGID(pid) - return subproc_pgid - - self._add_logger_mock() - - with mock.patch("os.getpgid", getpgid): - retcode, stdout = self._mocked_call([self._mock_logger], anc.execute_system_cmd, - "tests/system_cmd_test_script.sh 15", 1) - - self.assertEqual(15, retcode) - self.assertEqual("< BEFORE SLEEP >\nTerminated\n< SIGTERM RECEIVED >\n", stdout) - self.assertEqual([ - (LoggerMock.WARNING, - "Execution time exceeded for command 'tests/system_cmd_test_script.sh 15', sending " - f"SIGTERM to subprocess (pid={subproc_pid}, pgid={subproc_pgid})")], - self._log.get_history()) - - def test_execute_system_cmd_timeout_retcode_0(self): - subproc_pid = None - subproc_pgid = None - - def getpgid(pid): - nonlocal subproc_pid - nonlocal subproc_pgid - subproc_pid = pid - subproc_pgid = self._OS_GETPGID(pid) - return subproc_pgid - - self._add_logger_mock() - - with mock.patch("os.getpgid", getpgid): - retcode, stdout = self._mocked_call([self._mock_logger], anc.execute_system_cmd, - "tests/system_cmd_test_script.sh 0", 1) - - self.assertEqual(0, retcode) - self.assertEqual("< BEFORE SLEEP >\nTerminated\n< SIGTERM RECEIVED >\n", stdout) - self.assertEqual([ - (LoggerMock.WARNING, - "Execution time exceeded for command 'tests/system_cmd_test_script.sh 0', sending " - f"SIGTERM to subprocess (pid={subproc_pid}, pgid={subproc_pgid})"), - (LoggerMock.INFO, - "Command 'tests/system_cmd_test_script.sh 0' output:\n" - '< BEFORE SLEEP >\n' - 'Terminated\n' - '< SIGTERM RECEIVED >')], - self._log.get_history()) - - def test_execute_system_cmd_timeout_kill(self): - subproc_pid = None - subproc_pgid = None - - def getpgid(pid): - nonlocal subproc_pid - nonlocal subproc_pgid - subproc_pid = pid - subproc_pgid = self._OS_GETPGID(pid) - return subproc_pgid - - self._add_logger_mock() - - with (mock.patch("os.getpgid", getpgid), - mock.patch("src.bin.apply_network_config.TERM_WAIT_TIME", 1)): - retcode, stdout = self._mocked_call([self._mock_logger], anc.execute_system_cmd, - "tests/system_cmd_test_script.sh 0 -e", 1) - - self.assertEqual(-9, retcode) - self.assertEqual("< BEFORE SLEEP >\nTerminated\n< SIGTERM RECEIVED >\n", stdout) - self.assertEqual([ - (LoggerMock.WARNING, - "Execution time exceeded for command 'tests/system_cmd_test_script.sh 0 -e', sending " - f"SIGTERM to subprocess (pid={subproc_pid}, pgid={subproc_pgid})"), - (LoggerMock.WARNING, - "Command 'tests/system_cmd_test_script.sh 0 -e' has not terminated after " - f"1 seconds, sending SIGKILL to subprocess " - f"(pid={subproc_pid}, pgid={subproc_pgid})")], - self._log.get_history()) - - -class TestInterfaceDependencies(BaseTestCase): - - _AUTO = ["enp0s3", "enp0s3:1-9", "enp0s8", "enp0s8:2-13", "enp0s8:3-15", - "datavlan300", "datavlan300:6-22", "enp0s9", "enp0s10", "bond0", - "bond0:4-12", "vlan200", "vlan200:5-19"] - - _BASE_CFG = { - "interfaces": { - "auto": _AUTO, - "enp0s3": {}, - "enp0s3:1-9": {"address": "12.12.15.67/24", "gateway": "12.12.15.1"}, - "enp0s8": {}, - "enp0s8:2-13": {"address": "192.168.204.2/24"}, - "enp0s8:3-15": {"address": "192.168.206.2/24"}, - "datavlan300": {"raw_dev": "enp0s8", "vlan_id": 300}, - "datavlan300:6-22": {"address": "adad:efef::44:55:66/64", - "raw_dev": "enp0s8", "vlan_id": 300}, - "enp0s9": {"master": "bond0"}, - "enp0s10": {"master": "bond0"}, - "bond0": {"slaves": ["enp0s9", "enp0s10"], "hwaddress": "08:00:27:f2:66:72"}, - "bond0:4-12": {"address": "11.22.3.15/24", "slaves": ["enp0s9", "enp0s10"], - "hwaddress": "08:00:27:f2:66:72"}, - "vlan200": {"raw_dev": "bond0"}, - "vlan200:5-19": {"address": "dead:beef::1:2:3/64", "raw_dev": "bond0"}} - } - - _MODIFIED_CFG = { - "auto": _AUTO, - "enp0s3": {"mtu": 9000}, - "enp0s3:1-9": {"mtu": 9000, "address": "12.12.15.67/24", "gateway": "12.12.15.1"}, - "enp0s8": {"mtu": 9000}, - "enp0s8:2-13": {"mtu": 9000, "address": "192.168.204.2/24"}, - "enp0s8:3-15": {"mtu": 9000, "address": "192.168.206.2/24"}, - "datavlan300": {"mtu": 9000, "raw_dev": "enp0s8", "vlan_id": 300}, - "datavlan300:6-22": {"mtu": 9000, "address": "adad:efef::44:55:66/64", - "raw_dev": "enp0s8", "vlan_id": 300}, - "enp0s9": {"mtu": 9000, "master": "bond0"}, - "enp0s10": {"mtu": 9000, "master": "bond0"}, - "bond0": {"mtu": 9000, "slaves": ["enp0s9", "enp0s10"], "hwaddress": "08:00:27:f2:66:72"}, - "bond0:4-12": {"mtu": 9000, "address": "11.22.3.15/24", "slaves": ["enp0s9", "enp0s10"], - "hwaddress": "08:00:27:f2:66:72"}, - "vlan200": {"mtu": 9000, "raw_dev": "bond0"}, - "vlan200:5-19": {"mtu": 9000, "address": "dead:beef::1:2:3/64", "raw_dev": "bond0"} - } - - _STATIC_LINKS = ["lo", "enp0s3", "enp0s8", "enp0s9", "enp0s10"] - - _FS = ReadOnlyFileContainer(FILE_GEN.generate_file_tree(_BASE_CFG, _BASE_CFG)) - - _MODIFIED_FILES = {k: FILE_GEN.generate_ifcfg_file(k, v) - for k, v in _MODIFIED_CFG.items() if k != "auto"} - - def _setup_scenario(self, modified_ifaces): - contents = dict() - for iface in modified_ifaces: - path = anc.ETC_DIR + "/ifcfg-" + iface - contents[path] = self._MODIFIED_FILES[iface] - self._fs = FilesystemMock(fs=self._FS, contents=contents) - self._add_nw_mock(self._STATIC_LINKS) - self._add_scmd_mock() - self._add_logger_mock() - self._nwmock.apply_auto() - - def _run_update_interfaces(self): - self._mocked_call([self._mock_fs, self._mock_syscmd, - self._mock_sysinv_lock, self._mock_logger], anc.update_interfaces) - - def test_modify_label(self): - self._setup_scenario(["enp0s3:1-9"]) - self._run_update_interfaces() - self.assertEqual([("ifdown", "enp0s3:1-9"), - ("ifup", "enp0s3:1-9")], - self._nwmock.get_history()) - - def test_modify_eth_with_label(self): - self._setup_scenario(["enp0s3"]) - self._run_update_interfaces() - self.assertEqual([('ifdown', 'enp0s3:1-9'), - ('ifdown', 'enp0s3'), - ('ip_link_set_down', 'enp0s3'), - ('ip_addr_flush', 'enp0s3'), - ('ifup', 'enp0s3'), - ('ifup', 'enp0s3:1-9')], - self._nwmock.get_history()) - - def test_modify_vlan_over_eth(self): - self._setup_scenario(["datavlan300"]) - self._run_update_interfaces() - self.assertEqual([("ifdown", "datavlan300:6-22"), - ("ifdown", "datavlan300"), - ("ifup", "datavlan300"), - ("ifup", "datavlan300:6-22")], - self._nwmock.get_history()) - - def test_modify_vlan_over_bonding(self): - self._setup_scenario(["vlan200"]) - self._run_update_interfaces() - self.assertEqual([("ifdown", "vlan200:5-19"), - ("ifdown", "vlan200"), - ("ifup", "vlan200"), - ("ifup", "vlan200:5-19")], - self._nwmock.get_history()) - - def test_modify_eth_with_vlan(self): - self._setup_scenario(["enp0s8"]) - self._run_update_interfaces() - self.assertEqual([('ifdown', 'datavlan300:6-22'), - ('ifdown', 'enp0s8:2-13'), - ('ifdown', 'enp0s8:3-15'), - ('ifdown', 'datavlan300'), - ('ifdown', 'enp0s8'), - ('ip_link_set_down', 'enp0s8'), - ('ip_addr_flush', 'enp0s8'), - ('ifup', 'enp0s8'), - ('ifup', 'datavlan300'), - ('ifup', 'datavlan300:6-22'), - ('ifup', 'enp0s8:2-13'), - ('ifup', 'enp0s8:3-15')], - self._nwmock.get_history()) - - def test_modify_bonding(self): - self._setup_scenario(["bond0"]) - self._run_update_interfaces() - self.assertEqual([('ifdown', 'bond0:4-12'), - ('ifdown', 'vlan200:5-19'), - ('ifdown', 'vlan200'), - ('ifdown', 'bond0'), - ('ifup', 'bond0'), - ('ifup', 'vlan200'), - ('ifup', 'bond0:4-12'), - ('ifup', 'vlan200:5-19')], - self._nwmock.get_history()) - - def test_modify_slave(self): - self._setup_scenario(["enp0s9"]) - self._run_update_interfaces() - self.assertEqual([('ifdown', 'bond0:4-12'), - ('ifdown', 'vlan200:5-19'), - ('ifdown', 'vlan200'), - ('ifdown', 'bond0'), - ('ifup', 'bond0'), - ('ifup', 'vlan200'), - ('ifup', 'bond0:4-12'), - ('ifup', 'vlan200:5-19')], - self._nwmock.get_history()) - - -class MigrationBaseTestCase(BaseTestCase): - def _setup_scenario(self, from_cfg, to_cfg, static_links): - self._add_fs_mock(FILE_GEN.generate_file_tree(to_cfg, from_cfg)) - self._add_nw_mock(static_links) - self._add_scmd_mock() - self._add_logger_mock() - self._nwmock.apply_auto() - - def _run_apply_config(self): - self._mocked_call([self._mock_fs, self._mock_syscmd, - self._mock_sysinv_lock, self._mock_logger], anc.apply_config, False) - - def _check_etc_file_list(self, to_cfg): - files = self._fs.get_file_list(anc.ETC_DIR) - etc_ifaces = [] - has_auto = False - for file in files: - if file.startswith("ifcfg-"): - etc_ifaces.append(file.split("-", 1)[1]) - elif file == "auto": - has_auto = True - else: - raise Exception(f"Unexpected file in ETC dir: '{file}'") - self.assertEqual(True, has_auto, "'auto' file not present in ETC dir") - self.assertEqual(sorted(to_cfg["interfaces"]["auto"]), etc_ifaces) - - -class TestEthAndLoMigration(MigrationBaseTestCase): - - _LEFT = { - "interfaces": { - "auto": ["enp0s3", "enp0s3:1-1", "enp0s3:1-2", "lo", "lo:2-3", "lo:2-4", - "lo:3-5", "lo:3-6", "enp0s9", "enp0s9:7-11", "enp0s9:7-12"], - "enp0s3": {}, - "enp0s3:1-1": {"address": "10.20.1.2/24", "gateway": "10.20.1.1"}, - "enp0s3:1-2": {"address": "fd00::1:2/64", "gateway": "fd00::1"}, - "lo": {}, - "lo:2-3": {"address": "192.168.204.2/24"}, - "lo:2-4": {"address": "fd01::2/64"}, - "lo:3-5": {"address": "192.168.206.2/24"}, - "lo:3-6": {"address": "fd02::2/64"}, - "enp0s9": {}, - "enp0s9:7-11": {"address": "112.44.202.26/24"}, - "enp0s9:7-12": {"address": "ad60:b00::202:26/64"}, - }, - "routes": [ - {"net": "14.14.1.0/24", "via": "10.20.1.111", "dev": "enp0s3", "metric": 1}, - {"net": "14.14.2.0/24", "via": "192.168.204.111", "dev": "lo", "metric": 1}, - {"net": "14.14.3.0/24", "via": "192.168.206.111", "dev": "lo", "metric": 1}, - {"net": "14.14.4.0/24", "via": "112.44.202.111", "dev": "enp0s9", "metric": 1}], - "routes6": [ - {"net": "fa01:1::/64", "via": "fd00::111", "dev": "enp0s3", "metric": 1}, - {"net": "fa01:2::/64", "via": "fd01::111", "dev": "lo", "metric": 1}, - {"net": "fa01:3::/64", "via": "fd02::111", "dev": "lo", "metric": 1}, - {"net": "fa01:4::/64", "via": "ad60:b00::111", "dev": "enp0s9", "metric": 1}], - } - - _RIGHT = { - "interfaces": { - "auto": ["enp0s9", "enp0s9:1-1", "enp0s9:1-2", "lo", "enp0s8", "enp0s8:2-3", - "enp0s8:2-4", "enp0s8:3-5", "enp0s8:3-6", "enp0s3", "enp0s3:7-11", - "enp0s3:7-12"], - "enp0s9": {}, - "enp0s9:1-1": {"address": "10.20.1.2/24", "gateway": "10.20.1.1"}, - "enp0s9:1-2": {"address": "fd00::1:2/64", "gateway": "fd00::1"}, - "lo": {}, - "enp0s8": {"address": "169.254.202.2/24"}, - "enp0s8:2-3": {"address": "192.168.204.2/24"}, - "enp0s8:2-4": {"address": "fd01::2/64"}, - "enp0s8:3-5": {"address": "192.168.206.2/24"}, - "enp0s8:3-6": {"address": "fd02::2/64"}, - "enp0s3": {}, - "enp0s3:7-11": {"address": "112.44.202.26/24"}, - "enp0s3:7-12": {"address": "ad60:b00::202:26/64"}, - }, - "routes": [ - {"net": "14.14.1.0/24", "via": "10.20.1.111", "dev": "enp0s9", "metric": 1}, - {"net": "14.15.1.0/24", "via": "169.254.202.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.2.0/24", "via": "192.168.204.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.3.0/24", "via": "192.168.206.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.4.0/24", "via": "112.44.202.111", "dev": "enp0s3", "metric": 1}], - "routes6": [ - {"net": "fa01:1::/64", "via": "fd00::111", "dev": "enp0s9", "metric": 1}, - {"net": "fa01:2::/64", "via": "fd01::111", "dev": "enp0s8", "metric": 1}, - {"net": "fa01:3::/64", "via": "fd02::111", "dev": "enp0s8", "metric": 1}, - {"net": "fa01:4::/64", "via": "ad60:b00::111", "dev": "enp0s3", "metric": 1}], - } - - _STATIC_LINKS = ["lo", "enp0s3", "enp0s8", "enp0s9"] - - def test_eth_to_eth_migration_a(self): - self._setup_scenario(self._LEFT, self._RIGHT, self._STATIC_LINKS) - - self._run_apply_config() - - self.assertEqual([ - 'enp0s3 UP 112.44.202.26/24 ad60:b00::202:26/64', - 'enp0s8 UP 169.254.202.2/24 192.168.204.2/24 192.168.206.2/24 fd01::2/64 fd02::2/64', - 'enp0s9 UP 10.20.1.2/24 fd00::1:2/64', - 'lo UP'], - self._nwmock.get_links_status()) - - self.assertEqual(['default via 10.20.1.1 dev enp0s9', - 'default via fd00::1 dev enp0s9 metric 1024', - '14.14.1.0/24 via 10.20.1.111 dev enp0s9 metric 1', - '14.15.1.0/24 via 169.254.202.111 dev enp0s8 metric 1', - '14.14.2.0/24 via 192.168.204.111 dev enp0s8 metric 1', - '14.14.3.0/24 via 192.168.206.111 dev enp0s8 metric 1', - '14.14.4.0/24 via 112.44.202.111 dev enp0s3 metric 1', - 'fa01:1::/64 via fd00::111 dev enp0s9 metric 1', - 'fa01:2::/64 via fd01::111 dev enp0s8 metric 1', - 'fa01:3::/64 via fd02::111 dev enp0s8 metric 1', - 'fa01:4::/64 via ad60:b00::111 dev enp0s3 metric 1'], - self._nwmock.get_routes()) - - self._check_etc_file_list(self._RIGHT) - - def test_eth_to_eth_migration_b(self): - self._setup_scenario(self._RIGHT, self._LEFT, self._STATIC_LINKS) - - self._run_apply_config() - - self.assertEqual([ - 'enp0s3 UP 10.20.1.2/24 fd00::1:2/64', - 'enp0s8 DOWN', - 'enp0s9 UP 112.44.202.26/24 ad60:b00::202:26/64', - 'lo UP 192.168.204.2/24 192.168.206.2/24 fd01::2/64 fd02::2/64'], - self._nwmock.get_links_status()) - - self.assertEqual(['default via 10.20.1.1 dev enp0s3', - 'default via fd00::1 dev enp0s3 metric 1024', - '14.14.1.0/24 via 10.20.1.111 dev enp0s3 metric 1', - '14.14.2.0/24 via 192.168.204.111 dev lo metric 1', - '14.14.3.0/24 via 192.168.206.111 dev lo metric 1', - '14.14.4.0/24 via 112.44.202.111 dev enp0s9 metric 1', - 'fa01:1::/64 via fd00::111 dev enp0s3 metric 1', - 'fa01:2::/64 via fd01::111 dev lo metric 1', - 'fa01:3::/64 via fd02::111 dev lo metric 1', - 'fa01:4::/64 via ad60:b00::111 dev enp0s9 metric 1'], - self._nwmock.get_routes()) - - self._check_etc_file_list(self._LEFT) - - -class TestEthToVLANMigration(MigrationBaseTestCase): - - _LEFT = { - "interfaces": { - "auto": ["enp0s3", "enp0s3:1-1", "enp0s3:1-2", "enp0s8", - "enp0s8:2-3", "enp0s8:2-4", "enp0s8:3-5", "enp0s8:3-6"], - "enp0s3": {}, - "enp0s3:1-1": {"address": "10.20.1.2/24", "gateway": "10.20.1.1"}, - "enp0s3:1-2": {"address": "fd00::1:2/64", "gateway": "fd00::1"}, - "enp0s8": {"address": "169.254.202.2/24"}, - "enp0s8:2-3": {"address": "192.168.204.2/24"}, - "enp0s8:2-4": {"address": "fd01::2/64"}, - "enp0s8:3-5": {"address": "192.168.206.2/24"}, - "enp0s8:3-6": {"address": "fd02::2/64"}}, - "routes": [ - {"net": "14.14.1.0/24", "via": "10.20.1.111", "dev": "enp0s3", "metric": 1}, - {"net": "14.15.1.0/24", "via": "169.254.202.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.2.0/24", "via": "192.168.204.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.3.0/24", "via": "192.168.206.111", "dev": "enp0s8", "metric": 1}], - "routes6": [ - {"net": "fa01:1::/64", "via": "fd00::111", "dev": "enp0s3", "metric": 1}, - {"net": "fa01:2::/64", "via": "fd01::111", "dev": "enp0s8", "metric": 1}, - {"net": "fa01:3::/64", "via": "fd02::111", "dev": "enp0s8", "metric": 1}], - } - - _RIGHT = { - "interfaces": { - "auto": ["enp0s3", "enp0s3:1-1", "enp0s3:1-2", "enp0s8", "vlan100", "vlan200", - "vlan100:2-3", "vlan100:2-4", "vlan200:3-5", "vlan200:3-6"], - "enp0s3": {}, - "enp0s3:1-1": {"address": "10.20.1.2/24", "gateway": "10.20.1.1"}, - "enp0s3:1-2": {"address": "fd00::1:2/64", "gateway": "fd00::1"}, - "enp0s8": {"address": "169.254.202.2/24"}, - "vlan100": {"raw_dev": "enp0s8"}, - "vlan100:2-3": {"address": "192.168.204.2/24", "raw_dev": "enp0s8"}, - "vlan100:2-4": {"address": "fd01::2/64", "raw_dev": "enp0s8"}, - "vlan200": {"raw_dev": "enp0s8"}, - "vlan200:3-5": {"address": "192.168.206.2/24", "raw_dev": "enp0s8"}, - "vlan200:3-6": {"address": "fd02::2/64", "raw_dev": "enp0s8"}}, - "routes": [ - {"net": "14.14.1.0/24", "via": "10.20.1.111", "dev": "oam0", "metric": 1}, - {"net": "14.15.1.0/24", "via": "169.254.202.111", "dev": "pxeboot0", "metric": 1}, - {"net": "14.14.2.0/24", "via": "192.168.204.111", "dev": "vlan100", "metric": 1}, - {"net": "14.14.3.0/24", "via": "192.168.206.111", "dev": "vlan200", "metric": 1}], - "routes6": [ - {"net": "fa01:1::/64", "via": "fd00::111", "dev": "oam0", "metric": 1}, - {"net": "fa01:2::/64", "via": "fd01::111", "dev": "vlan100", "metric": 1}, - {"net": "fa01:3::/64", "via": "fd02::111", "dev": "vlan200", "metric": 1}], - } - - _STATIC_LINKS = ["enp0s3", "enp0s8"] - - def test_eth_to_vlan_migration(self): - self._setup_scenario(self._LEFT, self._RIGHT, self._STATIC_LINKS) - - self._run_apply_config() - - self.assertEqual(['enp0s3 UP 10.20.1.2/24 fd00::1:2/64', - 'enp0s8 UP 169.254.202.2/24', - 'vlan100 UP VLAN(enp0s8,100) 192.168.204.2/24 fd01::2/64', - 'vlan200 UP VLAN(enp0s8,200) 192.168.206.2/24 fd02::2/64'], - self._nwmock.get_links_status()) - - self.assertEqual(['default via 10.20.1.1 dev enp0s3', - 'default via fd00::1 dev enp0s3 metric 1024', - '14.14.2.0/24 via 192.168.204.111 dev vlan100 metric 1', - '14.14.3.0/24 via 192.168.206.111 dev vlan200 metric 1', - 'fa01:2::/64 via fd01::111 dev vlan100 metric 1', - 'fa01:3::/64 via fd02::111 dev vlan200 metric 1'], - self._nwmock.get_routes()) - - self._check_etc_file_list(self._RIGHT) - - def test_vlan_to_eth_migration(self): - self._setup_scenario(self._RIGHT, self._LEFT, self._STATIC_LINKS) - - self._run_apply_config() - - self.assertEqual(['enp0s3 UP 10.20.1.2/24 fd00::1:2/64', - 'enp0s8 UP 169.254.202.2/24 192.168.204.2/24 192.168.206.2/24 fd01::2/64 ' - 'fd02::2/64'], - self._nwmock.get_links_status()) - - self.assertEqual(['default via 10.20.1.1 dev enp0s3', - 'default via fd00::1 dev enp0s3 metric 1024', - '14.14.1.0/24 via 10.20.1.111 dev enp0s3 metric 1', - '14.15.1.0/24 via 169.254.202.111 dev enp0s8 metric 1', - '14.14.2.0/24 via 192.168.204.111 dev enp0s8 metric 1', - '14.14.3.0/24 via 192.168.206.111 dev enp0s8 metric 1', - 'fa01:1::/64 via fd00::111 dev enp0s3 metric 1', - 'fa01:2::/64 via fd01::111 dev enp0s8 metric 1', - 'fa01:3::/64 via fd02::111 dev enp0s8 metric 1'], - self._nwmock.get_routes()) - - self._check_etc_file_list(self._LEFT) - - -class TestEthToBondingMigration(MigrationBaseTestCase): - - _LEFT = { - "interfaces": { - "auto": ["enp0s3", "enp0s3:1-1", "enp0s3:1-2", "enp0s8", - "enp0s8:2-3", "enp0s8:2-4", "enp0s8:3-5", "enp0s8:3-6"], - "enp0s3": {}, - "enp0s3:1-1": {"address": "10.20.1.2/24", "gateway": "10.20.1.1"}, - "enp0s3:1-2": {"address": "fd00::1:2/64", "gateway": "fd00::1"}, - "enp0s8": {"address": "169.254.202.2/24"}, - "enp0s8:2-3": {"address": "192.168.204.2/24"}, - "enp0s8:2-4": {"address": "fd01::2/64"}, - "enp0s8:3-5": {"address": "192.168.206.2/24"}, - "enp0s8:3-6": {"address": "fd02::2/64"}}, - "routes": [ - {"net": "14.14.1.0/24", "via": "10.20.1.111", "dev": "enp0s3", "metric": 1}, - {"net": "14.15.1.0/24", "via": "169.254.202.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.2.0/24", "via": "192.168.204.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.3.0/24", "via": "192.168.206.111", "dev": "enp0s8", "metric": 1}], - "routes6": [ - {"net": "fa01:1::/64", "via": "fd00::111", "dev": "enp0s3", "metric": 1}, - {"net": "fa01:2::/64", "via": "fd01::111", "dev": "enp0s8", "metric": 1}, - {"net": "fa01:3::/64", "via": "fd02::111", "dev": "enp0s8", "metric": 1}], - } - - _RIGHT = { - "interfaces": { - "auto": ["enp0s3", "enp0s8", "oam0", "oam0:1-1", "oam0:1-2", - "enp0s9", "enp0s10", "pxeboot0", "vlan100", "vlan100:2-3", - "vlan100:2-4", "vlan200", "vlan200:3-5", "vlan200:3-6"], - "enp0s3": {"master": "oam0"}, - "enp0s8": {"master": "oam0"}, - "oam0": {"slaves": ["enp0s3", "enp0s8"], "hwaddress": "08:00:27:f2:66:72"}, - "oam0:1-1": {"address": "10.20.1.2/24", "gateway": "10.20.1.1", - "slaves": ["enp0s3", "enp0s8"], "hwaddress": "08:00:27:f2:66:72"}, - "oam0:1-2": {"address": "fd00::1:2/64", "gateway": "fd00::1", - "slaves": ["enp0s3", "enp0s8"], "hwaddress": "08:00:27:f2:66:72"}, - "enp0s9": {"master": "pxeboot0"}, - "enp0s10": {"master": "pxeboot0"}, - "pxeboot0": {"address": "169.254.202.2/24", "slaves": ["enp0s9", "enp0s10"], - "hwaddress": "08:00:27:f2:67:11"}, - "vlan100": {"raw_dev": "pxeboot0"}, - "vlan100:2-3": {"address": "192.168.204.2/24", "raw_dev": "pxeboot0"}, - "vlan100:2-4": {"address": "fd01::2/64", "raw_dev": "pxeboot0"}, - "vlan200": {"raw_dev": "pxeboot0"}, - "vlan200:3-5": {"address": "192.168.206.2/24", "raw_dev": "pxeboot0"}, - "vlan200:3-6": {"address": "fd02::2/64", "raw_dev": "pxeboot0"}}, - "routes": [ - {"net": "14.14.1.0/24", "via": "10.20.1.111", "dev": "oam0", "metric": 1}, - {"net": "14.15.1.0/24", "via": "169.254.202.111", "dev": "pxeboot0", "metric": 1}, - {"net": "14.14.2.0/24", "via": "192.168.204.111", "dev": "vlan100", "metric": 1}, - {"net": "14.14.3.0/24", "via": "192.168.206.111", "dev": "vlan200", "metric": 1}], - "routes6": [ - {"net": "fa01:1::/64", "via": "fd00::111", "dev": "oam0", "metric": 1}, - {"net": "fa01:2::/64", "via": "fd01::111", "dev": "vlan100", "metric": 1}, - {"net": "fa01:3::/64", "via": "fd02::111", "dev": "vlan200", "metric": 1}], - } - - _STATIC_LINKS = ["enp0s3", "enp0s8", "enp0s9", "enp0s10"] - - def test_eth_to_bonding_migration(self): - self._setup_scenario(self._LEFT, self._RIGHT, self._STATIC_LINKS) - - self._run_apply_config() - - self.assertEqual(['enp0s10 UP SLAVE(pxeboot0)', - 'enp0s3 UP SLAVE(oam0)', - 'enp0s8 UP SLAVE(oam0)', - 'enp0s9 UP SLAVE(pxeboot0)', - 'oam0 UP BONDING(enp0s3,enp0s8) 10.20.1.2/24 fd00::1:2/64', - 'pxeboot0 UP BONDING(enp0s9,enp0s10) 169.254.202.2/24', - 'vlan100 UP VLAN(pxeboot0,100) 192.168.204.2/24 fd01::2/64', - 'vlan200 UP VLAN(pxeboot0,200) 192.168.206.2/24 fd02::2/64'], - self._nwmock.get_links_status()) - - self.assertEqual(['default via 10.20.1.1 dev oam0', - 'default via fd00::1 dev oam0 metric 1024', - '14.14.1.0/24 via 10.20.1.111 dev oam0 metric 1', - '14.15.1.0/24 via 169.254.202.111 dev pxeboot0 metric 1', - '14.14.2.0/24 via 192.168.204.111 dev vlan100 metric 1', - '14.14.3.0/24 via 192.168.206.111 dev vlan200 metric 1', - 'fa01:1::/64 via fd00::111 dev oam0 metric 1', - 'fa01:2::/64 via fd01::111 dev vlan100 metric 1', - 'fa01:3::/64 via fd02::111 dev vlan200 metric 1'], - self._nwmock.get_routes()) - - self._check_etc_file_list(self._RIGHT) - - def test_bonding_to_eth_migration(self): - self._setup_scenario(self._RIGHT, self._LEFT, self._STATIC_LINKS) - - self._run_apply_config() - - self.assertEqual(['enp0s10 DOWN', - 'enp0s3 UP 10.20.1.2/24 fd00::1:2/64', - 'enp0s8 UP 169.254.202.2/24 192.168.204.2/24 192.168.206.2/24 ' - 'fd01::2/64 fd02::2/64', - 'enp0s9 DOWN'], - self._nwmock.get_links_status()) - - self.assertEqual(['default via 10.20.1.1 dev enp0s3', - 'default via fd00::1 dev enp0s3 metric 1024', - '14.14.1.0/24 via 10.20.1.111 dev enp0s3 metric 1', - '14.15.1.0/24 via 169.254.202.111 dev enp0s8 metric 1', - '14.14.2.0/24 via 192.168.204.111 dev enp0s8 metric 1', - '14.14.3.0/24 via 192.168.206.111 dev enp0s8 metric 1', - 'fa01:1::/64 via fd00::111 dev enp0s3 metric 1', - 'fa01:2::/64 via fd01::111 dev enp0s8 metric 1', - 'fa01:3::/64 via fd02::111 dev enp0s8 metric 1'], - self._nwmock.get_routes()) - - self._check_etc_file_list(self._LEFT) - - -class TestBondingMigration(MigrationBaseTestCase): - _LEFT = { - "interfaces": { - "auto": ["enp0s3", "enp0s3:1-1", "enp0s3:1-2", "enp0s8", "enp0s8:2-3", "enp0s8:2-4", - "enp0s8:3-5", "enp0s8:3-6", "enp0s9", "enp0s10", "data0", "data0:4-7", - "data0:4-8", "data1", "data1:5-9", "data1:5-10"], - "enp0s3": {}, - "enp0s3:1-1": {"address": "10.20.1.2/24", "gateway": "10.20.1.1"}, - "enp0s3:1-2": {"address": "fd00::1:2/64", "gateway": "fd00::1"}, - "enp0s8": {"address": "169.254.202.2/24"}, - "enp0s8:2-3": {"address": "192.168.204.2/24"}, - "enp0s8:2-4": {"address": "fd01::2/64"}, - "enp0s8:3-5": {"address": "192.168.206.2/24"}, - "enp0s8:3-6": {"address": "fd02::2/64"}, - "enp0s9": {"master": "data0"}, - "enp0s10": {"master": "data0"}, - "data0": {"slaves": ["enp0s9", "enp0s10"], "hwaddress": "08:00:27:f2:66:72"}, - "data0:4-7": {"address": "112.154.1.2/24", "slaves": ["enp0s9", "enp0s10"], - "hwaddress": "08:00:27:f2:66:72"}, - "data0:4-8": {"address": "fc01:154:1::2/64", "slaves": ["enp0s9", "enp0s10"], - "hwaddress": "08:00:27:f2:66:72"}, - "data1": {"raw_dev": "data0", "vlan_id": 50}, - "data1:5-9": {"address": "112.155.1.2/24", "raw_dev": "data0", "vlan_id": 50}, - "data1:5-10": {"address": "fc01:155:1::2/64", "raw_dev": "data0", "vlan_id": 50}}, - "routes": [ - {"net": "14.14.1.0/24", "via": "10.20.1.111", "dev": "enp0s3", "metric": 1}, - {"net": "14.15.1.0/24", "via": "169.254.202.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.2.0/24", "via": "192.168.204.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.3.0/24", "via": "192.168.206.111", "dev": "enp0s8", "metric": 1}, - {"net": "14.14.4.0/24", "via": "112.154.1.111", "dev": "data0", "metric": 1}, - {"net": "14.14.5.0/24", "via": "112.155.1.111", "dev": "data1", "metric": 1}, - ], - "routes6": [ - {"net": "fa01:1::/64", "via": "fd00::111", "dev": "enp0s3", "metric": 1}, - {"net": "fa01:2::/64", "via": "fd01::111", "dev": "enp0s8", "metric": 1}, - {"net": "fa01:3::/64", "via": "fd02::111", "dev": "enp0s8", "metric": 1}, - {"net": "fa01:4::/64", "via": "fc01:154:1::111", "dev": "data0", "metric": 1}, - {"net": "fa01:5::/64", "via": "fc01:155:1::111", "dev": "data1", "metric": 1}, - ], - } - - _RIGHT = { - "interfaces": { - "auto": ["enp0s3", "enp0s3:1-1", "enp0s3:1-2", "enp0s8", "enp0s10:2-3", "enp0s10:2-4", - "enp0s10:3-5", "enp0s10:3-6", "enp0s9", "enp0s10", "data0", "data0:4-7", - "data0:4-8", "data1", "data1:5-9", "data1:5-10"], - "enp0s3": {}, - "enp0s3:1-1": {"address": "10.20.1.2/24", "gateway": "10.20.1.1"}, - "enp0s3:1-2": {"address": "fd00::1:2/64", "gateway": "fd00::1"}, - "enp0s8": {"master": "data0"}, - "enp0s9": {"master": "data0"}, - "enp0s10": {"address": "169.254.202.2/24"}, - "enp0s10:2-3": {"address": "192.168.204.2/24"}, - "enp0s10:2-4": {"address": "fd01::2/64"}, - "enp0s10:3-5": {"address": "192.168.206.2/24"}, - "enp0s10:3-6": {"address": "fd02::2/64"}, - "data0": {"slaves": ["enp0s8", "enp0s9"], "hwaddress": "08:00:27:f2:66:72"}, - "data0:4-7": {"address": "112.154.1.2/24", "slaves": ["enp0s8", "enp0s9"], - "hwaddress": "08:00:27:f2:66:72"}, - "data0:4-8": {"address": "fc01:154:1::2/64", "slaves": ["enp0s8", "enp0s9"], - "hwaddress": "08:00:27:f2:66:72"}, - "data1": {"raw_dev": "data0", "vlan_id": 50}, - "data1:5-9": {"address": "112.155.1.2/24", "raw_dev": "data0", "vlan_id": 50}, - "data1:5-10": {"address": "fc01:155:1::2/64", "raw_dev": "data0", "vlan_id": 50}}, - "routes": [ - {"net": "14.14.1.0/24", "via": "10.20.1.111", "dev": "enp0s3", "metric": 1}, - {"net": "14.15.1.0/24", "via": "169.254.202.111", "dev": "enp0s10", "metric": 1}, - {"net": "14.14.2.0/24", "via": "192.168.204.111", "dev": "enp0s10", "metric": 1}, - {"net": "14.14.3.0/24", "via": "192.168.206.111", "dev": "enp0s10", "metric": 1}, - {"net": "14.14.4.0/24", "via": "112.154.1.111", "dev": "data0", "metric": 1}, - {"net": "14.14.5.0/24", "via": "112.155.1.111", "dev": "data1", "metric": 1}, - ], - "routes6": [ - {"net": "fa01:1::/64", "via": "fd00::111", "dev": "enp0s3", "metric": 1}, - {"net": "fa01:2::/64", "via": "fd01::111", "dev": "enp0s10", "metric": 1}, - {"net": "fa01:3::/64", "via": "fd02::111", "dev": "enp0s10", "metric": 1}, - {"net": "fa01:4::/64", "via": "fc01:154:1::111", "dev": "data0", "metric": 1}, - {"net": "fa01:5::/64", "via": "fc01:155:1::111", "dev": "data1", "metric": 1}, - ], - } - - _STATIC_LINKS = ["enp0s3", "enp0s8", "enp0s9", "enp0s10"] - - def test_bonding_migration_a(self): - self._setup_scenario(self._LEFT, self._RIGHT, self._STATIC_LINKS) - - self._run_apply_config() - - self.assertEqual(['data0 UP BONDING(enp0s8,enp0s9) 112.154.1.2/24 fc01:154:1::2/64', - 'data1 UP VLAN(data0,50) 112.155.1.2/24 fc01:155:1::2/64', - 'enp0s10 UP 169.254.202.2/24 192.168.204.2/24 192.168.206.2/24 ' - 'fd01::2/64 fd02::2/64', - 'enp0s3 UP 10.20.1.2/24 fd00::1:2/64', - 'enp0s8 UP SLAVE(data0)', - 'enp0s9 UP SLAVE(data0)'], - self._nwmock.get_links_status()) - - self.assertEqual(['default via 10.20.1.1 dev enp0s3', - 'default via fd00::1 dev enp0s3 metric 1024', - '14.14.1.0/24 via 10.20.1.111 dev enp0s3 metric 1', - 'fa01:1::/64 via fd00::111 dev enp0s3 metric 1', - '14.15.1.0/24 via 169.254.202.111 dev enp0s10 metric 1', - '14.14.2.0/24 via 192.168.204.111 dev enp0s10 metric 1', - '14.14.3.0/24 via 192.168.206.111 dev enp0s10 metric 1', - '14.14.4.0/24 via 112.154.1.111 dev data0 metric 1', - '14.14.5.0/24 via 112.155.1.111 dev data1 metric 1', - 'fa01:2::/64 via fd01::111 dev enp0s10 metric 1', - 'fa01:3::/64 via fd02::111 dev enp0s10 metric 1', - 'fa01:4::/64 via fc01:154:1::111 dev data0 metric 1', - 'fa01:5::/64 via fc01:155:1::111 dev data1 metric 1'], - self._nwmock.get_routes()) - - self._check_etc_file_list(self._RIGHT) - - def test_bonding_migration_b(self): - self._setup_scenario(self._RIGHT, self._LEFT, self._STATIC_LINKS) - - self._run_apply_config() - - self.assertEqual(['data0 UP BONDING(enp0s9,enp0s10) 112.154.1.2/24 fc01:154:1::2/64', - 'data1 UP VLAN(data0,50) 112.155.1.2/24 fc01:155:1::2/64', - 'enp0s10 UP SLAVE(data0)', - 'enp0s3 UP 10.20.1.2/24 fd00::1:2/64', - 'enp0s8 UP 169.254.202.2/24 192.168.204.2/24 192.168.206.2/24 ' - 'fd01::2/64 fd02::2/64', - 'enp0s9 UP SLAVE(data0)'], - self._nwmock.get_links_status()) - - self.assertEqual(['default via 10.20.1.1 dev enp0s3', - 'default via fd00::1 dev enp0s3 metric 1024', - '14.14.1.0/24 via 10.20.1.111 dev enp0s3 metric 1', - 'fa01:1::/64 via fd00::111 dev enp0s3 metric 1', - '14.15.1.0/24 via 169.254.202.111 dev enp0s8 metric 1', - '14.14.2.0/24 via 192.168.204.111 dev enp0s8 metric 1', - '14.14.3.0/24 via 192.168.206.111 dev enp0s8 metric 1', - '14.14.4.0/24 via 112.154.1.111 dev data0 metric 1', - '14.14.5.0/24 via 112.155.1.111 dev data1 metric 1', - 'fa01:2::/64 via fd01::111 dev enp0s8 metric 1', - 'fa01:3::/64 via fd02::111 dev enp0s8 metric 1', - 'fa01:4::/64 via fc01:154:1::111 dev data0 metric 1', - 'fa01:5::/64 via fc01:155:1::111 dev data1 metric 1'], - self._nwmock.get_routes()) - - self._check_etc_file_list(self._LEFT) - - -class TestUpgrade(BaseTestCase): - _CFG = { - "interfaces": { - "auto": ["enp0s3", "enp0s3:1-1", "enp0s3:1-2", "enp0s8", "enp0s8:2-3", "enp0s8:2-4"], - "lo": {}, - "enp0s3": {}, - "enp0s3:1-1": {"address": "10.20.1.2/24", "gateway": "10.20.1.1"}, - "enp0s3:1-2": {"address": "fd00::1:2/64", "gateway": "fd00::1"}, - "enp0s8": {"address": "169.254.202.2/24"}, - "enp0s8:2-3": {"address": "192.168.204.2/24"}, - "enp0s8:2-4": {"address": "fd01::2/64"}}, - } - - _MIN_CFG = { - "interfaces": { - "lo": {}, - } - } - - _STATIC_LINKS = ["enp0s3", "enp0s8"] - - def _setup_scenario(self, fs_contents): - self._add_fs_mock(fs_contents) - self._add_nw_mock(self._STATIC_LINKS) - self._add_scmd_mock() - self._add_logger_mock() - self._fs.set_file_contents(anc.UPGRADE_FILE, '') - self._nwmock.apply_auto() - - def _run_update_interfaces(self): - self._mocked_call([self._mock_fs, self._mock_syscmd, - self._mock_sysinv_lock, self._mock_logger], anc.update_interfaces) - - def test_upgrade_no_change(self): - self._setup_scenario(FILE_GEN.generate_file_tree( - etc_files=self._CFG, puppet_files=self._CFG)) - self._run_update_interfaces() - self.assertEqual([ - ('info', 'Upgrade bootstrap is in execution'), - ('info', 'Configuring interface enp0s3'), - ('info', 'Configuring interface enp0s8'), - ('info', 'Configuring interface enp0s3:1-1'), - ('info', "Link already has address '10.20.1.2/24', no need to set label up"), - ('info', 'Adding route: default via 10.20.1.1 dev enp0s3'), - ('info', 'Route already exists, skipping'), - ('info', 'Configuring interface enp0s3:1-2'), - ('info', "Link already has address 'fd00::1:2/64', no need to set label up"), - ('info', 'Adding route: default via fd00::1 dev enp0s3'), - ('info', 'Route already exists, skipping'), - ('info', 'Configuring interface enp0s8:2-3'), - ('info', "Link already has address '192.168.204.2/24', no need to set label up"), - ('info', 'Configuring interface enp0s8:2-4'), - ('info', "Link already has address 'fd01::2/64', no need to set label up")], - self._log.get_history()) - - def test_upgrade_none_configured(self): - self._setup_scenario(FILE_GEN.generate_file_tree( - etc_files=self._MIN_CFG, puppet_files=self._CFG)) - self._run_update_interfaces() - self.assertEqual(['enp0s3 UP 10.20.1.2/24 fd00::1:2/64', - 'enp0s8 UP 169.254.202.2/24 192.168.204.2/24 fd01::2/64'], - self._nwmock.get_links_status()) - self.assertEqual(['default via 10.20.1.1 dev enp0s3', - 'default via fd00::1 dev enp0s3 metric 1024'], - self._nwmock.get_routes()) - self.assertEqual([ - ('info', 'Upgrade bootstrap is in execution'), - ('info', 'Configuring interface enp0s3'), - ('info', "Interface 'enp0s3' is missing or down, flushing IPs and bringing up"), - ('info', 'Bringing enp0s3 up'), - ('info', 'Configuring interface enp0s8'), - ('info', "Interface 'enp0s8' is missing or down, flushing IPs and bringing up"), - ('info', 'Bringing enp0s8 up'), - ('info', 'Configuring interface enp0s3:1-1'), - ('info', 'Bringing enp0s3:1-1 up'), - ('info', 'Configuring interface enp0s3:1-2'), - ('info', 'Bringing enp0s3:1-2 up'), - ('info', 'Configuring interface enp0s8:2-3'), - ('info', 'Bringing enp0s8:2-3 up'), - ('info', 'Configuring interface enp0s8:2-4'), - ('info', 'Bringing enp0s8:2-4 up')], - self._log.get_history()) - - def test_upgrade_already_configured(self): - self._setup_scenario(FILE_GEN.generate_file_tree( - etc_files=self._MIN_CFG, puppet_files=self._CFG)) - self._nwmock.ip_link_set_up("enp0s3") - self._nwmock.ip_addr_add("10.20.1.2/24", "enp0s3") - self._nwmock.ip_addr_add("fd00::1:2/64", "enp0s3") - self._nwmock.ip_route_add("default", "10.20.1.111", "enp0s3", "1") - self._nwmock.ip_link_set_up("enp0s8") - self._nwmock.ip_addr_add("192.168.208.2/24", "enp0s8") - self._run_update_interfaces() - self.assertEqual([ - 'enp0s3 UP 10.20.1.2/24 fd00::1:2/64', - 'enp0s8 UP 169.254.202.2/24 192.168.204.2/24 192.168.208.2/24 fd01::2/64'], - self._nwmock.get_links_status()) - self.assertEqual(['default via 10.20.1.1 dev enp0s3', - 'default via fd00::1 dev enp0s3 metric 1024'], - self._nwmock.get_routes()) - self.assertEqual([ - ('info', 'Upgrade bootstrap is in execution'), - ('info', 'Configuring interface enp0s3'), - ('info', 'Configuring interface enp0s8'), - ('info', 'Adding IP 169.254.202.2/24 to interface enp0s8'), - ('info', 'Configuring interface enp0s3:1-1'), - ('info', "Link already has address '10.20.1.2/24', no need to set label up"), - ('info', 'Adding route: default via 10.20.1.1 dev enp0s3'), - ('info', 'Route to specified network already exists, replacing: default via ' - '10.20.1.111 dev enp0s3 metric 1'), - ('info', 'Configuring interface enp0s3:1-2'), - ('info', "Link already has address 'fd00::1:2/64', no need to set label up"), - ('info', 'Adding route: default via fd00::1 dev enp0s3'), - ('info', 'Configuring interface enp0s8:2-3'), - ('info', 'Bringing enp0s8:2-3 up'), - ('info', 'Configuring interface enp0s8:2-4'), - ('info', 'Bringing enp0s8:2-4 up')], - self._log.get_history()) diff --git a/puppet-manifests/tox.ini b/puppet-manifests/tox.ini index 8f2550882..5345c8efd 100644 --- a/puppet-manifests/tox.ini +++ b/puppet-manifests/tox.ini @@ -10,26 +10,12 @@ # and then run "tox" from this directory. [tox] toxworkdir = /tmp/{env:USER}_puppet-manifests -envlist = py39,puppetlint +envlist = puppetlint skipsdist = True [testenv] recreate = True -[testenv:py39] -basepython = python3.9 -sitepackages = False - -setenv = VIRTUAL_ENV={envdir} - OS_TEST_PATH=./tests - -deps = - -r{toxinidir}/test-requirements.txt - -commands = - stestr run {posargs} - stestr slowest - [testenv:puppetlint] # Note: centos developer env requires ruby-devel # Ubuntu developer env requires ruby-dev diff --git a/pylint.rc b/pylint.rc index 08da8b746..215dcbf3a 100755 --- a/pylint.rc +++ b/pylint.rc @@ -123,10 +123,8 @@ enable=E1603,E1609,E1610,E1602,E1606,E1608,E1607,E1605,E1604,E1601,E1611,W1652, # See "Messages Control" section of # https://pylint.readthedocs.io/en/latest/user_guide # We are disabling (C)onvention -# W0201: attribute-defined-outside-init -# W1202: logging-format-interpolation # W1618: no-absolute-import -disable=C, W0201,W1202,W1618 +disable=C, W1618 [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs diff --git a/test-requirements.txt b/test-requirements.txt index 612ccf7b3..b1fba817e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,5 +4,3 @@ bashate >= 0.2 bandit!=1.6.0,>=1.1.0,<2.0.0;python_version>="3.0" # GPLv2 shellcheck-py;python_version>="3.0" # MIT netaddr >= 0.7.19 -mock>=2.0.0 -testtools>=1.4.0 diff --git a/tox.ini b/tox.ini index 80595b923..37539c1d7 100644 --- a/tox.ini +++ b/tox.ini @@ -141,7 +141,7 @@ description = commands = - flake8 puppet-manifests + flake8 puppet-manifests/src/modules/platform/files [testenv:pylint] basepython = python3 @@ -155,17 +155,11 @@ commands = [flake8] # E123, E125 skipped as they are invalid PEP-8. -# E126 continuation line over-indented for hanging indent -# E127 continuation line over-indented for visual indent -# H104: File contains nothing but comments -# H306: imports not in alphabetical order -# H404: multi line docstring should start without a leading new line +# E501 skipped because some of the code files include templates +# that end up quite wide # H405: multi line docstring summary not separated with an empty line -# W504: line break after binary operator show-source = True -ignore = E123,E125,E126,E127,H104,H306,H404,H405,W504 -# Max line length set to 100 to coincide with opendev's code view width -max-line-length = 100 +ignore = E123,E125,E501,H405,W504 exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,release-tag-* [testenv:bandit]