summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@vyos.io>2025-09-15 11:19:01 +0100
committerGitHub <noreply@github.com>2025-09-15 11:19:01 +0100
commit48b9f84e5ce681946271e159a19013d23e13d43e (patch)
treede933d4fe1783a4afb10b53bfeabb08238a0e61a /src
parent238a9006cc78022ba5e95258c1033ccf8abfb1af (diff)
parent048ff4fab605a1c6ebf5124fe2adedb81788066e (diff)
downloadvyos-1x-48b9f84e5ce681946271e159a19013d23e13d43e.tar.gz
vyos-1x-48b9f84e5ce681946271e159a19013d23e13d43e.zip
Merge pull request #4686 from l0crian1/container-macvlan
container: T7186: Add macvlan network type for containers
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/container.py119
1 files changed, 97 insertions, 22 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 4ec9b8849..8950f857e 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -32,9 +32,12 @@ from vyos.configverify import verify_vrf
from vyos.ifconfig import Interface
from vyos.utils.cpu import get_core_count
from vyos.utils.file import write_file
+from vyos.utils.dict import dict_search
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
@@ -260,22 +263,59 @@ def verify(container):
# Add new network
if 'network' in container:
for network, network_config in container['network'].items():
- v4_prefix = 0
- v6_prefix = 0
+ net_dict = {'ipv4_pfx_len': 0, 'ipv6_pfx_len': 0, 'ipv4_gateway_len': 0, 'ipv6_gateway_len': 0}
+
# If ipv4-prefix not defined for user-defined network
if 'prefix' not in network_config:
raise ConfigError(f'prefix for network "{network}" must be defined!')
for prefix in network_config['prefix']:
if is_ipv4(prefix):
- v4_prefix += 1
+ net_dict['ipv4_pfx_len'] += 1
+ net_dict['ipv4_prefix'] = prefix
elif is_ipv6(prefix):
- v6_prefix += 1
-
- if v4_prefix > 1:
+ net_dict['ipv6_pfx_len'] += 1
+ net_dict['ipv6_prefix'] = prefix
+
+ for gateway in network_config.get('gateway', []):
+ if is_ipv4(gateway):
+ net_dict['ipv4_gateway_len'] += 1
+ net_dict['ipv4_gateway'] = gateway
+ elif is_ipv6(gateway):
+ net_dict['ipv6_gateway_len'] += 1
+ net_dict['ipv6_gateway'] = gateway
+
+ if net_dict['ipv4_pfx_len'] > 1:
raise ConfigError(f'Only one IPv4 prefix can be defined for network "{network}"!')
- if v6_prefix > 1:
+ if net_dict['ipv6_pfx_len'] > 1:
raise ConfigError(f'Only one IPv6 prefix can be defined for network "{network}"!')
+ if net_dict['ipv4_gateway_len'] > 1:
+ raise ConfigError(f'Only one IPv4 gateway can be defined for network "{network}"!')
+ if net_dict['ipv6_gateway_len'] > 1:
+ raise ConfigError(f'Only one IPv6 gateway can be defined for network "{network}"!')
+
+ if net_dict.get('ipv4_prefix') and net_dict.get('ipv4_gateway'):
+ if ip_address(net_dict['ipv4_gateway']) not in ip_network(net_dict['ipv4_prefix']):
+ raise ConfigError(f'IPv4 gateway "{net_dict["ipv4_gateway"]}" is not in the IPv4 prefix "{net_dict["ipv4_prefix"]}"!')
+ if net_dict.get('ipv6_prefix') and net_dict.get('ipv6_gateway'):
+ if ip_address(net_dict['ipv6_gateway']) not in ip_network(net_dict['ipv6_prefix']):
+ raise ConfigError(f'IPv6 gateway "{net_dict["ipv6_gateway"]}" is not in the IPv6 prefix "{net_dict["ipv6_prefix"]}"!')
+ if net_dict.get('ipv4_gateway') and not net_dict.get('ipv4_prefix'):
+ raise ConfigError(f'IPv4 gateway configured but no IPv4 prefix defined for network "{network}"!')
+ if net_dict.get('ipv6_gateway') and not net_dict.get('ipv6_prefix'):
+ raise ConfigError(f'IPv6 gateway configured but no IPv6 prefix defined for network "{network}"!')
+
+ type_config = dict_search('type', network_config)
+ if dict_search('macvlan', type_config):
+ parent = dict_search('macvlan.parent', type_config)
+ if not parent:
+ raise ConfigError(f'MACVLAN networks must have a parent interface!')
+ if not interface_exists(parent):
+ raise ConfigError(f'MACVLAN parent interface "{parent}" does not exist!')
+ if not dict_search('macvlan.mode', type_config):
+ raise ConfigError(f'MACVLAN networks must have a mode configured!')
+ if dict_search('vrf', network_config):
+ raise ConfigError(f'MACVLAN networks do not support direct VRF assignment!')
# Verify VRF exists
verify_vrf(network_config)
@@ -304,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']
@@ -430,6 +470,7 @@ def generate_run_arguments(name, container_config):
return f'{container_base_cmd} --net host {entrypoint} {image} {command} {command_arguments}'.strip()
ip_param = ''
+ addr_info = ''
networks = ",".join(container_config['network'])
for network in container_config['network']:
if 'address' not in container_config['network'][network]:
@@ -440,7 +481,11 @@ def generate_run_arguments(name, container_config):
else:
ip_param += f' --ip {address}'
- return f'{container_base_cmd} --no-healthcheck --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip()
+ addr_info = ''.join(container_config['network'][network]['address'])
+
+ 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()
def generate(container):
@@ -453,11 +498,22 @@ def generate(container):
if 'network' in container:
for network, network_config in container['network'].items():
+ type_config = dict_search('type', network_config)
+ if dict_search('macvlan', type_config):
+ net_interface = dict_search('macvlan.parent', type_config)
+ driver = 'macvlan'
+ mode = dict_search('macvlan.mode', type_config)
+ elif dict_search('bridge', type_config) is not None:
+ net_interface = f'pod-{network}'
+ driver = 'bridge'
+ else:
+ net_interface = f'pod-{network}'
+ driver = 'bridge'
tmp = {
'name': network,
'id': sha256(f'{network}'.encode()).hexdigest(),
- 'driver': 'bridge',
- 'network_interface': f'pod-{network}',
+ 'driver': driver,
+ 'network_interface': net_interface,
'subnets': [],
'ipv6_enabled': False,
'internal': False,
@@ -466,6 +522,7 @@ def generate(container):
'driver': 'host-local'
},
'options': {
+ **({'mode': mode} if driver == 'macvlan' else {}),
'mtu': '1500'
}
}
@@ -477,11 +534,26 @@ def generate(container):
tmp['options']['mtu'] = network_config['mtu']
for prefix in network_config['prefix']:
- net = {'subnet': prefix, 'gateway': inc_ip(prefix, 1)}
- tmp['subnets'].append(net)
+ gateway4, gateway6 = None, None
+ if dict_search('gateway', network_config):
+ for gw in network_config['gateway']:
+ if is_ipv6(gw):
+ gateway6 = gw
+ else:
+ gateway4 = gw
+
+ if is_ipv6(prefix) and not gateway6:
+ gateway6 = inc_ip(prefix, 1)
+ elif not gateway4:
+ gateway4 = inc_ip(prefix, 1)
if is_ipv6(prefix):
tmp['ipv6_enabled'] = True
+ net = {'subnet': prefix, 'gateway': gateway6}
+ else:
+ net = {'subnet': prefix, 'gateway': gateway4}
+
+ tmp['subnets'].append(net)
write_file(f'/etc/containers/networks/{network}.json', json_write(tmp, indent=2))
@@ -490,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("&quot;", '"').replace("&apos;", "'"))
@@ -553,14 +626,16 @@ def apply(container):
# the network interface in advance
if 'network' in container:
for network, network_config in container['network'].items():
- network_name = f'pod-{network}'
- # T5147: Networks are started only as soon as there is a consumer.
- # If only a network is created in the first place, no need to assign
- # it to a VRF as there's no consumer, yet.
- if interface_exists(network_name):
- tmp = Interface(network_name)
- tmp.set_vrf(network_config.get('vrf', ''))
- tmp.add_ipv6_eui64_address('fe80::/64')
+ type_config = dict_search('type', network_config)
+ if not dict_search('macvlan', type_config):
+ network_name = f'pod-{network}'
+ # T5147: Networks are started only as soon as there is a consumer.
+ # If only a network is created in the first place, no need to assign
+ # it to a VRF as there's no consumer, yet.
+ if interface_exists(network_name):
+ tmp = Interface(network_name)
+ tmp.set_vrf(network_config.get('vrf', ''))
+ tmp.add_ipv6_eui64_address('fe80::/64')
return None