1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
5 #include <linux/loop.h>
11 #include "capability-util.h"
13 #include "devnum-util.h"
14 #include "discover-image.h"
15 #include "dissect-image.h"
18 #include "extension-util.h"
21 #include "format-table.h"
24 #include "initrd-util.h"
26 #include "main-func.h"
27 #include "missing_magic.h"
29 #include "mount-util.h"
30 #include "mountpoint-util.h"
33 #include "parse-argument.h"
34 #include "parse-util.h"
35 #include "pretty-print.h"
36 #include "process-util.h"
37 #include "sort-util.h"
38 #include "terminal-util.h"
39 #include "user-util.h"
42 static char **arg_hierarchies
= NULL
; /* "/usr" + "/opt" by default */
43 static char *arg_root
= NULL
;
44 static JsonFormatFlags arg_json_format_flags
= JSON_FORMAT_OFF
;
45 static PagerFlags arg_pager_flags
= 0;
46 static bool arg_legend
= true;
47 static bool arg_force
= false;
48 static ImagePolicy
*arg_image_policy
= NULL
;
50 STATIC_DESTRUCTOR_REGISTER(arg_hierarchies
, strv_freep
);
51 STATIC_DESTRUCTOR_REGISTER(arg_root
, freep
);
52 STATIC_DESTRUCTOR_REGISTER(arg_image_policy
, image_policy_freep
);
54 static int is_our_mount_point(const char *p
) {
55 _cleanup_free_
char *buf
= NULL
, *f
= NULL
;
60 r
= path_is_mount_point(p
, NULL
, 0);
62 log_debug_errno(r
, "Hierarchy '%s' doesn't exist.", p
);
66 return log_error_errno(r
, "Failed to determine whether '%s' is a mount point: %m", p
);
68 log_debug("Hierarchy '%s' is not a mount point, skipping.", p
);
72 /* So we know now that it's a mount point. Now let's check if it's one of ours, so that we don't
73 * accidentally unmount the user's own /usr/ but just the mounts we established ourselves. We do this
74 * check by looking into the metadata directory we place in merged mounts: if the file
75 * .systemd-sysext/dev contains the major/minor device pair of the mount we have a good reason to
76 * believe this is one of our mounts. This thorough check has the benefit that we aren't easily
77 * confused if people tar up one of our merged trees and untar them elsewhere where we might mistake
78 * them for a live sysext tree. */
80 f
= path_join(p
, ".systemd-sysext/dev");
84 r
= read_one_line_file(f
, &buf
);
86 log_debug("Hierarchy '%s' does not carry a .systemd-sysext/dev file, not a sysext merged tree.", p
);
90 return log_error_errno(r
, "Failed to determine whether hierarchy '%s' contains '.systemd-sysext/dev': %m", p
);
92 r
= parse_devnum(buf
, &dev
);
94 return log_error_errno(r
, "Failed to parse device major/minor stored in '.systemd-sysext/dev' file on '%s': %m", p
);
96 if (lstat(p
, &st
) < 0)
97 return log_error_errno(r
, "Failed to stat %s: %m", p
);
99 if (st
.st_dev
!= dev
) {
100 log_debug("Hierarchy '%s' reports a different device major/minor than what we are seeing, assuming offline copy.", p
);
107 static int unmerge_hierarchy(const char *p
) {
111 /* We only unmount /usr/ if it is a mount point and really one of ours, in order not to break
112 * systems where /usr/ is a mount point of its own already. */
114 r
= is_our_mount_point(p
);
120 r
= umount_verbose(LOG_ERR
, p
, MNT_DETACH
|UMOUNT_NOFOLLOW
);
122 return log_error_errno(r
, "Failed to unmount file system '%s': %m", p
);
124 log_info("Unmerged '%s'.", p
);
130 static int unmerge(void) {
133 STRV_FOREACH(p
, arg_hierarchies
) {
134 _cleanup_free_
char *resolved
= NULL
;
136 r
= chase(*p
, arg_root
, CHASE_PREFIX_ROOT
, &resolved
, NULL
);
138 log_debug_errno(r
, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root
), *p
);
142 log_error_errno(r
, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root
), *p
);
149 r
= unmerge_hierarchy(resolved
);
150 if (r
< 0 && ret
== 0)
157 static int verb_unmerge(int argc
, char **argv
, void *userdata
) {
160 r
= have_effective_cap(CAP_SYS_ADMIN
);
162 return log_error_errno(r
, "Failed to check if we have enough privileges: %m");
164 return log_error_errno(SYNTHETIC_ERRNO(EPERM
), "Need to be privileged.");
169 static int verb_status(int argc
, char **argv
, void *userdata
) {
170 _cleanup_(table_unrefp
) Table
*t
= NULL
;
173 t
= table_new("hierarchy", "extensions", "since");
177 table_set_ersatz_string(t
, TABLE_ERSATZ_DASH
);
179 STRV_FOREACH(p
, arg_hierarchies
) {
180 _cleanup_free_
char *resolved
= NULL
, *f
= NULL
, *buf
= NULL
;
181 _cleanup_strv_free_
char **l
= NULL
;
184 r
= chase(*p
, arg_root
, CHASE_PREFIX_ROOT
, &resolved
, NULL
);
186 log_debug_errno(r
, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root
), *p
);
190 log_error_errno(r
, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root
), *p
);
194 r
= is_our_mount_point(resolved
);
201 TABLE_STRING
, "none",
202 TABLE_SET_COLOR
, ansi_grey(),
205 return table_log_add_error(r
);
210 f
= path_join(*p
, ".systemd-sysext/extensions");
214 r
= read_full_file(f
, &buf
, NULL
);
216 return log_error_errno(r
, "Failed to open '%s': %m", f
);
218 l
= strv_split_newlines(buf
);
222 if (stat(*p
, &st
) < 0)
223 return log_error_errno(r
, "Failed to stat() '%s': %m", *p
);
229 TABLE_TIMESTAMP
, timespec_load(&st
.st_mtim
));
231 return table_log_add_error(r
);
240 (void) table_set_sort(t
, (size_t) 0);
242 r
= table_print_with_pager(t
, arg_json_format_flags
, arg_pager_flags
, arg_legend
);
249 static int mount_overlayfs(
253 _cleanup_free_
char *options
= NULL
;
254 bool separator
= false;
259 options
= strdup("lowerdir=");
263 STRV_FOREACH(l
, layers
) {
264 _cleanup_free_
char *escaped
= NULL
;
266 escaped
= shell_escape(*l
, ",:");
270 if (!strextend(&options
, separator
? ":" : "", escaped
))
276 /* Now mount the actual overlayfs */
277 r
= mount_nofollow_verbose(LOG_ERR
, "sysext", where
, "overlay", MS_RDONLY
, options
);
284 static int merge_hierarchy(
285 const char *hierarchy
,
288 const char *meta_path
,
289 const char *overlay_path
) {
291 _cleanup_free_
char *resolved_hierarchy
= NULL
, *f
= NULL
, *buf
= NULL
;
292 _cleanup_strv_free_
char **layers
= NULL
;
298 assert(overlay_path
);
300 /* Resolve the path of the host's version of the hierarchy, i.e. what we want to use as lowest layer
301 * in the overlayfs stack. */
302 r
= chase(hierarchy
, arg_root
, CHASE_PREFIX_ROOT
, &resolved_hierarchy
, NULL
);
304 log_debug_errno(r
, "Hierarchy '%s' on host doesn't exist, not merging.", hierarchy
);
306 return log_error_errno(r
, "Failed to resolve host hierarchy '%s': %m", hierarchy
);
308 r
= dir_is_empty(resolved_hierarchy
, /* ignore_hidden_or_backup= */ false);
310 return log_error_errno(r
, "Failed to check if host hierarchy '%s' is empty: %m", resolved_hierarchy
);
312 log_debug("Host hierarchy '%s' is empty, not merging.", resolved_hierarchy
);
313 resolved_hierarchy
= mfree(resolved_hierarchy
);
317 /* Let's generate a metadata file that lists all extensions we took into account for this
318 * hierarchy. We include this in the final fs, to make things nicely discoverable and
320 f
= path_join(meta_path
, ".systemd-sysext/extensions");
324 buf
= strv_join(extensions
, "\n");
328 r
= write_string_file(f
, buf
, WRITE_STRING_FILE_CREATE
|WRITE_STRING_FILE_MKDIR_0755
);
330 return log_error_errno(r
, "Failed to write extension meta file '%s': %m", f
);
332 /* Put the meta path (i.e. our synthesized stuff) at the top of the layer stack */
333 layers
= strv_new(meta_path
);
337 /* Put the extensions in the middle */
338 STRV_FOREACH(p
, paths
) {
339 _cleanup_free_
char *resolved
= NULL
;
341 r
= chase(hierarchy
, *p
, CHASE_PREFIX_ROOT
, &resolved
, NULL
);
343 log_debug_errno(r
, "Hierarchy '%s' in extension '%s' doesn't exist, not merging.", hierarchy
, *p
);
347 return log_error_errno(r
, "Failed to resolve hierarchy '%s' in extension '%s': %m", hierarchy
, *p
);
349 r
= dir_is_empty(resolved
, /* ignore_hidden_or_backup= */ false);
351 return log_error_errno(r
, "Failed to check if hierarchy '%s' in extension '%s' is empty: %m", resolved
, *p
);
353 log_debug("Hierarchy '%s' in extension '%s' is empty, not merging.", hierarchy
, *p
);
357 r
= strv_consume(&layers
, TAKE_PTR(resolved
));
362 if (!layers
[1]) /* No extension with files in this hierarchy? Then don't do anything. */
365 if (resolved_hierarchy
) {
366 /* Add the host hierarchy as last (lowest) layer in the stack */
367 r
= strv_consume(&layers
, TAKE_PTR(resolved_hierarchy
));
372 r
= mkdir_p(overlay_path
, 0700);
374 return log_error_errno(r
, "Failed to make directory '%s': %m", overlay_path
);
376 r
= mount_overlayfs(overlay_path
, layers
);
380 /* The overlayfs superblock is read-only. Let's also mark the bind mount read-only. Extra turbo safety 😎 */
381 r
= bind_remount_recursive(overlay_path
, MS_RDONLY
, MS_RDONLY
, NULL
);
383 return log_error_errno(r
, "Failed to make bind mount '%s' read-only: %m", overlay_path
);
385 /* Now we have mounted the new file system. Let's now figure out its .st_dev field, and make that
386 * available in the metadata directory. This is useful to detect whether the metadata dir actually
387 * belongs to the fs it is found on: if .st_dev of the top-level mount matches it, it's pretty likely
388 * we are looking at a live sysext tree, and not an unpacked tar or so of one. */
389 if (stat(overlay_path
, &st
) < 0)
390 return log_error_errno(r
, "Failed to stat mount '%s': %m", overlay_path
);
393 f
= path_join(meta_path
, ".systemd-sysext/dev");
397 r
= write_string_filef(f
, WRITE_STRING_FILE_CREATE
, "%u:%u", major(st
.st_dev
), minor(st
.st_dev
));
399 return log_error_errno(r
, "Failed to write '%s': %m", f
);
401 /* Make sure the top-level dir has an mtime marking the point we established the merge */
402 if (utimensat(AT_FDCWD
, meta_path
, NULL
, AT_SYMLINK_NOFOLLOW
) < 0)
403 return log_error_errno(r
, "Failed fix mtime of '%s': %m", meta_path
);
408 static int strverscmp_improvedp(char *const* a
, char *const* b
) {
409 /* usable in qsort() for sorting a string array with strverscmp_improved() */
410 return strverscmp_improved(*a
, *b
);
413 static const ImagePolicy
*pick_image_policy(const Image
*img
) {
417 /* Explicitly specified policy always wins */
418 if (arg_image_policy
)
419 return arg_image_policy
;
421 /* If located in /.extra/sysext/ in the initrd, then it was placed there by systemd-stub, and was
422 * picked up from an untrusted ESP. Thus, require a stricter policy by default for them. (For the
423 * other directories we assume the appropriate level of trust was already established already. */
425 if (in_initrd() && path_startswith(img
->path
, "/.extra/sysext/"))
426 return &image_policy_sysext_strict
;
428 return &image_policy_sysext
;
431 static int merge_subprocess(Hashmap
*images
, const char *workspace
) {
432 _cleanup_free_
char *host_os_release_id
= NULL
, *host_os_release_version_id
= NULL
, *host_os_release_sysext_level
= NULL
,
434 _cleanup_strv_free_
char **extensions
= NULL
, **paths
= NULL
;
435 size_t n_extensions
= 0;
436 unsigned n_ignored
= 0;
440 /* Mark the whole of /run as MS_SLAVE, so that we can mount stuff below it that doesn't show up on
441 * the host otherwise. */
442 r
= mount_nofollow_verbose(LOG_ERR
, NULL
, "/run", NULL
, MS_SLAVE
|MS_REC
, NULL
);
444 return log_error_errno(r
, "Failed to remount /run/ MS_SLAVE: %m");
446 /* Let's create the workspace if it's missing */
447 r
= mkdir_p(workspace
, 0700);
449 return log_error_errno(r
, "Failed to create /run/systemd/sysext: %m");
451 /* Let's mount a tmpfs to our workspace. This way we don't need to clean up the inodes we mount over,
452 * but let the kernel do that entirely automatically, once our namespace dies. Note that this file
453 * system won't be visible to anyone but us, since we opened our own namespace and then made the
454 * /run/ hierarchy (which our workspace is contained in) MS_SLAVE, see above. */
455 r
= mount_nofollow_verbose(LOG_ERR
, "sysext", workspace
, "tmpfs", 0, "mode=0700");
459 /* Acquire host OS release info, so that we can compare it with the extension's data */
460 r
= parse_os_release(
462 "ID", &host_os_release_id
,
463 "VERSION_ID", &host_os_release_version_id
,
464 "SYSEXT_LEVEL", &host_os_release_sysext_level
);
466 return log_error_errno(r
, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(arg_root
));
467 if (isempty(host_os_release_id
))
468 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
469 "'ID' field not found or empty in 'os-release' data of OS tree '%s': %m",
470 empty_to_root(arg_root
));
472 /* Let's now mount all images */
473 HASHMAP_FOREACH(img
, images
) {
474 _cleanup_free_
char *p
= NULL
;
476 p
= path_join(workspace
, "extensions", img
->name
);
480 r
= mkdir_p(p
, 0700);
482 return log_error_errno(r
, "Failed to create %s: %m", p
);
485 case IMAGE_DIRECTORY
:
486 case IMAGE_SUBVOLUME
:
489 r
= extension_has_forbidden_content(p
);
498 r
= mount_nofollow_verbose(LOG_ERR
, img
->path
, p
, NULL
, MS_BIND
, NULL
);
502 /* Make this a read-only bind mount */
503 r
= bind_remount_recursive(p
, MS_RDONLY
, MS_RDONLY
, NULL
);
505 return log_error_errno(r
, "Failed to make bind mount '%s' read-only: %m", p
);
511 _cleanup_(dissected_image_unrefp
) DissectedImage
*m
= NULL
;
512 _cleanup_(loop_device_unrefp
) LoopDevice
*d
= NULL
;
513 _cleanup_(verity_settings_done
) VeritySettings verity_settings
= VERITY_SETTINGS_DEFAULT
;
514 DissectImageFlags flags
=
515 DISSECT_IMAGE_READ_ONLY
|
516 DISSECT_IMAGE_GENERIC_ROOT
|
517 DISSECT_IMAGE_REQUIRE_ROOT
|
518 DISSECT_IMAGE_MOUNT_ROOT_ONLY
|
519 DISSECT_IMAGE_USR_NO_ROOT
|
520 DISSECT_IMAGE_ADD_PARTITION_DEVICES
|
521 DISSECT_IMAGE_PIN_PARTITION_DEVICES
;
523 r
= verity_settings_load(&verity_settings
, img
->path
, NULL
, NULL
);
525 return log_error_errno(r
, "Failed to read verity artifacts for %s: %m", img
->path
);
527 if (verity_settings
.data_path
)
528 flags
|= DISSECT_IMAGE_NO_PARTITION_TABLE
;
531 flags
|= DISSECT_IMAGE_VALIDATE_OS_EXT
;
533 r
= loop_device_make_by_path(
536 /* sector_size= */ UINT32_MAX
,
537 FLAGS_SET(flags
, DISSECT_IMAGE_NO_PARTITION_TABLE
) ? 0 : LO_FLAGS_PARTSCAN
,
541 return log_error_errno(r
, "Failed to set up loopback device for %s: %m", img
->path
);
543 r
= dissect_loop_device_and_warn(
546 /* mount_options= */ NULL
,
547 pick_image_policy(img
),
553 r
= dissected_image_load_verity_sig_partition(
560 r
= dissected_image_decrypt_interactively(
567 r
= dissected_image_mount_and_warn(
573 if (r
< 0 && r
!= -ENOMEDIUM
)
575 if (r
== -ENOMEDIUM
&& !arg_force
) {
580 r
= dissected_image_relinquish(m
);
582 return log_error_errno(r
, "Failed to relinquish DM and loopback block devices: %m");
586 assert_not_reached();
590 log_debug("Force mode enabled, skipping version validation.");
592 r
= extension_release_validate(
595 host_os_release_version_id
,
596 host_os_release_sysext_level
,
597 in_initrd() ? "initrd" : "system",
598 img
->extension_release
);
607 /* Nice! This one is an extension we want. */
608 r
= strv_extend(&extensions
, img
->name
);
615 /* Nothing left? Then shortcut things */
616 if (n_extensions
== 0) {
618 log_info("No suitable extensions found (%u ignored due to incompatible image(s)).", n_ignored
);
620 log_info("No extensions found.");
624 /* Order by version sort with strverscmp_improved() */
625 typesafe_qsort(extensions
, n_extensions
, strverscmp_improvedp
);
627 buf
= strv_join(extensions
, "', '");
631 log_info("Using extensions '%s'.", buf
);
633 /* Build table of extension paths (in reverse order) */
634 paths
= new0(char*, n_extensions
+ 1);
638 for (size_t k
= 0; k
< n_extensions
; k
++) {
639 _cleanup_free_
char *p
= NULL
;
641 assert_se(img
= hashmap_get(images
, extensions
[n_extensions
- 1 - k
]));
643 p
= path_join(workspace
, "extensions", img
->name
);
647 paths
[k
] = TAKE_PTR(p
);
650 /* Let's now unmerge the status quo ante, since to build the new overlayfs we need a reference to the
652 STRV_FOREACH(h
, arg_hierarchies
) {
653 _cleanup_free_
char *resolved
= NULL
;
655 r
= chase(*h
, arg_root
, CHASE_PREFIX_ROOT
|CHASE_NONEXISTENT
, &resolved
, NULL
);
657 return log_error_errno(r
, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root
), *h
);
659 r
= unmerge_hierarchy(resolved
);
664 /* Create overlayfs mounts for all hierarchies */
665 STRV_FOREACH(h
, arg_hierarchies
) {
666 _cleanup_free_
char *meta_path
= NULL
, *overlay_path
= NULL
;
668 meta_path
= path_join(workspace
, "meta", *h
); /* The place where to store metadata about this instance */
672 overlay_path
= path_join(workspace
, "overlay", *h
); /* The resulting overlayfs instance */
676 r
= merge_hierarchy(*h
, extensions
, paths
, meta_path
, overlay_path
);
681 /* And move them all into place. This is where things appear in the host namespace */
682 STRV_FOREACH(h
, arg_hierarchies
) {
683 _cleanup_free_
char *p
= NULL
, *resolved
= NULL
;
685 p
= path_join(workspace
, "overlay", *h
);
689 if (laccess(p
, F_OK
) < 0) {
691 return log_error_errno(errno
, "Failed to check if '%s' exists: %m", p
);
693 /* Hierarchy apparently was empty in all extensions, and wasn't mounted, ignoring. */
697 r
= chase(*h
, arg_root
, CHASE_PREFIX_ROOT
|CHASE_NONEXISTENT
, &resolved
, NULL
);
699 return log_error_errno(r
, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root
), *h
);
701 r
= mkdir_p(resolved
, 0755);
703 return log_error_errno(r
, "Failed to create hierarchy mount point '%s': %m", resolved
);
705 r
= mount_nofollow_verbose(LOG_ERR
, p
, resolved
, NULL
, MS_BIND
, NULL
);
709 log_info("Merged extensions into '%s'.", resolved
);
715 static int merge(Hashmap
*images
) {
719 r
= safe_fork("(sd-sysext)", FORK_DEATHSIG
|FORK_LOG
|FORK_NEW_MOUNTNS
, &pid
);
721 return log_error_errno(r
, "Failed to fork off child: %m");
723 /* Child with its own mount namespace */
725 r
= merge_subprocess(images
, "/run/systemd/sysext");
729 /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we
730 * created below /run. Nice! */
732 _exit(r
> 0 ? EXIT_SUCCESS
: 123); /* 123 means: didn't find any extensions */
735 r
= wait_for_terminate_and_check("(sd-sysext)", pid
, WAIT_LOG_ABNORMAL
);
739 return r
!= 123; /* exit code 123 means: didn't do anything */
742 static int image_discover_and_read_metadata(Hashmap
**ret_images
) {
743 _cleanup_(hashmap_freep
) Hashmap
*images
= NULL
;
749 images
= hashmap_new(&image_hash_ops
);
753 r
= image_discover(IMAGE_EXTENSION
, arg_root
, images
);
755 return log_error_errno(r
, "Failed to discover extension images: %m");
757 HASHMAP_FOREACH(img
, images
) {
758 r
= image_read_metadata(img
, &image_policy_sysext
);
760 return log_error_errno(r
, "Failed to read metadata for image %s: %m", img
->name
);
763 *ret_images
= TAKE_PTR(images
);
768 static int verb_merge(int argc
, char **argv
, void *userdata
) {
769 _cleanup_(hashmap_freep
) Hashmap
*images
= NULL
;
772 r
= have_effective_cap(CAP_SYS_ADMIN
);
774 return log_error_errno(r
, "Failed to check if we have enough privileges: %m");
776 return log_error_errno(SYNTHETIC_ERRNO(EPERM
), "Need to be privileged.");
778 r
= image_discover_and_read_metadata(&images
);
782 /* In merge mode fail if things are already merged. (In --refresh mode below we'll unmerge if we find
783 * things are already merged...) */
784 STRV_FOREACH(p
, arg_hierarchies
) {
785 _cleanup_free_
char *resolved
= NULL
;
787 r
= chase(*p
, arg_root
, CHASE_PREFIX_ROOT
, &resolved
, NULL
);
789 log_debug_errno(r
, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root
), *p
);
793 return log_error_errno(r
, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root
), *p
);
795 r
= is_our_mount_point(resolved
);
799 return log_error_errno(SYNTHETIC_ERRNO(EBUSY
),
800 "Hierarchy '%s' is already merged.", *p
);
803 return merge(images
);
806 static int verb_refresh(int argc
, char **argv
, void *userdata
) {
807 _cleanup_(hashmap_freep
) Hashmap
*images
= NULL
;
810 r
= have_effective_cap(CAP_SYS_ADMIN
);
812 return log_error_errno(r
, "Failed to check if we have enough privileges: %m");
814 return log_error_errno(SYNTHETIC_ERRNO(EPERM
), "Need to be privileged.");
816 r
= image_discover_and_read_metadata(&images
);
820 r
= merge(images
); /* Returns > 0 if it did something, i.e. a new overlayfs is mounted now. When it
821 * does so it implicitly unmounts any overlayfs placed there before. Returns == 0
822 * if it did nothing, i.e. no extension images found. In this case the old
823 * overlayfs remains in place if there was one. */
826 if (r
== 0) /* No images found? Then unmerge. The goal of --refresh is after all that after having
827 * called there's a guarantee that the merge status matches the installed extensions. */
830 /* Net result here is that:
832 * 1. If an overlayfs was mounted before and no extensions exist anymore, we'll have unmerged things.
834 * 2. If an overlayfs was mounted before, and there are still extensions installed' we'll have
835 * unmerged and then merged things again.
837 * 3. If an overlayfs so far wasn't mounted, and there are extensions installed, we'll have it
840 * 4. If there was no overlayfs mount so far, and no extensions installed, we implement a NOP.
846 static int verb_list(int argc
, char **argv
, void *userdata
) {
847 _cleanup_(hashmap_freep
) Hashmap
*images
= NULL
;
848 _cleanup_(table_unrefp
) Table
*t
= NULL
;
852 images
= hashmap_new(&image_hash_ops
);
856 r
= image_discover(IMAGE_EXTENSION
, arg_root
, images
);
858 return log_error_errno(r
, "Failed to discover extension images: %m");
860 if ((arg_json_format_flags
& JSON_FORMAT_OFF
) && hashmap_isempty(images
)) {
861 log_info("No OS extensions found.");
865 t
= table_new("name", "type", "path", "time");
869 HASHMAP_FOREACH(img
, images
) {
872 TABLE_STRING
, img
->name
,
873 TABLE_STRING
, image_type_to_string(img
->type
),
874 TABLE_PATH
, img
->path
,
875 TABLE_TIMESTAMP
, img
->mtime
!= 0 ? img
->mtime
: img
->crtime
);
877 return table_log_add_error(r
);
880 (void) table_set_sort(t
, (size_t) 0);
882 return table_print_with_pager(t
, arg_json_format_flags
, arg_pager_flags
, arg_legend
);
885 static int verb_help(int argc
, char **argv
, void *userdata
) {
886 _cleanup_free_
char *link
= NULL
;
889 r
= terminal_urlify_man("systemd-sysext", "8", &link
);
893 printf("%1$s [OPTIONS...] COMMAND\n"
894 "\n%5$sMerge extension images into /usr/ and /opt/ hierarchies.%6$s\n"
895 "\n%3$sCommands:%4$s\n"
896 " status Show current merge status (default)\n"
897 " merge Merge extensions into /usr/ and /opt/\n"
898 " unmerge Unmerge extensions from /usr/ and /opt/\n"
899 " refresh Unmerge/merge extensions again\n"
900 " list List installed extensions\n"
901 " -h --help Show this help\n"
902 " --version Show package version\n"
903 "\n%3$sOptions:%4$s\n"
904 " --no-pager Do not pipe output into a pager\n"
905 " --no-legend Do not show the headers and footers\n"
906 " --root=PATH Operate relative to root path\n"
907 " --json=pretty|short|off\n"
908 " Generate JSON output\n"
909 " --force Ignore version incompatibilities\n"
910 " --image-policy=POLICY\n"
911 " Specify disk image dissection policy\n"
912 "\nSee the %2$s for details.\n",
913 program_invocation_short_name
,
923 static int parse_argv(int argc
, char *argv
[]) {
935 static const struct option options
[] = {
936 { "help", no_argument
, NULL
, 'h' },
937 { "version", no_argument
, NULL
, ARG_VERSION
},
938 { "no-pager", no_argument
, NULL
, ARG_NO_PAGER
},
939 { "no-legend", no_argument
, NULL
, ARG_NO_LEGEND
},
940 { "root", required_argument
, NULL
, ARG_ROOT
},
941 { "json", required_argument
, NULL
, ARG_JSON
},
942 { "force", no_argument
, NULL
, ARG_FORCE
},
943 { "image-policy", required_argument
, NULL
, ARG_IMAGE_POLICY
},
952 while ((c
= getopt_long(argc
, argv
, "h", options
, NULL
)) >= 0)
957 return verb_help(argc
, argv
, NULL
);
963 arg_pager_flags
|= PAGER_DISABLE
;
971 r
= parse_path_argument(optarg
, false, &arg_root
);
977 r
= parse_json_argument(optarg
, &arg_json_format_flags
);
987 case ARG_IMAGE_POLICY
: {
988 _cleanup_(image_policy_freep
) ImagePolicy
*p
= NULL
;
990 r
= image_policy_from_string(optarg
, &p
);
992 return log_error_errno(r
, "Failed to parse image policy: %s", optarg
);
994 image_policy_free(arg_image_policy
);
995 arg_image_policy
= TAKE_PTR(p
);
1002 assert_not_reached();
1008 static int sysext_main(int argc
, char *argv
[]) {
1010 static const Verb verbs
[] = {
1011 { "status", VERB_ANY
, 1, VERB_DEFAULT
, verb_status
},
1012 { "merge", VERB_ANY
, 1, 0, verb_merge
},
1013 { "unmerge", VERB_ANY
, 1, 0, verb_unmerge
},
1014 { "refresh", VERB_ANY
, 1, 0, verb_refresh
},
1015 { "list", VERB_ANY
, 1, 0, verb_list
},
1016 { "help", VERB_ANY
, 1, 0, verb_help
},
1020 return dispatch_verb(argc
, argv
, verbs
, NULL
);
1023 static int run(int argc
, char *argv
[]) {
1028 r
= parse_argv(argc
, argv
);
1032 /* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and
1033 * /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline
1035 r
= parse_env_extension_hierarchies(&arg_hierarchies
);
1037 return log_error_errno(r
, "Failed to parse $SYSTEMD_SYSEXT_HIERARCHIES environment variable: %m");
1039 return sysext_main(argc
, argv
);
1042 DEFINE_MAIN_FUNCTION(run
);