1 /* SPDX-License-Identifier: LGPL-2.1+ */
8 #include "alloc-util.h"
12 #include "dirent-util.h"
15 #include "format-table.h"
17 #include "locale-util.h"
18 #include "machine-image.h"
20 #include "parse-util.h"
21 #include "path-util.h"
22 #include "spawn-polkit-agent.h"
23 #include "string-util.h"
25 #include "terminal-util.h"
28 static PagerFlags arg_pager_flags
= 0;
29 static bool arg_legend
= true;
30 static bool arg_ask_password
= true;
31 static bool arg_quiet
= false;
32 static const char *arg_profile
= "default";
33 static const char* arg_copy_mode
= NULL
;
34 static bool arg_runtime
= false;
35 static bool arg_reload
= true;
36 static bool arg_cat
= false;
37 static BusTransport arg_transport
= BUS_TRANSPORT_LOCAL
;
38 static char *arg_host
= NULL
;
40 static int determine_image(const char *image
, bool permit_non_existing
, char **ret
) {
43 /* If the specified name is a valid image name, we pass it as-is to portabled, which will search for it in the
44 * usual search directories. Otherwise we presume it's a path, and will normalize it on the client's side
45 * (among other things, to make the path independent of the client's working directory) before passing it
48 if (image_name_is_valid(image
)) {
51 if (!arg_quiet
&& laccess(image
, F_OK
) >= 0)
52 log_warning("Ambiguous invocation: current working directory contains file matching non-path argument '%s', ignoring. "
53 "Prefix argument with './' to force reference to file in current working directory.", image
);
63 if (arg_transport
!= BUS_TRANSPORT_LOCAL
) {
64 log_error("Operations on images by path not supported when connecting to remote systems.");
68 r
= chase_symlinks(image
, NULL
, CHASE_TRAIL_SLASH
| (permit_non_existing
? CHASE_NONEXISTENT
: 0), ret
);
70 return log_error_errno(r
, "Cannot normalize specified image path '%s': %m", image
);
75 static int extract_prefix(const char *path
, char **ret
) {
76 _cleanup_free_
char *name
= NULL
;
77 const char *bn
, *underscore
;
82 underscore
= strchr(bn
, '_');
88 e
= endswith(bn
, ".raw");
95 name
= strndup(bn
, m
);
99 /* A slightly reduced version of what's permitted in unit names. With ':' and '\' are removed, as well as '_'
100 * which we use as delimiter for the second part of the image string, which we ignore for now. */
101 if (!in_charset(name
, DIGITS LETTERS
"-."))
104 if (!filename_is_valid(name
))
107 *ret
= TAKE_PTR(name
);
112 static int determine_matches(const char *image
, char **l
, bool allow_any
, char ***ret
) {
113 _cleanup_strv_free_
char **k
= NULL
;
116 /* Determine the matches to apply. If the list is empty we derive the match from the image name. If the list
117 * contains exactly the "-" we return a wildcard list (which is the empty list), but only if this is expressly
120 if (strv_isempty(l
)) {
123 r
= extract_prefix(image
, &prefix
);
125 return log_error_errno(r
, "Failed to extract prefix of image name '%s': %m", image
);
128 log_info("(Matching unit files with prefix '%s'.)", prefix
);
130 r
= strv_consume(&k
, prefix
);
134 } else if (strv_equal(l
, STRV_MAKE("-"))) {
137 log_error("Refusing all unit file match.");
142 log_info("(Matching all unit files.)");
150 _cleanup_free_
char *joined
= NULL
;
152 joined
= strv_join(k
, "', '");
156 log_info("(Matching unit files with prefixes '%s'.)", joined
);
165 static int acquire_bus(sd_bus
**bus
) {
173 r
= bus_connect_transport(arg_transport
, arg_host
, false, bus
);
175 return log_error_errno(r
, "Failed to connect to bus: %m");
177 (void) sd_bus_set_allow_interactive_authorization(*bus
, arg_ask_password
);
182 static int maybe_reload(sd_bus
**bus
) {
183 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
184 _cleanup_(sd_bus_message_unrefp
) sd_bus_message
*m
= NULL
;
190 r
= acquire_bus(bus
);
194 r
= sd_bus_message_new_method_call(
197 "org.freedesktop.systemd1",
198 "/org/freedesktop/systemd1",
199 "org.freedesktop.systemd1.Manager",
202 return bus_log_create_error(r
);
204 /* Reloading the daemon may take long, hence set a longer timeout here */
205 r
= sd_bus_call(*bus
, m
, DEFAULT_TIMEOUT_USEC
* 2, &error
, NULL
);
207 return log_error_errno(r
, "Failed to reload daemon: %s", bus_error_message(&error
, r
));
212 static int inspect_image(int argc
, char *argv
[], void *userdata
) {
213 _cleanup_(sd_bus_message_unrefp
) sd_bus_message
*m
= NULL
, *reply
= NULL
;
214 _cleanup_(sd_bus_error_free
) sd_bus_error error
= SD_BUS_ERROR_NULL
;
215 _cleanup_(sd_bus_flush_close_unrefp
) sd_bus
*bus
= NULL
;
216 _cleanup_strv_free_
char **matches
= NULL
;
217 _cleanup_free_
char *image
= NULL
;
218 bool nl
= false, header
= false;
224 r
= determine_image(argv
[1], false, &image
);
228 r
= determine_matches(argv
[1], argv
+ 2, true, &matches
);
232 r
= acquire_bus(&bus
);
236 r
= sd_bus_message_new_method_call(
239 "org.freedesktop.portable1",
240 "/org/freedesktop/portable1",
241 "org.freedesktop.portable1.Manager",
244 return bus_log_create_error(r
);
246 r
= sd_bus_message_append(m
, "s", image
);
248 return bus_log_create_error(r
);
250 r
= sd_bus_message_append_strv(m
, matches
);
252 return bus_log_create_error(r
);
254 r
= sd_bus_call(bus
, m
, 0, &error
, &reply
);
256 return log_error_errno(r
, "Failed to inspect image metadata: %s", bus_error_message(&error
, r
));
258 r
= sd_bus_message_read(reply
, "s", &path
);
260 return bus_log_parse_error(r
);
262 r
= sd_bus_message_read_array(reply
, 'y', &data
, &sz
);
264 return bus_log_parse_error(r
);
266 (void) pager_open(arg_pager_flags
);
269 printf("%s-- OS Release: --%s\n", ansi_highlight(), ansi_normal());
270 fwrite(data
, sz
, 1, stdout
);
274 _cleanup_free_
char *pretty_portable
= NULL
, *pretty_os
= NULL
;
276 _cleanup_fclose_
FILE *f
;
278 f
= fmemopen((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(ARROW
), source
);
369 else if (streq(type
, "copy")) {
371 log_info("Copied %s.", path
);
373 log_info("Copied %s %s %s.", source
, 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 [OPTIONS...] {COMMAND} ...\n\n"
785 "Attach or detach portable services from the local system.\n\n"
786 " -h --help Show this help\n"
787 " --version Show package version\n"
788 " --no-pager Do not pipe output into a pager\n"
789 " --no-legend Do not show the headers and footers\n"
790 " --no-ask-password Do not ask for system passwords\n"
791 " -H --host=[USER@]HOST Operate on remote host\n"
792 " -M --machine=CONTAINER Operate on local container\n"
793 " -q --quiet Suppress informational messages\n"
794 " -p --profile=PROFILE Pick security profile for portable service\n"
795 " --copy=copy|auto|symlink Prefer copying or symlinks if possible\n"
796 " --runtime Attach portable service until next reboot only\n"
797 " --no-reload Don't reload the system and service manager\n"
798 " --cat When inspecting include unit and os-release file\n"
801 " list List available portable service images\n"
802 " attach NAME|PATH [PREFIX...]\n"
803 " Attach the specified portable service image\n"
804 " detach NAME|PATH Detach the specified portable service image\n"
805 " inspect NAME|PATH [PREFIX...]\n"
806 " Show details of specified portable service image\n"
807 " is-attached NAME|PATH Query if portable service image is attached\n"
808 " read-only NAME|PATH [BOOL] Mark or unmark portable service image read-only\n"
809 " remove NAME|PATH... Remove a portable service image\n"
810 " set-limit [NAME|PATH] Set image or pool size limit (disk quota)\n"
811 "\nSee the %s for details.\n"
812 , program_invocation_short_name
819 static int parse_argv(int argc
, char *argv
[]) {
832 static const struct option options
[] = {
833 { "help", no_argument
, NULL
, 'h' },
834 { "version", no_argument
, NULL
, ARG_VERSION
},
835 { "no-pager", no_argument
, NULL
, ARG_NO_PAGER
},
836 { "no-legend", no_argument
, NULL
, ARG_NO_LEGEND
},
837 { "no-ask-password", no_argument
, NULL
, ARG_NO_ASK_PASSWORD
},
838 { "host", required_argument
, NULL
, 'H' },
839 { "machine", required_argument
, NULL
, 'M' },
840 { "quiet", no_argument
, NULL
, 'q' },
841 { "profile", required_argument
, NULL
, 'p' },
842 { "copy", required_argument
, NULL
, ARG_COPY
},
843 { "runtime", no_argument
, NULL
, ARG_RUNTIME
},
844 { "no-reload", no_argument
, NULL
, ARG_NO_RELOAD
},
845 { "cat", no_argument
, NULL
, ARG_CAT
},
855 c
= getopt_long(argc
, argv
, "hH:M:qp:", options
, NULL
);
862 return help(0, NULL
, NULL
);
868 arg_pager_flags
|= PAGER_DISABLE
;
875 case ARG_NO_ASK_PASSWORD
:
876 arg_ask_password
= false;
880 arg_transport
= BUS_TRANSPORT_REMOTE
;
885 arg_transport
= BUS_TRANSPORT_MACHINE
;
894 if (streq(optarg
, "help"))
895 return dump_profiles();
897 if (!filename_is_valid(optarg
)) {
898 log_error("Unit profile name not valid: %s", optarg
);
902 arg_profile
= optarg
;
906 if (streq(optarg
, "auto"))
907 arg_copy_mode
= NULL
;
908 else if (STR_IN_SET(optarg
, "copy", "symlink"))
909 arg_copy_mode
= optarg
;
910 else if (streq(optarg
, "help")) {
916 log_error("Failed to parse --copy= argument: %s", optarg
);
938 assert_not_reached("Unhandled option");
945 int main(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_parse_environment();
965 r
= parse_argv(argc
, argv
);
969 r
= dispatch_verb(argc
, argv
, verbs
, NULL
);
974 return r
< 0 ? EXIT_FAILURE
: EXIT_SUCCESS
;