uxc: add container management CLI tool
[project/procd.git] / uxc.c
1 /*
2 * Copyright (C) 2020 Daniel Golle <daniel@makrotopia.org>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License version 2.1
6 * as published by the Free Software Foundation
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 */
13
14 #include <stdlib.h>
15 #include <stdbool.h>
16 #include <fcntl.h>
17 #include <libubus.h>
18 #include <libubox/avl-cmp.h>
19 #include <libubox/blobmsg.h>
20 #include <libubox/blobmsg_json.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <unistd.h>
24 #include <fcntl.h>
25 #include <errno.h>
26 #include <sys/stat.h>
27 #include <sys/types.h>
28 #include <glob.h>
29
30 #include "log.h"
31
32 #define UXC_CONFDIR "/etc/uxc"
33 #define UXC_RUNDIR "/var/run/uxc"
34
35 struct runtime_state {
36 struct avl_node avl;
37 const char *container_name;
38 const char *instance_name;
39 const char *jail_name;
40 bool running;
41 int pid;
42 int exitcode;
43 };
44
45 AVL_TREE(runtime, avl_strcmp, false, NULL);
46 static struct blob_buf conf;
47 static struct blob_buf state;
48
49 static struct ubus_context *ctx;
50
51 static int usage(void) {
52 printf("syntax: uxc {command} [parameters ...]\n");
53 printf("commands:\n");
54 printf("\tlist\t\t\t\tlist all configured containers\n");
55 printf("\tcreate {conf} {path} [enabled]\tcreate {conf} for OCI bundle at {path}\n");
56 printf("\tstart {conf}\t\t\tstart container {conf}\n");
57 printf("\tstop {conf}\t\t\tstop container {conf}\n");
58 printf("\tenable {conf}\t\t\tstart container {conf} on boot\n");
59 printf("\tdisable {conf}\t\t\tdon't start container {conf} on boot\n");
60 printf("\tdelete {conf}\t\t\tdelete {conf}\n");
61 return EINVAL;
62 }
63
64 enum {
65 CONF_NAME,
66 CONF_PATH,
67 CONF_JAIL,
68 CONF_AUTOSTART,
69 __CONF_MAX,
70 };
71
72 static const struct blobmsg_policy conf_policy[__CONF_MAX] = {
73 [CONF_NAME] = { .name = "name", .type = BLOBMSG_TYPE_STRING },
74 [CONF_PATH] = { .name = "path", .type = BLOBMSG_TYPE_STRING },
75 [CONF_JAIL] = { .name = "jail", .type = BLOBMSG_TYPE_STRING },
76 [CONF_AUTOSTART] = { .name = "autostart", .type = BLOBMSG_TYPE_BOOL },
77 };
78
79 static int conf_load(bool load_state)
80 {
81 int gl_flags = GLOB_NOESCAPE | GLOB_MARK;
82 int j, res;
83 glob_t gl;
84 char *globstr;
85 struct blob_buf *target = load_state?&state:&conf;
86 void *c, *o;
87
88 if (asprintf(&globstr, "%s/*.json", load_state?UXC_RUNDIR:UXC_CONFDIR) == -1)
89 return ENOMEM;
90
91 blob_buf_init(target, 0);
92 c = blobmsg_open_table(target, NULL);
93
94 res = glob(globstr, gl_flags, NULL, &gl);
95 free(globstr);
96 if (res < 0)
97 return 0;
98
99 for (j = 0; j < gl.gl_pathc; j++) {
100 o = blobmsg_open_table(target, strdup(gl.gl_pathv[j]));
101 if (!blobmsg_add_json_from_file(target, gl.gl_pathv[j])) {
102 ERROR("uxc: failed to load %s\n", gl.gl_pathv[j]);
103 continue;
104 }
105 blobmsg_close_table(target, o);
106 }
107 blobmsg_close_table(target, c);
108 globfree(&gl);
109
110 return 0;
111 }
112
113 enum {
114 LIST_INSTANCES,
115 __LIST_MAX,
116 };
117
118 static const struct blobmsg_policy list_policy[__LIST_MAX] = {
119 [LIST_INSTANCES] = { .name = "instances", .type = BLOBMSG_TYPE_TABLE },
120 };
121
122 enum {
123 INSTANCE_RUNNING,
124 INSTANCE_PID,
125 INSTANCE_EXITCODE,
126 INSTANCE_JAIL,
127 __INSTANCE_MAX,
128 };
129
130 static const struct blobmsg_policy instance_policy[__INSTANCE_MAX] = {
131 [INSTANCE_RUNNING] = { .name = "running", .type = BLOBMSG_TYPE_BOOL },
132 [INSTANCE_PID] = { .name = "pid", .type = BLOBMSG_TYPE_INT32 },
133 [INSTANCE_EXITCODE] = { .name = "exit_code", .type = BLOBMSG_TYPE_INT32 },
134 [INSTANCE_JAIL] = { .name = "jail", .type = BLOBMSG_TYPE_TABLE },
135 };
136
137 enum {
138 JAIL_NAME,
139 __JAIL_MAX,
140 };
141
142 static const struct blobmsg_policy jail_policy[__JAIL_MAX] = {
143 [JAIL_NAME] = { .name = "name", .type = BLOBMSG_TYPE_STRING },
144 };
145
146 static struct runtime_state *
147 runtime_alloc(const char *container_name)
148 {
149 struct runtime_state *s;
150 char *new_name;
151 s = calloc_a(sizeof(*s), &new_name, strlen(container_name) + 1);
152 strcpy(new_name, container_name);
153 s->container_name = new_name;
154 s->avl.key = s->container_name;
155 return s;
156 }
157
158 static void list_cb(struct ubus_request *req, int type, struct blob_attr *msg)
159 {
160 struct blob_attr *cur, *curi, *tl[__LIST_MAX], *ti[__INSTANCE_MAX], *tj[__JAIL_MAX];
161 int rem, remi;
162 const char *container_name, *instance_name, *jail_name;
163 bool running;
164 int pid, exitcode;
165 struct runtime_state *rs;
166
167 blobmsg_for_each_attr(cur, msg, rem) {
168 container_name = blobmsg_name(cur);
169 blobmsg_parse(list_policy, __LIST_MAX, tl, blobmsg_data(cur), blobmsg_len(cur));
170 if (!tl[LIST_INSTANCES])
171 continue;
172
173 blobmsg_for_each_attr(curi, tl[LIST_INSTANCES], remi) {
174 instance_name = blobmsg_name(curi);
175 blobmsg_parse(instance_policy, __INSTANCE_MAX, ti, blobmsg_data(curi), blobmsg_len(curi));
176
177 if (!ti[INSTANCE_JAIL])
178 continue;
179
180 blobmsg_parse(jail_policy, __JAIL_MAX, tj, blobmsg_data(ti[INSTANCE_JAIL]), blobmsg_len(ti[INSTANCE_JAIL]));
181 if (!tj[JAIL_NAME])
182 continue;
183
184 jail_name = blobmsg_get_string(tj[JAIL_NAME]);
185
186 running = ti[INSTANCE_RUNNING] && blobmsg_get_bool(ti[INSTANCE_RUNNING]);
187
188 if (ti[INSTANCE_PID])
189 pid = blobmsg_get_u32(ti[INSTANCE_PID]);
190 else
191 pid = -1;
192
193 if (ti[INSTANCE_EXITCODE])
194 exitcode = blobmsg_get_u32(ti[INSTANCE_EXITCODE]);
195 else
196 exitcode = -1;
197
198 rs = runtime_alloc(container_name);
199 rs->instance_name = strdup(instance_name);
200 rs->jail_name = strdup(jail_name);
201 rs->pid = pid;
202 rs->exitcode = exitcode;
203 rs->running = running;
204 avl_insert(&runtime, &rs->avl);
205 }
206 }
207
208 return;
209 }
210
211 static int runtime_load(void)
212 {
213 uint32_t id;
214
215 avl_init(&runtime, avl_strcmp, false, NULL);
216 if (ubus_lookup_id(ctx, "container", &id) ||
217 ubus_invoke(ctx, id, "list", NULL, list_cb, &runtime, 3000))
218 return EIO;
219
220 return 0;
221 }
222
223 static void runtime_free(void)
224 {
225 struct runtime_state *item, *tmp;
226
227 avl_for_each_element_safe(&runtime, item, avl, tmp) {
228 avl_delete(&runtime, &item->avl);
229 free(item);
230 }
231
232 return;
233 }
234
235 static int uxc_list(void)
236 {
237 struct blob_attr *cur, *tb[__CONF_MAX];
238 int rem;
239 struct runtime_state *s = NULL;
240 char *name;
241 bool autostart;
242
243 blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
244 blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
245 if (!tb[CONF_NAME] || !tb[CONF_PATH])
246 continue;
247
248 autostart = tb[CONF_AUTOSTART] && blobmsg_get_bool(tb[CONF_AUTOSTART]);
249
250 name = blobmsg_get_string(tb[CONF_NAME]);
251 s = avl_find_element(&runtime, name, s, avl);
252
253 printf("[%c] %s %s", autostart?'*':' ', name, (s && s->running)?"RUNNING":"STOPPED");
254
255 if (s && !s->running && (s->exitcode >= 0))
256 printf(" exitcode: %d (%s)", s->exitcode, strerror(s->exitcode));
257
258 if (s && s->running && (s->pid >= 0))
259 printf(" pid: %d", s->pid);
260
261 printf("\n");
262 }
263
264 return 0;
265 }
266
267 static int uxc_start(char *name)
268 {
269 static struct blob_buf req;
270 struct blob_attr *cur, *tb[__CONF_MAX];
271 int rem, ret;
272 uint32_t id;
273 struct runtime_state *s = NULL;
274 char *path = NULL, *jailname = NULL;
275 void *in, *ins, *j;
276 bool found = false;
277
278 blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
279 blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
280 if (!tb[CONF_NAME] || !tb[CONF_PATH])
281 continue;
282
283 if (strcmp(name, blobmsg_get_string(tb[CONF_NAME])))
284 continue;
285
286 found = true;
287 path = strdup(blobmsg_get_string(tb[CONF_PATH]));
288
289
290 break;
291 }
292
293 if (!found)
294 return ENOENT;
295
296 s = avl_find_element(&runtime, name, s, avl);
297
298 if (s && (s->running))
299 return EEXIST;
300
301 if (tb[CONF_JAIL])
302 jailname = strdup(blobmsg_get_string(tb[CONF_JAIL]));
303
304 blob_buf_init(&req, 0);
305 blobmsg_add_string(&req, "name", name);
306 ins = blobmsg_open_table(&req, "instances");
307 in = blobmsg_open_table(&req, name);
308 blobmsg_add_string(&req, "bundle", path);
309 j = blobmsg_open_table(&req, "jail");
310 blobmsg_add_string(&req, "name", jailname?:name);
311 blobmsg_close_table(&req, j);
312 blobmsg_close_table(&req, in);
313 blobmsg_close_table(&req, ins);
314
315 ret = 0;
316 if (ubus_lookup_id(ctx, "container", &id) ||
317 ubus_invoke(ctx, id, "add", req.head, NULL, NULL, 3000)) {
318 ret = EIO;
319 }
320
321 free(jailname);
322 free(path);
323 blob_buf_free(&req);
324
325 return ret;
326 }
327
328 static int uxc_stop(char *name)
329 {
330 static struct blob_buf req;
331 struct blob_attr *cur, *tb[__CONF_MAX];
332 int rem, ret;
333 uint32_t id;
334 struct runtime_state *s = NULL;
335 bool found = false;
336
337 blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
338 blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
339 if (!tb[CONF_NAME] || !tb[CONF_PATH])
340 continue;
341
342 if (strcmp(name, blobmsg_get_string(tb[CONF_NAME])))
343 continue;
344
345 found = true;
346 break;
347 }
348
349 if (!found)
350 return ENOENT;
351
352 s = avl_find_element(&runtime, name, s, avl);
353
354 if (!s || !(s->running))
355 return ENOENT;
356
357 blob_buf_init(&req, 0);
358 blobmsg_add_string(&req, "name", name);
359 blobmsg_add_string(&req, "instance", s->instance_name);
360
361 ret = 0;
362 if (ubus_lookup_id(ctx, "container", &id) ||
363 ubus_invoke(ctx, id, "del", req.head, NULL, NULL, 3000)) {
364 ret = EIO;
365 }
366
367 blob_buf_free(&req);
368
369 return ret;
370 }
371
372 static int uxc_set(char *name, char *path, bool autostart, bool add)
373 {
374 static struct blob_buf req;
375 struct blob_attr *cur, *tb[__CONF_MAX];
376 int rem, ret;
377 bool found = false;
378 char *fname = NULL;
379 char *keeppath = NULL;
380 int f;
381 struct stat sb;
382
383 blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
384 blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
385 if (!tb[CONF_NAME] || !tb[CONF_PATH])
386 continue;
387
388 if (strcmp(name, blobmsg_get_string(tb[CONF_NAME])))
389 continue;
390
391 found = true;
392 break;
393 }
394
395 if (found && add)
396 return EEXIST;
397
398 if (!found && !add)
399 return ENOENT;
400
401 if (add && !path)
402 return EINVAL;
403
404 if (path) {
405 if (stat(path, &sb) == -1)
406 return ENOENT;
407
408 if ((sb.st_mode & S_IFMT) != S_IFDIR)
409 return ENOTDIR;
410 }
411
412 ret = mkdir(UXC_CONFDIR, 0755);
413
414 if (ret && errno != EEXIST)
415 return ret;
416
417 if (asprintf(&fname, "%s/%s.json", UXC_CONFDIR, name) < 1)
418 return ENOMEM;
419
420 f = open(fname, O_WRONLY | O_CREAT | O_TRUNC, 0644);
421 if (f < 0)
422 return errno;
423
424 if (!add)
425 keeppath = strdup(blobmsg_get_string(tb[CONF_PATH]));
426
427 blob_buf_init(&req, 0);
428 blobmsg_add_string(&req, "name", name);
429 blobmsg_add_string(&req, "path", path?:keeppath);
430 blobmsg_add_u8(&req, "autostart", autostart);
431
432 dprintf(f, "%s\n", blobmsg_format_json_indent(req.head, true, 0));
433
434 if (!add)
435 free(keeppath);
436
437 blob_buf_free(&req);
438
439 /* ToDo: tell ujail to run createRuntime and createContainer hooks */
440 return 0;
441 }
442
443 static int uxc_boot(void)
444 {
445 struct blob_attr *cur, *tb[__CONF_MAX];
446 int rem, ret = 0;
447 char *name;
448
449 blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
450 blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
451 if (!tb[CONF_NAME] || !tb[CONF_PATH] || !tb[CONF_AUTOSTART] || !blobmsg_get_bool(tb[CONF_AUTOSTART]))
452 continue;
453
454 name = strdup(blobmsg_get_string(tb[CONF_NAME]));
455 ret += uxc_start(name);
456 free(name);
457 }
458
459 return ret;
460 }
461
462 static int uxc_delete(char *name)
463 {
464 struct blob_attr *cur, *tb[__CONF_MAX];
465 int rem, ret = 0;
466 bool found = false;
467 char *fname;
468 struct stat sb;
469
470 blobmsg_for_each_attr(cur, blob_data(conf.head), rem) {
471 blobmsg_parse(conf_policy, __CONF_MAX, tb, blobmsg_data(cur), blobmsg_len(cur));
472 if (!tb[CONF_NAME] || !tb[CONF_PATH])
473 continue;
474
475 if (strcmp(name, blobmsg_get_string(tb[CONF_NAME])))
476 continue;
477
478 fname = strdup(blobmsg_name(cur));
479 if (!fname)
480 return errno;
481
482 found = true;
483 break;
484 }
485
486 if (!found)
487 return ENOENT;
488
489 if (stat(fname, &sb) == -1) {
490 ret=ENOENT;
491 goto errout;
492 }
493
494 if (unlink(fname) == -1)
495 ret=errno;
496
497 errout:
498 free(fname);
499 return ret;
500 }
501
502 int main(int argc, char **argv)
503 {
504 int ret = EINVAL;
505
506 if (argc < 2)
507 return usage();
508
509 ctx = ubus_connect(NULL);
510 if (!ctx)
511 return ENODEV;
512
513 ret = conf_load(false);
514 if (ret)
515 goto out;
516
517 ret = mkdir(UXC_RUNDIR, 0755);
518 if (ret && errno != EEXIST)
519 goto conf_out;
520
521 ret = conf_load(true);
522 if (ret)
523 goto conf_out;
524
525 ret = runtime_load();
526 if (ret)
527 goto state_out;
528
529 if (!strcmp("list", argv[1]))
530 ret = uxc_list();
531 else if (!strcmp("boot", argv[1]))
532 ret = uxc_boot();
533 else if(!strcmp("start", argv[1])) {
534 if (argc < 3)
535 goto usage_out;
536
537 ret = uxc_start(argv[2]);
538 } else if(!strcmp("stop", argv[1])) {
539 if (argc < 3)
540 goto usage_out;
541
542 ret = uxc_stop(argv[2]);
543 } else if(!strcmp("enable", argv[1])) {
544 if (argc < 3)
545 goto usage_out;
546
547 ret = uxc_set(argv[2], NULL, true, false);
548 } else if(!strcmp("disable", argv[1])) {
549 if (argc < 3)
550 goto usage_out;
551
552 ret = uxc_set(argv[2], NULL, false, false);
553 } else if(!strcmp("delete", argv[1])) {
554 if (argc < 3)
555 goto usage_out;
556
557 ret = uxc_delete(argv[2]);
558 } else if(!strcmp("create", argv[1])) {
559 bool autostart = false;
560 if (argc < 4)
561 goto usage_out;
562
563 if (argc == 5) {
564 if (!strncmp("true", argv[4], 5))
565 autostart = true;
566 else
567 autostart = atoi(argv[4]);
568 }
569 ret = uxc_set(argv[2], argv[3], autostart, true);
570 } else
571 goto usage_out;
572
573 goto runtime_out;
574
575 usage_out:
576 usage();
577 runtime_out:
578 runtime_free();
579 state_out:
580 blob_buf_free(&state);
581 conf_out:
582 blob_buf_free(&conf);
583 out:
584 ubus_free(ctx);
585
586 if (ret != 0)
587 fprintf(stderr, "uxc error: %s\n", strerror(ret));
588
589 return ret;
590 }