diff options
| -rw-r--r-- | interface-definitions/container.xml.in | 20 | ||||
| -rw-r--r-- | smoketest/config-tests/container-simple | 1 | ||||
| -rw-r--r-- | smoketest/configs/container-simple | 3 | ||||
| -rwxr-xr-x | src/conf_mode/container.py | 53 | 
4 files changed, 62 insertions, 15 deletions
| diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 1ad7215e5..cd970591b 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -71,6 +71,26 @@                <multi/>              </properties>            </leafNode> +          <tagNode name="kernel-parameter"> +            <properties> +              <help>Add custom kernel parameters (sysctl)</help> +              <constraint> +                <regex>[._a-z*]+</regex> +              </constraint> +              <constraintErrorMessage>Kernel parameter name must be alphanumeric and can contain periods, asterisks and underscores</constraintErrorMessage> +            </properties> +            <children> +              <leafNode name="value"> +                <properties> +                  <help>Set kernel parameter option value</help> +                  <valueHelp> +                    <format>txt</format> +                    <description>Set kernel parameter option value</description> +                  </valueHelp> +                </properties> +              </leafNode> +            </children> +          </tagNode>            #include <include/generic-description.xml.i>            <tagNode name="device">              <properties> diff --git a/smoketest/config-tests/container-simple b/smoketest/config-tests/container-simple index cc80ef4cf..ced770421 100644 --- a/smoketest/config-tests/container-simple +++ b/smoketest/config-tests/container-simple @@ -11,3 +11,4 @@ set container name c02 allow-host-networks  set container name c02 allow-host-pid  set container name c02 capability 'sys-time'  set container name c02 image 'busybox:stable' +set container name c02 kernel-parameter 'net.ipv4.conf.all.forwarding' value '1'
\ No newline at end of file diff --git a/smoketest/configs/container-simple b/smoketest/configs/container-simple index 82983afb7..0585fed29 100644 --- a/smoketest/configs/container-simple +++ b/smoketest/configs/container-simple @@ -10,6 +10,9 @@ container {          allow-host-pid          cap-add sys-time          image busybox:stable +        kernel-parameter "net.ipv4.ip_forward" { +            value "1" +        }      }  }  interfaces { diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 3efeb9b40..8757c5eab 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -43,6 +43,7 @@ from vyos.template import render  from vyos.xml_ref import default_value  from vyos import ConfigError  from vyos import airbag +  airbag.enable()  config_containers = '/etc/containers/containers.conf' @@ -50,16 +51,19 @@ config_registry = '/etc/containers/registries.conf'  config_storage = '/etc/containers/storage.conf'  systemd_unit_path = '/run/systemd/system' +  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: @@ -86,21 +90,22 @@ def get_config(config=None):      # registry is a tagNode with default values - merge the list from      # default_values['registry'] into the tagNode variables      if 'registry' not in container: -        container.update({'registry' : {}}) +        container.update({'registry': {}})          default_values = default_value(base + ['registry'])          for registry in default_values: -            tmp = {registry : {}} +            tmp = {registry: {}}              container['registry'] = dict_merge(tmp, container['registry'])      # Delete container network, delete containers      tmp = node_changed(conf, base + ['network']) -    if tmp: container.update({'network_remove' : tmp}) +    if tmp: container.update({'network_remove': tmp})      tmp = node_changed(conf, base + ['name']) -    if tmp: container.update({'container_remove' : tmp}) +    if tmp: container.update({'container_remove': tmp})      return container +  def verify(container):      # bail out early - looks like removal from running config      if not container: @@ -125,8 +130,8 @@ def verify(container):              # of image upgrade and deletion.              image = container_config['image']              if run(f'podman image exists {image}') != 0: -                Warning(f'Image "{image}" used in container "{name}" does not exist '\ -                        f'locally. Please use "add container image {image}" to add it '\ +                Warning(f'Image "{image}" used in container "{name}" does not exist ' \ +                        f'locally. Please use "add container image {image}" to add it ' \                          f'to the system! Container "{name}" will not be started!')              if 'cpu_quota' in container_config: @@ -167,11 +172,11 @@ def verify(container):                          # 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, '\ +                            raise ConfigError(f'IP address "{address}" can not be used for a container, ' \                                                'reserved for the container engine!')                      if cnt_ipv4 > 1 or cnt_ipv6 > 1: -                        raise ConfigError(f'Only one IP address per address family can be used for '\ +                        raise ConfigError(f'Only one IP address per address family can be used for ' \                                            f'container "{name}". {cnt_ipv4} IPv4 and {cnt_ipv6} IPv6 address(es)!')              if 'device' in container_config: @@ -186,6 +191,11 @@ def verify(container):                      if not os.path.exists(source):                          raise ConfigError(f'Device "{dev}" source path "{source}" does not exist!') +            if 'kernel-parameter' in container_config: +                for var, cfg in container_config['kernel-parameter'].items(): +                    if 'value' not in cfg: +                        raise ConfigError(f'Kernel parameter {var} has no value assigned!') +              if 'environment' in container_config:                  for var, cfg in container_config['environment'].items():                      if 'value' not in cfg: @@ -219,7 +229,8 @@ def verify(container):              # 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!') +                raise ConfigError( +                    f'"allow-host-networks" and "network" for "{name}" cannot be both configured at the same time!')              # gid cannot be set without uid              if 'gid' in container_config and 'uid' not in container_config: @@ -235,8 +246,10 @@ def verify(container):                  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 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}"!') @@ -262,6 +275,7 @@ def verify(container):      return None +  def generate_run_arguments(name, container_config):      image = container_config['image']      cpu_quota = container_config['cpu_quota'] @@ -269,6 +283,12 @@ def generate_run_arguments(name, container_config):      shared_memory = container_config['shared_memory']      restart = container_config['restart'] +    # Add sysctl options +    sysctl_opt = '' +    if 'kernel-parameter' in container_config: +        for k, v in container_config['kernel-parameter'].items(): +            sysctl_opt += f" --sysctl={k}={v['value']}" +      # Add capability options. Should be in uppercase      capabilities = ''      if 'capability' in container_config: @@ -341,7 +361,7 @@ def generate_run_arguments(name, container_config):      if 'allow_host_pid' in container_config:        host_pid = '--pid host' -    container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} ' \ +    container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} {sysctl_opt} ' \                           f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \                           f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid} {host_pid}' @@ -375,6 +395,7 @@ def generate_run_arguments(name, container_config):      return f'{container_base_cmd} --no-healthcheck --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip() +  def generate(container):      # bail out early - looks like removal from running config      if not container: @@ -387,7 +408,7 @@ def generate(container):          for network, network_config in container['network'].items():              tmp = {                  'name': network, -                'id' : sha256(f'{network}'.encode()).hexdigest(), +                'id': sha256(f'{network}'.encode()).hexdigest(),                  'driver': 'bridge',                  'network_interface': f'pod-{network}',                  'subnets': [], @@ -399,7 +420,7 @@ def generate(container):                  }              }              for prefix in network_config['prefix']: -                net = {'subnet' : prefix, 'gateway' : inc_ip(prefix, 1)} +                net = {'subnet': prefix, 'gateway': inc_ip(prefix, 1)}                  tmp['subnets'].append(net)                  if is_ipv6(prefix): @@ -418,11 +439,12 @@ def generate(container):              file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')              run_args = generate_run_arguments(name, container_config) -            render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args,}, +            render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args, },                     formater=lambda _: _.replace(""", '"').replace("'", "'"))      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 @@ -485,6 +507,7 @@ def apply(container):      return None +  if __name__ == '__main__':      try:          c = get_config() | 
