confd: major refactor of infix-containers after team review

- Rename container/container -> containers/container
 - Collapse containers-state into containers
 - Complete refactor of network/host-network for improved UX.  A new
   must expression ensure mutual exclusion between host and networks
 - Rename entrypoint -> command, for consistency (overrides ENTRYPOINT)
 - Move nodes around a bit for more logical placement in model
 - Clean out all traces of a container on removal
 - Rename container image_id -> image-id for consistency

The active queue is for tracking currently running containers, ensuring
only relevant changes are applied, and also for container image upgrade,
at the end of which these scripts are re-run to activate the new image

Also, fix misplaced '-f' when pruning unused volumes.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2024-02-25 19:49:27 +01:00
parent a9a7b7016d
commit 235d3e46e6
4 changed files with 192 additions and 187 deletions
+49 -20
View File
@@ -55,6 +55,14 @@ def json_get_yang_origin(addr):
return xlate.get(proto, "other")
def getitem(data, key):
"""Get sub-object from an object"""
while key:
data = data[key[0]]
key = key[1:]
return data
def get_proc_value(procfile):
"""Return contents of /proc file, or None"""
try:
@@ -438,36 +446,57 @@ def get_bridge_port_stp_state(ifname, iface_out, test):
return None
def container_inspect(name, key):
"""Call podman inspect {name}, return object at {path} or None"""
cmd = ['podman', 'inspect', name]
raw = run_json_cmd(cmd)
return getitem(raw[0], key)
def add_container(containers):
"""In container-state we list *all* containers, not just ones managed in configuration."""
"""In container-state we list *all* containers, not just ones manged in configuration."""
cmd = ['podman', 'ps', '-a', '--format=json']
raw = run_json_cmd(cmd)
for entry in raw:
container = {}
container["name"] = entry["Names"][0]
container["id"] = entry["Id"]
container["image"] = entry["Image"]
container["image_id"] = entry["ImageID"]
container["running"] = entry["State"] == "running"
container["status"] = entry["Status"]
running = entry["State"] == "running"
container = {
"name": entry["Names"][0],
"id": entry["Id"],
"image": entry["Image"],
"image-id": entry["ImageID"],
"running": running,
"status": entry["Status"]
}
# Bonus information, may not be available
if entry["Command"]:
container["command"] = " ".join(entry["Command"])
if entry["Networks"]:
container["networks"] = entry["Networks"]
# The 'podman ps' command lists ports even in host mode, but
# that's not applicable, so skip networks and port forwardings
networks = container_inspect(container["name"], ("NetworkSettings", "Networks"))
if "host" in networks:
container["network"] = { "host": True }
else:
container["network"] = {
"interface": [],
"publish": []
}
if entry["Ports"]:
container["ports"] = []
for port in entry["Ports"]:
addr = ""
if port["host_ip"]:
addr = f"{port['host_ip']}:"
if entry["Networks"]:
for net in entry["Networks"]:
container["network"]["interface"].append({ "name": net })
publish = f"{addr}{port['host_port']}:{port['container_port']}/{port['protocol']}"
container["ports"].append(publish)
if running and entry["Ports"]:
for port in entry["Ports"]:
addr = ""
if port["host_ip"]:
addr = f"{port['host_ip']}:"
pub = f"{addr}{port['host_port']}->{port['container_port']}/{port['protocol']}"
container["network"]["publish"].append(pub)
containers.append(container)
@@ -825,11 +854,11 @@ if __name__ == "__main__":
elif args.model == 'infix-containers':
yang_data = {
"infix-containers:container-state": {
"infix-containers:containers": {
"container": []
}
}
add_container(yang_data['infix-containers:container-state']['container'])
add_container(yang_data['infix-containers:containers']['container'])
else:
print(f"Unsupported model {args.model}", file=sys.stderr)
+50 -43
View File
@@ -16,14 +16,14 @@
#include "core.h"
#define ARPING_MSEC 1000
#define MODULE "infix-containers"
#define CFG_XPATH "/infix-containers:container"
#define CFG_XPATH "/infix-containers:containers"
#define INBOX_QUEUE "/run/containers/inbox"
#define JOB_QUEUE "/run/containers/queue"
#define ACTIVE_QUEUE "/var/lib/containers/active"
#define LOGGER "logger -t container -p local1.notice"
static const struct srx_module_requirement reqs[] = {
{ .dir = YANG_PATH_, .name = MODULE, .rev = "2023-12-14" },
{ .dir = YANG_PATH_, .name = MODULE, .rev = "2024-02-01" },
{ NULL }
};
@@ -31,7 +31,7 @@ static int add(const char *name, struct lyd_node *cif)
{
const char *image = lydx_get_cattr(cif, "image");
const char *restart_policy, *string;
struct lyd_node *node;
struct lyd_node *node, *network;
FILE *fp, *ap;
char *restart = ""; /* Default restart:10 */
@@ -56,9 +56,6 @@ static int add(const char *name, struct lyd_node *cif)
if ((string = lydx_get_cattr(cif, "hostname")))
fprintf(fp, " --hostname %s", string);
LYX_LIST_FOR_EACH(lyd_child(cif), node, "publish")
fprintf(fp, " -p %s", lyd_get_value(node));
if (lydx_is_enabled(cif, "read-only"))
fprintf(fp, " --read-only");
@@ -102,30 +99,37 @@ static int add(const char *name, struct lyd_node *cif)
fprintf(fp, " -e /run/containers/args/%s.env", name);
}
LYX_LIST_FOR_EACH(lyd_child(cif), node, "network") {
struct lyd_node *opt;
const char *name;
int first = 1;
network = lydx_get_descendant(lyd_child(cif), "network", NULL);
if (network) {
if (lydx_is_enabled(network, "host")) {
fprintf(fp, " --net host");
} else {
LYX_LIST_FOR_EACH(lyd_child(network), node, "interface") {
struct lyd_node *opt;
const char *name;
int first = 1;
name = lydx_get_cattr(node, "name");
fprintf(fp, " --net %s", name);
LYX_LIST_FOR_EACH(lyd_child(node), opt, "option") {
const char *option = lyd_get_value(opt);
name = lydx_get_cattr(node, "name");
fprintf(fp, " --net %s", name);
LYX_LIST_FOR_EACH(lyd_child(node), opt, "option") {
const char *option = lyd_get_value(opt);
fprintf(fp, "%s%s", first ? ":" : ",", option);
first = 0;
fprintf(fp, "%s%s", first ? ":" : ",", option);
first = 0;
}
}
LYX_LIST_FOR_EACH(lyd_child(network), node, "publish")
fprintf(fp, " -p %s", lyd_get_value(node));
}
}
if (lydx_is_enabled(cif, "host-network"))
fprintf(fp, " --net host");
if ((string = lydx_get_cattr(cif, "entrypoint")))
if ((string = lydx_get_cattr(cif, "command")))
fprintf(fp, " --entrypoint");
fprintf(fp, " create %s %s", name, image);
if ((string = lydx_get_cattr(cif, "entrypoint")))
if ((string = lydx_get_cattr(cif, "command")))
fprintf(fp, " %s", string);
fprintf(fp, "\n");
@@ -163,15 +167,26 @@ static int add(const char *name, struct lyd_node *cif)
static int del(const char *name)
{
char fn[strlen(JOB_QUEUE) + strlen(name) + 5];
const char *queue[] = {
JOB_QUEUE,
INBOX_QUEUE,
ACTIVE_QUEUE,
};
int rc;
/* Remove any pending download/create job first */
snprintf(fn, sizeof(fn), "%s/%s.sh", JOB_QUEUE, name);
erase(fn);
snprintf(fn, sizeof(fn), "%s/%s.sh", INBOX_QUEUE, name);
erase(fn);
for (size_t i = 0; i < NELEMS(queue); i++) {
char fn[strlen(queue[i]) + strlen(name) + 5];
return systemf("container delete %s", name);
snprintf(fn, sizeof(fn), "%s/%s.sh", queue[i], name);
erase(fn);
}
systemf("initctl -nbq disable container:%s", name);
rc = systemf("container delete %s", name);
erasef("/etc/finit.d/available/container:%s.conf", name);
return rc;
}
static int change(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
@@ -198,16 +213,14 @@ static int change(sr_session_ctx_t *session, uint32_t sub_id, const char *module
if (err)
goto err_release_data;
cifs = lydx_get_descendant(cfg->tree, "container", "container", NULL);
difs = lydx_get_descendant(diff, "container", "container", NULL);
cifs = lydx_get_descendant(cfg->tree, "containers", "container", NULL);
difs = lydx_get_descendant(diff, "containers", "container", NULL);
/* find the modified one, delete or recreate only that */
LYX_LIST_FOR_EACH(difs, dif, "container") {
const char *name = lydx_get_cattr(dif, "name");
ERROR("Change in container %s", name);
if (lydx_get_op(dif) == LYDX_OP_DELETE) {
ERROR("OP DELETE container %s", name);
del(name);
continue;
}
@@ -215,19 +228,13 @@ static int change(sr_session_ctx_t *session, uint32_t sub_id, const char *module
LYX_LIST_FOR_EACH(cifs, cif, "container") {
const char *nm = lydx_get_cattr(cif, "name");
ERROR("container %s vs %s", name, nm);
if (strcmp(name, nm)) {
ERROR("Skipping container %s", nm);
if (strcmp(name, nm))
continue;
}
if (!lydx_is_enabled(cif, "enabled")) {
ERROR("container %s not enabled", nm);
if (!lydx_is_enabled(cif, "enabled"))
del(name);
} else {
ERROR("container %s enabled", nm);
else
add(name, cif);
}
break;
}
}
@@ -250,7 +257,7 @@ static int action(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath,
char *cmd, *name, *ptr;
char quote;
/* /infix-containers:container/container[name='ntpd']/restart */
/* /infix-containers:containers/container[name='ntpd']/restart */
strlcpy(buf, xpath, sizeof(buf));
name = strstr(buf, "[name=");
@@ -271,7 +278,7 @@ static int action(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath,
return SR_ERR_INTERNAL;
cmd += 2;
ERROR("CALLING 'container %s %s' (xpath %s)", cmd, name, xpath);
DEBUG("CALLING 'container %s %s' (xpath %s)", cmd, name, xpath);
if (systemf("container %s %s", cmd, name))
return SR_ERR_INTERNAL;
@@ -309,7 +316,7 @@ void infix_containers_launch(void)
ERRNO("Failed moving %s to job queue %s", next, JOB_QUEUE);
}
systemf("container -f volume prune");
systemf("container volume prune -f");
}
int infix_containers_init(struct confd *confd)
@@ -22,7 +22,7 @@ module infix-containers {
prefix infix-if;
}
revision 2023-12-14 {
revision 2024-02-01 {
description "Initial revision";
reference "internal";
}
@@ -52,45 +52,25 @@ module infix-containers {
* Data Nodes
*/
container container {
container containers {
list container {
key "name";
leaf name {
description "Name of the container";
type string;
}
leaf enabled {
description "Enable or disable a container configuration.";
type boolean;
default true;
}
list env {
description "Set environment variables, key=\"value\" pairs.";
key key;
leaf key {
description "Single word, [A-Za-z_]";
type string;
}
leaf value {
description "Argument to key can be a single word or quoted multiple words.";
mandatory true;
type string;
}
}
leaf entrypoint {
description "Override the default ENTRYPOINT from the image.";
leaf name {
description "Name of the container";
type string;
}
leaf hostname {
description "Sets the container host name that is available inside the container.";
type inet:domain-name;
leaf id {
description "Container ID, unique hash.";
config false;
type string;
}
leaf image {
@@ -110,6 +90,38 @@ module infix-containers {
type string;
}
leaf image-id {
description "Docker image ID, exact hash used.";
config false;
type string;
}
list env {
description "Set environment variables, key=\"value\" pairs.";
key key;
leaf key {
description "Single word, [A-Za-z_]";
type string;
}
leaf value {
description "Argument to key can be a single word or quoted multiple words.";
mandatory true;
type string;
}
}
leaf command {
description "Override ENTRYPOINT from image and run command + args.";
type string;
}
leaf hostname {
description "Sets the container host name that is available inside the container.";
type inet:domain-name;
}
leaf restart-policy {
description "Restart policy to when containers exit/crash.";
type restart-policy;
@@ -121,56 +133,56 @@ module infix-containers {
type boolean;
}
choice network {
container network {
description "Select network mode: none, host, or container network interfaces.";
case host-network {
leaf host-network {
description "Run in same network namespace as host.";
type boolean;
}
}
leaf host {
description "Run in same network namespace as host, share DNS and publish all ports.";
type boolean;
}
case network {
list network {
description "Container network interface to connect to the container.";
key name;
list interface {
description "Container network interface(s) to connect to the container.";
key name;
leaf name {
description "CNI network to connect to the container.";
type if:interface-ref;
must "/if:interfaces/if:interface[if:name = current()]/infix-if:container-network" {
error-message "Container networks must be interfaces classified as container-network.";
}
}
leaf name {
description "CNI network (interface name) to connect to the container.";
type if:interface-ref;
must "/if:interfaces/if:interface[if:name = current()]/infix-if:container-network" {
error-message "Container networks must be interfaces classified as container-network.";
}
}
leaf-list option {
description "Options for CNI bridges.
leaf-list option {
description "Options for CNI bridges.
Example: ip=1.2.3.4 to request a specific IP, both IPv4 and IPv6.
interface_name=foo0 name to set interface name inside container.";
type string;
}
}
type string;
}
}
leaf-list publish {
description "Publish container port, or a range of ports, to the host.
leaf-list publish {
description "Publish container port, or a range of ports, to the host.
Syntax: [[ip:][hostPort]:]containerPort[/protocol]
Sample: 8080:80 -- forward tcp port 8080 to container port 80
69:69/udp -- forward udp port 69 to container port 69
127.0.0.1:8080:80 -- forward only from loopback interface";
type string;
}
69:69/udp -- forward udp port 69 to container port 69
127.0.0.1:8080:80 -- forward only from loopback interface";
type string;
}
leaf-list dns {
description "Set custom DNS servers, or 'none' to use /etc/resolv.conf in image.";
type string;
}
leaf-list dns {
description "Set custom DNS servers, or 'none' to use /etc/resolv.conf in image.";
type string;
}
leaf-list search {
description "Set custom DNS search domains, or '.' to not set search domain.";
type string;
}
}
leaf-list search {
description "Set custom DNS search domains, or '.' to not set search domain.";
type string;
}
must "(host and not(interface)) or (not(host) and interface) or (not(host) and not(interface))" {
error-message "Host and interfaces are mutually exclusive";
}
}
leaf read-only {
@@ -231,72 +243,29 @@ module infix-containers {
}
}
action start {
description "Start a stopped container.";
}
action stop {
description "Stop a running container.";
}
action restart {
description "Restart a running, or start, a stopped container.";
}
}
}
/*
* Operational state data nodes
*/
container container-state {
config false;
list container {
key "name";
leaf name {
description "Name of the container.";
type string;
}
leaf id {
description "Container ID, unique hash.";
type string;
}
leaf comand {
description "Command being run by container.";
type string;
}
leaf image {
description "Docker image used.";
type string;
}
leaf image_id {
description "Docker image ID, exact hash used.";
type string;
}
leaf-list networks {
description "CNI networks attached to container.";
type string;
}
leaf-list ports {
description "Exposed ports: [[ip:][hostPort]:]containerPort[/protocol]";
type string;
}
leaf running {
description "Status of container, running or not.";
config false;
type boolean;
}
leaf status {
description "Status of container, human friendly.";
config false;
type string;
}
action start {
description "Start a stopped container.";
}
action stop {
description "Stop a running container.";
}
action restart {
description "Restart a running, or start, a stopped container.";
}
}
}
}
+1 -1
View File
@@ -40,7 +40,7 @@
#define XPATH_ROUTING_TABLE "/ietf-routing:routing/ribs"
#define XPATH_HARDWARE_BASE "/ietf-hardware:hardware"
#define XPATH_ROUTING_OSPF XPATH_ROUTING_BASE "/ospf"
#define XPATH_CONTAIN_BASE "/infix-containers:container-state"
#define XPATH_CONTAIN_BASE "/infix-containers:containers"
TAILQ_HEAD(sub_head, sub);