initial
commit
27de066f27
@ -0,0 +1,147 @@
|
||||
### Python
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
### VIM
|
||||
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
@ -0,0 +1,31 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2019, Andreas Gonschorek (agonschorek) <simplezfs@andreasgonschorek.de>
|
||||
Copyright (c) 2019, Stefan Valouch (svalouch) <svalouch@valouch.com>
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,50 @@
|
||||
################
|
||||
Python-SimpleZFS
|
||||
################
|
||||
|
||||
A thin wrapper around ZFS from the `ZFS on Linux <https://zfsonlinux.org/>`_ project.
|
||||
|
||||
The library aims at providing a simple, low-level interface for working with ZFS, either by wrapping the ``zfs(8)`` and ``zpool(8)`` CLI utilities or by accessing the native python API.
|
||||
|
||||
It does not provide a high-level interface, however, and does not aim to. It also tries to keep as little state as possible.
|
||||
|
||||
Two interface classes make up the API, ``ZFS`` and ``ZPool``, which are wrappers around the functionality of the CLI tools of the same name. They come with two implementations:
|
||||
|
||||
* The CLI implementation wraps the executables
|
||||
* The Native implementation uses the native API released with ZoL 0.8.
|
||||
|
||||
In this early stage, the native implementation has not been written.
|
||||
|
||||
Usage
|
||||
*****
|
||||
|
||||
One can either get a concrete implementation by calling ``ZFSCli``/``ZFSNative`` or ``ZPoolCli``/``ZPoolNative``, or more conveniently use the functions ``get_zfs(implementation_name)`` or ``get_zpool(implementation_name)``.
|
||||
First, get an instance:
|
||||
|
||||
.. code-block:: python-shell
|
||||
|
||||
>>> from simplezfs import get_zfs
|
||||
>>> zfs = get_zfs('cli') # or "native" for the native API
|
||||
>>> zfs
|
||||
<simplezfs.zfs_cli.ZFSCli object at 0x7ffbca7fb9e8>
|
||||
>>>
|
||||
>>> for ds in zfs.list_datasets():
|
||||
... print(ds.name)
|
||||
...
|
||||
tank
|
||||
tank/system
|
||||
tank/system/rootfs
|
||||
|
||||
Compatibility
|
||||
*************
|
||||
The library is written with `Python` 3.6 or higher in mind, which was in a stable release in a few of the major Linux distributions we care about (Debian Buster, Ubuntu 18.04 LTS, RHEL 8, Gentoo).
|
||||
|
||||
On the ZoL_ side, the code is developed mostly on version ``0.8``, and takes some validation values from that release. The library doesn't make a lot of assumptions, the code should work on ``0.7``, too. If you spot an incompatibility, please let us know via the issue tracker.
|
||||
|
||||
Testing
|
||||
*******
|
||||
An extensive set of tests are in the ``tests/`` subfolder, it can be run using ``pytest`` from the source of the repository. At this time, only the validation functions and the ZFS Cli API are tested, the tests are non-destructive and won't run the actual commands but instead mock away the ``subprocess`` invocations and supply dummy commands to run (usually ``/bin/true``) should the code be changed in a way that isn't caught by the test framework. Nevertheless, keep in mind that if commands are run for whatever reason, they most likely result in unrecoverable data loss.
|
||||
|
||||
It is planned to add a separate set of `destructive` tests that need to be specially activated for testing if the code works when run against an actual Linux system. This can't be done using most of the CI providers, as the nature of ZFS requires having a operating system with loaded modules that may be destroyed during the test run.
|
||||
|
||||
.. _ZoL: https://zfsonlinux.org/
|
@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
@ -0,0 +1,90 @@
|
||||
###
|
||||
API
|
||||
###
|
||||
|
||||
Enumerations
|
||||
************
|
||||
|
||||
.. autoclass:: simplezfs.types.DatasetType
|
||||
:members:
|
||||
|
||||
.. autoclass:: simplezfs.types.PropertySource
|
||||
:members:
|
||||
|
||||
.. autoclass:: simplezfs.types.ZPoolHealth
|
||||
:members:
|
||||
|
||||
Types
|
||||
*****
|
||||
|
||||
.. autoclass:: simplezfs.types.Dataset
|
||||
:members:
|
||||
|
||||
.. autoclass:: simplezfs.types.Property
|
||||
:members:
|
||||
|
||||
Interfaces
|
||||
**********
|
||||
|
||||
ZFS
|
||||
===
|
||||
|
||||
.. autofunction:: simplezfs.zfs.get_zfs
|
||||
|
||||
.. autoclass:: simplezfs.ZFS
|
||||
:members:
|
||||
|
||||
ZPool
|
||||
=====
|
||||
|
||||
.. autofunction:: simplezfs.zpool.get_zpool
|
||||
|
||||
.. autoclass:: simplezfs.ZPool
|
||||
:members:
|
||||
|
||||
Implementations
|
||||
***************
|
||||
|
||||
.. autoclass:: simplezfs.zfs_cli.ZFSCli
|
||||
:members:
|
||||
|
||||
.. autoclass:: simplezfs.zfs_native.ZFSNative
|
||||
:members:
|
||||
|
||||
.. autoclass:: simplezfs.zpool_cli.ZPoolCli
|
||||
:members:
|
||||
|
||||
.. autoclass:: simplezfs.zpool_native.ZPoolNative
|
||||
:members:
|
||||
|
||||
Validation functions
|
||||
********************
|
||||
A set of validation functions exist to validate names and other data. All of them raise a
|
||||
:class:`simplezfs.exceptions.ValidationError` as a result of a failed validation and return nothing if everything is okay.
|
||||
|
||||
.. autofunction:: simplezfs.validation.validate_dataset_name
|
||||
|
||||
.. autofunction:: simplezfs.validation.validate_dataset_path
|
||||
|
||||
.. autofunction:: simplezfs.validation.validate_native_property_name
|
||||
|
||||
.. autofunction:: simplezfs.validation.validate_metadata_property_name
|
||||
|
||||
.. autofunction:: simplezfs.validation.validate_pool_name
|
||||
|
||||
.. autofunction:: simplezfs.validation.validate_property_value
|
||||
|
||||
Exceptions
|
||||
**********
|
||||
|
||||
.. autoexception:: simplezfs.exceptions.ZFSException
|
||||
|
||||
.. autoexception:: simplezfs.exceptions.DatasetNotFound
|
||||
|
||||
.. autoexception:: simplezfs.exceptions.PermissionError
|
||||
|
||||
.. autoexception:: simplezfs.exceptions.PoolNotFound
|
||||
|
||||
.. autoexception:: simplezfs.exceptions.PropertyNotFound
|
||||
|
||||
.. autoexception:: simplezfs.exceptions.ValidationError
|
@ -0,0 +1,57 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Python-SimpleZFS'
|
||||
copyright = '2019, Andreas Gonschorek, Stefan Valouch'
|
||||
author = 'Andreas Gonschorek, Stefan Valouch'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '0.0.1'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx_autodoc_typehints',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
@ -0,0 +1,310 @@
|
||||
###################
|
||||
Guide to python-zfs
|
||||
###################
|
||||
|
||||
Overview
|
||||
********
|
||||
|
||||
The interfaces are similar to calling the zfs toolset on the command line. That is, there are not state holding classes
|
||||
representing filesets or pools, each call must include the entire dataset path or pool name. There is also no way to
|
||||
collect actions and run them at the end, each action is carried out immediately.
|
||||
|
||||
There are, however, two implementations of the functionality, using the ``cli`` tools and another one using the
|
||||
`libzfs_core` ``native`` library. We'll focus on the ``cli`` version here.
|
||||
|
||||
Most of the functions raise a :class:`:: simplezfs.simplezfs.exceptions.ValidationError` with some helpful text if any of the data turns
|
||||
out not to be valid. For example, including invalid or reserved strings in the dataset name raises this exception.
|
||||
|
||||
Let's take a look at the two interfaces **ZFS** and **ZPool**...
|
||||
|
||||
.. warning:: All of the commands here attempt to modify something in the pool or dataset given as parameters. If run
|
||||
with high enough permission (usually ``root``, but there's ``zfs allow`` that can delegate to lower-
|
||||
privileged users, too) these commands can and will **delete** data! Always run these against pools, disks
|
||||
and datasets that bear no important data! You have been warned.
|
||||
|
||||
The ZFS interface
|
||||
*****************
|
||||
|
||||
The :class:`:: simplezfs.simplezfs.zfs.ZFS` is the interface that corresponds to the ``zfs(8)`` program. It holds very little state,
|
||||
and it is recommended to get an instance through the function :func:`~zfs.zfs.get_zfs`. It selects the desired
|
||||
implementation and passes the required parameters. At the very least, it requires the ``api``-parameter, which is a
|
||||
string that selects the actual implementation, either ``cli`` or ``native``.
|
||||
|
||||
All our examples use the ``cli`` implementation for simplicity.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from zfs import get_zfs
|
||||
>>> zfs = get_zfs('cli')
|
||||
>>> zfs
|
||||
<zfs.zfs_cli.ZFSCli object at 0x7f9f00faa9b0>
|
||||
|
||||
For the remainder of this guide, we're going to assume that the variable ``zfs`` always holds a
|
||||
:class:`:: simplezfs.simplezfs.zfs_cli.ZFSCli` object.
|
||||
|
||||
Viewing data
|
||||
============
|
||||
To get an overview over the interface, we'll dive in and inspect our running system. Information is returned in the
|
||||
form if :class:`:: simplezfs.simplezfs.types.Dataset` instances, which is a named tuple containing a set of fields. For simplicity,
|
||||
we'll output only a few of its fields to not clobber the screen, so don't be alarmed if there seems to be information
|
||||
missing: we just omitted the boring parts.
|
||||
|
||||
Listing datasets
|
||||
----------------
|
||||
By default when listing datasets, all of them are returned regardless of their type. That means that it includes
|
||||
|
||||
* volumes
|
||||
* filesets
|
||||
* snapshots
|
||||
* bookmars
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.list_datasets()
|
||||
<Dataset pool/system/root>
|
||||
<Dataset pool/system/root@pre-distupgrade>
|
||||
<Dataset tank/vol>
|
||||
<Dataset tank/vol@test>
|
||||
|
||||
This is often unneccessary, and it allows to limit both by ``type`` and by including only datasets that are children
|
||||
of another, and both at the same time:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.list_datasets(type=DatasetType.SNAPSHOT)
|
||||
<Dataset pool/root@pre-distupgrade>
|
||||
<Dataset tank/vol@test>
|
||||
>>> zfs.list_datasets(parent='pool/system')
|
||||
<Dataset pool/root>
|
||||
<Dataset pool/root@pre-distupgrade>
|
||||
>>> zfs.list_datasets(parent='pool/system', type=DatasetType.SNAPSHOT)
|
||||
<Dataset pool/root@pre-distupgrade>
|
||||
|
||||
Creating something new
|
||||
======================
|
||||
|
||||
There are functions for creating the four different types of datasets with nice interfaces:
|
||||
|
||||
* :func:`~zfs.ZFS.create_fileset` for ordinary filesets, the most commonly used parameter is ``mountpoint`` for
|
||||
telling it where it should be mounted.
|
||||
* :func:`~zfs.ZFS.create_volume` creates volumes, or ZVols, this features a parameter ``thin`` for creating thin-
|
||||
provisioned or sparse volumes.
|
||||
* :func:`~zfs.ZFS.create_snapshot` creates a snapshot on a volume or fileset.
|
||||
* :func:`~zfs.ZFS.create_bookmark` creates a bookmark (on recent versions of ZFS).
|
||||
|
||||
These essentially call :func:`~zfs.ZFS.create_dataset`, which can be called directly, but its interface is not as
|
||||
nice as the special purpose create functions.
|
||||
|
||||
|
||||
Filesets
|
||||
--------
|
||||
|
||||
Creating a fileset requires the dataset path, like this:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.create_fileset('pool/test', mountpoint='/tmp/test')
|
||||
<Dataset pool/test>
|
||||
|
||||
:todo: add create_dataset
|
||||
|
||||
Volumes
|
||||
-------
|
||||
|
||||
Volumes are created similar to filesets, this example creates a thin-provisioned sparse volume:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.create_volume('pool/vol', thin=True)
|
||||
<Dataset pool/vol>
|
||||
|
||||
:todo: add create_dataset
|
||||
|
||||
Snapshots
|
||||
---------
|
||||
|
||||
Snapshots are, like bookmarks, created on an existing fileset or volume, hence the first parameter to the function is
|
||||
the dataset that is our base, and the second parameter is the name of the snapshot.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.create_snapshot('pool/test', 'pre-distupgrade')
|
||||
<Dataset pool/test@pre-distupgrade>
|
||||
|
||||
Bookmarks
|
||||
---------
|
||||
|
||||
Like snapshots above, bookmarks are created on an existing fileset or volume.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.create_bookmark('pool/test', 'book-20190723')
|
||||
<Dataset pool/test#book-20190723>
|
||||
|
||||
Destroying things
|
||||
=================
|
||||
|
||||
After creating some datasets of various kinds and playing around with some of their properties, it's time to clean up.
|
||||
We'll use the ``destroy_*`` family of methods.
|
||||
|
||||
.. warning:: Bear in mind that things happening here are final and cannot be undone. When playing around, always make
|
||||
sure not to run this on pools containing important data!
|
||||
|
||||
Filesets
|
||||
--------
|
||||
|
||||
Volumes
|
||||
-------
|
||||
|
||||
Snapshots
|
||||
---------
|
||||
|
||||
Bookmarks
|
||||
---------
|
||||
|
||||
Properties
|
||||
==========
|
||||
|
||||
Properties are one of the many cool and useful features of ZFS. They control its behaviour (like ``compression``) or
|
||||
return information about the internal states (like ``creation`` time).
|
||||
|
||||
.. note:: The python library does not validate the names of native properties, as these are subject to change with the
|
||||
ZFS version and it would mean that the library needs an update every time a new ZFS version changes some of
|
||||
these. Thus, it relies on validating the input for syntax based on the ZFS documentation of the ZFS on Linux
|
||||
(ZoL) project and ZFS telling it that it did not like a name.
|
||||
|
||||
A word on metadata/user properties
|
||||
----------------------------------
|
||||
|
||||
The API allows to get and set properties, for both ``native`` properties (the ones defined by ZFS, exposing information
|
||||
or altering how it works) and ``user`` properties that we call **metadata properties** in the API.
|
||||
|
||||
When working with metadata properties, you need to supply a ``namespace`` to distinguish it from a native property.
|
||||
This works by separating the namespace and the property name using a ``:`` character, so a property ``myprop``
|
||||
in the namespace ``com.company.department`` becomes ``com.company.department:myprop`` in the ZFS property system. This
|
||||
is done automatically for you if you supply a ``metadata_namespace`` when creating the ZFS instance and can be
|
||||
overwritten when working with the get and set functions. It is also possible not to define the namespace and passing
|
||||
it to the functions every time.
|
||||
|
||||
When you want to get or set a metadata property, set ``metadata`` to **True** when calling
|
||||
:func:`~zfs.ZFS.get_property` or :func:`~zfs.ZFS.set_property`. This will cause it to automatically prepend the
|
||||
namespace given on instantiation or to prepend the one given in the ``overwrite_metadata_namespace`` when calling the
|
||||
functions. The name of the property **must not** include the namespace, though it may contain ``:`` characters on its
|
||||
own, properties of the form ``zfs:is:cool`` are valid afterall. ``:`` characters are never valid in the context of
|
||||
native properties, and this is the reason why there is a separate switch to turn on metadata properties when using
|
||||
these functions.
|
||||
|
||||
Error handling
|
||||
--------------
|
||||
If a property name is not valid or the value exceeds certain bounds, a :class:`:: simplezfs.simplezfs.exceptions.ValidationError` is
|
||||
raised. This includes specifying a namespace in the property name if ``metadata`` is **False**, or exceeding the
|
||||
length allowed for a metadata property (8192 - 1 bytes).
|
||||
|
||||
Though not an error for the ``zfs(8)`` utility, getting a non-existing metadata property also raises the above
|
||||
exception to indicate that the property does not exist.
|
||||
|
||||
Getting a property
|
||||
------------------
|
||||
|
||||
Getting properties is fairly straight-forward, especially for native properties:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.get_property('tank/system/root', 'mountpoint')
|
||||
Property(key='mountpoint', value='/', source='local', namespace=None)
|
||||
|
||||
For **metadata** properties, one needs to enable their usage by setting ``metadata`` to True. With a globally saved
|
||||
namespace, it looks like this:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs = get_zfs('cli', metadata_namespace='com.company')
|
||||
>>> zfs.get_property('tank/system/root', 'do_backup', metdata=True)
|
||||
Property(key='do_backup', value='true', source='local', namespace='com.company')
|
||||
|
||||
If you don't specify a namespace when calling :func:`~zfs.zfs.get_zfs` or if you want to use a different namespace for
|
||||
one call, specify the desired namespace in ``overwrite_metadata_namespace`` like so:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.get_property('tank/system/root', 'requires', metadata=True, overwrite_metadata_namespace='user')
|
||||
Property(key='requires', value='coffee', source='local', namespace='user')
|
||||
|
||||
This is the equivalent of calling ``zfs get user:requires tank/system/root`` on the shell.
|
||||
|
||||
Asking it to get a native property that does not exist results in an error:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.get_property('tank/system/root', 'notexisting', metadata=False)
|
||||
zfs.exceptions.PropertyNotFound: invalid property on dataset tank/test
|
||||
|
||||
Setting a property
|
||||
------------------
|
||||
|
||||
The interface for setting both native and metadata properties works exactly like the get interface shown earlier,
|
||||
though it obviously needs a value to set. We won't go into ZFS delegation system (``zfs allow``) and assume the
|
||||
following is run using **root** privileges.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.set_property('tank/service/backup', 'mountpoint', ''/backup')
|
||||
|
||||
Setting a metadata property works like this (again, like above):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.set_property('tank/system/root', 'requires', 'tea', metadata=True, overwrite_metadata_namespace='user')
|
||||
|
||||
Listing properties
|
||||
------------------
|
||||
|
||||
:todo: ``zfs.get_properties``
|
||||
|
||||
The ZPool interface
|
||||
*******************
|
||||
|
||||
The :class:`:: simplezfs.simplezfs.zfs.ZPool` is the interface that corresponds to the ``zpool(8)`` program. It holds very little state,
|
||||
and it is recommended to get an instance through the function :func:`~simplezfs.zpool.get_zpool`. It selects the desired
|
||||
implementation and passes the required parameters. At the very least, it requires the ``api``-parameter, which is a
|
||||
string that selects the actual implementation, either ``cli`` or ``native``.
|
||||
|
||||
All our examples use the ``cli`` implementation for simplicity.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from simplezfs import get_zpool
|
||||
>>> zpool = get_zpool('cli')
|
||||
>>> zpool
|
||||
<zfs.zpool_cli.ZPoolCli object at 0x7f67d5254940>
|
||||
|
||||
For the remainder of this guide, we're going to assume that the variable ``zpool`` always holds a
|
||||
:class:`:: simplezfs.simplezfs.zpool_cli.ZPoolCli` object.
|
||||
|
||||
Error handling
|
||||
**************
|
||||
We kept the most important part for last: handling errors. The module defines its own hierarchy with
|
||||
:class:`:: simplezfs.simplezfs.exceptions.ZFSError` as toplevel exception. Various specific exceptions are based on ot. When working
|
||||
with :class:`:: simplezfs.simplezfs.zfs.ZFS`, the three most common ones are:
|
||||
|
||||
* :class:`:: simplezfs.simplezfs.exceptions.ValidationError` which indicates that a name (e.g. dataset name) was invalid.
|
||||
* :class:`:: simplezfs.simplezfs.exceptions.DatasetNotFound` is, like FileNotFound in standard python, indicating that the dataset the
|
||||
module was instructed to work on (e.g. get/set properties, destroy) was not present.
|
||||
* :class:`:: simplezfs.simplezfs.exceptions.PermissionError` is raised when the current users permissions are not sufficient to perform
|
||||
the requested operation. While some actions can be delegated using ``zfs allow``, linux, for example, doesn't allow
|
||||
non-root users to mount filesystems, which means that a non-root user may create filesets with a valid mountpoint
|
||||
property, but it won't be mounted.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.list_dataset(parent=':pool/name/invalid')
|
||||
zfs.exceptions.ValidationError: malformed name
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> zfs.list_datasets(parent='pool/not/existing')
|
||||
zfs.exceptions.DatasetNotFound: Dataset "pool/not/existing" not found
|
||||
|
@ -0,0 +1,31 @@
|
||||
##############################
|
||||
Python-SimpleZFS documentation
|
||||
##############################
|
||||
|
||||
The module implements a simple and straight-forward (hopefully) API for interacting with ZFS. It consists of two main
|
||||
classes :class:`~simplezfs.zfs.ZFS` and :class:`~simplezfs.zpool.ZPool` that can be thought of as wrappers around the
|
||||
ZFS command line utilities ``zfs(8)`` and ``zpool(8)``. This module provides two implementations:
|
||||
|
||||
* The ``cli``-API wrapps the command line utilities
|
||||
* And the ``native``-API uses ``libzfs_core``.
|
||||
|
||||
At the time of writing, the ``native``-API has not been implemented.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
quickstart
|
||||
security
|
||||
guide
|
||||
properties_metadata
|
||||
testing
|
||||
api
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
@ -0,0 +1,7 @@
|
||||
#######################
|
||||
Properties and Metadata
|
||||
#######################
|
||||
|
||||
ZFS features a property system using ``zfs(8) get/set``, which is used to read information about datasets and change their behaviour. Additionally, it allows users to attach arbitrary properties on their own to datasets. ZFS calls these "`user properties`", distinguishing them from its own "`native properties`" by requirim a ``:`` character. This library calls the user properties **metadata** properties.
|
||||
|
||||
|
@ -0,0 +1,54 @@
|
||||
##########
|
||||
Quickstart
|
||||
##########
|
||||
|
||||
Installation
|
||||
************
|
||||
|
||||
From the repo
|
||||
=============
|
||||
|
||||
You can pull the code from the repository at URL using pip:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install git+ssh://URL/python-simplezfs
|
||||
|
||||
|
||||
Interfaces
|
||||
**********
|
||||
For both ``zfs(8)`` and ``zpool(8)`` there exist two interfaces:
|
||||
|
||||
* **cli** calls the ``zfs`` and ``zpool`` binaries in subprocesses
|
||||
* **native** uses ``libzfs_core`` to achieve the same goals
|
||||
|
||||
There exist two functions :func:`~simplezfs.zfs.get_zfs` and :func:`~simplezfs.zpool.get_zpool` that take the implementation as
|
||||
first argument and return the appropriate implementation. The main documentation about how it works are in the api
|
||||
documentation for the interface classes :class:`~simplezfs.zfs.ZFS` and :class:`~simplezfs.zpool.ZPool`.
|
||||
|
||||
This guide focuses on the ``cli`` variants.
|
||||
|
||||
It is not strictly neccessary to get and pass around an instance every time, as the classes hold very little state.
|
||||
They need to do some initialization however, such as finding the binaries (and thus hitting the local filesystem).
|
||||
|
||||
For the rest of the guide, we'll assume that the following has been run and we have a :class:`~simplezfs.zfs.ZFS` instance
|
||||
in ``zfs``:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from simplezfs import get_zfs
|
||||
>>> zfs = get_zfs('cli')
|
||||
>>> zfs
|
||||
<zfs.zfs_cli.ZFSCli object at 0x7f9f00faa9b0>
|
||||
|
||||
As well as a :class:`~simplezfs.zpool.ZPool` instance in ``zpool`` after running the following:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from simplezfs import get_zpool
|
||||
>>> zpool = get_zpool('cli')
|
||||
>>> zpool
|
||||
<zfs.zpool_cli.ZPoolCli object at 0x7f67d5254940>
|
||||
|
||||
To be continued
|
||||
|
@ -0,0 +1,2 @@
|
||||
Sphinx>=2.0
|
||||
sphinx-autodoc-typehints
|
@ -0,0 +1,36 @@
|
||||
########
|
||||
Security
|
||||
########
|
||||
|
||||
The authors of this library tried their best to make sure that nothing bad happens, but they are only human. Thus:
|
||||
|
||||
.. warning:: Use this library at your own risk.
|
||||
|
||||
Running as root
|
||||
***************
|
||||
|
||||
The library goes to great length to make sure everything passed to the operating system is safe. Due to the nature of
|
||||
operating systems, many tasks can only be carried out with elevated privileges, commonly running with the privileges
|
||||
of the user ``root``. This means that care must be taken when using it with elevated privileges!
|
||||
|
||||
Thankfully, ZFS allows to delegate permission to some extend, allowing a user or a group on the local system to carry
|
||||
out administrative tasks. One exception is mounting, which is handled in the next paragraph.
|
||||
|
||||
It is suggested to take a look at the ``zfs(8)`` manpage, especially the part that covers ``zfs allow``.
|
||||
|
||||
.. _the_mount_problem:
|
||||
|
||||
The mount problem
|
||||
*****************
|
||||
On Linux, only root is allowed to manipulate the global namespace. This means that no amount of ``zfs allow`` will
|
||||
allow any other user to mount a fileset. They can be created with the ``mountpoint`` property set, but can't be
|
||||
mounted. One workaround is to specify ``legacy``, and usr ``/etc/fstab`` to mount it, the other is to install and use
|
||||
a special privilege escalation helper.
|
||||
|
||||
There are two places where it is needed to have **root** privileges: Setting or changing the ``mountpoint`` property
|
||||
of a fileset. For this, a helper program is provided as an example. It is intended to be edited by the administrator
|
||||
before use.
|
||||
|
||||
Installed **setuid root**, it allows the caller to mount a fileset if it is below a hardcoded (by the administrator
|
||||
of the system) dataset and targets a hardcoded subtree of the local filesystem hierarchy. The path to that helper has
|
||||
to be passed to the create function for filesets.
|
@ -0,0 +1,194 @@
|
||||
#######
|
||||
Testing
|
||||
#######
|
||||
|
||||
``python-zfs`` uses **pytest** for testing. The test code can be found in the ``tests``-subdirectory of the source
|
||||
tree.
|
||||
|
||||
Preparation
|
||||
===========
|
||||
|
||||
Usually, when running the test suite locally in your shell, it is advised to use a virtual environment. Which one
|
||||
to use is up to the reader. The authors use the ``venv`` module. The requirements can be found in the file
|
||||
``requirements_develop.txt`` or by installing the module with tests.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 -m venv venv
|
||||
$ source venv/bin/activate
|
||||
(venv) $ pip install -e .[tests]
|
||||
|
||||
Running the tests
|
||||
=================
|
||||
Then run the tests using pytest: ``pytest -v --cov``. The test suite will be expanded in the future.
|
||||
|
||||
Creating local pools and datasets
|
||||
=================================
|
||||
|
||||
Pool with most vdev types
|
||||
-------------------------
|
||||
To have a pool with most of the supported vdev types, create a pool from a set of files. The following sequence
|
||||
creates a set of 64MB files (the minimum size ZFS accepts). The new pool is not really usable for storing data,
|
||||
mostly because it is based on sparse files and ZFS does not like that. But it should be usable for most of the actions
|
||||
that one wants to perform on it for testing purposes.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
mkdir /tmp/z
|
||||
for i in {1..12}; do truncate -s 64M /tmp/z/vol$i; done
|
||||
sudo zpool create testpool \
|
||||
raidz /tmp/z/vol1 /tmp/z/vol2 /tmp/z/vol3 \
|
||||
raidz /tmp/z/vol4 /tmp/z/vol5 /tmp/z/vol6 \
|
||||
log mirror /tmp/z/vol7 /tmp/z/vol8 \
|
||||
cache /tmp/z/vol9 /tmp/z/vol10 \
|
||||
spare /tmp/z/vol11 /tmp/z/vol12
|
||||
|
||||
The new pool should now look like this:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
pool: testpool
|
||||
state: ONLINE
|
||||
scan: none requested
|
||||
config:
|
||||
|
||||
NAME STATE READ WRITE CKSUM
|
||||
testpool ONLINE 0 0 0
|
||||
raidz1-0 ONLINE 0 0 0
|
||||
/tmp/z/vol1 ONLINE 0 0 0
|
||||
/tmp/z/vol2 ONLINE 0 0 0
|
||||
/tmp/z/vol3 ONLINE 0 0 0
|
||||
raidz1-1 ONLINE 0 0 0
|
||||
/tmp/z/vol4 ONLINE 0 0 0
|
||||
/tmp/z/vol5 ONLINE 0 0 0
|
||||
/tmp/z/vol6 ONLINE 0 0 0
|
||||
logs
|
||||
mirror-2 ONLINE 0 0 0
|
||||
/tmp/z/vol7 ONLINE 0 0 0
|
||||
/tmp/z/vol8 ONLINE 0 0 0
|
||||
cache
|
||||
/tmp/z/vol9 ONLINE 0 0 0
|
||||
/tmp/z/vol10 ONLINE 0 0 0
|
||||
spares
|
||||
/tmp/z/vol11 AVAIL
|
||||
/tmp/z/vol12 AVAIL
|
||||
|
||||
errors: No known data errors
|
||||
|
||||
For reference, when getting a listing of the pools content, the output should look like this (converted to json using
|
||||
``json.dumps()`` and pretty-printed for readability:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"testpool": {
|
||||
"drives": [
|
||||
{
|
||||
"type": "raidz1",
|
||||
"health": "ONLINE",
|
||||
"size": 184549376,
|
||||
"alloc": 88064,
|
||||
"free": 184461312,
|
||||
"frag": 0,
|
||||
"cap": 0,
|
||||
"members": [
|
||||
{
|
||||
"name": "/tmp/z/vol1",
|
||||
"health": "ONLINE"
|
||||
},
|
||||
{
|
||||
"name": "/tmp/z/vol2",
|
||||
"health": "ONLINE"
|
||||
},
|
||||
{
|
||||
"name": "/tmp/z/vol3",
|
||||
"health": "ONLINE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "raidz1",
|
||||
"health": "ONLINE",
|
||||
"size": 184549376,
|
||||
"alloc": 152576,
|
||||
"free": 184396800,
|
||||
"frag": 0,
|
||||
"cap": 0,
|
||||
"members": [
|
||||
{
|
||||
"name": "/tmp/z/vol4",
|
||||
"health": "ONLINE"
|
||||
},
|
||||
{
|
||||
"name": "/tmp/z/vol5",
|
||||
"health": "ONLINE"
|
||||
},
|
||||
{
|
||||
"name": "/tmp/z/vol6",
|
||||
"health": "ONLINE"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"log": [
|
||||
{
|
||||
"type": "mirror",
|
||||
"size": 50331648,
|
||||
"alloc": 0,
|
||||
"free": 50331648,
|
||||
"frag": 0,
|
||||
"cap": 0,
|
||||
"members": [
|
||||
{
|
||||
"name": "/tmp/z/vol7",
|
||||
"health": "ONLINE"
|
||||
},
|
||||
{
|
||||
"name": "/tmp/z/vol8",
|
||||
"health": "ONLINE"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"cache": [
|
||||
{
|
||||
"type": "none",
|
||||
"members": [
|
||||
{
|
||||
"name": "/tmp/z/vol9",
|
||||
"health": "ONLINE"
|
||||
},
|
||||
{
|
||||
"name": "/tmp/z/vol10",
|
||||
"health": "ONLINE"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"spare": [
|
||||
{
|
||||
"type": "none",
|
||||
"members": [
|
||||
{
|
||||
"name": "/tmp/z/vol11",
|
||||
"health": "AVAIL"
|
||||
},
|
||||
{
|
||||
"name": "/tmp/z/vol12",
|
||||
"health": "AVAIL"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"size": 369098752,
|
||||
"alloc": 240640,
|
||||
"free": 368858112,
|
||||
"chkpoint": null,
|
||||
"expandsz": null,
|
||||
"frag": 0,
|
||||
"cap": 0,
|
||||
"dedup": 1,
|
||||
"health": "ONLINE",
|
||||
"altroot": null
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
pydantic
|
@ -0,0 +1,4 @@
|
||||
flake8
|
||||
mypy
|
||||
pytest
|
||||
pytest-cov
|
@ -0,0 +1,25 @@
|
||||
|
||||
[flake8]
|
||||
ignore = E501,E402
|
||||
max-line-length = 120
|
||||
exclude = .git,.tox,build,_build,env,venv,__pycache__
|
||||
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
python_files =
|
||||
test_*.py
|
||||
*_test.py
|
||||
tests.py
|
||||
addopts =
|
||||
-ra
|
||||
--strict
|
||||
--tb=short
|
||||
|
||||
# potentially dangerous!
|
||||
# --doctest-modules
|
||||
# --doctest-glob=\*.rst
|
||||
|
||||
[coverage:run]
|
||||
omit =
|
||||
venv/*
|
||||
tests/*
|
@ -0,0 +1,47 @@
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
with open('README.rst', 'r') as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(
|
||||
name='simplezfs',
|
||||
version='0.0.1',
|
||||
author='Andreas Gonschorek, Stefan Valouch',
|
||||
description='Simple, low-level ZFS API',
|
||||
long_description=long_description,
|
||||
packages=['simplezfs'],
|
||||
package_data={'simplezfs': ['py.typed']},
|
||||
package_dir={'': 'src'},
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
license='BSD-3-Clause',
|
||||
url='https://github.com/svalouch/python-simplezfs',
|
||||
platforms='any',
|
||||
python_requires='>=3.6',
|
||||
|
||||
install_requires=[
|
||||
'pydantic',
|
||||
],
|
||||
|
||||
extras_require={
|
||||
'tests': [
|
||||
'mypy',
|
||||
'pytest',
|
||||
'pytest-cov',
|
||||
],
|
||||
'docs': [
|
||||
'Sphinx>=2.0',
|
||||
],
|
||||
},
|
||||
|
||||
classifiers=[
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
],
|
||||
)
|
@ -0,0 +1,16 @@
|
||||
|
||||
'''
|
||||
Python ZFS API.
|
||||
'''
|
||||
|
||||
__version__ = '0.0.1'
|
||||
|
||||
from .zfs import ZFS, get_zfs
|
||||
from .zpool import ZPool, get_zpool
|
||||
|
||||
__all__ = [
|
||||
'ZFS',
|
||||
'get_zfs',
|
||||
'ZPool',
|
||||
'get_zpool',
|
||||
]
|
@ -0,0 +1,48 @@
|
||||
|
||||
'''
|
||||
Exceptions
|
||||
'''
|
||||
|
||||
|
||||
class ZFSException(Exception):
|
||||
'''
|
||||
Base for the exception tree used by this library.
|
||||
|
||||
:note: Some functions throw ordinary python base exceptions as well.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class DatasetNotFound(ZFSException):
|
||||
'''
|
||||
A dataset was not found.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class PermissionError(ZFSException):
|
||||
'''
|
||||
Permissions are not sufficient to carry out the task.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class PoolNotFound(ZFSException):
|
||||
'''
|
||||
A pool was not found.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class PropertyNotFound(ZFSException):
|
||||
'''
|
||||
A property was not found.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(ZFSException):
|
||||
'''
|
||||
Indicates that a value failed validation.
|
||||
'''
|
||||
pass
|
@ -0,0 +1,181 @@
|
||||
|
||||
'''
|
||||
Type declarations
|
||||
'''
|
||||
|
||||
from enum import Enum, unique
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
@unique
|
||||
class DatasetType(str, Enum):
|
||||
'''
|
||||
Enumeration of dataset types that ZFS supports.
|
||||
'''
|
||||
#: Dataset is an ordinary fileset
|
||||
FILESET = 'fileset'
|
||||
#: Dataset is a ZVOL
|
||||
VOLUME = 'volume'
|
||||
#: Dataset is a snapshot
|
||||
SNAPSHOT = 'snapshot'
|
||||
#: Dataset is a bookmark
|
||||
BOOKMARK = 'bookmark'
|
||||
|
||||
@staticmethod
|
||||
def from_string(value: str) -> 'DatasetType':
|
||||
'''
|
||||
Helper to convert a string to an instance.
|
||||
|
||||
:param value: The string to converts.
|
||||
:returns: The enum value
|
||||
:raises ValueError: If the supplied value is not found in the enumeration.
|
||||
'''
|
||||
if not isinstance(value, str):
|
||||
raise ValueError('only string types allowed')
|
||||
val = value.lower()
|
||||
if val == 'fileset':
|
||||
return DatasetType.FILESET
|
||||
elif val == 'volume':
|
||||
return DatasetType.VOLUME
|
||||
elif val == 'snapshot':
|
||||
return DatasetType.SNAPSHOT
|
||||
elif val == 'bookmark':
|
||||
return DatasetType.BOOKMARK
|
||||
else:
|
||||
raise ValueError(f'Value {value} is not a valid DatasetType')
|
||||
|
||||
|
||||
class Dataset(NamedTuple):
|
||||
'''
|
||||
Container describing a single dataset.
|
||||
'''
|
||||
#: Name of the dataset (excluding the path)
|
||||
name: str
|
||||
#: Full path to and including the dataset itself
|
||||
full_path: str
|
||||
#: Pool name
|
||||
pool: str
|
||||
#: Parent dataset, or None for the topmost dataset (pool)
|
||||
parent: Optional[str]
|
||||
#: Dataset type
|
||||
type: DatasetType
|
||||
|
||||
|
||||
@unique
|
||||
class PropertySource(str, Enum):
|
||||
'''
|
||||
Enumeration of the valid property sources in ZFS.
|
||||
'''
|
||||
#: Property is at default
|
||||
DEFAULT = 'default'
|
||||
#: Property was inerited
|
||||
INHERITED = 'inherited'
|
||||
#: Property is temporary
|
||||
TEMPORARY = 'temporary'
|
||||
#: Property is set to the value that it had due to the sender of a "zfs send/receive" operation having it set this way.
|
||||
RECEIVED = 'received'
|
||||
#: Property is set on the dataset in question
|
||||
NONE = 'none'
|
||||
|
||||
@staticmethod
|
||||
def from_string(value: str) -> 'PropertySource':
|
||||
'''
|
||||
Helper to convert a string to an instance.
|
||||
|
||||
:param value: The string to convert.
|
||||
:returns: The enum value.
|
||||
:raises ValueError: If the supplied value is not found in the enumeration.
|
||||
'''
|
||||
if not isinstance(value, str):
|
||||
raise ValueError('only string types allowed')
|
||||
val = value.lower()
|
||||
if val == 'default':
|
||||
return PropertySource.DEFAULT
|
||||
elif val == 'inherited':
|
||||
return PropertySource.INHERITED
|
||||
elif val == 'temporary':
|
||||
return PropertySource.TEMPORARY
|
||||
elif val == 'received':
|
||||
return PropertySource.RECEIVED
|
||||
elif val == 'none':
|
||||
return PropertySource.NONE
|
||||
else:
|
||||
raise ValueError(f'Value {value} is not a valid PropertySource')
|
||||
|
||||
|
||||
class Property(NamedTuple):
|
||||
'''
|
||||
Container for a single ZFS property.
|
||||
'''
|
||||
#: Key or name of the property (excluding namespace for non-native properties)
|
||||
key: str
|
||||
#: Value of the property
|
||||
value: str
|
||||
#: Source of the property value
|
||||
source: PropertySource = PropertySource.NONE
|
||||
#: Namespace name of the property, None for native properties
|
||||
namespace: Optional[str] = None
|
||||
|
||||
|
||||
class VDevType(str, Enum):
|
||||
'''
|
||||
Type of a vdev. vdevs are either one of two storages (disk or file) or one of the special parts (mirror, raidz*,
|
||||
spare, log, dedup, special and cache).
|
||||
When reading zpool output, it is not always clear what type a vdev is when it comes to "disk" vs. "file", so a
|
||||
special type "DISK_OR_FILE" is used. This type is invalid when setting values.
|
||||
'''
|
||||
DISK = 'disk'
|
||||
FILE = 'file'
|
||||
DISK_OR_FILE = 'disk_or_file'
|
||||
MIRROR = 'mirror'
|
||||
RAIDZ1 = 'raidz1'
|
||||
RAIDZ2 = 'raidz2'
|
||||
RAIDZ3 = 'raidz3'
|
||||
SPARE = 'spare'
|
||||
LOG = 'log'
|
||||
DEDUP = 'dedup'
|
||||
SPECIAL = 'special'
|
||||
CACHE = 'cache'
|
||||
|
||||
|
||||
class ZPoolHealth(str, Enum):
|
||||
'''
|
||||
|
||||
'''
|
||||
ONLINE = 'ONLINE'
|
||||
DEGRADED = 'DEGRADED'
|
||||
FAULTED = 'FAULTED'
|
||||
OFFLINE = 'OFFLINE'
|
||||
UNAVAIL = 'UNAVAIL'
|
||||
REMOVED = 'REMOVED'
|
||||
#: This one is used for spares
|
||||
AVAIL = 'AVAIL'
|
||||
|
||||
@staticmethod
|
||||
def from_string(value: str) -> 'ZPoolHealth':
|
||||
'''
|
||||
Helper to convert a string to an instance.
|
||||
|
||||
:param value: The string to convert.
|
||||
:returns: The enum value.
|
||||
:raises ValueError: If the supplied value is not found in the enumeration.
|
||||
'''
|
||||
if not isinstance(value, str):
|
||||
raise ValueError('only string types are allowed')
|
||||
val = value.lower()
|
||||
if val == 'online':
|
||||
return ZPoolHealth.ONLINE
|
||||
elif val == 'degraded':
|
||||
return ZPoolHealth.DEGRADED
|
||||
elif val == 'faulted':
|
||||
return ZPoolHealth.FAULTED
|
||||
elif val == 'offline':
|
||||
return ZPoolHealth.OFFLINE
|
||||
elif val == 'unavail':
|
||||
return ZPoolHealth.UNAVAIL
|
||||
elif val == 'removed':
|
||||
return ZPoolHealth.REMOVED
|
||||
elif val == 'avail':
|
||||
return ZPoolHealth.AVAIL
|
||||
else:
|
||||
raise ValueError(f'Value {value} is not a valid ZPoolHealth')
|
@ -0,0 +1,174 @@
|
||||
|
||||
'''
|
||||
Functions for validating data.
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
from .exceptions import ValidationError
|
||||
|
||||
|
||||
#: Maximum length of a dataset (from zfs(8)) in bytes
|
||||
MAXNAMELEN: int = 256
|
||||
#: Maximum length of a native property name (assumed, TODO check in source)
|
||||
NATIVE_PROPERTY_NAME_LEN_MAX: int = MAXNAMELEN
|
||||
#: Maximum length of a metadata property name (assumed, TODO check in source)
|
||||
METADATA_PROPERTY_NAME_LEN_MAX: int = MAXNAMELEN
|
||||
#: Maximum length of a metadata property value in bytes
|
||||
METADATA_PROPERTY_VALUE_LEN_MAX: int = 8192
|
||||
|
||||
#: Regular expression for validating dataset names, handling both the name itself as well as snapshot or bookmark names.
|
||||
DATASET_NAME_RE = re.compile(r'^(?P<dataset>[a-zA-Z0-9_\-.:]+)(?P<detail>(@|#)[a-zA-Z0-9_\-.:]+)?$')
|
||||
#: Regular expression for validating a native property name
|
||||
NATIVE_PROPERTY_NAME_RE = re.compile(r'^[a-z]([a-z0-9]+)?$')
|
||||
#: Regular expression for validating the syntax of a user property (metadata property in python-zfs)
|
||||
METADATA_PROPERTY_NAME_RE = re.compile(r'^([a-z0-9_.]([a-z0-9_.\-]+)?)?:([a-z0-9:_.\-]+)$')
|
||||
|
||||
|
||||
def validate_pool_name(name: str) -> None:
|
||||
'''
|
||||
Validates a pool name.
|
||||
|
||||
:raises ValidationError: Indicates validation failed
|
||||
'''
|
||||
try:
|
||||
b_name = name.encode('utf-8')
|
||||
except UnicodeEncodeError as err:
|
||||
raise ValidationError(f'unicode encoding error: {err}') from err
|
||||
if len(b_name) < 1:
|
||||
raise ValidationError('too short')
|
||||
# TODO find maximum pool name length
|
||||
|
||||
# The pool name must begin with a letter, and can only contain alphanumeric characters as well as underscore
|
||||
# ("_"), dash ("-"), colon (":"), space (" "), and period (".").
|
||||
if not re.match(r'^[a-z]([a-z0-9\-_: .]+)?$', name):
|
||||
raise ValidationError('malformed name')
|
||||
# The pool names mirror, raidz, spare and log are reserved,
|
||||
if name in ['mirror', 'raidz', 'spare', 'log']:
|
||||
raise ValidationError('reserved name')
|
||||
# as are names beginning with mirror, raidz, spare,
|
||||
for word in ['mirror', 'raidz', 'spare']:
|
||||
if name.startswith(word):
|
||||
raise ValidationError(f'starts with invalid token {word}')
|
||||
# and the pattern c[0-9].
|
||||
if re.match(r'^c[0-9]', name):
|
||||
raise ValidationError('begins with reserved sequence c[0-9]')
|
||||
|
||||
|
||||
def validate_dataset_name(name: str, *, strict: bool = False) -> None:
|
||||
'''
|
||||
Validates a dataset name. By default (``strict`` set to **False**) a snapshot (``name@snapshotname``) or bookmark
|
||||
(``name#bookmarkname``) postfix are allowed. Otherwise, these names are rejected. To validate a complete path, see
|
||||
:func:`validate_dataset_path`.
|
||||
|
||||
Example:
|
||||
|
||||
>>> from zfs.validation import validate_dataset_name
|
||||
>>> validate_dataset_name('swap')
|
||||
>>> validate_dataset_name('backup', strict=True)
|
||||
>>> validate_dataset_name('backup@20190525', strict=True)
|
||||
zfs.validation.ValidationError: snapshot or bookmark identifier are not allowed in strict mode
|
||||
>>> validate_dataset_name('')
|
||||
zfs.validation.ValidationError: name is too short
|
||||
>>> validate_dataset_name('pool/system')
|
||||
zfs.validation.ValidationError: name contains disallowed characters
|
||||
>>> validate_dataset_name('a' * 1024) # really long name
|
||||
zfs.validation.ValidationError: length > 255
|
||||
|
||||
:param name: The name to validate
|
||||
:param strict: Whether to allow (``False``) or disallow (``True``) snapshot and bookmark names.
|
||||
:raises ValidationError: Indicates validation failed
|
||||
'''
|
||||
try:
|
||||
b_name = name.encode('utf-8')
|
||||
except UnicodeEncodeError as err:
|
||||
raise ValidationError(f'unicode encoding error: {err}') from err
|
||||
if len(b_name) < 1:
|
||||
raise ValidationError('name is too short')
|
||||
if len(b_name) > MAXNAMELEN - 1:
|
||||
raise ValidationError(f'length > {MAXNAMELEN - 1}')
|
||||
match = DATASET_NAME_RE.match(name)
|
||||
if not match:
|
||||
raise ValidationError('name contains disallowed characters')
|
||||
elif strict and match.group('detail') is not None:
|
||||
raise ValidationError('snapshot or bookmark identifier are not allowed in strict mode')
|
||||
|
||||
|
||||
def validate_dataset_path(path: str) -> None:
|
||||
'''
|
||||
Validates a path of datasets. While :func:`validate_dataset_name` validates only a single entry in a path to a
|
||||
dataset, this function validates the whole path beginning with the pool.
|
||||
|
||||
:raises ValidationError: Indicates validation failed
|
||||
'''
|
||||
try:
|
||||
b_name = path.encode('utf-8')
|
||||
except UnicodeEncodeError as err:
|
||||
raise ValidationError(f'unicode encoding error: {err}') from err
|
||||
if len(b_name) < 3: # a/a is the smallest path
|
||||
raise ValidationError('path is too short')
|
||||
if '/' not in path:
|
||||
raise ValidationError('Not a path')
|
||||
if path.startswith('/'):
|
||||
raise ValidationError('zfs dataset paths are never absolute')
|
||||
|
||||
tokens = path.split('/')
|
||||
# the first token is the pool name
|
||||
validate_pool_name(tokens[0])
|
||||
# second token to second-to-last token are normal datasets that must not be snapshots or bookmarks
|
||||
for dataset in tokens[1:-1]:
|
||||
validate_dataset_name(dataset, strict=True)
|
||||
# last token is the actual dataset, this may be a snapshot or bookmark
|
||||
validate_dataset_name(tokens[-1])
|
||||
|
||||
|
||||
def validate_native_property_name(name: str) -> None:
|
||||
'''
|
||||
Validates the name of a native property. Length and syntax is checked.
|
||||
|
||||
:note: No check is performed to match the name against the actual names the target ZFS version supports.
|
||||
:raises ValidationError: Indicates that the validation failed.
|
||||
'''
|
||||
try:
|
||||
b_name = name.encode('utf-8')
|
||||
except UnicodeEncodeError as err:
|
||||
raise ValidationError(f'unicode encoding error: {err}') from err
|
||||
if len(b_name) < 1:
|
||||
raise ValidationError('name is too short')
|
||||
if len(b_name) > NATIVE_PROPERTY_NAME_LEN_MAX - 1:
|
||||
raise ValidationError(f'length > {NATIVE_PROPERTY_NAME_LEN_MAX - 1}')
|
||||
if not NATIVE_PROPERTY_NAME_RE.match(name):
|
||||
raise ValidationError('property name does not match')
|
||||
|
||||
|
||||
def validate_metadata_property_name(name: str) -> None:
|
||||
'''
|
||||
Validate the name of a metadata property (user property in ZFS manual).
|
||||
|
||||
:raises ValidationError: Indicates that the validation failed.
|
||||
'''
|
||||
try:
|
||||
b_name = name.encode('utf-8')
|
||||
except UnicodeEncodeError as err:
|
||||
raise ValidationError(f'unicode encoding error: {err}') from err
|
||||
if len(b_name) < 1:
|
||||
raise ValidationError('name is too short')
|
||||
if len(b_name) > METADATA_PROPERTY_NAME_LEN_MAX - 1:
|
||||
raise ValidationError(f'length > {METADATA_PROPERTY_NAME_LEN_MAX - 1}')
|
||||
if not METADATA_PROPERTY_NAME_RE.match(name):
|
||||
raise ValidationError('property name does not match')
|
||||
|
||||
def validate_property_value(value: str) -> None:
|
||||
'''
|
||||
Validates the value of a property. This works for both native properties, where the driver will tell us if the
|
||||
value was good or not, as well metadata (or user) properties where the only limit is its length.
|
||||
|
||||
:param value: The value to validate.
|
||||
:raises ValidationError: Indicates that the validation failed.
|
||||
'''
|
||||
try:
|
||||
b_value = value.encode('utf-8')
|
||||
except UnicodeEncodeError as err:
|
||||
raise ValidationError(f'unicode encoding error: {err}') from err
|
||||
if len(b_value) > METADATA_PROPERTY_VALUE_LEN_MAX - 1:
|
||||
raise ValidationError(f'length > {METADATA_PROPERTY_VALUE_LEN_MAX - 1}')
|
@ -0,0 +1,550 @@
|
||||
|
||||
'''
|
||||
ZFS frontend API
|
||||
'''
|
||||
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from .exceptions import (
|
||||
DatasetNotFound,
|
||||
PermissionError,
|
||||
PoolNotFound,
|
||||
PropertyNotFound,
|
||||
ValidationError,
|
||||
)
|
||||
from .types import Dataset, DatasetType, Property
|
||||
from .validation import (
|
||||
validate_dataset_path,
|
||||
validate_metadata_property_name,
|
||||
validate_native_property_name,
|
||||
validate_pool_name,
|
||||
validate_property_value,
|
||||
)
|
||||
|
||||
|
||||
class ZFS:
|
||||
'''
|
||||
ZFS interface class. This API generally covers only the zfs(8) tool, for zpool(8) please see :class:`~ZPool`.
|
||||
|
||||
**ZFS implementation**
|
||||
|
||||
There are two ways how the API actually communicates with the ZFS filesystem:
|
||||
|
||||
* Using the CLI tools (:class:`~zfs.zfs_cli.ZFSCli`)
|
||||
* Using the native API (:class:`~zfs.zfs_native.ZFSNative`)
|
||||
|
||||
You can select the API by either creating an instance of one of them or using :func:`~zfs.zfs.get_zfs`.
|
||||
|
||||
|
||||
**Properties and Metadata**
|
||||
|
||||
The functions :func:`set_property`, :func:`get_property` and :func:`get_properties` wraps the ZFS get/set
|
||||
functionality. To support so-alled `user properties`, which are called `metadata` in this API, a default namespace
|
||||
can be stored using `metadata_namespace` when instantiating the interface or by calling :func:`set_metadata_namespace`
|
||||
at any time.
|
||||
|
||||
:note: Not setting a metadata namespace means that one can't set or get metadata properties, unless the overwrite
|
||||
parameter for the get/set functions is used.
|
||||
|
||||
:param api: API to use, either ``cli`` for zfs(8) or ``native`` for the `libzfs_core` api.
|
||||
:param metadata_namespace: Default namespace
|
||||
:param kwargs: Extra arguments TODO
|
||||
'''
|
||||
def __init__(self, *, metadata_namespace: Optional[str] = None, **kwargs) -> None:
|
||||
self.metadata_namespace = metadata_namespace
|
||||
|
||||
@property
|
||||
def metadata_namespace(self) -> Optional[str]:
|
||||
'''
|
||||
Returns the metadata namespace, which may be None if not set.
|
||||
'''
|
||||
return self._metadata_namespace
|
||||
|
||||
@metadata_namespace.setter
|
||||
def metadata_namespace(self, namespace: str) -> None:
|
||||
'''
|
||||
Sets a new metadata namespace
|
||||
|
||||
:todo: validate!
|
||||
'''
|
||||
self._metadata_namespace = namespace
|
||||
|
||||
def dataset_exists(self, name: str ) -> bool:
|
||||
'''
|
||||
Checks is a dataset exists. This is done by querying for its `type` property.
|
||||
|
||||
:param name: Name of the dataset to check for.
|
||||
:return: Whether the dataset exists.
|
||||
'''
|
||||
try:
|
||||
return self.get_property(name, 'type') is not None
|
||||
except (DatasetNotFound, PermissionError, PoolNotFound):
|
||||
return |