From 86b3b035b8559ee5dd8da8d4a57a11c041b32882 Mon Sep 17 00:00:00 2001 From: l0crian1 Date: Thu, 28 Aug 2025 15:24:45 -0400 Subject: container: T7186: Add macvlan network type for containers Modified: - interface-definitions/container.xml.in: - Add macvlan network type - Add gateway option - python/vyos/utils/network.py: - Add gen_mac function to generate mac address - smoketest/scripts/cli/test_container.py: - Add test for container network types - src/conf_mode/container.py: - Add support for macvlan network type - Add gateway option --- python/vyos/utils/network.py | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) (limited to 'python') diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 93f780d8b..50f1771e2 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -13,8 +13,10 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . +import hashlib from socket import AF_INET from socket import AF_INET6 +from vyos.utils.process import cmd def _are_same_ip(one, two): from socket import inet_pton @@ -48,6 +50,50 @@ def is_netns_interface(interface, netns): return True return False +def get_host_identity() -> str: + """ + Build a stable host identity string for deterministic MAC generation. + + Combines: + • The system's HardwareUUID (from /sys/class/dmi/id/product_uuid) + • The system hostname + + Both are normalized (lowercase, dashes removed in UUID) and joined with a colon. + + Returns: + str: A string ":", used as part of the host-specific seed when + generating deterministic MAC addresses. + """ + uuid = cmd(f"cat /sys/class/dmi/id/product_uuid").strip().replace("-", "").lower() + host = cmd("hostname").strip().lower() + return f"{uuid}:{host}" + +def gen_mac(name: str, addr: str) -> str: + """ + Generate a deterministic locally-administered MAC address. + + The MAC is derived from: + • Host identity (UUID + hostname) + • Container name + • Concatenated address string (IPv4 and/or IPv6 addresses) + + A SHA-256 digest is computed from the combined string. The first 5 bytes + of the digest are used, prefixed with 0x02 to mark the address as + locally-administered and unicast. + + Args: + name (str): Container name to differentiate MACs. + addr (str): Concatenated list of container addresses (IPv4/IPv6). + + Returns: + str: Deterministic MAC address in standard "xx:xx:xx:xx:xx:xx" format. + """ + ident = get_host_identity() + h = hashlib.sha256(f"{ident}:{name}:{addr}".encode()).hexdigest() + # 0x02 = locally-administered, unicast + b = [0x02] + [int(h[i:i+2], 16) for i in range(0, 10, 2)] # 5 bytes = 40 bits + return ":".join(f"{x:02x}" for x in b) + def get_netns_all() -> list: from json import loads from vyos.utils.process import cmd -- cgit v1.2.3 From 048ff4fab605a1c6ebf5124fe2adedb81788066e Mon Sep 17 00:00:00 2001 From: l0crian1 Date: Thu, 28 Aug 2025 22:05:54 -0400 Subject: container: T7186: Add macvlan network type for containers - Moved gen_mac function outside of 'for network' loop - Cached result of get_host_identity() to prevent multiple calls - Other minor improvements per Copilot suggestions --- python/vyos/utils/network.py | 3 +-- src/conf_mode/container.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'python') diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 50f1771e2..e6b838cdc 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -68,7 +68,7 @@ def get_host_identity() -> str: host = cmd("hostname").strip().lower() return f"{uuid}:{host}" -def gen_mac(name: str, addr: str) -> str: +def gen_mac(name: str, addr: str, ident: str) -> str: """ Generate a deterministic locally-administered MAC address. @@ -88,7 +88,6 @@ def gen_mac(name: str, addr: str) -> str: Returns: str: Deterministic MAC address in standard "xx:xx:xx:xx:xx:xx" format. """ - ident = get_host_identity() h = hashlib.sha256(f"{ident}:{name}:{addr}".encode()).hexdigest() # 0x02 = locally-administered, unicast b = [0x02] + [int(h[i:i+2], 16) for i in range(0, 10, 2)] # 5 bytes = 40 bits diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index b58c722fc..8950f857e 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -37,6 +37,7 @@ from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import run from vyos.utils.network import gen_mac +from vyos.utils.network import get_host_identity from vyos.utils.network import interface_exists from vyos.template import bracketize_ipv6 from vyos.template import inc_ip @@ -343,7 +344,7 @@ def verify(container): return None -def generate_run_arguments(name, container_config): +def generate_run_arguments(name, container_config, host_ident): image = container_config['image'] cpu_quota = container_config['cpu_quota'] memory = container_config['memory'] @@ -469,7 +470,7 @@ def generate_run_arguments(name, container_config): return f'{container_base_cmd} --net host {entrypoint} {image} {command} {command_arguments}'.strip() ip_param = '' - mac_address = '' + addr_info = '' networks = ",".join(container_config['network']) for network in container_config['network']: if 'address' not in container_config['network'][network]: @@ -481,7 +482,8 @@ def generate_run_arguments(name, container_config): ip_param += f' --ip {address}' addr_info = ''.join(container_config['network'][network]['address']) - mac_address = f'--mac-address {gen_mac(name, addr_info)}' + + mac_address = f'--mac-address {gen_mac(name, addr_info, host_ident)}' return f'{container_base_cmd} --no-healthcheck --net {networks} {ip_param} {mac_address} {entrypoint} {image} {command} {command_arguments}'.strip() @@ -501,7 +503,7 @@ def generate(container): net_interface = dict_search('macvlan.parent', type_config) driver = 'macvlan' mode = dict_search('macvlan.mode', type_config) - elif dict_search('bridge', type_config) != None: + elif dict_search('bridge', type_config) is not None: net_interface = f'pod-{network}' driver = 'bridge' else: @@ -560,12 +562,13 @@ def generate(container): render(config_storage, 'container/storage.conf.j2', container) if 'name' in container: + host_ident = get_host_identity() for name, container_config in container['name'].items(): if 'disable' in container_config: continue file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') - run_args = generate_run_arguments(name, container_config) + run_args = generate_run_arguments(name, container_config, host_ident) render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args, }, formater=lambda _: _.replace(""", '"').replace("'", "'")) -- cgit v1.2.3