summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/conf_mode/container.py432
1 files changed, 189 insertions, 243 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 764c26d84..5a6ac216a 100644
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -17,10 +17,8 @@
import os
from decimal import Decimal
-from hashlib import sha256
from ipaddress import ip_address
from ipaddress import ip_network
-from json import dumps as json_write
import psutil
from vyos.base import Warning
@@ -34,9 +32,7 @@ from vyos.container import restart_network
from vyos.utils.configfs import delete_cli_node
from vyos.utils.configfs import add_cli_node
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 cmdl
from vyos.utils.process import run
from vyos.utils.network import gen_mac
@@ -56,7 +52,7 @@ airbag.enable()
config_containers = '/etc/containers/containers.conf'
config_registry = '/etc/containers/registries.conf'
config_storage = '/etc/containers/storage.conf'
-systemd_unit_path = '/run/systemd/system'
+quadlet_unit_path = '/run/containers/systemd'
def _cmdl(command):
@@ -375,212 +371,227 @@ def verify(container):
return None
+def generate_network_options(name, network_config):
+ out = [
+ f'NetworkName={name}',
+ 'Internal=false',
+ 'IPAMDriver=host-local',
+ ]
-def generate_run_arguments(name, container_config, host_ident, network_config):
- image = container_config['image']
- cpu_quota = container_config['cpu_quota']
- memory = container_config['memory']
- shared_memory = container_config['shared_memory']
- restart = container_config['restart']
- log_driver = container_config['log_driver']
+ ifname = f'pod-{name}'
+ driver = 'bridge'
- # Add sysctl options
- sysctl_opt = ''
- if 'sysctl' in container_config and 'parameter' in container_config['sysctl']:
- for k, v in container_config['sysctl']['parameter'].items():
- sysctl_opt += f" --sysctl \"{k}={v['value']}\""
+ type_config = dict_search('type', network_config)
- # Add capability options. Should be in uppercase
- capabilities = ''
- if 'capability' in container_config:
- for cap in container_config['capability']:
- cap = cap.upper().replace('-', '_')
- capabilities += f' --cap-add={cap}'
+ if dict_search('macvlan', type_config):
+ ifname = dict_search('macvlan.parent', type_config)
+ driver = 'macvlan'
+ macvlan_mode = dict_search('macvlan.mode', type_config)
+ out.append(f'Options=mode={macvlan_mode}')
- # Grant root capabilities to the container
- privileged = ''
- if 'privileged' in container_config:
- privileged = '--privileged'
+ out.append(f'Driver={driver}')
+ out.append(f'PodmanArgs=--interface-name={ifname}')
- # Add a host device to the container /dev/x:/dev/x
- device = ''
- if 'device' in container_config:
- for dev, dev_config in container_config['device'].items():
- source_dev = dev_config['source']
- dest_dev = dev_config['destination']
- device += f' --device={source_dev}:{dest_dev}'
+ if 'no_name_server' in network_config:
+ out.append('DisableDNS=true')
- # Check/set environment options "-e foo=bar"
- env_opt = ''
- if 'environment' in container_config:
- for k, v in container_config['environment'].items():
- env_opt += f" --env \"{k}={v['value']}\""
+ mtu = network_config['mtu'] if 'mtu' in network_config else '1500'
+ out.append(f'Options=mtu={mtu}')
- # Check/set label options "--label foo=bar"
- label = ''
- if 'label' in container_config:
- for k, v in container_config['label'].items():
- label += f" --label \"{k}={v['value']}\""
+ ipv6 = False
- hostname = ''
- if 'host_name' in container_config:
- hostname = container_config['host_name']
- hostname = f'--hostname {hostname}'
+ for prefix in network_config['prefix']:
+ v6 = is_ipv6(prefix)
+ gateway4, gateway6 = None, None
+ if 'gateway' in network_config:
+ for gw in network_config['gateway']:
+ if is_ipv6(gw):
+ gateway6 = gw
+ else:
+ gateway4 = gw
- # Publish ports
- port = ''
- if 'port' in container_config:
- protocol = ''
- for portmap in container_config['port']:
- protocol = container_config['port'][portmap]['protocol']
- sport = container_config['port'][portmap]['source']
- dport = container_config['port'][portmap]['destination']
- listen_addresses = container_config['port'][portmap].get('listen_address', [])
+ if v6 and not gateway6:
+ gateway6 = inc_ip(prefix, 1)
+ elif not gateway4:
+ gateway4 = inc_ip(prefix, 1)
- # If listen_addresses is not empty, include them in the publish command
- if listen_addresses:
- for listen_address in listen_addresses:
- port += f' --publish {bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}'
- else:
- # If listen_addresses is empty, just include the standard publish command
- port += f' --publish {sport}:{dport}/{protocol}'
+ out.append(f'Subnet={prefix}')
+ out.append('Gateway=' + (gateway6 if v6 else gateway4))
- # Set uid and gid
- uid = ''
- if 'uid' in container_config:
- uid = container_config['uid']
- if 'gid' in container_config:
- uid += ':' + container_config['gid']
- uid = f'--user {uid}'
+ if v6:
+ ipv6 = True
- # Bind volume
- volume = ''
- if 'volume' in container_config:
- for vol, vol_config in container_config['volume'].items():
- svol = vol_config['source']
- dvol = vol_config['destination']
- mode = vol_config['mode']
- prop = vol_config['propagation']
- volume += f' --volume {svol}:{dvol}:{mode},{prop}'
+ if ipv6:
+ out.append('IPv6=true')
- # Mount tmpfs
- tmpfs = ''
- if 'tmpfs' in container_config:
- for tmpfs_config in container_config['tmpfs'].values():
- dest = tmpfs_config['destination']
- size = tmpfs_config['size']
- tmpfs += f' --mount=type=tmpfs,tmpfs-size={size}M,destination={dest}'
+ return out
- host_pid = ''
- if 'allow_host_pid' in container_config:
- host_pid = '--pid host'
+def generate_quadlet_options(name, container_config, host_ident, network_config):
+ out = [
+ f'ContainerName={name}',
+ f'Image={container_config["image"]}',
+ f'LogDriver={container_config["log_driver"]}',
+ f'PodmanArgs=--memory={container_config["memory"]}m',
+ f'ShmSize={container_config["shared_memory"]}m',
+ f'PodmanArgs=--cpus={container_config["cpu_quota"]}',
+ 'PodmanArgs=--interactive',
+ 'PodmanArgs=--tty',
+ ]
- cgroupns = ''
if 'allow_host_cgroups' in container_config:
- cgroupns = '--cgroupns host'
+ out.append('PodmanArgs=--cgroupns host')
- name_server = []
- if 'name_server' in container_config:
- for ns in container_config['name_server']:
- name_server.append(f'--dns {ns}')
- if name_server:
- name_server = ' '.join(name_server)
- else:
- name_server = ''
+ if 'allow_host_networks' in container_config:
+ out.append('Network=host')
+
+ if 'allow_host_pid' in container_config:
+ out.append('PodmanArgs=--pid host')
- container_base_cmd = f'--detach --interactive --tty --replace {capabilities} {privileged} --cpus {cpu_quota} {sysctl_opt} ' \
- f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} --log-driver={log_driver} ' \
- f'--name {name} {hostname} {device} {port} {name_server} {volume} {tmpfs} {env_opt} {label} {uid} {host_pid} {cgroupns}'
+ if 'capability' in container_config:
+ for cap in container_config['capability']:
+ cap = cap.upper().replace('-', '_')
+ out.append(f'AddCapability={cap}')
+
+ if 'command' in container_config:
+ command = container_config['command'].strip()
+
+ if 'arguments' in container_config:
+ command += ' ' + container_config['arguments'].strip()
+
+ out.append(f'Exec={command}')
+
+ if 'device' in container_config:
+ for dev, dev_config in container_config['device'].items():
+ source_dev = dev_config['source']
+ dest_dev = dev_config['destination']
+ out.append(f'AddDevice={source_dev}:{dest_dev}')
- entrypoint = ''
if 'entrypoint' in container_config:
- # it needs to be json-formatted with single quote on the outside
- entrypoint = json_write(container_config['entrypoint'].split()).replace('"', """)
- entrypoint = f'--entrypoint '{entrypoint}''
+ entrypoint = container_config['entrypoint']
+ out.append(f'Entrypoint={entrypoint}')
+
+ if 'environment' in container_config:
+ for k, v in container_config['environment'].items():
+ out.append(f'Environment={k}={v["value"]}')
- healthcheck = ' --no-healthcheck'
if 'health_check' in container_config:
- healthcheck = ''
if 'command' in container_config['health_check']:
health_cmd = container_config['health_check']['command']
- healthcheck += f' --health-cmd="{health_cmd}"'
+ out.append(f'HealthCmd={health_cmd}')
if 'interval' in container_config['health_check']:
health_int = container_config['health_check']['interval']
if health_int != 'disable':
health_int = f'{health_int}s'
- healthcheck += f' --health-interval={health_int}'
+ out.append(f'HealthInterval={health_int}')
if 'timeout' in container_config['health_check']:
health_to = container_config['health_check']['timeout']
- healthcheck += f' --health-timeout={health_to}s'
+ out.append(f'HealthTimeout={health_to}s')
if 'retry' in container_config['health_check']:
health_rt = container_config['health_check']['retry']
- healthcheck += f' --health-retries={health_rt}'
+ out.append(f'HealthRetries={health_rt}')
+ else:
+ out.append('PodmanArgs=--no-healthcheck')
- command = ''
- if 'command' in container_config:
- command = container_config['command'].strip()
+ if 'host_name' in container_config:
+ hostname = container_config['host_name']
+ out.append(f'HostName={hostname}')
- command_arguments = ''
- if 'arguments' in container_config:
- command_arguments = container_config['arguments'].strip()
+ if 'label' in container_config:
+ for k, v in container_config['label'].items():
+ out.append(f'Label={k}={v["value"]}')
- net = ''
- if 'allow_host_networks' in container_config:
- net = '--net host'
- else:
- ip_param = ''
- addr_info = ''
- network_opts = []
+ if 'name_server' in container_config:
+ for ns in container_config['name_server']:
+ out.append(f'DNS={ns}')
+
+ if 'network' in container_config:
for network in container_config['network']:
- network_name = network
+ addr_info = ''
+ network_opts = []
+
+ if 'address' in container_config['network'][network]:
+ addr_info = ''.join(container_config['network'][network]['address'])
+ for address in container_config['network'][network]['address']:
+ prefix = 'ip' if is_ipv4(address) else 'ip6'
+ network_opts.append(f'{prefix}={address}')
+
+ get_mac = dict_search(f'network.{network}.mac', container_config)
+ if get_mac == 'auto' or get_mac is None:
+ mac_add = gen_mac(name, addr_info, host_ident)
+ else:
+ mac_add = get_mac
+
+ network_opts.append(f'mac={mac_add}')
+
# T7736: give the host-side veth a name that can never collide
# with a VyOS "virtual-ethernet vethN" interface.
type_config = dict_search(f'{network}.type', network_config)
is_macvlan = dict_search('macvlan', type_config) is not None
- net_opt = network
if not is_macvlan:
ifname = get_container_host_ifname(name)
- net_opt += f':host_interface_name={ifname}'
- network_opts.append(net_opt)
+ network_opts.append(f'host_interface_name={ifname}')
- if 'address' not in container_config['network'][network]:
- continue
- for address in container_config['network'][network]['address']:
- if is_ipv6(address):
- ip_param += f' --ip6 {address}'
- else:
- ip_param += f' --ip {address}'
+ opts_str = (':' + ','.join(network_opts)) if network_opts else ''
+ out.append(f'Network=vyos-{network}.network{opts_str}')
+
+ # Replace mac-auto with the generated mac address
+ if get_mac == 'auto':
+ mac_config_path = [
+ 'container',
+ 'name',
+ name,
+ 'network',
+ network,
+ 'mac',
+ ]
- addr_info = ''.join(container_config['network'][network]['address'])
+ delete_cli_node(mac_config_path)
+ add_cli_node(mac_config_path, value=mac_add)
- networks = ' '.join(f'--network {opt}' for opt in network_opts)
+ if 'port' in container_config:
+ protocol = ''
+ for portmap in container_config['port']:
+ protocol = container_config['port'][portmap]['protocol']
+ sport = container_config['port'][portmap]['source']
+ dport = container_config['port'][portmap]['destination']
+ listen_addresses = container_config['port'][portmap].get('listen_address', [])
- get_mac = dict_search(f'network.{network_name}.mac', container_config)
- if get_mac == 'auto' or get_mac is None:
- mac_add = gen_mac(name, addr_info, host_ident)
- else:
- mac_add = get_mac
+ # If listen_addresses is not empty, include them in the publish command
+ if listen_addresses:
+ for listen_address in listen_addresses:
+ out.append(f'PublishPort={bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}')
+ else:
+ # If listen_addresses is empty, just include the standard publish command
+ out.append(f'PublishPort={sport}:{dport}/{protocol}')
- mac_address = f'--mac-address {mac_add}'
+ if 'privileged' in container_config:
+ out.append('PodmanArgs=--privileged')
- # Replace mac-auto with the generated mac address
- if get_mac == 'auto':
- mac_config_path = [
- 'container',
- 'name',
- name,
- 'network',
- network_name,
- 'mac',
- ]
+ if 'sysctl' in container_config and 'parameter' in container_config['sysctl']:
+ for k, v in container_config['sysctl']['parameter'].items():
+ out.append(f'Sysctl={k}={v["value"]}')
- delete_cli_node(mac_config_path)
- add_cli_node(mac_config_path, value=mac_add)
+ if 'tmpfs' in container_config:
+ for tmpfs_config in container_config['tmpfs'].values():
+ dest = tmpfs_config['destination']
+ size = tmpfs_config['size']
+ out.append(f'Mount=type=tmpfs,tmpfs-size={size}M,destination={dest}')
- net = f'{networks} {ip_param} {mac_address}'
+ if 'uid' in container_config:
+ uid = container_config['uid']
+ if 'gid' in container_config:
+ uid += ':' + container_config['gid']
+ out.append(f'User={uid}')
- return f'{container_base_cmd} {healthcheck} {net} {entrypoint} {image} {command} {command_arguments}'.strip()
+ if 'volume' in container_config:
+ for _, vol_config in container_config['volume'].items():
+ svol = vol_config['source']
+ dvol = vol_config['destination']
+ mode = vol_config['mode']
+ prop = vol_config['propagation']
+ out.append(f'Volume={svol}:{dvol}:{mode},{prop}')
+ return out
def generate(container):
# bail out early - looks like removal from running config
@@ -590,71 +601,16 @@ def generate(container):
os.unlink(file)
return None
- 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': driver,
- 'network_interface': net_interface,
- 'subnets': [],
- 'ipv6_enabled': False,
- 'internal': False,
- 'dns_enabled': True,
- 'ipam_options': {
- 'driver': 'host-local'
- },
- 'options': {
- **({'mode': mode} if driver == 'macvlan' else {}),
- 'mtu': '1500'
- }
- }
-
- if 'no_name_server' in network_config:
- tmp['dns_enabled'] = False
-
- if 'mtu' in network_config:
- tmp['options']['mtu'] = network_config['mtu']
-
- for prefix in network_config['prefix']:
- 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))
-
render(config_containers, 'container/containers.conf.j2', container)
render(config_registry, 'container/registries.conf.j2', container)
render(config_storage, 'container/storage.conf.j2', container)
+ if 'network' in container:
+ for network, network_config in container['network'].items():
+ file_path = os.path.join(quadlet_unit_path, f'vyos-{network}.network')
+ opts = generate_network_options(network, network_config)
+ render(file_path, 'container/quadlet-network.j2', {'name': network, 'opts': opts})
+
if 'name' in container:
host_ident = get_host_identity()
network_config = container.get('network', {})
@@ -662,10 +618,10 @@ def generate(container):
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, host_ident, network_config)
- render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args, },
- formatter=lambda _: _.replace(""", '"').replace("'", "'"))
+ file_path = os.path.join(quadlet_unit_path, f'vyos-container-{name}.container')
+ quadlet_opts = generate_quadlet_options(name, container_config, host_ident, network_config)
+ restart = container_config['restart']
+ render(file_path, 'container/quadlet-unit.j2', {'name': name, 'opts': quadlet_opts, 'restart': restart})
return None
@@ -674,21 +630,18 @@ def apply(container):
# Delete old containers if needed. We can't delete running container
# Option "--force" allows to delete containers with any status
if 'container_remove' in container:
- for name in container['container_remove']:
- file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
- call(f'systemctl stop vyos-container-{name}.service')
- if os.path.exists(file_path):
- os.unlink(file_path)
-
- call('systemctl daemon-reload')
+ quadlet_paths = [f'vyos-container-{name}.container' for name in container['container_remove']]
+ run(['podman', 'quadlet', 'rm', '-f'] + quadlet_paths)
# Delete old networks if needed
if 'network_remove' in container:
- for network in container['network_remove']:
- call(f'podman network rm {network} >/dev/null 2>&1')
+ quadlet_paths = [f'vyos-{name}.network' for name in container['network_remove']]
+ run(['podman', 'quadlet', 'rm', '-f'] + quadlet_paths)
+ run(['podman', 'network', 'rm'] + container['network_remove']) # `quadlet rm` does not remove the instance
+
+ run(['systemctl', 'daemon-reload'])
# Add container
- disabled_new = False
if 'name' in container:
for name, container_config in container['name'].items():
image = container_config['image']
@@ -702,18 +655,11 @@ def apply(container):
# check if there is a container by that name running
tmp = _cmdl(['podman', 'ps', '-a', '--format', '{{.Names}}'])
if name in tmp:
- file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
- call(f'systemctl stop vyos-container-{name}.service')
- if os.path.exists(file_path):
- disabled_new = True
- os.unlink(file_path)
+ run(['podman', 'quadlet', 'rm', '-f', f'vyos-container-{name}.container'])
continue
if 'container_restart' in container and name in container['container_restart']:
- cmdl(['systemctl', 'restart', f'vyos-container-{name}.service'])
-
- if disabled_new:
- call('systemctl daemon-reload')
+ cmdl(['systemctl', 'restart', f'vyos-container-{name}'])
# Re-Start network and assign it to given VRF if requested.
restart_network(container)