stx-puppet/puppet-manifests/tests/test_apply_network_config.py
Lucas Ratusznei Fonseca 1b054d2a90 Revert^2 "Convert apply_network_config.sh to Python and add automated tests"
This reverts commit 850269646f5b4b30f3bf43f65cc0d244c09e0ed5.

Reason for revert: a patch is being applied on top of the original
change (https://review.opendev.org/c/starlingx/stx-puppet/+/938636)
which ensures that the interfaces in the auto file are ordered as
follows: lo -> eths -> bondings -> slaves -> vlans -> labels. This
guarantees that the interfaces are properly initialized at boot,
effectively fixing the issue that caused the change to be reverted in
the first place.

This commit replaces the apply_network_config.sh script by the Python-
coded equivalent apply_network_config.py, and includes automated tests
for it.
The original scripts apply_network_config.sh, network_ifupdown.sh and
network_sysconfig.sh are being marked as obsolete and shall be removed
in the future.
Flake8 test has been changed to include all files in the
puppet-manifests folder, adjustments had to be made to
puppet-update-grub-env.py, k8s_wait_for_endpoints_health.py and
change_k8s_control_plane_params.py for conformance.

Tests:

1. Installation

[PASS] AIO-SX IPv4 full install on virtualBox
[PASS] AIO-DX IPv6 full install on virtualBox
[PASS] AIO-DX IPv4 full install on a physical lab
[PASS] AIO-DX IPv6 full install on a physical lab

2. Network configuration change

For this test, a regular AIO-SX setup on VirtualBox is used with
default settings:

Address pools
  - oam-ipv4: 10.20.6.0/24
  - management-ipv4: 192.168.204.0/24
  - cluster-host-ipv4: 192.168.206.0/24
Interfaces
  - lo (lo, platform): management, cluster-host
  - enp0s3 (oam0, platform): oam
  - enp0s9 (data0, data)
  - enp0s10 (data1, data)

Procedure:
  - Lock host
  - Execute sequence of commands
  - Unlock host
  - Wait for the host to unlock-reboot
  - Check that kernel networking config reflects sysinv config
  - Check that files in /etc/network/ were correctly generated
  - Check that logs in /var/log/user.log are coherent
  - Reboot host
  - Check that interfaces are correctly initialized on boot

[PASS] Test 2.1: Add IPv6 stack, add addresses and routes to data1

system addrpool-add oam-ipv6 fd00:: 64 --ranges fd00::1-fd00::ffff \
    --order random --floating-address fd00::3 --gateway-address fd00::1
system addrpool-add management-ipv6 fd01:: 64 \
    --ranges fd01::1-fd01::ffff --order random \
    --floating-address fd01::1 --controller0-address \
    fd01::2 --controller1-address fd01::3
system addrpool-add cluster-host-ipv6 fd02:: 64 \
    --ranges fd02::1-fd02::ffff --order random --floating-address \
    fd02::1 --controller0-address fd02::2 --controller1-address fd02::3
system network-addrpool-assign oam oam-ipv6
system network-addrpool-assign mgmt management-ipv6
system network-addrpool-assign cluster-host cluster-host-ipv6
system host-if-modify controller-0 data1 --ipv4-mode static \
    --ipv6-mode static
system host-addr-add controller-0 data1 177.201.1.2 24
system host-addr-add controller-0 data1 bd01:201::1:2 64
system host-route-add controller-0 data1 208.166.11.0 24 177.201.1.111
system host-route-add controller-0 data1 af04:11:: 64 bd01:201::111

[PASS] Test 2.2: Move mgmt and cluster-host networks from lo to eth

mgmt_intnet=$(system interface-network-list controller-0 | \
    grep "mgmt" | awk '{print $4}')
clhost_intnet=$(system interface-network-list controller-0 | \
    grep "cluster-host" | awk '{print $4}')
system interface-network-remove $mgmt_intnet
system interface-network-remove $clhost_intnet
system host-if-modify controller-0 enp0s8 -n mgmt0 -c platform
system interface-network-assign controller-0 mgmt0 mgmt
system interface-network-assign controller-0 mgmt0 cluster-host
system host-route-add controller-0 mgmt0 208.166.1.0 24 192.168.204.111
system host-route-add controller-0 mgmt0 af04:1:: 64 fd01::111
system host-route-add controller-0 mgmt0 208.166.2.0 24 192.168.206.111
system host-route-add controller-0 mgmt0 af04:2:: 64 fd02::111

[PASS] Test 2.3: Move mgmt and cluster-host networks to VLANs

mgmt_intnet=$(system interface-network-list controller-0 | \
    grep "mgmt" | awk '{print $4}');
clhost_intnet=$(system interface-network-list controller-0 | \
    grep "cluster-host" | awk '{print $4}');
system interface-network-remove $mgmt_intnet
system interface-network-remove $clhost_intnet
while read i; do uuid=$(echo "$i" | awk '{print $2}'); system \
    host-route-delete "$uuid"; done <<< $(system host-route-list \
    controller-0 | grep "mgmt0");
system host-if-modify controller-0 mgmt0 -n pxeboot0
system host-if-add controller-0 mgmt0 vlan pxeboot0 -V 10 -c platform
system host-if-add controller-0 cluster0 vlan pxeboot0 -V 11 -c platform
system host-if-add controller-0 datavlan1 vlan data1 -V 201 -c data \
    --ipv4-mode static --ipv6-mode static
system interface-network-assign controller-0 pxeboot0 pxeboot
system interface-network-assign controller-0 mgmt0 mgmt
system interface-network-assign controller-0 cluster0 cluster-host
system host-addr-add controller-0 datavlan1 177.202.1.2 24
system host-addr-add controller-0 datavlan1 bd01:202::1:2 64
system host-route-add controller-0 mgmt0 208.166.1.0 24 192.168.204.111
system host-route-add controller-0 mgmt0 af04:1:: 64 fd01::111
system host-route-add controller-0 cluster0 \
    208.166.2.0 24 192.168.206.111
system host-route-add controller-0 cluster0 af04:2:: 64 fd02::111
system host-route-add controller-0 datavlan1 \
    208.166.21.0 24 177.202.1.111
system host-route-add controller-0 datavlan1 af04:21:: 64 bd01:202::111

[PASS] Test 2.4: Move pxeboot, mgmt and cluster-host to a bonding

pxeboot_intnet=$(system interface-network-list controller-0 | \
    grep "pxeboot" | awk '{print $4}')
mgmt_intnet=$(system interface-network-list controller-0 | \
    grep "mgmt" | awk '{print $4}')
clhost_intnet=$(system interface-network-list controller-0 | \
    grep "cluster-host" | awk '{print $4}')
system interface-network-remove $pxeboot_intnet
system interface-network-remove $mgmt_intnet
system interface-network-remove $clhost_intnet
system host-if-delete controller-0 mgmt0
system host-if-delete controller-0 cluster0
system host-if-modify controller-0 pxeboot0 -c none
system host-if-modify controller-0 data0 -c none
system host-if-add controller-0 bond0 ae enp0s8 enp0s9 -c platform
system interface-network-assign controller-0 bond0 mgmt
system interface-network-assign controller-0 bond0 cluster-host
system host-route-add controller-0 bond0 208.166.1.0 24 192.168.204.111
system host-route-add controller-0 bond0 af04:1:: 64 fd01::111
system host-route-add controller-0 bond0 208.166.2.0 24 192.168.206.111
system host-route-add controller-0 bond0 af04:2:: 64 fd02::111

[PASS] Test 2.5: Move mgmt and cluster-host to vlans on top of a bonding

mgmt_intnet=$(system interface-network-list controller-0 | \
    grep "mgmt" | awk '{print $4}')
clhost_intnet=$(system interface-network-list controller-0 | \
    grep "cluster-host" | awk '{print $4}')
system interface-network-remove $mgmt_intnet
system interface-network-remove $clhost_intnet
system host-if-add controller-0 mgmt0 vlan bond0 -V 10 -c platform
system host-if-add controller-0 cluster0 vlan bond0 -V 11 -c platform
system interface-network-assign controller-0 mgmt0 mgmt
system interface-network-assign controller-0 cluster0 cluster-host
while read i; do uuid=$(echo "$i" | awk '{print $2}'); system \
    host-route-delete "$uuid"; done <<< $(system host-route-list \
    controller-0 | grep "bond0");
system host-route-add controller-0 mgmt0 208.166.1.0 24 192.168.204.111
system host-route-add controller-0 mgmt0 af04:1:: 64 fd01::111
system host-route-add controller-0 cluster0 \
    208.166.2.0 24 192.168.206.111
system host-route-add controller-0 cluster0 af04:2:: 64 fd02::111

3. Distributed cloud

[PASS] Subcloud enrollment, OAM over ethernet, same subnet
[PASS] Subcloud enrollment, OAM over VLAN, different VLAN ID,
       different subnet

Story: 2011338
Task: 51635
Change-Id: I0a9e095dcff5c59a0e543b481c8d0856e8485340
Signed-off-by: Lucas Ratusznei Fonseca <lucas.ratuszneifonseca@windriver.com>
2025-03-18 12:50:40 -03:00

3062 lines
139 KiB
Python

#
# 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
def _update_config(self):
if not self._etc_changed:
return
self._etc_changed = False
auto, ifaces = self._decode_config()
self._current_config = {"auto": auto, "ifaces": 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()
auto = [iface for iface in self._current_config["auto"]
if self._current_config["ifaces"][iface]["type"] != anc.SLAVE]
for iface in 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_SAMPLE_CFG = {
"auto": {"enp0s8", "enp0s3:1-3", "lo:3-7", "vlan10", "vlan11:4-8", "lo", "bond0:2-5",
"vlan11", "bond0", "enp0s3", "vlan10:5-11", "enp0s9"},
"ifaces_types": {"enp0s8": anc.SLAVE,
"enp0s3:1-3": anc.LABEL,
"lo:3-7": anc.LABEL,
"vlan10": anc.VLAN,
"vlan11:4-8": anc.LABEL,
"lo": anc.LO,
"bond0:2-5": anc.LABEL,
"vlan11": anc.VLAN,
"bond0": anc.BONDING,
"enp0s3": anc.ETH,
"vlan10:5-11": anc.LABEL,
"enp0s9": anc.SLAVE}
}
_AUTO_FILE = (f"{_HEADER}\n"
"auto lo enp0s3 bond0 enp0s8 enp0s9 vlan10 vlan11 bond0:2-5 enp0s3:1-3 lo:3-7 "
"vlan10:5-11 vlan11:4-8\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_SAMPLE_CFG)
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()
stdout = self._nwmock.apply_auto()
self.assertEqual("", stdout)
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": "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": "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": "enp0s3", "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.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',
'fa01:1::/64 via fd00::111 dev enp0s3 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: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',
'fa01:1::/64 via fd00::111 dev enp0s3 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: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, '')
stdout = self._nwmock.apply_auto()
self.assertEqual("", stdout)
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())