diff options
-rw-r--r-- | data/templates/containers/registry.tmpl | 5 | ||||
-rw-r--r-- | data/templates/containers/storage.tmpl | 5 | ||||
-rw-r--r-- | interface-definitions/containers.xml.in | 102 | ||||
-rw-r--r-- | op-mode-definitions/containers.xml.in | 55 | ||||
-rwxr-xr-x | src/conf_mode/containers.py | 235 | ||||
-rwxr-xr-x | src/op_mode/containers_op.sh | 51 |
6 files changed, 453 insertions, 0 deletions
diff --git a/data/templates/containers/registry.tmpl b/data/templates/containers/registry.tmpl new file mode 100644 index 000000000..c6611ef1d --- /dev/null +++ b/data/templates/containers/registry.tmpl @@ -0,0 +1,5 @@ +### Autogenerated by /usr/libexec/vyos/conf_mode/containers.py ### + +{% if registry is defined and registry is not none %} + unqualified-search-registries = {{ registry }} +{% endif %} diff --git a/data/templates/containers/storage.tmpl b/data/templates/containers/storage.tmpl new file mode 100644 index 000000000..3a69b7252 --- /dev/null +++ b/data/templates/containers/storage.tmpl @@ -0,0 +1,5 @@ +### Autogenerated by /usr/libexec/vyos/conf_mode/containers.py ### + +[storage] + driver = "vfs" + graphroot = "/config/containers/storage" diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in new file mode 100644 index 000000000..f54207936 --- /dev/null +++ b/interface-definitions/containers.xml.in @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="container" owner="${vyos_conf_scripts_dir}/containers.py"> + <properties> + <help>Container applications</help> + </properties> + <children> + <tagNode name="name"> + <properties> + <help>Container name</help> + </properties> + <children> + <leafNode name="allow-host-networks"> + <properties> + <help>Allow host networks in container</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Container description</help> + </properties> + </leafNode> + <tagNode name="environment"> + <properties> + <help>Add custom environment variables</help> + </properties> + <children> + <leafNode name="value"> + <properties> + <help>Set environment option value</help> + <valueHelp> + <format>txt</format> + <description>Set environment option value</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="image"> + <properties> + <help>Image name in the hub-registry</help> + </properties> + </leafNode> + <tagNode name="network"> + <properties> + <help>Attach user defined network to container</help> + <completionHelp> + <path>container network</path> + </completionHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>Set IPv4 static address to container (optional)</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address (x.x.x.1 reserved)</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="network"> + <properties> + <help>Network name</help> + </properties> + <children> + <leafNode name="description"> + <properties> + <help>Network description</help> + </properties> + </leafNode> + <leafNode name="ipv4-prefix"> + <properties> + <help>Prefix which allocated to that network</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network and prefix length</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="registry"> + <properties> + <help>Add registry (default docker.io)</help> + <multi/> + </properties> + <defaultValue>docker.io</defaultValue> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/containers.xml.in b/op-mode-definitions/containers.xml.in new file mode 100644 index 000000000..06f98b24e --- /dev/null +++ b/op-mode-definitions/containers.xml.in @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="add"> + <children> + <node name="container"> + <properties> + <help>Add container image</help> + </properties> + <children> + <tagNode name="image"> + <properties> + <help>Pull a new image for container</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/containers_op.sh --pull "${4}"</command> + </tagNode> + </children> + </node> + </children> + </node> + <node name="delete"> + <children> + <node name="container"> + <properties> + <help>Delete container image</help> + </properties> + <children> + <tagNode name="image"> + <properties> + <help>Delete container image</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/containers_op.sh --remove "${4}"</command> + </tagNode> + </children> + </node> + </children> + </node> + <node name="show"> + <children> + <node name="container"> + <properties> + <help>Show containers</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/containers_op.sh --show-containers</command> + <children> + <leafNode name="image"> + <properties> + <help>Delete container image</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/containers_op.sh --show-images</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py new file mode 100755 index 000000000..e2fa5bd44 --- /dev/null +++ b/src/conf_mode/containers.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. + +import os + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import node_changed +from vyos.configdict import leaf_node_changed +from vyos import ConfigError +from vyos.util import cmd, process_named_running +from vyos.template import render +from vyos.xml import defaults +from vyos import airbag +import json +airbag.enable() + +config_containers_registry = '/etc/containers/registries.conf' +config_containers_storage = '/etc/containers/storage.conf' + +# Container management functions +def container_exists(name): + ''' + https://docs.podman.io/en/latest/_static/api.html#operation/ContainerExistsLibpod + Check if container exists. Response codes. + 204 - container exists + 404 - no such container + ''' + c = cmd(f"curl --unix-socket /run/podman/podman.sock 'http://d/v3.0.0/libpod/containers/{name}/exists'") + # If container exists it return status code "0" + # This code not displayed + if c == "": + # Container exists + return True + else: + # Container not exists + return False + +def container_status(name): + ''' + https://docs.podman.io/en/latest/_static/api.html#operation/ContainerInspectLibpod + ''' + c = cmd(f"curl --unix-socket /run/podman/podman.sock 'http://d/v3.0.0/libpod/containers/{name}/json'") + data = json.loads(c) + status = data['State']['Status'] + + return status + +def container_stop(name): + c = cmd(f'podman stop {name}') + +def container_start(name): + c = cmd(f'podman start {name}') + +def ctnr_network_exists(name): + # Check explicit name for network. + c = cmd(f'podman network ls --quiet --filter name=^{name}$') + # If network name is found, return true + if bool(c) == True: + return True + else: + return False + + +# Common functions +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['container'] + container = conf.get_config_dict(base, get_first_key=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 = dict_merge(default_values, container) + + if 'name' in container or 'network' in container: + container['configured'] = True + + # Delete container network, delete containers + dict = {} + tmp_net = node_changed(conf, ['container', 'network']) + if tmp_net: + dict = dict_merge({'net_remove' : tmp_net}, dict) + container.update(dict) + + tmp_name = node_changed(conf, ['container', 'name']) + if tmp_name: + dict = dict_merge({'container_remove' : tmp_name}, dict) + container.update(dict) + + 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 cont, container_config in container['name'].items(): + # Dont add container with wrong/undefined name network + if 'network' in container_config and 'network' in container: + if list(container_config['network'])[0] not in container['network']: + # Don't allow delete network if container use this network. + raise ConfigError('Netowrk with name: {0} shuld be specified!'.format(list(container_config['network'])[0])) + + # If image not defined + if 'image' not in container_config: + raise ConfigError(f'Image for container "{cont}" is required!') + + # 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'"Network" or "allow-host-networks" for container "{cont}" is required!') + + # If set both parameters for networks (host and user-defined). We require only one. + if 'allow-host-networks' in container_config and 'network' in container_config: + raise ConfigError(f'"allow-host-networks" and "network" for "{cont}" cannot be both configured at the same time!') + + # Add new network + if 'network' in container: + for net in container['network']: + # If ipv4-prefix not defined for user-defined network + if 'ipv4-prefix' not in container['network'][net]: + raise ConfigError(f'IPv4 prefix for network "{net}" is required!') + + # Don't allow to remove network which used for container + if 'net_remove' in container: + for net in container['net_remove']: + if 'name' in container: + for cont in container['name']: + if 'network' in container['name'][cont]: + if net in container['name'][cont]['network']: + raise ConfigError(f'Can\'t remove network "{net}" used for "{cont}"') + + return None + +def generate(container): + # bail out early - looks like removal from running config + if not container: + return None + + render(config_containers_registry, 'containers/registry.tmpl', container) + render(config_containers_storage, 'containers/storage.tmpl', 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']: + if container_status(name) == 'running': + cmd(f'podman stop {name}') + cmd(f'podman rm --force {name}') + print(f'Container "{name}" deleted') + + # Delete old networks if needed + if 'net_remove' in container: + for net in container['net_remove']: + cmd(f'podman network rm {net}') + + # Add network + if 'network' in container: + for net in container['network']: + # Check if the network has already been created + if ctnr_network_exists(net) is False: + prefix = container['network'][net]['ipv4-prefix'] + if container['network'][net]['ipv4-prefix']: + # Create user-defined network + try: + cmd(f'podman network create {net} --subnet={prefix}') + except: + print(f'Can\'t add network {net}') + + # Add container + if 'name' in container: + for name in container['name']: + # Check if the container has already been created + #if len(cmd(f'podman ps -a --filter "name=^{name}$" -q')) == 0: + if container_exists(name) is False: + image = container['name'][name]['image'] + + # Check/set environment options "-e foo=bar" + env_opt = '' + if 'environment' in container['name'][name]: + env_opt = '-e ' + env_opt += " -e ".join(f"{k}={v['value']}" for k, v in container['name'][name]['environment'].items()) + + if 'allow-host-networks' in container['name'][name]: + try: + cmd(f'podman run -dit --name {name} --net host {env_opt} {image}') + except: + print(f'Can\'t add container {name}') + + else: + for net in container['name'][name]['network']: + if container['name'][name]['image']: + ipparam = '' + if 'address' in container['name'][name]['network'][net]: + ipparam = '--ip {}'.format(container['name'][name]['network'][net]['address']) + try: + cmd(f'podman run --name {name} -dit --net {net} {ipparam} {env_opt} {image}') + except: + print(f'Can\'t add container {name}') + # Else container is already created. Just start it. + # It's needed after reboot. + else: + if container_status(name) != 'running': + container_start(name) + 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/op_mode/containers_op.sh b/src/op_mode/containers_op.sh new file mode 100755 index 000000000..bdc0ead98 --- /dev/null +++ b/src/op_mode/containers_op.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Expect 2 args or "show-containers" or "show-images" +if [[ $# -ne 2 ]] && [[ $1 != "--show-containers" ]] && [[ $1 != "--show-images" ]] ; then + echo "Image not set or not found" + exit 1 +fi + +OPTION=$1 +IMAGE=$2 + +# Download image +pull_image() { + sudo podman pull ${IMAGE} +} + +# Remove image +remove_image() { + sudo podman image rm ${IMAGE} +} + +# Show containers +show_containers() { + sudo podman ps -a +} + +# Show image +show_images() { + sudo podman image ls +} + + +if [ "$OPTION" = "--pull" ]; then + pull_image + exit 0 +fi + +if [ "$OPTION" = "--remove" ]; then + remove_image + exit 0 +fi + +if [ "$OPTION" = "--show-containers" ]; then + show_containers + exit 0 +fi + +if [ "$OPTION" = "--show-images" ]; then + show_images + exit 0 +fi |