diff --git a/man/systemd-repart.xml b/man/systemd-repart.xml index 27e35d19da..fa7b282bc0 100644 --- a/man/systemd-repart.xml +++ b/man/systemd-repart.xml @@ -244,6 +244,18 @@ + + + + Controls copy-on-write for newly created image files on filesystems that support it. + If true, copy-on-write is enabled; if false, it is disabled. If set to auto, the + filesystem or parent directory default is left unchanged. Copy-on-write allows files to be reflinked + into the image, reducing both build time and disk usage, but may result in fragmentation for images + subject to frequent random writes. Defaults to auto. + + + + diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index 2e9a27f2a8..76c55e30b6 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -1183,12 +1183,13 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_ /* Sockets cannot be open()ed, only pinned via O_PATH. */ assert(!FLAGS_SET(xopen_flags, XO_SOCKET) || FLAGS_SET(open_flags, O_PATH)); /* XO_TRIGGER_AUTOMOUNT requires O_PATH and does not support creating inodes. XO_SUBVOLUME - * requires O_CREAT, and XO_NOCOW needs a writable fd for its chattr ioctl, so neither is + * requires O_CREAT, and XO_COW/XO_NOCOW need a writable fd for their chattr ioctl, so none are * compatible with XO_TRIGGER_AUTOMOUNT. */ assert(!FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) || (FLAGS_SET(open_flags, O_PATH) && !FLAGS_SET(open_flags, O_CREAT))); assert(!(FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) && FLAGS_SET(xopen_flags, XO_SUBVOLUME))); - assert(!(FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) && FLAGS_SET(xopen_flags, XO_NOCOW))); + assert(!(FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) && (xopen_flags & (XO_COW|XO_NOCOW)))); + assert((xopen_flags & (XO_COW|XO_NOCOW)) != (XO_COW|XO_NOCOW)); /* Don't specify an access mode if you want auto mode. */ assert(!FLAGS_SET(xopen_flags, XO_AUTO_RW_RO) || (open_flags & O_ACCMODE_STRICT) == 0); @@ -1202,7 +1203,8 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_ * * • If the path is specified NULL or empty, behaves like fd_reopen(). * - * • If XO_NOCOW is specified will turn on the NOCOW btrfs flag on the file, if available. + * • If XO_COW or XO_NOCOW is specified will turn off or on the NOCOW btrfs flag on the file, if + * available. * * • if XO_REGULAR is specified will return an error if inode is not a regular file. * @@ -1436,8 +1438,8 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_ goto error; } - if (FLAGS_SET(xopen_flags, XO_NOCOW)) { - r = chattr_fd(fd, FS_NOCOW_FL, FS_NOCOW_FL); + if (xopen_flags & (XO_COW|XO_NOCOW)) { + r = chattr_fd(fd, FLAGS_SET(xopen_flags, XO_NOCOW) ? FS_NOCOW_FL : 0, FS_NOCOW_FL); if (r < 0 && !ERRNO_IS_IOCTL_NOT_SUPPORTED(r)) goto error; } diff --git a/src/basic/fs-util.h b/src/basic/fs-util.h index 0608d713ae..bc33f2f8e9 100644 --- a/src/basic/fs-util.h +++ b/src/basic/fs-util.h @@ -112,10 +112,11 @@ typedef enum XOpenFlags { XO_LABEL = 1 << 0, /* When creating: relabel */ XO_SUBVOLUME = 1 << 1, /* When creating as directory: make it a subvolume */ XO_NOCOW = 1 << 2, /* Enable NOCOW mode after opening */ - XO_REGULAR = 1 << 3, /* Fail if the inode is not a regular file */ - XO_SOCKET = 1 << 4, /* Fail if the inode is not a socket */ - XO_TRIGGER_AUTOMOUNT = 1 << 5, /* Trigger automounts via open_tree(). Requires O_PATH. */ - XO_AUTO_RW_RO = 1 << 6, /* Open in O_RDWR mode if possible, O_RDONLY if not */ + XO_COW = 1 << 3, /* Enable COW mode after opening */ + XO_REGULAR = 1 << 4, /* Fail if the inode is not a regular file */ + XO_SOCKET = 1 << 5, /* Fail if the inode is not a socket */ + XO_TRIGGER_AUTOMOUNT = 1 << 6, /* Trigger automounts via open_tree(). Requires O_PATH. */ + XO_AUTO_RW_RO = 1 << 7, /* Open in O_RDWR mode if possible, O_RDONLY if not */ } XOpenFlags; int open_mkdir_at_full(int dirfd, const char *path, int flags, XOpenFlags xopen_flags, mode_t mode); diff --git a/src/repart/repart.c b/src/repart/repart.c index 5ba535e032..e3dd9ab3f6 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -215,6 +215,7 @@ static char *arg_generate_crypttab = NULL; static Set *arg_verity_settings = NULL; static bool arg_relax_copy_block_security = false; static bool arg_varlink = false; +static int arg_cow = -1; static bool arg_eltorito = false; static char *arg_eltorito_system = NULL; static char *arg_eltorito_volume = NULL; @@ -10232,6 +10233,13 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + OPTION_LONG("cow", "BOOL|auto", + "Whether to enable copy-on-write for newly created image files"): + r = parse_tristate_argument_with_auto("--cow=", opts.arg, &arg_cow); + if (r < 0) + return r; + break; + OPTION_LONG("sector-size", "SIZE", "Set the logical sector size for the image"): r = parse_sector_size(opts.arg, &arg_sector_size); @@ -11018,7 +11026,12 @@ static int find_root(Context *context) { if (!s) return log_oom(); - fd = xopenat_full(AT_FDCWD, arg_node, open_flags|O_CREAT|O_EXCL|O_NOFOLLOW, XO_NOCOW, 0666); + fd = xopenat_full( + AT_FDCWD, + arg_node, + open_flags|O_CREAT|O_EXCL|O_NOFOLLOW, + arg_cow < 0 ? 0 : (arg_cow > 0 ? XO_COW : XO_NOCOW), + 0666); if (fd < 0) return log_error_errno(fd, "Failed to create '%s': %m", arg_node); diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh index 9970dac662..551e72c53c 100755 --- a/test/units/TEST-58-REPART.sh +++ b/test/units/TEST-58-REPART.sh @@ -94,6 +94,108 @@ else exit 1 fi +testcase_cow() { + local attrs cow_image default_nocow defs image imgs nocow_image probe + + defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")" + imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$imgs'" RETURN + + # Skip the checks entirely if the underlying filesystem does not support the attribute. + if ! chattr -C "$imgs"; then + echo "NOCOW is not supported on $imgs, skipping tests" + return + fi + + probe="$imgs/probe" + touch "$probe" + if ! chattr +C "$probe"; then + echo "NOCOW is not supported on $imgs, skipping tests" + return + fi + if ! chattr -C "$probe"; then + echo "COW is not supported on $imgs, skipping tests" + return + fi + rm "$probe" + + chattr +C "$imgs" + + image="$imgs/inherit-nocow.raw" + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --empty=create \ + --size=16M \ + --cow=auto \ + --dry-run=no \ + "$image" + + attrs="$(lsattr -d -- "$image")" + assert_neq "$attrs" "" + read -r attrs _ <<<"$attrs" + assert_in "C" "$attrs" + + cow_image="$imgs/cow.raw" + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --empty=create \ + --size=16M \ + --cow=yes \ + --dry-run=no \ + "$cow_image" + + attrs="$(lsattr -d -- "$cow_image")" + assert_neq "$attrs" "" + read -r attrs _ <<<"$attrs" + assert_not_in "C" "$attrs" + + chattr -C "$imgs" + + probe="$imgs/probe" + touch "$probe" + attrs="$(lsattr -d -- "$probe")" + assert_neq "$attrs" "" + read -r attrs _ <<<"$attrs" + if [[ "$attrs" == *C* ]]; then + default_nocow=1 + else + default_nocow=0 + fi + rm "$probe" + + image="$imgs/inherit-cow.raw" + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --empty=create \ + --size=16M \ + --dry-run=no \ + "$image" + + attrs="$(lsattr -d -- "$image")" + assert_neq "$attrs" "" + read -r attrs _ <<<"$attrs" + if (( default_nocow )); then + assert_in "C" "$attrs" + else + assert_not_in "C" "$attrs" + fi + + nocow_image="$imgs/nocow.raw" + systemd-repart --offline="$OFFLINE" \ + --definitions="$defs" \ + --empty=create \ + --size=16M \ + --cow=no \ + --dry-run=no \ + "$nocow_image" + + attrs="$(lsattr -d -- "$nocow_image")" + assert_neq "$attrs" "" + read -r attrs _ <<<"$attrs" + assert_in "C" "$attrs" +} + testcase_basic() { local defs imgs output local loop volume