diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml index 4cdac2b013f..6430501bdf0 100644 --- a/man/systemd-dissect.xml +++ b/man/systemd-dissect.xml @@ -28,6 +28,9 @@ systemd-dissect OPTIONS IMAGE PATH + + systemd-dissect OPTIONS PATH + systemd-dissect OPTIONS IMAGE PATH TARGET @@ -40,7 +43,7 @@ Description systemd-dissect is a tool for introspecting and interacting with file system OS - disk images. It supports four different operations: + disk images. It supports five different operations: Show general OS image information, including the image's @@ -51,6 +54,10 @@ mount the included partitions according to their designation onto a directory and possibly sub-directories. + Unmount an OS image from a local directory. In this mode it will recursively unmount + the mounted partitions and remove the underlying loop device, including all the partition sub-devices. + + Copy files and directories in and out of an OS image. @@ -103,10 +110,7 @@ multiple nested mounts are established. This command expects two arguments: a path to an image file and a path to a directory where to mount the image. - To unmount an OS image mounted like this use umount8's - switch (for recursive operation), so that the OS image and all nested partition - mounts are unmounted. + To unmount an OS image mounted like this use the operation. When the OS image contains LUKS encrypted or Verity integrity protected file systems appropriate volumes are automatically set up and marked for automatic disassembly when the image is @@ -128,6 +132,23 @@ This is a shortcut for . + + + + + Unmount an OS image from the specified directory. This command expects one argument: + a directory where an OS image was mounted. + + All mounted partitions will be recursively unmounted, and the underlying loop device will be + removed, along with all it's partition sub-devices. + + + + + + This is a shortcut for . + + @@ -224,6 +245,13 @@ unmounted again. + + + + If combined with the specified directory where the OS image + is mounted is removed after unmounting the OS image. + + diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 4e39f959cfe..d9f3fab835b 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -8,7 +8,10 @@ #include #include +#include "sd-device.h" + #include "architecture.h" +#include "blockdev-util.h" #include "chase-symlinks.h" #include "copy.h" #include "dissect-image.h" @@ -24,6 +27,7 @@ #include "main-func.h" #include "mkdir.h" #include "mount-util.h" +#include "mountpoint-util.h" #include "namespace-util.h" #include "parse-argument.h" #include "parse-util.h" @@ -40,6 +44,7 @@ static enum { ACTION_DISSECT, ACTION_MOUNT, + ACTION_UMOUNT, ACTION_COPY_FROM, ACTION_COPY_TO, } arg_action = ACTION_DISSECT; @@ -58,6 +63,7 @@ static VeritySettings arg_verity_settings = VERITY_SETTINGS_DEFAULT; static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; static PagerFlags arg_pager_flags = 0; static bool arg_legend = true; +static bool arg_rmdir = false; STATIC_DESTRUCTOR_REGISTER(arg_verity_settings, verity_settings_done); @@ -81,6 +87,7 @@ static int help(void) { " --fsck=BOOL Run fsck before mounting\n" " --growfs=BOOL Grow file system to partition size, if marked\n" " --mkdir Make mount directory before mounting, if missing\n" + " --rmdir Remove mount directory after unmounting\n" " --discard=MODE Choose 'discard' mode (disabled, loop, all, crypto)\n" " --root-hash=HASH Specify root hash for verity\n" " --root-hash-sig=SIG Specify pkcs7 signature of root hash for verity\n" @@ -96,6 +103,8 @@ static int help(void) { " --version Show package version\n" " -m --mount Mount the image to the specified directory\n" " -M Shortcut for --mount --mkdir\n" + " -u --umount Unmount the image from the specified directory\n" + " -U Shortcut for --umount --rmdir\n" " -x --copy-from Copy files from image to host\n" " -a --copy-to Copy files from host to image\n" "\nSee the %2$s for details.\n", @@ -122,6 +131,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_ROOT_HASH_SIG, ARG_VERITY_DATA, ARG_MKDIR, + ARG_RMDIR, ARG_JSON, }; @@ -131,6 +141,7 @@ static int parse_argv(int argc, char *argv[]) { { "no-pager", no_argument, NULL, ARG_NO_PAGER }, { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, { "mount", no_argument, NULL, 'm' }, + { "umount", no_argument, NULL, 'u' }, { "read-only", no_argument, NULL, 'r' }, { "discard", required_argument, NULL, ARG_DISCARD }, { "fsck", required_argument, NULL, ARG_FSCK }, @@ -139,6 +150,7 @@ static int parse_argv(int argc, char *argv[]) { { "root-hash-sig", required_argument, NULL, ARG_ROOT_HASH_SIG }, { "verity-data", required_argument, NULL, ARG_VERITY_DATA }, { "mkdir", no_argument, NULL, ARG_MKDIR }, + { "rmdir", no_argument, NULL, ARG_RMDIR }, { "copy-from", no_argument, NULL, 'x' }, { "copy-to", no_argument, NULL, 'a' }, { "json", required_argument, NULL, ARG_JSON }, @@ -150,7 +162,7 @@ static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hmrMxa", options, NULL)) >= 0) { + while ((c = getopt_long(argc, argv, "hmurMUxa", options, NULL)) >= 0) { switch (c) { @@ -182,6 +194,20 @@ static int parse_argv(int argc, char *argv[]) { arg_flags |= DISSECT_IMAGE_MKDIR; break; + case 'u': + arg_action = ACTION_UMOUNT; + break; + + case ARG_RMDIR: + arg_rmdir = true; + break; + + case 'U': + /* Shortcut combination of the above two */ + arg_action = ACTION_UMOUNT; + arg_rmdir = true; + break; + case 'x': arg_action = ACTION_COPY_FROM; arg_flags |= DISSECT_IMAGE_READ_ONLY; @@ -316,6 +342,14 @@ static int parse_argv(int argc, char *argv[]) { arg_flags |= DISSECT_IMAGE_REQUIRE_ROOT; break; + case ACTION_UMOUNT: + if (optind + 1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Expected a mount point path as only argument."); + + arg_path = argv[optind]; + break; + case ACTION_COPY_FROM: if (argc < optind + 2 || argc > optind + 3) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), @@ -823,6 +857,82 @@ static int action_copy(DissectedImage *m, LoopDevice *d) { return 0; } +static int action_umount(const char *path) { + _cleanup_close_ int fd = -1; + _cleanup_free_ char *canonical = NULL; + dev_t devno; + const char *devname; + _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + int r, k; + + fd = chase_symlinks_and_open(path, NULL, 0, O_DIRECTORY, &canonical); + if (fd == -ENOTDIR) + return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "'%s' is not a directory", path); + if (fd < 0) + return log_error_errno(fd, "Failed to resolve path '%s': %m", path); + + r = fd_is_mount_point(fd, NULL, 0); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "'%s' is not a mount point", canonical); + if (r < 0) + return log_error_errno(r, "Failed to determine whether '%s' is a mount point: %m", canonical); + + r = fd_get_whole_disk(fd, /*backing=*/ true, &devno); + if (r < 0) + return log_error_errno(r, "Failed to find backing block device for '%s': %m", canonical); + + r = sd_device_new_from_devnum(&device, 'b', devno); + if (r < 0) + return log_error_errno(r, "Failed to create sd-device object for block device %u:%u: %m", + major(devno), minor(devno)); + + r = sd_device_get_devname(device, &devname); + if (r < 0) + return log_error_errno(r, "Failed to get devname of block device %u:%u: %m", + major(devno), minor(devno)); + + r = loop_device_open(devname, 0, &d); + if (r < 0) + return log_error_errno(r, "Failed to open loop device '%s': %m", devname); + + r = loop_device_flock(d, LOCK_EX); + if (r < 0) + return log_error_errno(r, "Failed to lock loop device '%s': %m", devname); + + /* We've locked the loop device, now we're ready to unmount. To allow the unmount to succeed, we have + * to close the O_PATH fd we opened earlier. */ + fd = safe_close(fd); + + r = umount_recursive(canonical, 0); + if (r < 0) + return log_error_errno(r, "Failed to unmount '%s': %m", canonical); + + /* We managed to lock and unmount successfully? That means we can try to remove the loop device.*/ + loop_device_unrelinquish(d); + + if (arg_rmdir) { + k = RET_NERRNO(rmdir(canonical)); + if (k < 0) + log_error_errno(k, "Failed to remove mount directory '%s': %m", canonical); + } else + k = 0; + + /* Before loop_device_unrefp() kicks in, let's explicitly remove all the partition subdevices of the + * loop device. We do this to ensure that all traces of the loop device are gone by the time this + * command exits. */ + r = block_device_remove_all_partitions(d->fd); + if (r == -EBUSY) { + log_error_errno(r, "One or more partitions of '%s' are busy, ignoring", devname); + r = 0; + } + if (r < 0) + log_error_errno(r, "Failed to remove one or more partitions of '%s': %m", devname); + + + return k < 0 ? k : r; +} + static int run(int argc, char *argv[]) { _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; @@ -835,6 +945,9 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; + if (arg_action == ACTION_UMOUNT) + return action_umount(arg_path); + r = verity_settings_load( &arg_verity_settings, arg_image, NULL, NULL); diff --git a/src/shared/blockdev-util.c b/src/shared/blockdev-util.c index c3b90bb2273..0d921cc045c 100644 --- a/src/shared/blockdev-util.c +++ b/src/shared/blockdev-util.c @@ -1,13 +1,19 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include +#include #include +#include "sd-device.h" + #include "alloc-util.h" #include "blockdev-util.h" #include "btrfs-util.h" +#include "device-util.h" #include "devnum-util.h" #include "dirent-util.h" +#include "errno-util.h" #include "fd-util.h" #include "fileio.h" #include "missing_magic.h" @@ -377,3 +383,162 @@ int path_is_encrypted(const char *path) { return blockdev_is_encrypted(p, 10 /* safety net: maximum recursion depth */); } + +int fd_get_whole_disk(int fd, bool backing, dev_t *ret) { + dev_t devt; + struct stat st; + int r; + + assert(ret); + + if (fstat(fd, &st) < 0) + return -errno; + + if (S_ISBLK(st.st_mode)) + devt = st.st_rdev; + else if (!backing) + return -ENOTBLK; + else if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) + return -ENOTBLK; + else if (major(st.st_dev) != 0) + devt = st.st_dev; + else { + _cleanup_close_ int regfd = -1; + + /* If major(st.st_dev) is zero, this might mean we are backed by btrfs, which needs special + * handing, to get the backing device node. */ + + regfd = fd_reopen(fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK); + if (regfd < 0) + return regfd; + + r = btrfs_get_block_device_fd(regfd, &devt); + if (r == -ENOTTY) + return -ENOTBLK; + if (r < 0) + return r; + } + + return block_get_whole_disk(devt, ret); +} + +int path_get_whole_disk(const char *path, bool backing, dev_t *ret) { + _cleanup_close_ int fd = -1; + + fd = open(path, O_CLOEXEC|O_PATH); + if (fd < 0) + return -errno; + + return fd_get_whole_disk(fd, backing, ret); +} + +int block_device_add_partition(int fd, const char *name, int nr, uint64_t start, uint64_t size) { + assert(fd >= 0); + assert(name); + assert(nr > 0); + + struct blkpg_partition bp = { + .pno = nr, + .start = start, + .length = size, + }; + + struct blkpg_ioctl_arg ba = { + .op = BLKPG_ADD_PARTITION, + .data = &bp, + .datalen = sizeof(bp), + }; + + if (strlen(name) >= sizeof(bp.devname)) + return -EINVAL; + + strcpy(bp.devname, name); + + return RET_NERRNO(ioctl(fd, BLKPG, &ba)); +} + +int block_device_remove_partition(int fd, const char *name, int nr) { + assert(fd >= 0); + assert(name); + assert(nr > 0); + + struct blkpg_partition bp = { + .pno = nr, + }; + + struct blkpg_ioctl_arg ba = { + .op = BLKPG_DEL_PARTITION, + .data = &bp, + .datalen = sizeof(bp), + }; + + if (strlen(name) >= sizeof(bp.devname)) + return -EINVAL; + + strcpy(bp.devname, name); + + return RET_NERRNO(ioctl(fd, BLKPG, &ba)); +} + +int block_device_remove_all_partitions(int fd) { + struct stat stat; + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + sd_device *part; + int r, k = 0; + + if (fstat(fd, &stat) < 0) + return -errno; + + r = sd_device_new_from_devnum(&dev, 'b', stat.st_rdev); + if (r < 0) + return r; + + r = sd_device_enumerator_new(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_parent(e, dev); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_subsystem(e, "block", true); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_property(e, "DEVTYPE", "partition"); + if (r < 0) + return r; + + FOREACH_DEVICE(e, part) { + const char *v, *devname; + int nr; + + r = sd_device_get_devname(part, &devname); + if (r < 0) + return r; + + r = sd_device_get_property_value(part, "PARTN", &v); + if (r < 0) + return r; + + r = safe_atoi(v, &nr); + if (r < 0) + return r; + + r = block_device_remove_partition(fd, devname, nr); + if (r == -ENODEV) { + log_debug("Kernel removed partition %s before us, ignoring", devname); + continue; + } + if (r < 0) { + log_debug_errno(r, "Failed to remove partition %s: %m", devname); + k = k ?: r; + continue; + } + + log_debug("Removed partition %s", devname); + } + + return k; +} diff --git a/src/shared/blockdev-util.h b/src/shared/blockdev-util.h index 05501f26570..8c9401b4a78 100644 --- a/src/shared/blockdev-util.h +++ b/src/shared/blockdev-util.h @@ -27,3 +27,10 @@ int blockdev_partscan_enabled(int fd); int fd_is_encrypted(int fd); int path_is_encrypted(const char *path); + +int fd_get_whole_disk(int fd, bool backing, dev_t *ret); +int path_get_whole_disk(const char *path, bool backing, dev_t *ret); + +int block_device_add_partition(int fd, const char *name, int nr, uint64_t start, uint64_t size); +int block_device_remove_partition(int fd, const char *name, int nr); +int block_device_remove_all_partitions(int fd); diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index c7a336d4326..87712abfb3b 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -4,7 +4,6 @@ #include #endif -#include #include #include #include @@ -149,29 +148,6 @@ static void check_partition_flags( log_debug("Unexpected partition flag %llu set on %s!", bit, node); } } - -static int ioctl_partition_remove(int fd, const char *name, int nr) { - assert(fd >= 0); - assert(name); - assert(nr > 0); - - struct blkpg_partition bp = { - .pno = nr, - }; - - struct blkpg_ioctl_arg ba = { - .op = BLKPG_DEL_PARTITION, - .data = &bp, - .datalen = sizeof(bp), - }; - - if (strlen(name) >= sizeof(bp.devname)) - return -EINVAL; - - strcpy(bp.devname, name); - - return RET_NERRNO(ioctl(fd, BLKPG, &ba)); -} #endif static void dissected_partition_done(int fd, DissectedPartition *p) { @@ -182,7 +158,7 @@ static void dissected_partition_done(int fd, DissectedPartition *p) { if (p->node && p->partno > 0 && !p->relinquished) { int r; - r = ioctl_partition_remove(fd, p->node, p->partno); + r = block_device_remove_partition(fd, p->node, p->partno); if (r < 0) log_debug_errno(r, "BLKPG_DEL_PARTITION failed, ignoring: %m"); } @@ -202,37 +178,6 @@ static void dissected_partition_done(int fd, DissectedPartition *p) { } #if HAVE_BLKID -static int ioctl_partition_add( - int fd, - const char *name, - int nr, - uint64_t start, - uint64_t size) { - - assert(fd >= 0); - assert(name); - assert(nr > 0); - - struct blkpg_partition bp = { - .pno = nr, - .start = start, - .length = size, - }; - - struct blkpg_ioctl_arg ba = { - .op = BLKPG_ADD_PARTITION, - .data = &bp, - .datalen = sizeof(bp), - }; - - if (strlen(name) >= sizeof(bp.devname)) - return -EINVAL; - - strcpy(bp.devname, name); - - return RET_NERRNO(ioctl(fd, BLKPG, &ba)); -} - static int make_partition_devname( const char *whole_devname, int nr, @@ -548,7 +493,7 @@ int dissect_image( * Kernel returns EBUSY if there's already a partition by that number or an overlapping * partition already existent. */ - r = ioctl_partition_add(fd, node, nr, (uint64_t) start * 512, (uint64_t) size * 512); + r = block_device_add_partition(fd, node, nr, (uint64_t) start * 512, (uint64_t) size * 512); if (r < 0) { if (r != -EBUSY) return log_debug_errno(r, "BLKPG_ADD_PARTITION failed: %m"); @@ -831,7 +776,7 @@ int dissect_image( if (!PARTITION_DESIGNATOR_VERSIONED(designator) || strverscmp_improved(m->partitions[designator].label, label) >= 0) { - r = ioctl_partition_remove(fd, node, nr); + r = block_device_remove_partition(fd, node, nr); if (r < 0) log_debug_errno(r, "BLKPG_DEL_PARTITION failed, ignoring: %m"); continue; @@ -908,7 +853,7 @@ int dissect_image( /* First one wins */ if (m->partitions[PARTITION_XBOOTLDR].found) { - r = ioctl_partition_remove(fd, node, nr); + r = block_device_remove_partition(fd, node, nr); if (r < 0) log_debug_errno(r, "BLKPG_DEL_PARTITION failed, ignoring: %m"); continue; diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index 530688fc97f..a5ad9145770 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -768,6 +768,11 @@ void loop_device_relinquish(LoopDevice *d) { d->relinquished = true; } +void loop_device_unrelinquish(LoopDevice *d) { + assert(d); + d->relinquished = false; +} + int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret) { _cleanup_close_ int loop_fd = -1; _cleanup_free_ char *p = NULL; diff --git a/src/shared/loop-util.h b/src/shared/loop-util.h index 964ce3ed083..a33d7e3e59d 100644 --- a/src/shared/loop-util.h +++ b/src/shared/loop-util.h @@ -27,6 +27,7 @@ LoopDevice* loop_device_unref(LoopDevice *d); DEFINE_TRIVIAL_CLEANUP_FUNC(LoopDevice*, loop_device_unref); void loop_device_relinquish(LoopDevice *d); +void loop_device_unrelinquish(LoopDevice *d); int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size); diff --git a/src/udev/udevadm-lock.c b/src/udev/udevadm-lock.c index 60d6507aaf2..a1e04f65166 100644 --- a/src/udev/udevadm-lock.c +++ b/src/udev/udevadm-lock.c @@ -144,9 +144,7 @@ static int find_devno( const char *device, bool backing) { - _cleanup_close_ int fd = -1; - dev_t devt, whole_devt; - struct stat st; + dev_t devt; int r; assert(devnos); @@ -154,51 +152,19 @@ static int find_devno( assert(*devnos || *n_devnos == 0); assert(device); - fd = open(device, O_CLOEXEC|O_PATH); - if (fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", device); - - if (fstat(fd, &st) < 0) - return log_error_errno(errno, "Failed to stat '%s': %m", device); - - if (S_ISBLK(st.st_mode)) - devt = st.st_rdev; - else if (!backing) - return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Not a block device: %s", device); - else if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) - return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Not a block device, regular file or directory: %s", device); - else if (major(st.st_dev) != 0) - devt = st.st_dev; - else { - _cleanup_close_ int regfd = -1; - - /* If major(st.st_dev) is zero, this might mean we are backed by btrfs, which needs special - * handing, to get the backing device node. */ - - regfd = fd_reopen(fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK); - if (regfd < 0) - return log_error_errno(regfd, "Failed to open '%s': %m", device); - - r = btrfs_get_block_device_fd(regfd, &devt); - if (r == -ENOTTY) - return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Path '%s' not backed by block device.", device); - if (r < 0) - return log_error_errno(r, "Failed to acquire btrfs backing device of '%s': %m", device); - } - - r = block_get_whole_disk(devt, &whole_devt); + r = path_get_whole_disk(device, backing, &devt); if (r < 0) return log_error_errno(r, "Failed to find whole block device for '%s': %m", device); - if (typesafe_bsearch(&whole_devt, *devnos, *n_devnos, devt_compare_func)) { - log_debug("Device %u:%u already listed for locking, ignoring.", major(whole_devt), minor(whole_devt)); + if (typesafe_bsearch(&devt, *devnos, *n_devnos, devt_compare_func)) { + log_debug("Device %u:%u already listed for locking, ignoring.", major(devt), minor(devt)); return 0; } if (!GREEDY_REALLOC(*devnos, *n_devnos + 1)) return log_oom(); - (*devnos)[(*n_devnos)++] = whole_devt; + (*devnos)[(*n_devnos)++] = devt; /* Immediately sort again, to ensure the binary search above will work for the next device we add */ typesafe_qsort(*devnos, *n_devnos, devt_compare_func); diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index 2f1844ccf74..31cb52064ea 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -58,8 +58,8 @@ if [ "${verity_count}" -lt 1 ]; then echo "Verity device ${image}.raw not found in /dev/mapper/" exit 1 fi -umount "${image_dir}/mount" -umount "${image_dir}/mount2" +systemd-dissect --umount "${image_dir}/mount" +systemd-dissect --umount "${image_dir}/mount2" systemd-run -P -p RootImage="${image}.raw" cat /usr/lib/os-release | grep -q -F "MARKER=1" mv "${image}.verity" "${image}.fooverity" @@ -207,7 +207,7 @@ systemd-dissect --root-hash "${roothash}" --mount "${image}.gpt" "${image_dir}/m grep -q -F -f "$os_release" "${image_dir}/mount/usr/lib/os-release" grep -q -F -f "$os_release" "${image_dir}/mount/etc/os-release" grep -q -F "MARKER=1" "${image_dir}/mount/usr/lib/os-release" -umount "${image_dir}/mount" +systemd-dissect --umount "${image_dir}/mount" # add explicit -p MountAPIVFS=yes once to test the parser systemd-run -P -p RootImage="${image}.gpt" -p RootHash="${roothash}" -p MountAPIVFS=yes cat /usr/lib/os-release | grep -q -F "MARKER=1" @@ -350,8 +350,8 @@ RemainAfterExit=yes EOF systemctl start testservice-50f.service systemctl is-active testservice-50f.service -umount "${image_dir}/app0" -umount "${image_dir}/app1" +systemd-dissect --umount "${image_dir}/app0" +systemd-dissect --umount "${image_dir}/app1" echo OK >/testok