From e6af32344b1647f59543cbc659704bb9ef08be5f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 May 2022 18:43:47 +0200 Subject: container: T4353: fix Jinja2 linting errors --- data/templates/container/registries.conf.j2 | 27 ++ data/templates/container/storage.conf.j2 | 5 + data/templates/containers/registries.conf.j2 | 27 -- data/templates/containers/storage.conf.j2 | 5 - interface-definitions/container.xml.in | 293 ++++++++++++++++++++++ interface-definitions/containers.xml.in | 293 ---------------------- src/conf_mode/container.py | 361 +++++++++++++++++++++++++++ src/conf_mode/containers.py | 361 --------------------------- 8 files changed, 686 insertions(+), 686 deletions(-) create mode 100644 data/templates/container/registries.conf.j2 create mode 100644 data/templates/container/storage.conf.j2 delete mode 100644 data/templates/containers/registries.conf.j2 delete mode 100644 data/templates/containers/storage.conf.j2 create mode 100644 interface-definitions/container.xml.in delete mode 100644 interface-definitions/containers.xml.in create mode 100755 src/conf_mode/container.py delete mode 100755 src/conf_mode/containers.py diff --git a/data/templates/container/registries.conf.j2 b/data/templates/container/registries.conf.j2 new file mode 100644 index 000000000..4057bb452 --- /dev/null +++ b/data/templates/container/registries.conf.j2 @@ -0,0 +1,27 @@ +### Autogenerated by /usr/libexec/vyos/conf_mode/containers.py ### + +# For more information on this configuration file, see containers-registries.conf(5). +# +# NOTE: RISK OF USING UNQUALIFIED IMAGE NAMES +# We recommend always using fully qualified image names including the registry +# server (full dns name), namespace, image name, and tag +# (e.g., registry.redhat.io/ubi8/ubi:latest). Pulling by digest (i.e., +# quay.io/repository/name@digest) further eliminates the ambiguity of tags. +# When using short names, there is always an inherent risk that the image being +# pulled could be spoofed. For example, a user wants to pull an image named +# `foobar` from a registry and expects it to come from myregistry.com. If +# myregistry.com is not first in the search list, an attacker could place a +# different `foobar` image at a registry earlier in the search list. The user +# would accidentally pull and run the attacker's image and code rather than the +# intended content. We recommend only adding registries which are completely +# trusted (i.e., registries which don't allow unknown or anonymous users to +# create accounts with arbitrary names). This will prevent an image from being +# spoofed, squatted or otherwise made insecure. If it is necessary to use one +# of these registries, it should be added at the end of the list. +# +# An array of host[:port] registries to try when pulling an unqualified image, in order. +# unqualified-search-registries = ["example.com"] + +{% if registry is vyos_defined %} +unqualified-search-registries = {{ registry }} +{% endif %} diff --git a/data/templates/container/storage.conf.j2 b/data/templates/container/storage.conf.j2 new file mode 100644 index 000000000..3a69b7252 --- /dev/null +++ b/data/templates/container/storage.conf.j2 @@ -0,0 +1,5 @@ +### Autogenerated by /usr/libexec/vyos/conf_mode/containers.py ### + +[storage] + driver = "vfs" + graphroot = "/config/containers/storage" diff --git a/data/templates/containers/registries.conf.j2 b/data/templates/containers/registries.conf.j2 deleted file mode 100644 index 4057bb452..000000000 --- a/data/templates/containers/registries.conf.j2 +++ /dev/null @@ -1,27 +0,0 @@ -### Autogenerated by /usr/libexec/vyos/conf_mode/containers.py ### - -# For more information on this configuration file, see containers-registries.conf(5). -# -# NOTE: RISK OF USING UNQUALIFIED IMAGE NAMES -# We recommend always using fully qualified image names including the registry -# server (full dns name), namespace, image name, and tag -# (e.g., registry.redhat.io/ubi8/ubi:latest). Pulling by digest (i.e., -# quay.io/repository/name@digest) further eliminates the ambiguity of tags. -# When using short names, there is always an inherent risk that the image being -# pulled could be spoofed. For example, a user wants to pull an image named -# `foobar` from a registry and expects it to come from myregistry.com. If -# myregistry.com is not first in the search list, an attacker could place a -# different `foobar` image at a registry earlier in the search list. The user -# would accidentally pull and run the attacker's image and code rather than the -# intended content. We recommend only adding registries which are completely -# trusted (i.e., registries which don't allow unknown or anonymous users to -# create accounts with arbitrary names). This will prevent an image from being -# spoofed, squatted or otherwise made insecure. If it is necessary to use one -# of these registries, it should be added at the end of the list. -# -# An array of host[:port] registries to try when pulling an unqualified image, in order. -# unqualified-search-registries = ["example.com"] - -{% if registry is vyos_defined %} -unqualified-search-registries = {{ registry }} -{% endif %} diff --git a/data/templates/containers/storage.conf.j2 b/data/templates/containers/storage.conf.j2 deleted file mode 100644 index 3a69b7252..000000000 --- a/data/templates/containers/storage.conf.j2 +++ /dev/null @@ -1,5 +0,0 @@ -### Autogenerated by /usr/libexec/vyos/conf_mode/containers.py ### - -[storage] - driver = "vfs" - graphroot = "/config/containers/storage" diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in new file mode 100644 index 000000000..85231b50c --- /dev/null +++ b/interface-definitions/container.xml.in @@ -0,0 +1,293 @@ + + + + + Container applications + 1280 + + + + + Container name + + [-a-zA-Z0-9]+ + + Container name must be alphanumeric and can contain hyphens + + + + + Allow host networks in container + + + + + + Container capabilities/permissions + + net-admin net-bind-service net-raw setpcap sys-admin sys-time + + + net-admin + Network operations (interface, firewall, routing tables) + + + net-bind-service + Bind a socket to privileged ports (port numbers less than 1024) + + + net-raw + Permission to create raw network sockets + + + setpcap + Capability sets (from bounded or inherited set) + + + sys-admin + Administation operations (quotactl, mount, sethostname, setdomainame) + + + sys-time + Permission to set system clock + + + (net-admin|net-bind-service|net-raw|setpcap|sys-admin|sys-time) + + + + + #include + + + Add a host device to the container + + + + + Source device (Example: "/dev/x") + + txt + Source device + + + + + + Destination container device (Example: "/dev/x") + + txt + Destination container device + + + + + + #include + + + Add custom environment variables + + [-_a-zA-Z0-9]+ + + Environment variable name must be alphanumeric and can contain hyphen and underscores + + + + + Set environment option value + + txt + Set environment option value + + + + + + + + Image name in the hub-registry + + + + + Constrain the memory available to a container + + u32:0 + Unlimited + + + u32:1-16384 + Container memory in megabytes (MB) + + + + + Container memory must be in range 0 to 16384 MB + + 512 + + + + Attach user defined network to container + + container network + + + + + + + Assign static IP address to container + + ipv4 + IPv4 address + + + + + + + + + + + Publish port to the container + + + + + Source host port + + u32:1-65535 + Source host port + + + start-end + Source host port range (e.g. 10025-10030) + + + + + + + + + Destination container port + + u32:1-65535 + Destination container port + + + start-end + Destination container port range (e.g. 10025-10030) + + + + + + + + + Protocol tcp/udp + + tcp udp + + + (tcp|udp) + + + + + + + + Restart options for container + + no on-failure always + + + no + Do not restart containers on exit + + + on-failure + Restart containers when they exit with a non-zero exit code, retrying indefinitely + + + always + Restart containers when they exit, regardless of status, retrying indefinitely + + + (no|on-failure|always) + + + on-failure + + + + Mount a volume into the container + + + + + Source host directory + + txt + Source host directory + + + + + + Destination container directory + + txt + Destination container directory + + + + + + + + + + Network name + + + + + Network description + + + + + Prefix which allocated to that network + + ipv4net + IPv4 network prefix + + + ipv6net + IPv6 network prefix + + + + + + + + + + + + + Registry Name + + + docker.io quay.io + + + + diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in deleted file mode 100644 index 85231b50c..000000000 --- a/interface-definitions/containers.xml.in +++ /dev/null @@ -1,293 +0,0 @@ - - - - - Container applications - 1280 - - - - - Container name - - [-a-zA-Z0-9]+ - - Container name must be alphanumeric and can contain hyphens - - - - - Allow host networks in container - - - - - - Container capabilities/permissions - - net-admin net-bind-service net-raw setpcap sys-admin sys-time - - - net-admin - Network operations (interface, firewall, routing tables) - - - net-bind-service - Bind a socket to privileged ports (port numbers less than 1024) - - - net-raw - Permission to create raw network sockets - - - setpcap - Capability sets (from bounded or inherited set) - - - sys-admin - Administation operations (quotactl, mount, sethostname, setdomainame) - - - sys-time - Permission to set system clock - - - (net-admin|net-bind-service|net-raw|setpcap|sys-admin|sys-time) - - - - - #include - - - Add a host device to the container - - - - - Source device (Example: "/dev/x") - - txt - Source device - - - - - - Destination container device (Example: "/dev/x") - - txt - Destination container device - - - - - - #include - - - Add custom environment variables - - [-_a-zA-Z0-9]+ - - Environment variable name must be alphanumeric and can contain hyphen and underscores - - - - - Set environment option value - - txt - Set environment option value - - - - - - - - Image name in the hub-registry - - - - - Constrain the memory available to a container - - u32:0 - Unlimited - - - u32:1-16384 - Container memory in megabytes (MB) - - - - - Container memory must be in range 0 to 16384 MB - - 512 - - - - Attach user defined network to container - - container network - - - - - - - Assign static IP address to container - - ipv4 - IPv4 address - - - - - - - - - - - Publish port to the container - - - - - Source host port - - u32:1-65535 - Source host port - - - start-end - Source host port range (e.g. 10025-10030) - - - - - - - - - Destination container port - - u32:1-65535 - Destination container port - - - start-end - Destination container port range (e.g. 10025-10030) - - - - - - - - - Protocol tcp/udp - - tcp udp - - - (tcp|udp) - - - - - - - - Restart options for container - - no on-failure always - - - no - Do not restart containers on exit - - - on-failure - Restart containers when they exit with a non-zero exit code, retrying indefinitely - - - always - Restart containers when they exit, regardless of status, retrying indefinitely - - - (no|on-failure|always) - - - on-failure - - - - Mount a volume into the container - - - - - Source host directory - - txt - Source host directory - - - - - - Destination container directory - - txt - Destination container directory - - - - - - - - - - Network name - - - - - Network description - - - - - Prefix which allocated to that network - - ipv4net - IPv4 network prefix - - - ipv6net - IPv6 network prefix - - - - - - - - - - - - - Registry Name - - - docker.io quay.io - - - - diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py new file mode 100755 index 000000000..7e1dc5911 --- /dev/null +++ b/src/conf_mode/container.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import json + +from ipaddress import ip_address +from ipaddress import ip_network +from time import sleep +from json import dumps as json_write + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import node_changed +from vyos.util import call +from vyos.util import cmd +from vyos.util import run +from vyos.util import write_file +from vyos.template import inc_ip +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 +from vyos.template import render +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_containers_registry = '/etc/containers/registries.conf' +config_containers_storage = '/etc/containers/storage.conf' + +def _run_rerun(container_cmd): + counter = 0 + while True: + if counter >= 10: + break + try: + _cmd(container_cmd) + break + except: + counter = counter +1 + sleep(0.5) + + return None + +def _cmd(command): + if os.path.exists('/tmp/vyos.container.debug'): + print(command) + return cmd(command) + +def network_exists(name): + # Check explicit name for network, returns True if network exists + c = _cmd(f'podman network ls --quiet --filter name=^{name}$') + return bool(c) + +# Common functions +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['container'] + container = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + # container base default values can not be merged here - remove and add them later + if 'name' in default_values: + del default_values['name'] + container = dict_merge(default_values, container) + + # Merge per-container default values + if 'name' in container: + default_values = defaults(base + ['name']) + for name in container['name']: + container['name'][name] = dict_merge(default_values, container['name'][name]) + + # Delete container network, delete containers + tmp = node_changed(conf, base + ['container', 'network']) + if tmp: container.update({'network_remove' : tmp}) + + tmp = node_changed(conf, base + ['container', 'name']) + if tmp: container.update({'container_remove' : tmp}) + + return container + +def verify(container): + # bail out early - looks like removal from running config + if not container: + return None + + # Add new container + if 'name' in container: + for name, container_config in container['name'].items(): + # Container image is a mandatory option + if 'image' not in container_config: + raise ConfigError(f'Container image for "{name}" is mandatory!') + + # verify container image exists locally + image = container_config['image'] + + # Check if requested container image exists locally. If it does not + # exist locally - inform the user. + if run(f'podman image exists {image}') != 0: + raise ConfigError(f'Image "{image}" used in contianer "{name}" does not exist '\ + f'locally.\nPlease use "add container image {image}" to add it '\ + 'to the system!') + + if 'network' in container_config: + if len(container_config['network']) > 1: + raise ConfigError(f'Only one network can be specified for container "{name}"!') + + # Check if the specified container network exists + network_name = list(container_config['network'])[0] + if network_name not in container['network']: + raise ConfigError(f'Container network "{network_name}" does not exist!') + + if 'address' in container_config['network'][network_name]: + if 'network' not in container_config: + raise ConfigError(f'Can not use "address" without "network" for container "{name}"!') + + address = container_config['network'][network_name]['address'] + network = None + if is_ipv4(address): + network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0] + elif is_ipv6(address): + network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0] + + # Specified container IP address must belong to network prefix + if ip_address(address) not in ip_network(network): + raise ConfigError(f'Used container address "{address}" not in network "{network}"!') + + # We can not use the first IP address of a network prefix as this is used by podman + if ip_address(address) == ip_network(network)[1]: + raise ConfigError(f'IP address "{address}" can not be used for a container, '\ + 'reserved for the container engine!') + + if 'device' in container_config: + for dev, dev_config in container_config['device'].items(): + if 'source' not in dev_config: + raise ConfigError(f'Device "{dev}" has no source path configured!') + + if 'destination' not in dev_config: + raise ConfigError(f'Device "{dev}" has no destination path configured!') + + source = dev_config['source'] + if not os.path.exists(source): + raise ConfigError(f'Device "{dev}" source path "{source}" does not exist!') + + if 'environment' in container_config: + for var, cfg in container_config['environment'].items(): + if 'value' not in cfg: + raise ConfigError(f'Environment variable {var} has no value assigned!') + + if 'volume' in container_config: + for volume, volume_config in container_config['volume'].items(): + if 'source' not in volume_config: + raise ConfigError(f'Volume "{volume}" has no source path configured!') + + if 'destination' not in volume_config: + raise ConfigError(f'Volume "{volume}" has no destination path configured!') + + source = volume_config['source'] + if not os.path.exists(source): + raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!') + + # If 'allow-host-networks' or 'network' not set. + if 'allow_host_networks' not in container_config and 'network' not in container_config: + raise ConfigError(f'Must either set "network" or "allow-host-networks" for container "{name}"!') + + # Can not set both allow-host-networks and network at the same time + if {'allow_host_networks', 'network'} <= set(container_config): + raise ConfigError(f'"allow-host-networks" and "network" for "{name}" cannot be both configured at the same time!') + + # Add new network + if 'network' in container: + for network, network_config in container['network'].items(): + v4_prefix = 0 + v6_prefix = 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 + elif is_ipv6(prefix): v6_prefix += 1 + + if v4_prefix > 1: + raise ConfigError(f'Only one IPv4 prefix can be defined for network "{network}"!') + if v6_prefix > 1: + raise ConfigError(f'Only one IPv6 prefix can be defined for network "{network}"!') + + + # A network attached to a container can not be deleted + if {'network_remove', 'name'} <= set(container): + for network in container['network_remove']: + for container, container_config in container['name'].items(): + if 'network' in container_config and network in container_config['network']: + raise ConfigError(f'Can not remove network "{network}", used by container "{container}"!') + + return None + +def generate(container): + # bail out early - looks like removal from running config + if not container: + if os.path.exists(config_containers_registry): + os.unlink(config_containers_registry) + if os.path.exists(config_containers_storage): + os.unlink(config_containers_storage) + return None + + if 'network' in container: + for network, network_config in container['network'].items(): + tmp = { + 'cniVersion' : '0.4.0', + 'name' : network, + 'plugins' : [{ + 'type': 'bridge', + 'bridge': f'cni-{network}', + 'isGateway': True, + 'ipMasq': False, + 'hairpinMode': False, + 'ipam' : { + 'type': 'host-local', + 'routes': [], + 'ranges' : [], + }, + }] + } + + for prefix in network_config['prefix']: + net = [{'gateway' : inc_ip(prefix, 1), 'subnet' : prefix}] + tmp['plugins'][0]['ipam']['ranges'].append(net) + + # install per address-family default orutes + default_route = '0.0.0.0/0' + if is_ipv6(prefix): + default_route = '::/0' + tmp['plugins'][0]['ipam']['routes'].append({'dst': default_route}) + + write_file(f'/etc/cni/net.d/{network}.conflist', json_write(tmp, indent=2)) + + render(config_containers_registry, 'container/registries.conf.j2', container) + render(config_containers_storage, 'container/storage.conf.j2', container) + + return None + +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']: + call(f'podman stop {name}') + call(f'podman rm --force {name}') + + # Delete old networks if needed + if 'network_remove' in container: + for network in container['network_remove']: + tmp = f'/etc/cni/net.d/{network}.conflist' + if os.path.exists(tmp): + os.unlink(tmp) + + # Add container + if 'name' in container: + for name, container_config in container['name'].items(): + image = container_config['image'] + + if 'disable' in container_config: + # check if there is a container by that name running + tmp = _cmd('podman ps -a --format "{{.Names}}"') + if name in tmp: + _cmd(f'podman stop {name}') + _cmd(f'podman rm --force {name}') + continue + + memory = container_config['memory'] + restart = container_config['restart'] + + # Add capability options. Should be in uppercase + cap_add = '' + if 'cap_add' in container_config: + for c in container_config['cap_add']: + c = c.upper() + c = c.replace('-', '_') + cap_add += f' --cap-add={c}' + + # 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}' + + # 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" -e \"{k}={v['value']}\"" + + # Publish ports + port = '' + if 'port' in container_config: + protocol = '' + for portmap in container_config['port']: + if 'protocol' in container_config['port'][portmap]: + protocol = container_config['port'][portmap]['protocol'] + protocol = f'/{protocol}' + else: + protocol = '/tcp' + sport = container_config['port'][portmap]['source'] + dport = container_config['port'][portmap]['destination'] + port += f' -p {sport}:{dport}{protocol}' + + # 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'] + volume += f' -v {svol}:{dvol}' + + container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \ + f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ + f'--name {name} {device} {port} {volume} {env_opt}' + if 'allow_host_networks' in container_config: + _run_rerun(f'{container_base_cmd} --net host {image}') + else: + for network in container_config['network']: + ipparam = '' + if 'address' in container_config['network'][network]: + address = container_config['network'][network]['address'] + ipparam = f'--ip {address}' + + _run_rerun(f'{container_base_cmd} --net {network} {ipparam} {image}') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py deleted file mode 100755 index 1cc6f5a35..000000000 --- a/src/conf_mode/containers.py +++ /dev/null @@ -1,361 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021-2022 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import json - -from ipaddress import ip_address -from ipaddress import ip_network -from time import sleep -from json import dumps as json_write - -from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdict import node_changed -from vyos.util import call -from vyos.util import cmd -from vyos.util import run -from vyos.util import write_file -from vyos.template import inc_ip -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 -from vyos.template import render -from vyos.xml import defaults -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -config_containers_registry = '/etc/containers/registries.conf' -config_containers_storage = '/etc/containers/storage.conf' - -def _run_rerun(container_cmd): - counter = 0 - while True: - if counter >= 10: - break - try: - _cmd(container_cmd) - break - except: - counter = counter +1 - sleep(0.5) - - return None - -def _cmd(command): - if os.path.exists('/tmp/vyos.container.debug'): - print(command) - return cmd(command) - -def network_exists(name): - # Check explicit name for network, returns True if network exists - c = _cmd(f'podman network ls --quiet --filter name=^{name}$') - return bool(c) - -# Common functions -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - base = ['container'] - container = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = defaults(base) - # container base default values can not be merged here - remove and add them later - if 'name' in default_values: - del default_values['name'] - container = dict_merge(default_values, container) - - # Merge per-container default values - if 'name' in container: - default_values = defaults(base + ['name']) - for name in container['name']: - container['name'][name] = dict_merge(default_values, container['name'][name]) - - # Delete container network, delete containers - tmp = node_changed(conf, base + ['container', 'network']) - if tmp: container.update({'network_remove' : tmp}) - - tmp = node_changed(conf, base + ['container', 'name']) - if tmp: container.update({'container_remove' : tmp}) - - return container - -def verify(container): - # bail out early - looks like removal from running config - if not container: - return None - - # Add new container - if 'name' in container: - for name, container_config in container['name'].items(): - # Container image is a mandatory option - if 'image' not in container_config: - raise ConfigError(f'Container image for "{name}" is mandatory!') - - # verify container image exists locally - image = container_config['image'] - - # Check if requested container image exists locally. If it does not - # exist locally - inform the user. - if run(f'podman image exists {image}') != 0: - raise ConfigError(f'Image "{image}" used in contianer "{name}" does not exist '\ - f'locally.\nPlease use "add container image {image}" to add it '\ - 'to the system!') - - if 'network' in container_config: - if len(container_config['network']) > 1: - raise ConfigError(f'Only one network can be specified for container "{name}"!') - - # Check if the specified container network exists - network_name = list(container_config['network'])[0] - if network_name not in container['network']: - raise ConfigError(f'Container network "{network_name}" does not exist!') - - if 'address' in container_config['network'][network_name]: - if 'network' not in container_config: - raise ConfigError(f'Can not use "address" without "network" for container "{name}"!') - - address = container_config['network'][network_name]['address'] - network = None - if is_ipv4(address): - network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0] - elif is_ipv6(address): - network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0] - - # Specified container IP address must belong to network prefix - if ip_address(address) not in ip_network(network): - raise ConfigError(f'Used container address "{address}" not in network "{network}"!') - - # We can not use the first IP address of a network prefix as this is used by podman - if ip_address(address) == ip_network(network)[1]: - raise ConfigError(f'IP address "{address}" can not be used for a container, '\ - 'reserved for the container engine!') - - if 'device' in container_config: - for dev, dev_config in container_config['device'].items(): - if 'source' not in dev_config: - raise ConfigError(f'Device "{dev}" has no source path configured!') - - if 'destination' not in dev_config: - raise ConfigError(f'Device "{dev}" has no destination path configured!') - - source = dev_config['source'] - if not os.path.exists(source): - raise ConfigError(f'Device "{dev}" source path "{source}" does not exist!') - - if 'environment' in container_config: - for var, cfg in container_config['environment'].items(): - if 'value' not in cfg: - raise ConfigError(f'Environment variable {var} has no value assigned!') - - if 'volume' in container_config: - for volume, volume_config in container_config['volume'].items(): - if 'source' not in volume_config: - raise ConfigError(f'Volume "{volume}" has no source path configured!') - - if 'destination' not in volume_config: - raise ConfigError(f'Volume "{volume}" has no destination path configured!') - - source = volume_config['source'] - if not os.path.exists(source): - raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!') - - # If 'allow-host-networks' or 'network' not set. - if 'allow_host_networks' not in container_config and 'network' not in container_config: - raise ConfigError(f'Must either set "network" or "allow-host-networks" for container "{name}"!') - - # Can not set both allow-host-networks and network at the same time - if {'allow_host_networks', 'network'} <= set(container_config): - raise ConfigError(f'"allow-host-networks" and "network" for "{name}" cannot be both configured at the same time!') - - # Add new network - if 'network' in container: - for network, network_config in container['network'].items(): - v4_prefix = 0 - v6_prefix = 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 - elif is_ipv6(prefix): v6_prefix += 1 - - if v4_prefix > 1: - raise ConfigError(f'Only one IPv4 prefix can be defined for network "{network}"!') - if v6_prefix > 1: - raise ConfigError(f'Only one IPv6 prefix can be defined for network "{network}"!') - - - # A network attached to a container can not be deleted - if {'network_remove', 'name'} <= set(container): - for network in container['network_remove']: - for container, container_config in container['name'].items(): - if 'network' in container_config and network in container_config['network']: - raise ConfigError(f'Can not remove network "{network}", used by container "{container}"!') - - return None - -def generate(container): - # bail out early - looks like removal from running config - if not container: - if os.path.exists(config_containers_registry): - os.unlink(config_containers_registry) - if os.path.exists(config_containers_storage): - os.unlink(config_containers_storage) - return None - - if 'network' in container: - for network, network_config in container['network'].items(): - tmp = { - 'cniVersion' : '0.4.0', - 'name' : network, - 'plugins' : [{ - 'type': 'bridge', - 'bridge': f'cni-{network}', - 'isGateway': True, - 'ipMasq': False, - 'hairpinMode': False, - 'ipam' : { - 'type': 'host-local', - 'routes': [], - 'ranges' : [], - }, - }] - } - - for prefix in network_config['prefix']: - net = [{'gateway' : inc_ip(prefix, 1), 'subnet' : prefix}] - tmp['plugins'][0]['ipam']['ranges'].append(net) - - # install per address-family default orutes - default_route = '0.0.0.0/0' - if is_ipv6(prefix): - default_route = '::/0' - tmp['plugins'][0]['ipam']['routes'].append({'dst': default_route}) - - write_file(f'/etc/cni/net.d/{network}.conflist', json_write(tmp, indent=2)) - - render(config_containers_registry, 'containers/registries.conf.j2', container) - render(config_containers_storage, 'containers/storage.conf.j2', container) - - return None - -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']: - call(f'podman stop {name}') - call(f'podman rm --force {name}') - - # Delete old networks if needed - if 'network_remove' in container: - for network in container['network_remove']: - tmp = f'/etc/cni/net.d/{network}.conflist' - if os.path.exists(tmp): - os.unlink(tmp) - - # Add container - if 'name' in container: - for name, container_config in container['name'].items(): - image = container_config['image'] - - if 'disable' in container_config: - # check if there is a container by that name running - tmp = _cmd('podman ps -a --format "{{.Names}}"') - if name in tmp: - _cmd(f'podman stop {name}') - _cmd(f'podman rm --force {name}') - continue - - memory = container_config['memory'] - restart = container_config['restart'] - - # Add capability options. Should be in uppercase - cap_add = '' - if 'cap_add' in container_config: - for c in container_config['cap_add']: - c = c.upper() - c = c.replace('-', '_') - cap_add += f' --cap-add={c}' - - # 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}' - - # 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" -e \"{k}={v['value']}\"" - - # Publish ports - port = '' - if 'port' in container_config: - protocol = '' - for portmap in container_config['port']: - if 'protocol' in container_config['port'][portmap]: - protocol = container_config['port'][portmap]['protocol'] - protocol = f'/{protocol}' - else: - protocol = '/tcp' - sport = container_config['port'][portmap]['source'] - dport = container_config['port'][portmap]['destination'] - port += f' -p {sport}:{dport}{protocol}' - - # 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'] - volume += f' -v {svol}:{dvol}' - - container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \ - f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ - f'--name {name} {device} {port} {volume} {env_opt}' - if 'allow_host_networks' in container_config: - _run_rerun(f'{container_base_cmd} --net host {image}') - else: - for network in container_config['network']: - ipparam = '' - if 'address' in container_config['network'][network]: - address = container_config['network'][network]['address'] - ipparam = f'--ip {address}' - - _run_rerun(f'{container_base_cmd} --net {network} {ipparam} {image}') - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) -- cgit v1.2.3