1 /* SPDX-License-Identifier: LGPL-2.1+ */
8 #include "alloc-util.h"
12 #include "dirent-util.h"
16 #include "format-table.h"
18 #include "locale-util.h"
19 #include "machine-image.h"
20 #include "main-func.h"
22 #include "parse-util.h"
23 #include "path-util.h"
24 #include "pretty-print.h"
25 #include "spawn-polkit-agent.h"
26 #include "string-util.h"
28 #include "terminal-util.h"
31 static PagerFlags arg_pager_flags
= 0;
32 static bool arg_legend
= true;
33 static bool arg_ask_password
= true;
34 static bool arg_quiet
= false;
35 static const char *arg_profile
= "default";
36 static const char* arg_copy_mode
= NULL
;
37 static bool arg_runtime
= false;
38 static bool arg_reload
= true;
39 static bool arg_cat
= false;
40 static BusTransport arg_transport
= BUS_TRANSPORT_LOCAL
;
41 static const char *arg_host
= NULL
;
43 static int determine_image(const char *image
, bool permit_non_existing
, char **ret
) {
46 /* If the specified name is a valid image name, we pass it as-is to portabled, which will search for it in the
47 * usual search directories. Otherwise we presume it's a path, and will normalize it on the client's side
48 * (among other things, to make the path independent of the client's working directory) before passing it
51 if (image_name_is_valid(image
)) {
54 if (!arg_quiet
&& laccess(image
, F_OK
) >= 0)
55 log_warning("Ambiguous invocation: current working directory contains file matching non-path argument '%s', ignoring. "
56 "Prefix argument with './' to force reference to file in current working directory.", image
);
66 if (arg_transport
!= BUS_TRANSPORT_LOCAL
)
67 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP
),
68 "Operations on images by path not supported when connecting to remote systems.");
70 r
= chase_symlinks(image
, NULL
, CHASE_TRAIL_SLASH
| (permit_non_existing
? CHASE_NONEXISTENT
: 0), ret
);
72 return log_error_errno(r
, "Cannot normalize specified image path '%s': %m", image
);
77 static int extract_prefix(const char *path
, char **ret
) {
78 _cleanup_free_
char *name
= NULL
;
79 const char *bn
, *underscore
;
84 underscore
= strchr(bn
, '_');
90 e
= endswith(bn
, ".raw");
97 name
= strndup(bn
, m
);
101 /* A slightly reduced version of what's permitted in unit names. With ':' and '\' are removed, as well as '_'
102 * which we use as delimiter for the second part of the image string, which we ignore for now. */
103 if (!in_charset(name
, DIGITS LETTERS
"-."))
106 if (!filename_is_valid(name
))
109 *ret
= TAKE_PTR(name
);
114 static int determine_matches(const char *image
, char **l
, bool allow_any
, char ***ret
) {
115 _cleanup_strv_free_
char **k
= NULL
;
118 /* Determine the matches to apply. If the list is empty we derive the match from the image name. If the list
119 * contains exactly the "-" we return a wildcard list (which is the empty list), but only if this is expressly
122 if (strv_isempty(l
)) {
125 r
= extract_prefix(image
, &prefix
);
127 return log_error_errno(r
, "Failed to extract prefix of image name '%s': %m", image
);
130 log_info("(Matching unit files with prefix '%s'.)", prefix
);
132 r
= strv_consume(&k
, prefix
);
136 } else if (strv_equal(l
, STRV_MAKE("-"))) {
139 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
140 "Refusing all unit file match.");
143 log_info("(Matching all unit files.)");
151 _cleanup_free_
char *joined
= NULL
;
153 joined
= strv_join(k
, "', '");
157 log_info("(Matching unit files with prefixes '%s'.)", joined
);
166 static int acquire_bus(sd_bus
**bus
) {
174 r
= bus_connect_transport(arg_transport
, arg_host
, false, bus
);
176 return log_error_errno(r
, "Failed to connect to bus: %m");
178 (void) sd_bus_set_allow_interactive_authorization(*bus
, arg_ask_password
);
183 static int maybe_reload(sd_bus
**bus
) {
184 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
185 _cleanup_(sd_bus_message_unrefp
) sd_bus_message
*m
= NULL
;
191 r
= acquire_bus(bus
);
195 r
= sd_bus_message_new_method_call(
198 "org.freedesktop.systemd1",
199 "/org/freedesktop/systemd1",
200 "org.freedesktop.systemd1.Manager",
203 return bus_log_create_error(r
);
205 /* Reloading the daemon may take long, hence set a longer timeout here */
206 r
= sd_bus_call(*bus
, m
, DEFAULT_TIMEOUT_USEC
* 2, &error
, NULL
);
208 return log_error_errno(r
, "Failed to reload daemon: %s", bus_error_message(&error
, r
));
213 static int inspect_image(int argc
, char *argv
[], void *userdata
) {
214 _cleanup_(sd_bus_message_unrefp
) sd_bus_message
*m
= NULL
, *reply
= NULL
;
215 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
216 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
217 _cleanup_strv_free_
char **matches
= NULL
;
218 _cleanup_free_
char *image
= NULL
;
219 bool nl
= false, header
= false;
225 r
= determine_image(argv
[1], false, &image
);
229 r
= determine_matches(argv
[1], argv
+ 2, true, &matches
);
233 r
= acquire_bus(&bus
);
237 r
= sd_bus_message_new_method_call(
240 "org.freedesktop.portable1",
241 "/org/freedesktop/portable1",
242 "org.freedesktop.portable1.Manager",
245 return bus_log_create_error(r
);
247 r
= sd_bus_message_append(m
, "s", image
);
249 return bus_log_create_error(r
);
251 r
= sd_bus_message_append_strv(m
, matches
);
253 return bus_log_create_error(r
);
255 r
= sd_bus_call(bus
, m
, 0, &error
, &reply
);
257 return log_error_errno(r
, "Failed to inspect image metadata: %s", bus_error_message(&error
, r
));
259 r
= sd_bus_message_read(reply
, "s", &path
);
261 return bus_log_parse_error(r
);
263 r
= sd_bus_message_read_array(reply
, 'y', &data
, &sz
);
265 return bus_log_parse_error(r
);
267 (void) pager_open(arg_pager_flags
);
270 printf("%s-- OS Release: --%s\n", ansi_highlight(), ansi_normal());
271 fwrite(data
, sz
, 1, stdout
);
275 _cleanup_free_
char *pretty_portable
= NULL
, *pretty_os
= NULL
;
276 _cleanup_fclose_
FILE *f
;
278 f
= fmemopen_unlocked((void*) data
, sz
, "re");
280 return log_error_errno(errno
, "Failed to open /etc/os-release buffer: %m");
282 r
= parse_env_file(f
, "/etc/os-release",
283 "PORTABLE_PRETTY_NAME", &pretty_portable
,
284 "PRETTY_NAME", &pretty_os
);
286 return log_error_errno(r
, "Failed to parse /etc/os-release: %m");
288 printf("Image:\n\t%s\n"
289 "Portable Service:\n\t%s\n"
290 "Operating System:\n\t%s\n",
292 strna(pretty_portable
),
296 r
= sd_bus_message_enter_container(reply
, 'a', "{say}");
298 return bus_log_parse_error(r
);
303 r
= sd_bus_message_enter_container(reply
, 'e', "say");
305 return bus_log_parse_error(r
);
309 r
= sd_bus_message_read(reply
, "s", &name
);
311 return bus_log_parse_error(r
);
313 r
= sd_bus_message_read_array(reply
, 'y', &data
, &sz
);
315 return bus_log_parse_error(r
);
321 printf("%s-- Unit file: %s --%s\n", ansi_highlight(), name
, ansi_normal());
322 fwrite(data
, sz
, 1, stdout
);
327 fputs("Unit files:\n", stdout
);
336 r
= sd_bus_message_exit_container(reply
);
338 return bus_log_parse_error(r
);
341 r
= sd_bus_message_exit_container(reply
);
343 return bus_log_parse_error(r
);
348 static int print_changes(sd_bus_message
*m
) {
354 r
= sd_bus_message_enter_container(m
, 'a', "(sss)");
356 return bus_log_parse_error(r
);
359 const char *type
, *path
, *source
;
361 r
= sd_bus_message_read(m
, "(sss)", &type
, &path
, &source
);
363 return bus_log_parse_error(r
);
367 if (streq(type
, "symlink"))
368 log_info("Created symlink %s %s %s.", path
, special_glyph(SPECIAL_GLYPH_ARROW
), source
);
369 else if (streq(type
, "copy")) {
371 log_info("Copied %s.", path
);
373 log_info("Copied %s %s %s.", source
, special_glyph(SPECIAL_GLYPH_ARROW
), path
);
374 } else if (streq(type
, "unlink"))
375 log_info("Removed %s.", path
);
376 else if (streq(type
, "write"))
377 log_info("Written %s.", path
);
378 else if (streq(type
, "mkdir"))
379 log_info("Created directory %s.", path
);
381 log_error("Unexpected change: %s/%s/%s", type
, path
, source
);
384 r
= sd_bus_message_exit_container(m
);
391 static int attach_image(int argc
, char *argv
[], void *userdata
) {
392 _cleanup_(sd_bus_message_unrefp
) sd_bus_message
*m
= NULL
, *reply
= NULL
;
393 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
394 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
395 _cleanup_strv_free_
char **matches
= NULL
;
396 _cleanup_free_
char *image
= NULL
;
399 r
= determine_image(argv
[1], false, &image
);
403 r
= determine_matches(argv
[1], argv
+ 2, false, &matches
);
407 r
= acquire_bus(&bus
);
411 (void) polkit_agent_open_if_enabled(arg_transport
, arg_ask_password
);
413 r
= sd_bus_message_new_method_call(
416 "org.freedesktop.portable1",
417 "/org/freedesktop/portable1",
418 "org.freedesktop.portable1.Manager",
421 return bus_log_create_error(r
);
423 r
= sd_bus_message_append(m
, "s", image
);
425 return bus_log_create_error(r
);
427 r
= sd_bus_message_append_strv(m
, matches
);
429 return bus_log_create_error(r
);
431 r
= sd_bus_message_append(m
, "sbs", arg_profile
, arg_runtime
, arg_copy_mode
);
433 return bus_log_create_error(r
);
435 r
= sd_bus_call(bus
, m
, 0, &error
, &reply
);
437 return log_error_errno(r
, "Failed to attach image: %s", bus_error_message(&error
, r
));
439 (void) maybe_reload(&bus
);
441 print_changes(reply
);
445 static int detach_image(int argc
, char *argv
[], void *userdata
) {
446 _cleanup_(sd_bus_message_unrefp
) sd_bus_message
*reply
= NULL
;
447 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
448 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
449 _cleanup_free_
char *image
= NULL
;
452 r
= determine_image(argv
[1], true, &image
);
456 r
= acquire_bus(&bus
);
460 (void) polkit_agent_open_if_enabled(arg_transport
, arg_ask_password
);
462 r
= sd_bus_call_method(
464 "org.freedesktop.portable1",
465 "/org/freedesktop/portable1",
466 "org.freedesktop.portable1.Manager",
470 "sb", image
, arg_runtime
);
472 return log_error_errno(r
, "Failed to detach image: %s", bus_error_message(&error
, r
));
474 (void) maybe_reload(&bus
);
476 print_changes(reply
);
480 static int list_images(int argc
, char *argv
[], void *userdata
) {
481 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
482 _cleanup_(sd_bus_message_unrefp
) sd_bus_message
*reply
= NULL
;
483 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
484 _cleanup_(table_unrefp
) Table
*table
= NULL
;
487 r
= acquire_bus(&bus
);
491 r
= sd_bus_call_method(
493 "org.freedesktop.portable1",
494 "/org/freedesktop/portable1",
495 "org.freedesktop.portable1.Manager",
501 return log_error_errno(r
, "Failed to list images: %s", bus_error_message(&error
, r
));
503 table
= table_new("name", "type", "ro", "crtime", "mtime", "usage", "state");
507 r
= sd_bus_message_enter_container(reply
, 'a', "(ssbtttso)");
509 return bus_log_parse_error(r
);
512 const char *name
, *type
, *state
;
513 uint64_t crtime
, mtime
, usage
;
518 r
= sd_bus_message_read(reply
, "(ssbtttso)", &name
, &type
, &ro_int
, &crtime
, &mtime
, &usage
, &state
, NULL
);
520 return bus_log_parse_error(r
);
524 r
= table_add_many(table
,
528 return log_error_errno(r
, "Failed to add row to table: %m");
531 r
= table_add_cell(table
, &cell
, TABLE_BOOLEAN
, &ro_bool
);
533 return log_error_errno(r
, "Failed to add row to table: %m");
536 r
= table_set_color(table
, cell
, ansi_highlight_red());
538 return log_error_errno(r
, "Failed to set table cell color: %m");
541 r
= table_add_many(table
,
542 TABLE_TIMESTAMP
, crtime
,
543 TABLE_TIMESTAMP
, mtime
,
546 return log_error_errno(r
, "Failed to add row to table: %m");
548 r
= table_add_cell(table
, &cell
, TABLE_STRING
, state
);
550 return log_error_errno(r
, "Failed to add row to table: %m");
552 if (!streq(state
, "detached")) {
553 r
= table_set_color(table
, cell
, ansi_highlight_green());
555 return log_error_errno(r
, "Failed to set table cell color: %m");
559 r
= sd_bus_message_exit_container(reply
);
561 return bus_log_parse_error(r
);
563 if (table_get_rows(table
) > 1) {
564 r
= table_set_sort(table
, (size_t) 0, (size_t) -1);
566 return log_error_errno(r
, "Failed to sort table: %m");
568 table_set_header(table
, arg_legend
);
570 r
= table_print(table
, NULL
);
572 return log_error_errno(r
, "Failed to show table: %m");
576 if (table_get_rows(table
) > 1)
577 printf("\n%zu images listed.\n", table_get_rows(table
) - 1);
579 printf("No images.\n");
585 static int remove_image(int argc
, char *argv
[], void *userdata
) {
586 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
589 r
= acquire_bus(&bus
);
593 (void) polkit_agent_open_if_enabled(arg_transport
, arg_ask_password
);
595 for (i
= 1; i
< argc
; i
++) {
596 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
597 _cleanup_(sd_bus_message_unrefp
) sd_bus_message
*m
= NULL
;
599 r
= sd_bus_message_new_method_call(
602 "org.freedesktop.portable1",
603 "/org/freedesktop/portable1",
604 "org.freedesktop.portable1.Manager",
607 return bus_log_create_error(r
);
609 r
= sd_bus_message_append(m
, "s", argv
[i
]);
611 return bus_log_create_error(r
);
613 /* This is a slow operation, hence turn off any method call timeouts */
614 r
= sd_bus_call(bus
, m
, USEC_INFINITY
, &error
, NULL
);
616 return log_error_errno(r
, "Could not remove image: %s", bus_error_message(&error
, r
));
622 static int read_only_image(int argc
, char *argv
[], void *userdata
) {
623 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
624 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
628 b
= parse_boolean(argv
[2]);
630 return log_error_errno(b
, "Failed to parse boolean argument: %s", argv
[2]);
633 r
= acquire_bus(&bus
);
637 (void) polkit_agent_open_if_enabled(arg_transport
, arg_ask_password
);
639 r
= sd_bus_call_method(
641 "org.freedesktop.portable1",
642 "/org/freedesktop/portable1",
643 "org.freedesktop.portable1.Manager",
649 return log_error_errno(r
, "Could not mark image read-only: %s", bus_error_message(&error
, r
));
654 static int set_limit(int argc
, char *argv
[], void *userdata
) {
655 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
656 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
660 r
= acquire_bus(&bus
);
664 (void) polkit_agent_open_if_enabled(arg_transport
, arg_ask_password
);
666 if (STR_IN_SET(argv
[argc
-1], "-", "none", "infinity"))
667 limit
= (uint64_t) -1;
669 r
= parse_size(argv
[argc
-1], 1024, &limit
);
671 return log_error_errno(r
, "Failed to parse size: %s", argv
[argc
-1]);
675 /* With two arguments changes the quota limit of the specified image */
676 r
= sd_bus_call_method(
678 "org.freedesktop.portable1",
679 "/org/freedesktop/portable1",
680 "org.freedesktop.portable1.Manager",
684 "st", argv
[1], limit
);
686 /* With one argument changes the pool quota limit */
687 r
= sd_bus_call_method(
689 "org.freedesktop.portable1",
690 "/org/freedesktop/portable1",
691 "org.freedesktop.portable1.Manager",
698 return log_error_errno(r
, "Could not set limit: %s", bus_error_message(&error
, r
));
703 static int is_image_attached(int argc
, char *argv
[], void *userdata
) {
704 _cleanup_(sd_bus_message_unrefp
) sd_bus_message
*reply
= NULL
;
705 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
706 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
707 _cleanup_free_
char *image
= NULL
;
711 r
= determine_image(argv
[1], true, &image
);
715 r
= acquire_bus(&bus
);
719 r
= sd_bus_call_method(
721 "org.freedesktop.portable1",
722 "/org/freedesktop/portable1",
723 "org.freedesktop.portable1.Manager",
729 return log_error_errno(r
, "Failed to get image state: %s", bus_error_message(&error
, r
));
731 r
= sd_bus_message_read(reply
, "s", &state
);
738 return streq(state
, "detached");
741 static int dump_profiles(void) {
742 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
743 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
744 _cleanup_strv_free_
char **l
= NULL
;
748 r
= acquire_bus(&bus
);
752 r
= sd_bus_get_property_strv(
754 "org.freedesktop.portable1",
755 "/org/freedesktop/portable1",
756 "org.freedesktop.portable1.Manager",
761 return log_error_errno(r
, "Failed to acquire list of profiles: %s", bus_error_message(&error
, r
));
764 log_info("Available unit profiles:");
774 static int help(int argc
, char *argv
[], void *userdata
) {
775 _cleanup_free_
char *link
= NULL
;
778 (void) pager_open(arg_pager_flags
);
780 r
= terminal_urlify_man("portablectl", "1", &link
);
784 printf("%s%s [OPTIONS...] {COMMAND} ...\n\n"
785 "Attach or detach portable services from the local system.%s\n"
787 " list List available portable service images\n"
788 " attach NAME|PATH [PREFIX...]\n"
789 " Attach the specified portable service image\n"
790 " detach NAME|PATH Detach the specified portable service image\n"
791 " inspect NAME|PATH [PREFIX...]\n"
792 " Show details of specified portable service image\n"
793 " is-attached NAME|PATH Query if portable service image is attached\n"
794 " read-only NAME|PATH [BOOL] Mark or unmark portable service image read-only\n"
795 " remove NAME|PATH... Remove a portable service image\n"
796 " set-limit [NAME|PATH] Set image or pool size limit (disk quota)\n"
798 " -h --help Show this help\n"
799 " --version Show package version\n"
800 " --no-pager Do not pipe output into a pager\n"
801 " --no-legend Do not show the headers and footers\n"
802 " --no-ask-password Do not ask for system passwords\n"
803 " -H --host=[USER@]HOST Operate on remote host\n"
804 " -M --machine=CONTAINER Operate on local container\n"
805 " -q --quiet Suppress informational messages\n"
806 " -p --profile=PROFILE Pick security profile for portable service\n"
807 " --copy=copy|auto|symlink Prefer copying or symlinks if possible\n"
808 " --runtime Attach portable service until next reboot only\n"
809 " --no-reload Don't reload the system and service manager\n"
810 " --cat When inspecting include unit and os-release file\n"
812 "\nSee the %s for details.\n"
814 , program_invocation_short_name
822 static int parse_argv(int argc
, char *argv
[]) {
835 static const struct option options
[] = {
836 { "help", no_argument
, NULL
, 'h' },
837 { "version", no_argument
, NULL
, ARG_VERSION
},
838 { "no-pager", no_argument
, NULL
, ARG_NO_PAGER
},
839 { "no-legend", no_argument
, NULL
, ARG_NO_LEGEND
},
840 { "no-ask-password", no_argument
, NULL
, ARG_NO_ASK_PASSWORD
},
841 { "host", required_argument
, NULL
, 'H' },
842 { "machine", required_argument
, NULL
, 'M' },
843 { "quiet", no_argument
, NULL
, 'q' },
844 { "profile", required_argument
, NULL
, 'p' },
845 { "copy", required_argument
, NULL
, ARG_COPY
},
846 { "runtime", no_argument
, NULL
, ARG_RUNTIME
},
847 { "no-reload", no_argument
, NULL
, ARG_NO_RELOAD
},
848 { "cat", no_argument
, NULL
, ARG_CAT
},
858 c
= getopt_long(argc
, argv
, "hH:M:qp:", options
, NULL
);
865 return help(0, NULL
, NULL
);
871 arg_pager_flags
|= PAGER_DISABLE
;
878 case ARG_NO_ASK_PASSWORD
:
879 arg_ask_password
= false;
883 arg_transport
= BUS_TRANSPORT_REMOTE
;
888 arg_transport
= BUS_TRANSPORT_MACHINE
;
897 if (streq(optarg
, "help"))
898 return dump_profiles();
900 if (!filename_is_valid(optarg
))
901 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
902 "Unit profile name not valid: %s", optarg
);
904 arg_profile
= optarg
;
908 if (streq(optarg
, "auto"))
909 arg_copy_mode
= NULL
;
910 else if (STR_IN_SET(optarg
, "copy", "symlink"))
911 arg_copy_mode
= optarg
;
912 else if (streq(optarg
, "help")) {
918 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
919 "Failed to parse --copy= argument: %s", optarg
);
939 assert_not_reached("Unhandled option");
946 static int run(int argc
, char *argv
[]) {
947 static const Verb verbs
[] = {
948 { "help", VERB_ANY
, VERB_ANY
, 0, help
},
949 { "list", VERB_ANY
, 1, VERB_DEFAULT
, list_images
},
950 { "attach", 2, VERB_ANY
, 0, attach_image
},
951 { "detach", 2, 2, 0, detach_image
},
952 { "inspect", 2, VERB_ANY
, 0, inspect_image
},
953 { "is-attached", 2, 2, 0, is_image_attached
},
954 { "read-only", 2, 3, 0, read_only_image
},
955 { "remove", 2, VERB_ANY
, 0, remove_image
},
956 { "set-limit", 3, 3, 0, set_limit
},
962 log_show_color(true);
963 log_parse_environment();
966 r
= parse_argv(argc
, argv
);
970 return dispatch_verb(argc
, argv
, verbs
, NULL
);
973 DEFINE_MAIN_FUNCTION(run
);