uxc: add container management CLI tool
authorDaniel Golle <daniel@makrotopia.org>
Fri, 10 Jul 2020 09:57:23 +0000 (10:57 +0100)
committerDaniel Golle <daniel@makrotopia.org>
Fri, 10 Jul 2020 17:32:50 +0000 (18:32 +0100)
As procd can now provide a fully fetured container runtime using ujail,
add a (for now) simple CLI tool to list, add, delete, start and stop
OCI-complaint container bundles and selecting whether they should be
launched on boot.
In future commits, this will be extended to provide state output, take
care of hooks, send signals and fetch remote container images in
accordance with the Open Container Initiative Runtime Specification.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
CMakeLists.txt
uxc.c [new file with mode: 0644]

index 8084674dc1e07f3bfae6079f267d0e79b24f7d27..fa90193931bbade46061d5d1c8028a835d7b400f 100644 (file)
@@ -120,6 +120,12 @@ TARGET_LINK_LIBRARIES(ujail-console ${ubox} ${ubus} ${blobmsg_json})
 INSTALL(TARGETS ujail-console
        RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}
 )
+
+ADD_EXECUTABLE(uxc uxc.c)
+TARGET_LINK_LIBRARIES(uxc ${ubox} ${ubus} ${blobmsg_json})
+INSTALL(TARGETS uxc
+       RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}
+)
 endif()
 
 IF(UTRACE_SUPPORT)
