summaryrefslogtreecommitdiff
path: root/libbtrfsutil/python
diff options
context:
space:
mode:
authorDimitri John Ledkov <xnox@ubuntu.com>2018-05-08 14:17:29 -0700
committerDimitri John Ledkov <xnox@ubuntu.com>2018-05-08 14:17:29 -0700
commitd00c9550da1801a0eaff5cedf4312e24691b31ea (patch)
tree3881ca1764ef792259e1b70f12c884a3ac0c0715 /libbtrfsutil/python
parentdab6d2181f1f194ec3a76d900cf2c6533379cbea (diff)
New upstream release.
Diffstat (limited to 'libbtrfsutil/python')
-rw-r--r--libbtrfsutil/python/.gitignore7
-rw-r--r--libbtrfsutil/python/btrfsutilpy.h84
-rw-r--r--libbtrfsutil/python/error.c202
-rw-r--r--libbtrfsutil/python/filesystem.c94
-rw-r--r--libbtrfsutil/python/module.c321
-rw-r--r--libbtrfsutil/python/qgroup.c141
-rwxr-xr-xlibbtrfsutil/python/setup.py111
-rw-r--r--libbtrfsutil/python/subvolume.c667
-rw-r--r--libbtrfsutil/python/tests/__init__.py70
-rw-r--r--libbtrfsutil/python/tests/test_filesystem.py73
-rw-r--r--libbtrfsutil/python/tests/test_qgroup.py57
-rw-r--r--libbtrfsutil/python/tests/test_subvolume.py385
12 files changed, 2212 insertions, 0 deletions
diff --git a/libbtrfsutil/python/.gitignore b/libbtrfsutil/python/.gitignore
new file mode 100644
index 00000000..d050ff7c
--- /dev/null
+++ b/libbtrfsutil/python/.gitignore
@@ -0,0 +1,7 @@
+__pycache__
+*.pyc
+/btrfsutil.egg-info
+/btrfsutil*.so
+/build
+/constants.c
+/dist
diff --git a/libbtrfsutil/python/btrfsutilpy.h b/libbtrfsutil/python/btrfsutilpy.h
new file mode 100644
index 00000000..af3a6edf
--- /dev/null
+++ b/libbtrfsutil/python/btrfsutilpy.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2018 Facebook
+ *
+ * This file is part of libbtrfsutil.
+ *
+ * libbtrfsutil is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * libbtrfsutil is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef BTRFSUTILPY_H
+#define BTRFSUTILPY_H
+
+#define PY_SSIZE_T_CLEAN
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <Python.h>
+#include "structmember.h"
+
+#include <btrfsutil.h>
+
+typedef struct {
+ PyObject_HEAD
+ struct btrfs_util_qgroup_inherit *inherit;
+} QgroupInherit;
+
+extern PyTypeObject BtrfsUtilError_type;
+extern PyStructSequence_Desc SubvolumeInfo_desc;
+extern PyTypeObject SubvolumeInfo_type;
+extern PyTypeObject SubvolumeIterator_type;
+extern PyTypeObject QgroupInherit_type;
+
+/*
+ * Helpers for path arguments based on posixmodule.c in CPython.
+ */
+struct path_arg {
+ bool allow_fd;
+ char *path;
+ int fd;
+ Py_ssize_t length;
+ PyObject *object;
+ PyObject *cleanup;
+};
+int path_converter(PyObject *o, void *p);
+void path_cleanup(struct path_arg *path);
+
+PyObject *list_from_uint64_array(const uint64_t *arr, size_t n);
+
+void SetFromBtrfsUtilError(enum btrfs_util_error err);
+void SetFromBtrfsUtilErrorWithPath(enum btrfs_util_error err,
+ struct path_arg *path);
+void SetFromBtrfsUtilErrorWithPaths(enum btrfs_util_error err,
+ struct path_arg *path1,
+ struct path_arg *path2);
+
+PyObject *filesystem_sync(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *start_sync(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *wait_sync(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *is_subvolume(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *subvolume_id(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *subvolume_path(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *subvolume_info(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *get_subvolume_read_only(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *set_subvolume_read_only(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *get_default_subvolume(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *set_default_subvolume(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *create_subvolume(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *create_snapshot(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *delete_subvolume(PyObject *self, PyObject *args, PyObject *kwds);
+PyObject *deleted_subvolumes(PyObject *self, PyObject *args, PyObject *kwds);
+
+void add_module_constants(PyObject *m);
+
+#endif /* BTRFSUTILPY_H */
diff --git a/libbtrfsutil/python/error.c b/libbtrfsutil/python/error.c
new file mode 100644
index 00000000..0876c9b4
--- /dev/null
+++ b/libbtrfsutil/python/error.c
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2018 Facebook
+ *
+ * This file is part of libbtrfsutil.
+ *
+ * libbtrfsutil is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * libbtrfsutil is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "btrfsutilpy.h"
+
+typedef struct {
+ PyOSErrorObject os_error;
+ PyObject *btrfsutilerror;
+} BtrfsUtilError;
+
+void SetFromBtrfsUtilError(enum btrfs_util_error err)
+{
+ SetFromBtrfsUtilErrorWithPaths(err, NULL, NULL);
+}
+
+void SetFromBtrfsUtilErrorWithPath(enum btrfs_util_error err,
+ struct path_arg *path1)
+{
+ SetFromBtrfsUtilErrorWithPaths(err, path1, NULL);
+}
+
+void SetFromBtrfsUtilErrorWithPaths(enum btrfs_util_error err,
+ struct path_arg *path1,
+ struct path_arg *path2)
+{
+ PyObject *strobj, *args, *exc;
+ int i = errno;
+ const char *str1 = btrfs_util_strerror(err), *str2 = strerror(i);
+
+ if (str1 && str2 && strcmp(str1, str2) != 0) {
+ strobj = PyUnicode_FromFormat("%s: %s", str1, str2);
+ } else if (str1) {
+ strobj = PyUnicode_FromString(str1);
+ } else if (str2) {
+ strobj = PyUnicode_FromString(str2);
+ } else {
+ Py_INCREF(Py_None);
+ strobj = Py_None;
+ }
+ if (strobj == NULL)
+ return;
+
+ args = Py_BuildValue("iOOOOi", i, strobj,
+ path1 ? path1->object : Py_None, Py_None,
+ path2 ? path2->object : Py_None, (int)err);
+ Py_DECREF(strobj);
+ if (args == NULL)
+ return;
+
+ exc = PyObject_CallObject((PyObject *)&BtrfsUtilError_type, args);
+ Py_DECREF(args);
+ if (exc == NULL)
+ return;
+
+ PyErr_SetObject((PyObject *)&BtrfsUtilError_type, exc);
+ Py_DECREF(exc);
+}
+
+static int BtrfsUtilError_clear(BtrfsUtilError *self)
+{
+ Py_CLEAR(self->btrfsutilerror);
+ return Py_TYPE(self)->tp_base->tp_clear((PyObject *)self);
+}
+
+static void BtrfsUtilError_dealloc(BtrfsUtilError *self)
+{
+ PyObject_GC_UnTrack(self);
+ BtrfsUtilError_clear(self);
+ Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+static int BtrfsUtilError_traverse(BtrfsUtilError *self, visitproc visit,
+ void *arg)
+{
+ Py_VISIT(self->btrfsutilerror);
+ return Py_TYPE(self)->tp_base->tp_traverse((PyObject *)self, visit, arg);
+}
+
+static PyObject *BtrfsUtilError_new(PyTypeObject *type, PyObject *args,
+ PyObject *kwds)
+{
+ BtrfsUtilError *self;
+ PyObject *oserror_args = args;
+
+ if (PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 6) {
+ oserror_args = PyTuple_GetSlice(args, 0, 5);
+ if (oserror_args == NULL)
+ return NULL;
+ }
+
+ self = (BtrfsUtilError *)type->tp_base->tp_new(type, oserror_args,
+ kwds);
+ if (oserror_args != args)
+ Py_DECREF(oserror_args);
+ if (self == NULL)
+ return NULL;
+
+ if (PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 6) {
+ self->btrfsutilerror = PyTuple_GET_ITEM(args, 5);
+ Py_INCREF(self->btrfsutilerror);
+ }
+
+ return (PyObject *)self;
+}
+
+static PyObject *BtrfsUtilError_str(BtrfsUtilError *self)
+{
+#define OR_NONE(x) ((x) ? (x) : Py_None)
+ if (self->btrfsutilerror) {
+ if (self->os_error.filename) {
+ if (self->os_error.filename2) {
+ return PyUnicode_FromFormat("[BtrfsUtilError %S Errno %S] %S: %R -> %R",
+ OR_NONE(self->btrfsutilerror),
+ OR_NONE(self->os_error.myerrno),
+ OR_NONE(self->os_error.strerror),
+ self->os_error.filename,
+ self->os_error.filename2);
+ } else {
+ return PyUnicode_FromFormat("[BtrfsUtilError %S Errno %S] %S: %R",
+ OR_NONE(self->btrfsutilerror),
+ OR_NONE(self->os_error.myerrno),
+ OR_NONE(self->os_error.strerror),
+ self->os_error.filename);
+ }
+ }
+ if (self->os_error.myerrno && self->os_error.strerror) {
+ return PyUnicode_FromFormat("[BtrfsUtilError %S Errno %S] %S",
+ self->btrfsutilerror,
+ self->os_error.myerrno,
+ self->os_error.strerror);
+ }
+ }
+ return Py_TYPE(self)->tp_base->tp_str((PyObject *)self);
+#undef OR_NONE
+}
+
+static PyMemberDef BtrfsUtilError_members[] = {
+ {"btrfsutilerror", T_OBJECT,
+ offsetof(BtrfsUtilError, btrfsutilerror), 0,
+ "btrfsutil error code"},
+ {},
+};
+
+#define BtrfsUtilError_DOC \
+ "Btrfs operation error."
+
+PyTypeObject BtrfsUtilError_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "btrfsutil.BtrfsUtilError", /* tp_name */
+ sizeof(BtrfsUtilError), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ (destructor)BtrfsUtilError_dealloc, /* tp_dealloc */
+ NULL, /* tp_print */
+ NULL, /* tp_getattr */
+ NULL, /* tp_setattr */
+ NULL, /* tp_as_async */
+ NULL, /* tp_repr */
+ NULL, /* tp_as_number */
+ NULL, /* tp_as_sequence */
+ NULL, /* tp_as_mapping */
+ NULL, /* tp_hash */
+ NULL, /* tp_call */
+ (reprfunc)BtrfsUtilError_str, /* tp_str */
+ NULL, /* tp_getattro */
+ NULL, /* tp_setattro */
+ NULL, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* tp_flags */
+ BtrfsUtilError_DOC, /* tp_doc */
+ (traverseproc)BtrfsUtilError_traverse, /* tp_traverse */
+ (inquiry)BtrfsUtilError_clear, /* tp_clear */
+ NULL, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ NULL, /* tp_iter */
+ NULL, /* tp_iternext */
+ NULL, /* tp_methods */
+ BtrfsUtilError_members, /* tp_members */
+ NULL, /* tp_getset */
+ NULL, /* tp_base */
+ NULL, /* tp_dict */
+ NULL, /* tp_descr_get */
+ NULL, /* tp_descr_set */
+ offsetof(BtrfsUtilError, os_error.dict), /* tp_dictoffset */
+ NULL, /* tp_init */
+ NULL, /* tp_alloc */
+ BtrfsUtilError_new, /* tp_new */
+};
diff --git a/libbtrfsutil/python/filesystem.c b/libbtrfsutil/python/filesystem.c
new file mode 100644
index 00000000..627ed193
--- /dev/null
+++ b/libbtrfsutil/python/filesystem.c
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 Facebook
+ *
+ * This file is part of libbtrfsutil.
+ *
+ * libbtrfsutil is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * libbtrfsutil is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "btrfsutilpy.h"
+
+PyObject *filesystem_sync(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", NULL};
+ struct path_arg path = {.allow_fd = true};
+ enum btrfs_util_error err;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:sync", keywords,
+ &path_converter, &path))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_sync(path.path);
+ else
+ err = btrfs_util_sync_fd(path.fd);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ Py_RETURN_NONE;
+}
+
+PyObject *start_sync(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", NULL};
+ struct path_arg path = {.allow_fd = true};
+ uint64_t transid;
+ enum btrfs_util_error err;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:start_sync", keywords,
+ &path_converter, &path))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_start_sync(path.path, &transid);
+ else
+ err = btrfs_util_start_sync_fd(path.fd, &transid);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ return PyLong_FromUnsignedLongLong(transid);
+}
+
+PyObject *wait_sync(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", "transid", NULL};
+ struct path_arg path = {.allow_fd = true};
+ unsigned long long transid = 0;
+ enum btrfs_util_error err;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|K:wait_sync", keywords,
+ &path_converter, &path, &transid))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_wait_sync(path.path, transid);
+ else
+ err = btrfs_util_wait_sync_fd(path.fd, transid);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ Py_RETURN_NONE;
+}
diff --git a/libbtrfsutil/python/module.c b/libbtrfsutil/python/module.c
new file mode 100644
index 00000000..2dbdc7be
--- /dev/null
+++ b/libbtrfsutil/python/module.c
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2018 Facebook
+ *
+ * This file is part of libbtrfsutil.
+ *
+ * libbtrfsutil is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * libbtrfsutil is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "btrfsutilpy.h"
+
+static int fd_converter(PyObject *o, void *p)
+{
+ int *fd = p;
+ long tmp;
+ int overflow;
+
+ tmp = PyLong_AsLongAndOverflow(o, &overflow);
+ if (tmp == -1 && PyErr_Occurred())
+ return 0;
+ if (overflow > 0 || tmp > INT_MAX) {
+ PyErr_SetString(PyExc_OverflowError,
+ "fd is greater than maximum");
+ return 0;
+ }
+ if (overflow < 0 || tmp < 0) {
+ PyErr_SetString(PyExc_ValueError, "fd is negative");
+ return 0;
+ }
+ *fd = tmp;
+ return 1;
+}
+
+int path_converter(PyObject *o, void *p)
+{
+ struct path_arg *path = p;
+ int is_index, is_bytes, is_unicode;
+ PyObject *bytes = NULL;
+ Py_ssize_t length = 0;
+ char *tmp;
+
+ if (o == NULL) {
+ path_cleanup(p);
+ return 1;
+ }
+
+ path->object = path->cleanup = NULL;
+ Py_INCREF(o);
+
+ path->fd = -1;
+
+ is_index = path->allow_fd && PyIndex_Check(o);
+ is_bytes = PyBytes_Check(o);
+ is_unicode = PyUnicode_Check(o);
+
+ if (!is_index && !is_bytes && !is_unicode) {
+ _Py_IDENTIFIER(__fspath__);
+ PyObject *func;
+
+ func = _PyObject_LookupSpecial(o, &PyId___fspath__);
+ if (func == NULL)
+ goto err_format;
+ Py_DECREF(o);
+ o = PyObject_CallFunctionObjArgs(func, NULL);
+ Py_DECREF(func);
+ if (o == NULL)
+ return 0;
+ is_bytes = PyBytes_Check(o);
+ is_unicode = PyUnicode_Check(o);
+ }
+
+ if (is_unicode) {
+ if (!PyUnicode_FSConverter(o, &bytes))
+ goto err;
+ } else if (is_bytes) {
+ bytes = o;
+ Py_INCREF(bytes);
+ } else if (is_index) {
+ if (!fd_converter(o, &path->fd))
+ goto err;
+ path->path = NULL;
+ goto out;
+ } else {
+err_format:
+ PyErr_Format(PyExc_TypeError, "expected %s, not %s",
+ path->allow_fd ? "string, bytes, os.PathLike, or integer" :
+ "string, bytes, or os.PathLike",
+ Py_TYPE(o)->tp_name);
+ goto err;
+ }
+
+ length = PyBytes_GET_SIZE(bytes);
+ tmp = PyBytes_AS_STRING(bytes);
+ if ((size_t)length != strlen(tmp)) {
+ PyErr_SetString(PyExc_TypeError,
+ "path has embedded nul character");
+ goto err;
+ }
+
+ path->path = tmp;
+ if (bytes == o)
+ Py_DECREF(bytes);
+ else
+ path->cleanup = bytes;
+ path->fd = -1;
+
+out:
+ path->length = length;
+ path->object = o;
+ return Py_CLEANUP_SUPPORTED;
+
+err:
+ Py_XDECREF(o);
+ Py_XDECREF(bytes);
+ return 0;
+}
+
+PyObject *list_from_uint64_array(const uint64_t *arr, size_t n)
+{
+ PyObject *ret;
+ size_t i;
+
+ ret = PyList_New(n);
+ if (!ret)
+ return NULL;
+
+ for (i = 0; i < n; i++) {
+ PyObject *tmp;
+
+ tmp = PyLong_FromUnsignedLongLong(arr[i]);
+ if (!tmp) {
+ Py_DECREF(ret);
+ return NULL;
+ }
+ PyList_SET_ITEM(ret, i, tmp);
+ }
+
+ return ret;
+}
+
+void path_cleanup(struct path_arg *path)
+{
+ Py_CLEAR(path->object);
+ Py_CLEAR(path->cleanup);
+}
+
+static PyMethodDef btrfsutil_methods[] = {
+ {"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"},
+ {"start_sync", (PyCFunction)start_sync,
+ METH_VARARGS | METH_KEYWORDS,
+ "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"},
+ {"wait_sync", (PyCFunction)wait_sync,
+ METH_VARARGS | METH_KEYWORDS,
+ "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"},
+ {"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_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_path", (PyCFunction)subvolume_path,
+ METH_VARARGS | METH_KEYWORDS,
+ "subvolume_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"},
+ {"subvolume_info", (PyCFunction)subvolume_info,
+ METH_VARARGS | METH_KEYWORDS,
+ "subvolume_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"},
+ {"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"},
+ {"set_subvolume_read_only", (PyCFunction)set_subvolume_read_only,
+ METH_VARARGS | METH_KEYWORDS,
+ "set_subvolume_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"},
+ {"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"},
+ {"set_default_subvolume", (PyCFunction)set_default_subvolume,
+ METH_VARARGS | METH_KEYWORDS,
+ "set_default_subvolume(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"},
+ {"create_subvolume", (PyCFunction)create_subvolume,
+ METH_VARARGS | METH_KEYWORDS,
+ "create_subvolume(path, async=False)\n\n"
+ "Create a new subvolume.\n\n"
+ "Arguments:\n"
+ "path -- string, bytes, or path-like object\n"
+ "async -- create the subvolume without waiting for it to commit to\n"
+ "disk and return the transaction ID"},
+ {"create_snapshot", (PyCFunction)create_snapshot,
+ METH_VARARGS | METH_KEYWORDS,
+ "create_snapshot(source, path, recursive=False, read_only=False, async=False)\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 -- create the subvolume without waiting for it to commit to\n"
+ "disk and return the transaction ID"},
+ {"delete_subvolume", (PyCFunction)delete_subvolume,
+ METH_VARARGS | METH_KEYWORDS,
+ "delete_subvolume(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"},
+ {"deleted_subvolumes", (PyCFunction)deleted_subvolumes,
+ METH_VARARGS | METH_KEYWORDS,
+ "deleted_subvolumes(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"},
+ {},
+};
+
+static struct PyModuleDef btrfsutilmodule = {
+ PyModuleDef_HEAD_INIT,
+ "btrfsutil",
+ "Library for managing Btrfs filesystems",
+ -1,
+ btrfsutil_methods,
+};
+
+PyMODINIT_FUNC
+PyInit_btrfsutil(void)
+{
+ PyObject *m;
+
+ BtrfsUtilError_type.tp_base = (PyTypeObject *)PyExc_OSError;
+ if (PyType_Ready(&BtrfsUtilError_type) < 0)
+ return NULL;
+
+ if (PyStructSequence_InitType2(&SubvolumeInfo_type, &SubvolumeInfo_desc) < 0)
+ return NULL;
+
+ SubvolumeIterator_type.tp_new = PyType_GenericNew;
+ if (PyType_Ready(&SubvolumeIterator_type) < 0)
+ return NULL;
+
+ QgroupInherit_type.tp_new = PyType_GenericNew;
+ if (PyType_Ready(&QgroupInherit_type) < 0)
+ return NULL;
+
+ m = PyModule_Create(&btrfsutilmodule);
+ if (!m)
+ return NULL;
+
+ Py_INCREF(&BtrfsUtilError_type);
+ PyModule_AddObject(m, "BtrfsUtilError",
+ (PyObject *)&BtrfsUtilError_type);
+
+ Py_INCREF(&SubvolumeInfo_type);
+ PyModule_AddObject(m, "SubvolumeInfo", (PyObject *)&SubvolumeInfo_type);
+
+ Py_INCREF(&SubvolumeIterator_type);
+ PyModule_AddObject(m, "SubvolumeIterator",
+ (PyObject *)&SubvolumeIterator_type);
+
+ Py_INCREF(&QgroupInherit_type);
+ PyModule_AddObject(m, "QgroupInherit",
+ (PyObject *)&QgroupInherit_type);
+
+ add_module_constants(m);
+
+ return m;
+}
diff --git a/libbtrfsutil/python/qgroup.c b/libbtrfsutil/python/qgroup.c
new file mode 100644
index 00000000..44ac5ebc
--- /dev/null
+++ b/libbtrfsutil/python/qgroup.c
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2018 Facebook
+ *
+ * This file is part of libbtrfsutil.
+ *
+ * libbtrfsutil is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * libbtrfsutil is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "btrfsutilpy.h"
+
+static void QgroupInherit_dealloc(QgroupInherit *self)
+{
+ btrfs_util_destroy_qgroup_inherit(self->inherit);
+ Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+static int QgroupInherit_init(QgroupInherit *self, PyObject *args,
+ PyObject *kwds)
+{
+ static char *keywords[] = {NULL};
+ enum btrfs_util_error err;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, ":QgroupInherit",
+ keywords))
+ return -1;
+
+ err = btrfs_util_create_qgroup_inherit(0, &self->inherit);
+ if (err) {
+ SetFromBtrfsUtilError(err);
+ return -1;
+ }
+
+ return 0;
+}
+
+static PyObject *QgroupInherit_getattro(QgroupInherit *self, PyObject *nameobj)
+{
+ const char *name = "";
+
+ if (PyUnicode_Check(nameobj)) {
+ name = PyUnicode_AsUTF8(nameobj);
+ if (!name)
+ return NULL;
+ }
+
+ if (strcmp(name, "groups") == 0) {
+ const uint64_t *arr;
+ size_t n;
+
+ btrfs_util_qgroup_inherit_get_groups(self->inherit, &arr, &n);
+
+ return list_from_uint64_array(arr, n);
+ } else {
+ return PyObject_GenericGetAttr((PyObject *)self, nameobj);
+ }
+}
+
+static PyObject *QgroupInherit_add_group(QgroupInherit *self, PyObject *args,
+ PyObject *kwds)
+{
+ static char *keywords[] = {"qgroupid", NULL};
+ enum btrfs_util_error err;
+ uint64_t qgroupid;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "K:add_group", keywords,
+ &qgroupid))
+ return NULL;
+
+ err = btrfs_util_qgroup_inherit_add_group(&self->inherit, qgroupid);
+ if (err) {
+ SetFromBtrfsUtilError(err);
+ return NULL;
+ }
+
+ Py_RETURN_NONE;
+}
+
+static PyMethodDef QgroupInherit_methods[] = {
+ {"add_group", (PyCFunction)QgroupInherit_add_group,
+ METH_VARARGS | METH_KEYWORDS,
+ "add_group(qgroupid)\n\n"
+ "Add a qgroup to inherit from.\n\n"
+ "Arguments:\n"
+ "qgroupid -- ID of qgroup to add"},
+ {},
+};
+
+#define QgroupInherit_DOC \
+ "QgroupInherit() -> new qgroup inheritance specifier\n\n" \
+ "Create a new object which specifies what qgroups to inherit\n" \
+ "from for create_subvolume() and create_snapshot()"
+
+PyTypeObject QgroupInherit_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "btrfsutil.QgroupInherit", /* tp_name */
+ sizeof(QgroupInherit), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ (destructor)QgroupInherit_dealloc, /* tp_dealloc */
+ NULL, /* tp_print */
+ NULL, /* tp_getattr */
+ NULL, /* tp_setattr */
+ NULL, /* tp_as_async */
+ NULL, /* tp_repr */
+ NULL, /* tp_as_number */
+ NULL, /* tp_as_sequence */
+ NULL, /* tp_as_mapping */
+ NULL, /* tp_hash */
+ NULL, /* tp_call */
+ NULL, /* tp_str */
+ (getattrofunc)QgroupInherit_getattro, /* tp_getattro */
+ NULL, /* tp_setattro */
+ NULL, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT, /* tp_flags */
+ QgroupInherit_DOC, /* tp_doc */
+ NULL, /* tp_traverse */
+ NULL, /* tp_clear */
+ NULL, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ NULL, /* tp_iter */
+ NULL, /* tp_iternext */
+ QgroupInherit_methods, /* tp_methods */
+ NULL, /* tp_members */
+ NULL, /* tp_getset */
+ NULL, /* tp_base */
+ NULL, /* tp_dict */
+ NULL, /* tp_descr_get */
+ NULL, /* tp_descr_set */
+ 0, /* tp_dictoffset */
+ (initproc)QgroupInherit_init, /* tp_init */
+};
diff --git a/libbtrfsutil/python/setup.py b/libbtrfsutil/python/setup.py
new file mode 100755
index 00000000..1cd3bc00
--- /dev/null
+++ b/libbtrfsutil/python/setup.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2018 Facebook
+#
+# This file is part of libbtrfsutil.
+#
+# libbtrfsutil is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# libbtrfsutil is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import os
+import os.path
+from setuptools import setup, Extension
+from setuptools.command.build_ext import build_ext
+import subprocess
+
+
+def get_version():
+ with open('../btrfsutil.h', 'r') as f:
+ btrfsutil_h = f.read()
+ major = re.search(r'^#define BTRFS_UTIL_VERSION_MAJOR ([0-9]+)$',
+ btrfsutil_h, flags=re.MULTILINE).group(1)
+ minor = re.search(r'^#define BTRFS_UTIL_VERSION_MINOR ([0-9]+)$',
+ btrfsutil_h, flags=re.MULTILINE).group(1)
+ patch = re.search(r'^#define BTRFS_UTIL_VERSION_PATCH ([0-9]+)$',
+ btrfsutil_h, flags=re.MULTILINE).group(1)
+ return major + '.' + minor + '.' + patch
+
+
+def out_of_date(dependencies, target):
+ dependency_mtimes = [os.path.getmtime(dependency) for dependency in dependencies]
+ try:
+ target_mtime = os.path.getmtime(target)
+ except OSError:
+ return True
+ return any(dependency_mtime >= target_mtime for dependency_mtime in dependency_mtimes)
+
+
+def gen_constants():
+ with open('../btrfsutil.h', 'r') as f:
+ btrfsutil_h = f.read()
+
+ constants = re.findall(
+ r'^\s*(BTRFS_UTIL_ERROR_[a-zA-Z0-9_]+)',
+ btrfsutil_h, flags=re.MULTILINE)
+
+ with open('constants.c', 'w') as f:
+ f.write("""\
+#include <btrfsutil.h>
+#include "btrfsutilpy.h"
+
+void add_module_constants(PyObject *m)
+{
+""")
+ for constant in constants:
+ assert constant.startswith('BTRFS_UTIL_')
+ name = constant[len('BTRFS_UTIL_'):]
+ f.write('\tPyModule_AddIntConstant(m, "{}", {});\n'.format(name, constant))
+ f.write("""\
+}
+""")
+
+
+class my_build_ext(build_ext):
+ def run(self):
+ if out_of_date(['../btrfsutil.h'], 'constants.c'):
+ try:
+ gen_constants()
+ except Exception as e:
+ try:
+ os.remove('constants.c')
+ except OSError:
+ pass
+ raise e
+ super().run()
+
+
+module = Extension(
+ name='btrfsutil',
+ sources=[
+ 'constants.c',
+ 'error.c',
+ 'filesystem.c',
+ 'module.c',
+ 'qgroup.c',
+ 'subvolume.c',
+ ],
+ include_dirs=['..'],
+ library_dirs=['../..'],
+ libraries=['btrfsutil'],
+)
+
+setup(
+ name='btrfsutil',
+ version=get_version(),
+ description='Library for managing Btrfs filesystems',
+ url='https://github.com/kdave/btrfs-progs',
+ license='LGPLv3',
+ cmdclass={'build_ext': my_build_ext},
+ ext_modules=[module],
+)
diff --git a/libbtrfsutil/python/subvolume.c b/libbtrfsutil/python/subvolume.c
new file mode 100644
index 00000000..069e606b
--- /dev/null
+++ b/libbtrfsutil/python/subvolume.c
@@ -0,0 +1,667 @@
+/*
+ * Copyright (C) 2018 Facebook
+ *
+ * This file is part of libbtrfsutil.
+ *
+ * libbtrfsutil is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * libbtrfsutil is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "btrfsutilpy.h"
+
+PyObject *is_subvolume(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", NULL};
+ struct path_arg path = {.allow_fd = true};
+ enum btrfs_util_error err;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:is_subvolume",
+ keywords, &path_converter, &path))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_is_subvolume(path.path);
+ else
+ err = btrfs_util_is_subvolume_fd(path.fd);
+ if (err == BTRFS_UTIL_OK) {
+ path_cleanup(&path);
+ Py_RETURN_TRUE;
+ } else if (err == BTRFS_UTIL_ERROR_NOT_BTRFS ||
+ err == BTRFS_UTIL_ERROR_NOT_SUBVOLUME) {
+ path_cleanup(&path);
+ Py_RETURN_FALSE;
+ } else {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+}
+
+PyObject *subvolume_id(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", NULL};
+ struct path_arg path = {.allow_fd = true};
+ enum btrfs_util_error err;
+ uint64_t id;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:subvolume_id",
+ keywords, &path_converter, &path))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_subvolume_id(path.path, &id);
+ else
+ err = btrfs_util_subvolume_id_fd(path.fd, &id);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ return PyLong_FromUnsignedLongLong(id);
+}
+
+PyObject *subvolume_path(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", "id", NULL};
+ struct path_arg path = {.allow_fd = true};
+ enum btrfs_util_error err;
+ uint64_t id = 0;
+ char *subvol_path;
+ PyObject *ret;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|K:subvolume_path",
+ keywords, &path_converter, &path, &id))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_subvolume_path(path.path, id, &subvol_path);
+ else
+ err = btrfs_util_subvolume_path_fd(path.fd, id, &subvol_path);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+
+ ret = PyUnicode_DecodeFSDefault(subvol_path);
+ free(subvol_path);
+ return ret;
+}
+
+static PyObject *subvolume_info_to_object(const struct btrfs_util_subvolume_info *subvol)
+{
+ PyObject *ret, *tmp;
+
+ ret = PyStructSequence_New(&SubvolumeInfo_type);
+ if (ret == NULL)
+ return NULL;
+
+#define SET_UINT64(i, field) \
+ tmp = PyLong_FromUnsignedLongLong(subvol->field); \
+ if (tmp == NULL) { \
+ Py_DECREF(ret); \
+ return ret; \
+ } \
+ PyStructSequence_SET_ITEM(ret, i, tmp);
+
+#define SET_UUID(i, field) \
+ tmp = PyBytes_FromStringAndSize((char *)subvol->field, 16); \
+ if (tmp == NULL) { \
+ Py_DECREF(ret); \
+ return ret; \
+ } \
+ PyStructSequence_SET_ITEM(ret, i, tmp);
+
+#define SET_TIME(i, field) \
+ tmp = PyFloat_FromDouble(subvol->field.tv_sec + \
+ subvol->field.tv_nsec / 1000000000); \
+ if (tmp == NULL) { \
+ Py_DECREF(ret); \
+ return ret; \
+ } \
+ PyStructSequence_SET_ITEM(ret, i, tmp);
+
+ SET_UINT64(0, id);
+ SET_UINT64(1, parent_id);
+ SET_UINT64(2, dir_id);
+ SET_UINT64(3, flags);
+ SET_UUID(4, uuid);
+ SET_UUID(5, parent_uuid);
+ SET_UUID(6, received_uuid);
+ SET_UINT64(7, generation);
+ SET_UINT64(8, ctransid);
+ SET_UINT64(9, otransid);
+ SET_UINT64(10, stransid);
+ SET_UINT64(11, rtransid);
+ SET_TIME(12, ctime);
+ SET_TIME(13, otime);
+ SET_TIME(14, stime);
+ SET_TIME(15, rtime);
+
+#undef SET_TIME
+#undef SET_UUID
+#undef SET_UINT64
+
+ return ret;
+}
+
+PyObject *subvolume_info(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", "id", NULL};
+ struct path_arg path = {.allow_fd = true};
+ struct btrfs_util_subvolume_info subvol;
+ enum btrfs_util_error err;
+ uint64_t id = 0;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|K:subvolume_info",
+ keywords, &path_converter, &path, &id))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_subvolume_info(path.path, id, &subvol);
+ else
+ err = btrfs_util_subvolume_info_fd(path.fd, id, &subvol);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+
+ return subvolume_info_to_object(&subvol);
+}
+
+static PyStructSequence_Field SubvolumeInfo_fields[] = {
+ {"id", "int ID of this subvolume"},
+ {"parent_id", "int ID of the subvolume containing this subvolume"},
+ {"dir_id", "int inode number of the directory containing this subvolume"},
+ {"flags", "int root item flags"},
+ {"uuid", "bytes UUID of this subvolume"},
+ {"parent_uuid", "bytes UUID of the subvolume this is a snapshot of"},
+ {"received_uuid", "bytes UUID of the subvolume this was received from"},
+ {"generation", "int transaction ID of the subvolume root"},
+ {"ctransid", "int transaction ID when an inode was last changed"},
+ {"otransid", "int transaction ID when this subvolume was created"},
+ {"stransid", "int transaction ID of the sent subvolume this subvolume was received from"},
+ {"rtransid", "int transaction ID when this subvolume was received"},
+ {"ctime", "float time when an inode was last changed"},
+ {"otime", "float time when this subvolume was created"},
+ {"stime", "float time, usually zero"},
+ {"rtime", "float time when this subvolume was received"},
+ {},
+};
+
+PyStructSequence_Desc SubvolumeInfo_desc = {
+ "btrfsutil.SubvolumeInfo",
+ "Information about a Btrfs subvolume.",
+ SubvolumeInfo_fields,
+ 14,
+};
+
+PyTypeObject SubvolumeInfo_type;
+
+PyObject *get_subvolume_read_only(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", NULL};
+ struct path_arg path = {.allow_fd = true};
+ enum btrfs_util_error err;
+ bool read_only;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds,
+ "O&:get_subvolume_read_only",
+ keywords, &path_converter, &path))
+ return NULL;
+
+ if (path.path) {
+ err = btrfs_util_get_subvolume_read_only(path.path, &read_only);
+ } else {
+ err = btrfs_util_get_subvolume_read_only_fd(path.fd,
+ &read_only);
+ }
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ return PyBool_FromLong(read_only);
+}
+
+PyObject *set_subvolume_read_only(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", "read_only", NULL};
+ struct path_arg path = {.allow_fd = true};
+ enum btrfs_util_error err;
+ int read_only = 1;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds,
+ "O&|p:set_subvolume_read_only",
+ keywords, &path_converter, &path,
+ &read_only))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_set_subvolume_read_only(path.path, read_only);
+ else
+ err = btrfs_util_set_subvolume_read_only_fd(path.fd, read_only);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ Py_RETURN_NONE;
+}
+
+PyObject *get_default_subvolume(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", NULL};
+ struct path_arg path = {.allow_fd = true};
+ enum btrfs_util_error err;
+ uint64_t id;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:get_default_subvolume",
+ keywords, &path_converter, &path))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_get_default_subvolume(path.path, &id);
+ else
+ err = btrfs_util_get_default_subvolume_fd(path.fd, &id);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ return PyLong_FromUnsignedLongLong(id);
+}
+
+PyObject *set_default_subvolume(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", "id", NULL};
+ struct path_arg path = {.allow_fd = true};
+ enum btrfs_util_error err;
+ uint64_t id = 0;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|K:set_default_subvolume",
+ keywords, &path_converter, &path, &id))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_set_default_subvolume(path.path, id);
+ else
+ err = btrfs_util_set_default_subvolume_fd(path.fd, id);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ Py_RETURN_NONE;
+}
+
+PyObject *create_subvolume(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", "async", "qgroup_inherit", NULL};
+ struct path_arg path = {.allow_fd = false};
+ enum btrfs_util_error err;
+ int async = 0;
+ QgroupInherit *inherit = NULL;
+ uint64_t transid;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|pO!:create_subvolume",
+ keywords, &path_converter, &path,
+ &async, &QgroupInherit_type, &inherit))
+ return NULL;
+
+ err = btrfs_util_create_subvolume(path.path, 0, async ? &transid : NULL,
+ inherit ? inherit->inherit : NULL);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ if (async)
+ return PyLong_FromUnsignedLongLong(transid);
+ else
+ Py_RETURN_NONE;
+}
+
+PyObject *create_snapshot(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {
+ "source", "path", "recursive", "read_only", "async",
+ "qgroup_inherit", NULL,
+ };
+ struct path_arg src = {.allow_fd = true}, dst = {.allow_fd = false};
+ enum btrfs_util_error err;
+ int recursive = 0, read_only = 0, async = 0;
+ int flags = 0;
+ QgroupInherit *inherit = NULL;
+ uint64_t transid;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|pppO!:create_snapshot",
+ keywords, &path_converter, &src,
+ &path_converter, &dst, &recursive,
+ &read_only, &async,
+ &QgroupInherit_type, &inherit))
+ return NULL;
+
+ if (recursive)
+ flags |= BTRFS_UTIL_CREATE_SNAPSHOT_RECURSIVE;
+ if (read_only)
+ flags |= BTRFS_UTIL_CREATE_SNAPSHOT_READ_ONLY;
+
+ if (src.path) {
+ err = btrfs_util_create_snapshot(src.path, dst.path, flags,
+ async ? &transid : NULL,
+ inherit ? inherit->inherit : NULL);
+ } else {
+ err = btrfs_util_create_snapshot_fd(src.fd, dst.path, flags,
+ async ? &transid : NULL,
+ inherit ? inherit->inherit : NULL);
+ }
+ if (err) {
+ SetFromBtrfsUtilErrorWithPaths(err, &src, &dst);
+ path_cleanup(&src);
+ path_cleanup(&dst);
+ return NULL;
+ }
+
+ path_cleanup(&src);
+ path_cleanup(&dst);
+ if (async)
+ return PyLong_FromUnsignedLongLong(transid);
+ else
+ Py_RETURN_NONE;
+}
+
+PyObject *delete_subvolume(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", "recursive", NULL};
+ struct path_arg path = {.allow_fd = false};
+ enum btrfs_util_error err;
+ int recursive = 0;
+ int flags = 0;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|p:delete_subvolume",
+ keywords, &path_converter, &path,
+ &recursive))
+ return NULL;
+
+ if (recursive)
+ flags |= BTRFS_UTIL_DELETE_SUBVOLUME_RECURSIVE;
+
+ err = btrfs_util_delete_subvolume(path.path, flags);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+ Py_RETURN_NONE;
+}
+
+PyObject *deleted_subvolumes(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ static char *keywords[] = {"path", NULL};
+ struct path_arg path = {.allow_fd = true};
+ PyObject *ret;
+ uint64_t *ids;
+ size_t n;
+ enum btrfs_util_error err;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:deleted_subvolumes",
+ keywords, &path_converter, &path))
+ return NULL;
+
+ if (path.path)
+ err = btrfs_util_deleted_subvolumes(path.path, &ids, &n);
+ else
+ err = btrfs_util_deleted_subvolumes_fd(path.fd, &ids, &n);
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return NULL;
+ }
+
+ path_cleanup(&path);
+
+ ret = list_from_uint64_array(ids, n);
+ free(ids);
+ return ret;
+}
+
+typedef struct {
+ PyObject_HEAD
+ struct btrfs_util_subvolume_iterator *iter;
+ bool info;
+} SubvolumeIterator;
+
+static void SubvolumeIterator_dealloc(SubvolumeIterator *self)
+{
+ btrfs_util_destroy_subvolume_iterator(self->iter);
+ Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+static PyObject *SubvolumeIterator_next(SubvolumeIterator *self)
+{
+ enum btrfs_util_error err;
+ PyObject *ret, *tmp;
+ char *path;
+
+ if (!self->iter) {
+ PyErr_SetString(PyExc_ValueError,
+ "operation on closed iterator");
+ return NULL;
+ }
+
+ if (self->info) {
+ struct btrfs_util_subvolume_info subvol;
+
+ err = btrfs_util_subvolume_iterator_next_info(self->iter, &path,
+ &subvol);
+ if (err == BTRFS_UTIL_ERROR_STOP_ITERATION) {
+ PyErr_SetNone(PyExc_StopIteration);
+ return NULL;
+ } else if (err) {
+ SetFromBtrfsUtilError(err);
+ return NULL;
+ }
+
+ tmp = subvolume_info_to_object(&subvol);
+ } else {
+ uint64_t id;
+
+ err = btrfs_util_subvolume_iterator_next(self->iter, &path, &id);
+ if (err == BTRFS_UTIL_ERROR_STOP_ITERATION) {
+ PyErr_SetNone(PyExc_StopIteration);
+ return NULL;
+ } else if (err) {
+ SetFromBtrfsUtilError(err);
+ return NULL;
+ }
+
+ tmp = PyLong_FromUnsignedLongLong(id);
+
+ }
+ if (tmp) {
+ ret = Py_BuildValue("O&O", PyUnicode_DecodeFSDefault, path,
+ tmp);
+ Py_DECREF(tmp);
+ free(path);
+ } else {
+ ret = NULL;
+ }
+ return ret;
+}
+
+static int SubvolumeIterator_init(SubvolumeIterator *self, PyObject *args,
+ PyObject *kwds)
+{
+ static char *keywords[] = {"path", "top", "info", "post_order", NULL};
+ struct path_arg path = {.allow_fd = true};
+ enum btrfs_util_error err;
+ unsigned long long top = 5;
+ int info = 0;
+ int post_order = 0;
+ int flags = 0;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|Kpp:SubvolumeIterator",
+ keywords, &path_converter, &path, &top,
+ &info, &post_order))
+ return -1;
+
+ if (post_order)
+ flags |= BTRFS_UTIL_SUBVOLUME_ITERATOR_POST_ORDER;
+
+ if (path.path) {
+ err = btrfs_util_create_subvolume_iterator(path.path, top,
+ flags, &self->iter);
+ } else {
+ err = btrfs_util_create_subvolume_iterator_fd(path.fd, top,
+ flags,
+ &self->iter);
+ }
+ if (err) {
+ SetFromBtrfsUtilErrorWithPath(err, &path);
+ path_cleanup(&path);
+ return -1;
+ }
+
+ self->info = info;
+
+ return 0;
+}
+
+static PyObject *SubvolumeIterator_close(SubvolumeIterator *self)
+{
+ if (self->iter) {
+ btrfs_util_destroy_subvolume_iterator(self->iter);
+ self->iter = NULL;
+ }
+ Py_RETURN_NONE;
+}
+
+static PyObject *SubvolumeIterator_fileno(SubvolumeIterator *self)
+{
+ if (!self->iter) {
+ PyErr_SetString(PyExc_ValueError,
+ "operation on closed iterator");
+ return NULL;
+ }
+ return PyLong_FromLong(btrfs_util_subvolume_iterator_fd(self->iter));
+}
+
+static PyObject *SubvolumeIterator_enter(SubvolumeIterator *self)
+{
+ Py_INCREF((PyObject *)self);
+ return (PyObject *)self;
+}
+
+static PyObject *SubvolumeIterator_exit(SubvolumeIterator *self, PyObject *args,
+ PyObject *kwds)
+{
+ static char *keywords[] = {"exc_type", "exc_value", "traceback", NULL};
+ PyObject *exc_type, *exc_value, *traceback;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO:__exit__", keywords,
+ &exc_type, &exc_value, &traceback))
+ return NULL;
+
+ return SubvolumeIterator_close(self);
+}
+
+#define SubvolumeIterator_DOC \
+ "SubvolumeIterator(path, top=0, info=False, post_order=False) -> new subvolume iterator\n\n" \
+ "Create a new iterator that produces tuples of (path, ID) representing\n" \
+ "subvolumes on a filesystem.\n\n" \
+ "Arguments:\n" \
+ "path -- string, bytes, path-like object, or open file descriptor in\n" \
+ "filesystem to list\n" \
+ "top -- if not zero, instead of only listing subvolumes beneath the\n" \
+ "given path, list subvolumes beneath the subvolume with this ID; passing\n" \
+ "BTRFS_FS_TREE_OBJECTID (5) here lists all subvolumes. The subvolumes\n" \
+ "are listed relative to the subvolume with this ID.\n" \
+ "info -- bool indicating the iterator should yield SubvolumeInfo instead of\n" \
+ "the subvolume ID\n" \
+ "post_order -- bool indicating whether to yield parent subvolumes before\n" \
+ "child subvolumes (e.g., 'foo/bar' before 'foo')"
+
+static PyMethodDef SubvolumeIterator_methods[] = {
+ {"close", (PyCFunction)SubvolumeIterator_close,
+ METH_NOARGS,
+ "close()\n\n"
+ "Close this iterator."},
+ {"fileno", (PyCFunction)SubvolumeIterator_fileno,
+ METH_NOARGS,
+ "fileno() -> int\n\n"
+ "Get the file descriptor associated with this iterator."},
+ {"__enter__", (PyCFunction)SubvolumeIterator_enter,
+ METH_NOARGS, ""},
+ {"__exit__", (PyCFunction)SubvolumeIterator_exit,
+ METH_VARARGS | METH_KEYWORDS, ""},
+ {},
+};
+
+PyTypeObject SubvolumeIterator_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "btrfsutil.SubvolumeIterator", /* tp_name */
+ sizeof(SubvolumeIterator), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ (destructor)SubvolumeIterator_dealloc, /* tp_dealloc */
+ NULL, /* tp_print */
+ NULL, /* tp_getattr */
+ NULL, /* tp_setattr */
+ NULL, /* tp_as_async */
+ NULL, /* tp_repr */
+ NULL, /* tp_as_number */
+ NULL, /* tp_as_sequence */
+ NULL, /* tp_as_mapping */
+ NULL, /* tp_hash */
+ NULL, /* tp_call */
+ NULL, /* tp_str */
+ NULL, /* tp_getattro */
+ NULL, /* tp_setattro */
+ NULL, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT, /* tp_flags */
+ SubvolumeIterator_DOC, /* tp_doc */
+ NULL, /* tp_traverse */
+ NULL, /* tp_clear */
+ NULL, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ PyObject_SelfIter, /* tp_iter */
+ (iternextfunc)SubvolumeIterator_next, /* tp_iternext */
+ SubvolumeIterator_methods, /* tp_methods */
+ NULL, /* tp_members */
+ NULL, /* tp_getset */
+ NULL, /* tp_base */
+ NULL, /* tp_dict */
+ NULL, /* tp_descr_get */
+ NULL, /* tp_descr_set */
+ 0, /* tp_dictoffset */
+ (initproc)SubvolumeIterator_init, /* tp_init */
+};
diff --git a/libbtrfsutil/python/tests/__init__.py b/libbtrfsutil/python/tests/__init__.py
new file mode 100644
index 00000000..35550e0a
--- /dev/null
+++ b/libbtrfsutil/python/tests/__init__.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2018 Facebook
+#
+# This file is part of libbtrfsutil.
+#
+# libbtrfsutil is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# libbtrfsutil is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+from pathlib import PurePath
+import subprocess
+import tempfile
+import unittest
+
+
+HAVE_PATH_LIKE = hasattr(PurePath, '__fspath__')
+
+
+@unittest.skipIf(os.geteuid() != 0, 'must be run as root')
+class BtrfsTestCase(unittest.TestCase):
+ def setUp(self):
+ self.mountpoint = tempfile.mkdtemp()
+ try:
+ with tempfile.NamedTemporaryFile(delete=False) as f:
+ os.truncate(f.fileno(), 1024 * 1024 * 1024)
+ self.image = f.name
+ except Exception as e:
+ os.rmdir(self.mountpoint)
+ raise e
+
+ if os.path.exists('../../mkfs.btrfs'):
+ mkfs = '../../mkfs.btrfs'
+ else:
+ mkfs = 'mkfs.btrfs'
+ try:
+ subprocess.check_call([mkfs, '-q', self.image])
+ subprocess.check_call(['mount', '-o', 'loop', '--', self.image, self.mountpoint])
+ except Exception as e:
+ os.remove(self.image)
+ os.rmdir(self.mountpoint)
+ raise e
+
+ def tearDown(self):
+ try:
+ subprocess.check_call(['umount', self.mountpoint])
+ finally:
+ os.remove(self.image)
+ os.rmdir(self.mountpoint)
+
+ @staticmethod
+ def path_or_fd(path, open_flags=os.O_RDONLY):
+ yield path
+ yield path.encode()
+ if HAVE_PATH_LIKE:
+ yield PurePath(path)
+ fd = os.open(path, open_flags)
+ try:
+ yield fd
+ finally:
+ os.close(fd)
+
diff --git a/libbtrfsutil/python/tests/test_filesystem.py b/libbtrfsutil/python/tests/test_filesystem.py
new file mode 100644
index 00000000..006a6b1e
--- /dev/null
+++ b/libbtrfsutil/python/tests/test_filesystem.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2018 Facebook
+#
+# This file is part of libbtrfsutil.
+#
+# libbtrfsutil is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# libbtrfsutil is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import time
+
+import btrfsutil
+from tests import BtrfsTestCase, HAVE_PATH_LIKE
+
+
+def touch(path):
+ now = time.time()
+ os.utime(path, (now, now))
+
+
+class TestSubvolume(BtrfsTestCase):
+ def super_generation(self):
+ with open(self.image, 'rb') as f:
+ # csum is 32 bytes, fsid is 16 bytes, bytenr is 8 bytes, flags is 8
+ # bytes
+ f.seek(65536 + 32 + 16 + 8 + 8)
+ self.assertEqual(f.read(8), b'_BHRfS_M')
+ return int.from_bytes(f.read(8), 'little')
+
+ def test_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.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):
+ with self.subTest(type=type(arg)):
+ touch(arg)
+ transid = btrfsutil.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):
+ with self.subTest(type=type(arg)):
+ touch(arg)
+ transid = btrfsutil.start_sync(arg)
+ btrfsutil.wait_sync(arg, transid)
+ new_generation = self.super_generation()
+ self.assertGreater(new_generation, old_generation)
+ old_generation = new_generation
+
+ touch(arg)
+ btrfsutil.start_sync(arg)
+ btrfsutil.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
new file mode 100644
index 00000000..74fc46b6
--- /dev/null
+++ b/libbtrfsutil/python/tests/test_qgroup.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2018 Facebook
+#
+# This file is part of libbtrfsutil.
+#
+# libbtrfsutil is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# libbtrfsutil is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+import btrfsutil
+from tests import BtrfsTestCase
+
+
+class TestQgroup(BtrfsTestCase):
+ def test_subvolume_inherit(self):
+ subvol = os.path.join(self.mountpoint, 'subvol')
+
+ inherit = btrfsutil.QgroupInherit()
+ inherit.add_group(5)
+
+ btrfsutil.create_subvolume(subvol, qgroup_inherit=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.create_subvolume(subvol)
+ btrfsutil.create_snapshot(subvol, snapshot, qgroup_inherit=inherit)
+
+
+class TestQgroupInherit(unittest.TestCase):
+ def test_new(self):
+ inherit = btrfsutil.QgroupInherit()
+ self.assertEqual(inherit.groups, [])
+
+ def test_add_group(self):
+ inherit = btrfsutil.QgroupInherit()
+ inherit.add_group(1)
+ self.assertEqual(inherit.groups, [1])
+ inherit.add_group(2)
+ self.assertEqual(inherit.groups, [1, 2])
+ inherit.add_group(3)
+ self.assertEqual(inherit.groups, [1, 2, 3])
diff --git a/libbtrfsutil/python/tests/test_subvolume.py b/libbtrfsutil/python/tests/test_subvolume.py
new file mode 100644
index 00000000..93396cba
--- /dev/null
+++ b/libbtrfsutil/python/tests/test_subvolume.py
@@ -0,0 +1,385 @@
+# Copyright (C) 2018 Facebook
+#
+# This file is part of libbtrfsutil.
+#
+# libbtrfsutil is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# libbtrfsutil is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
+
+import fcntl
+import errno
+import os
+import os.path
+from pathlib import PurePath
+import traceback
+
+import btrfsutil
+from tests import BtrfsTestCase, HAVE_PATH_LIKE
+
+
+class TestSubvolume(BtrfsTestCase):
+ def test_is_subvolume(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.is_subvolume(arg))
+ for arg in self.path_or_fd(dir):
+ with self.subTest(type=type(arg)):
+ self.assertFalse(btrfsutil.is_subvolume(arg))
+
+ with self.assertRaises(btrfsutil.BtrfsUtilError) as e:
+ btrfsutil.is_subvolume(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)
+
+ for arg in self.path_or_fd(self.mountpoint):
+ with self.subTest(type=type(arg)):
+ self.assertEqual(btrfsutil.subvolume_id(arg), 5)
+ for arg in self.path_or_fd(dir):
+ with self.subTest(type=type(arg)):
+ self.assertEqual(btrfsutil.subvolume_id(arg), 5)
+
+ def test_subvolume_path(self):
+ btrfsutil.create_subvolume(os.path.join(self.mountpoint, 'subvol1'))
+ os.mkdir(os.path.join(self.mountpoint, 'dir1'))
+ os.mkdir(os.path.join(self.mountpoint, 'dir1/dir2'))
+ btrfsutil.create_subvolume(os.path.join(self.mountpoint, 'dir1/dir2/subvol2'))
+ btrfsutil.create_subvolume(os.path.join(self.mountpoint, 'dir1/dir2/subvol2/subvol3'))
+ os.mkdir(os.path.join(self.mountpoint, 'subvol1/dir3'))
+ btrfsutil.create_subvolume(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_path(arg), '')
+ self.assertEqual(btrfsutil.subvolume_path(arg, 5), '')
+ self.assertEqual(btrfsutil.subvolume_path(arg, 256), 'subvol1')
+ self.assertEqual(btrfsutil.subvolume_path(arg, 257), 'dir1/dir2/subvol2')
+ self.assertEqual(btrfsutil.subvolume_path(arg, 258), 'dir1/dir2/subvol2/subvol3')
+ self.assertEqual(btrfsutil.subvolume_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.create_subvolume(name)
+ os.chdir(name)
+ self.assertEqual(btrfsutil.subvolume_path('.'), path)
+ finally:
+ os.chdir(pwd)
+
+ def test_subvolume_info(self):
+ for arg in self.path_or_fd(self.mountpoint):
+ with self.subTest(type=type(arg)):
+ info = btrfsutil.subvolume_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.assertEqual(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)
+
+ subvol = os.path.join(self.mountpoint, 'subvol')
+ btrfsutil.create_subvolume(subvol)
+
+ info = btrfsutil.subvolume_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
+ snapshot = os.path.join(self.mountpoint, 'snapshot')
+ btrfsutil.create_snapshot(subvol, snapshot)
+
+ info = btrfsutil.subvolume_info(snapshot)
+ self.assertEqual(info.parent_uuid, subvol_uuid)
+
+ # TODO: test received_uuid, stransid, rtransid, stime, and rtime
+
+ 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_info(arg, 2)
+
+ def test_read_only(self):
+ for arg in self.path_or_fd(self.mountpoint):
+ with self.subTest(type=type(arg)):
+ btrfsutil.set_subvolume_read_only(arg)
+ self.assertTrue(btrfsutil.get_subvolume_read_only(arg))
+ self.assertTrue(btrfsutil.subvolume_info(arg).flags & 1)
+
+ btrfsutil.set_subvolume_read_only(arg, False)
+ self.assertFalse(btrfsutil.get_subvolume_read_only(arg))
+ self.assertFalse(btrfsutil.subvolume_info(arg).flags & 1)
+
+ btrfsutil.set_subvolume_read_only(arg, True)
+ self.assertTrue(btrfsutil.get_subvolume_read_only(arg))
+ self.assertTrue(btrfsutil.subvolume_info(arg).flags & 1)
+
+ btrfsutil.set_subvolume_read_only(arg, False)
+
+ def test_default_subvolume(self):
+ for arg in self.path_or_fd(self.mountpoint):
+ with self.subTest(type=type(arg)):
+ self.assertEqual(btrfsutil.get_default_subvolume(arg), 5)
+
+ subvol = os.path.join(self.mountpoint, 'subvol')
+ btrfsutil.create_subvolume(subvol)
+ for arg in self.path_or_fd(subvol):
+ with self.subTest(type=type(arg)):
+ btrfsutil.set_default_subvolume(arg)
+ self.assertEqual(btrfsutil.get_default_subvolume(arg), 256)
+ btrfsutil.set_default_subvolume(arg, 5)
+ self.assertEqual(btrfsutil.get_default_subvolume(arg), 5)
+
+ def test_create_subvolume(self):
+ subvol = os.path.join(self.mountpoint, 'subvol')
+
+ btrfsutil.create_subvolume(subvol + '1')
+ self.assertTrue(btrfsutil.is_subvolume(subvol + '1'))
+ btrfsutil.create_subvolume((subvol + '2').encode())
+ self.assertTrue(btrfsutil.is_subvolume(subvol + '2'))
+ if HAVE_PATH_LIKE:
+ btrfsutil.create_subvolume(PurePath(subvol + '3'))
+ self.assertTrue(btrfsutil.is_subvolume(subvol + '3'))
+
+ pwd = os.getcwd()
+ try:
+ os.chdir(self.mountpoint)
+ btrfsutil.create_subvolume('subvol4')
+ self.assertTrue(btrfsutil.is_subvolume('subvol4'))
+ finally:
+ os.chdir(pwd)
+
+ btrfsutil.create_subvolume(subvol + '5/')
+ self.assertTrue(btrfsutil.is_subvolume(subvol + '5'))
+
+ btrfsutil.create_subvolume(subvol + '6//')
+ self.assertTrue(btrfsutil.is_subvolume(subvol + '6'))
+
+ transid = btrfsutil.create_subvolume(subvol + '7', async=True)
+ self.assertTrue(btrfsutil.is_subvolume(subvol + '7'))
+ self.assertGreater(transid, 0)
+
+ # Test creating subvolumes under '/' in a chroot.
+ pid = os.fork()
+ if pid == 0:
+ try:
+ os.chroot(self.mountpoint)
+ os.chdir('/')
+ btrfsutil.create_subvolume('/subvol8')
+ self.assertTrue(btrfsutil.is_subvolume('/subvol8'))
+ with self.assertRaises(btrfsutil.BtrfsUtilError):
+ btrfsutil.create_subvolume('/')
+ 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')
+
+ btrfsutil.create_subvolume(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.create_snapshot(subvol, snapshot + '1')
+ self.assertTrue(btrfsutil.is_subvolume(snapshot + '1'))
+ self.assertTrue(os.path.exists(os.path.join(snapshot + '1', 'dir')))
+
+ btrfsutil.create_snapshot(subvol, (snapshot + '2').encode())
+ self.assertTrue(btrfsutil.is_subvolume(snapshot + '2'))
+ self.assertTrue(os.path.exists(os.path.join(snapshot + '2', 'dir')))
+
+ if HAVE_PATH_LIKE:
+ btrfsutil.create_snapshot(subvol, PurePath(snapshot + '3'))
+ self.assertTrue(btrfsutil.is_subvolume(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.create_subvolume(nested_subvol)
+ btrfsutil.create_subvolume(more_nested_subvol)
+ os.mkdir(os.path.join(more_nested_subvol, 'nested_dir'))
+
+ snapshot = os.path.join(self.mountpoint, 'snapshot')
+
+ btrfsutil.create_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.create_snapshot(subvol, snapshot + '2', recursive=True)
+ self.assertTrue(os.path.exists(os.path.join(snapshot + '2', 'nested/more_nested/nested_dir')))
+
+ transid = btrfsutil.create_snapshot(subvol, snapshot + '3', recursive=True, async=True)
+ self.assertTrue(os.path.exists(os.path.join(snapshot + '3', 'nested/more_nested/nested_dir')))
+ self.assertGreater(transid, 0)
+
+ btrfsutil.create_snapshot(subvol, snapshot + '4', read_only=True)
+ self.assertTrue(btrfsutil.get_subvolume_read_only(snapshot + '4'))
+
+ def test_delete_subvolume(self):
+ subvol = os.path.join(self.mountpoint, 'subvol')
+ btrfsutil.create_subvolume(subvol + '1')
+ self.assertTrue(os.path.exists(subvol + '1'))
+ btrfsutil.create_subvolume(subvol + '2')
+ self.assertTrue(os.path.exists(subvol + '2'))
+ btrfsutil.create_subvolume(subvol + '3')
+ self.assertTrue(os.path.exists(subvol + '3'))
+
+ btrfsutil.delete_subvolume(subvol + '1')
+ self.assertFalse(os.path.exists(subvol + '1'))
+ btrfsutil.delete_subvolume((subvol + '2').encode())
+ self.assertFalse(os.path.exists(subvol + '2'))
+ if HAVE_PATH_LIKE:
+ btrfsutil.delete_subvolume(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.create_subvolume('/subvol4')
+ self.assertTrue(os.path.exists('/subvol4'))
+ btrfsutil.delete_subvolume('/subvol4')
+ self.assertFalse(os.path.exists('/subvol4'))
+ with self.assertRaises(btrfsutil.BtrfsUtilError):
+ btrfsutil.delete_subvolume('/')
+ 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.create_subvolume(subvol + '5')
+ btrfsutil.create_subvolume(subvol + '5/foo')
+ btrfsutil.create_subvolume(subvol + '5/bar')
+ btrfsutil.create_subvolume(subvol + '5/bar/baz')
+ btrfsutil.create_subvolume(subvol + '5/bar/qux')
+ btrfsutil.create_subvolume(subvol + '5/quux')
+ with self.assertRaises(btrfsutil.BtrfsUtilError):
+ btrfsutil.delete_subvolume(subvol + '5')
+ btrfsutil.delete_subvolume(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')
+ btrfsutil.delete_subvolume(subvol + '1')
+ for arg in self.path_or_fd(self.mountpoint):
+ with self.subTest(type=type(arg)):
+ self.assertEqual(btrfsutil.deleted_subvolumes(arg), [256])
+
+ def test_subvolume_iterator(self):
+ pwd = os.getcwd()
+ try:
+ os.chdir(self.mountpoint)
+ btrfsutil.create_subvolume('foo')
+
+ path, subvol = next(btrfsutil.SubvolumeIterator('.', info=True))
+ self.assertEqual(path, 'foo')
+ self.assertIsInstance(subvol, btrfsutil.SubvolumeInfo)
+ self.assertEqual(subvol.id, 256)
+ self.assertEqual(subvol.parent_id, 5)
+
+ btrfsutil.create_subvolume('foo/bar')
+ btrfsutil.create_subvolume('foo/bar/baz')
+
+ subvols = [
+ ('foo', 256),
+ ('foo/bar', 257),
+ ('foo/bar/baz', 258),
+ ]
+
+ for arg in self.path_or_fd('.'):
+ with self.subTest(type=type(arg)):
+ self.assertEqual(list(btrfsutil.SubvolumeIterator(arg)), subvols)
+ self.assertEqual(list(btrfsutil.SubvolumeIterator('.', top=0)), subvols)
+
+ self.assertEqual(list(btrfsutil.SubvolumeIterator('.', post_order=True)),
+ [('foo/bar/baz', 258),
+ ('foo/bar', 257),
+ ('foo', 256)])
+
+ subvols = [
+ ('bar', 257),
+ ('bar/baz', 258),
+ ]
+
+ self.assertEqual(list(btrfsutil.SubvolumeIterator('.', top=256)), subvols)
+ self.assertEqual(list(btrfsutil.SubvolumeIterator('foo', top=0)), subvols)
+
+ os.rename('foo/bar/baz', 'baz')
+ self.assertEqual(sorted(btrfsutil.SubvolumeIterator('.')),
+ [('baz', 258),
+ ('foo', 256),
+ ('foo/bar', 257)])
+
+ with btrfsutil.SubvolumeIterator('.') as it:
+ self.assertGreaterEqual(it.fileno(), 0)
+ it.close()
+ with self.assertRaises(ValueError):
+ next(iter(it))
+ with self.assertRaises(ValueError):
+ it.fileno()
+ it.close()
+ finally:
+ os.chdir(pwd)