diff --git a/Documentation/Feature-by-version.rst b/Documentation/Feature-by-version.rst index 52c72b1a6..d3a625421 100644 --- a/Documentation/Feature-by-version.rst +++ b/Documentation/Feature-by-version.rst @@ -177,7 +177,7 @@ features see :doc:`Status` page. 6.12 (stable) ------------- --6.12 - cancellable discard/TRIM +6.12 - cancellable discard/TRIM Add more points where the discard can be interrupted by signals before it finishes the whole operation. @@ -237,6 +237,9 @@ features see :doc:`Status` page. The defrag ioctl also accepts the negative zstd levels that can be set as mount option. +6.16 - *standalone mount option nologreplay removed* + Deprecated in 5.9 and replaced with *rescue=nologreplay*. + 6.17 (latest) ------------- @@ -261,6 +264,18 @@ features see :doc:`Status` page. File holes, ranges not representing data, were emulated by a zero filled data. This is less efficient than puching holes. +6.18 - *move rev-verify feature under CONFIG_BTRFS_DEBUG* + Config option CONFIG_BTRFS_FS_REF_VERIFY has been removed and the + debugging functionality of *ref-verify* moved under CONFIG_BTRFS_DEBUG. + +6.18 - *new mount option ref_tracker for reference tracking* + A debugging feature to track references (now implemented for delayed + refs) and report leaks eventually. + +6.18 - *experimental* enable block size > page size support + Initial support for *bs > ps* with limitations (no direct IO, raid56, + encoded read/write). + 5.x --- diff --git a/Documentation/btrfs-replace.rst b/Documentation/btrfs-replace.rst index c63f1dfaa..ddbb81c2d 100644 --- a/Documentation/btrfs-replace.rst +++ b/Documentation/btrfs-replace.rst @@ -37,6 +37,18 @@ start [options] | larger target device; this can be achieved with ``btrfs filesystem resize :max /path`` + .. note:: + Device replace can be interrupted by various events after v6.19 kernel, + including but not limited to power management suspend/hibernate, + filesystem freezing, cgroup freezing (utilized by systemd for slice freezing) + and pending signals. + + The running device replace will be cancelled after such interruption, and + the end user needs to restart the device replace from the beginning. + + Thus it's recommended to inhibit suspend/hiberate before executing the + device replace operation. + ``Options`` -r diff --git a/Documentation/ch-scrub-intro.rst b/Documentation/ch-scrub-intro.rst index c688cfeac..6819595cb 100644 --- a/Documentation/ch-scrub-intro.rst +++ b/Documentation/ch-scrub-intro.rst @@ -56,6 +56,14 @@ read-write mounted filesystem. To avoid any writes from scrub, one has to run read-only scrub on read-only filesystem. +.. note:: + Scrub can be interrupted by various events after v6.19 kernel, including + but not limited to power management suspend/hibernate, filesystem freezing, + cgroup freezing (utilized by systemd for slice freezing) and pending signals. + + The running scrub will be cancelled after such interruption, and can be resumed + by :command:`btrfs scrub resume` command. + The user is supposed to run it manually or via a periodic system service. The recommended period is a month but it could be less. The estimated device bandwidth utilization is about 80% on an idle filesystem. diff --git a/btrfs-find-root.c b/btrfs-find-root.c index f0f0079eb..5ed6fa515 100644 --- a/btrfs-find-root.c +++ b/btrfs-find-root.c @@ -321,7 +321,7 @@ static void print_find_root_result(struct cache_tree *result, } static const char * const btrfs_find_root_usage[] = { - "btrfs-find-usage [options] ", + "btrfs-find-root [options] ", "Attempt to find tree roots on the device", "", OPTLINE("-a", "search through all metadata even if the root has been found"), diff --git a/check/qgroup-verify.c b/check/qgroup-verify.c index 0deac0b3f..001fce078 100644 --- a/check/qgroup-verify.c +++ b/check/qgroup-verify.c @@ -45,7 +45,6 @@ void qgroup_set_item_count_ptr(u64 *item_count_ptr) qgroup_item_count = item_count_ptr; } -/*#define QGROUP_VERIFY_DEBUG*/ static unsigned long tot_extents_scanned = 0; static struct qgroup_count *find_count(u64 qgroupid); @@ -157,7 +156,6 @@ struct ref { struct rb_node bytenr_node; }; -#ifdef QGROUP_VERIFY_DEBUG static void print_ref(struct ref *ref) { printf("bytenr: %llu\t\tnum_bytes: %llu\t\t parent: %llu\t\t" @@ -165,7 +163,7 @@ static void print_ref(struct ref *ref) ref->parent, ref->root); } -static void print_all_refs(void) +static void __maybe_unused print_all_refs(void) { unsigned long count = 0; struct ref *ref; @@ -184,7 +182,6 @@ static void print_all_refs(void) printf("%lu extents scanned with %lu refs in total.\n", tot_extents_scanned, count); } -#endif /* * Store by bytenr in rbtree @@ -490,14 +487,12 @@ static int account_one_extent(struct ulist *roots, u64 bytenr, u64 num_bytes) count->info.exclusive_compressed += num_bytes; } } -#ifdef QGROUP_VERIFY_DEBUG - printf("account (%llu, %llu), qgroup %llu/%llu, rfer %llu," + pr_verbose(LOG_DEBUG, "account (%llu, %llu), qgroup %u/%llu, rfer %llu," " excl %llu, refs %llu, roots %llu\n", bytenr, num_bytes, btrfs_qgroup_level(count->qgroupid), btrfs_qgroup_subvolid(count->qgroupid), count->info.referenced, count->info.exclusive, nr_refs, nr_roots); -#endif } inc_qgroup_seq(roots->nnodes); @@ -636,7 +631,6 @@ static void free_tree_blocks(void) tree_blocks = NULL; } -#ifdef QGROUP_VERIFY_DEBUG static void print_tree_block(u64 bytenr, struct tree_block *block) { struct ref *ref; @@ -656,7 +650,7 @@ static void print_tree_block(u64 bytenr, struct tree_block *block) printf("\n"); } -static void print_all_tree_blocks(void) +static void __maybe_unused print_all_tree_blocks(void) { struct ulist_iterator uiter; struct ulist_node *unode; @@ -670,7 +664,6 @@ static void print_all_tree_blocks(void) while ((unode = ulist_next(tree_blocks, &uiter))) print_tree_block(unode_bytenr(unode), unode_tree_block(unode)); } -#endif static int add_refs_for_leaf_items(struct extent_buffer *eb, u64 ref_parent) { diff --git a/common/parse-utils.c b/common/parse-utils.c index d2e2e0887..77cf2a0c7 100644 --- a/common/parse-utils.c +++ b/common/parse-utils.c @@ -72,7 +72,7 @@ int parse_u64(const char *str, u64 *result) * Returned values are u64, value validation and interpretation should be done * by the caller. */ -int parse_range(const char *range, u64 *start, u64 *end) +int parse_range_u64(const char *range, u64 *start, u64 *end) { char *dots; char *endptr; @@ -135,7 +135,7 @@ int parse_range_u32(const char *range, u32 *start, u32 *end) u64 tmp_start; u64 tmp_end; - if (parse_range(range, &tmp_start, &tmp_end)) + if (parse_range_u64(range, &tmp_start, &tmp_end)) return 1; if (range_to_u32(tmp_start, tmp_end, start, end)) @@ -150,7 +150,7 @@ int parse_range_u32(const char *range, u32 *start, u32 *end) */ int parse_range_strict(const char *range, u64 *start, u64 *end) { - if (parse_range(range, start, end) == 0) { + if (parse_range_u64(range, start, end) == 0) { if (*start >= *end) { error("range %llu..%llu not allowed", *start, *end); return 1; diff --git a/common/parse-utils.h b/common/parse-utils.h index 75a0596fd..1f77293c3 100644 --- a/common/parse-utils.h +++ b/common/parse-utils.h @@ -24,7 +24,7 @@ enum btrfs_csum_type parse_csum_type(const char *s); int parse_u64(const char *str, u64 *result); int parse_u64_with_suffix(const char *s, u64 *value_ret); int parse_range_u32(const char *range, u32 *start, u32 *end); -int parse_range(const char *range, u64 *start, u64 *end); +int parse_range_u64(const char *range, u64 *start, u64 *end); int parse_range_strict(const char *range, u64 *start, u64 *end); int parse_bg_profile(const char *profile, u64 *flags); int parse_compress_type(const char *type); diff --git a/convert/main.c b/convert/main.c index 02677070d..190f38a11 100644 --- a/convert/main.c +++ b/convert/main.c @@ -948,8 +948,7 @@ static int make_convert_data_block_groups(struct btrfs_trans_handle *trans, u64 cur_backup = cur; len = min(max_chunk_size, - round_up(cache->start + cache->size, - BTRFS_STRIPE_LEN) - cur); + cache->start + cache->size - cur); ret = btrfs_alloc_data_chunk(trans, fs_info, &cur_backup, len); if (ret < 0) break; diff --git a/libbtrfsutil/python/module.c b/libbtrfsutil/python/module.c index b4a89732c..b10a5f576 100644 --- a/libbtrfsutil/python/module.c +++ b/libbtrfsutil/python/module.c @@ -120,12 +120,21 @@ void path_cleanup(struct path_arg *path) } static PyMethodDef btrfsutil_methods[] = { + /* Aliases: sync, fs_sync */ {"sync", (PyCFunction)filesystem_sync, METH_VARARGS | METH_KEYWORDS, "sync(path)\n\n" "Sync a specific Btrfs filesystem.\n\n" "Arguments:\n" "path -- string, bytes, path-like object, or open file descriptor"}, + {"fs_sync", (PyCFunction)filesystem_sync, + METH_VARARGS | METH_KEYWORDS, + "fs_sync(path)\n\n" + "Sync a specific Btrfs filesystem.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor"}, + + /* Aliases: start_sync, fs_start_sync */ {"start_sync", (PyCFunction)start_sync, METH_VARARGS | METH_KEYWORDS, "start_sync(path) -> int\n\n" @@ -133,6 +142,15 @@ static PyMethodDef btrfsutil_methods[] = { "transaction ID.\n\n" "Arguments:\n" "path -- string, bytes, path-like object, or open file descriptor"}, + {"fs_start_sync", (PyCFunction)start_sync, + METH_VARARGS | METH_KEYWORDS, + "fs_start_sync(path) -> int\n\n" + "Start a sync on a specific Btrfs filesystem and return the\n" + "transaction ID.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor"}, + + /* Aliases: wait_sync, fs_wait_sync */ {"wait_sync", (PyCFunction)wait_sync, METH_VARARGS | METH_KEYWORDS, "wait_sync(path, transid=0)\n\n" @@ -141,18 +159,44 @@ static PyMethodDef btrfsutil_methods[] = { "path -- string, bytes, path-like object, or open file descriptor\n" "transid -- int transaction ID to wait for, or zero for the current\n" "transaction"}, + {"fs_wait_sync", (PyCFunction)wait_sync, + METH_VARARGS | METH_KEYWORDS, + "fs_wait_sync(path, transid=0)\n\n" + "Wait for a transaction to sync.\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor\n" + "transid -- int transaction ID to wait for, or zero for the current\n" + "transaction"}, + + /* Aliases: is_subvolume, subvolume_is_valid */ {"is_subvolume", (PyCFunction)is_subvolume, METH_VARARGS | METH_KEYWORDS, "is_subvolume(path) -> bool\n\n" "Get whether a file is a subvolume.\n\n" "Arguments:\n" "path -- string, bytes, path-like object, or open file descriptor"}, + {"subvolume_is_valid", (PyCFunction)is_subvolume, + METH_VARARGS | METH_KEYWORDS, + "subvolume_is_valid(path) -> bool\n\n" + "Get whether a file is a subvolume.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor"}, + + /* Aliases: subvolume_id, subvolume_get_id */ {"subvolume_id", (PyCFunction)subvolume_id, METH_VARARGS | METH_KEYWORDS, "subvolume_id(path) -> int\n\n" "Get the ID of the subvolume containing a file.\n\n" "Arguments:\n" "path -- string, bytes, path-like object, or open file descriptor"}, + {"subvolume_get_id", (PyCFunction)subvolume_id, + METH_VARARGS | METH_KEYWORDS, + "subvolume_id(path) -> int\n\n" + "Get the ID of the subvolume containing a file.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor"}, + + /* Aliases: subvolume_path, subvolume_get_path */ {"subvolume_path", (PyCFunction)subvolume_path, METH_VARARGS | METH_KEYWORDS, "subvolume_path(path, id=0) -> int\n\n" @@ -161,6 +205,16 @@ static PyMethodDef btrfsutil_methods[] = { "path -- string, bytes, path-like object, or open file descriptor\n" "id -- if not zero, instead of returning the subvolume path of the\n" "given path, return the path of the subvolume with this ID"}, + {"subvolume_get_path", (PyCFunction)subvolume_path, + METH_VARARGS | METH_KEYWORDS, + "subvolume_get_path(path, id=0) -> int\n\n" + "Get the path of a subvolume relative to the filesystem root.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor\n" + "id -- if not zero, instead of returning the subvolume path of the\n" + "given path, return the path of the subvolume with this ID"}, + + /* Aliases: subvolume_info, subvolume_get_info */ {"subvolume_info", (PyCFunction)subvolume_info, METH_VARARGS | METH_KEYWORDS, "subvolume_info(path, id=0) -> SubvolumeInfo\n\n" @@ -169,12 +223,30 @@ static PyMethodDef btrfsutil_methods[] = { "path -- string, bytes, path-like object, or open file descriptor\n" "id -- if not zero, instead of returning information about the\n" "given path, return information about the subvolume with this ID"}, + {"subvolume_get_info", (PyCFunction)subvolume_info, + METH_VARARGS | METH_KEYWORDS, + "subvolume_get_info(path, id=0) -> SubvolumeInfo\n\n" + "Get information about a subvolume.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor\n" + "id -- if not zero, instead of returning information about the\n" + "given path, return information about the subvolume with this ID"}, + + /* Aliases get_subvolume_read_only, subvolume_get_read_only */ {"get_subvolume_read_only", (PyCFunction)get_subvolume_read_only, METH_VARARGS | METH_KEYWORDS, "get_subvolume_read_only(path) -> bool\n\n" "Get whether a subvolume is read-only.\n\n" "Arguments:\n" "path -- string, bytes, path-like object, or open file descriptor"}, + {"subvolume_get_read_only", (PyCFunction)get_subvolume_read_only, + METH_VARARGS | METH_KEYWORDS, + "subvolume_get_read_only(path) -> bool\n\n" + "Get whether a subvolume is read-only.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor"}, + + /* Aliases set_subvolume_read_only, subvolume_set_read_only */ {"set_subvolume_read_only", (PyCFunction)set_subvolume_read_only, METH_VARARGS | METH_KEYWORDS, "set_subvolume_read_only(path, read_only=True)\n\n" @@ -182,12 +254,29 @@ static PyMethodDef btrfsutil_methods[] = { "Arguments:\n" "path -- string, bytes, path-like object, or open file descriptor\n" "read_only -- bool flag value"}, + {"subvolume_set_read_only", (PyCFunction)set_subvolume_read_only, + METH_VARARGS | METH_KEYWORDS, + "subvolume_set_read_only(path, read_only=True)\n\n" + "Set whether a subvolume is read-only.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor\n" + "read_only -- bool flag value"}, + + /* Aliases get_default_subvolume, subvolume_get_default */ {"get_default_subvolume", (PyCFunction)get_default_subvolume, METH_VARARGS | METH_KEYWORDS, "get_default_subvolume(path) -> int\n\n" "Get the ID of the default subvolume of a filesystem.\n\n" "Arguments:\n" "path -- string, bytes, path-like object, or open file descriptor"}, + {"subvolume_get_default", (PyCFunction)get_default_subvolume, + METH_VARARGS | METH_KEYWORDS, + "subvolume_get_default(path) -> int\n\n" + "Get the ID of the default subvolume of a filesystem.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor"}, + + /* Aliases: set_default_subvolume, subvolume_set_default */ {"set_default_subvolume", (PyCFunction)set_default_subvolume, METH_VARARGS | METH_KEYWORDS, "set_default_subvolume(path, id=0)\n\n" @@ -196,6 +285,16 @@ static PyMethodDef btrfsutil_methods[] = { "path -- string, bytes, path-like object, or open file descriptor\n" "id -- if not zero, set the default subvolume to the subvolume with\n" "this ID instead of the given path"}, + {"subvolume_set_default", (PyCFunction)set_default_subvolume, + METH_VARARGS | METH_KEYWORDS, + "subvolume_set_default(path, id=0)\n\n" + "Set the default subvolume of a filesystem.\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor\n" + "id -- if not zero, set the default subvolume to the subvolume with\n" + "this ID instead of the given path"}, + + /* Aliases: create_subvolume, subvolume_create */ {"create_subvolume", (PyCFunction)create_subvolume, METH_VARARGS | METH_KEYWORDS, "create_subvolume(path, async_=False, qgroup_inherit=None)\n\n" @@ -205,6 +304,17 @@ static PyMethodDef btrfsutil_methods[] = { "async_ -- no longer used\n" "qgroup_inherit -- optional QgroupInherit object of qgroups to\n" "inherit from"}, + {"subvolume_create", (PyCFunction)create_subvolume, + METH_VARARGS | METH_KEYWORDS, + "subvolume_create(path, async_=False, qgroup_inherit=None)\n\n" + "Create a new subvolume.\n\n" + "Arguments:\n" + "path -- string, bytes, or path-like object\n" + "async_ -- no longer used\n" + "qgroup_inherit -- optional QgroupInherit object of qgroups to\n" + "inherit from"}, + + /* Aliases: create_snapshot, subvolume_snapshot */ {"create_snapshot", (PyCFunction)create_snapshot, METH_VARARGS | METH_KEYWORDS, "create_snapshot(source, path, recursive=False, read_only=False,\n" @@ -218,6 +328,21 @@ static PyMethodDef btrfsutil_methods[] = { "async_ -- no longer used\n" "qgroup_inherit -- optional QgroupInherit object of qgroups to\n" "inherit from"}, + {"subvolume_snapshot", (PyCFunction)create_snapshot, + METH_VARARGS | METH_KEYWORDS, + "subvolume_snapshot(source, path, recursive=False, read_only=False,\n" + " async_=False, qgroup_inherit=None)\n\n" + "Create a new snapshot.\n\n" + "Arguments:\n" + "source -- string, bytes, path-like object, or open file descriptor\n" + "path -- string, bytes, or path-like object\n" + "recursive -- also snapshot child subvolumes\n" + "read_only -- create a read-only snapshot\n" + "async_ -- no longer used\n" + "qgroup_inherit -- optional QgroupInherit object of qgroups to\n" + "inherit from"}, + + /* Aliases: delete_subvolume, subvolume_delete */ {"delete_subvolume", (PyCFunction)delete_subvolume, METH_VARARGS | METH_KEYWORDS, "delete_subvolume(path, recursive=False)\n\n" @@ -226,6 +351,16 @@ static PyMethodDef btrfsutil_methods[] = { "path -- string, bytes, or path-like object\n" "recursive -- if the given subvolume has child subvolumes, delete\n" "them instead of failing"}, + {"subvolume_delete", (PyCFunction)delete_subvolume, + METH_VARARGS | METH_KEYWORDS, + "subvolume_delete(path, recursive=False)\n\n" + "Delete a subvolume or snapshot.\n\n" + "Arguments:\n" + "path -- string, bytes, or path-like object\n" + "recursive -- if the given subvolume has child subvolumes, delete\n" + "them instead of failing"}, + + /* Aliases: deleted_subvolumes, subvolume_list_deleted */ {"deleted_subvolumes", (PyCFunction)deleted_subvolumes, METH_VARARGS | METH_KEYWORDS, "deleted_subvolumes(path)\n\n" @@ -233,6 +368,14 @@ static PyMethodDef btrfsutil_methods[] = { "cleaned up\n\n" "Arguments:\n" "path -- string, bytes, path-like object, or open file descriptor"}, + {"subvolume_list_deleted", (PyCFunction)deleted_subvolumes, + METH_VARARGS | METH_KEYWORDS, + "subvolumes_list_deleted(path)\n\n" + "Get the list of subvolume IDs which have been deleted but not yet\n" + "cleaned up\n\n" + "Arguments:\n" + "path -- string, bytes, path-like object, or open file descriptor"}, + {}, }; diff --git a/libbtrfsutil/python/tests/test_filesystem.py b/libbtrfsutil/python/tests/test_filesystem.py index 04b30c764..405f894c1 100644 --- a/libbtrfsutil/python/tests/test_filesystem.py +++ b/libbtrfsutil/python/tests/test_filesystem.py @@ -46,6 +46,17 @@ def test_sync(self): self.assertGreater(new_generation, old_generation) old_generation = new_generation + # Copy of test_sync + def test_fs_sync(self): + old_generation = self.super_generation() + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + touch(arg) + btrfsutil.fs_sync(arg) + new_generation = self.super_generation() + self.assertGreater(new_generation, old_generation) + old_generation = new_generation + def test_start_sync(self): old_generation = self.super_generation() for arg in self.path_or_fd(self.mountpoint): @@ -54,6 +65,15 @@ def test_start_sync(self): transid = btrfsutil.start_sync(arg) self.assertGreater(transid, old_generation) + # Copy of test_start_sync + def test_fs_start_sync(self): + old_generation = self.super_generation() + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + touch(arg) + transid = btrfsutil.fs_start_sync(arg) + self.assertGreater(transid, old_generation) + def test_wait_sync(self): old_generation = self.super_generation() for arg in self.path_or_fd(self.mountpoint): @@ -71,3 +91,21 @@ def test_wait_sync(self): new_generation = self.super_generation() self.assertGreater(new_generation, old_generation) old_generation = new_generation + + def test_fs_wait_sync(self): + old_generation = self.super_generation() + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + touch(arg) + transid = btrfsutil.fs_start_sync(arg) + btrfsutil.fs_wait_sync(arg, transid) + new_generation = self.super_generation() + self.assertGreater(new_generation, old_generation) + old_generation = new_generation + + touch(arg) + btrfsutil.fs_start_sync(arg) + btrfsutil.fs_wait_sync(arg) + new_generation = self.super_generation() + self.assertGreater(new_generation, old_generation) + old_generation = new_generation diff --git a/libbtrfsutil/python/tests/test_qgroup.py b/libbtrfsutil/python/tests/test_qgroup.py index 74fc46b65..ee46a910b 100644 --- a/libbtrfsutil/python/tests/test_qgroup.py +++ b/libbtrfsutil/python/tests/test_qgroup.py @@ -41,6 +41,17 @@ def test_snapshot_inherit(self): btrfsutil.create_subvolume(subvol) btrfsutil.create_snapshot(subvol, snapshot, qgroup_inherit=inherit) + # Copy of test_snapshot_inherit + def test_snapshot_inherit(self): + subvol = os.path.join(self.mountpoint, 'subvol') + snapshot = os.path.join(self.mountpoint, 'snapshot') + + inherit = btrfsutil.QgroupInherit() + inherit.add_group(5) + + btrfsutil.subvolume_create(subvol) + btrfsutil.subvolume_snapshot(subvol, snapshot, qgroup_inherit=inherit) + class TestQgroupInherit(unittest.TestCase): def test_new(self): diff --git a/libbtrfsutil/python/tests/test_subvolume.py b/libbtrfsutil/python/tests/test_subvolume.py index d67af8324..8119d2137 100644 --- a/libbtrfsutil/python/tests/test_subvolume.py +++ b/libbtrfsutil/python/tests/test_subvolume.py @@ -53,6 +53,25 @@ def test_is_subvolume(self): self.assertEqual(e.exception.btrfsutilerror, btrfsutil.ERROR_STATFS_FAILED) self.assertEqual(e.exception.errno, errno.ENOENT) + # Copy of test_is_subvolume + def test_subvolume_is_valid(self): + dir = os.path.join(self.mountpoint, 'foo') + os.mkdir(dir) + + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + self.assertTrue(btrfsutil.subvolume_is_valid(arg)) + for arg in self.path_or_fd(dir): + with self.subTest(type=type(arg)): + self.assertFalse(btrfsutil.subvolume_is_valid(arg)) + + with self.assertRaises(btrfsutil.BtrfsUtilError) as e: + btrfsutil.subvolume_is_valid(os.path.join(self.mountpoint, 'bar')) + # This is a bit of an implementation detail, but really this is testing + # that the exception is initialized correctly. + self.assertEqual(e.exception.btrfsutilerror, btrfsutil.ERROR_STATFS_FAILED) + self.assertEqual(e.exception.errno, errno.ENOENT) + def test_subvolume_id(self): dir = os.path.join(self.mountpoint, 'foo') os.mkdir(dir) @@ -64,10 +83,22 @@ def test_subvolume_id(self): with self.subTest(type=type(arg)): self.assertEqual(btrfsutil.subvolume_id(arg), 5) + # Copy of test_subvolume_id + def test_subvolume_get_id(self): + dir = os.path.join(self.mountpoint, 'foo') + os.mkdir(dir) + + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + self.assertEqual(btrfsutil.subvolume_get_id(arg), 5) + for arg in self.path_or_fd(dir): + with self.subTest(type=type(arg)): + self.assertEqual(btrfsutil.subvolume_get_id(arg), 5) + def test_subvolume_id_error(self): fd = os.open('/dev/null', os.O_RDONLY) try: - btrfsutil.subvolume_id(fd) + btrfsutil.subvolume_get_id(fd) except Exception: pass finally: @@ -107,6 +138,38 @@ def test_subvolume_path(self): finally: os.chdir(pwd) + # Copy of test_subvolume_path + def test_subvolume_get_path(self): + btrfsutil.subvolume_create(os.path.join(self.mountpoint, 'subvol1')) + os.mkdir(os.path.join(self.mountpoint, 'dir1')) + os.mkdir(os.path.join(self.mountpoint, 'dir1/dir2')) + btrfsutil.subvolume_create(os.path.join(self.mountpoint, 'dir1/dir2/subvol2')) + btrfsutil.subvolume_create(os.path.join(self.mountpoint, 'dir1/dir2/subvol2/subvol3')) + os.mkdir(os.path.join(self.mountpoint, 'subvol1/dir3')) + btrfsutil.subvolume_create(os.path.join(self.mountpoint, 'subvol1/dir3/subvol4')) + + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + self.assertEqual(btrfsutil.subvolume_get_path(arg), '') + self.assertEqual(btrfsutil.subvolume_get_path(arg, 5), '') + self.assertEqual(btrfsutil.subvolume_get_path(arg, 256), 'subvol1') + self.assertEqual(btrfsutil.subvolume_get_path(arg, 257), 'dir1/dir2/subvol2') + self.assertEqual(btrfsutil.subvolume_get_path(arg, 258), 'dir1/dir2/subvol2/subvol3') + self.assertEqual(btrfsutil.subvolume_get_path(arg, 259), 'subvol1/dir3/subvol4') + + pwd = os.getcwd() + try: + os.chdir(self.mountpoint) + path = '' + for i in range(26): + name = chr(ord('a') + i) * 255 + path = os.path.join(path, name) + btrfsutil.subvolume_create(name) + os.chdir(name) + self.assertEqual(btrfsutil.subvolume_get_path('.'), path) + finally: + os.chdir(pwd) + def _test_subvolume_info(self, subvol, snapshot): for arg in self.path_or_fd(self.mountpoint): with self.subTest(type=type(arg)): @@ -155,6 +218,55 @@ def _test_subvolume_info(self, subvol, snapshot): # TODO: test received_uuid, stransid, rtransid, stime, and rtime + # Copy of _test_subvolume_info + def _test_subvolume_get_info(self, subvol, snapshot): + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + info = btrfsutil.subvolume_get_info(arg) + self.assertEqual(info.id, 5) + self.assertEqual(info.parent_id, 0) + self.assertEqual(info.dir_id, 0) + self.assertEqual(info.flags, 0) + self.assertIsInstance(info.uuid, bytes) + self.assertEqual(len(info.uuid), 16) + self.assertEqual(info.parent_uuid, bytes(16)) + self.assertEqual(info.received_uuid, bytes(16)) + self.assertNotEqual(info.generation, 0) + self.assertGreaterEqual(info.ctransid, 0) + self.assertEqual(info.otransid, 0) + self.assertEqual(info.stransid, 0) + self.assertEqual(info.rtransid, 0) + self.assertIsInstance(info.ctime, float) + self.assertIsInstance(info.otime, float) + self.assertEqual(info.stime, 0) + self.assertEqual(info.rtime, 0) + + info = btrfsutil.subvolume_get_info(subvol) + self.assertEqual(info.id, 256) + self.assertEqual(info.parent_id, 5) + self.assertEqual(info.dir_id, 256) + self.assertEqual(info.flags, 0) + self.assertIsInstance(info.uuid, bytes) + self.assertEqual(len(info.uuid), 16) + self.assertEqual(info.parent_uuid, bytes(16)) + self.assertEqual(info.received_uuid, bytes(16)) + self.assertNotEqual(info.generation, 0) + self.assertNotEqual(info.ctransid, 0) + self.assertNotEqual(info.otransid, 0) + self.assertEqual(info.stransid, 0) + self.assertEqual(info.rtransid, 0) + self.assertNotEqual(info.ctime, 0) + self.assertNotEqual(info.otime, 0) + self.assertEqual(info.stime, 0) + self.assertEqual(info.rtime, 0) + + subvol_uuid = info.uuid + + info = btrfsutil.subvolume_get_info(snapshot) + self.assertEqual(info.parent_uuid, subvol_uuid) + + # TODO: test received_uuid, stransid, rtransid, stime, and rtime + def test_subvolume_info(self): subvol = os.path.join(self.mountpoint, 'subvol') btrfsutil.create_subvolume(subvol) @@ -171,6 +283,23 @@ def test_subvolume_info(self): self.assertEqual(e.exception.btrfsutilerror, btrfsutil.ERROR_SUBVOLUME_NOT_FOUND) + # Copy of test_subvolume_info + def test_subvolume_get_info(self): + subvol = os.path.join(self.mountpoint, 'subvol') + btrfsutil.subvolume_create(subvol) + snapshot = os.path.join(self.mountpoint, 'snapshot') + btrfsutil.subvolume_snapshot(subvol, snapshot) + + self._test_subvolume_info(subvol, snapshot) + + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + with self.assertRaises(btrfsutil.BtrfsUtilError) as e: + # BTRFS_EXTENT_TREE_OBJECTID + btrfsutil.subvolume_get_info(arg, 2) + self.assertEqual(e.exception.btrfsutilerror, + btrfsutil.ERROR_SUBVOLUME_NOT_FOUND) + @skipUnlessHaveNobody def test_subvolume_info_unprivileged(self): subvol = os.path.join(self.mountpoint, 'subvol') @@ -188,6 +317,24 @@ def test_subvolume_info_unprivileged(self): raise self._test_subvolume_info(subvol, snapshot) + # Copy of test_subvolume_info_unprivileged + @skipUnlessHaveNobody + def test_subvolume_get_info_unprivileged(self): + subvol = os.path.join(self.mountpoint, 'subvol') + btrfsutil.subvolume_create(subvol) + snapshot = os.path.join(self.mountpoint, 'snapshot') + btrfsutil.subvolume_snapshot(subvol, snapshot) + + with drop_privs(): + try: + btrfsutil.subvolume_get_info(self.mountpoint) + except OSError as e: + if e.errno == errno.ENOTTY: + self.skipTest('BTRFS_IOC_GET_SUBVOL_INFO is not available') + else: + raise + self._test_subvolume_info(subvol, snapshot) + def test_read_only(self): for arg in self.path_or_fd(self.mountpoint): with self.subTest(type=type(arg)): @@ -205,6 +352,24 @@ def test_read_only(self): btrfsutil.set_subvolume_read_only(arg, False) + # Copy of test_read_only + def test_subvolume_read_only(self): + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + btrfsutil.subvolume_set_read_only(arg) + self.assertTrue(btrfsutil.subvolume_get_read_only(arg)) + self.assertTrue(btrfsutil.subvolume_get_info(arg).flags & 1) + + btrfsutil.subvolume_set_read_only(arg, False) + self.assertFalse(btrfsutil.subvolume_get_read_only(arg)) + self.assertFalse(btrfsutil.subvolume_get_info(arg).flags & 1) + + btrfsutil.subvolume_set_read_only(arg, True) + self.assertTrue(btrfsutil.subvolume_get_read_only(arg)) + self.assertTrue(btrfsutil.subvolume_get_info(arg).flags & 1) + + btrfsutil.subvolume_set_read_only(arg, False) + def test_default_subvolume(self): for arg in self.path_or_fd(self.mountpoint): with self.subTest(type=type(arg)): @@ -219,6 +384,21 @@ def test_default_subvolume(self): btrfsutil.set_default_subvolume(arg, 5) self.assertEqual(btrfsutil.get_default_subvolume(arg), 5) + # Copy of test_default_subvolume + def test_subvolume_default(self): + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + self.assertEqual(btrfsutil.subvolume_get_default(arg), 5) + + subvol = os.path.join(self.mountpoint, 'subvol') + btrfsutil.subvolume_create(subvol) + for arg in self.path_or_fd(subvol): + with self.subTest(type=type(arg)): + btrfsutil.subvolume_set_default(arg) + self.assertEqual(btrfsutil.subvolume_get_default(arg), 256) + btrfsutil.subvolume_set_default(arg, 5) + self.assertEqual(btrfsutil.subvolume_get_default(arg), 5) + def test_create_subvolume(self): subvol = os.path.join(self.mountpoint, 'subvol') @@ -262,6 +442,50 @@ def test_create_subvolume(self): self.assertTrue(os.WIFEXITED(wstatus)) self.assertEqual(os.WEXITSTATUS(wstatus), 0) + # Copy of test_create_subvolume + def test_subvolume_create(self): + subvol = os.path.join(self.mountpoint, 'subvol') + + btrfsutil.subvolume_create(subvol + '1') + self.assertTrue(btrfsutil.subvolume_is_valid(subvol + '1')) + btrfsutil.subvolume_create((subvol + '2').encode()) + self.assertTrue(btrfsutil.subvolume_is_valid(subvol + '2')) + if HAVE_PATH_LIKE: + btrfsutil.subvolume_create(PurePath(subvol + '3')) + self.assertTrue(btrfsutil.subvolume_is_valid(subvol + '3')) + + pwd = os.getcwd() + try: + os.chdir(self.mountpoint) + btrfsutil.subvolume_create('subvol4') + self.assertTrue(btrfsutil.subvolume_is_valid('subvol4')) + finally: + os.chdir(pwd) + + btrfsutil.subvolume_create(subvol + '5/') + self.assertTrue(btrfsutil.subvolume_is_valid(subvol + '5')) + + btrfsutil.subvolume_create(subvol + '6//') + self.assertTrue(btrfsutil.subvolume_is_valid(subvol + '6')) + + # Test creating subvolumes under '/' in a chroot. + pid = os.fork() + if pid == 0: + try: + os.chroot(self.mountpoint) + os.chdir('/') + btrfsutil.subvolume_create('/subvol8') + self.assertTrue(btrfsutil.subvolume_is_valid('/subvol8')) + with self.assertRaises(btrfsutil.BtrfsUtilError): + btrfsutil.subvolume_create('/') + os._exit(0) + except Exception: + traceback.print_exc() + os._exit(1) + wstatus = os.waitpid(pid, 0)[1] + self.assertTrue(os.WIFEXITED(wstatus)) + self.assertEqual(os.WEXITSTATUS(wstatus), 0) + def test_create_snapshot(self): subvol = os.path.join(self.mountpoint, 'subvol') @@ -306,6 +530,51 @@ def test_create_snapshot(self): btrfsutil.create_snapshot(subvol, snapshot + '3', read_only=True) self.assertTrue(btrfsutil.get_subvolume_read_only(snapshot + '3')) + # Copy of test_create_snapshot + def test_subvolume_snapshot(self): + subvol = os.path.join(self.mountpoint, 'subvol') + + btrfsutil.subvolume_create(subvol) + os.mkdir(os.path.join(subvol, 'dir')) + + for i, arg in enumerate(self.path_or_fd(subvol)): + with self.subTest(type=type(arg)): + snapshots_dir = os.path.join(self.mountpoint, 'snapshots{}'.format(i)) + os.mkdir(snapshots_dir) + snapshot = os.path.join(snapshots_dir, 'snapshot') + + btrfsutil.subvolume_snapshot(subvol, snapshot + '1') + self.assertTrue(btrfsutil.subvolume_is_valid(snapshot + '1')) + self.assertTrue(os.path.exists(os.path.join(snapshot + '1', 'dir'))) + + btrfsutil.subvolume_snapshot(subvol, (snapshot + '2').encode()) + self.assertTrue(btrfsutil.subvolume_is_valid(snapshot + '2')) + self.assertTrue(os.path.exists(os.path.join(snapshot + '2', 'dir'))) + + if HAVE_PATH_LIKE: + btrfsutil.subvolume_snapshot(subvol, PurePath(snapshot + '3')) + self.assertTrue(btrfsutil.subvolume_is_valid(snapshot + '3')) + self.assertTrue(os.path.exists(os.path.join(snapshot + '3', 'dir'))) + + nested_subvol = os.path.join(subvol, 'nested') + more_nested_subvol = os.path.join(nested_subvol, 'more_nested') + btrfsutil.subvolume_create(nested_subvol) + btrfsutil.subvolume_create(more_nested_subvol) + os.mkdir(os.path.join(more_nested_subvol, 'nested_dir')) + + snapshot = os.path.join(self.mountpoint, 'snapshot') + + btrfsutil.subvolume_snapshot(subvol, snapshot + '1') + # Dummy subvolume. + self.assertEqual(os.stat(os.path.join(snapshot + '1', 'nested')).st_ino, 2) + self.assertFalse(os.path.exists(os.path.join(snapshot + '1', 'nested', 'more_nested'))) + + btrfsutil.subvolume_snapshot(subvol, snapshot + '2', recursive=True) + self.assertTrue(os.path.exists(os.path.join(snapshot + '2', 'nested/more_nested/nested_dir'))) + + btrfsutil.subvolume_snapshot(subvol, snapshot + '3', read_only=True) + self.assertTrue(btrfsutil.subvolume_get_read_only(snapshot + '3')) + def test_delete_subvolume(self): subvol = os.path.join(self.mountpoint, 'subvol') btrfsutil.create_subvolume(subvol + '1') @@ -354,6 +623,55 @@ def test_delete_subvolume(self): btrfsutil.delete_subvolume(subvol + '5', recursive=True) self.assertFalse(os.path.exists(subvol + '5')) + # Copy of test_delete_subvolume + def test_subvolume_delete(self): + subvol = os.path.join(self.mountpoint, 'subvol') + btrfsutil.subvolume_create(subvol + '1') + self.assertTrue(os.path.exists(subvol + '1')) + btrfsutil.subvolume_create(subvol + '2') + self.assertTrue(os.path.exists(subvol + '2')) + btrfsutil.subvolume_create(subvol + '3') + self.assertTrue(os.path.exists(subvol + '3')) + + btrfsutil.subvolume_delete(subvol + '1') + self.assertFalse(os.path.exists(subvol + '1')) + btrfsutil.subvolume_delete((subvol + '2').encode()) + self.assertFalse(os.path.exists(subvol + '2')) + if HAVE_PATH_LIKE: + btrfsutil.subvolume_delete(PurePath(subvol + '3')) + self.assertFalse(os.path.exists(subvol + '3')) + + # Test deleting subvolumes under '/' in a chroot. + pid = os.fork() + if pid == 0: + try: + os.chroot(self.mountpoint) + os.chdir('/') + btrfsutil.subvolume_create('/subvol4') + self.assertTrue(os.path.exists('/subvol4')) + btrfsutil.subvolume_delete('/subvol4') + self.assertFalse(os.path.exists('/subvol4')) + with self.assertRaises(btrfsutil.BtrfsUtilError): + btrfsutil.subvolume_delete('/') + os._exit(0) + except Exception: + traceback.print_exc() + os._exit(1) + wstatus = os.waitpid(pid, 0)[1] + self.assertTrue(os.WIFEXITED(wstatus)) + self.assertEqual(os.WEXITSTATUS(wstatus), 0) + + btrfsutil.subvolume_create(subvol + '5') + btrfsutil.subvolume_create(subvol + '5/foo') + btrfsutil.subvolume_create(subvol + '5/bar') + btrfsutil.subvolume_create(subvol + '5/bar/baz') + btrfsutil.subvolume_create(subvol + '5/bar/qux') + btrfsutil.subvolume_create(subvol + '5/quux') + with self.assertRaises(btrfsutil.BtrfsUtilError): + btrfsutil.subvolume_delete(subvol + '5') + btrfsutil.subvolume_delete(subvol + '5', recursive=True) + self.assertFalse(os.path.exists(subvol + '5')) + def test_deleted_subvolumes(self): subvol = os.path.join(self.mountpoint, 'subvol') btrfsutil.create_subvolume(subvol + '1') @@ -362,8 +680,18 @@ def test_deleted_subvolumes(self): with self.subTest(type=type(arg)): self.assertEqual(btrfsutil.deleted_subvolumes(arg), [256]) + # Copy of test_deleted_subvolumes + def test_subvolume_list_deleted(self): + subvol = os.path.join(self.mountpoint, 'subvol') + btrfsutil.subvolume_create(subvol + '1') + btrfsutil.subvolume_delete(subvol + '1') + for arg in self.path_or_fd(self.mountpoint): + with self.subTest(type=type(arg)): + self.assertEqual(btrfsutil.subvolume_list_deleted(arg), [256]) + + # Only the new API def _test_subvolume_iterator(self): - btrfsutil.create_subvolume('foo') + btrfsutil.subvolume_create('foo') with btrfsutil.SubvolumeIterator('.', info=True) as it: path, subvol = next(it) @@ -373,8 +701,8 @@ def _test_subvolume_iterator(self): self.assertEqual(subvol.parent_id, 5) self.assertRaises(StopIteration, next, it) - btrfsutil.create_subvolume('foo/bar') - btrfsutil.create_subvolume('foo/bar/baz') + btrfsutil.subvolume_create('foo/bar') + btrfsutil.subvolume_create('foo/bar/baz') subvols = [ ('foo', 256), @@ -412,9 +740,9 @@ def _test_subvolume_iterator(self): os.rename('foo/bar/baz', 'baz') os.mkdir('dir') - btrfsutil.create_subvolume('dir/qux') + btrfsutil.subvolume_create('dir/qux') os.mkdir('dir/qux/dir2') - btrfsutil.create_subvolume('dir/qux/dir2/quux') + btrfsutil.subvolume_create('dir/qux/dir2/quux') subvols = [ ('baz', 258), @@ -430,28 +758,28 @@ def _test_subvolume_iterator(self): with regain_privs(): # We don't have permission to traverse the path. os.mkdir('directory_perms', 0o700) - btrfsutil.create_subvolume('directory_perms/subvol') + btrfsutil.subvolume_create('directory_perms/subvol') # We don't have permission to resolve the subvolume path. os.mkdir('subvol_perms', 0o755) - btrfsutil.create_subvolume('subvol_perms/subvol') + btrfsutil.subvolume_create('subvol_perms/subvol') os.chmod('subvol_perms/subvol', 0o700) # The path doesn't exist. os.mkdir('enoent', 0o755) - btrfsutil.create_subvolume('enoent/subvol') + btrfsutil.subvolume_create('enoent/subvol') subprocess.check_call(['mount', '-t', 'tmpfs', 'tmpfs', 'enoent']) # The path exists but it's not a subvolume. os.mkdir('notsubvol', 0o755) - btrfsutil.create_subvolume('notsubvol/subvol') + btrfsutil.subvolume_create('notsubvol/subvol') subprocess.check_call(['mount', '-t', 'tmpfs', 'tmpfs', 'notsubvol']) os.mkdir('notsubvol/subvol') # The path exists and is a subvolume, but on a different # filesystem. os.mkdir('wrongfs', 0o755) - btrfsutil.create_subvolume('wrongfs/subvol') + btrfsutil.subvolume_create('wrongfs/subvol') other_mountpoint, _ = self.mount_btrfs() subprocess.check_call(['mount', '--bind', '--', other_mountpoint, 'wrongfs/subvol']) @@ -459,7 +787,7 @@ def _test_subvolume_iterator(self): # The path exists and is a subvolume on the same # filesystem, but not the right one. os.mkdir('wrongsubvol', 0o755) - btrfsutil.create_subvolume('wrongsubvol/subvol') + btrfsutil.subvolume_create('wrongsubvol/subvol') subprocess.check_call(['mount', '--bind', 'baz', 'wrongsubvol/subvol']) @@ -508,16 +836,18 @@ def test_subvolume_iterator_unprivileged(self): finally: os.chdir(pwd) + # Only the new API @staticmethod def _create_and_delete_subvolume(i): dir_name = f'dir{i}' subvol_name = dir_name + '/subvol' while True: os.mkdir(dir_name) - btrfsutil.create_subvolume(subvol_name) - btrfsutil.delete_subvolume(subvol_name) + btrfsutil.subvolume_create(subvol_name) + btrfsutil.subvolume_delete(subvol_name) os.rmdir(dir_name) + # Only the new API def _test_subvolume_iterator_race(self): procs = [] fd = os.open('.', os.O_RDONLY | os.O_DIRECTORY) @@ -542,6 +872,7 @@ def _test_subvolume_iterator_race(self): proc.join() os.close(fd) + # Only the new API def test_subvolume_iterator_race(self): pwd = os.getcwd() try: @@ -550,6 +881,7 @@ def test_subvolume_iterator_race(self): finally: os.chdir(pwd) + # Only the new API def test_subvolume_iterator_race_unprivileged(self): os.chown(self.mountpoint, NOBODY_UID, -1) pwd = os.getcwd() @@ -561,11 +893,12 @@ def test_subvolume_iterator_race_unprivileged(self): finally: os.chdir(pwd) + # Only the new API def test_subvolume_iterator_fd_unprivileged(self): pwd = os.getcwd() try: os.chdir(self.mountpoint) - btrfsutil.create_subvolume('subvol') + btrfsutil.subvolume_create('subvol') with drop_privs(): fd = os.open('.', os.O_RDONLY | os.O_DIRECTORY) try: