diff --git a/meson.build b/meson.build index b50466dcfd0ea..3945d31e0e19e 100644 --- a/meson.build +++ b/meson.build @@ -291,6 +291,7 @@ conf.set_quoted('SYSTEMD_USERWORK_PATH', libexecdir / 'syst conf.set_quoted('SYSTEMD_MOUNTWORK_PATH', libexecdir / 'systemd-mountwork') conf.set_quoted('SYSTEMD_NSRESOURCEWORK_PATH', libexecdir / 'systemd-nsresourcework') conf.set_quoted('SYSTEMD_VERITYSETUP_PATH', libexecdir / 'systemd-veritysetup') +conf.set_quoted('SYSTEMD_CLONESETUP_PATH', bindir / 'systemd-clonesetup') conf.set_quoted('SYSTEM_CONFIG_UNIT_DIR', pkgsysconfdir / 'system') conf.set_quoted('SYSTEM_DATA_UNIT_DIR', systemunitdir) conf.set_quoted('SYSTEM_ENV_GENERATOR_DIR', systemenvgeneratordir) @@ -2349,6 +2350,7 @@ subdir('src/debug-generator') subdir('src/delta') subdir('src/detect-virt') subdir('src/dissect') +subdir('src/clonesetup') subdir('src/environment-d-generator') subdir('src/escape') subdir('src/factory-reset') diff --git a/src/basic/special.h b/src/basic/special.h index a5cdfebae57be..bdf62327f73cb 100644 --- a/src/basic/special.h +++ b/src/basic/special.h @@ -97,6 +97,7 @@ #define SPECIAL_PCRFS_ROOT_SERVICE "systemd-pcrfs-root.service" #define SPECIAL_VALIDATEFS_SERVICE "systemd-validatefs@.service" #define SPECIAL_HIBERNATE_RESUME_SERVICE "systemd-hibernate-resume.service" +#define SPECIAL_CLONESETUP_TARGET "clonesetup.target" /* Services systemd relies on */ #define SPECIAL_DBUS_SERVICE "dbus.service" diff --git a/src/clonesetup/clonesetup-generator.c b/src/clonesetup/clonesetup-generator.c new file mode 100644 index 0000000000000..6214e43bba399 --- /dev/null +++ b/src/clonesetup/clonesetup-generator.c @@ -0,0 +1,193 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "alloc-util.h" +#include "errno-util.h" +#include "dropin.h" +#include "escape.h" +#include "fd-util.h" +#include "fileio.h" +#include "generator.h" +#include "log.h" +#include "path-util.h" +#include "special.h" +#include "string-util.h" +#include "unit-name.h" + + +static const char *arg_dest = NULL; + +/* Generate unit files that call the systemd-clonesetup binary to create or remove clone devices. */ +static int generate_clone_units(const char *clone_name, const char *source_dev, const char *dest_dev, + const char *metadata_dev, const char *options) { + + /* unit files for each device */ + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *source_unit = NULL, *dest_unit = NULL, *metadata_unit = NULL, + *escaped_source = NULL, *escaped_dest = NULL, *escaped_metadata = NULL, + *e = NULL, *unit = NULL, *clone_dev_path = NULL, *dmname = NULL; + int r; + + assert(clone_name); + assert(source_dev); + assert(dest_dev); + assert(metadata_dev); + + /* create clone_dev_path that holds path for new cloned device */ + clone_dev_path = path_join("/dev/mapper", clone_name); + if (!clone_dev_path) + return log_oom(); + + /* escape clone name */ + e = unit_name_escape(clone_name); + if (!e) + return log_oom(); + + /* Generate unit name for the clone service */ + r = unit_name_build("systemd-clonesetup", e, ".service", &unit); + if (r < 0) + return log_error_errno(r, "Failed to generate unit name: %m"); + + /* Generate unit names for dependencies */ + r = unit_name_from_path(source_dev, ".device", &source_unit); + if (r < 0) + return log_error_errno(r, "Failed to generate source device unit name: %m"); + + r = unit_name_from_path(dest_dev, ".device", &dest_unit); + if (r < 0) + return log_error_errno(r, "Failed to generate dest device unit name: %m"); + + r = unit_name_from_path(metadata_dev, ".device", &metadata_unit); + if (r < 0) + return log_error_errno(r, "Failed to generate metadata device unit name: %m"); + + /* Escape device paths for ExecStart command */ + escaped_source = cescape(source_dev); + if (!escaped_source) + return log_oom(); + + escaped_dest = cescape(dest_dev); + if (!escaped_dest) + return log_oom(); + + escaped_metadata = cescape(metadata_dev); + if (!escaped_metadata) + return log_oom(); + + r = generator_open_unit_file(arg_dest, /* source = */ NULL, unit, &f); + if (r < 0) + return r; + + fprintf(f, + "[Unit]\n" + "Description=Create dm-clone device %s\n" + "Documentation=man:dmsetup(8) man:fstab(5) man:systemd-fstab-generator(8)\n" + "DefaultDependencies=no\n" + "BindsTo=%s %s %s\n" + "Requires=%s %s %s\n" + "After=%s %s %s\n" + "Before=blockdev@dev-mapper-%s.target\n" + "Wants=blockdev@dev-mapper-%s.target\n" + "Conflicts=shutdown.target\n" + "\n" + "[Service]\n" + "Type=oneshot\n" + "RemainAfterExit=yes\n" + "ExecStart=" SYSTEMD_CLONESETUP_PATH " add '%s' '%s' '%s' '%s' '%s'\n" + "ExecStop=" SYSTEMD_CLONESETUP_PATH " remove %s\n" + "TimeoutSec=0\n", + clone_dev_path, + source_unit, dest_unit, metadata_unit, + source_unit, dest_unit, metadata_unit, + source_unit, dest_unit, metadata_unit, + e, e, + clone_name, escaped_source, escaped_dest, escaped_metadata, "", + clone_name); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write unit %s: %m", unit); + + /* symlink unit file to enable it */ + dmname = strjoin("dev-mapper-", e, ".device"); + r = generator_add_symlink(arg_dest, dmname, "requires", unit); + if (r < 0) + return r; + + /* Extend device timeout to allow clone service to complete */ + r = write_drop_in(arg_dest, dmname, 40, "device-timeout", + "# Automatically generated by systemd-clonesetup-generator\n\n" + "[Unit]\n" + "JobTimeoutSec=infinity\n"); + if (r < 0) + log_warning_errno(r, "Failed to write device timeout drop-in: %m"); + + /* Add to clonesetup.target so it starts at boot */ + r = generator_add_symlink(arg_dest, SPECIAL_CLONESETUP_TARGET, "requires", unit); + if (r < 0) + return r; + + return 0; +} + +static int add_clone_devices(void) { + _cleanup_fclose_ FILE *f = NULL; + unsigned clone_line = 0; + int r, ret = 0; + const char *fname; + + fname = secure_getenv("SYSTEMD_CLONETAB") ?: "/etc/clonetab"; + + r = fopen_unlocked(fname, "re", &f); + if (r < 0) { + if (errno != ENOENT) + log_error_errno(errno, "Failed to open %s: %m", fname); + return 0; + } + + for (;;) { + _cleanup_free_ char *line = NULL, *src = NULL, *name = NULL, *dst = NULL, *meta = NULL, *options = NULL; + int k; + + r = read_stripped_line(f, LONG_LINE_MAX, &line); + if (r < 0) + return log_error_errno(r, "Failed to read %s: %m", fname); + if (r == 0) + break; + + clone_line++; + + if (IN_SET(line[0], 0, '#')) + continue; + + k = sscanf(line, "%ms %ms %ms %ms %ms", &name, &src, &dst, &meta, &options); + if (k < 4 || k > 5) { + log_error("Failed to parse %s:%u, ignoring.", fname, clone_line); + continue; + } + + RET_GATHER(ret, generate_clone_units(name, src, dst, meta, options)); + } + + return ret; +} + +/* This generator reads /etc/clonetab and for each entry, writes unit files + * (creates systemd-clonesetup@.service and clonesetup.target.requires/systemd-clonesetup@.service) + * that clonesetup.target requires, and that run systemd-clonesetup (add device at boot, + * remove it at shutdown); systemd-clonesetup (used in systemd-clonesetup@.service) is the binary that + * uses device-mapper ioctls to create and remove the dm-clone devices. + * clonesetup.target groups these units so they run together at boot. + * Boot chain: sysinit.target has clonesetup.target in sysinit.target.wants/ (see units/meson.build), + * so at boot clonesetup.target starts and pulls in these units via clonesetup.target.requires/. */ +static int run(const char *dest, const char *dest_early, const char *dest_late) { + + /* dest usually is /run/systemd/generator */ + assert_se(arg_dest = dest); + + return add_clone_devices(); +} + +DEFINE_MAIN_GENERATOR_FUNCTION(run); diff --git a/src/clonesetup/clonesetup-ioctl.c b/src/clonesetup/clonesetup-ioctl.c new file mode 100644 index 0000000000000..71df4df637001 --- /dev/null +++ b/src/clonesetup/clonesetup-ioctl.c @@ -0,0 +1,199 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include + +#include "clonesetup-ioctl.h" +#include "device-private.h" +#include "fd-util.h" +#include "log.h" +#include "sd-device.h" +#include "stdio-util.h" /* xsprintf() */ +#include "string-util.h" + +/* Returns the size in bytes of the block device at dev_path. + * Loading the dm-clone table needs the source device size in sectors; sysfs + * reports size in 512-byte sectors. This reads sysfs and returns bytes so the + * caller can divide by 512 and pass the sector count to dm_clone_load_table(). */ +static int get_size(const char *dev_path, uint64_t *ret_size) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + uint64_t size; + int r; + + assert(dev_path); + assert(ret_size); + + r = sd_device_new_from_devname(&dev, dev_path); + if (r < 0) + return log_error_errno(r, "Failed to create device from '%s': %m", dev_path); + + r = device_get_sysattr_u64(dev, "size", &size); + if (r < 0) + return log_error_errno(r, "Failed to get device size for '%s': %m", dev_path); + + /* sysfs 'size' is in 512-byte sectors */ + *ret_size = size * 512; + return 0; +} + +/* Common helper used to run dm ioctls. */ +static int dm_ioctl_run(const char *name, uint32_t cmd, struct dm_ioctl *data, size_t data_size) { + _cleanup_close_ int fd = -EBADF; + struct dm_ioctl *dm = data; + + assert(name); + assert(data); + assert(data_size >= sizeof(struct dm_ioctl)); + + dm->version[0] = DM_VERSION_MAJOR; + dm->version[1] = DM_VERSION_MINOR; + dm->version[2] = DM_VERSION_PATCHLEVEL; + dm->data_size = data_size; + + assert(strlen(name) < sizeof_field(struct dm_ioctl, name)); + strncpy_exact(dm->name, name, sizeof(dm->name)); + + fd = open("/dev/mapper/control", O_RDWR | O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open /dev/mapper/control: %m"); + + if (ioctl(fd, cmd, dm) < 0) + return log_error_errno(errno, "DM ioctl failed: %m"); + + return 0; +} + +/* First dm ioctl needed to create a device. */ +static int dm_clone_create(const char *name) { + assert(name); + + struct dm_ioctl dm = {}; + return dm_ioctl_run(name, DM_DEV_CREATE, &dm, sizeof(struct dm_ioctl)); +} + +/* Second dm ioctl needed to create a device. */ +static int dm_clone_load_table(const char *name, uint64_t size_sectors, const char *target_params) { + _cleanup_free_ void *dm_buf = NULL; + char *params_buf; + size_t params_len, dm_size; + struct dm_ioctl *dm; + struct dm_target_spec *tgt; + + assert(name); + assert(target_params); + + params_len = strlen(target_params) + 1; + dm_size = sizeof(struct dm_ioctl) + sizeof(struct dm_target_spec) + params_len; + dm_buf = malloc0(dm_size); + if (!dm_buf) + return -ENOMEM; + dm = dm_buf; + + dm->data_start = sizeof(struct dm_ioctl); + dm->target_count = 1; + + tgt = (struct dm_target_spec *) ((uint8_t *) dm + dm->data_start); + tgt->sector_start = 0; + tgt->length = size_sectors; + strncpy(tgt->target_type, "clone", sizeof(tgt->target_type)); + tgt->next = 0; + + params_buf = (char *) tgt + sizeof(struct dm_target_spec); + strcpy(params_buf, target_params); + tgt->status = 0; + + return dm_ioctl_run(name, DM_TABLE_LOAD, dm, dm_size); +} + +/* Third and final dm ioctl needed to create a device. */ +static int dm_clone_activate(const char *name) { + assert(name); + + struct dm_ioctl dm = {}; + + return dm_ioctl_run(name, DM_DEV_SUSPEND, &dm, sizeof(struct dm_ioctl)); +} + +/* Calls multiple dm ioctls to create device. */ +int dm_clone_create_device( + const char *name, + const char *source_dev, + const char *dest_dev, + const char *metadata_dev) { + + uint64_t src_dev_size_sectors, src_dev_size; + char target_params[256]; + int r; + + assert(name); + assert(source_dev); + assert(dest_dev); + assert(metadata_dev); + + r = get_size(source_dev, &src_dev_size); + if (r < 0) + return r; + + src_dev_size_sectors = src_dev_size / 512; + + /* dm-clone target params: [options] + * 8 = region size in sectors (4KB regions with 512-byte sectors) + * 1 = hydration threshold (regions to hydrate per batch) + * no_hydration = don't start automatic background hydration */ + xsprintf(target_params, "%s %s %s 8 1 no_hydration", metadata_dev, dest_dev, source_dev); + + r = dm_clone_create(name); + if (r < 0) + return r; + + r = dm_clone_load_table(name, src_dev_size_sectors, target_params); + if (r < 0) + return r; + + r = dm_clone_activate(name); + if (r < 0) + return r; + + log_info("Device %s active.", name); + return 0; +} + +/* Calls dm ioctl to send a message to the device. */ +int dm_clone_send_message(const char *name, const char *message) { + _cleanup_free_ void *dm_buf = NULL; + struct dm_ioctl *dm; + struct dm_target_msg *msg; + size_t dm_size, msg_len; + + assert(name); + assert(message); + + msg_len = strlen(message) + 1; + dm_size = sizeof(struct dm_ioctl) + sizeof(struct dm_target_msg) + msg_len; + dm_buf = malloc0(dm_size); + if (!dm_buf) + return -ENOMEM; + dm = dm_buf; + dm->data_start = sizeof(struct dm_ioctl); + + msg = (struct dm_target_msg *) ((char *) dm + dm->data_start); + strcpy(msg->message, message); + + return dm_ioctl_run(name, DM_TARGET_MSG, dm, dm_size); +} + +/* Calls dm ioctl to remove a device. */ +int dm_clone_remove_device(const char *name) { + struct dm_ioctl dm = {}; + int r; + + assert(name); + r = dm_ioctl_run(name, DM_DEV_REMOVE, &dm, sizeof(struct dm_ioctl)); + if (r < 0) + return r; + + log_info("Device %s inactive.", name); + return 0; +} diff --git a/src/clonesetup/clonesetup-ioctl.h b/src/clonesetup/clonesetup-ioctl.h new file mode 100644 index 0000000000000..8bbc781a85e82 --- /dev/null +++ b/src/clonesetup/clonesetup-ioctl.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +int dm_clone_create_device( + const char *name, + const char *source_dev, + const char *dest_dev, + const char *metadata_dev); + +int dm_clone_send_message(const char *name, const char *message); + +int dm_clone_remove_device(const char *name); + diff --git a/src/clonesetup/clonesetup.c b/src/clonesetup/clonesetup.c new file mode 100644 index 0000000000000..3f230b63b932a --- /dev/null +++ b/src/clonesetup/clonesetup.c @@ -0,0 +1,170 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include +#include +#include +#include /* access */ + +#include "alloc-util.h" +#include "argv-util.h" +#include "build.h" +#include "clonesetup-ioctl.h" +#include "log.h" +#include "main-func.h" +#include "pretty-print.h" +#include "verbs.h" +#include "path-util.h" /* path_join */ +#include "time-util.h" /* USEC_PER_SEC */ +#include "udev-util.h" /* device_wait_for_devlink */ + +/* dm-clone device creation workflow: + * 1. Create the dm-clone device + * 2. Enable background hydration + * 3. (Optional) Replace with linear mapping to finalize */ +static int clone_device(const char *clone_name, const char *source_dev, const char *dest_dev, + const char *metadata_dev) { + + _cleanup_free_ char *clone_dev_path = NULL; + int r; + + assert(clone_name); + assert(source_dev); + assert(dest_dev); + assert(metadata_dev); + + /* create clone device path to check if clone device already exists */ + clone_dev_path = path_join("/dev/mapper", clone_name); + if (!clone_dev_path) + return log_oom(); + + if (access(clone_dev_path, F_OK) >= 0) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device '%s' already exists.", clone_dev_path); + + r = dm_clone_create_device(clone_name, source_dev, dest_dev, metadata_dev); + if (r < 0) + return log_error_errno(r, "Failed to create dm-clone device: %m"); + + /* Wait for udev to create /dev/mapper/ */ + r = device_wait_for_devlink(clone_dev_path, "block", 10 * USEC_PER_SEC, NULL); + if (r < 0) + return log_error_errno(r, "Failed to wait for device %s: %m", clone_dev_path); + + r = dm_clone_send_message(clone_name, "enable_hydration"); + if (r < 0) + return log_error_errno(r, "Failed to send dm message: %m"); + + return 0; +} + +/* Arguments: systemd-clonesetup add NAME SOURCE-DEVICE DST_DEVICE META-DEVICE [OPTIONS] */ +static int verb_add(int argc, char *argv[], void *userdata) { + int r; + + assert(argc >= 5 && argc <= 6); + + const char *name = ASSERT_PTR(argv[1]), + *src_dev = ASSERT_PTR(argv[2]), + *dst_dev = ASSERT_PTR(argv[3]), + *meta_dev = ASSERT_PTR(argv[4]); + + log_debug("%s %s %s %s %s opts=%s ", __func__, + name, src_dev, dst_dev, meta_dev, ""); + + r = clone_device(name, src_dev, dst_dev, meta_dev); + if (r < 0) + return r; + + return 0; +} + +static int verb_remove(int argc, char *argv[], void *userdata) { + const char *name = ASSERT_PTR(argv[1]); + int r; + + r = dm_clone_remove_device(name); + if (r < 0) + return r; + + return 0; +} + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-clonesetup", "8", &link); + if (r < 0) + return log_oom(); + + printf("%1$s add NAME SOURCE-DEVICE DST-DEVICE META-DEVICE [OPTIONS] \n" + "%1$s remove VOLUME\n\n" + "%2$sAdd or remove a dm clone device.%3$s\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + "\nSee the %4$s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + {} + }; + + int c; + + assert(argc >= 0); + assert(argv); + + if (argv_looks_like_help(argc, argv)) + return help(); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + return 1; +} + +/* systemd-clonesetup uses device-mapper ioctls to create and remove the + * dm-clone devices. */ +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + umask(0022); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + static const Verb verbs[] = { + { "add", 5, 6, 0, verb_add }, + { "remove", 2, 2, 0, verb_remove }, + {} + }; + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/clonesetup/meson.build b/src/clonesetup/meson.build new file mode 100644 index 0000000000000..9e7cdfe347fa4 --- /dev/null +++ b/src/clonesetup/meson.build @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +systemd_clonesetup_sources = files( + 'clonesetup-ioctl.c', + 'clonesetup.c', +) + +executables += [ + executable_template + { + 'name' : 'systemd-clonesetup', + 'public' : true, + 'sources' : systemd_clonesetup_sources, + }, + generator_template + { + 'name' : 'systemd-clonesetup-generator', + 'sources' : files('clonesetup-generator.c'), + }, +] diff --git a/test/integration-tests/TEST-90-CLONESETUP/meson.build b/test/integration-tests/TEST-90-CLONESETUP/meson.build new file mode 100644 index 0000000000000..77370ce4588c4 --- /dev/null +++ b/test/integration-tests/TEST-90-CLONESETUP/meson.build @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +integration_tests += [ + integration_test_template + { + 'name' : fs.name(meson.current_source_dir()), + 'vm' : true, + }, +] diff --git a/test/integration-tests/meson.build b/test/integration-tests/meson.build index 5d71e87c79cbc..c0843fec5e7d6 100644 --- a/test/integration-tests/meson.build +++ b/test/integration-tests/meson.build @@ -102,6 +102,7 @@ foreach dirname : [ 'TEST-87-AUX-UTILS-VM', 'TEST-88-UPGRADE', 'TEST-89-RESOLVED-MDNS', + 'TEST-90-CLONESETUP', ] subdir(dirname) endforeach diff --git a/test/units/TEST-90-CLONESETUP.sh b/test/units/TEST-90-CLONESETUP.sh new file mode 100755 index 0000000000000..757916c4c2dc4 --- /dev/null +++ b/test/units/TEST-90-CLONESETUP.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Test clonesetup generator and systemd-clonesetup + +at_exit() { + set +e + + rm -f /etc/clonetab + [[ -e /tmp/clonetab.bak ]] && cp -fv /tmp/clonetab.bak /etc/clonetab + [[ -n "${LOOP_SRC:-}" ]] && losetup -d "$LOOP_SRC" + [[ -n "${LOOP_DST:-}" ]] && losetup -d "$LOOP_DST" + [[ -n "${LOOP_META:-}" ]] && losetup -d "$LOOP_META" + [[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR" + dmsetup remove testclonesetup 2>/dev/null || true + + systemctl daemon-reload +} + +trap at_exit EXIT + +clonesetup_start_and_check() { + local volume unit + + volume="${1:?}" + unit="systemd-clonesetup@$volume.service" + + # The unit existence check should always pass + [[ "$(systemctl show -P LoadState "$unit")" == loaded ]] + systemctl list-unit-files "$unit" + + systemctl start "$unit" + systemctl status "$unit" + test -e "/dev/mapper/$volume" + dmsetup status "$volume" + + systemctl stop "$unit" + # wait for udev to finish processing so the device node state is in sync + # before the API returns. + udevadm settle --timeout=10 + test ! -e "/dev/mapper/$volume" +} + +prereq() { + # Skip when kernel lacks dm-clone (CONFIG_DM_CLONE) + modprobe dm_clone 2>/dev/null || true + if [[ ! -d /sys/module/dm_clone ]]; then + echo "no dm-clone" >/skipped + exit 77 + fi + echo "Found required kernel module: dm_clone" +} + +prereq + +# Use a common workdir +WORKDIR="$(mktemp -d)" + +# Create test images for source, destination, and metadata +IMG_SRC="$WORKDIR/source.img" +IMG_DST="$WORKDIR/dest.img" +IMG_META="$WORKDIR/meta.img" + +truncate -s 32M "$IMG_SRC" +truncate -s 32M "$IMG_DST" +truncate -s 8M "$IMG_META" + +# Set up loop devices +LOOP_SRC="$(losetup --show --find "$IMG_SRC")" +LOOP_DST="$(losetup --show --find "$IMG_DST")" +LOOP_META="$(losetup --show --find "$IMG_META")" + +udevadm settle --timeout=60 + +# Backup existing clonetab if any +[[ -e /etc/clonetab ]] && cp -fv /etc/clonetab /tmp/clonetab.bak + +# Create test clonetab +cat >/etc/clonetab <