diff options
author | Dimitri John Ledkov <xnox@ubuntu.com> | 2018-05-08 14:17:29 -0700 |
---|---|---|
committer | Dimitri John Ledkov <xnox@ubuntu.com> | 2018-05-08 14:17:29 -0700 |
commit | d00c9550da1801a0eaff5cedf4312e24691b31ea (patch) | |
tree | 3881ca1764ef792259e1b70f12c884a3ac0c0715 /libbtrfsutil/python | |
parent | dab6d2181f1f194ec3a76d900cf2c6533379cbea (diff) |
New upstream release.
Diffstat (limited to 'libbtrfsutil/python')
-rw-r--r-- | libbtrfsutil/python/.gitignore | 7 | ||||
-rw-r--r-- | libbtrfsutil/python/btrfsutilpy.h | 84 | ||||
-rw-r--r-- | libbtrfsutil/python/error.c | 202 | ||||
-rw-r--r-- | libbtrfsutil/python/filesystem.c | 94 | ||||
-rw-r--r-- | libbtrfsutil/python/module.c | 321 | ||||
-rw-r--r-- | libbtrfsutil/python/qgroup.c | 141 | ||||
-rwxr-xr-x | libbtrfsutil/python/setup.py | 111 | ||||
-rw-r--r-- | libbtrfsutil/python/subvolume.c | 667 | ||||
-rw-r--r-- | libbtrfsutil/python/tests/__init__.py | 70 | ||||
-rw-r--r-- | libbtrfsutil/python/tests/test_filesystem.py | 73 | ||||
-rw-r--r-- | libbtrfsutil/python/tests/test_qgroup.py | 57 | ||||
-rw-r--r-- | libbtrfsutil/python/tests/test_subvolume.py | 385 |
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) |