diff --git a/uxc.c b/uxc.c
new file mode 100644 (file)
index 0000000..6013637
--- /dev/null
+++ b/uxc.c
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2020 Daniel Golle <daniel@makrotopia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License version 2.1
+ * 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.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <fcntl.h>
+#include <libubus.h>
+#include <libubox/avl-cmp.h>
+#include <libubox/blobmsg.h>
+#include <libubox/blobmsg_json.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <glob.h>
+
+#include "log.h"
+
+#define UXC_CONFDIR "/etc/uxc"
+#define UXC_RUNDIR "/var/run/uxc"
+
+struct runtime_state {
+       struct avl_node avl;
+       const char *container_name;
+       const char *instance_name;
+       const char *jail_name;
+       bool running;
+       int pid;
+       int exitcode;
+};
+
+AVL_TREE(runtime, avl_strcmp, false, NULL);
+static struct blob_buf conf;
+static struct blob_buf state;
+
+static struct ubus_context *ctx;
+
+static int usage(void) {
+       printf("syntax: uxc {command} [parameters ...]\n");
+       printf("commands:\n");
+       printf("\tlist\t\t\t\tlist all configured containers\n");
+       printf("\tcreate {conf} {path} [enabled]\tcreate {conf} for OCI bundle at {path}\n");
+       printf("\tstart {conf}\t\t\tstart container {conf}\n");
+       printf("\tstop {conf}\t\t\tstop container {conf}\n");
+       printf("\tenable {conf}\t\t\tstart container {conf} on boot\n");
+       printf("\tdisable {conf}\t\t\tdon't start container {conf} on boot\n");
+       printf("\tdelete {conf}\t\t\tdelete {conf}\n");
+       return EINVAL;
+}
+
+enum {
+       CONF_NAME,
+       CONF_PATH,
+       CONF_JAIL,
+       CONF_AUTOSTART,
+       __CONF_MAX,
+};
+
+static const struct blobmsg_policy conf_policy[__CONF_MAX] = {
+       [CONF_NAME] = { .name = "name", .type = BLOBMSG_TYPE_STRING },
+       [CONF_PATH] = { .name = "path", .type = BLOBMSG_TYPE_STRING },
+       [CONF_JAIL] = { .name = "jail", .type = BLOBMSG_TYPE_STRING },
+       [CONF_AUTOSTART] = { .name = "autostart", .type = BLOBMSG_TYPE_BOOL },
+};
+
+static int conf_load(bool load_state)
+{
+       int gl_flags = GLOB_NOESCAPE | GLOB_MARK;
+       int j, res;
+       glob_t gl;
+       char *globstr;
+       struct blob_buf *target = load_state?&state:&conf;
+       void *c, *o;
+
+       if (asprintf(&globstr, "%s/*.json", load_state?UXC_RUNDIR:UXC_CONFDIR) == -1)
+               return ENOMEM;
+
+       blob_buf_init(target, 0);
+       c = blobmsg_open_table(target, NULL);
+
+       res = glob(globstr, gl_flags, NULL, &gl);
+       free(globstr);
+       if (res < 0)
+               return 0;
+
+       for (j = 0; j < gl.gl_pathc; j++) {
+               o = blobmsg_open_table(target, strdup(gl.gl_pathv[j]));
+               if (!blobmsg_add_json_from_file(target, gl.gl_pathv[j])) {
+                       ERROR("uxc: failed to load %s\n", gl.gl_pathv[j]);
+                       continue;
+               }
+               blobmsg_close_table(target, o);
+       }
+       blobmsg_close_table(target, c);
+       globfree(&gl);
+
+       return 0;
+}
+
+enum {
+       LIST_INSTANCES,
+       __LIST_MAX,
+};
+
+static const struct blobmsg_policy list_policy[__LIST_MAX] = {
+       [LIST_INSTANCES] = { .name = "instances", .type = BLOBMSG_TYPE_TABLE },
+};
+
+enum {
+       INSTANCE_RUNNING,
+       INSTANCE_PID,
+       INSTANCE_EXITCODE,
+       INSTANCE_JAIL,
+       __INSTANCE_MAX,
+};
+
+static const struct blobmsg_policy instance_policy[__INSTANCE_MAX] = {
+       [INSTANCE_RUNNING] = { .name = "running", .type = BLOBMSG_TYPE_BOOL },
+       [INSTANCE_PID] = { .name = "pid", .type = BLOBMSG_TYPE_INT32 },
+       [INSTANCE_EXITCODE] = { .name = "exit_code", .type = BLOBMSG_TYPE_INT32 },
+       [INSTANCE_JAIL] = { .name = "jail", .type = BLOBMSG_TYPE_TABLE },
+};
+
+enum {
+       JAIL_NAME,
+       __JAIL_MAX,
+};
+
+static const struct blobmsg_policy jail_policy[__JAIL_MAX] = {
+       [JAIL_NAME] = { .name = "name", .type = BLOBMSG_TYPE_STRING },
+};
+
+static struct runtime_state *
+runtime_alloc(const char *container_name)
+{
+       struct runtime_state *s;
+       char *new_name;
+       s = calloc_a(sizeof(*s), &new_name, strlen(container_name) + 1);
+       strcpy(new_name, container_name);
+       s->container_name = new_name;
+       s->avl.key = s->container_name;
+       return s;
+}
+
+static void list_cb(struct ubus_request *req, int type, struct blob_attr *msg)
+{
+       struct blob_attr *cur, *curi, *tl[__LIST_MAX], *ti[__INSTANCE_MAX], *tj[__JAIL_MAX];
+       int rem, remi;
+       const char *container_name, *instance_name, *jail_name;
+       bool running;
+       int pid, exitcode;
+       struct runtime_state *rs;
+
+       blobmsg_for_each_attr(cur, msg, rem) {
+               container_name = blobmsg_name(cur);
+               blobmsg_parse(list_policy, __LIST_MAX, tl, blobmsg_data(cur), blobmsg_len(cur));
+               if (!tl[LIST_INSTANCES])
+                       continue;
+
+               blobmsg_for_each_attr(curi, tl[LIST_INSTANCES], remi) {
+                       instance_name = blobmsg_name(curi);
+                       blobmsg_parse(instance_policy, __INSTANCE_MAX, ti, blobmsg_data(curi), blobmsg_len(curi));
+
+                       if (!ti[INSTANCE_JAIL])
+                               continue;
+
+                       blobmsg_parse(jail_policy, __JAIL_MAX, tj, blobmsg_data(ti[INSTANCE_JAIL]), blobmsg_len(ti[INSTANCE_JAIL]));
+                       if (!tj[JAIL_NAME])
+                               continue;
+
+                       jail_name = blobmsg_get_string(tj[JAIL_NAME]);
+
+                       running = ti[INSTANCE_RUNNING] && blobmsg_get_bool(ti[INSTANCE_RUNNING]);
+
+                       if (ti[INSTANCE_PID])
+                               pid = blobmsg_get_u32(ti[INSTANCE_PID]);
+                       else
+                               pid = -1;
+
+                       if (ti[INSTANCE_EXITCODE])
+                               exitcode = blobmsg_get_u32(ti[INSTANCE_EXITCODE]);
+                       else
+                               exitcode = -1;
+
+                       rs = runtime_alloc(container_name);
+                       rs->instance_name = strdup(instance_name);
+                       rs->jail_name = strdup(jail_name);
+                       rs->pid = pid;
+                       rs->exitcode = exitcode;
+                       rs->running = running;
+                       avl_insert(&runtime, &rs->avl);
+               }
+       }
+
+       return;
+}
+
+static int runtime_load(void)
+{
+       uint32_t id;
+
+       avl_init(&runtime, avl_strcmp, false, NULL);
+       if (ubus_lookup_id(ctx, "container", &id) ||
+               ubus_invoke(ctx, id, "list", NULL, list_cb, &runtime, 3000))
+               return EIO;
+
+       return 0;
+}
+
+static void runtime_free(void)
+{
+       struct runtime_state *item, *tmp;
+
+       avl_for_each_element_safe(&runtime, item, avl, tmp) {
+               avl_delete(&runtime, &item->avl);
+               free(item);
+       }
+
+       return;
+}
+
+static int uxc_list(void)
+{
+       struct blob_attr *cur, *tb[__CONF_MAX];
+       int rem;
+       struct runtime_state *s = NULL;
+       char *name;
+       bool autostart;
+
+       blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
+               blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
+               if (!tb[CONF_NAME] || !tb[CONF_PATH])
+                       continue;
+
+               autostart = tb[CONF_AUTOSTART] && blobmsg_get_bool(tb[CONF_AUTOSTART]);
+
+               name = blobmsg_get_string(tb[CONF_NAME]);
+               s = avl_find_element(&runtime, name, s, avl);
+
+               printf("[%c] %s %s", autostart?'*':' ', name, (s && s->running)?"RUNNING":"STOPPED");
+
+               if (s && !s->running && (s->exitcode >= 0))
+                       printf(" exitcode: %d (%s)", s->exitcode, strerror(s->exitcode));
+
+               if (s && s->running && (s->pid >= 0))
+                       printf(" pid: %d", s->pid);
+
+               printf("\n");
+       }
+
+       return 0;
+}
+
+static int uxc_start(char *name)
+{
+       static struct blob_buf req;
+       struct blob_attr *cur, *tb[__CONF_MAX];
+       int rem, ret;
+       uint32_t id;
+       struct runtime_state *s = NULL;
+       char *path = NULL, *jailname = NULL;
+       void *in, *ins, *j;
+       bool found = false;
+
+       blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
+               blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
+               if (!tb[CONF_NAME] || !tb[CONF_PATH])
+                       continue;
+
+               if (strcmp(name, blobmsg_get_string(tb[CONF_NAME])))
+                       continue;
+
+               found = true;
+               path = strdup(blobmsg_get_string(tb[CONF_PATH]));
+
+
+               break;
+       }
+
+       if (!found)
+               return ENOENT;
+
+       s = avl_find_element(&runtime, name, s, avl);
+
+       if (s && (s->running))
+               return EEXIST;
+
+       if (tb[CONF_JAIL])
+               jailname = strdup(blobmsg_get_string(tb[CONF_JAIL]));
+
+       blob_buf_init(&req, 0);
+       blobmsg_add_string(&req, "name", name);
+       ins = blobmsg_open_table(&req, "instances");
+       in = blobmsg_open_table(&req, name);
+       blobmsg_add_string(&req, "bundle", path);
+       j = blobmsg_open_table(&req, "jail");
+       blobmsg_add_string(&req, "name", jailname?:name);
+       blobmsg_close_table(&req, j);
+       blobmsg_close_table(&req, in);
+       blobmsg_close_table(&req, ins);
+
+       ret = 0;
+       if (ubus_lookup_id(ctx, "container", &id) ||
+               ubus_invoke(ctx, id, "add", req.head, NULL, NULL, 3000)) {
+               ret = EIO;
+       }
+
+       free(jailname);
+       free(path);
+       blob_buf_free(&req);
+
+       return ret;
+}
+
+static int uxc_stop(char *name)
+{
+       static struct blob_buf req;
+       struct blob_attr *cur, *tb[__CONF_MAX];
+       int rem, ret;
+       uint32_t id;
+       struct runtime_state *s = NULL;
+       bool found = false;
+
+       blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
+               blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
+               if (!tb[CONF_NAME] || !tb[CONF_PATH])
+                       continue;
+
+               if (strcmp(name, blobmsg_get_string(tb[CONF_NAME])))
+                       continue;
+
+               found = true;
+               break;
+       }
+
+       if (!found)
+               return ENOENT;
+
+       s = avl_find_element(&runtime, name, s, avl);
+
+       if (!s || !(s->running))
+               return ENOENT;
+
+       blob_buf_init(&req, 0);
+       blobmsg_add_string(&req, "name", name);
+       blobmsg_add_string(&req, "instance", s->instance_name);
+
+       ret = 0;
+       if (ubus_lookup_id(ctx, "container", &id) ||
+               ubus_invoke(ctx, id, "del", req.head, NULL, NULL, 3000)) {
+               ret = EIO;
+       }
+
+       blob_buf_free(&req);
+
+       return ret;
+}
+
+static int uxc_set(char *name, char *path, bool autostart, bool add)
+{
+       static struct blob_buf req;
+       struct blob_attr *cur, *tb[__CONF_MAX];
+       int rem, ret;
+       bool found = false;
+       char *fname = NULL;
+       char *keeppath = NULL;
+       int f;
+       struct stat sb;
+
+       blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
+               blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
+               if (!tb[CONF_NAME] || !tb[CONF_PATH])
+                       continue;
+
+               if (strcmp(name, blobmsg_get_string(tb[CONF_NAME])))
+                       continue;
+
+               found = true;
+               break;
+       }
+
+       if (found && add)
+               return EEXIST;
+
+       if (!found && !add)
+               return ENOENT;
+
+       if (add && !path)
+               return EINVAL;
+
+       if (path) {
+               if (stat(path, &sb) == -1)
+                       return ENOENT;
+
+               if ((sb.st_mode & S_IFMT) != S_IFDIR)
+                       return ENOTDIR;
+       }
+
+       ret = mkdir(UXC_CONFDIR, 0755);
+
+       if (ret && errno != EEXIST)
+               return ret;
+
+       if (asprintf(&fname, "%s/%s.json", UXC_CONFDIR, name) < 1)
+               return ENOMEM;
+
+       f = open(fname, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+       if (f < 0)
+               return errno;
+
+       if (!add)
+               keeppath = strdup(blobmsg_get_string(tb[CONF_PATH]));
+
+       blob_buf_init(&req, 0);
+       blobmsg_add_string(&req, "name", name);
+       blobmsg_add_string(&req, "path", path?:keeppath);
+       blobmsg_add_u8(&req, "autostart", autostart);
+
+       dprintf(f, "%s\n", blobmsg_format_json_indent(req.head, true, 0));
+
+       if (!add)
+               free(keeppath);
+
+       blob_buf_free(&req);
+
+       /* ToDo: tell ujail to run createRuntime and createContainer hooks */
+       return 0;
+}
+
+static int uxc_boot(void)
+{
+       struct blob_attr *cur, *tb[__CONF_MAX];
+       int rem, ret = 0;
+       char *name;
+
+       blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
+               blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
+               if (!tb[CONF_NAME] || !tb[CONF_PATH] || !tb[CONF_AUTOSTART] || !blobmsg_get_bool(tb[CONF_AUTOSTART]))
+                       continue;
+
+               name = strdup(blobmsg_get_string(tb[CONF_NAME]));
+               ret += uxc_start(name);
+               free(name);
+       }
+
+       return ret;
+}
+
+static int uxc_delete(char *name)
+{
+       struct blob_attr *cur, *tb[__CONF_MAX];
+       int rem, ret = 0;
+       bool found = false;
+       char *fname;
+       struct stat sb;
+
+       blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
+               blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
+               if (!tb[CONF_NAME] || !tb[CONF_PATH])
+                       continue;
+
+               if (strcmp(name, blobmsg_get_string(tb[CONF_NAME])))
+                       continue;
+
+               fname = strdup(blobmsg_name(cur));
+               if (!fname)
+                       return errno;
+
+               found = true;
+               break;
+       }
+
+       if (!found)
+               return ENOENT;
+
+       if (stat(fname, &sb) == -1) {
+               ret=ENOENT;
+               goto errout;
+       }
+
+       if (unlink(fname) == -1)
+               ret=errno;
+
+errout:
+       free(fname);
+       return ret;
+}
+
+int main(int argc, char **argv)
+{
+       int ret = EINVAL;
+
+       if (argc < 2)
+               return usage();
+
+       ctx = ubus_connect(NULL);
+       if (!ctx)
+               return ENODEV;
+
+       ret = conf_load(false);
+       if (ret)
+               goto out;
+
+       ret = mkdir(UXC_RUNDIR, 0755);
+       if (ret && errno != EEXIST)
+               goto conf_out;
+
+       ret = conf_load(true);
+       if (ret)
+               goto conf_out;
+
+       ret = runtime_load();
+       if (ret)
+               goto state_out;
+
+       if (!strcmp("list", argv[1]))
+               ret = uxc_list();
+       else if (!strcmp("boot", argv[1]))
+               ret = uxc_boot();
+       else if(!strcmp("start", argv[1])) {
+               if (argc < 3)
+                       goto usage_out;
+
+               ret = uxc_start(argv[2]);
+       } else if(!strcmp("stop", argv[1])) {
+               if (argc < 3)
+                       goto usage_out;
+
+               ret = uxc_stop(argv[2]);
+       } else if(!strcmp("enable", argv[1])) {
+               if (argc < 3)
+                       goto usage_out;
+
+               ret = uxc_set(argv[2], NULL, true, false);
+       } else if(!strcmp("disable", argv[1])) {
+               if (argc < 3)
+                       goto usage_out;
+
+               ret = uxc_set(argv[2], NULL, false, false);
+       } else if(!strcmp("delete", argv[1])) {
+               if (argc < 3)
+                       goto usage_out;
+
+               ret = uxc_delete(argv[2]);
+       } else if(!strcmp("create", argv[1])) {
+               bool autostart = false;
+               if (argc < 4)
+                       goto usage_out;
+
+               if (argc == 5) {
+                       if (!strncmp("true", argv[4], 5))
+                               autostart = true;
+                       else
+                               autostart = atoi(argv[4]);
+               }
+               ret = uxc_set(argv[2], argv[3], autostart, true);
+       } else
+               goto usage_out;
+
+       goto runtime_out;
+
+usage_out:
+       usage();
+runtime_out:
+       runtime_free();
+state_out:
+       blob_buf_free(&state);
+conf_out:
+       blob_buf_free(&conf);
+out:
+       ubus_free(ctx);
+
+       if (ret != 0)
+               fprintf(stderr, "uxc error: %s\n", strerror(ret));
+
+       return ret;
+}