diff options
-rw-r--r-- | contrib/pyzfs/LICENSE | 201 | ||||
-rw-r--r-- | contrib/pyzfs/README | 28 | ||||
-rw-r--r-- | contrib/pyzfs/docs/source/conf.py | 304 | ||||
-rw-r--r-- | contrib/pyzfs/docs/source/index.rst | 44 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/__init__.py | 100 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/_constants.py | 10 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/_error_translation.py | 629 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/_libzfs_core.py | 1270 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/_nvlist.py | 259 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/bindings/__init__.py | 45 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/bindings/libnvpair.py | 117 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/bindings/libzfs_core.py | 99 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/ctypes.py | 56 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/exceptions.py | 443 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/test/__init__.py | 0 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/test/test_libzfs_core.py | 3708 | ||||
-rw-r--r-- | contrib/pyzfs/libzfs_core/test/test_nvlist.py | 612 | ||||
-rw-r--r-- | contrib/pyzfs/setup.py | 40 |
18 files changed, 7965 insertions, 0 deletions
diff --git a/contrib/pyzfs/LICENSE b/contrib/pyzfs/LICENSE new file mode 100644 index 000000000..370c9bc6f --- /dev/null +++ b/contrib/pyzfs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015 ClusterHQ + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/contrib/pyzfs/README b/contrib/pyzfs/README new file mode 100644 index 000000000..bb3a7f0ff --- /dev/null +++ b/contrib/pyzfs/README @@ -0,0 +1,28 @@ +This package provides a wrapper for libzfs_core C library. + +libzfs_core is intended to be a stable interface for programmatic +administration of ZFS. +This wrapper provides one-to-one wrappers for libzfs_core API functions, +but the signatures and types are more natural to Python. +nvlists are wrapped as dictionaries or lists depending on their usage. +Some parameters have default values depending on typical use for +increased convenience. +Enumerations and bit flags become strings and lists of strings in Python. +Errors are reported as exceptions rather than integer errno-style +error codes. The wrapper takes care to provide one-to-many mapping +of the error codes to the exceptions by interpreting a context +in which the error code is produced. + +Unit tests and automated test for the libzfs_core API are provided +with this package. +Please note that the API tests perform lots of ZFS dataset level +operations and ZFS tries hard to ensure that any modifications +do reach stable storage. That means that the operations are done +synchronously and that, for example, disk caches are flushed. +Thus, the tests can be very slow on real hardware. +It is recommended to place the default temporary directory or +a temporary directory specified by, for instance, TMP environment +variable on a memory backed filesystem. + +Package documentation: http://pyzfs.readthedocs.org +Package development: https://github.com/ClusterHQ/pyzfs diff --git a/contrib/pyzfs/docs/source/conf.py b/contrib/pyzfs/docs/source/conf.py new file mode 100644 index 000000000..511c9b2bc --- /dev/null +++ b/contrib/pyzfs/docs/source/conf.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- +# +# pyzfs documentation build configuration file, created by +# sphinx-quickstart on Mon Apr 6 23:48:40 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import shlex + +# 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. +sys.path.insert(0, os.path.abspath('../..')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# 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', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'pyzfs' +copyright = u'2015, ClusterHQ' +author = u'ClusterHQ' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.2.3' +# The full version, including alpha/beta/rc tags. +release = '0.2.3' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- 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 = 'classic' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# 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'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pyzfsdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'pyzfs.tex', u'pyzfs Documentation', + u'ClusterHQ', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pyzfs', u'pyzfs Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'pyzfs', u'pyzfs Documentation', + author, 'pyzfs', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + +# Sort documentation in the same order as the source files. +autodoc_member_order = 'bysource' + + +####################### +# Neutralize effects of function wrapping on documented signatures. +# The affected signatures could be explcitly placed into the +# documentation (either in .rst files or as a first line of a +# docstring). +import functools + +def no_op_wraps(func): + def wrapper(decorator): + return func + return wrapper + +functools.wraps = no_op_wraps diff --git a/contrib/pyzfs/docs/source/index.rst b/contrib/pyzfs/docs/source/index.rst new file mode 100644 index 000000000..36c227a49 --- /dev/null +++ b/contrib/pyzfs/docs/source/index.rst @@ -0,0 +1,44 @@ +.. pyzfs documentation master file, created by + sphinx-quickstart on Mon Apr 6 23:48:40 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to pyzfs's documentation! +================================= + +Contents: + +.. toctree:: + :maxdepth: 2 + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +Documentation for the libzfs_core +********************************* + +.. automodule:: libzfs_core + :members: + :exclude-members: lzc_snap, lzc_recv, lzc_destroy_one, + lzc_inherit, lzc_set_props, lzc_list + +Documentation for the libzfs_core exceptions +******************************************** + +.. automodule:: libzfs_core.exceptions + :members: + :undoc-members: + +Documentation for the miscellaneous types that correspond to specific width C types +*********************************************************************************** + +.. automodule:: libzfs_core.ctypes + :members: + :undoc-members: + diff --git a/contrib/pyzfs/libzfs_core/__init__.py b/contrib/pyzfs/libzfs_core/__init__.py new file mode 100644 index 000000000..60e0c2514 --- /dev/null +++ b/contrib/pyzfs/libzfs_core/__init__.py @@ -0,0 +1,100 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. +''' +Python wrappers for **libzfs_core** library. + +*libzfs_core* is intended to be a stable, committed interface for programmatic +administration of ZFS. +This wrapper provides one-to-one wrappers for libzfs_core API functions, +but the signatures and types are more natural to Python. +nvlists are wrapped as dictionaries or lists depending on their usage. +Some parameters have default values depending on typical use for +increased convenience. +Output parameters are not used and return values are directly returned. +Enumerations and bit flags become strings and lists of strings in Python. +Errors are reported as exceptions rather than integer errno-style +error codes. The wrapper takes care to provide one-to-many mapping +of the error codes to the exceptions by interpreting a context +in which the error code is produced. + +To submit an issue or contribute to development of this package +please visit its `GitHub repository <https://github.com/ClusterHQ/pyzfs>`_. + +.. data:: MAXNAMELEN + + Maximum length of any ZFS name. +''' + +from ._constants import ( + MAXNAMELEN, +) + +from ._libzfs_core import ( + lzc_create, + lzc_clone, + lzc_rollback, + lzc_rollback_to, + lzc_snapshot, + lzc_snap, + lzc_destroy_snaps, + lzc_bookmark, + lzc_get_bookmarks, + lzc_destroy_bookmarks, + lzc_snaprange_space, + lzc_hold, + lzc_release, + lzc_get_holds, + lzc_send, + lzc_send_space, + lzc_receive, + lzc_receive_with_header, + lzc_recv, + lzc_exists, + is_supported, + lzc_promote, + lzc_rename, + lzc_destroy, + lzc_inherit_prop, + lzc_set_prop, + lzc_get_props, + lzc_list_children, + lzc_list_snaps, + receive_header, +) + +__all__ = [ + 'ctypes', + 'exceptions', + 'MAXNAMELEN', + 'lzc_create', + 'lzc_clone', + 'lzc_rollback', + 'lzc_rollback_to', + 'lzc_snapshot', + 'lzc_snap', + 'lzc_destroy_snaps', + 'lzc_bookmark', + 'lzc_get_bookmarks', + 'lzc_destroy_bookmarks', + 'lzc_snaprange_space', + 'lzc_hold', + 'lzc_release', + 'lzc_get_holds', + 'lzc_send', + 'lzc_send_space', + 'lzc_receive', + 'lzc_receive_with_header', + 'lzc_recv', + 'lzc_exists', + 'is_supported', + 'lzc_promote', + 'lzc_rename', + 'lzc_destroy', + 'lzc_inherit_prop', + 'lzc_set_prop', + 'lzc_get_props', + 'lzc_list_children', + 'lzc_list_snaps', + 'receive_header', +] + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/_constants.py b/contrib/pyzfs/libzfs_core/_constants.py new file mode 100644 index 000000000..45016b431 --- /dev/null +++ b/contrib/pyzfs/libzfs_core/_constants.py @@ -0,0 +1,10 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +Important `libzfs_core` constants. +""" + +#: Maximum length of any ZFS name. +MAXNAMELEN = 255 + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/_error_translation.py b/contrib/pyzfs/libzfs_core/_error_translation.py new file mode 100644 index 000000000..64ce870ab --- /dev/null +++ b/contrib/pyzfs/libzfs_core/_error_translation.py @@ -0,0 +1,629 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +Helper routines for converting ``errno`` style error codes from C functions +to Python exceptions defined by `libzfs_core` API. + +The conversion heavily depends on the context of the error: the attempted +operation and the input parameters. For this reason, there is a conversion +routine for each `libzfs_core` interface function. The conversion routines +have the return code as a parameter as well as all the parameters of the +corresponding interface functions. + +The parameters and exceptions are documented in the `libzfs_core` interfaces. +""" + +import errno +import re +import string +from . import exceptions as lzc_exc +from ._constants import MAXNAMELEN + + +def lzc_create_translate_error(ret, name, ds_type, props): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_name(name) + raise lzc_exc.PropertyInvalid(name) + + if ret == errno.EEXIST: + raise lzc_exc.FilesystemExists(name) + if ret == errno.ENOENT: + raise lzc_exc.ParentNotFound(name) + raise _generic_exception(ret, name, "Failed to create filesystem") + + +def lzc_clone_translate_error(ret, name, origin, props): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_name(name) + _validate_snap_name(origin) + if _pool_name(name) != _pool_name(origin): + raise lzc_exc.PoolsDiffer(name) # see https://www.illumos.org/issues/5824 + else: + raise lzc_exc.PropertyInvalid(name) + + if ret == errno.EEXIST: + raise lzc_exc.FilesystemExists(name) + if ret == errno.ENOENT: + if not _is_valid_snap_name(origin): + raise lzc_exc.SnapshotNameInvalid(origin) + raise lzc_exc.DatasetNotFound(name) + raise _generic_exception(ret, name, "Failed to create clone") + + +def lzc_rollback_translate_error(ret, name): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_name(name) + raise lzc_exc.SnapshotNotFound(name) + if ret == errno.ENOENT: + if not _is_valid_fs_name(name): + raise lzc_exc.NameInvalid(name) + else: + raise lzc_exc.FilesystemNotFound(name) + raise _generic_exception(ret, name, "Failed to rollback") + +def lzc_rollback_to_translate_error(ret, name, snap): + if ret == 0: + return + if ret == errno.EEXIST: + raise lzc_exc.SnapshotNotLatest(snap) + raise _generic_exception(ret, name, "Failed to rollback") + +def lzc_snapshot_translate_errors(ret, errlist, snaps, props): + if ret == 0: + return + + def _map(ret, name): + if ret == errno.EXDEV: + pool_names = map(_pool_name, snaps) + same_pool = all(x == pool_names[0] for x in pool_names) + if same_pool: + return lzc_exc.DuplicateSnapshots(name) + else: + return lzc_exc.PoolsDiffer(name) + elif ret == errno.EINVAL: + if any(not _is_valid_snap_name(s) for s in snaps): + return lzc_exc.NameInvalid(name) + elif any(len(s) > MAXNAMELEN for s in snaps): + return lzc_exc.NameTooLong(name) + else: + return lzc_exc.PropertyInvalid(name) + + if ret == errno.EEXIST: + return lzc_exc.SnapshotExists(name) + if ret == errno.ENOENT: + return lzc_exc.FilesystemNotFound(name) + return _generic_exception(ret, name, "Failed to create snapshot") + + _handle_err_list(ret, errlist, snaps, lzc_exc.SnapshotFailure, _map) + + +def lzc_destroy_snaps_translate_errors(ret, errlist, snaps, defer): + if ret == 0: + return + + def _map(ret, name): + if ret == errno.EEXIST: + return lzc_exc.SnapshotIsCloned(name) + if ret == errno.ENOENT: + return lzc_exc.PoolNotFound(name) + if ret == errno.EBUSY: + return lzc_exc.SnapshotIsHeld(name) + return _generic_exception(ret, name, "Failed to destroy snapshot") + + _handle_err_list(ret, errlist, snaps, lzc_exc.SnapshotDestructionFailure, _map) + + +def lzc_bookmark_translate_errors(ret, errlist, bookmarks): + if ret == 0: + return + + def _map(ret, name): + if ret == errno.EINVAL: + if name: + snap = bookmarks[name] + pool_names = map(_pool_name, bookmarks.keys()) + if not _is_valid_bmark_name(name): + return lzc_exc.BookmarkNameInvalid(name) + elif not _is_valid_snap_name(snap): + return lzc_exc.SnapshotNameInvalid(snap) + elif _fs_name(name) != _fs_name(snap): + return lzc_exc.BookmarkMismatch(name) + elif any(x != _pool_name(name) for x in pool_names): + return lzc_exc.PoolsDiffer(name) + else: + invalid_names = [b for b in bookmarks.keys() if not _is_valid_bmark_name(b)] + if invalid_names: + return lzc_exc.BookmarkNameInvalid(invalid_names[0]) + if ret == errno.EEXIST: + return lzc_exc.BookmarkExists(name) + if ret == errno.ENOENT: + return lzc_exc.SnapshotNotFound(name) + if ret == errno.ENOTSUP: + return lzc_exc.BookmarkNotSupported(name) + return _generic_exception(ret, name, "Failed to create bookmark") + + _handle_err_list(ret, errlist, bookmarks.keys(), lzc_exc.BookmarkFailure, _map) + + +def lzc_get_bookmarks_translate_error(ret, fsname, props): + if ret == 0: + return + if ret == errno.ENOENT: + raise lzc_exc.FilesystemNotFound(fsname) + raise _generic_exception(ret, fsname, "Failed to list bookmarks") + + +def lzc_destroy_bookmarks_translate_errors(ret, errlist, bookmarks): + if ret == 0: + return + + def _map(ret, name): + if ret == errno.EINVAL: + return lzc_exc.NameInvalid(name) + return _generic_exception(ret, name, "Failed to destroy bookmark") + + _handle_err_list(ret, errlist, bookmarks, lzc_exc.BookmarkDestructionFailure, _map) + + +def lzc_snaprange_space_translate_error(ret, firstsnap, lastsnap): + if ret == 0: + return + if ret == errno.EXDEV and firstsnap is not None: + if _pool_name(firstsnap) != _pool_name(lastsnap): + raise lzc_exc.PoolsDiffer(lastsnap) + else: + raise lzc_exc.SnapshotMismatch(lastsnap) + if ret == errno.EINVAL: + if not _is_valid_snap_name(firstsnap): + raise lzc_exc.NameInvalid(firstsnap) + elif not _is_valid_snap_name(lastsnap): + raise lzc_exc.NameInvalid(lastsnap) + elif len(firstsnap) > MAXNAMELEN: + raise lzc_exc.NameTooLong(firstsnap) + elif len(lastsnap) > MAXNAMELEN: + raise lzc_exc.NameTooLong(lastsnap) + elif _pool_name(firstsnap) != _pool_name(lastsnap): + raise lzc_exc.PoolsDiffer(lastsnap) + else: + raise lzc_exc.SnapshotMismatch(lastsnap) + if ret == errno.ENOENT: + raise lzc_exc.SnapshotNotFound(lastsnap) + raise _generic_exception(ret, lastsnap, "Failed to calculate space used by range of snapshots") + + +def lzc_hold_translate_errors(ret, errlist, holds, fd): + if ret == 0: + return + + def _map(ret, name): + if ret == errno.EXDEV: + return lzc_exc.PoolsDiffer(name) + elif ret == errno.EINVAL: + if name: + pool_names = map(_pool_name, holds.keys()) + if not _is_valid_snap_name(name): + return lzc_exc.NameInvalid(name) + elif len(name) > MAXNAMELEN: + return lzc_exc.NameTooLong(name) + elif any(x != _pool_name(name) for x in pool_names): + return lzc_exc.PoolsDiffer(name) + else: + invalid_names = [b for b in holds.keys() if not _is_valid_snap_name(b)] + if invalid_names: + return lzc_exc.NameInvalid(invalid_names[0]) + fs_name = None + hold_name = None + pool_name = None + if name is not None: + fs_name = _fs_name(name) + pool_name = _pool_name(name) + hold_name = holds[name] + if ret == errno.ENOENT: + return lzc_exc.FilesystemNotFound(fs_name) + if ret == errno.EEXIST: + return lzc_exc.HoldExists(name) + if ret == errno.E2BIG: + return lzc_exc.NameTooLong(hold_name) + if ret == errno.ENOTSUP: + return lzc_exc.FeatureNotSupported(pool_name) + return _generic_exception(ret, name, "Failed to hold snapshot") + + if ret == errno.EBADF: + raise lzc_exc.BadHoldCleanupFD() + _handle_err_list(ret, errlist, holds.keys(), lzc_exc.HoldFailure, _map) + + +def lzc_release_translate_errors(ret, errlist, holds): + if ret == 0: + return + for _, hold_list in holds.iteritems(): + if not isinstance(hold_list, list): + raise lzc_exc.TypeError('holds must be in a list') + + def _map(ret, name): + if ret == errno.EXDEV: + return lzc_exc.PoolsDiffer(name) + elif ret == errno.EINVAL: + if name: + pool_names = map(_pool_name, holds.keys()) + if not _is_valid_snap_name(name): + return lzc_exc.NameInvalid(name) + elif len(name) > MAXNAMELEN: + return lzc_exc.NameTooLong(name) + elif any(x != _pool_name(name) for x in pool_names): + return lzc_exc.PoolsDiffer(name) + else: + invalid_names = [b for b in holds.keys() if not _is_valid_snap_name(b)] + if invalid_names: + return lzc_exc.NameInvalid(invalid_names[0]) + elif ret == errno.ENOENT: + return lzc_exc.HoldNotFound(name) + elif ret == errno.E2BIG: + tag_list = holds[name] + too_long_tags = [t for t in tag_list if len(t) > MAXNAMELEN] + return lzc_exc.NameTooLong(too_long_tags[0]) + elif ret == errno.ENOTSUP: + pool_name = None + if name is not None: + pool_name = _pool_name(name) + return lzc_exc.FeatureNotSupported(pool_name) + else: + return _generic_exception(ret, name, "Failed to release snapshot hold") + + _handle_err_list(ret, errlist, holds.keys(), lzc_exc.HoldReleaseFailure, _map) + + +def lzc_get_holds_translate_error(ret, snapname): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_snap_name(snapname) + if ret == errno.ENOENT: + raise lzc_exc.SnapshotNotFound(snapname) + if ret == errno.ENOTSUP: + raise lzc_exc.FeatureNotSupported(_pool_name(snapname)) + raise _generic_exception(ret, snapname, "Failed to get holds on snapshot") + + +def lzc_send_translate_error(ret, snapname, fromsnap, fd, flags): + if ret == 0: + return + if ret == errno.EXDEV and fromsnap is not None: + if _pool_name(fromsnap) != _pool_name(snapname): + raise lzc_exc.PoolsDiffer(snapname) + else: + raise lzc_exc.SnapshotMismatch(snapname) + elif ret == errno.EINVAL: + if (fromsnap is not None and not _is_valid_snap_name(fromsnap) and + not _is_valid_bmark_name(fromsnap)): + raise lzc_exc.NameInvalid(fromsnap) + elif not _is_valid_snap_name(snapname) and not _is_valid_fs_name(snapname): + raise lzc_exc.NameInvalid(snapname) + elif fromsnap is not None and len(fromsnap) > MAXNAMELEN: + raise lzc_exc.NameTooLong(fromsnap) + elif len(snapname) > MAXNAMELEN: + raise lzc_exc.NameTooLong(snapname) + elif fromsnap is not None and _pool_name(fromsnap) != _pool_name(snapname): + raise lzc_exc.PoolsDiffer(snapname) + elif ret == errno.ENOENT: + if (fromsnap is not None and not _is_valid_snap_name(fromsnap) and + not _is_valid_bmark_name(fromsnap)): + raise lzc_exc.NameInvalid(fromsnap) + raise lzc_exc.SnapshotNotFound(snapname) + elif ret == errno.ENAMETOOLONG: + if fromsnap is not None and len(fromsnap) > MAXNAMELEN: + raise lzc_exc.NameTooLong(fromsnap) + else: + raise lzc_exc.NameTooLong(snapname) + raise lzc_exc.StreamIOError(ret) + + +def lzc_send_space_translate_error(ret, snapname, fromsnap): + if ret == 0: + return + if ret == errno.EXDEV and fromsnap is not None: + if _pool_name(fromsnap) != _pool_name(snapname): + raise lzc_exc.PoolsDiffer(snapname) + else: + raise lzc_exc.SnapshotMismatch(snapname) + elif ret == errno.EINVAL: + if fromsnap is not None and not _is_valid_snap_name(fromsnap): + raise lzc_exc.NameInvalid(fromsnap) + elif not _is_valid_snap_name(snapname): + raise lzc_exc.NameInvalid(snapname) + elif fromsnap is not None and len(fromsnap) > MAXNAMELEN: + raise lzc_exc.NameTooLong(fromsnap) + elif len(snapname) > MAXNAMELEN: + raise lzc_exc.NameTooLong(snapname) + elif fromsnap is not None and _pool_name(fromsnap) != _pool_name(snapname): + raise lzc_exc.PoolsDiffer(snapname) + elif ret == errno.ENOENT and fromsnap is not None: + if not _is_valid_snap_name(fromsnap): + raise lzc_exc.NameInvalid(fromsnap) + if ret == errno.ENOENT: + raise lzc_exc.SnapshotNotFound(snapname) + raise _generic_exception(ret, snapname, "Failed to estimate backup stream size") + + +def lzc_receive_translate_error(ret, snapname, fd, force, origin, props): + if ret == 0: + return + if ret == errno.EINVAL: + if not _is_valid_snap_name(snapname) and not _is_valid_fs_name(snapname): + raise lzc_exc.NameInvalid(snapname) + elif len(snapname) > MAXNAMELEN: + raise lzc_exc.NameTooLong(snapname) + elif origin is not None and not _is_valid_snap_name(origin): + raise lzc_exc.NameInvalid(origin) + else: + raise lzc_exc.BadStream() + if ret == errno.ENOENT: + if not _is_valid_snap_name(snapname): + raise lzc_exc.NameInvalid(snapname) + else: + raise lzc_exc.DatasetNotFound(snapname) + if ret == errno.EEXIST: + raise lzc_exc.DatasetExists(snapname) + if ret == errno.ENOTSUP: + raise lzc_exc.StreamFeatureNotSupported() + if ret == errno.ENODEV: + raise lzc_exc.StreamMismatch(_fs_name(snapname)) + if ret == errno.ETXTBSY: + raise lzc_exc.DestinationModified(_fs_name(snapname)) + if ret == errno.EBUSY: + raise lzc_exc.DatasetBusy(_fs_name(snapname)) + if ret == errno.ENOSPC: + raise lzc_exc.NoSpace(_fs_name(snapname)) + if ret == errno.EDQUOT: + raise lzc_exc.QuotaExceeded(_fs_name(snapname)) + if ret == errno.ENAMETOOLONG: + raise lzc_exc.NameTooLong(snapname) + if ret == errno.EROFS: + raise lzc_exc.ReadOnlyPool(_pool_name(snapname)) + if ret == errno.EAGAIN: + raise lzc_exc.SuspendedPool(_pool_name(snapname)) + + raise lzc_exc.StreamIOError(ret) + + +def lzc_promote_translate_error(ret, name): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_name(name) + raise lzc_exc.NotClone(name) + if ret == errno.ENOTSOCK: + raise lzc_exc.NotClone(name) + if ret == errno.ENOENT: + raise lzc_exc.FilesystemNotFound(name) + if ret == errno.EEXIST: + raise lzc_exc.SnapshotExists(name) + raise _generic_exception(ret, name, "Failed to promote dataset") + + +def lzc_rename_translate_error(ret, source, target): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_name(source) + _validate_fs_name(target) + if _pool_name(source) != _pool_name(target): + raise lzc_exc.PoolsDiffer(source) + if ret == errno.EEXIST: + raise lzc_exc.FilesystemExists(target) + if ret == errno.ENOENT: + raise lzc_exc.FilesystemNotFound(source) + raise _generic_exception(ret, source, "Failed to rename dataset") + + +def lzc_destroy_translate_error(ret, name): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_name(name) + if ret == errno.ENOENT: + raise lzc_exc.FilesystemNotFound(name) + raise _generic_exception(ret, name, "Failed to destroy dataset") + + +def lzc_inherit_prop_translate_error(ret, name, prop): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_name(name) + raise lzc_exc.PropertyInvalid(prop) + if ret == errno.ENOENT: + raise lzc_exc.DatasetNotFound(name) + raise _generic_exception(ret, name, "Failed to inherit a property") + + +def lzc_set_prop_translate_error(ret, name, prop, val): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_or_snap_name(name) + raise lzc_exc.PropertyInvalid(prop) + if ret == errno.ENOENT: + raise lzc_exc.DatasetNotFound(name) + raise _generic_exception(ret, name, "Failed to set a property") + + +def lzc_get_props_translate_error(ret, name): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_or_snap_name(name) + if ret == errno.ENOENT: + raise lzc_exc.DatasetNotFound(name) + raise _generic_exception(ret, name, "Failed to get properties") + + +def lzc_list_children_translate_error(ret, name): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_name(name) + raise _generic_exception(ret, name, "Error while iterating children") + + +def lzc_list_snaps_translate_error(ret, name): + if ret == 0: + return + if ret == errno.EINVAL: + _validate_fs_name(name) + raise _generic_exception(ret, name, "Error while iterating snapshots") + + +def lzc_list_translate_error(ret, name, opts): + if ret == 0: + return + if ret == errno.ENOENT: + raise lzc_exc.DatasetNotFound(name) + if ret == errno.EINVAL: + _validate_fs_or_snap_name(name) + raise _generic_exception(ret, name, "Error obtaining a list") + + +def _handle_err_list(ret, errlist, names, exception, mapper): + ''' + Convert one or more errors from an operation into the requested exception. + + :param int ret: the overall return code. + :param errlist: the dictionary that maps entity names to their specific error codes. + :type errlist: dict of bytes:int + :param names: the list of all names of the entities on which the operation was attempted. + :param type exception: the type of the exception to raise if an error occurred. + The exception should be a subclass of `MultipleOperationsFailure`. + :param function mapper: the function that maps an error code and a name to a Python exception. + + Unless ``ret`` is zero this function will raise the ``exception``. + If the ``errlist`` is not empty, then the compound exception will contain a list of exceptions + corresponding to each individual error code in the ``errlist``. + Otherwise, the ``exception`` will contain a list with a single exception corresponding to the + ``ret`` value. If the ``names`` list contains only one element, that is, the operation was + attempted on a single entity, then the name of that entity is passed to the ``mapper``. + If the operation was attempted on multiple entities, but the ``errlist`` is empty, then we + can not know which entity caused the error and, thus, ``None`` is used as a name to signify + thati fact. + + .. note:: + Note that the ``errlist`` can contain a special element with a key of "N_MORE_ERRORS". + That element means that there were too many errors to place on the ``errlist``. + Those errors are suppressed and only their count is provided as a value of the special + ``N_MORE_ERRORS`` element. + ''' + if ret == 0: + return + + if len(errlist) == 0: + suppressed_count = 0 + if len(names) == 1: + name = names[0] + else: + name = None + errors = [mapper(ret, name)] + else: + errors = [] + suppressed_count = errlist.pop('N_MORE_ERRORS', 0) + for name, err in errlist.iteritems(): + errors.append(mapper(err, name)) + + raise exception(errors, suppressed_count) + + +def _pool_name(name): + ''' + Extract a pool name from the given dataset or bookmark name. + + '/' separates dataset name components. + '@' separates a snapshot name from the rest of the dataset name. + '#' separates a bookmark name from the rest of the dataset name. + ''' + return re.split('[/@#]', name, 1)[0] + + +def _fs_name(name): + ''' + Extract a dataset name from the given snapshot or bookmark name. + + '@' separates a snapshot name from the rest of the dataset name. + '#' separates a bookmark name from the rest of the dataset name. + ''' + return re.split('[@#]', name, 1)[0] + + +def _is_valid_name_component(component): + allowed = string.ascii_letters + string.digits + '-_.: ' + return component and all(x in allowed for x in component) + + +def _is_valid_fs_name(name): + return name and all(_is_valid_name_component(c) for c in name.split('/')) + + +def _is_valid_snap_name(name): + parts = name.split('@') + return (len(parts) == 2 and _is_valid_fs_name(parts[0]) and + _is_valid_name_component(parts[1])) + + +def _is_valid_bmark_name(name): + parts = name.split('#') + return (len(parts) == 2 and _is_valid_fs_name(parts[0]) and + _is_valid_name_component(parts[1])) + + +def _validate_fs_name(name): + if not _is_valid_fs_name(name): + raise lzc_exc.FilesystemNameInvalid(name) + elif len(name) > MAXNAMELEN: + raise lzc_exc.NameTooLong(name) + + +def _validate_snap_name(name): + if not _is_valid_snap_name(name): + raise lzc_exc.SnapshotNameInvalid(name) + elif len(name) > MAXNAMELEN: + raise lzc_exc.NameTooLong(name) + + +def _validate_bmark_name(name): + if not _is_valid_bmark_name(name): + raise lzc_exc.BookmarkNameInvalid(name) + elif len(name) > MAXNAMELEN: + raise lzc_exc.NameTooLong(name) + + +def _validate_fs_or_snap_name(name): + if not _is_valid_fs_name(name) and not _is_valid_snap_name(name): + raise lzc_exc.NameInvalid(name) + elif len(name) > MAXNAMELEN: + raise lzc_exc.NameTooLong(name) + + +def _generic_exception(err, name, message): + if err in _error_to_exception: + return _error_to_exception[err](name) + else: + return lzc_exc.ZFSGenericError(err, message, name) + +_error_to_exception = {e.errno: e for e in [ + lzc_exc.ZIOError, + lzc_exc.NoSpace, + lzc_exc.QuotaExceeded, + lzc_exc.DatasetBusy, + lzc_exc.NameTooLong, + lzc_exc.ReadOnlyPool, + lzc_exc.SuspendedPool, + lzc_exc.PoolsDiffer, + lzc_exc.PropertyNotSupported, +]} + + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/_libzfs_core.py b/contrib/pyzfs/libzfs_core/_libzfs_core.py new file mode 100644 index 000000000..00824f5f6 --- /dev/null +++ b/contrib/pyzfs/libzfs_core/_libzfs_core.py @@ -0,0 +1,1270 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +Python wrappers for libzfs_core interfaces. + +As a rule, there is a Python function for each C function. +The signatures of the Python functions generally follow those of the +functions, but the argument types are natural to Python. +nvlists are wrapped as dictionaries or lists depending on their usage. +Some parameters have default values depending on typical use for +increased convenience. Output parameters are not used and return values +are directly returned. Error conditions are signalled by exceptions +rather than by integer error codes. +""" + +import errno +import functools +import fcntl +import os +import struct +import threading +from . import exceptions +from . import _error_translation as errors +from .bindings import libzfs_core +from ._constants import MAXNAMELEN +from .ctypes import int32_t +from ._nvlist import nvlist_in, nvlist_out + + +def lzc_create(name, ds_type='zfs', props=None): + ''' + Create a ZFS filesystem or a ZFS volume ("zvol"). + + :param bytes name: a name of the dataset to be created. + :param str ds_type: the type of the dataset to be create, currently supported + types are "zfs" (the default) for a filesystem + and "zvol" for a volume. + :param props: a `dict` of ZFS dataset property name-value pairs (empty by default). + :type props: dict of bytes:Any + + :raises FilesystemExists: if a dataset with the given name already exists. + :raises ParentNotFound: if a parent dataset of the requested dataset does not exist. + :raises PropertyInvalid: if one or more of the specified properties is invalid + or has an invalid type or value. + :raises NameInvalid: if the name is not a valid dataset name. + :raises NameTooLong: if the name is too long. + ''' + if props is None: + props = {} + if ds_type == 'zfs': + ds_type = _lib.DMU_OST_ZFS + elif ds_type == 'zvol': + ds_type = _lib.DMU_OST_ZVOL + else: + raise exceptions.DatasetTypeInvalid(ds_type) + nvlist = nvlist_in(props) + ret = _lib.lzc_create(name, ds_type, nvlist) + errors.lzc_create_translate_error(ret, name, ds_type, props) + + +def lzc_clone(name, origin, props=None): + ''' + Clone a ZFS filesystem or a ZFS volume ("zvol") from a given snapshot. + + :param bytes name: a name of the dataset to be created. + :param bytes origin: a name of the origin snapshot. + :param props: a `dict` of ZFS dataset property name-value pairs (empty by default). + :type props: dict of bytes:Any + + :raises FilesystemExists: if a dataset with the given name already exists. + :raises DatasetNotFound: if either a parent dataset of the requested dataset + or the origin snapshot does not exist. + :raises PropertyInvalid: if one or more of the specified properties is invalid + or has an invalid type or value. + :raises FilesystemNameInvalid: if the name is not a valid dataset name. + :raises SnapshotNameInvalid: if the origin is not a valid snapshot name. + :raises NameTooLong: if the name or the origin name is too long. + :raises PoolsDiffer: if the clone and the origin have different pool names. + + .. note:: + Because of a deficiency of the underlying C interface + :exc:`.DatasetNotFound` can mean that either a parent filesystem of the target + or the origin snapshot does not exist. + It is currently impossible to distinguish between the cases. + :func:`lzc_hold` can be used to check that the snapshot exists and ensure that + it is not destroyed before cloning. + ''' + if props is None: + props = {} + nvlist = nvlist_in(props) + ret = _lib.lzc_clone(name, origin, nvlist) + errors.lzc_clone_translate_error(ret, name, origin, props) + + +def lzc_rollback(name): + ''' + Roll back a filesystem or volume to its most recent snapshot. + + Note that the latest snapshot may change if a new one is concurrently + created or the current one is destroyed. lzc_rollback_to can be used + to roll back to a specific latest snapshot. + + :param bytes name: a name of the dataset to be rolled back. + :return: a name of the most recent snapshot. + :rtype: bytes + + :raises FilesystemNotFound: if the dataset does not exist. + :raises SnapshotNotFound: if the dataset does not have any snapshots. + :raises NameInvalid: if the dataset name is invalid. + :raises NameTooLong: if the dataset name is too long. + ''' + # Account for terminating NUL in C strings. + snapnamep = _ffi.new('char[]', MAXNAMELEN + 1) + ret = _lib.lzc_rollback(name, snapnamep, MAXNAMELEN + 1) + errors.lzc_rollback_translate_error(ret, name) + return _ffi.string(snapnamep) + +def lzc_rollback_to(name, snap): + ''' + Roll back this filesystem or volume to the specified snapshot, if possible. + + :param bytes name: a name of the dataset to be rolled back. + :param bytes snap: a name of the snapshot to be rolled back. + + :raises FilesystemNotFound: if the dataset does not exist. + :raises SnapshotNotFound: if the dataset does not have any snapshots. + :raises NameInvalid: if the dataset name is invalid. + :raises NameTooLong: if the dataset name is too long. + :raises SnapshotNotLatest: if the snapshot is not the latest. + ''' + ret = _lib.lzc_rollback_to(name, snap) + errors.lzc_rollback_to_translate_error(ret, name, snap) + +def lzc_snapshot(snaps, props=None): + ''' + Create snapshots. + + All snapshots must be in the same pool. + + Optionally snapshot properties can be set on all snapshots. + Currently only user properties (prefixed with "user:") are supported. + + Either all snapshots are successfully created or none are created if + an exception is raised. + + :param snaps: a list of names of snapshots to be created. + :type snaps: list of bytes + :param props: a `dict` of ZFS dataset property name-value pairs (empty by default). + :type props: dict of bytes:bytes + + :raises SnapshotFailure: if one or more snapshots could not be created. + + .. note:: + :exc:`.SnapshotFailure` is a compound exception that provides at least + one detailed error object in :attr:`SnapshotFailure.errors` `list`. + + .. warning:: + The underlying implementation reports an individual, per-snapshot error + only for :exc:`.SnapshotExists` condition and *sometimes* for + :exc:`.NameTooLong`. + In all other cases a single error is reported without connection to any + specific snapshot name(s). + + This has the following implications: + + * if multiple error conditions are encountered only one of them is reported + + * unless only one snapshot is requested then it is impossible to tell + how many snapshots are problematic and what they are + + * only if there are no other error conditions :exc:`.SnapshotExists` + is reported for all affected snapshots + + * :exc:`.NameTooLong` can behave either in the same way as + :exc:`.SnapshotExists` or as all other exceptions. + The former is the case where the full snapshot name exceeds the maximum + allowed length but the short snapshot name (after '@') is within + the limit. + The latter is the case when the short name alone exceeds the maximum + allowed length. + ''' + snaps_dict = {name: None for name in snaps} + errlist = {} + snaps_nvlist = nvlist_in(snaps_dict) + if props is None: + props = {} + props_nvlist = nvlist_in(props) + with nvlist_out(errlist) as errlist_nvlist: + ret = _lib.lzc_snapshot(snaps_nvlist, props_nvlist, errlist_nvlist) + errors.lzc_snapshot_translate_errors(ret, errlist, snaps, props) + + +lzc_snap = lzc_snapshot + + +def lzc_destroy_snaps(snaps, defer): + ''' + Destroy snapshots. + + They must all be in the same pool. + Snapshots that do not exist will be silently ignored. + + If 'defer' is not set, and a snapshot has user holds or clones, the + destroy operation will fail and none of the snapshots will be + destroyed. + + If 'defer' is set, and a snapshot has user holds or clones, it will be + marked for deferred destruction, and will be destroyed when the last hold + or clone is removed/destroyed. + + The operation succeeds if all snapshots were destroyed (or marked for + later destruction if 'defer' is set) or didn't exist to begin with. + + :param snaps: a list of names of snapshots to be destroyed. + :type snaps: list of bytes + :param bool defer: whether to mark busy snapshots for deferred destruction + rather than immediately failing. + + :raises SnapshotDestructionFailure: if one or more snapshots could not be created. + + .. note:: + :exc:`.SnapshotDestructionFailure` is a compound exception that provides at least + one detailed error object in :attr:`SnapshotDestructionFailure.errors` `list`. + + Typical error is :exc:`SnapshotIsCloned` if `defer` is `False`. + The snapshot names are validated quite loosely and invalid names are typically + ignored as nonexisiting snapshots. + + A snapshot name referring to a filesystem that doesn't exist is ignored. + However, non-existent pool name causes :exc:`PoolNotFound`. + ''' + snaps_dict = {name: None for name in snaps} + errlist = {} + snaps_nvlist = nvlist_in(snaps_dict) + with nvlist_out(errlist) as errlist_nvlist: + ret = _lib.lzc_destroy_snaps(snaps_nvlist, defer, errlist_nvlist) + errors.lzc_destroy_snaps_translate_errors(ret, errlist, snaps, defer) + + +def lzc_bookmark(bookmarks): + ''' + Create bookmarks. + + :param bookmarks: a dict that maps names of wanted bookmarks to names of existing snapshots. + :type bookmarks: dict of bytes to bytes + + :raises BookmarkFailure: if any of the bookmarks can not be created for any reason. + + The bookmarks `dict` maps from name of the bookmark (e.g. :file:`{pool}/{fs}#{bmark}`) to + the name of the snapshot (e.g. :file:`{pool}/{fs}@{snap}`). All the bookmarks and + snapshots must be in the same pool. + ''' + errlist = {} + nvlist = nvlist_in(bookmarks) + with nvlist_out(errlist) as errlist_nvlist: + ret = _lib.lzc_bookmark(nvlist, errlist_nvlist) + errors.lzc_bookmark_translate_errors(ret, errlist, bookmarks) + + +def lzc_get_bookmarks(fsname, props=None): + ''' + Retrieve a listing of bookmarks for the given file system. + + :param bytes fsname: a name of the filesystem. + :param props: a `list` of properties that will be returned for each bookmark. + :type props: list of bytes + :return: a `dict` that maps the bookmarks' short names to their properties. + :rtype: dict of bytes:dict + + :raises FilesystemNotFound: if the filesystem is not found. + + The following are valid properties on bookmarks: + + guid : integer + globally unique identifier of the snapshot the bookmark refers to + createtxg : integer + txg when the snapshot the bookmark refers to was created + creation : integer + timestamp when the snapshot the bookmark refers to was created + + Any other properties passed in ``props`` are ignored without reporting + any error. + Values in the returned dictionary map the names of the requested properties + to their respective values. + ''' + bmarks = {} + if props is None: + props = [] + props_dict = {name: None for name in props} + nvlist = nvlist_in(props_dict) + with nvlist_out(bmarks) as bmarks_nvlist: + ret = _lib.lzc_get_bookmarks(fsname, nvlist, bmarks_nvlist) + errors.lzc_get_bookmarks_translate_error(ret, fsname, props) + return bmarks + + +def lzc_destroy_bookmarks(bookmarks): + ''' + Destroy bookmarks. + + :param bookmarks: a list of the bookmarks to be destroyed. + The bookmarks are specified as :file:`{fs}#{bmark}`. + :type bookmarks: list of bytes + + :raises BookmarkDestructionFailure: if any of the bookmarks may not be destroyed. + + The bookmarks must all be in the same pool. + Bookmarks that do not exist will be silently ignored. + This also includes the case where the filesystem component of the bookmark + name does not exist. + However, an invalid bookmark name will cause :exc:`.NameInvalid` error + reported in :attr:`SnapshotDestructionFailure.errors`. + + Either all bookmarks that existed are destroyed or an exception is raised. + ''' + errlist = {} + bmarks_dict = {name: None for name in bookmarks} + nvlist = nvlist_in(bmarks_dict) + with nvlist_out(errlist) as errlist_nvlist: + ret = _lib.lzc_destroy_bookmarks(nvlist, errlist_nvlist) + errors.lzc_destroy_bookmarks_translate_errors(ret, errlist, bookmarks) + + +def lzc_snaprange_space(firstsnap, lastsnap): + ''' + Calculate a size of data referenced by snapshots in the inclusive range between + the ``firstsnap`` and the ``lastsnap`` and not shared with any other datasets. + + :param bytes firstsnap: the name of the first snapshot in the range. + :param bytes lastsnap: the name of the last snapshot in the range. + :return: the calculated stream size, in bytes. + :rtype: `int` or `long` + + :raises SnapshotNotFound: if either of the snapshots does not exist. + :raises NameInvalid: if the name of either snapshot is invalid. + :raises NameTooLong: if the name of either snapshot is too long. + :raises SnapshotMismatch: if ``fromsnap`` is not an ancestor snapshot of ``snapname``. + :raises PoolsDiffer: if the snapshots belong to different pools. + + ``lzc_snaprange_space`` calculates total size of blocks that exist + because they are referenced only by one or more snapshots in the given range + but no other dataset. + In other words, this is the set of blocks that were born after the snap before + firstsnap, and died before the snap after the last snap. + Yet another interpretation is that the result of ``lzc_snaprange_space`` is the size + of the space that would be freed if the snapshots in the range are destroyed. + + If the same snapshot is given as both the ``firstsnap`` and the ``lastsnap``. + In that case ``lzc_snaprange_space`` calculates space used by the snapshot. + ''' + valp = _ffi.new('uint64_t *') + ret = _lib.lzc_snaprange_space(firstsnap, lastsnap, valp) + errors.lzc_snaprange_space_translate_error(ret, firstsnap, lastsnap) + return int(valp[0]) + + +def lzc_hold(holds, fd=None): + ''' + Create *user holds* on snapshots. If there is a hold on a snapshot, + the snapshot can not be destroyed. (However, it can be marked for deletion + by :func:`lzc_destroy_snaps` ( ``defer`` = `True` ).) + + :param holds: the dictionary of names of the snapshots to hold mapped to the hold names. + :type holds: dict of bytes : bytes + :type fd: int or None + :param fd: if not None then it must be the result of :func:`os.open` called as ``os.open("/dev/zfs", O_EXCL)``. + :type fd: int or None + :return: a list of the snapshots that do not exist. + :rtype: list of bytes + + :raises HoldFailure: if a hold was impossible on one or more of the snapshots. + :raises BadHoldCleanupFD: if ``fd`` is not a valid file descriptor associated with :file:`/dev/zfs`. + + The snapshots must all be in the same pool. + + If ``fd`` is not None, then when the ``fd`` is closed (including on process + termination), the holds will be released. If the system is shut down + uncleanly, the holds will be released when the pool is next opened + or imported. + + Holds for snapshots which don't exist will be skipped and have an entry + added to the return value, but will not cause an overall failure. + No exceptions is raised if all holds, for snapshots that existed, were succesfully created. + Otherwise :exc:`.HoldFailure` exception is raised and no holds will be created. + :attr:`.HoldFailure.errors` may contain a single element for an error that is not + specific to any hold / snapshot, or it may contain one or more elements + detailing specific error per each affected hold. + ''' + errlist = {} + if fd is None: + fd = -1 + nvlist = nvlist_in(holds) + with nvlist_out(errlist) as errlist_nvlist: + ret = _lib.lzc_hold(nvlist, fd, errlist_nvlist) + errors.lzc_hold_translate_errors(ret, errlist, holds, fd) + # If there is no error (no exception raised by _handleErrList), but errlist + # is not empty, then it contains missing snapshots. + assert all(x == errno.ENOENT for x in errlist.itervalues()) + return errlist.keys() + + +def lzc_release(holds): + ''' + Release *user holds* on snapshots. + + If the snapshot has been marked for + deferred destroy (by lzc_destroy_snaps(defer=B_TRUE)), it does not have + any clones, and all the user holds are removed, then the snapshot will be + destroyed. + + The snapshots must all be in the same pool. + + :param holds: a ``dict`` where keys are snapshot names and values are + lists of hold tags to remove. + :type holds: dict of bytes : list of bytes + :return: a list of any snapshots that do not exist and of any tags that do not + exist for existing snapshots. + Such tags are qualified with a corresponding snapshot name + using the following format :file:`{pool}/{fs}@{snap}#{tag}` + :rtype: list of bytes + + :raises HoldReleaseFailure: if one or more existing holds could not be released. + + Holds which failed to release because they didn't exist will have an entry + added to errlist, but will not cause an overall failure. + + This call is success if ``holds`` was empty or all holds that + existed, were successfully removed. + Otherwise an exception will be raised. + ''' + errlist = {} + holds_dict = {} + for snap, hold_list in holds.iteritems(): + if not isinstance(hold_list, list): + raise TypeError('holds must be in a list') + holds_dict[snap] = {hold: None for hold in hold_list} + nvlist = nvlist_in(holds_dict) + with nvlist_out(errlist) as errlist_nvlist: + ret = _lib.lzc_release(nvlist, errlist_nvlist) + errors.lzc_release_translate_errors(ret, errlist, holds) + # If there is no error (no exception raised by _handleErrList), but errlist + # is not empty, then it contains missing snapshots and tags. + assert all(x == errno.ENOENT for x in errlist.itervalues()) + return errlist.keys() + + +def lzc_get_holds(snapname): + ''' + Retrieve list of *user holds* on the specified snapshot. + + :param bytes snapname: the name of the snapshot. + :return: holds on the snapshot along with their creation times + in seconds since the epoch + :rtype: dict of bytes : int + ''' + holds = {} + with nvlist_out(holds) as nvlist: + ret = _lib.lzc_get_holds(snapname, nvlist) + errors.lzc_get_holds_translate_error(ret, snapname) + return holds + + +def lzc_send(snapname, fromsnap, fd, flags=None): + ''' + Generate a zfs send stream for the specified snapshot and write it to + the specified file descriptor. + + :param bytes snapname: the name of the snapshot to send. + :param fromsnap: if not None the name of the starting snapshot + for the incremental stream. + :type fromsnap: bytes or None + :param int fd: the file descriptor to write the send stream to. + :param flags: the flags that control what enhanced features can be used + in the stream. + :type flags: list of bytes + + :raises SnapshotNotFound: if either the starting snapshot is not `None` and does not exist, + or if the ending snapshot does not exist. + :raises NameInvalid: if the name of either snapshot is invalid. + :raises NameTooLong: if the name of either snapshot is too long. + :raises SnapshotMismatch: if ``fromsnap`` is not an ancestor snapshot of ``snapname``. + :raises PoolsDiffer: if the snapshots belong to different pools. + :raises IOError: if an input / output error occurs while writing to ``fd``. + :raises UnknownStreamFeature: if the ``flags`` contain an unknown flag name. + + If ``fromsnap`` is None, a full (non-incremental) stream will be sent. + If ``fromsnap`` is not None, it must be the full name of a snapshot or + bookmark to send an incremental from, e.g. :file:`{pool}/{fs}@{earlier_snap}` + or :file:`{pool}/{fs}#{earlier_bmark}`. + + The specified snapshot or bookmark must represent an earlier point in the history + of ``snapname``. + It can be an earlier snapshot in the same filesystem or zvol as ``snapname``, + or it can be the origin of ``snapname``'s filesystem, or an earlier + snapshot in the origin, etc. + ``fromsnap`` must be strictly an earlier snapshot, specifying the same snapshot + as both ``fromsnap`` and ``snapname`` is an error. + + If ``flags`` contains *"large_blocks"*, the stream is permitted + to contain ``DRR_WRITE`` records with ``drr_length`` > 128K, and ``DRR_OBJECT`` + records with ``drr_blksz`` > 128K. + + If ``flags`` contains *"embedded_data"*, the stream is permitted + to contain ``DRR_WRITE_EMBEDDED`` records with + ``drr_etype`` == ``BP_EMBEDDED_TYPE_DATA``, + which the receiving system must support (as indicated by support + for the *embedded_data* feature). + + .. note:: + ``lzc_send`` can actually accept a filesystem name as the ``snapname``. + In that case ``lzc_send`` acts as if a temporary snapshot was created + after the start of the call and before the stream starts being produced. + + .. note:: + ``lzc_send`` does not return until all of the stream is written to ``fd``. + + .. note:: + ``lzc_send`` does *not* close ``fd`` upon returning. + ''' + if fromsnap is not None: + c_fromsnap = fromsnap + else: + c_fromsnap = _ffi.NULL + c_flags = 0 + if flags is None: + flags = [] + for flag in flags: + c_flag = { + 'embedded_data': _lib.LZC_SEND_FLAG_EMBED_DATA, + 'large_blocks': _lib.LZC_SEND_FLAG_LARGE_BLOCK, + }.get(flag) + if c_flag is None: + raise exceptions.UnknownStreamFeature(flag) + c_flags |= c_flag + + ret = _lib.lzc_send(snapname, c_fromsnap, fd, c_flags) + errors.lzc_send_translate_error(ret, snapname, fromsnap, fd, flags) + + +def lzc_send_space(snapname, fromsnap=None, flags=None): + ''' + Estimate size of a full or incremental backup stream + given the optional starting snapshot and the ending snapshot. + + :param bytes snapname: the name of the snapshot for which the estimate should be done. + :param fromsnap: the optional starting snapshot name. + If not `None` then an incremental stream size is estimated, + otherwise a full stream is esimated. + :type fromsnap: `bytes` or `None` + :param flags: the flags that control what enhanced features can be used + in the stream. + :type flags: list of bytes + + :return: the estimated stream size, in bytes. + :rtype: `int` or `long` + + :raises SnapshotNotFound: if either the starting snapshot is not `None` and does not exist, + or if the ending snapshot does not exist. + :raises NameInvalid: if the name of either snapshot is invalid. + :raises NameTooLong: if the name of either snapshot is too long. + :raises SnapshotMismatch: if ``fromsnap`` is not an ancestor snapshot of ``snapname``. + :raises PoolsDiffer: if the snapshots belong to different pools. + + ``fromsnap``, if not ``None``, must be strictly an earlier snapshot, + specifying the same snapshot as both ``fromsnap`` and ``snapname`` is an error. + ''' + if fromsnap is not None: + c_fromsnap = fromsnap + else: + c_fromsnap = _ffi.NULL + c_flags = 0 + if flags is None: + flags = [] + for flag in flags: + c_flag = { + 'embedded_data': _lib.LZC_SEND_FLAG_EMBED_DATA, + 'large_blocks': _lib.LZC_SEND_FLAG_LARGE_BLOCK, + }.get(flag) + if c_flag is None: + raise exceptions.UnknownStreamFeature(flag) + c_flags |= c_flag + valp = _ffi.new('uint64_t *') + + ret = _lib.lzc_send_space(snapname, c_fromsnap, c_flags, valp) + errors.lzc_send_space_translate_error(ret, snapname, fromsnap) + return int(valp[0]) + + +def lzc_receive(snapname, fd, force=False, raw=False, origin=None, props=None): + ''' + Receive from the specified ``fd``, creating the specified snapshot. + + :param bytes snapname: the name of the snapshot to create. + :param int fd: the file descriptor from which to read the stream. + :param bool force: whether to roll back or destroy the target filesystem + if that is required to receive the stream. + :param bool raw: whether this is a "raw" stream. + :param origin: the optional origin snapshot name if the stream is for a clone. + :type origin: bytes or None + :param props: the properties to set on the snapshot as *received* properties. + :type props: dict of bytes : Any + + :raises IOError: if an input / output error occurs while reading from the ``fd``. + :raises DatasetExists: if the snapshot named ``snapname`` already exists. + :raises DatasetExists: if the stream is a full stream and the destination filesystem already exists. + :raises DatasetExists: if ``force`` is `True` but the destination filesystem could not + be rolled back to a matching snapshot because a newer snapshot + exists and it is an origin of a cloned filesystem. + :raises StreamMismatch: if an incremental stream is received and the latest + snapshot of the destination filesystem does not match + the source snapshot of the stream. + :raises StreamMismatch: if a full stream is received and the destination + filesystem already exists and it has at least one snapshot, + and ``force`` is `False`. + :raises StreamMismatch: if an incremental clone stream is received but the specified + ``origin`` is not the actual received origin. + :raises DestinationModified: if an incremental stream is received and the destination + filesystem has been modified since the last snapshot + and ``force`` is `False`. + :raises DestinationModified: if a full stream is received and the destination + filesystem already exists and it does not have any + snapshots, and ``force`` is `False`. + :raises DatasetNotFound: if the destination filesystem and its parent do not exist. + :raises DatasetNotFound: if the ``origin`` is not `None` and does not exist. + :raises DatasetBusy: if ``force`` is `True` but the destination filesystem could not + be rolled back to a matching snapshot because a newer snapshot + is held and could not be destroyed. + :raises DatasetBusy: if another receive operation is being performed on the + destination filesystem. + :raises BadStream: if the stream is corrupt or it is not recognized or it is + a compound stream or it is a clone stream, but ``origin`` + is `None`. + :raises BadStream: if a clone stream is received and the destination filesystem + already exists. + :raises StreamFeatureNotSupported: if the stream has a feature that is not + supported on this side. + :raises PropertyInvalid: if one or more of the specified properties is invalid + or has an invalid type or value. + :raises NameInvalid: if the name of either snapshot is invalid. + :raises NameTooLong: if the name of either snapshot is too long. + + .. note:: + The ``origin`` is ignored if the actual stream is an incremental stream + that is not a clone stream and the destination filesystem exists. + If the stream is a full stream and the destination filesystem does not + exist then the ``origin`` is checked for existence: if it does not exist + :exc:`.DatasetNotFound` is raised, otherwise :exc:`.StreamMismatch` is + raised, because that snapshot can not have any relation to the stream. + + .. note:: + If ``force`` is `True` and the stream is incremental then the destination + filesystem is rolled back to a matching source snapshot if necessary. + Intermediate snapshots are destroyed in that case. + + However, none of the existing snapshots may have the same name as + ``snapname`` even if such a snapshot were to be destroyed. + The existing ``snapname`` snapshot always causes :exc:`.SnapshotExists` + to be raised. + + If ``force`` is `True` and the stream is a full stream then the destination + filesystem is replaced with the received filesystem unless the former + has any snapshots. This prevents the destination filesystem from being + rolled back / replaced. + + .. note:: + This interface does not work on dedup'd streams + (those with ``DMU_BACKUP_FEATURE_DEDUP``). + + .. note:: + ``lzc_receive`` does not return until all of the stream is read from ``fd`` + and applied to the pool. + + .. note:: + ``lzc_receive`` does *not* close ``fd`` upon returning. + ''' + + if origin is not None: + c_origin = origin + else: + c_origin = _ffi.NULL + if props is None: + props = {} + nvlist = nvlist_in(props) + ret = _lib.lzc_receive(snapname, nvlist, c_origin, force, raw, fd) + errors.lzc_receive_translate_error(ret, snapname, fd, force, origin, props) + + +lzc_recv = lzc_receive + + +def lzc_receive_with_header(snapname, fd, header, force=False, origin=None, props=None): + ''' + Like :func:`lzc_receive`, but allows the caller to read the begin record + and then to pass it in. + + That could be useful if the caller wants to derive, for example, + the snapname or the origin parameters based on the information contained in + the begin record. + :func:`receive_header` can be used to receive the begin record from the file + descriptor. + + :param bytes snapname: the name of the snapshot to create. + :param int fd: the file descriptor from which to read the stream. + :param header: the stream's begin header. + :type header: ``cffi`` `CData` representing the header structure. + :param bool force: whether to roll back or destroy the target filesystem + if that is required to receive the stream. + :param origin: the optional origin snapshot name if the stream is for a clone. + :type origin: bytes or None + :param props: the properties to set on the snapshot as *received* properties. + :type props: dict of bytes : Any + + :raises IOError: if an input / output error occurs while reading from the ``fd``. + :raises DatasetExists: if the snapshot named ``snapname`` already exists. + :raises DatasetExists: if the stream is a full stream and the destination filesystem already exists. + :raises DatasetExists: if ``force`` is `True` but the destination filesystem could not + be rolled back to a matching snapshot because a newer snapshot + exists and it is an origin of a cloned filesystem. + :raises StreamMismatch: if an incremental stream is received and the latest + snapshot of the destination filesystem does not match + the source snapshot of the stream. + :raises StreamMismatch: if a full stream is received and the destination + filesystem already exists and it has at least one snapshot, + and ``force`` is `False`. + :raises StreamMismatch: if an incremental clone stream is received but the specified + ``origin`` is not the actual received origin. + :raises DestinationModified: if an incremental stream is received and the destination + filesystem has been modified since the last snapshot + and ``force`` is `False`. + :raises DestinationModified: if a full stream is received and the destination + filesystem already exists and it does not have any + snapshots, and ``force`` is `False`. + :raises DatasetNotFound: if the destination filesystem and its parent do not exist. + :raises DatasetNotFound: if the ``origin`` is not `None` and does not exist. + :raises DatasetBusy: if ``force`` is `True` but the destination filesystem could not + be rolled back to a matching snapshot because a newer snapshot + is held and could not be destroyed. + :raises DatasetBusy: if another receive operation is being performed on the + destination filesystem. + :raises BadStream: if the stream is corrupt or it is not recognized or it is + a compound stream or it is a clone stream, but ``origin`` + is `None`. + :raises BadStream: if a clone stream is received and the destination filesystem + already exists. + :raises StreamFeatureNotSupported: if the stream has a feature that is not + supported on this side. + :raises PropertyInvalid: if one or more of the specified properties is invalid + or has an invalid type or value. + :raises NameInvalid: if the name of either snapshot is invalid. + :raises NameTooLong: if the name of either snapshot is too long. + ''' + + if origin is not None: + c_origin = origin + else: + c_origin = _ffi.NULL + if props is None: + props = {} + nvlist = nvlist_in(props) + ret = _lib.lzc_receive_with_header(snapname, nvlist, c_origin, force, + False, fd, header) + errors.lzc_receive_translate_error(ret, snapname, fd, force, origin, props) + + +def receive_header(fd): + ''' + Read the begin record of the ZFS backup stream from the given file descriptor. + + This is a helper function for :func:`lzc_receive_with_header`. + + :param int fd: the file descriptor from which to read the stream. + :return: a tuple with two elements where the first one is a Python `dict` representing + the fields of the begin record and the second one is an opaque object + suitable for passing to :func:`lzc_receive_with_header`. + :raises IOError: if an input / output error occurs while reading from the ``fd``. + + At present the following fields can be of interest in the header: + + drr_toname : bytes + the name of the snapshot for which the stream has been created + drr_toguid : integer + the GUID of the snapshot for which the stream has been created + drr_fromguid : integer + the GUID of the starting snapshot in the case the stream is incremental, + zero otherwise + drr_flags : integer + the flags describing the stream's properties + drr_type : integer + the type of the dataset for which the stream has been created + (volume, filesystem) + ''' + # read sizeof(dmu_replay_record_t) bytes directly into the memort backing 'record' + record = _ffi.new("dmu_replay_record_t *") + _ffi.buffer(record)[:] = os.read(fd, _ffi.sizeof(record[0])) + # get drr_begin member and its representation as a Pythn dict + drr_begin = record.drr_u.drr_begin + header = {} + for field, descr in _ffi.typeof(drr_begin).fields: + if descr.type.kind == 'primitive': + header[field] = getattr(drr_begin, field) + elif descr.type.kind == 'enum': + header[field] = getattr(drr_begin, field) + elif descr.type.kind == 'array' and descr.type.item.cname == 'char': + header[field] = _ffi.string(getattr(drr_begin, field)) + else: + raise TypeError('Unexpected field type in drr_begin: ' + str(descr.type)) + return (header, record) + + +def lzc_exists(name): + ''' + Check if a dataset (a filesystem, or a volume, or a snapshot) + with the given name exists. + + :param bytes name: the dataset name to check. + :return: `True` if the dataset exists, `False` otherwise. + :rtype: bool + + .. note:: + ``lzc_exists`` can not be used to check for existence of bookmarks. + ''' + ret = _lib.lzc_exists(name) + return bool(ret) + + +def is_supported(func): + ''' + Check whether C *libzfs_core* provides implementation required + for the given Python wrapper. + + If `is_supported` returns ``False`` for the function, then + calling the function would result in :exc:`NotImplementedError`. + + :param function func: the function to check. + :return bool: whether the function can be used. + ''' + fname = func.__name__ + if fname not in globals(): + raise ValueError(fname + ' is not from libzfs_core') + if not callable(func): + raise ValueError(fname + ' is not a function') + if not fname.startswith("lzc_"): + raise ValueError(fname + ' is not a libzfs_core API function') + check_func = getattr(func, "_check_func", None) + if check_func is not None: + return is_supported(check_func) + return getattr(_lib, fname, None) is not None + + +def _uncommitted(depends_on=None): + ''' + Mark an API function as being an uncommitted extension that might not be + available. + + :param function depends_on: the function that would be checked + instead of a decorated function. + For example, if the decorated function uses + another uncommitted function. + + This decorator transforms a decorated function to raise + :exc:`NotImplementedError` if the C libzfs_core library does not provide + a function with the same name as the decorated function. + + The optional `depends_on` parameter can be provided if the decorated + function does not directly call the C function but instead calls another + Python function that follows the typical convention. + One example is :func:`lzc_list_snaps` that calls :func:`lzc_list` that + calls ``lzc_list`` in libzfs_core. + + This decorator is implemented using :func:`is_supported`. + ''' + def _uncommitted_decorator(func, depends_on=depends_on): + @functools.wraps(func) + def _f(*args, **kwargs): + if not is_supported(_f): + raise NotImplementedError(func.__name__) + return func(*args, **kwargs) + if depends_on is not None: + _f._check_func = depends_on + return _f + return _uncommitted_decorator + + +@_uncommitted() +def lzc_promote(name): + ''' + Promotes the ZFS dataset. + + :param bytes name: the name of the dataset to promote. + :raises NameInvalid: if the dataset name is invalid. + :raises NameTooLong: if the dataset name is too long. + :raises NameTooLong: if the dataset's origin has a snapshot that, + if transferred to the dataset, would get + a too long name. + :raises NotClone: if the dataset is not a clone. + :raises FilesystemNotFound: if the dataset does not exist. + :raises SnapshotExists: if the dataset already has a snapshot with + the same name as one of the origin's snapshots. + ''' + ret = _lib.lzc_promote(name, _ffi.NULL, _ffi.NULL) + errors.lzc_promote_translate_error(ret, name) + + +@_uncommitted() +def lzc_rename(source, target): + ''' + Rename the ZFS dataset. + + :param source name: the current name of the dataset to rename. + :param target name: the new name of the dataset. + :raises NameInvalid: if either the source or target name is invalid. + :raises NameTooLong: if either the source or target name is too long. + :raises NameTooLong: if a snapshot of the source would get a too long + name after renaming. + :raises FilesystemNotFound: if the source does not exist. + :raises FilesystemNotFound: if the target's parent does not exist. + :raises FilesystemExists: if the target already exists. + :raises PoolsDiffer: if the source and target belong to different pools. + ''' + ret = _lib.lzc_rename(source, target, _ffi.NULL, _ffi.NULL) + errors.lzc_rename_translate_error(ret, source, target) + + +@_uncommitted() +def lzc_destroy_one(name): + ''' + Destroy the ZFS dataset. + + :param bytes name: the name of the dataset to destroy. + :raises NameInvalid: if the dataset name is invalid. + :raises NameTooLong: if the dataset name is too long. + :raises FilesystemNotFound: if the dataset does not exist. + ''' + ret = _lib.lzc_destroy_one(name, _ffi.NULL) + errors.lzc_destroy_translate_error(ret, name) + + +# As the extended API is not committed yet, the names of the new interfaces +# are not settled down yet. +# lzc_destroy() might make more sense as we do not have lzc_create_one(). +lzc_destroy = lzc_destroy_one + + +@_uncommitted() +def lzc_inherit(name, prop): + ''' + Inherit properties from a parent dataset of the given ZFS dataset. + + :param bytes name: the name of the dataset. + :param bytes prop: the name of the property to inherit. + :raises NameInvalid: if the dataset name is invalid. + :raises NameTooLong: if the dataset name is too long. + :raises DatasetNotFound: if the dataset does not exist. + :raises PropertyInvalid: if one or more of the specified properties is invalid + or has an invalid type or value. + + Inheriting a property actually resets it to its default value + or removes it if it's a user property, so that the property could be + inherited if it's inheritable. If the property is not inheritable + then it would just have its default value. + + This function can be used on snapshots to inherit user defined properties. + ''' + ret = _lib.lzc_inherit(name, prop, _ffi.NULL) + errors.lzc_inherit_prop_translate_error(ret, name, prop) + + +# As the extended API is not committed yet, the names of the new interfaces +# are not settled down yet. +# lzc_inherit_prop makes it clearer what is to be inherited. +lzc_inherit_prop = lzc_inherit + + +@_uncommitted() +def lzc_set_props(name, prop, val): + ''' + Set properties of the ZFS dataset. + + :param bytes name: the name of the dataset. + :param bytes prop: the name of the property. + :param Any val: the value of the property. + :raises NameInvalid: if the dataset name is invalid. + :raises NameTooLong: if the dataset name is too long. + :raises DatasetNotFound: if the dataset does not exist. + :raises NoSpace: if the property controls a quota and the values is + too small for that quota. + :raises PropertyInvalid: if one or more of the specified properties is invalid + or has an invalid type or value. + + This function can be used on snapshots to set user defined properties. + + .. note:: + An attempt to set a readonly / statistic property is ignored + without reporting any error. + ''' + props = {prop: val} + props_nv = nvlist_in(props) + ret = _lib.lzc_set_props(name, props_nv, _ffi.NULL, _ffi.NULL) + errors.lzc_set_prop_translate_error(ret, name, prop, val) + + +# As the extended API is not committed yet, the names of the new interfaces +# are not settled down yet. +# It's not clear if atomically setting multiple properties is an achievable +# goal and an interface acting on mutiple entities must do so atomically +# by convention. +# Being able to set a single property at a time is sufficient for ClusterHQ. +lzc_set_prop = lzc_set_props + + +@_uncommitted() +def lzc_list(name, options): + ''' + List subordinate elements of the given dataset. + + This function can be used to list child datasets and snapshots + of the given dataset. The listed elements can be filtered by + their type and by their depth relative to the starting dataset. + + :param bytes name: the name of the dataset to be listed, could + be a snapshot or a dataset. + :param options: a `dict` of the options that control the listing + behavior. + :type options: dict of bytes:Any + :return: a pair of file descriptors the first of which can be + used to read the listing. + :rtype: tuple of (int, int) + :raises DatasetNotFound: if the dataset does not exist. + + Two options are currently available: + + recurse : integer or None + specifies depth of the recursive listing. If ``None`` the + depth is not limited. + Absence of this option means that only the given dataset + is listed. + + type : dict of bytes:None + specifies dataset types to include into the listing. + Currently allowed keys are "filesystem", "volume", "snapshot". + Absence of this option implies all types. + + The first of the returned file descriptors can be used to + read the listing in a binary encounded format. The data is + a series of variable sized records each starting with a fixed + size header, the header is followed by a serialized ``nvlist``. + Each record describes a single element and contains the element's + name as well as its properties. + The file descriptor must be closed after reading from it. + + The second file descriptor represents a pipe end to which the + kernel driver is writing information. It should not be closed + until all interesting information has been read and it must + be explicitly closed afterwards. + ''' + (rfd, wfd) = os.pipe() + fcntl.fcntl(rfd, fcntl.F_SETFD, fcntl.FD_CLOEXEC) + fcntl.fcntl(wfd, fcntl.F_SETFD, fcntl.FD_CLOEXEC) + options = options.copy() + options['fd'] = int32_t(wfd) + opts_nv = nvlist_in(options) + ret = _lib.lzc_list(name, opts_nv) + if ret == errno.ESRCH: + return (None, None) + errors.lzc_list_translate_error(ret, name, options) + return (rfd, wfd) + + +# Description of the binary format used to pass data from the kernel. +_PIPE_RECORD_FORMAT = 'IBBBB' +_PIPE_RECORD_SIZE = struct.calcsize(_PIPE_RECORD_FORMAT) + + +def _list(name, recurse=None, types=None): + ''' + A wrapper for :func:`lzc_list` that hides details of working + with the file descriptors and provides data in an easy to + consume format. + + :param bytes name: the name of the dataset to be listed, could + be a snapshot, a volume or a filesystem. + :param recurse: specifies depth of the recursive listing. + If ``None`` the depth is not limited. + :param types: specifies dataset types to include into the listing. + Currently allowed keys are "filesystem", "volume", "snapshot". + ``None`` is equivalent to specifying the type of the dataset + named by `name`. + :type types: list of bytes or None + :type recurse: integer or None + :return: a list of dictionaries each describing a single listed + element. + :rtype: list of dict + ''' + options = {} + + # Convert types to a dict suitable for mapping to an nvlist. + if types is not None: + types = {x: None for x in types} + options['type'] = types + if recurse is None or recurse > 0: + options['recurse'] = recurse + + # Note that other_fd is used by the kernel side to write + # the data, so we have to keep that descriptor open until + # we are done. + # Also, we have to explicitly close the descriptor as the + # kernel doesn't do that. + (fd, other_fd) = lzc_list(name, options) + if fd is None: + return + + try: + while True: + record_bytes = os.read(fd, _PIPE_RECORD_SIZE) + if not record_bytes: + break + (size, _, err, _, _) = struct.unpack( + _PIPE_RECORD_FORMAT, record_bytes) + if err == errno.ESRCH: + break + errors.lzc_list_translate_error(err, name, options) + if size == 0: + break + data_bytes = os.read(fd, size) + result = {} + with nvlist_out(result) as nvp: + ret = _lib.nvlist_unpack(data_bytes, size, nvp, 0) + if ret != 0: + raise exceptions.ZFSGenericError(ret, None, + "Failed to unpack list data") + yield result + finally: + os.close(other_fd) + os.close(fd) + + +@_uncommitted(lzc_list) +def lzc_get_props(name): + ''' + Get properties of the ZFS dataset. + + :param bytes name: the name of the dataset. + :raises DatasetNotFound: if the dataset does not exist. + :raises NameInvalid: if the dataset name is invalid. + :raises NameTooLong: if the dataset name is too long. + :return: a dictionary mapping the property names to their values. + :rtype: dict of bytes:Any + + .. note:: + The value of ``clones`` property is a `list` of clone names + as byte strings. + + .. warning:: + The returned dictionary does not contain entries for properties + with default values. One exception is the ``mountpoint`` property + for which the default value is derived from the dataset name. + ''' + result = next(_list(name, recurse=0)) + is_snapshot = result['dmu_objset_stats']['dds_is_snapshot'] + result = result['properties'] + # In most cases the source of the property is uninteresting and the + # value alone is sufficient. One exception is the 'mountpoint' + # property the final value of which is not the same as the inherited + # value. + mountpoint = result.get('mountpoint') + if mountpoint is not None: + mountpoint_src = mountpoint['source'] + mountpoint_val = mountpoint['value'] + # 'source' is the name of the dataset that has 'mountpoint' set + # to a non-default value and from which the current dataset inherits + # the property. 'source' can be the current dataset if its + # 'mountpoint' is explicitly set. + # 'source' can also be a special value like '$recvd', that case + # is equivalent to the property being set on the current dataset. + # Note that a normal mountpoint value should start with '/' + # unlike the special values "none" and "legacy". + if mountpoint_val.startswith('/') and not mountpoint_src.startswith('$'): + mountpoint_val = mountpoint_val + name[len(mountpoint_src):] + elif not is_snapshot: + mountpoint_val = '/' + name + else: + mountpoint_val = None + result = {k: v['value'] for k, v in result.iteritems()} + if 'clones' in result: + result['clones'] = result['clones'].keys() + if mountpoint_val is not None: + result['mountpoint'] = mountpoint_val + return result + + +@_uncommitted(lzc_list) +def lzc_list_children(name): + ''' + List the children of the ZFS dataset. + + :param bytes name: the name of the dataset. + :return: an iterator that produces the names of the children. + :raises NameInvalid: if the dataset name is invalid. + :raises NameTooLong: if the dataset name is too long. + :raises DatasetNotFound: if the dataset does not exist. + + .. warning:: + If the dataset does not exist, then the returned iterator would produce + no results and no error is reported. + That case is indistinguishable from the dataset having no children. + + An attempt to list children of a snapshot is silently ignored as well. + ''' + children = [] + for entry in _list(name, recurse=1, types=['filesystem', 'volume']): + child = entry['name'] + if child != name: + children.append(child) + + return iter(children) + + +@_uncommitted(lzc_list) +def lzc_list_snaps(name): + ''' + List the snapshots of the ZFS dataset. + + :param bytes name: the name of the dataset. + :return: an iterator that produces the names of the snapshots. + :raises NameInvalid: if the dataset name is invalid. + :raises NameTooLong: if the dataset name is too long. + :raises DatasetNotFound: if the dataset does not exist. + + .. warning:: + If the dataset does not exist, then the returned iterator would produce + no results and no error is reported. + That case is indistinguishable from the dataset having no snapshots. + + An attempt to list snapshots of a snapshot is silently ignored as well. + ''' + snaps = [] + for entry in _list(name, recurse=1, types=['snapshot']): + snap = entry['name'] + if snap != name: + snaps.append(snap) + + return iter(snaps) + + +# TODO: a better way to init and uninit the library +def _initialize(): + class LazyInit(object): + + def __init__(self, lib): + self._lib = lib + self._inited = False + self._lock = threading.Lock() + + def __getattr__(self, name): + if not self._inited: + with self._lock: + if not self._inited: + ret = self._lib.libzfs_core_init() + if ret != 0: + raise exceptions.ZFSInitializationFailed(ret) + self._inited = True + return getattr(self._lib, name) + + return LazyInit(libzfs_core.lib) + +_ffi = libzfs_core.ffi +_lib = _initialize() + + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/_nvlist.py b/contrib/pyzfs/libzfs_core/_nvlist.py new file mode 100644 index 000000000..1f1c39bbf --- /dev/null +++ b/contrib/pyzfs/libzfs_core/_nvlist.py @@ -0,0 +1,259 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +nvlist_in and nvlist_out provide support for converting between +a dictionary on the Python side and an nvlist_t on the C side +with the automatic memory management for C memory allocations. + +nvlist_in takes a dictionary and produces a CData object corresponding +to a C nvlist_t pointer suitable for passing as an input parameter. +The nvlist_t is populated based on the dictionary. + +nvlist_out takes a dictionary and produces a CData object corresponding +to a C nvlist_t pointer to pointer suitable for passing as an output parameter. +Upon exit from a with-block the dictionary is populated based on the nvlist_t. + +The dictionary must follow a certain format to be convertible +to the nvlist_t. The dictionary produced from the nvlist_t +will follow the same format. + +Format: +- keys are always byte strings +- a value can be None in which case it represents boolean truth by its mere presence +- a value can be a bool +- a value can be a byte string +- a value can be an integer +- a value can be a CFFI CData object representing one of the following C types: + int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, boolean_t, uchar_t +- a value can be a dictionary that recursively adheres to this format +- a value can be a list of bools, byte strings, integers or CData objects of types specified above +- a value can be a list of dictionaries that adhere to this format +- all elements of a list value must be of the same type +""" + +import numbers +from collections import namedtuple +from contextlib import contextmanager +from .bindings import libnvpair +from .ctypes import _type_to_suffix + +_ffi = libnvpair.ffi +_lib = libnvpair.lib + + +def nvlist_in(props): + """ + This function converts a python dictionary to a C nvlist_t + and provides automatic memory management for the latter. + + :param dict props: the dictionary to be converted. + :return: an FFI CData object representing the nvlist_t pointer. + :rtype: CData + """ + nvlistp = _ffi.new("nvlist_t **") + res = _lib.nvlist_alloc(nvlistp, 1, 0) # UNIQUE_NAME == 1 + if res != 0: + raise MemoryError('nvlist_alloc failed') + nvlist = _ffi.gc(nvlistp[0], _lib.nvlist_free) + _dict_to_nvlist(props, nvlist) + return nvlist + + +@contextmanager +def nvlist_out(props): + """ + A context manager that allocates a pointer to a C nvlist_t and yields + a CData object representing a pointer to the pointer via 'as' target. + The caller can pass that pointer to a pointer to a C function that + creates a new nvlist_t object. + The context manager takes care of memory management for the nvlist_t + and also populates the 'props' dictionary with data from the nvlist_t + upon leaving the 'with' block. + + :param dict props: the dictionary to be populated with data from the nvlist. + :return: an FFI CData object representing the pointer to nvlist_t pointer. + :rtype: CData + """ + nvlistp = _ffi.new("nvlist_t **") + nvlistp[0] = _ffi.NULL # to be sure + try: + yield nvlistp + # clear old entries, if any + props.clear() + _nvlist_to_dict(nvlistp[0], props) + finally: + if nvlistp[0] != _ffi.NULL: + _lib.nvlist_free(nvlistp[0]) + nvlistp[0] = _ffi.NULL + + +_TypeInfo = namedtuple('_TypeInfo', ['suffix', 'ctype', 'is_array', 'convert']) + + +def _type_info(typeid): + return { + _lib.DATA_TYPE_BOOLEAN: _TypeInfo(None, None, None, None), + _lib.DATA_TYPE_BOOLEAN_VALUE: _TypeInfo("boolean_value", "boolean_t *", False, bool), + _lib.DATA_TYPE_BYTE: _TypeInfo("byte", "uchar_t *", False, int), + _lib.DATA_TYPE_INT8: _TypeInfo("int8", "int8_t *", False, int), + _lib.DATA_TYPE_UINT8: _TypeInfo("uint8", "uint8_t *", False, int), + _lib.DATA_TYPE_INT16: _TypeInfo("int16", "int16_t *", False, int), + _lib.DATA_TYPE_UINT16: _TypeInfo("uint16", "uint16_t *", False, int), + _lib.DATA_TYPE_INT32: _TypeInfo("int32", "int32_t *", False, int), + _lib.DATA_TYPE_UINT32: _TypeInfo("uint32", "uint32_t *", False, int), + _lib.DATA_TYPE_INT64: _TypeInfo("int64", "int64_t *", False, int), + _lib.DATA_TYPE_UINT64: _TypeInfo("uint64", "uint64_t *", False, int), + _lib.DATA_TYPE_STRING: _TypeInfo("string", "char **", False, _ffi.string), + _lib.DATA_TYPE_NVLIST: _TypeInfo("nvlist", "nvlist_t **", False, lambda x: _nvlist_to_dict(x, {})), + _lib.DATA_TYPE_BOOLEAN_ARRAY: _TypeInfo("boolean_array", "boolean_t **", True, bool), + # XXX use bytearray ? + _lib.DATA_TYPE_BYTE_ARRAY: _TypeInfo("byte_array", "uchar_t **", True, int), + _lib.DATA_TYPE_INT8_ARRAY: _TypeInfo("int8_array", "int8_t **", True, int), + _lib.DATA_TYPE_UINT8_ARRAY: _TypeInfo("uint8_array", "uint8_t **", True, int), + _lib.DATA_TYPE_INT16_ARRAY: _TypeInfo("int16_array", "int16_t **", True, int), + _lib.DATA_TYPE_UINT16_ARRAY: _TypeInfo("uint16_array", "uint16_t **", True, int), + _lib.DATA_TYPE_INT32_ARRAY: _TypeInfo("int32_array", "int32_t **", True, int), + _lib.DATA_TYPE_UINT32_ARRAY: _TypeInfo("uint32_array", "uint32_t **", True, int), + _lib.DATA_TYPE_INT64_ARRAY: _TypeInfo("int64_array", "int64_t **", True, int), + _lib.DATA_TYPE_UINT64_ARRAY: _TypeInfo("uint64_array", "uint64_t **", True, int), + _lib.DATA_TYPE_STRING_ARRAY: _TypeInfo("string_array", "char ***", True, _ffi.string), + _lib.DATA_TYPE_NVLIST_ARRAY: _TypeInfo("nvlist_array", "nvlist_t ***", True, lambda x: _nvlist_to_dict(x, {})), + }[typeid] + +# only integer properties need to be here +_prop_name_to_type_str = { + "rewind-request": "uint32", + "type": "uint32", + "N_MORE_ERRORS": "int32", + "pool_context": "int32", +} + + +def _nvlist_add_array(nvlist, key, array): + def _is_integer(x): + return isinstance(x, numbers.Integral) and not isinstance(x, bool) + + ret = 0 + specimen = array[0] + is_integer = _is_integer(specimen) + specimen_ctype = None + if isinstance(specimen, _ffi.CData): + specimen_ctype = _ffi.typeof(specimen) + + for element in array[1:]: + if is_integer and _is_integer(element): + pass + elif type(element) is not type(specimen): + raise TypeError('Array has elements of different types: ' + + type(specimen).__name__ + + ' and ' + + type(element).__name__) + elif specimen_ctype is not None: + ctype = _ffi.typeof(element) + if ctype is not specimen_ctype: + raise TypeError('Array has elements of different C types: ' + + _ffi.typeof(specimen).cname + + ' and ' + + _ffi.typeof(element).cname) + + if isinstance(specimen, dict): + # NB: can't use automatic memory management via nvlist_in() here, + # we have a loop, but 'with' would require recursion + c_array = [] + for dictionary in array: + nvlistp = _ffi.new('nvlist_t **') + res = _lib.nvlist_alloc(nvlistp, 1, 0) # UNIQUE_NAME == 1 + if res != 0: + raise MemoryError('nvlist_alloc failed') + nested_nvlist = _ffi.gc(nvlistp[0], _lib.nvlist_free) + _dict_to_nvlist(dictionary, nested_nvlist) + c_array.append(nested_nvlist) + ret = _lib.nvlist_add_nvlist_array(nvlist, key, c_array, len(c_array)) + elif isinstance(specimen, bytes): + c_array = [] + for string in array: + c_array.append(_ffi.new('char[]', string)) + ret = _lib.nvlist_add_string_array(nvlist, key, c_array, len(c_array)) + elif isinstance(specimen, bool): + ret = _lib.nvlist_add_boolean_array(nvlist, key, array, len(array)) + elif isinstance(specimen, numbers.Integral): + suffix = _prop_name_to_type_str.get(key, "uint64") + cfunc = getattr(_lib, "nvlist_add_%s_array" % (suffix,)) + ret = cfunc(nvlist, key, array, len(array)) + elif isinstance(specimen, _ffi.CData) and _ffi.typeof(specimen) in _type_to_suffix: + suffix = _type_to_suffix[_ffi.typeof(specimen)][True] + cfunc = getattr(_lib, "nvlist_add_%s_array" % (suffix,)) + ret = cfunc(nvlist, key, array, len(array)) + else: + raise TypeError('Unsupported value type ' + type(specimen).__name__) + if ret != 0: + raise MemoryError('nvlist_add failed, err = %d' % ret) + + +def _nvlist_to_dict(nvlist, props): + pair = _lib.nvlist_next_nvpair(nvlist, _ffi.NULL) + while pair != _ffi.NULL: + name = _ffi.string(_lib.nvpair_name(pair)) + typeid = int(_lib.nvpair_type(pair)) + typeinfo = _type_info(typeid) + # XXX nvpair_type_is_array() is broken for DATA_TYPE_INT8_ARRAY at the moment + # see https://www.illumos.org/issues/5778 + # is_array = bool(_lib.nvpair_type_is_array(pair)) + is_array = typeinfo.is_array + cfunc = getattr(_lib, "nvpair_value_%s" % (typeinfo.suffix,), None) + val = None + ret = 0 + if is_array: + valptr = _ffi.new(typeinfo.ctype) + lenptr = _ffi.new("uint_t *") + ret = cfunc(pair, valptr, lenptr) + if ret != 0: + raise RuntimeError('nvpair_value failed') + length = int(lenptr[0]) + val = [] + for i in range(length): + val.append(typeinfo.convert(valptr[0][i])) + else: + if typeid == _lib.DATA_TYPE_BOOLEAN: + val = None # XXX or should it be True ? + else: + valptr = _ffi.new(typeinfo.ctype) + ret = cfunc(pair, valptr) + if ret != 0: + raise RuntimeError('nvpair_value failed') + val = typeinfo.convert(valptr[0]) + props[name] = val + pair = _lib.nvlist_next_nvpair(nvlist, pair) + return props + + +def _dict_to_nvlist(props, nvlist): + for k, v in props.items(): + if not isinstance(k, bytes): + raise TypeError('Unsupported key type ' + type(k).__name__) + ret = 0 + if isinstance(v, dict): + ret = _lib.nvlist_add_nvlist(nvlist, k, nvlist_in(v)) + elif isinstance(v, list): + _nvlist_add_array(nvlist, k, v) + elif isinstance(v, bytes): + ret = _lib.nvlist_add_string(nvlist, k, v) + elif isinstance(v, bool): + ret = _lib.nvlist_add_boolean_value(nvlist, k, v) + elif v is None: + ret = _lib.nvlist_add_boolean(nvlist, k) + elif isinstance(v, numbers.Integral): + suffix = _prop_name_to_type_str.get(k, "uint64") + cfunc = getattr(_lib, "nvlist_add_%s" % (suffix,)) + ret = cfunc(nvlist, k, v) + elif isinstance(v, _ffi.CData) and _ffi.typeof(v) in _type_to_suffix: + suffix = _type_to_suffix[_ffi.typeof(v)][False] + cfunc = getattr(_lib, "nvlist_add_%s" % (suffix,)) + ret = cfunc(nvlist, k, v) + else: + raise TypeError('Unsupported value type ' + type(v).__name__) + if ret != 0: + raise MemoryError('nvlist_add failed') + + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/bindings/__init__.py b/contrib/pyzfs/libzfs_core/bindings/__init__.py new file mode 100644 index 000000000..d6fd2b8ba --- /dev/null +++ b/contrib/pyzfs/libzfs_core/bindings/__init__.py @@ -0,0 +1,45 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +The package that contains a module per each C library that +`libzfs_core` uses. The modules expose CFFI objects required +to make calls to functions in the libraries. +""" + +import threading +import importlib + +from cffi import FFI + + +def _setup_cffi(): + class LazyLibrary(object): + + def __init__(self, ffi, libname): + self._ffi = ffi + self._libname = libname + self._lib = None + self._lock = threading.Lock() + + def __getattr__(self, name): + if self._lib is None: + with self._lock: + if self._lib is None: + self._lib = self._ffi.dlopen(self._libname) + + return getattr(self._lib, name) + + MODULES = ["libnvpair", "libzfs_core"] + ffi = FFI() + + for module_name in MODULES: + module = importlib.import_module("." + module_name, __package__) + ffi.cdef(module.CDEF) + lib = LazyLibrary(ffi, module.LIBRARY) + setattr(module, "ffi", ffi) + setattr(module, "lib", lib) + + +_setup_cffi() + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/bindings/libnvpair.py b/contrib/pyzfs/libzfs_core/bindings/libnvpair.py new file mode 100644 index 000000000..d3f3adf4b --- /dev/null +++ b/contrib/pyzfs/libzfs_core/bindings/libnvpair.py @@ -0,0 +1,117 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +Python bindings for ``libnvpair``. +""" + +CDEF = """ + typedef ... nvlist_t; + typedef ... nvpair_t; + + + typedef enum { + DATA_TYPE_UNKNOWN = 0, + DATA_TYPE_BOOLEAN, + DATA_TYPE_BYTE, + DATA_TYPE_INT16, + DATA_TYPE_UINT16, + DATA_TYPE_INT32, + DATA_TYPE_UINT32, + DATA_TYPE_INT64, + DATA_TYPE_UINT64, + DATA_TYPE_STRING, + DATA_TYPE_BYTE_ARRAY, + DATA_TYPE_INT16_ARRAY, + DATA_TYPE_UINT16_ARRAY, + DATA_TYPE_INT32_ARRAY, + DATA_TYPE_UINT32_ARRAY, + DATA_TYPE_INT64_ARRAY, + DATA_TYPE_UINT64_ARRAY, + DATA_TYPE_STRING_ARRAY, + DATA_TYPE_HRTIME, + DATA_TYPE_NVLIST, + DATA_TYPE_NVLIST_ARRAY, + DATA_TYPE_BOOLEAN_VALUE, + DATA_TYPE_INT8, + DATA_TYPE_UINT8, + DATA_TYPE_BOOLEAN_ARRAY, + DATA_TYPE_INT8_ARRAY, + DATA_TYPE_UINT8_ARRAY + } data_type_t; + typedef enum { B_FALSE, B_TRUE } boolean_t; + + typedef unsigned char uchar_t; + typedef unsigned int uint_t; + + int nvlist_alloc(nvlist_t **, uint_t, int); + void nvlist_free(nvlist_t *); + + int nvlist_unpack(char *, size_t, nvlist_t **, int); + + void dump_nvlist(nvlist_t *, int); + int nvlist_dup(nvlist_t *, nvlist_t **, int); + + int nvlist_add_boolean(nvlist_t *, const char *); + int nvlist_add_boolean_value(nvlist_t *, const char *, boolean_t); + int nvlist_add_byte(nvlist_t *, const char *, uchar_t); + int nvlist_add_int8(nvlist_t *, const char *, int8_t); + int nvlist_add_uint8(nvlist_t *, const char *, uint8_t); + int nvlist_add_int16(nvlist_t *, const char *, int16_t); + int nvlist_add_uint16(nvlist_t *, const char *, uint16_t); + int nvlist_add_int32(nvlist_t *, const char *, int32_t); + int nvlist_add_uint32(nvlist_t *, const char *, uint32_t); + int nvlist_add_int64(nvlist_t *, const char *, int64_t); + int nvlist_add_uint64(nvlist_t *, const char *, uint64_t); + int nvlist_add_string(nvlist_t *, const char *, const char *); + int nvlist_add_nvlist(nvlist_t *, const char *, nvlist_t *); + int nvlist_add_boolean_array(nvlist_t *, const char *, boolean_t *, uint_t); + int nvlist_add_byte_array(nvlist_t *, const char *, uchar_t *, uint_t); + int nvlist_add_int8_array(nvlist_t *, const char *, int8_t *, uint_t); + int nvlist_add_uint8_array(nvlist_t *, const char *, uint8_t *, uint_t); + int nvlist_add_int16_array(nvlist_t *, const char *, int16_t *, uint_t); + int nvlist_add_uint16_array(nvlist_t *, const char *, uint16_t *, uint_t); + int nvlist_add_int32_array(nvlist_t *, const char *, int32_t *, uint_t); + int nvlist_add_uint32_array(nvlist_t *, const char *, uint32_t *, uint_t); + int nvlist_add_int64_array(nvlist_t *, const char *, int64_t *, uint_t); + int nvlist_add_uint64_array(nvlist_t *, const char *, uint64_t *, uint_t); + int nvlist_add_string_array(nvlist_t *, const char *, char *const *, uint_t); + int nvlist_add_nvlist_array(nvlist_t *, const char *, nvlist_t **, uint_t); + + nvpair_t *nvlist_next_nvpair(nvlist_t *, nvpair_t *); + nvpair_t *nvlist_prev_nvpair(nvlist_t *, nvpair_t *); + char *nvpair_name(nvpair_t *); + data_type_t nvpair_type(nvpair_t *); + int nvpair_type_is_array(nvpair_t *); + int nvpair_value_boolean_value(nvpair_t *, boolean_t *); + int nvpair_value_byte(nvpair_t *, uchar_t *); + int nvpair_value_int8(nvpair_t *, int8_t *); + int nvpair_value_uint8(nvpair_t *, uint8_t *); + int nvpair_value_int16(nvpair_t *, int16_t *); + int nvpair_value_uint16(nvpair_t *, uint16_t *); + int nvpair_value_int32(nvpair_t *, int32_t *); + int nvpair_value_uint32(nvpair_t *, uint32_t *); + int nvpair_value_int64(nvpair_t *, int64_t *); + int nvpair_value_uint64(nvpair_t *, uint64_t *); + int nvpair_value_string(nvpair_t *, char **); + int nvpair_value_nvlist(nvpair_t *, nvlist_t **); + int nvpair_value_boolean_array(nvpair_t *, boolean_t **, uint_t *); + int nvpair_value_byte_array(nvpair_t *, uchar_t **, uint_t *); + int nvpair_value_int8_array(nvpair_t *, int8_t **, uint_t *); + int nvpair_value_uint8_array(nvpair_t *, uint8_t **, uint_t *); + int nvpair_value_int16_array(nvpair_t *, int16_t **, uint_t *); + int nvpair_value_uint16_array(nvpair_t *, uint16_t **, uint_t *); + int nvpair_value_int32_array(nvpair_t *, int32_t **, uint_t *); + int nvpair_value_uint32_array(nvpair_t *, uint32_t **, uint_t *); + int nvpair_value_int64_array(nvpair_t *, int64_t **, uint_t *); + int nvpair_value_uint64_array(nvpair_t *, uint64_t **, uint_t *); + int nvpair_value_string_array(nvpair_t *, char ***, uint_t *); + int nvpair_value_nvlist_array(nvpair_t *, nvlist_t ***, uint_t *); +""" + +SOURCE = """ +#include <libzfs/sys/nvpair.h> +""" + +LIBRARY = "nvpair" + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/bindings/libzfs_core.py b/contrib/pyzfs/libzfs_core/bindings/libzfs_core.py new file mode 100644 index 000000000..d0bf570c3 --- /dev/null +++ b/contrib/pyzfs/libzfs_core/bindings/libzfs_core.py @@ -0,0 +1,99 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +Python bindings for ``libzfs_core``. +""" + +CDEF = """ + enum lzc_send_flags { + LZC_SEND_FLAG_EMBED_DATA = 1, + LZC_SEND_FLAG_LARGE_BLOCK = 2 + }; + + typedef enum { + DMU_OST_NONE, + DMU_OST_META, + DMU_OST_ZFS, + DMU_OST_ZVOL, + DMU_OST_OTHER, + DMU_OST_ANY, + DMU_OST_NUMTYPES + } dmu_objset_type_t; + + #define MAXNAMELEN 256 + + struct drr_begin { + uint64_t drr_magic; + uint64_t drr_versioninfo; /* was drr_version */ + uint64_t drr_creation_time; + dmu_objset_type_t drr_type; + uint32_t drr_flags; + uint64_t drr_toguid; + uint64_t drr_fromguid; + char drr_toname[MAXNAMELEN]; + }; + + typedef struct zio_cksum { + uint64_t zc_word[4]; + } zio_cksum_t; + + typedef struct dmu_replay_record { + enum { + DRR_BEGIN, DRR_OBJECT, DRR_FREEOBJECTS, + DRR_WRITE, DRR_FREE, DRR_END, DRR_WRITE_BYREF, + DRR_SPILL, DRR_WRITE_EMBEDDED, DRR_NUMTYPES + } drr_type; + uint32_t drr_payloadlen; + union { + struct drr_begin drr_begin; + /* ... */ + struct drr_checksum { + uint64_t drr_pad[34]; + zio_cksum_t drr_checksum; + } drr_checksum; + } drr_u; + } dmu_replay_record_t; + + int libzfs_core_init(void); + void libzfs_core_fini(void); + + int lzc_snapshot(nvlist_t *, nvlist_t *, nvlist_t **); + int lzc_create(const char *, dmu_objset_type_t, nvlist_t *); + int lzc_clone(const char *, const char *, nvlist_t *); + int lzc_destroy_snaps(nvlist_t *, boolean_t, nvlist_t **); + int lzc_bookmark(nvlist_t *, nvlist_t **); + int lzc_get_bookmarks(const char *, nvlist_t *, nvlist_t **); + int lzc_destroy_bookmarks(nvlist_t *, nvlist_t **); + + int lzc_snaprange_space(const char *, const char *, uint64_t *); + + int lzc_hold(nvlist_t *, int, nvlist_t **); + int lzc_release(nvlist_t *, nvlist_t **); + int lzc_get_holds(const char *, nvlist_t **); + + int lzc_send(const char *, const char *, int, enum lzc_send_flags); + int lzc_send_space(const char *, const char *, enum lzc_send_flags, uint64_t *); + int lzc_receive(const char *, nvlist_t *, const char *, boolean_t, int); + int lzc_receive_with_header(const char *, nvlist_t *, const char *, boolean_t, + boolean_t, int, const struct dmu_replay_record *); + + boolean_t lzc_exists(const char *); + + int lzc_rollback(const char *, char *, int); + int lzc_rollback_to(const char *, const char *); + + int lzc_promote(const char *, nvlist_t *, nvlist_t **); + int lzc_rename(const char *, const char *, nvlist_t *, char **); + int lzc_destroy_one(const char *fsname, nvlist_t *); + int lzc_inherit(const char *fsname, const char *name, nvlist_t *); + int lzc_set_props(const char *, nvlist_t *, nvlist_t *, nvlist_t *); + int lzc_list (const char *, nvlist_t *); +""" + +SOURCE = """ +#include <libzfs/libzfs_core.h> +""" + +LIBRARY = "zfs_core" + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/ctypes.py b/contrib/pyzfs/libzfs_core/ctypes.py new file mode 100644 index 000000000..bd168f22a --- /dev/null +++ b/contrib/pyzfs/libzfs_core/ctypes.py @@ -0,0 +1,56 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +Utility functions for casting to a specific C type. +""" + +from .bindings.libnvpair import ffi as _ffi + + +def _ffi_cast(type_name): + type_info = _ffi.typeof(type_name) + + def _func(value): + # this is for overflow / underflow checking only + if type_info.kind == 'enum': + try: + type_info.elements[value] + except KeyError as e: + raise OverflowError('Invalid enum <%s> value %s' % + (type_info.cname, e.message)) + else: + _ffi.new(type_name + '*', value) + return _ffi.cast(type_name, value) + _func.__name__ = type_name + return _func + + +uint8_t = _ffi_cast('uint8_t') +int8_t = _ffi_cast('int8_t') +uint16_t = _ffi_cast('uint16_t') +int16_t = _ffi_cast('int16_t') +uint32_t = _ffi_cast('uint32_t') +int32_t = _ffi_cast('int32_t') +uint64_t = _ffi_cast('uint64_t') +int64_t = _ffi_cast('int64_t') +boolean_t = _ffi_cast('boolean_t') +uchar_t = _ffi_cast('uchar_t') + + +# First element of the value tuple is a suffix for a single value function +# while the second element is for an array function +_type_to_suffix = { + _ffi.typeof('uint8_t'): ('uint8', 'uint8'), + _ffi.typeof('int8_t'): ('int8', 'int8'), + _ffi.typeof('uint16_t'): ('uint16', 'uint16'), + _ffi.typeof('int16_t'): ('int16', 'int16'), + _ffi.typeof('uint32_t'): ('uint32', 'uint32'), + _ffi.typeof('int32_t'): ('int32', 'int32'), + _ffi.typeof('uint64_t'): ('uint64', 'uint64'), + _ffi.typeof('int64_t'): ('int64', 'int64'), + _ffi.typeof('boolean_t'): ('boolean_value', 'boolean'), + _ffi.typeof('uchar_t'): ('byte', 'byte'), +} + + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/exceptions.py b/contrib/pyzfs/libzfs_core/exceptions.py new file mode 100644 index 000000000..c52d43771 --- /dev/null +++ b/contrib/pyzfs/libzfs_core/exceptions.py @@ -0,0 +1,443 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +Exceptions that can be raised by libzfs_core operations. +""" + +import errno + + +class ZFSError(Exception): + errno = None + message = None + name = None + + def __str__(self): + if self.name is not None: + return "[Errno %d] %s: '%s'" % (self.errno, self.message, self.name) + else: + return "[Errno %d] %s" % (self.errno, self.message) + + def __repr__(self): + return "%s(%r, %r)" % (self.__class__.__name__, self.errno, self.message) + + +class ZFSGenericError(ZFSError): + + def __init__(self, errno, name, message): + self.errno = errno + self.message = message + self.name = name + + +class ZFSInitializationFailed(ZFSError): + message = "Failed to initialize libzfs_core" + + def __init__(self, errno): + self.errno = errno + + +class MultipleOperationsFailure(ZFSError): + + def __init__(self, errors, suppressed_count): + # Use first of the individual error codes + # as an overall error code. This is more consistent. + self.errno = errors[0].errno + self.errors = errors + #: this many errors were encountered but not placed on the `errors` list + self.suppressed_count = suppressed_count + + def __str__(self): + return "%s, %d errors included, %d suppressed" % (ZFSError.__str__(self), + len(self.errors), self.suppressed_count) + + def __repr__(self): + return "%s(%r, %r, errors=%r, supressed=%r)" % (self.__class__.__name__, + self.errno, self.message, self.errors, self.suppressed_count) + + +class DatasetNotFound(ZFSError): + + """ + This exception is raised when an operation failure can be caused by a missing + snapshot or a missing filesystem and it is impossible to distinguish between + the causes. + """ + errno = errno.ENOENT + message = "Dataset not found" + + def __init__(self, name): + self.name = name + + +class DatasetExists(ZFSError): + + """ + This exception is raised when an operation failure can be caused by an existing + snapshot or filesystem and it is impossible to distinguish between + the causes. + """ + errno = errno.EEXIST + message = "Dataset already exists" + + def __init__(self, name): + self.name = name + + +class NotClone(ZFSError): + errno = errno.EINVAL + message = "Filesystem is not a clone, can not promote" + + def __init__(self, name): + self.name = name + + +class FilesystemExists(DatasetExists): + message = "Filesystem already exists" + + def __init__(self, name): + self.name = name + + +class FilesystemNotFound(DatasetNotFound): + message = "Filesystem not found" + + def __init__(self, name): + self.name = name + + +class ParentNotFound(ZFSError): + errno = errno.ENOENT + message = "Parent not found" + + def __init__(self, name): + self.name = name + + +class WrongParent(ZFSError): + errno = errno.EINVAL + message = "Parent dataset is not a filesystem" + + def __init__(self, name): + self.name = name + + +class SnapshotExists(DatasetExists): + message = "Snapshot already exists" + + def __init__(self, name): + self.name = name + + +class SnapshotNotFound(DatasetNotFound): + message = "Snapshot not found" + + def __init__(self, name): + self.name = name + +class SnapshotNotLatest(ZFSError): + errno = errno.EEXIST + message = "Snapshot is not the latest" + + def __init__(self, name): + self.name = name + +class SnapshotIsCloned(ZFSError): + errno = errno.EEXIST + message = "Snapshot is cloned" + + def __init__(self, name): + self.name = name + + +class SnapshotIsHeld(ZFSError): + errno = errno.EBUSY + message = "Snapshot is held" + + def __init__(self, name): + self.name = name + + +class DuplicateSnapshots(ZFSError): + errno = errno.EXDEV + message = "Requested multiple snapshots of the same filesystem" + + def __init__(self, name): + self.name = name + + +class SnapshotFailure(MultipleOperationsFailure): + message = "Creation of snapshot(s) failed for one or more reasons" + + def __init__(self, errors, suppressed_count): + super(SnapshotFailure, self).__init__(errors, suppressed_count) + + +class SnapshotDestructionFailure(MultipleOperationsFailure): + message = "Destruction of snapshot(s) failed for one or more reasons" + + def __init__(self, errors, suppressed_count): + super(SnapshotDestructionFailure, self).__init__(errors, suppressed_count) + + +class BookmarkExists(ZFSError): + errno = errno.EEXIST + message = "Bookmark already exists" + + def __init__(self, name): + self.name = name + + +class BookmarkNotFound(ZFSError): + errno = errno.ENOENT + message = "Bookmark not found" + + def __init__(self, name): + self.name = name + + +class BookmarkMismatch(ZFSError): + errno = errno.EINVAL + message = "Bookmark is not in snapshot's filesystem" + + def __init__(self, name): + self.name = name + + +class BookmarkNotSupported(ZFSError): + errno = errno.ENOTSUP + message = "Bookmark feature is not supported" + + def __init__(self, name): + self.name = name + + +class BookmarkFailure(MultipleOperationsFailure): + message = "Creation of bookmark(s) failed for one or more reasons" + + def __init__(self, errors, suppressed_count): + super(BookmarkFailure, self).__init__(errors, suppressed_count) + + +class BookmarkDestructionFailure(MultipleOperationsFailure): + message = "Destruction of bookmark(s) failed for one or more reasons" + + def __init__(self, errors, suppressed_count): + super(BookmarkDestructionFailure, self).__init__(errors, suppressed_count) + + +class BadHoldCleanupFD(ZFSError): + errno = errno.EBADF + message = "Bad file descriptor as cleanup file descriptor" + + +class HoldExists(ZFSError): + errno = errno.EEXIST + message = "Hold with a given tag already exists on snapshot" + + def __init__(self, name): + self.name = name + + +class HoldNotFound(ZFSError): + errno = errno.ENOENT + message = "Hold with a given tag does not exist on snapshot" + + def __init__(self, name): + self.name = name + + +class HoldFailure(MultipleOperationsFailure): + message = "Placement of hold(s) failed for one or more reasons" + + def __init__(self, errors, suppressed_count): + super(HoldFailure, self).__init__(errors, suppressed_count) + + +class HoldReleaseFailure(MultipleOperationsFailure): + message = "Release of hold(s) failed for one or more reasons" + + def __init__(self, errors, suppressed_count): + super(HoldReleaseFailure, self).__init__(errors, suppressed_count) + + +class SnapshotMismatch(ZFSError): + errno = errno.ENODEV + message = "Snapshot is not descendant of source snapshot" + + def __init__(self, name): + self.name = name + + +class StreamMismatch(ZFSError): + errno = errno.ENODEV + message = "Stream is not applicable to destination dataset" + + def __init__(self, name): + self.name = name + + +class DestinationModified(ZFSError): + errno = errno.ETXTBSY + message = "Destination dataset has modifications that can not be undone" + + def __init__(self, name): + self.name = name + + +class BadStream(ZFSError): + errno = errno.EINVAL + message = "Bad backup stream" + + +class StreamFeatureNotSupported(ZFSError): + errno = errno.ENOTSUP + message = "Stream contains unsupported feature" + + +class UnknownStreamFeature(ZFSError): + errno = errno.ENOTSUP + message = "Unknown feature requested for stream" + + +class StreamIOError(ZFSError): + message = "I/O error while writing or reading stream" + + def __init__(self, errno): + self.errno = errno + + +class ZIOError(ZFSError): + errno = errno.EIO + message = "I/O error" + + def __init__(self, name): + self.name = name + + +class NoSpace(ZFSError): + errno = errno.ENOSPC + message = "No space left" + + def __init__(self, name): + self.name = name + + +class QuotaExceeded(ZFSError): + errno = errno.EDQUOT + message = "Quouta exceeded" + + def __init__(self, name): + self.name = name + + +class DatasetBusy(ZFSError): + errno = errno.EBUSY + message = "Dataset is busy" + + def __init__(self, name): + self.name = name + + +class NameTooLong(ZFSError): + errno = errno.ENAMETOOLONG + message = "Dataset name is too long" + + def __init__(self, name): + self.name = name + + +class NameInvalid(ZFSError): + errno = errno.EINVAL + message = "Invalid name" + + def __init__(self, name): + self.name = name + + +class SnapshotNameInvalid(NameInvalid): + message = "Invalid name for snapshot" + + def __init__(self, name): + self.name = name + + +class FilesystemNameInvalid(NameInvalid): + message = "Invalid name for filesystem or volume" + + def __init__(self, name): + self.name = name + + +class BookmarkNameInvalid(NameInvalid): + message = "Invalid name for bookmark" + + def __init__(self, name): + self.name = name + + +class ReadOnlyPool(ZFSError): + errno = errno.EROFS + message = "Pool is read-only" + + def __init__(self, name): + self.name = name + + +class SuspendedPool(ZFSError): + errno = errno.EAGAIN + message = "Pool is suspended" + + def __init__(self, name): + self.name = name + + +class PoolNotFound(ZFSError): + errno = errno.EXDEV + message = "No such pool" + + def __init__(self, name): + self.name = name + + +class PoolsDiffer(ZFSError): + errno = errno.EXDEV + message = "Source and target belong to different pools" + + def __init__(self, name): + self.name = name + + +class FeatureNotSupported(ZFSError): + errno = errno.ENOTSUP + message = "Feature is not supported in this version" + + def __init__(self, name): + self.name = name + + +class PropertyNotSupported(ZFSError): + errno = errno.ENOTSUP + message = "Property is not supported in this version" + + def __init__(self, name): + self.name = name + + +class PropertyInvalid(ZFSError): + errno = errno.EINVAL + message = "Invalid property or property value" + + def __init__(self, name): + self.name = name + + +class DatasetTypeInvalid(ZFSError): + errno = errno.EINVAL + message = "Specified dataset type is unknown" + + def __init__(self, name): + self.name = name + + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/test/__init__.py b/contrib/pyzfs/libzfs_core/test/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/contrib/pyzfs/libzfs_core/test/__init__.py diff --git a/contrib/pyzfs/libzfs_core/test/test_libzfs_core.py b/contrib/pyzfs/libzfs_core/test/test_libzfs_core.py new file mode 100644 index 000000000..b6c971c9c --- /dev/null +++ b/contrib/pyzfs/libzfs_core/test/test_libzfs_core.py @@ -0,0 +1,3708 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +Tests for `libzfs_core` operations. + +These are mostly functional and conformance tests that validate +that the operations produce expected effects or fail with expected +exceptions. +""" + +import unittest +import contextlib +import errno +import filecmp +import os +import platform +import resource +import shutil +import stat +import subprocess +import tempfile +import time +import uuid +from .. import _libzfs_core as lzc +from .. import exceptions as lzc_exc + + +def _print(*args): + for arg in args: + print arg, + print + + +def suppress(exceptions=None): + try: + yield + except BaseException as e: + if exceptions is None or isinstance(e, exceptions): + pass + else: + raise + + +def _zfs_mount(fs): + mntdir = tempfile.mkdtemp() + if platform.system() == 'SunOS': + mount_cmd = ['mount', '-F', 'zfs', fs, mntdir] + else: + mount_cmd = ['mount', '-t', 'zfs', fs, mntdir] + unmount_cmd = ['umount', '-f', mntdir] + + try: + subprocess.check_output(mount_cmd, stderr=subprocess.STDOUT) + try: + yield mntdir + finally: + with suppress(): + subprocess.check_output(unmount_cmd, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + print 'failed to mount %s @ %s : %s' % (fs, mntdir, e.output) + raise + finally: + os.rmdir(mntdir) + + +# XXX On illumos it is impossible to explicitly mount a snapshot. +# So, either we need to implicitly mount it using .zfs/snapshot/ +# or we need to create a clone and mount it readonly (and discard +# it afterwards). +# At the moment the former approach is implemented. + +# This dictionary is used to keep track of mounted filesystems +# (not snapshots), so that we do not try to mount a filesystem +# more than once in the case more than one snapshot of the +# filesystem is accessed from the same context or the filesystem +# and its snapshot are accessed. +_mnttab = {} + + +def _illumos_mount_fs(fs): + if fs in _mnttab: + yield _mnttab[fs] + else: + with _zfs_mount(fs) as mntdir: + _mnttab[fs] = mntdir + try: + yield mntdir + finally: + _mnttab.pop(fs, None) + + +def _illumos_mount_snap(fs): + (base, snap) = fs.split('@', 1) + with _illumos_mount_fs(base) as mntdir: + yield os.path.join(mntdir, '.zfs', 'snapshot', snap) + + +def _zfs_mount_illumos(fs): + if '@' not in fs: + with _illumos_mount_fs(fs) as mntdir: + yield mntdir + else: + with _illumos_mount_snap(fs) as mntdir: + yield mntdir + + +if platform.system() == 'SunOS': + zfs_mount = _zfs_mount_illumos +else: + zfs_mount = _zfs_mount + + +def cleanup_fd(): + fd = os.open('/dev/zfs', os.O_EXCL) + try: + yield fd + finally: + os.close(fd) + + +def os_open(name, mode): + fd = os.open(name, mode) + try: + yield fd + finally: + os.close(fd) + + +def dev_null(): + with os_open('/dev/null', os.O_WRONLY) as fd: + yield fd + + +def dev_zero(): + with os_open('/dev/zero', os.O_RDONLY) as fd: + yield fd + + +def temp_file_in_fs(fs): + with zfs_mount(fs) as mntdir: + with tempfile.NamedTemporaryFile(dir=mntdir) as f: + for i in range(1024): + f.write('x' * 1024) + f.flush() + yield f.name + + +def make_snapshots(fs, before, modified, after): + def _maybe_snap(snap): + if snap is not None: + if not snap.startswith(fs): + snap = fs + '@' + snap + lzc.lzc_snapshot([snap]) + return snap + + before = _maybe_snap(before) + with temp_file_in_fs(fs) as name: + modified = _maybe_snap(modified) + after = _maybe_snap(after) + + return (name, (before, modified, after)) + + +def streams(fs, first, second): + (filename, snaps) = make_snapshots(fs, None, first, second) + with tempfile.TemporaryFile(suffix='.ztream') as full: + lzc.lzc_send(snaps[1], None, full.fileno()) + full.seek(0) + if snaps[2] is not None: + with tempfile.TemporaryFile(suffix='.ztream') as incremental: + lzc.lzc_send(snaps[2], snaps[1], incremental.fileno()) + incremental.seek(0) + yield (filename, (full, incremental)) + else: + yield (filename, (full, None)) + + +def runtimeSkipIf(check_method, message): + def _decorator(f): + def _f(_self, *args, **kwargs): + if check_method(_self): + return _self.skipTest(message) + else: + return f(_self, *args, **kwargs) + _f.__name__ = f.__name__ + return _f + return _decorator + + +def skipIfFeatureAvailable(feature, message): + return runtimeSkipIf(lambda _self: _self.__class__.pool.isPoolFeatureAvailable(feature), message) + + +def skipUnlessFeatureEnabled(feature, message): + return runtimeSkipIf(lambda _self: not _self.__class__.pool.isPoolFeatureEnabled(feature), message) + + +def skipUnlessBookmarksSupported(f): + return skipUnlessFeatureEnabled('bookmarks', 'bookmarks are not enabled')(f) + + +def snap_always_unmounted_before_destruction(): + # Apparently ZoL automatically unmounts the snapshot + # only if it is mounted at its default .zfs/snapshot + # mountpoint. + return (platform.system() != 'Linux', 'snapshot is not auto-unmounted') + + +def illumos_bug_6379(): + # zfs_ioc_hold() panics on a bad cleanup fd + return (platform.system() == 'SunOS', 'see https://www.illumos.org/issues/6379') + + +def needs_support(function): + return unittest.skipUnless(lzc.is_supported(function), + '{} not available'.format(function.__name__)) + + +class ZFSTest(unittest.TestCase): + POOL_FILE_SIZE = 128 * 1024 * 1024 + FILESYSTEMS = ['fs1', 'fs2', 'fs1/fs'] + + pool = None + misc_pool = None + readonly_pool = None + + @classmethod + def setUpClass(cls): + try: + cls.pool = _TempPool(filesystems=cls.FILESYSTEMS) + cls.misc_pool = _TempPool() + cls.readonly_pool = _TempPool( + filesystems=cls.FILESYSTEMS, readonly=True) + cls.pools = [cls.pool, cls.misc_pool, cls.readonly_pool] + except Exception: + cls._cleanUp() + raise + + @classmethod + def tearDownClass(cls): + cls._cleanUp() + + @classmethod + def _cleanUp(cls): + for pool in [cls.pool, cls.misc_pool, cls.readonly_pool]: + if pool is not None: + pool.cleanUp() + + def setUp(self): + pass + + def tearDown(self): + for pool in ZFSTest.pools: + pool.reset() + + def assertExists(self, name): + self.assertTrue( + lzc.lzc_exists(name), 'ZFS dataset %s does not exist' % (name, )) + + def assertNotExists(self, name): + self.assertFalse( + lzc.lzc_exists(name), 'ZFS dataset %s exists' % (name, )) + + def test_exists(self): + self.assertExists(ZFSTest.pool.makeName()) + + def test_exists_in_ro_pool(self): + self.assertExists(ZFSTest.readonly_pool.makeName()) + + def test_exists_failure(self): + self.assertNotExists(ZFSTest.pool.makeName('nonexistent')) + + def test_create_fs(self): + name = ZFSTest.pool.makeName("fs1/fs/test1") + + lzc.lzc_create(name) + self.assertExists(name) + + def test_create_zvol(self): + name = ZFSTest.pool.makeName("fs1/fs/zvol") + props = {"volsize": 1024 * 1024} + + lzc.lzc_create(name, ds_type='zvol', props=props) + self.assertExists(name) + # On Gentoo with ZFS 0.6.5.4 the volume is busy + # and can not be destroyed right after its creation. + # A reason for this is unknown at the moment. + # Because of that the post-test clean up could fail. + time.sleep(0.1) + + def test_create_fs_with_prop(self): + name = ZFSTest.pool.makeName("fs1/fs/test2") + props = {"atime": 0} + + lzc.lzc_create(name, props=props) + self.assertExists(name) + + def test_create_fs_wrong_ds_type(self): + name = ZFSTest.pool.makeName("fs1/fs/test1") + + with self.assertRaises(lzc_exc.DatasetTypeInvalid): + lzc.lzc_create(name, ds_type='wrong') + + @unittest.skip("https://www.illumos.org/issues/6101") + def test_create_fs_below_zvol(self): + name = ZFSTest.pool.makeName("fs1/fs/zvol") + props = {"volsize": 1024 * 1024} + + lzc.lzc_create(name, ds_type='zvol', props=props) + with self.assertRaises(lzc_exc.WrongParent): + lzc.lzc_create(name + '/fs') + + def test_create_fs_duplicate(self): + name = ZFSTest.pool.makeName("fs1/fs/test6") + + lzc.lzc_create(name) + + with self.assertRaises(lzc_exc.FilesystemExists): + lzc.lzc_create(name) + + def test_create_fs_in_ro_pool(self): + name = ZFSTest.readonly_pool.makeName("fs") + + with self.assertRaises(lzc_exc.ReadOnlyPool): + lzc.lzc_create(name) + + def test_create_fs_without_parent(self): + name = ZFSTest.pool.makeName("fs1/nonexistent/test") + + with self.assertRaises(lzc_exc.ParentNotFound): + lzc.lzc_create(name) + self.assertNotExists(name) + + def test_create_fs_in_nonexistent_pool(self): + name = "no-such-pool/fs" + + with self.assertRaises(lzc_exc.ParentNotFound): + lzc.lzc_create(name) + self.assertNotExists(name) + + def test_create_fs_with_invalid_prop(self): + name = ZFSTest.pool.makeName("fs1/fs/test3") + props = {"BOGUS": 0} + + with self.assertRaises(lzc_exc.PropertyInvalid): + lzc.lzc_create(name, 'zfs', props) + self.assertNotExists(name) + + def test_create_fs_with_invalid_prop_type(self): + name = ZFSTest.pool.makeName("fs1/fs/test4") + props = {"recordsize": "128k"} + + with self.assertRaises(lzc_exc.PropertyInvalid): + lzc.lzc_create(name, 'zfs', props) + self.assertNotExists(name) + + def test_create_fs_with_invalid_prop_val(self): + name = ZFSTest.pool.makeName("fs1/fs/test5") + props = {"atime": 20} + + with self.assertRaises(lzc_exc.PropertyInvalid): + lzc.lzc_create(name, 'zfs', props) + self.assertNotExists(name) + + def test_create_fs_with_invalid_name(self): + name = ZFSTest.pool.makeName("@badname") + + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_create(name) + self.assertNotExists(name) + + def test_create_fs_with_invalid_pool_name(self): + name = "bad!pool/fs" + + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_create(name) + self.assertNotExists(name) + + def test_snapshot(self): + snapname = ZFSTest.pool.makeName("@snap") + snaps = [snapname] + + lzc.lzc_snapshot(snaps) + self.assertExists(snapname) + + def test_snapshot_empty_list(self): + lzc.lzc_snapshot([]) + + def test_snapshot_user_props(self): + snapname = ZFSTest.pool.makeName("@snap") + snaps = [snapname] + props = {"user:foo": "bar"} + + lzc.lzc_snapshot(snaps, props) + self.assertExists(snapname) + + def test_snapshot_invalid_props(self): + snapname = ZFSTest.pool.makeName("@snap") + snaps = [snapname] + props = {"foo": "bar"} + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps, props) + + self.assertEquals(len(ctx.exception.errors), len(snaps)) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.PropertyInvalid) + self.assertNotExists(snapname) + + def test_snapshot_ro_pool(self): + snapname1 = ZFSTest.readonly_pool.makeName("@snap") + snapname2 = ZFSTest.readonly_pool.makeName("fs1@snap") + snaps = [snapname1, snapname2] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + # NB: one common error is reported. + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.ReadOnlyPool) + self.assertNotExists(snapname1) + self.assertNotExists(snapname2) + + def test_snapshot_nonexistent_pool(self): + snapname = "no-such-pool@snap" + snaps = [snapname] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.FilesystemNotFound) + + def test_snapshot_nonexistent_fs(self): + snapname = ZFSTest.pool.makeName("nonexistent@snap") + snaps = [snapname] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.FilesystemNotFound) + + def test_snapshot_nonexistent_and_existent_fs(self): + snapname1 = ZFSTest.pool.makeName("@snap") + snapname2 = ZFSTest.pool.makeName("nonexistent@snap") + snaps = [snapname1, snapname2] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.FilesystemNotFound) + self.assertNotExists(snapname1) + self.assertNotExists(snapname2) + + # FIXME: should not be failing + @unittest.expectedFailure + def test_multiple_snapshots_nonexistent_fs(self): + snapname1 = ZFSTest.pool.makeName("nonexistent@snap1") + snapname2 = ZFSTest.pool.makeName("nonexistent@snap2") + snaps = [snapname1, snapname2] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + # XXX two errors should be reported but alas + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.FilesystemNotFound) + self.assertNotExists(snapname1) + self.assertNotExists(snapname2) + + # FIXME: should not be failing + @unittest.expectedFailure + def test_multiple_snapshots_multiple_nonexistent_fs(self): + snapname1 = ZFSTest.pool.makeName("nonexistent1@snap") + snapname2 = ZFSTest.pool.makeName("nonexistent2@snap") + snaps = [snapname1, snapname2] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + # XXX two errors should be reported but alas + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.FilesystemNotFound) + self.assertNotExists(snapname1) + self.assertNotExists(snapname2) + + def test_snapshot_already_exists(self): + snapname = ZFSTest.pool.makeName("@snap") + snaps = [snapname] + + lzc.lzc_snapshot(snaps) + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.SnapshotExists) + + def test_multiple_snapshots_for_same_fs(self): + snapname1 = ZFSTest.pool.makeName("@snap1") + snapname2 = ZFSTest.pool.makeName("@snap2") + snaps = [snapname1, snapname2] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.DuplicateSnapshots) + self.assertNotExists(snapname1) + self.assertNotExists(snapname2) + + def test_multiple_snapshots(self): + snapname1 = ZFSTest.pool.makeName("@snap") + snapname2 = ZFSTest.pool.makeName("fs1@snap") + snaps = [snapname1, snapname2] + + lzc.lzc_snapshot(snaps) + self.assertExists(snapname1) + self.assertExists(snapname2) + + def test_multiple_existing_snapshots(self): + snapname1 = ZFSTest.pool.makeName("@snap") + snapname2 = ZFSTest.pool.makeName("fs1@snap") + snaps = [snapname1, snapname2] + + lzc.lzc_snapshot(snaps) + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + self.assertEqual(len(ctx.exception.errors), 2) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.SnapshotExists) + + def test_multiple_new_and_existing_snapshots(self): + snapname1 = ZFSTest.pool.makeName("@snap") + snapname2 = ZFSTest.pool.makeName("fs1@snap") + snapname3 = ZFSTest.pool.makeName("fs2@snap") + snaps = [snapname1, snapname2] + more_snaps = snaps + [snapname3] + + lzc.lzc_snapshot(snaps) + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(more_snaps) + + self.assertEqual(len(ctx.exception.errors), 2) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.SnapshotExists) + self.assertNotExists(snapname3) + + def test_snapshot_multiple_errors(self): + snapname1 = ZFSTest.pool.makeName("@snap") + snapname2 = ZFSTest.pool.makeName("nonexistent@snap") + snapname3 = ZFSTest.pool.makeName("fs1@snap") + snaps = [snapname1] + more_snaps = [snapname1, snapname2, snapname3] + + # create 'snapname1' snapshot + lzc.lzc_snapshot(snaps) + + # attempt to create 3 snapshots: + # 1. duplicate snapshot name + # 2. refers to filesystem that doesn't exist + # 3. could have succeeded if not for 1 and 2 + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(more_snaps) + + # It seems that FilesystemNotFound overrides the other error, + # but it doesn't have to. + self.assertGreater(len(ctx.exception.errors), 0) + for e in ctx.exception.errors: + self.assertIsInstance(e, (lzc_exc.SnapshotExists, lzc_exc.FilesystemNotFound)) + self.assertNotExists(snapname2) + self.assertNotExists(snapname3) + + def test_snapshot_different_pools(self): + snapname1 = ZFSTest.pool.makeName("@snap") + snapname2 = ZFSTest.misc_pool.makeName("@snap") + snaps = [snapname1, snapname2] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + # NB: one common error is reported. + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.PoolsDiffer) + self.assertNotExists(snapname1) + self.assertNotExists(snapname2) + + def test_snapshot_different_pools_ro_pool(self): + snapname1 = ZFSTest.pool.makeName("@snap") + snapname2 = ZFSTest.readonly_pool.makeName("@snap") + snaps = [snapname1, snapname2] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + # NB: one common error is reported. + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + # NB: depending on whether the first attempted snapshot is + # for the read-only pool a different error is reported. + self.assertIsInstance( + e, (lzc_exc.PoolsDiffer, lzc_exc.ReadOnlyPool)) + self.assertNotExists(snapname1) + self.assertNotExists(snapname2) + + def test_snapshot_invalid_name(self): + snapname1 = ZFSTest.pool.makeName("@bad&name") + snapname2 = ZFSTest.pool.makeName("fs1@bad*name") + snapname3 = ZFSTest.pool.makeName("fs2@snap") + snaps = [snapname1, snapname2, snapname3] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + # NB: one common error is reported. + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameInvalid) + self.assertIsNone(e.name) + + def test_snapshot_too_long_complete_name(self): + snapname1 = ZFSTest.pool.makeTooLongName("fs1@") + snapname2 = ZFSTest.pool.makeTooLongName("fs2@") + snapname3 = ZFSTest.pool.makeName("@snap") + snaps = [snapname1, snapname2, snapname3] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + self.assertEquals(len(ctx.exception.errors), 2) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameTooLong) + self.assertIsNotNone(e.name) + + def test_snapshot_too_long_snap_name(self): + snapname1 = ZFSTest.pool.makeTooLongComponent("fs1@") + snapname2 = ZFSTest.pool.makeTooLongComponent("fs2@") + snapname3 = ZFSTest.pool.makeName("@snap") + snaps = [snapname1, snapname2, snapname3] + + with self.assertRaises(lzc_exc.SnapshotFailure) as ctx: + lzc.lzc_snapshot(snaps) + + # NB: one common error is reported. + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameTooLong) + self.assertIsNone(e.name) + + def test_destroy_nonexistent_snapshot(self): + lzc.lzc_destroy_snaps([ZFSTest.pool.makeName("@nonexistent")], False) + lzc.lzc_destroy_snaps([ZFSTest.pool.makeName("@nonexistent")], True) + + def test_destroy_snapshot_of_nonexistent_pool(self): + with self.assertRaises(lzc_exc.SnapshotDestructionFailure) as ctx: + lzc.lzc_destroy_snaps(["no-such-pool@snap"], False) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.PoolNotFound) + + with self.assertRaises(lzc_exc.SnapshotDestructionFailure) as ctx: + lzc.lzc_destroy_snaps(["no-such-pool@snap"], True) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.PoolNotFound) + + # NB: note the difference from the nonexistent pool test. + def test_destroy_snapshot_of_nonexistent_fs(self): + lzc.lzc_destroy_snaps( + [ZFSTest.pool.makeName("nonexistent@snap")], False) + lzc.lzc_destroy_snaps( + [ZFSTest.pool.makeName("nonexistent@snap")], True) + + # Apparently the name is not checked for validity. + @unittest.expectedFailure + def test_destroy_invalid_snap_name(self): + with self.assertRaises(lzc_exc.SnapshotDestructionFailure): + lzc.lzc_destroy_snaps( + [ZFSTest.pool.makeName("@non$&*existent")], False) + with self.assertRaises(lzc_exc.SnapshotDestructionFailure): + lzc.lzc_destroy_snaps( + [ZFSTest.pool.makeName("@non$&*existent")], True) + + # Apparently the full name is not checked for length. + @unittest.expectedFailure + def test_destroy_too_long_full_snap_name(self): + snapname1 = ZFSTest.pool.makeTooLongName("fs1@") + snaps = [snapname1] + + with self.assertRaises(lzc_exc.SnapshotDestructionFailure): + lzc.lzc_destroy_snaps(snaps, False) + with self.assertRaises(lzc_exc.SnapshotDestructionFailure): + lzc.lzc_destroy_snaps(snaps, True) + + def test_destroy_too_long_short_snap_name(self): + snapname1 = ZFSTest.pool.makeTooLongComponent("fs1@") + snapname2 = ZFSTest.pool.makeTooLongComponent("fs2@") + snapname3 = ZFSTest.pool.makeName("@snap") + snaps = [snapname1, snapname2, snapname3] + + with self.assertRaises(lzc_exc.SnapshotDestructionFailure) as ctx: + lzc.lzc_destroy_snaps(snaps, False) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameTooLong) + + @unittest.skipUnless(*snap_always_unmounted_before_destruction()) + def test_destroy_mounted_snap(self): + snap = ZFSTest.pool.getRoot().getSnap() + + lzc.lzc_snapshot([snap]) + with zfs_mount(snap): + # the snapshot should be force-unmounted + lzc.lzc_destroy_snaps([snap], defer=False) + self.assertNotExists(snap) + + def test_clone(self): + # NB: note the special name for the snapshot. + # Since currently we can not destroy filesystems, + # it would be impossible to destroy the snapshot, + # so no point in attempting to clean it up. + snapname = ZFSTest.pool.makeName("fs2@origin1") + name = ZFSTest.pool.makeName("fs1/fs/clone1") + + lzc.lzc_snapshot([snapname]) + + lzc.lzc_clone(name, snapname) + self.assertExists(name) + + def test_clone_nonexistent_snapshot(self): + snapname = ZFSTest.pool.makeName("fs2@nonexistent") + name = ZFSTest.pool.makeName("fs1/fs/clone2") + + # XXX The error should be SnapshotNotFound + # but limitations of C interface do not allow + # to differentiate between the errors. + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_clone(name, snapname) + self.assertNotExists(name) + + def test_clone_nonexistent_parent_fs(self): + snapname = ZFSTest.pool.makeName("fs2@origin3") + name = ZFSTest.pool.makeName("fs1/nonexistent/clone3") + + lzc.lzc_snapshot([snapname]) + + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_clone(name, snapname) + self.assertNotExists(name) + + def test_clone_to_nonexistent_pool(self): + snapname = ZFSTest.pool.makeName("fs2@snap") + name = "no-such-pool/fs" + + lzc.lzc_snapshot([snapname]) + + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_clone(name, snapname) + self.assertNotExists(name) + + def test_clone_invalid_snap_name(self): + # Use a valid filesystem name of filesystem that + # exists as a snapshot name + snapname = ZFSTest.pool.makeName("fs1/fs") + name = ZFSTest.pool.makeName("fs2/clone") + + with self.assertRaises(lzc_exc.SnapshotNameInvalid): + lzc.lzc_clone(name, snapname) + self.assertNotExists(name) + + def test_clone_invalid_snap_name_2(self): + # Use a valid filesystem name of filesystem that + # doesn't exist as a snapshot name + snapname = ZFSTest.pool.makeName("fs1/nonexistent") + name = ZFSTest.pool.makeName("fs2/clone") + + with self.assertRaises(lzc_exc.SnapshotNameInvalid): + lzc.lzc_clone(name, snapname) + self.assertNotExists(name) + + def test_clone_invalid_name(self): + snapname = ZFSTest.pool.makeName("fs2@snap") + name = ZFSTest.pool.makeName("fs1/bad#name") + + lzc.lzc_snapshot([snapname]) + + with self.assertRaises(lzc_exc.FilesystemNameInvalid): + lzc.lzc_clone(name, snapname) + self.assertNotExists(name) + + def test_clone_invalid_pool_name(self): + snapname = ZFSTest.pool.makeName("fs2@snap") + name = "bad!pool/fs1" + + lzc.lzc_snapshot([snapname]) + + with self.assertRaises(lzc_exc.FilesystemNameInvalid): + lzc.lzc_clone(name, snapname) + self.assertNotExists(name) + + def test_clone_across_pools(self): + snapname = ZFSTest.pool.makeName("fs2@snap") + name = ZFSTest.misc_pool.makeName("clone1") + + lzc.lzc_snapshot([snapname]) + + with self.assertRaises(lzc_exc.PoolsDiffer): + lzc.lzc_clone(name, snapname) + self.assertNotExists(name) + + def test_clone_across_pools_to_ro_pool(self): + snapname = ZFSTest.pool.makeName("fs2@snap") + name = ZFSTest.readonly_pool.makeName("fs1/clone1") + + lzc.lzc_snapshot([snapname]) + + # it's legal to report either of the conditions + with self.assertRaises((lzc_exc.ReadOnlyPool, lzc_exc.PoolsDiffer)): + lzc.lzc_clone(name, snapname) + self.assertNotExists(name) + + def test_destroy_cloned_fs(self): + snapname1 = ZFSTest.pool.makeName("fs2@origin4") + snapname2 = ZFSTest.pool.makeName("fs1@snap") + clonename = ZFSTest.pool.makeName("fs1/fs/clone4") + snaps = [snapname1, snapname2] + + lzc.lzc_snapshot(snaps) + lzc.lzc_clone(clonename, snapname1) + + with self.assertRaises(lzc_exc.SnapshotDestructionFailure) as ctx: + lzc.lzc_destroy_snaps(snaps, False) + + self.assertEquals(len(ctx.exception.errors), 1) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.SnapshotIsCloned) + for snap in snaps: + self.assertExists(snap) + + def test_deferred_destroy_cloned_fs(self): + snapname1 = ZFSTest.pool.makeName("fs2@origin5") + snapname2 = ZFSTest.pool.makeName("fs1@snap") + clonename = ZFSTest.pool.makeName("fs1/fs/clone5") + snaps = [snapname1, snapname2] + + lzc.lzc_snapshot(snaps) + lzc.lzc_clone(clonename, snapname1) + + lzc.lzc_destroy_snaps(snaps, defer=True) + + self.assertExists(snapname1) + self.assertNotExists(snapname2) + + def test_rollback(self): + name = ZFSTest.pool.makeName("fs1") + snapname = name + "@snap" + + lzc.lzc_snapshot([snapname]) + ret = lzc.lzc_rollback(name) + self.assertEqual(ret, snapname) + + def test_rollback_2(self): + name = ZFSTest.pool.makeName("fs1") + snapname1 = name + "@snap1" + snapname2 = name + "@snap2" + + lzc.lzc_snapshot([snapname1]) + lzc.lzc_snapshot([snapname2]) + ret = lzc.lzc_rollback(name) + self.assertEqual(ret, snapname2) + + # FIXME: should not be failing + @unittest.expectedFailure + def test_rollback_no_snaps(self): + name = ZFSTest.pool.makeName("fs1") + + with self.assertRaises(lzc_exc.SnapshotNotFound): + lzc.lzc_rollback(name) + + def test_rollback_non_existent_fs(self): + name = ZFSTest.pool.makeName("nonexistent") + + with self.assertRaises(lzc_exc.FilesystemNotFound): + lzc.lzc_rollback(name) + + def test_rollback_invalid_fs_name(self): + name = ZFSTest.pool.makeName("bad~name") + + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_rollback(name) + + def test_rollback_snap_name(self): + name = ZFSTest.pool.makeName("fs1@snap") + + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_rollback(name) + + def test_rollback_snap_name_2(self): + name = ZFSTest.pool.makeName("fs1@snap") + + lzc.lzc_snapshot([name]) + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_rollback(name) + + def test_rollback_too_long_fs_name(self): + name = ZFSTest.pool.makeTooLongName() + + with self.assertRaises(lzc_exc.NameTooLong): + lzc.lzc_rollback(name) + + def test_rollback_to_snap_name(self): + name = ZFSTest.pool.makeName("fs1") + snap = name + "@snap" + + lzc.lzc_snapshot([snap]) + lzc.lzc_rollback_to(name, snap) + + def test_rollback_to_not_latest(self): + fsname = ZFSTest.pool.makeName('fs1') + snap1 = fsname + "@snap1" + snap2 = fsname + "@snap2" + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + with self.assertRaises(lzc_exc.SnapshotNotLatest): + lzc.lzc_rollback_to(fsname, fsname + "@snap1") + + @skipUnlessBookmarksSupported + def test_bookmarks(self): + snaps = [ZFSTest.pool.makeName( + 'fs1@snap1'), ZFSTest.pool.makeName('fs2@snap1')] + bmarks = [ZFSTest.pool.makeName( + 'fs1#bmark1'), ZFSTest.pool.makeName('fs2#bmark1')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps) + lzc.lzc_bookmark(bmark_dict) + + @skipUnlessBookmarksSupported + def test_bookmarks_2(self): + snaps = [ZFSTest.pool.makeName( + 'fs1@snap1'), ZFSTest.pool.makeName('fs2@snap1')] + bmarks = [ZFSTest.pool.makeName( + 'fs1#bmark1'), ZFSTest.pool.makeName('fs2#bmark1')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps) + lzc.lzc_bookmark(bmark_dict) + lzc.lzc_destroy_snaps(snaps, defer=False) + + @skipUnlessBookmarksSupported + def test_bookmarks_empty(self): + lzc.lzc_bookmark({}) + + @skipUnlessBookmarksSupported + def test_bookmarks_mismatching_name(self): + snaps = [ZFSTest.pool.makeName('fs1@snap1')] + bmarks = [ZFSTest.pool.makeName('fs2#bmark1')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.BookmarkMismatch) + + @skipUnlessBookmarksSupported + def test_bookmarks_invalid_name(self): + snaps = [ZFSTest.pool.makeName('fs1@snap1')] + bmarks = [ZFSTest.pool.makeName('fs1#bmark!')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameInvalid) + + @skipUnlessBookmarksSupported + def test_bookmarks_invalid_name_2(self): + snaps = [ZFSTest.pool.makeName('fs1@snap1')] + bmarks = [ZFSTest.pool.makeName('fs1@bmark')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameInvalid) + + @skipUnlessBookmarksSupported + def test_bookmarks_too_long_name(self): + snaps = [ZFSTest.pool.makeName('fs1@snap1')] + bmarks = [ZFSTest.pool.makeTooLongName('fs1#')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameTooLong) + + @skipUnlessBookmarksSupported + def test_bookmarks_too_long_name_2(self): + snaps = [ZFSTest.pool.makeName('fs1@snap1')] + bmarks = [ZFSTest.pool.makeTooLongComponent('fs1#')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameTooLong) + + @skipUnlessBookmarksSupported + def test_bookmarks_mismatching_names(self): + snaps = [ZFSTest.pool.makeName( + 'fs1@snap1'), ZFSTest.pool.makeName('fs2@snap1')] + bmarks = [ZFSTest.pool.makeName( + 'fs2#bmark1'), ZFSTest.pool.makeName('fs1#bmark1')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.BookmarkMismatch) + + @skipUnlessBookmarksSupported + def test_bookmarks_partially_mismatching_names(self): + snaps = [ZFSTest.pool.makeName( + 'fs1@snap1'), ZFSTest.pool.makeName('fs2@snap1')] + bmarks = [ZFSTest.pool.makeName( + 'fs2#bmark'), ZFSTest.pool.makeName('fs2#bmark1')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.BookmarkMismatch) + + @skipUnlessBookmarksSupported + def test_bookmarks_cross_pool(self): + snaps = [ZFSTest.pool.makeName( + 'fs1@snap1'), ZFSTest.misc_pool.makeName('@snap1')] + bmarks = [ZFSTest.pool.makeName( + 'fs1#bmark1'), ZFSTest.misc_pool.makeName('#bmark1')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps[0:1]) + lzc.lzc_snapshot(snaps[1:2]) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.PoolsDiffer) + + @skipUnlessBookmarksSupported + def test_bookmarks_missing_snap(self): + snaps = [ZFSTest.pool.makeName( + 'fs1@snap1'), ZFSTest.pool.makeName('fs2@snap1')] + bmarks = [ZFSTest.pool.makeName( + 'fs1#bmark1'), ZFSTest.pool.makeName('fs2#bmark1')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + lzc.lzc_snapshot(snaps[0:1]) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.SnapshotNotFound) + + @skipUnlessBookmarksSupported + def test_bookmarks_missing_snaps(self): + snaps = [ZFSTest.pool.makeName( + 'fs1@snap1'), ZFSTest.pool.makeName('fs2@snap1')] + bmarks = [ZFSTest.pool.makeName( + 'fs1#bmark1'), ZFSTest.pool.makeName('fs2#bmark1')] + bmark_dict = {x: y for x, y in zip(bmarks, snaps)} + + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.SnapshotNotFound) + + @skipUnlessBookmarksSupported + def test_bookmarks_for_the_same_snap(self): + snap = ZFSTest.pool.makeName('fs1@snap1') + bmark1 = ZFSTest.pool.makeName('fs1#bmark1') + bmark2 = ZFSTest.pool.makeName('fs1#bmark2') + bmark_dict = {bmark1: snap, bmark2: snap} + + lzc.lzc_snapshot([snap]) + lzc.lzc_bookmark(bmark_dict) + + @skipUnlessBookmarksSupported + def test_bookmarks_for_the_same_snap_2(self): + snap = ZFSTest.pool.makeName('fs1@snap1') + bmark1 = ZFSTest.pool.makeName('fs1#bmark1') + bmark2 = ZFSTest.pool.makeName('fs1#bmark2') + bmark_dict1 = {bmark1: snap} + bmark_dict2 = {bmark2: snap} + + lzc.lzc_snapshot([snap]) + lzc.lzc_bookmark(bmark_dict1) + lzc.lzc_bookmark(bmark_dict2) + + @skipUnlessBookmarksSupported + def test_bookmarks_duplicate_name(self): + snap1 = ZFSTest.pool.makeName('fs1@snap1') + snap2 = ZFSTest.pool.makeName('fs1@snap2') + bmark = ZFSTest.pool.makeName('fs1#bmark') + bmark_dict1 = {bmark: snap1} + bmark_dict2 = {bmark: snap2} + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + lzc.lzc_bookmark(bmark_dict1) + with self.assertRaises(lzc_exc.BookmarkFailure) as ctx: + lzc.lzc_bookmark(bmark_dict2) + + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.BookmarkExists) + + @skipUnlessBookmarksSupported + def test_get_bookmarks(self): + snap1 = ZFSTest.pool.makeName('fs1@snap1') + snap2 = ZFSTest.pool.makeName('fs1@snap2') + bmark = ZFSTest.pool.makeName('fs1#bmark') + bmark1 = ZFSTest.pool.makeName('fs1#bmark1') + bmark2 = ZFSTest.pool.makeName('fs1#bmark2') + bmark_dict1 = {bmark1: snap1, bmark2: snap2} + bmark_dict2 = {bmark: snap2} + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + lzc.lzc_bookmark(bmark_dict1) + lzc.lzc_bookmark(bmark_dict2) + lzc.lzc_destroy_snaps([snap1, snap2], defer=False) + + bmarks = lzc.lzc_get_bookmarks(ZFSTest.pool.makeName('fs1')) + self.assertEquals(len(bmarks), 3) + for b in 'bmark', 'bmark1', 'bmark2': + self.assertIn(b, bmarks) + self.assertIsInstance(bmarks[b], dict) + self.assertEquals(len(bmarks[b]), 0) + + bmarks = lzc.lzc_get_bookmarks( + ZFSTest.pool.makeName('fs1'), ['guid', 'createtxg', 'creation']) + self.assertEquals(len(bmarks), 3) + for b in 'bmark', 'bmark1', 'bmark2': + self.assertIn(b, bmarks) + self.assertIsInstance(bmarks[b], dict) + self.assertEquals(len(bmarks[b]), 3) + + @skipUnlessBookmarksSupported + def test_get_bookmarks_invalid_property(self): + snap = ZFSTest.pool.makeName('fs1@snap') + bmark = ZFSTest.pool.makeName('fs1#bmark') + bmark_dict = {bmark: snap} + + lzc.lzc_snapshot([snap]) + lzc.lzc_bookmark(bmark_dict) + + bmarks = lzc.lzc_get_bookmarks( + ZFSTest.pool.makeName('fs1'), ['badprop']) + self.assertEquals(len(bmarks), 1) + for b in ('bmark', ): + self.assertIn(b, bmarks) + self.assertIsInstance(bmarks[b], dict) + self.assertEquals(len(bmarks[b]), 0) + + @skipUnlessBookmarksSupported + def test_get_bookmarks_nonexistent_fs(self): + with self.assertRaises(lzc_exc.FilesystemNotFound): + lzc.lzc_get_bookmarks(ZFSTest.pool.makeName('nonexistent')) + + @skipUnlessBookmarksSupported + def test_destroy_bookmarks(self): + snap = ZFSTest.pool.makeName('fs1@snap') + bmark = ZFSTest.pool.makeName('fs1#bmark') + bmark_dict = {bmark: snap} + + lzc.lzc_snapshot([snap]) + lzc.lzc_bookmark(bmark_dict) + + lzc.lzc_destroy_bookmarks( + [bmark, ZFSTest.pool.makeName('fs1#nonexistent')]) + bmarks = lzc.lzc_get_bookmarks(ZFSTest.pool.makeName('fs1')) + self.assertEquals(len(bmarks), 0) + + @skipUnlessBookmarksSupported + def test_destroy_bookmarks_invalid_name(self): + snap = ZFSTest.pool.makeName('fs1@snap') + bmark = ZFSTest.pool.makeName('fs1#bmark') + bmark_dict = {bmark: snap} + + lzc.lzc_snapshot([snap]) + lzc.lzc_bookmark(bmark_dict) + + with self.assertRaises(lzc_exc.BookmarkDestructionFailure) as ctx: + lzc.lzc_destroy_bookmarks( + [bmark, ZFSTest.pool.makeName('fs1/nonexistent')]) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameInvalid) + + bmarks = lzc.lzc_get_bookmarks(ZFSTest.pool.makeName('fs1')) + self.assertEquals(len(bmarks), 1) + self.assertIn('bmark', bmarks) + + @skipUnlessBookmarksSupported + def test_destroy_bookmark_nonexistent_fs(self): + lzc.lzc_destroy_bookmarks([ZFSTest.pool.makeName('nonexistent#bmark')]) + + @skipUnlessBookmarksSupported + def test_destroy_bookmarks_empty(self): + lzc.lzc_bookmark({}) + + def test_snaprange_space(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + snap3 = ZFSTest.pool.makeName("fs1@snap") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + lzc.lzc_snapshot([snap3]) + + space = lzc.lzc_snaprange_space(snap1, snap2) + self.assertIsInstance(space, (int, long)) + space = lzc.lzc_snaprange_space(snap2, snap3) + self.assertIsInstance(space, (int, long)) + space = lzc.lzc_snaprange_space(snap1, snap3) + self.assertIsInstance(space, (int, long)) + + def test_snaprange_space_2(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + snap3 = ZFSTest.pool.makeName("fs1@snap") + + lzc.lzc_snapshot([snap1]) + with zfs_mount(ZFSTest.pool.makeName("fs1")) as mntdir: + with tempfile.NamedTemporaryFile(dir=mntdir) as f: + for i in range(1024): + f.write('x' * 1024) + f.flush() + lzc.lzc_snapshot([snap2]) + lzc.lzc_snapshot([snap3]) + + space = lzc.lzc_snaprange_space(snap1, snap2) + self.assertGreater(space, 1024 * 1024) + space = lzc.lzc_snaprange_space(snap2, snap3) + self.assertGreater(space, 1024 * 1024) + space = lzc.lzc_snaprange_space(snap1, snap3) + self.assertGreater(space, 1024 * 1024) + + def test_snaprange_space_same_snap(self): + snap = ZFSTest.pool.makeName("fs1@snap") + + with zfs_mount(ZFSTest.pool.makeName("fs1")) as mntdir: + with tempfile.NamedTemporaryFile(dir=mntdir) as f: + for i in range(1024): + f.write('x' * 1024) + f.flush() + lzc.lzc_snapshot([snap]) + + space = lzc.lzc_snaprange_space(snap, snap) + self.assertGreater(space, 1024 * 1024) + self.assertAlmostEqual(space, 1024 * 1024, delta=1024 * 1024 / 20) + + def test_snaprange_space_wrong_order(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with self.assertRaises(lzc_exc.SnapshotMismatch): + lzc.lzc_snaprange_space(snap2, snap1) + + def test_snaprange_space_unrelated(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs2@snap2") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with self.assertRaises(lzc_exc.SnapshotMismatch): + lzc.lzc_snaprange_space(snap1, snap2) + + def test_snaprange_space_across_pools(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.misc_pool.makeName("@snap2") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with self.assertRaises(lzc_exc.PoolsDiffer): + lzc.lzc_snaprange_space(snap1, snap2) + + def test_snaprange_space_nonexistent(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + + lzc.lzc_snapshot([snap1]) + + with self.assertRaises(lzc_exc.SnapshotNotFound) as ctx: + lzc.lzc_snaprange_space(snap1, snap2) + self.assertEquals(ctx.exception.name, snap2) + + with self.assertRaises(lzc_exc.SnapshotNotFound) as ctx: + lzc.lzc_snaprange_space(snap2, snap1) + self.assertEquals(ctx.exception.name, snap1) + + def test_snaprange_space_invalid_name(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@sn#p") + + lzc.lzc_snapshot([snap1]) + + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_snaprange_space(snap1, snap2) + + def test_snaprange_space_not_snap(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1") + + lzc.lzc_snapshot([snap1]) + + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_snaprange_space(snap1, snap2) + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_snaprange_space(snap2, snap1) + + def test_snaprange_space_not_snap_2(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1#bmark") + + lzc.lzc_snapshot([snap1]) + + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_snaprange_space(snap1, snap2) + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_snaprange_space(snap2, snap1) + + def test_send_space(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + snap3 = ZFSTest.pool.makeName("fs1@snap") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + lzc.lzc_snapshot([snap3]) + + space = lzc.lzc_send_space(snap2, snap1) + self.assertIsInstance(space, (int, long)) + space = lzc.lzc_send_space(snap3, snap2) + self.assertIsInstance(space, (int, long)) + space = lzc.lzc_send_space(snap3, snap1) + self.assertIsInstance(space, (int, long)) + space = lzc.lzc_send_space(snap1) + self.assertIsInstance(space, (int, long)) + space = lzc.lzc_send_space(snap2) + self.assertIsInstance(space, (int, long)) + space = lzc.lzc_send_space(snap3) + self.assertIsInstance(space, (int, long)) + + def test_send_space_2(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + snap3 = ZFSTest.pool.makeName("fs1@snap") + + lzc.lzc_snapshot([snap1]) + with zfs_mount(ZFSTest.pool.makeName("fs1")) as mntdir: + with tempfile.NamedTemporaryFile(dir=mntdir) as f: + for i in range(1024): + f.write('x' * 1024) + f.flush() + lzc.lzc_snapshot([snap2]) + lzc.lzc_snapshot([snap3]) + + space = lzc.lzc_send_space(snap2, snap1) + self.assertGreater(space, 1024 * 1024) + + space = lzc.lzc_send_space(snap3, snap2) + + space = lzc.lzc_send_space(snap3, snap1) + + space_empty = lzc.lzc_send_space(snap1) + + space = lzc.lzc_send_space(snap2) + self.assertGreater(space, 1024 * 1024) + + space = lzc.lzc_send_space(snap3) + self.assertEquals(space, space_empty) + + def test_send_space_same_snap(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + lzc.lzc_snapshot([snap1]) + with self.assertRaises(lzc_exc.SnapshotMismatch): + lzc.lzc_send_space(snap1, snap1) + + def test_send_space_wrong_order(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with self.assertRaises(lzc_exc.SnapshotMismatch): + lzc.lzc_send_space(snap1, snap2) + + def test_send_space_unrelated(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs2@snap2") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with self.assertRaises(lzc_exc.SnapshotMismatch): + lzc.lzc_send_space(snap1, snap2) + + def test_send_space_across_pools(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.misc_pool.makeName("@snap2") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with self.assertRaises(lzc_exc.PoolsDiffer): + lzc.lzc_send_space(snap1, snap2) + + def test_send_space_nonexistent(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs2@snap2") + + lzc.lzc_snapshot([snap1]) + + with self.assertRaises(lzc_exc.SnapshotNotFound) as ctx: + lzc.lzc_send_space(snap1, snap2) + self.assertEquals(ctx.exception.name, snap1) + + with self.assertRaises(lzc_exc.SnapshotNotFound) as ctx: + lzc.lzc_send_space(snap2, snap1) + self.assertEquals(ctx.exception.name, snap2) + + with self.assertRaises(lzc_exc.SnapshotNotFound) as ctx: + lzc.lzc_send_space(snap2) + self.assertEquals(ctx.exception.name, snap2) + + def test_send_space_invalid_name(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@sn!p") + + lzc.lzc_snapshot([snap1]) + + with self.assertRaises(lzc_exc.NameInvalid) as ctx: + lzc.lzc_send_space(snap2, snap1) + self.assertEquals(ctx.exception.name, snap2) + with self.assertRaises(lzc_exc.NameInvalid) as ctx: + lzc.lzc_send_space(snap2) + self.assertEquals(ctx.exception.name, snap2) + with self.assertRaises(lzc_exc.NameInvalid) as ctx: + lzc.lzc_send_space(snap1, snap2) + self.assertEquals(ctx.exception.name, snap2) + + def test_send_space_not_snap(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1") + + lzc.lzc_snapshot([snap1]) + + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_send_space(snap1, snap2) + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_send_space(snap2, snap1) + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_send_space(snap2) + + def test_send_space_not_snap_2(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1#bmark") + + lzc.lzc_snapshot([snap1]) + + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_send_space(snap2, snap1) + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_send_space(snap2) + + def test_send_full(self): + snap = ZFSTest.pool.makeName("fs1@snap") + + with zfs_mount(ZFSTest.pool.makeName("fs1")) as mntdir: + with tempfile.NamedTemporaryFile(dir=mntdir) as f: + for i in range(1024): + f.write('x' * 1024) + f.flush() + lzc.lzc_snapshot([snap]) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + estimate = lzc.lzc_send_space(snap) + + fd = output.fileno() + lzc.lzc_send(snap, None, fd) + st = os.fstat(fd) + # 5%, arbitrary. + self.assertAlmostEqual(st.st_size, estimate, delta=estimate / 20) + + def test_send_incremental(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + + lzc.lzc_snapshot([snap1]) + with zfs_mount(ZFSTest.pool.makeName("fs1")) as mntdir: + with tempfile.NamedTemporaryFile(dir=mntdir) as f: + for i in range(1024): + f.write('x' * 1024) + f.flush() + lzc.lzc_snapshot([snap2]) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + estimate = lzc.lzc_send_space(snap2, snap1) + + fd = output.fileno() + lzc.lzc_send(snap2, snap1, fd) + st = os.fstat(fd) + # 5%, arbitrary. + self.assertAlmostEqual(st.st_size, estimate, delta=estimate / 20) + + def test_send_flags(self): + snap = ZFSTest.pool.makeName("fs1@snap") + lzc.lzc_snapshot([snap]) + with dev_null() as fd: + lzc.lzc_send(snap, None, fd, ['large_blocks']) + lzc.lzc_send(snap, None, fd, ['embedded_data']) + lzc.lzc_send(snap, None, fd, ['embedded_data', 'large_blocks']) + + def test_send_unknown_flags(self): + snap = ZFSTest.pool.makeName("fs1@snap") + lzc.lzc_snapshot([snap]) + with dev_null() as fd: + with self.assertRaises(lzc_exc.UnknownStreamFeature): + lzc.lzc_send(snap, None, fd, ['embedded_data', 'UNKNOWN']) + + def test_send_same_snap(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + lzc.lzc_snapshot([snap1]) + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + with self.assertRaises(lzc_exc.SnapshotMismatch): + lzc.lzc_send(snap1, snap1, fd) + + def test_send_wrong_order(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + with self.assertRaises(lzc_exc.SnapshotMismatch): + lzc.lzc_send(snap1, snap2, fd) + + def test_send_unrelated(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs2@snap2") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + with self.assertRaises(lzc_exc.SnapshotMismatch): + lzc.lzc_send(snap1, snap2, fd) + + def test_send_across_pools(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.misc_pool.makeName("@snap2") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + with self.assertRaises(lzc_exc.PoolsDiffer): + lzc.lzc_send(snap1, snap2, fd) + + def test_send_nonexistent(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + + lzc.lzc_snapshot([snap1]) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + with self.assertRaises(lzc_exc.SnapshotNotFound) as ctx: + lzc.lzc_send(snap1, snap2, fd) + self.assertEquals(ctx.exception.name, snap1) + + with self.assertRaises(lzc_exc.SnapshotNotFound) as ctx: + lzc.lzc_send(snap2, snap1, fd) + self.assertEquals(ctx.exception.name, snap2) + + with self.assertRaises(lzc_exc.SnapshotNotFound) as ctx: + lzc.lzc_send(snap2, None, fd) + self.assertEquals(ctx.exception.name, snap2) + + def test_send_invalid_name(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@sn!p") + + lzc.lzc_snapshot([snap1]) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + with self.assertRaises(lzc_exc.NameInvalid) as ctx: + lzc.lzc_send(snap2, snap1, fd) + self.assertEquals(ctx.exception.name, snap2) + with self.assertRaises(lzc_exc.NameInvalid) as ctx: + lzc.lzc_send(snap2, None, fd) + self.assertEquals(ctx.exception.name, snap2) + with self.assertRaises(lzc_exc.NameInvalid) as ctx: + lzc.lzc_send(snap1, snap2, fd) + self.assertEquals(ctx.exception.name, snap2) + + # XXX Although undocumented the API allows to create an incremental + # or full stream for a filesystem as if a temporary unnamed snapshot + # is taken at some time after the call is made and before the stream + # starts being produced. + def test_send_filesystem(self): + snap = ZFSTest.pool.makeName("fs1@snap1") + fs = ZFSTest.pool.makeName("fs1") + + lzc.lzc_snapshot([snap]) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + lzc.lzc_send(fs, snap, fd) + lzc.lzc_send(fs, None, fd) + + def test_send_from_filesystem(self): + snap = ZFSTest.pool.makeName("fs1@snap1") + fs = ZFSTest.pool.makeName("fs1") + + lzc.lzc_snapshot([snap]) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_send(snap, fs, fd) + + @skipUnlessBookmarksSupported + def test_send_bookmark(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + bmark = ZFSTest.pool.makeName("fs1#bmark") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + lzc.lzc_bookmark({bmark: snap2}) + lzc.lzc_destroy_snaps([snap2], defer=False) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_send(bmark, snap1, fd) + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_send(bmark, None, fd) + + @skipUnlessBookmarksSupported + def test_send_from_bookmark(self): + snap1 = ZFSTest.pool.makeName("fs1@snap1") + snap2 = ZFSTest.pool.makeName("fs1@snap2") + bmark = ZFSTest.pool.makeName("fs1#bmark") + + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + lzc.lzc_bookmark({bmark: snap1}) + lzc.lzc_destroy_snaps([snap1], defer=False) + + with tempfile.TemporaryFile(suffix='.ztream') as output: + fd = output.fileno() + lzc.lzc_send(snap2, bmark, fd) + + def test_send_bad_fd(self): + snap = ZFSTest.pool.makeName("fs1@snap") + lzc.lzc_snapshot([snap]) + + with tempfile.TemporaryFile() as tmp: + bad_fd = tmp.fileno() + + with self.assertRaises(lzc_exc.StreamIOError) as ctx: + lzc.lzc_send(snap, None, bad_fd) + self.assertEquals(ctx.exception.errno, errno.EBADF) + + def test_send_bad_fd_2(self): + snap = ZFSTest.pool.makeName("fs1@snap") + lzc.lzc_snapshot([snap]) + + with self.assertRaises(lzc_exc.StreamIOError) as ctx: + lzc.lzc_send(snap, None, -2) + self.assertEquals(ctx.exception.errno, errno.EBADF) + + def test_send_bad_fd_3(self): + snap = ZFSTest.pool.makeName("fs1@snap") + lzc.lzc_snapshot([snap]) + + with tempfile.TemporaryFile() as tmp: + bad_fd = tmp.fileno() + + (soft, hard) = resource.getrlimit(resource.RLIMIT_NOFILE) + bad_fd = hard + 1 + with self.assertRaises(lzc_exc.StreamIOError) as ctx: + lzc.lzc_send(snap, None, bad_fd) + self.assertEquals(ctx.exception.errno, errno.EBADF) + + def test_send_to_broken_pipe(self): + snap = ZFSTest.pool.makeName("fs1@snap") + lzc.lzc_snapshot([snap]) + + proc = subprocess.Popen(['true'], stdin=subprocess.PIPE) + proc.wait() + with self.assertRaises(lzc_exc.StreamIOError) as ctx: + lzc.lzc_send(snap, None, proc.stdin.fileno()) + self.assertEquals(ctx.exception.errno, errno.EPIPE) + + def test_send_to_broken_pipe_2(self): + snap = ZFSTest.pool.makeName("fs1@snap") + with zfs_mount(ZFSTest.pool.makeName("fs1")) as mntdir: + with tempfile.NamedTemporaryFile(dir=mntdir) as f: + for i in range(1024): + f.write('x' * 1024) + f.flush() + lzc.lzc_snapshot([snap]) + + proc = subprocess.Popen(['sleep', '2'], stdin=subprocess.PIPE) + with self.assertRaises(lzc_exc.StreamIOError) as ctx: + lzc.lzc_send(snap, None, proc.stdin.fileno()) + self.assertTrue(ctx.exception.errno == errno.EPIPE or + ctx.exception.errno == errno.EINTR) + + def test_send_to_ro_file(self): + snap = ZFSTest.pool.makeName("fs1@snap") + lzc.lzc_snapshot([snap]) + + with tempfile.NamedTemporaryFile(suffix='.ztream', delete=False) as output: + # tempfile always opens a temporary file in read-write mode + # regardless of the specified mode, so we have to open it again. + os.chmod(output.name, stat.S_IRUSR) + fd = os.open(output.name, os.O_RDONLY) + with self.assertRaises(lzc_exc.StreamIOError) as ctx: + lzc.lzc_send(snap, None, fd) + os.close(fd) + self.assertEquals(ctx.exception.errno, errno.EBADF) + + def test_recv_full(self): + src = ZFSTest.pool.makeName("fs1@snap") + dst = ZFSTest.pool.makeName("fs2/received-1@snap") + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")) as name: + lzc.lzc_snapshot([src]) + + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(dst, stream.fileno()) + + name = os.path.basename(name) + with zfs_mount(src) as mnt1, zfs_mount(dst) as mnt2: + self.assertTrue( + filecmp.cmp(os.path.join(mnt1, name), os.path.join(mnt2, name), False)) + + def test_recv_incremental(self): + src1 = ZFSTest.pool.makeName("fs1@snap1") + src2 = ZFSTest.pool.makeName("fs1@snap2") + dst1 = ZFSTest.pool.makeName("fs2/received-2@snap1") + dst2 = ZFSTest.pool.makeName("fs2/received-2@snap2") + + lzc.lzc_snapshot([src1]) + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")) as name: + lzc.lzc_snapshot([src2]) + + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src1, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(dst1, stream.fileno()) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src2, src1, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(dst2, stream.fileno()) + + name = os.path.basename(name) + with zfs_mount(src2) as mnt1, zfs_mount(dst2) as mnt2: + self.assertTrue( + filecmp.cmp(os.path.join(mnt1, name), os.path.join(mnt2, name), False)) + + def test_recv_clone(self): + orig_src = ZFSTest.pool.makeName("fs2@send-origin") + clone = ZFSTest.pool.makeName("fs1/fs/send-clone") + clone_snap = clone + "@snap" + orig_dst = ZFSTest.pool.makeName("fs1/fs/recv-origin@snap") + clone_dst = ZFSTest.pool.makeName("fs1/fs/recv-clone@snap") + + lzc.lzc_snapshot([orig_src]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(orig_src, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(orig_dst, stream.fileno()) + + lzc.lzc_clone(clone, orig_src) + lzc.lzc_snapshot([clone_snap]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(clone_snap, orig_src, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(clone_dst, stream.fileno(), origin=orig_dst) + + def test_recv_full_already_existing_empty_fs(self): + src = ZFSTest.pool.makeName("fs1@snap") + dstfs = ZFSTest.pool.makeName("fs2/received-3") + dst = dstfs + '@snap' + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + lzc.lzc_create(dstfs) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + with self.assertRaises((lzc_exc.DestinationModified, lzc_exc.DatasetExists)): + lzc.lzc_receive(dst, stream.fileno()) + + def test_recv_full_into_root_empty_pool(self): + empty_pool = None + try: + srcfs = ZFSTest.pool.makeName("fs1") + empty_pool = _TempPool() + dst = empty_pool.makeName('@snap') + + with streams(srcfs, "snap", None) as (_, (stream, _)): + with self.assertRaises((lzc_exc.DestinationModified, lzc_exc.DatasetExists)): + lzc.lzc_receive(dst, stream.fileno()) + finally: + if empty_pool is not None: + empty_pool.cleanUp() + + def test_recv_full_into_ro_pool(self): + srcfs = ZFSTest.pool.makeName("fs1") + dst = ZFSTest.readonly_pool.makeName('fs2/received@snap') + + with streams(srcfs, "snap", None) as (_, (stream, _)): + with self.assertRaises(lzc_exc.ReadOnlyPool): + lzc.lzc_receive(dst, stream.fileno()) + + def test_recv_full_already_existing_modified_fs(self): + src = ZFSTest.pool.makeName("fs1@snap") + dstfs = ZFSTest.pool.makeName("fs2/received-5") + dst = dstfs + '@snap' + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + lzc.lzc_create(dstfs) + with temp_file_in_fs(dstfs): + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + with self.assertRaises((lzc_exc.DestinationModified, lzc_exc.DatasetExists)): + lzc.lzc_receive(dst, stream.fileno()) + + def test_recv_full_already_existing_with_snapshots(self): + src = ZFSTest.pool.makeName("fs1@snap") + dstfs = ZFSTest.pool.makeName("fs2/received-4") + dst = dstfs + '@snap' + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + lzc.lzc_create(dstfs) + lzc.lzc_snapshot([dstfs + "@snap1"]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + with self.assertRaises((lzc_exc.StreamMismatch, lzc_exc.DatasetExists)): + lzc.lzc_receive(dst, stream.fileno()) + + def test_recv_full_already_existing_snapshot(self): + src = ZFSTest.pool.makeName("fs1@snap") + dstfs = ZFSTest.pool.makeName("fs2/received-6") + dst = dstfs + '@snap' + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + lzc.lzc_create(dstfs) + lzc.lzc_snapshot([dst]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.DatasetExists): + lzc.lzc_receive(dst, stream.fileno()) + + def test_recv_full_missing_parent_fs(self): + src = ZFSTest.pool.makeName("fs1@snap") + dst = ZFSTest.pool.makeName("fs2/nonexistent/fs@snap") + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_receive(dst, stream.fileno()) + + # FIXME: should not be failing + @unittest.expectedFailure + def test_recv_full_but_specify_origin(self): + srcfs = ZFSTest.pool.makeName("fs1") + src = srcfs + "@snap" + dstfs = ZFSTest.pool.makeName("fs2/received-30") + dst = dstfs + '@snap' + origin1 = ZFSTest.pool.makeName("fs2@snap1") + origin2 = ZFSTest.pool.makeName("fs2@snap2") + + lzc.lzc_snapshot([origin1]) + with streams(srcfs, src, None) as (_, (stream, _)): + with self.assertRaises(lzc_exc.StreamMismatch): + lzc.lzc_receive(dst, stream.fileno(), origin=origin1) + stream.seek(0) + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_receive(dst, stream.fileno(), origin=origin2) + + # FIXME: should not be failing + @unittest.expectedFailure + def test_recv_full_existing_empty_fs_and_origin(self): + srcfs = ZFSTest.pool.makeName("fs1") + src = srcfs + "@snap" + dstfs = ZFSTest.pool.makeName("fs2/received-31") + dst = dstfs + '@snap' + origin = dstfs + '@dummy' + + lzc.lzc_create(dstfs) + with streams(srcfs, src, None) as (_, (stream, _)): + # because the destination fs already exists and has no snaps + with self.assertRaises((lzc_exc.DestinationModified, lzc_exc.DatasetExists)): + lzc.lzc_receive(dst, stream.fileno(), origin=origin) + lzc.lzc_snapshot([origin]) + stream.seek(0) + # because the destination fs already exists and has the snap + with self.assertRaises((lzc_exc.StreamMismatch, lzc_exc.DatasetExists)): + lzc.lzc_receive(dst, stream.fileno(), origin=origin) + + def test_recv_incremental_mounted_fs(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-7") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + with zfs_mount(dstfs): + lzc.lzc_receive(dst2, incr.fileno()) + + def test_recv_incremental_modified_fs(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-15") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + with temp_file_in_fs(dstfs): + with self.assertRaises(lzc_exc.DestinationModified): + lzc.lzc_receive(dst2, incr.fileno()) + + def test_recv_incremental_snapname_used(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-8") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + lzc.lzc_snapshot([dst2]) + with self.assertRaises(lzc_exc.DatasetExists): + lzc.lzc_receive(dst2, incr.fileno()) + + def test_recv_incremental_more_recent_snap_with_no_changes(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-9") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + dst_snap = dstfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + lzc.lzc_snapshot([dst_snap]) + lzc.lzc_receive(dst2, incr.fileno()) + + # FIXME: should not be failing + @unittest.expectedFailure + def test_recv_incremental_non_clone_but_set_origin(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-20") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + dst_snap = dstfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + lzc.lzc_snapshot([dst_snap]) + lzc.lzc_receive(dst2, incr.fileno(), origin=dst1) + + # FIXME: should not be failing + @unittest.expectedFailure + def test_recv_incremental_non_clone_but_set_random_origin(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-21") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + dst_snap = dstfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + lzc.lzc_snapshot([dst_snap]) + lzc.lzc_receive(dst2, incr.fileno(), + origin=ZFSTest.pool.makeName("fs2/fs@snap")) + + def test_recv_incremental_more_recent_snap(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-10") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + dst_snap = dstfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + with temp_file_in_fs(dstfs): + lzc.lzc_snapshot([dst_snap]) + with self.assertRaises(lzc_exc.DestinationModified): + lzc.lzc_receive(dst2, incr.fileno()) + + def test_recv_incremental_duplicate(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-11") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + dst_snap = dstfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + lzc.lzc_receive(dst2, incr.fileno()) + incr.seek(0) + with self.assertRaises(lzc_exc.DestinationModified): + lzc.lzc_receive(dst_snap, incr.fileno()) + + def test_recv_incremental_unrelated_fs(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-12") + dst_snap = dstfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (_, incr)): + lzc.lzc_create(dstfs) + with self.assertRaises(lzc_exc.StreamMismatch): + lzc.lzc_receive(dst_snap, incr.fileno()) + + def test_recv_incremental_nonexistent_fs(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-13") + dst_snap = dstfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (_, incr)): + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_receive(dst_snap, incr.fileno()) + + def test_recv_incremental_same_fs(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + src_snap = srcfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (_, incr)): + with self.assertRaises(lzc_exc.DestinationModified): + lzc.lzc_receive(src_snap, incr.fileno()) + + def test_recv_clone_without_specifying_origin(self): + orig_src = ZFSTest.pool.makeName("fs2@send-origin-2") + clone = ZFSTest.pool.makeName("fs1/fs/send-clone-2") + clone_snap = clone + "@snap" + orig_dst = ZFSTest.pool.makeName("fs1/fs/recv-origin-2@snap") + clone_dst = ZFSTest.pool.makeName("fs1/fs/recv-clone-2@snap") + + lzc.lzc_snapshot([orig_src]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(orig_src, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(orig_dst, stream.fileno()) + + lzc.lzc_clone(clone, orig_src) + lzc.lzc_snapshot([clone_snap]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(clone_snap, orig_src, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.BadStream): + lzc.lzc_receive(clone_dst, stream.fileno()) + + def test_recv_clone_invalid_origin(self): + orig_src = ZFSTest.pool.makeName("fs2@send-origin-3") + clone = ZFSTest.pool.makeName("fs1/fs/send-clone-3") + clone_snap = clone + "@snap" + orig_dst = ZFSTest.pool.makeName("fs1/fs/recv-origin-3@snap") + clone_dst = ZFSTest.pool.makeName("fs1/fs/recv-clone-3@snap") + + lzc.lzc_snapshot([orig_src]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(orig_src, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(orig_dst, stream.fileno()) + + lzc.lzc_clone(clone, orig_src) + lzc.lzc_snapshot([clone_snap]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(clone_snap, orig_src, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_receive( + clone_dst, stream.fileno(), origin=ZFSTest.pool.makeName("fs1/fs")) + + def test_recv_clone_wrong_origin(self): + orig_src = ZFSTest.pool.makeName("fs2@send-origin-4") + clone = ZFSTest.pool.makeName("fs1/fs/send-clone-4") + clone_snap = clone + "@snap" + orig_dst = ZFSTest.pool.makeName("fs1/fs/recv-origin-4@snap") + clone_dst = ZFSTest.pool.makeName("fs1/fs/recv-clone-4@snap") + wrong_origin = ZFSTest.pool.makeName("fs1/fs@snap") + + lzc.lzc_snapshot([orig_src]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(orig_src, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(orig_dst, stream.fileno()) + + lzc.lzc_clone(clone, orig_src) + lzc.lzc_snapshot([clone_snap]) + lzc.lzc_snapshot([wrong_origin]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(clone_snap, orig_src, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.StreamMismatch): + lzc.lzc_receive( + clone_dst, stream.fileno(), origin=wrong_origin) + + def test_recv_clone_nonexistent_origin(self): + orig_src = ZFSTest.pool.makeName("fs2@send-origin-5") + clone = ZFSTest.pool.makeName("fs1/fs/send-clone-5") + clone_snap = clone + "@snap" + orig_dst = ZFSTest.pool.makeName("fs1/fs/recv-origin-5@snap") + clone_dst = ZFSTest.pool.makeName("fs1/fs/recv-clone-5@snap") + wrong_origin = ZFSTest.pool.makeName("fs1/fs@snap") + + lzc.lzc_snapshot([orig_src]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(orig_src, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(orig_dst, stream.fileno()) + + lzc.lzc_clone(clone, orig_src) + lzc.lzc_snapshot([clone_snap]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(clone_snap, orig_src, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_receive( + clone_dst, stream.fileno(), origin=wrong_origin) + + def test_force_recv_full_existing_fs(self): + src = ZFSTest.pool.makeName("fs1@snap") + dstfs = ZFSTest.pool.makeName("fs2/received-50") + dst = dstfs + '@snap' + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + + lzc.lzc_create(dstfs) + with temp_file_in_fs(dstfs): + pass # enough to taint the fs + + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(dst, stream.fileno(), force=True) + + def test_force_recv_full_existing_modified_mounted_fs(self): + src = ZFSTest.pool.makeName("fs1@snap") + dstfs = ZFSTest.pool.makeName("fs2/received-53") + dst = dstfs + '@snap' + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + + lzc.lzc_create(dstfs) + + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + with zfs_mount(dstfs) as mntdir: + f = tempfile.NamedTemporaryFile(dir=mntdir, delete=False) + for i in range(1024): + f.write('x' * 1024) + lzc.lzc_receive(dst, stream.fileno(), force=True) + # The temporary file dissappears and any access, even close(), + # results in EIO. + self.assertFalse(os.path.exists(f.name)) + with self.assertRaises(IOError): + f.close() + + # This test-case expects the behavior that should be there, + # at the moment it may fail with DatasetExists or StreamMismatch + # depending on the implementation. + def test_force_recv_full_already_existing_with_snapshots(self): + src = ZFSTest.pool.makeName("fs1@snap") + dstfs = ZFSTest.pool.makeName("fs2/received-51") + dst = dstfs + '@snap' + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + + lzc.lzc_create(dstfs) + with temp_file_in_fs(dstfs): + pass # enough to taint the fs + lzc.lzc_snapshot([dstfs + "@snap1"]) + + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(dst, stream.fileno(), force=True) + + def test_force_recv_full_already_existing_with_same_snap(self): + src = ZFSTest.pool.makeName("fs1@snap") + dstfs = ZFSTest.pool.makeName("fs2/received-52") + dst = dstfs + '@snap' + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + + lzc.lzc_create(dstfs) + with temp_file_in_fs(dstfs): + pass # enough to taint the fs + lzc.lzc_snapshot([dst]) + + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.DatasetExists): + lzc.lzc_receive(dst, stream.fileno(), force=True) + + def test_force_recv_full_missing_parent_fs(self): + src = ZFSTest.pool.makeName("fs1@snap") + dst = ZFSTest.pool.makeName("fs2/nonexistent/fs@snap") + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")): + lzc.lzc_snapshot([src]) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_receive(dst, stream.fileno(), force=True) + + def test_force_recv_incremental_modified_fs(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-60") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + with temp_file_in_fs(dstfs): + pass # enough to taint the fs + lzc.lzc_receive(dst2, incr.fileno(), force=True) + + def test_force_recv_incremental_modified_mounted_fs(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-64") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + with zfs_mount(dstfs) as mntdir: + f = tempfile.NamedTemporaryFile(dir=mntdir, delete=False) + for i in range(1024): + f.write('x' * 1024) + lzc.lzc_receive(dst2, incr.fileno(), force=True) + # The temporary file dissappears and any access, even close(), + # results in EIO. + self.assertFalse(os.path.exists(f.name)) + with self.assertRaises(IOError): + f.close() + + def test_force_recv_incremental_modified_fs_plus_later_snap(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-61") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + dst3 = dstfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + with temp_file_in_fs(dstfs): + pass # enough to taint the fs + lzc.lzc_snapshot([dst3]) + lzc.lzc_receive(dst2, incr.fileno(), force=True) + self.assertExists(dst1) + self.assertExists(dst2) + self.assertNotExists(dst3) + + def test_force_recv_incremental_modified_fs_plus_same_name_snap(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-62") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + with temp_file_in_fs(dstfs): + pass # enough to taint the fs + lzc.lzc_snapshot([dst2]) + with self.assertRaises(lzc_exc.DatasetExists): + lzc.lzc_receive(dst2, incr.fileno(), force=True) + + def test_force_recv_incremental_modified_fs_plus_held_snap(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-63") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + dst3 = dstfs + '@snap' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + with temp_file_in_fs(dstfs): + pass # enough to taint the fs + lzc.lzc_snapshot([dst3]) + with cleanup_fd() as cfd: + lzc.lzc_hold({dst3: 'tag'}, cfd) + with self.assertRaises(lzc_exc.DatasetBusy): + lzc.lzc_receive(dst2, incr.fileno(), force=True) + self.assertExists(dst1) + self.assertNotExists(dst2) + self.assertExists(dst3) + + def test_force_recv_incremental_modified_fs_plus_cloned_snap(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-70") + dst1 = dstfs + '@snap1' + dst2 = dstfs + '@snap2' + dst3 = dstfs + '@snap' + cloned = ZFSTest.pool.makeName("fs2/received-cloned-70") + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + with temp_file_in_fs(dstfs): + pass # enough to taint the fs + lzc.lzc_snapshot([dst3]) + lzc.lzc_clone(cloned, dst3) + with self.assertRaises(lzc_exc.DatasetExists): + lzc.lzc_receive(dst2, incr.fileno(), force=True) + self.assertExists(dst1) + self.assertNotExists(dst2) + self.assertExists(dst3) + + def test_recv_with_header_full(self): + src = ZFSTest.pool.makeName("fs1@snap") + dst = ZFSTest.pool.makeName("fs2/received") + + with temp_file_in_fs(ZFSTest.pool.makeName("fs1")) as name: + lzc.lzc_snapshot([src]) + + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(src, None, stream.fileno()) + stream.seek(0) + + (header, c_header) = lzc.receive_header(stream.fileno()) + self.assertEqual(src, header['drr_toname']) + snap = header['drr_toname'].split('@', 1)[1] + lzc.lzc_receive_with_header(dst + '@' + snap, stream.fileno(), c_header) + + name = os.path.basename(name) + with zfs_mount(src) as mnt1, zfs_mount(dst) as mnt2: + self.assertTrue( + filecmp.cmp(os.path.join(mnt1, name), os.path.join(mnt2, name), False)) + + def test_recv_incremental_into_cloned_fs(self): + srcfs = ZFSTest.pool.makeName("fs1") + src1 = srcfs + "@snap1" + src2 = srcfs + "@snap2" + dstfs = ZFSTest.pool.makeName("fs2/received-71") + dst1 = dstfs + '@snap1' + cloned = ZFSTest.pool.makeName("fs2/received-cloned-71") + dst2 = cloned + '@snap' + + with streams(srcfs, src1, src2) as (_, (full, incr)): + lzc.lzc_receive(dst1, full.fileno()) + lzc.lzc_clone(cloned, dst1) + # test both graceful and with-force attempts + with self.assertRaises(lzc_exc.StreamMismatch): + lzc.lzc_receive(dst2, incr.fileno()) + incr.seek(0) + with self.assertRaises(lzc_exc.StreamMismatch): + lzc.lzc_receive(dst2, incr.fileno(), force=True) + self.assertExists(dst1) + self.assertNotExists(dst2) + + def test_send_full_across_clone_branch_point(self): + origfs = ZFSTest.pool.makeName("fs2") + + (_, (fromsnap, origsnap, _)) = make_snapshots( + origfs, "snap1", "send-origin-20", None) + + clonefs = ZFSTest.pool.makeName("fs1/fs/send-clone-20") + lzc.lzc_clone(clonefs, origsnap) + + (_, (_, tosnap, _)) = make_snapshots(clonefs, None, "snap", None) + + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(tosnap, None, stream.fileno()) + + def test_send_incr_across_clone_branch_point(self): + origfs = ZFSTest.pool.makeName("fs2") + + (_, (fromsnap, origsnap, _)) = make_snapshots( + origfs, "snap1", "send-origin-21", None) + + clonefs = ZFSTest.pool.makeName("fs1/fs/send-clone-21") + lzc.lzc_clone(clonefs, origsnap) + + (_, (_, tosnap, _)) = make_snapshots(clonefs, None, "snap", None) + + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(tosnap, fromsnap, stream.fileno()) + + def test_recv_full_across_clone_branch_point(self): + origfs = ZFSTest.pool.makeName("fs2") + + (_, (fromsnap, origsnap, _)) = make_snapshots( + origfs, "snap1", "send-origin-30", None) + + clonefs = ZFSTest.pool.makeName("fs1/fs/send-clone-30") + lzc.lzc_clone(clonefs, origsnap) + + (_, (_, tosnap, _)) = make_snapshots(clonefs, None, "snap", None) + + recvfs = ZFSTest.pool.makeName("fs1/recv-clone-30") + recvsnap = recvfs + "@snap" + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(tosnap, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(recvsnap, stream.fileno()) + + def test_recv_incr_across_clone_branch_point__no_origin(self): + origfs = ZFSTest.pool.makeName("fs2") + + (_, (fromsnap, origsnap, _)) = make_snapshots( + origfs, "snap1", "send-origin-32", None) + + clonefs = ZFSTest.pool.makeName("fs1/fs/send-clone-32") + lzc.lzc_clone(clonefs, origsnap) + + (_, (_, tosnap, _)) = make_snapshots(clonefs, None, "snap", None) + + recvfs = ZFSTest.pool.makeName("fs1/recv-clone-32") + recvsnap1 = recvfs + "@snap1" + recvsnap2 = recvfs + "@snap2" + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(fromsnap, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(recvsnap1, stream.fileno()) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(tosnap, fromsnap, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.BadStream): + lzc.lzc_receive(recvsnap2, stream.fileno()) + + def test_recv_incr_across_clone_branch_point(self): + origfs = ZFSTest.pool.makeName("fs2") + + (_, (fromsnap, origsnap, _)) = make_snapshots( + origfs, "snap1", "send-origin-31", None) + + clonefs = ZFSTest.pool.makeName("fs1/fs/send-clone-31") + lzc.lzc_clone(clonefs, origsnap) + + (_, (_, tosnap, _)) = make_snapshots(clonefs, None, "snap", None) + + recvfs = ZFSTest.pool.makeName("fs1/recv-clone-31") + recvsnap1 = recvfs + "@snap1" + recvsnap2 = recvfs + "@snap2" + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(fromsnap, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(recvsnap1, stream.fileno()) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(tosnap, fromsnap, stream.fileno()) + stream.seek(0) + with self.assertRaises(lzc_exc.BadStream): + lzc.lzc_receive(recvsnap2, stream.fileno(), origin=recvsnap1) + + def test_recv_incr_across_clone_branch_point__new_fs(self): + origfs = ZFSTest.pool.makeName("fs2") + + (_, (fromsnap, origsnap, _)) = make_snapshots( + origfs, "snap1", "send-origin-33", None) + + clonefs = ZFSTest.pool.makeName("fs1/fs/send-clone-33") + lzc.lzc_clone(clonefs, origsnap) + + (_, (_, tosnap, _)) = make_snapshots(clonefs, None, "snap", None) + + recvfs1 = ZFSTest.pool.makeName("fs1/recv-clone-33") + recvsnap1 = recvfs1 + "@snap" + recvfs2 = ZFSTest.pool.makeName("fs1/recv-clone-33_2") + recvsnap2 = recvfs2 + "@snap" + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(fromsnap, None, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(recvsnap1, stream.fileno()) + with tempfile.TemporaryFile(suffix='.ztream') as stream: + lzc.lzc_send(tosnap, fromsnap, stream.fileno()) + stream.seek(0) + lzc.lzc_receive(recvsnap2, stream.fileno(), origin=recvsnap1) + + def test_recv_bad_stream(self): + dstfs = ZFSTest.pool.makeName("fs2/received") + dst_snap = dstfs + '@snap' + + with dev_zero() as fd: + with self.assertRaises(lzc_exc.BadStream): + lzc.lzc_receive(dst_snap, fd) + + @needs_support(lzc.lzc_promote) + def test_promote(self): + origfs = ZFSTest.pool.makeName("fs2") + snap = "@promote-snap-1" + origsnap = origfs + snap + lzc.lzc_snap([origsnap]) + + clonefs = ZFSTest.pool.makeName("fs1/fs/promote-clone-1") + lzc.lzc_clone(clonefs, origsnap) + + lzc.lzc_promote(clonefs) + # the snapshot now should belong to the promoted fs + self.assertExists(clonefs + snap) + + @needs_support(lzc.lzc_promote) + def test_promote_too_long_snapname(self): + # origfs name must be shorter than clonefs name + origfs = ZFSTest.pool.makeName("fs2") + clonefs = ZFSTest.pool.makeName("fs1/fs/promote-clone-2") + snapprefix = "@promote-snap-2-" + pad_len = 1 + lzc.MAXNAMELEN - len(clonefs) - len(snapprefix) + snap = snapprefix + 'x' * pad_len + origsnap = origfs + snap + + lzc.lzc_snap([origsnap]) + lzc.lzc_clone(clonefs, origsnap) + + # This may fail on older buggy systems. + # See: https://www.illumos.org/issues/5909 + with self.assertRaises(lzc_exc.NameTooLong): + lzc.lzc_promote(clonefs) + + @needs_support(lzc.lzc_promote) + def test_promote_not_cloned(self): + fs = ZFSTest.pool.makeName("fs2") + with self.assertRaises(lzc_exc.NotClone): + lzc.lzc_promote(fs) + + @unittest.skipIf(*illumos_bug_6379()) + def test_hold_bad_fd(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with tempfile.TemporaryFile() as tmp: + bad_fd = tmp.fileno() + + with self.assertRaises(lzc_exc.BadHoldCleanupFD): + lzc.lzc_hold({snap: 'tag'}, bad_fd) + + @unittest.skipIf(*illumos_bug_6379()) + def test_hold_bad_fd_2(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with self.assertRaises(lzc_exc.BadHoldCleanupFD): + lzc.lzc_hold({snap: 'tag'}, -2) + + @unittest.skipIf(*illumos_bug_6379()) + def test_hold_bad_fd_3(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + (soft, hard) = resource.getrlimit(resource.RLIMIT_NOFILE) + bad_fd = hard + 1 + with self.assertRaises(lzc_exc.BadHoldCleanupFD): + lzc.lzc_hold({snap: 'tag'}, bad_fd) + + @unittest.skipIf(*illumos_bug_6379()) + def test_hold_wrong_fd(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with tempfile.TemporaryFile() as tmp: + fd = tmp.fileno() + with self.assertRaises(lzc_exc.BadHoldCleanupFD): + lzc.lzc_hold({snap: 'tag'}, fd) + + def test_hold_fd(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap: 'tag'}, fd) + + def test_hold_empty(self): + with cleanup_fd() as fd: + lzc.lzc_hold({}, fd) + + def test_hold_empty_2(self): + lzc.lzc_hold({}) + + def test_hold_vs_snap_destroy(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap: 'tag'}, fd) + + with self.assertRaises(lzc_exc.SnapshotDestructionFailure) as ctx: + lzc.lzc_destroy_snaps([snap], defer=False) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.SnapshotIsHeld) + + lzc.lzc_destroy_snaps([snap], defer=True) + self.assertExists(snap) + + # after automatic hold cleanup and deferred destruction + self.assertNotExists(snap) + + def test_hold_many_tags(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap: 'tag1'}, fd) + lzc.lzc_hold({snap: 'tag2'}, fd) + + def test_hold_many_snaps(self): + snap1 = ZFSTest.pool.getRoot().getSnap() + snap2 = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap1: 'tag', snap2: 'tag'}, fd) + + def test_hold_many_with_one_missing(self): + snap1 = ZFSTest.pool.getRoot().getSnap() + snap2 = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap1]) + + with cleanup_fd() as fd: + missing = lzc.lzc_hold({snap1: 'tag', snap2: 'tag'}, fd) + self.assertEqual(len(missing), 1) + self.assertEqual(missing[0], snap2) + + def test_hold_many_with_all_missing(self): + snap1 = ZFSTest.pool.getRoot().getSnap() + snap2 = ZFSTest.pool.getRoot().getSnap() + + with cleanup_fd() as fd: + missing = lzc.lzc_hold({snap1: 'tag', snap2: 'tag'}, fd) + self.assertEqual(len(missing), 2) + self.assertEqual(sorted(missing), sorted([snap1, snap2])) + + # FIXME: should not be failing + @unittest.expectedFailure + def test_hold_missing_fs(self): + # XXX skip pre-created filesystems + ZFSTest.pool.getRoot().getFilesystem() + ZFSTest.pool.getRoot().getFilesystem() + ZFSTest.pool.getRoot().getFilesystem() + ZFSTest.pool.getRoot().getFilesystem() + ZFSTest.pool.getRoot().getFilesystem() + snap = ZFSTest.pool.getRoot().getFilesystem().getSnap() + + with self.assertRaises(lzc_exc.HoldFailure) as ctx: + lzc.lzc_hold({snap: 'tag'}) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.FilesystemNotFound) + + # FIXME: should not be failing + @unittest.expectedFailure + def test_hold_missing_fs_auto_cleanup(self): + # XXX skip pre-created filesystems + ZFSTest.pool.getRoot().getFilesystem() + ZFSTest.pool.getRoot().getFilesystem() + ZFSTest.pool.getRoot().getFilesystem() + ZFSTest.pool.getRoot().getFilesystem() + ZFSTest.pool.getRoot().getFilesystem() + snap = ZFSTest.pool.getRoot().getFilesystem().getSnap() + + with cleanup_fd() as fd: + with self.assertRaises(lzc_exc.HoldFailure) as ctx: + lzc.lzc_hold({snap: 'tag'}, fd) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.FilesystemNotFound) + + def test_hold_duplicate(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap: 'tag'}, fd) + with self.assertRaises(lzc_exc.HoldFailure) as ctx: + lzc.lzc_hold({snap: 'tag'}, fd) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.HoldExists) + + def test_hold_across_pools(self): + snap1 = ZFSTest.pool.getRoot().getSnap() + snap2 = ZFSTest.misc_pool.getRoot().getSnap() + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with cleanup_fd() as fd: + with self.assertRaises(lzc_exc.HoldFailure) as ctx: + lzc.lzc_hold({snap1: 'tag', snap2: 'tag'}, fd) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.PoolsDiffer) + + def test_hold_too_long_tag(self): + snap = ZFSTest.pool.getRoot().getSnap() + tag = 't' * 256 + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + with self.assertRaises(lzc_exc.HoldFailure) as ctx: + lzc.lzc_hold({snap: tag}, fd) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameTooLong) + self.assertEquals(e.name, tag) + + # Apparently the full snapshot name is not checked for length + # and this snapshot is treated as simply missing. + @unittest.expectedFailure + def test_hold_too_long_snap_name(self): + snap = ZFSTest.pool.getRoot().getTooLongSnap(False) + with cleanup_fd() as fd: + with self.assertRaises(lzc_exc.HoldFailure) as ctx: + lzc.lzc_hold({snap: 'tag'}, fd) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameTooLong) + self.assertEquals(e.name, snap) + + def test_hold_too_long_snap_name_2(self): + snap = ZFSTest.pool.getRoot().getTooLongSnap(True) + with cleanup_fd() as fd: + with self.assertRaises(lzc_exc.HoldFailure) as ctx: + lzc.lzc_hold({snap: 'tag'}, fd) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameTooLong) + self.assertEquals(e.name, snap) + + def test_hold_invalid_snap_name(self): + snap = ZFSTest.pool.getRoot().getSnap() + '@bad' + with cleanup_fd() as fd: + with self.assertRaises(lzc_exc.HoldFailure) as ctx: + lzc.lzc_hold({snap: 'tag'}, fd) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameInvalid) + self.assertEquals(e.name, snap) + + def test_hold_invalid_snap_name_2(self): + snap = ZFSTest.pool.getRoot().getFilesystem().getName() + with cleanup_fd() as fd: + with self.assertRaises(lzc_exc.HoldFailure) as ctx: + lzc.lzc_hold({snap: 'tag'}, fd) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameInvalid) + self.assertEquals(e.name, snap) + + def test_get_holds(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap: 'tag1'}, fd) + lzc.lzc_hold({snap: 'tag2'}, fd) + + holds = lzc.lzc_get_holds(snap) + self.assertEquals(len(holds), 2) + self.assertIn('tag1', holds) + self.assertIn('tag2', holds) + self.assertIsInstance(holds['tag1'], (int, long)) + + def test_get_holds_after_auto_cleanup(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap: 'tag1'}, fd) + lzc.lzc_hold({snap: 'tag2'}, fd) + + holds = lzc.lzc_get_holds(snap) + self.assertEquals(len(holds), 0) + self.assertIsInstance(holds, dict) + + def test_get_holds_nonexistent_snap(self): + snap = ZFSTest.pool.getRoot().getSnap() + with self.assertRaises(lzc_exc.SnapshotNotFound): + lzc.lzc_get_holds(snap) + + def test_get_holds_too_long_snap_name(self): + snap = ZFSTest.pool.getRoot().getTooLongSnap(False) + with self.assertRaises(lzc_exc.NameTooLong): + lzc.lzc_get_holds(snap) + + def test_get_holds_too_long_snap_name_2(self): + snap = ZFSTest.pool.getRoot().getTooLongSnap(True) + with self.assertRaises(lzc_exc.NameTooLong): + lzc.lzc_get_holds(snap) + + def test_get_holds_invalid_snap_name(self): + snap = ZFSTest.pool.getRoot().getSnap() + '@bad' + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_get_holds(snap) + + # A filesystem-like snapshot name is not recognized as + # an invalid name. + @unittest.expectedFailure + def test_get_holds_invalid_snap_name_2(self): + snap = ZFSTest.pool.getRoot().getFilesystem().getName() + with self.assertRaises(lzc_exc.NameInvalid): + lzc.lzc_get_holds(snap) + + def test_release_hold(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + lzc.lzc_hold({snap: 'tag'}) + ret = lzc.lzc_release({snap: ['tag']}) + self.assertEquals(len(ret), 0) + + def test_release_hold_empty(self): + ret = lzc.lzc_release({}) + self.assertEquals(len(ret), 0) + + def test_release_hold_complex(self): + snap1 = ZFSTest.pool.getRoot().getSnap() + snap2 = ZFSTest.pool.getRoot().getSnap() + snap3 = ZFSTest.pool.getRoot().getFilesystem().getSnap() + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2, snap3]) + + lzc.lzc_hold({snap1: 'tag1'}) + lzc.lzc_hold({snap1: 'tag2'}) + lzc.lzc_hold({snap2: 'tag'}) + lzc.lzc_hold({snap3: 'tag1'}) + lzc.lzc_hold({snap3: 'tag2'}) + + holds = lzc.lzc_get_holds(snap1) + self.assertEquals(len(holds), 2) + holds = lzc.lzc_get_holds(snap2) + self.assertEquals(len(holds), 1) + holds = lzc.lzc_get_holds(snap3) + self.assertEquals(len(holds), 2) + + release = { + snap1: ['tag1', 'tag2'], + snap2: ['tag'], + snap3: ['tag2'], + } + ret = lzc.lzc_release(release) + self.assertEquals(len(ret), 0) + + holds = lzc.lzc_get_holds(snap1) + self.assertEquals(len(holds), 0) + holds = lzc.lzc_get_holds(snap2) + self.assertEquals(len(holds), 0) + holds = lzc.lzc_get_holds(snap3) + self.assertEquals(len(holds), 1) + + ret = lzc.lzc_release({snap3: ['tag1']}) + self.assertEquals(len(ret), 0) + holds = lzc.lzc_get_holds(snap3) + self.assertEquals(len(holds), 0) + + def test_release_hold_before_auto_cleanup(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap: 'tag'}, fd) + ret = lzc.lzc_release({snap: ['tag']}) + self.assertEquals(len(ret), 0) + + def test_release_hold_and_snap_destruction(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap: 'tag1'}, fd) + lzc.lzc_hold({snap: 'tag2'}, fd) + + lzc.lzc_destroy_snaps([snap], defer=True) + self.assertExists(snap) + + lzc.lzc_release({snap: ['tag1']}) + self.assertExists(snap) + + lzc.lzc_release({snap: ['tag2']}) + self.assertNotExists(snap) + + def test_release_hold_and_multiple_snap_destruction(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap: 'tag'}, fd) + + lzc.lzc_destroy_snaps([snap], defer=True) + self.assertExists(snap) + + lzc.lzc_destroy_snaps([snap], defer=True) + self.assertExists(snap) + + lzc.lzc_release({snap: ['tag']}) + self.assertNotExists(snap) + + def test_release_hold_missing_tag(self): + snap = ZFSTest.pool.getRoot().getSnap() + lzc.lzc_snapshot([snap]) + + ret = lzc.lzc_release({snap: ['tag']}) + self.assertEquals(len(ret), 1) + self.assertEquals(ret[0], snap + '#tag') + + def test_release_hold_missing_snap(self): + snap = ZFSTest.pool.getRoot().getSnap() + + ret = lzc.lzc_release({snap: ['tag']}) + self.assertEquals(len(ret), 1) + self.assertEquals(ret[0], snap) + + def test_release_hold_missing_snap_2(self): + snap = ZFSTest.pool.getRoot().getSnap() + + ret = lzc.lzc_release({snap: ['tag', 'another']}) + self.assertEquals(len(ret), 1) + self.assertEquals(ret[0], snap) + + def test_release_hold_across_pools(self): + snap1 = ZFSTest.pool.getRoot().getSnap() + snap2 = ZFSTest.misc_pool.getRoot().getSnap() + lzc.lzc_snapshot([snap1]) + lzc.lzc_snapshot([snap2]) + + with cleanup_fd() as fd: + lzc.lzc_hold({snap1: 'tag'}, fd) + lzc.lzc_hold({snap2: 'tag'}, fd) + with self.assertRaises(lzc_exc.HoldReleaseFailure) as ctx: + lzc.lzc_release({snap1: ['tag'], snap2: ['tag']}) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.PoolsDiffer) + + # Apparently the tag name is not verified, + # only its existence is checked. + @unittest.expectedFailure + def test_release_hold_too_long_tag(self): + snap = ZFSTest.pool.getRoot().getSnap() + tag = 't' * 256 + lzc.lzc_snapshot([snap]) + + with self.assertRaises(lzc_exc.HoldReleaseFailure): + lzc.lzc_release({snap: [tag]}) + + # Apparently the full snapshot name is not checked for length + # and this snapshot is treated as simply missing. + @unittest.expectedFailure + def test_release_hold_too_long_snap_name(self): + snap = ZFSTest.pool.getRoot().getTooLongSnap(False) + + with self.assertRaises(lzc_exc.HoldReleaseFailure): + lzc.lzc_release({snap: ['tag']}) + + def test_release_hold_too_long_snap_name_2(self): + snap = ZFSTest.pool.getRoot().getTooLongSnap(True) + with self.assertRaises(lzc_exc.HoldReleaseFailure) as ctx: + lzc.lzc_release({snap: ['tag']}) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameTooLong) + self.assertEquals(e.name, snap) + + def test_release_hold_invalid_snap_name(self): + snap = ZFSTest.pool.getRoot().getSnap() + '@bad' + with self.assertRaises(lzc_exc.HoldReleaseFailure) as ctx: + lzc.lzc_release({snap: ['tag']}) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameInvalid) + self.assertEquals(e.name, snap) + + def test_release_hold_invalid_snap_name_2(self): + snap = ZFSTest.pool.getRoot().getFilesystem().getName() + with self.assertRaises(lzc_exc.HoldReleaseFailure) as ctx: + lzc.lzc_release({snap: ['tag']}) + for e in ctx.exception.errors: + self.assertIsInstance(e, lzc_exc.NameInvalid) + self.assertEquals(e.name, snap) + + @needs_support(lzc.lzc_list_children) + def test_list_children(self): + name = ZFSTest.pool.makeName("fs1/fs") + names = [ZFSTest.pool.makeName("fs1/fs/test1"), + ZFSTest.pool.makeName("fs1/fs/test2"), + ZFSTest.pool.makeName("fs1/fs/test3"), ] + # and one snap to see that it is not listed + snap = ZFSTest.pool.makeName("fs1/fs@test") + + for fs in names: + lzc.lzc_create(fs) + lzc.lzc_snapshot([snap]) + + children = list(lzc.lzc_list_children(name)) + self.assertItemsEqual(children, names) + + @needs_support(lzc.lzc_list_children) + def test_list_children_nonexistent(self): + fs = ZFSTest.pool.makeName("nonexistent") + + with self.assertRaises(lzc_exc.DatasetNotFound): + list(lzc.lzc_list_children(fs)) + + @needs_support(lzc.lzc_list_children) + def test_list_children_of_snap(self): + snap = ZFSTest.pool.makeName("@newsnap") + + lzc.lzc_snapshot([snap]) + children = list(lzc.lzc_list_children(snap)) + self.assertEqual(children, []) + + @needs_support(lzc.lzc_list_snaps) + def test_list_snaps(self): + name = ZFSTest.pool.makeName("fs1/fs") + names = [ZFSTest.pool.makeName("fs1/fs@test1"), + ZFSTest.pool.makeName("fs1/fs@test2"), + ZFSTest.pool.makeName("fs1/fs@test3"), ] + # and one filesystem to see that it is not listed + fs = ZFSTest.pool.makeName("fs1/fs/test") + + for snap in names: + lzc.lzc_snapshot([snap]) + lzc.lzc_create(fs) + + snaps = list(lzc.lzc_list_snaps(name)) + self.assertItemsEqual(snaps, names) + + @needs_support(lzc.lzc_list_snaps) + def test_list_snaps_nonexistent(self): + fs = ZFSTest.pool.makeName("nonexistent") + + with self.assertRaises(lzc_exc.DatasetNotFound): + list(lzc.lzc_list_snaps(fs)) + + @needs_support(lzc.lzc_list_snaps) + def test_list_snaps_of_snap(self): + snap = ZFSTest.pool.makeName("@newsnap") + + lzc.lzc_snapshot([snap]) + snaps = list(lzc.lzc_list_snaps(snap)) + self.assertEqual(snaps, []) + + @needs_support(lzc.lzc_get_props) + def test_get_fs_props(self): + fs = ZFSTest.pool.makeName("new") + props = {"user:foo": "bar"} + + lzc.lzc_create(fs, props=props) + actual_props = lzc.lzc_get_props(fs) + self.assertDictContainsSubset(props, actual_props) + + @needs_support(lzc.lzc_get_props) + def test_get_fs_props_with_child(self): + parent = ZFSTest.pool.makeName("parent") + child = ZFSTest.pool.makeName("parent/child") + parent_props = {"user:foo": "parent"} + child_props = {"user:foo": "child"} + + lzc.lzc_create(parent, props=parent_props) + lzc.lzc_create(child, props=child_props) + actual_parent_props = lzc.lzc_get_props(parent) + actual_child_props = lzc.lzc_get_props(child) + self.assertDictContainsSubset(parent_props, actual_parent_props) + self.assertDictContainsSubset(child_props, actual_child_props) + + @needs_support(lzc.lzc_get_props) + def test_get_snap_props(self): + snapname = ZFSTest.pool.makeName("@snap") + snaps = [snapname] + props = {"user:foo": "bar"} + + lzc.lzc_snapshot(snaps, props) + actual_props = lzc.lzc_get_props(snapname) + self.assertDictContainsSubset(props, actual_props) + + @needs_support(lzc.lzc_get_props) + def test_get_props_nonexistent(self): + fs = ZFSTest.pool.makeName("nonexistent") + + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_get_props(fs) + + @needs_support(lzc.lzc_get_props) + def test_get_mountpoint_none(self): + ''' + If the *mountpoint* property is set to none, then its + value is returned as `bytes` "none". + Also, a child filesystem inherits that value. + ''' + fs = ZFSTest.pool.makeName("new") + child = ZFSTest.pool.makeName("new/child") + props = {"mountpoint": "none"} + + lzc.lzc_create(fs, props=props) + lzc.lzc_create(child) + actual_props = lzc.lzc_get_props(fs) + self.assertDictContainsSubset(props, actual_props) + # check that mountpoint value is correctly inherited + child_props = lzc.lzc_get_props(child) + self.assertDictContainsSubset(props, child_props) + + @needs_support(lzc.lzc_get_props) + def test_get_mountpoint_legacy(self): + ''' + If the *mountpoint* property is set to legacy, then its + value is returned as `bytes` "legacy". + Also, a child filesystem inherits that value. + ''' + fs = ZFSTest.pool.makeName("new") + child = ZFSTest.pool.makeName("new/child") + props = {"mountpoint": "legacy"} + + lzc.lzc_create(fs, props=props) + lzc.lzc_create(child) + actual_props = lzc.lzc_get_props(fs) + self.assertDictContainsSubset(props, actual_props) + # check that mountpoint value is correctly inherited + child_props = lzc.lzc_get_props(child) + self.assertDictContainsSubset(props, child_props) + + @needs_support(lzc.lzc_get_props) + def test_get_mountpoint_path(self): + ''' + If the *mountpoint* property is set to a path and the property + is not explicitly set on a child filesystem, then its + value is that of the parent filesystem with the child's + name appended using the '/' separator. + ''' + fs = ZFSTest.pool.makeName("new") + child = ZFSTest.pool.makeName("new/child") + props = {"mountpoint": "/mnt"} + + lzc.lzc_create(fs, props=props) + lzc.lzc_create(child) + actual_props = lzc.lzc_get_props(fs) + self.assertDictContainsSubset(props, actual_props) + # check that mountpoint value is correctly inherited + child_props = lzc.lzc_get_props(child) + self.assertDictContainsSubset( + {"mountpoint": "/mnt/child"}, child_props) + + @needs_support(lzc.lzc_get_props) + def test_get_snap_clones(self): + fs = ZFSTest.pool.makeName("new") + snap = ZFSTest.pool.makeName("@snap") + clone1 = ZFSTest.pool.makeName("clone1") + clone2 = ZFSTest.pool.makeName("clone2") + + lzc.lzc_create(fs) + lzc.lzc_snapshot([snap]) + lzc.lzc_clone(clone1, snap) + lzc.lzc_clone(clone2, snap) + + clones_prop = lzc.lzc_get_props(snap)["clones"] + self.assertItemsEqual(clones_prop, [clone1, clone2]) + + @needs_support(lzc.lzc_rename) + def test_rename(self): + src = ZFSTest.pool.makeName("source") + tgt = ZFSTest.pool.makeName("target") + + lzc.lzc_create(src) + lzc.lzc_rename(src, tgt) + self.assertNotExists(src) + self.assertExists(tgt) + + @needs_support(lzc.lzc_rename) + def test_rename_nonexistent(self): + src = ZFSTest.pool.makeName("source") + tgt = ZFSTest.pool.makeName("target") + + with self.assertRaises(lzc_exc.FilesystemNotFound): + lzc.lzc_rename(src, tgt) + + @needs_support(lzc.lzc_rename) + def test_rename_existing_target(self): + src = ZFSTest.pool.makeName("source") + tgt = ZFSTest.pool.makeName("target") + + lzc.lzc_create(src) + lzc.lzc_create(tgt) + with self.assertRaises(lzc_exc.FilesystemExists): + lzc.lzc_rename(src, tgt) + + @needs_support(lzc.lzc_rename) + def test_rename_nonexistent_target_parent(self): + src = ZFSTest.pool.makeName("source") + tgt = ZFSTest.pool.makeName("parent/target") + + lzc.lzc_create(src) + with self.assertRaises(lzc_exc.FilesystemNotFound): + lzc.lzc_rename(src, tgt) + + @needs_support(lzc.lzc_destroy) + def test_destroy(self): + fs = ZFSTest.pool.makeName("test-fs") + + lzc.lzc_create(fs) + lzc.lzc_destroy(fs) + self.assertNotExists(fs) + + @needs_support(lzc.lzc_destroy) + def test_destroy_nonexistent(self): + fs = ZFSTest.pool.makeName("test-fs") + + with self.assertRaises(lzc_exc.FilesystemNotFound): + lzc.lzc_destroy(fs) + + @needs_support(lzc.lzc_inherit_prop) + def test_inherit_prop(self): + parent = ZFSTest.pool.makeName("parent") + child = ZFSTest.pool.makeName("parent/child") + the_prop = "user:foo" + parent_props = {the_prop: "parent"} + child_props = {the_prop: "child"} + + lzc.lzc_create(parent, props=parent_props) + lzc.lzc_create(child, props=child_props) + lzc.lzc_inherit_prop(child, the_prop) + actual_props = lzc.lzc_get_props(child) + self.assertDictContainsSubset(parent_props, actual_props) + + @needs_support(lzc.lzc_inherit_prop) + def test_inherit_missing_prop(self): + parent = ZFSTest.pool.makeName("parent") + child = ZFSTest.pool.makeName("parent/child") + the_prop = "user:foo" + child_props = {the_prop: "child"} + + lzc.lzc_create(parent) + lzc.lzc_create(child, props=child_props) + lzc.lzc_inherit_prop(child, the_prop) + actual_props = lzc.lzc_get_props(child) + self.assertNotIn(the_prop, actual_props) + + @needs_support(lzc.lzc_inherit_prop) + def test_inherit_readonly_prop(self): + parent = ZFSTest.pool.makeName("parent") + child = ZFSTest.pool.makeName("parent/child") + the_prop = "createtxg" + + lzc.lzc_create(parent) + lzc.lzc_create(child) + with self.assertRaises(lzc_exc.PropertyInvalid): + lzc.lzc_inherit_prop(child, the_prop) + + @needs_support(lzc.lzc_inherit_prop) + def test_inherit_unknown_prop(self): + parent = ZFSTest.pool.makeName("parent") + child = ZFSTest.pool.makeName("parent/child") + the_prop = "nosuchprop" + + lzc.lzc_create(parent) + lzc.lzc_create(child) + with self.assertRaises(lzc_exc.PropertyInvalid): + lzc.lzc_inherit_prop(child, the_prop) + + @needs_support(lzc.lzc_inherit_prop) + def test_inherit_prop_on_snap(self): + fs = ZFSTest.pool.makeName("new") + snapname = ZFSTest.pool.makeName("new@snap") + prop = "user:foo" + fs_val = "fs" + snap_val = "snap" + + lzc.lzc_create(fs, props={prop: fs_val}) + lzc.lzc_snapshot([snapname], props={prop: snap_val}) + + actual_props = lzc.lzc_get_props(snapname) + self.assertDictContainsSubset({prop: snap_val}, actual_props) + + lzc.lzc_inherit_prop(snapname, prop) + actual_props = lzc.lzc_get_props(snapname) + self.assertDictContainsSubset({prop: fs_val}, actual_props) + + @needs_support(lzc.lzc_set_prop) + def test_set_fs_prop(self): + fs = ZFSTest.pool.makeName("new") + prop = "user:foo" + val = "bar" + + lzc.lzc_create(fs) + lzc.lzc_set_prop(fs, prop, val) + actual_props = lzc.lzc_get_props(fs) + self.assertDictContainsSubset({prop: val}, actual_props) + + @needs_support(lzc.lzc_set_prop) + def test_set_snap_prop(self): + snapname = ZFSTest.pool.makeName("@snap") + prop = "user:foo" + val = "bar" + + lzc.lzc_snapshot([snapname]) + lzc.lzc_set_prop(snapname, prop, val) + actual_props = lzc.lzc_get_props(snapname) + self.assertDictContainsSubset({prop: val}, actual_props) + + @needs_support(lzc.lzc_set_prop) + def test_set_prop_nonexistent(self): + fs = ZFSTest.pool.makeName("nonexistent") + prop = "user:foo" + val = "bar" + + with self.assertRaises(lzc_exc.DatasetNotFound): + lzc.lzc_set_prop(fs, prop, val) + + @needs_support(lzc.lzc_set_prop) + def test_set_sys_prop(self): + fs = ZFSTest.pool.makeName("new") + prop = "recordsize" + val = 4096 + + lzc.lzc_create(fs) + lzc.lzc_set_prop(fs, prop, val) + actual_props = lzc.lzc_get_props(fs) + self.assertDictContainsSubset({prop: val}, actual_props) + + @needs_support(lzc.lzc_set_prop) + def test_set_invalid_prop(self): + fs = ZFSTest.pool.makeName("new") + prop = "nosuchprop" + val = 0 + + lzc.lzc_create(fs) + with self.assertRaises(lzc_exc.PropertyInvalid): + lzc.lzc_set_prop(fs, prop, val) + + @needs_support(lzc.lzc_set_prop) + def test_set_invalid_value_prop(self): + fs = ZFSTest.pool.makeName("new") + prop = "atime" + val = 100 + + lzc.lzc_create(fs) + with self.assertRaises(lzc_exc.PropertyInvalid): + lzc.lzc_set_prop(fs, prop, val) + + @needs_support(lzc.lzc_set_prop) + def test_set_invalid_value_prop_2(self): + fs = ZFSTest.pool.makeName("new") + prop = "readonly" + val = 100 + + lzc.lzc_create(fs) + with self.assertRaises(lzc_exc.PropertyInvalid): + lzc.lzc_set_prop(fs, prop, val) + + @needs_support(lzc.lzc_set_prop) + def test_set_prop_too_small_quota(self): + fs = ZFSTest.pool.makeName("new") + prop = "refquota" + val = 1 + + lzc.lzc_create(fs) + with self.assertRaises(lzc_exc.NoSpace): + lzc.lzc_set_prop(fs, prop, val) + + @needs_support(lzc.lzc_set_prop) + def test_set_readonly_prop(self): + fs = ZFSTest.pool.makeName("new") + prop = "creation" + val = 0 + + lzc.lzc_create(fs) + lzc.lzc_set_prop(fs, prop, val) + actual_props = lzc.lzc_get_props(fs) + # the change is silently ignored + self.assertTrue(actual_props[prop] != val) + + +class _TempPool(object): + SNAPSHOTS = ['snap', 'snap1', 'snap2'] + BOOKMARKS = ['bmark', 'bmark1', 'bmark2'] + + _cachefile_suffix = ".cachefile" + + # XXX Whether to do a sloppy but much faster cleanup + # or a proper but slower one. + _recreate_pools = True + + def __init__(self, size=128 * 1024 * 1024, readonly=False, filesystems=[]): + self._filesystems = filesystems + self._readonly = readonly + self._pool_name = 'pool.' + bytes(uuid.uuid4()) + self._root = _Filesystem(self._pool_name) + (fd, self._pool_file_path) = tempfile.mkstemp( + suffix='.zpool', prefix='tmp-') + if readonly: + cachefile = self._pool_file_path + _TempPool._cachefile_suffix + else: + cachefile = 'none' + self._zpool_create = ['zpool', 'create', '-o', 'cachefile=' + cachefile, '-O', 'mountpoint=legacy', + self._pool_name, self._pool_file_path] + try: + os.ftruncate(fd, size) + os.close(fd) + + subprocess.check_output( + self._zpool_create, stderr=subprocess.STDOUT) + + for fs in filesystems: + lzc.lzc_create(self.makeName(fs)) + + self._bmarks_supported = self.isPoolFeatureEnabled('bookmarks') + + if readonly: + # To make a pool read-only it must exported and re-imported with readonly option. + # The most deterministic way to re-import the pool is by using a cache file. + # But the cache file has to be stashed away before the pool is exported, + # because otherwise the pool is removed from the cache. + shutil.copyfile(cachefile, cachefile + '.tmp') + subprocess.check_output( + ['zpool', 'export', '-f', self._pool_name], stderr=subprocess.STDOUT) + os.rename(cachefile + '.tmp', cachefile) + subprocess.check_output(['zpool', 'import', '-f', '-N', '-c', cachefile, '-o', 'readonly=on', self._pool_name], + stderr=subprocess.STDOUT) + os.remove(cachefile) + + except subprocess.CalledProcessError as e: + self.cleanUp() + if 'permission denied' in e.output: + raise unittest.SkipTest( + 'insufficient privileges to run libzfs_core tests') + print 'command failed: ', e.output + raise + except Exception: + self.cleanUp() + raise + + def reset(self): + if self._readonly: + return + + if not self.__class__._recreate_pools: + snaps = [] + for fs in [''] + self._filesystems: + for snap in self.__class__.SNAPSHOTS: + snaps.append(self.makeName(fs + '@' + snap)) + self.getRoot().visitSnaps(lambda snap: snaps.append(snap)) + lzc.lzc_destroy_snaps(snaps, defer=False) + + if self._bmarks_supported: + bmarks = [] + for fs in [''] + self._filesystems: + for bmark in self.__class__.BOOKMARKS: + bmarks.append(self.makeName(fs + '#' + bmark)) + self.getRoot().visitBookmarks( + lambda bmark: bmarks.append(bmark)) + lzc.lzc_destroy_bookmarks(bmarks) + self.getRoot().reset() + return + + try: + subprocess.check_output( + ['zpool', 'destroy', '-f', self._pool_name], stderr=subprocess.STDOUT) + subprocess.check_output( + self._zpool_create, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + print 'command failed: ', e.output + raise + for fs in self._filesystems: + lzc.lzc_create(self.makeName(fs)) + self.getRoot().reset() + + def cleanUp(self): + try: + subprocess.check_output( + ['zpool', 'destroy', '-f', self._pool_name], stderr=subprocess.STDOUT) + except Exception: + pass + try: + os.remove(self._pool_file_path) + except Exception: + pass + try: + os.remove(self._pool_file_path + _TempPool._cachefile_suffix) + except Exception: + pass + try: + os.remove( + self._pool_file_path + _TempPool._cachefile_suffix + '.tmp') + except Exception: + pass + + def makeName(self, relative=None): + if not relative: + return self._pool_name + if relative.startswith(('@', '#')): + return self._pool_name + relative + return self._pool_name + '/' + relative + + def makeTooLongName(self, prefix=None): + if not prefix: + prefix = 'x' + prefix = self.makeName(prefix) + pad_len = lzc.MAXNAMELEN + 1 - len(prefix) + if pad_len > 0: + return prefix + 'x' * pad_len + else: + return prefix + + def makeTooLongComponent(self, prefix=None): + padding = 'x' * (lzc.MAXNAMELEN + 1) + if not prefix: + prefix = padding + else: + prefix = prefix + padding + return self.makeName(prefix) + + def getRoot(self): + return self._root + + def isPoolFeatureAvailable(self, feature): + output = subprocess.check_output( + ['zpool', 'get', '-H', 'feature@' + feature, self._pool_name]) + output = output.strip() + return output != '' + + def isPoolFeatureEnabled(self, feature): + output = subprocess.check_output( + ['zpool', 'get', '-H', 'feature@' + feature, self._pool_name]) + output = output.split()[2] + return output in ['active', 'enabled'] + + +class _Filesystem(object): + + def __init__(self, name): + self._name = name + self.reset() + + def getName(self): + return self._name + + def reset(self): + self._children = [] + self._fs_id = 0 + self._snap_id = 0 + self._bmark_id = 0 + + def getFilesystem(self): + self._fs_id += 1 + fsname = self._name + '/fs' + bytes(self._fs_id) + fs = _Filesystem(fsname) + self._children.append(fs) + return fs + + def _makeSnapName(self, i): + return self._name + '@snap' + bytes(i) + + def getSnap(self): + self._snap_id += 1 + return self._makeSnapName(self._snap_id) + + def _makeBookmarkName(self, i): + return self._name + '#bmark' + bytes(i) + + def getBookmark(self): + self._bmark_id += 1 + return self._makeBookmarkName(self._bmark_id) + + def _makeTooLongName(self, too_long_component): + if too_long_component: + return 'x' * (lzc.MAXNAMELEN + 1) + + # Note that another character is used for one of '/', '@', '#'. + comp_len = lzc.MAXNAMELEN - len(self._name) + if comp_len > 0: + return 'x' * comp_len + else: + return 'x' + + def getTooLongFilesystemName(self, too_long_component): + return self._name + '/' + self._makeTooLongName(too_long_component) + + def getTooLongSnap(self, too_long_component): + return self._name + '@' + self._makeTooLongName(too_long_component) + + def getTooLongBookmark(self, too_long_component): + return self._name + '#' + self._makeTooLongName(too_long_component) + + def _visitFilesystems(self, visitor): + for child in self._children: + child._visitFilesystems(visitor) + visitor(self) + + def visitFilesystems(self, visitor): + def _fsVisitor(fs): + visitor(fs._name) + + self._visitFilesystems(_fsVisitor) + + def visitSnaps(self, visitor): + def _snapVisitor(fs): + for i in range(1, fs._snap_id + 1): + visitor(fs._makeSnapName(i)) + + self._visitFilesystems(_snapVisitor) + + def visitBookmarks(self, visitor): + def _bmarkVisitor(fs): + for i in range(1, fs._bmark_id + 1): + visitor(fs._makeBookmarkName(i)) + + self._visitFilesystems(_bmarkVisitor) + + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/libzfs_core/test/test_nvlist.py b/contrib/pyzfs/libzfs_core/test/test_nvlist.py new file mode 100644 index 000000000..61a4b69c2 --- /dev/null +++ b/contrib/pyzfs/libzfs_core/test/test_nvlist.py @@ -0,0 +1,612 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +""" +Tests for _nvlist module. +The tests convert from a `dict` to C ``nvlist_t`` and back to a `dict` +and verify that no information is lost and value types are correct. +The tests also check that various error conditions like unsupported +value types or out of bounds values are detected. +""" + +import unittest + +from .._nvlist import nvlist_in, nvlist_out, _lib +from ..ctypes import ( + uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, + uint64_t, int64_t, boolean_t, uchar_t +) + + +class TestNVList(unittest.TestCase): + + def _dict_to_nvlist_to_dict(self, props): + res = {} + nv_in = nvlist_in(props) + with nvlist_out(res) as nv_out: + _lib.nvlist_dup(nv_in, nv_out, 0) + return res + + def _assertIntDictsEqual(self, dict1, dict2): + self.assertEqual(len(dict1), len(dict1), "resulting dictionary is of different size") + for key in dict1.keys(): + self.assertEqual(int(dict1[key]), int(dict2[key])) + + def _assertIntArrayDictsEqual(self, dict1, dict2): + self.assertEqual(len(dict1), len(dict1), "resulting dictionary is of different size") + for key in dict1.keys(): + val1 = dict1[key] + val2 = dict2[key] + self.assertEqual(len(val1), len(val2), "array values of different sizes") + for x, y in zip(val1, val2): + self.assertEqual(int(x), int(y)) + + def test_empty(self): + res = self._dict_to_nvlist_to_dict({}) + self.assertEqual(len(res), 0, "expected empty dict") + + def test_invalid_key_type(self): + with self.assertRaises(TypeError): + self._dict_to_nvlist_to_dict({1: None}) + + def test_invalid_val_type__tuple(self): + with self.assertRaises(TypeError): + self._dict_to_nvlist_to_dict({"key": (1, 2)}) + + def test_invalid_val_type__set(self): + with self.assertRaises(TypeError): + self._dict_to_nvlist_to_dict({"key": set(1, 2)}) + + def test_invalid_array_val_type(self): + with self.assertRaises(TypeError): + self._dict_to_nvlist_to_dict({"key": [(1, 2), (3, 4)]}) + + def test_invalid_array_of_arrays_val_type(self): + with self.assertRaises(TypeError): + self._dict_to_nvlist_to_dict({"key": [[1, 2], [3, 4]]}) + + def test_string_value(self): + props = {"key": "value"} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_implicit_boolean_value(self): + props = {"key": None} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_boolean_values(self): + props = {"key1": True, "key2": False} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_explicit_boolean_true_value(self): + props = {"key": boolean_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_boolean_false_value(self): + props = {"key": boolean_t(0)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_boolean_invalid_value(self): + with self.assertRaises(OverflowError): + props = {"key": boolean_t(2)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_boolean_another_invalid_value(self): + with self.assertRaises(OverflowError): + props = {"key": boolean_t(-1)} + self._dict_to_nvlist_to_dict(props) + + def test_uint64_value(self): + props = {"key": 1} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_uint64_max_value(self): + props = {"key": 2 ** 64 - 1} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_uint64_too_large_value(self): + props = {"key": 2 ** 64} + with self.assertRaises(OverflowError): + self._dict_to_nvlist_to_dict(props) + + def test_uint64_negative_value(self): + props = {"key": -1} + with self.assertRaises(OverflowError): + self._dict_to_nvlist_to_dict(props) + + def test_explicit_uint64_value(self): + props = {"key": uint64_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_uint64_max_value(self): + props = {"key": uint64_t(2 ** 64 - 1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_uint64_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": uint64_t(2 ** 64)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_uint64_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": uint64_t(-1)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_uint32_value(self): + props = {"key": uint32_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_uint32_max_value(self): + props = {"key": uint32_t(2 ** 32 - 1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_uint32_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": uint32_t(2 ** 32)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_uint32_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": uint32_t(-1)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_uint16_value(self): + props = {"key": uint16_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_uint16_max_value(self): + props = {"key": uint16_t(2 ** 16 - 1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_uint16_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": uint16_t(2 ** 16)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_uint16_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": uint16_t(-1)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_uint8_value(self): + props = {"key": uint8_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_uint8_max_value(self): + props = {"key": uint8_t(2 ** 8 - 1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_uint8_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": uint8_t(2 ** 8)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_uint8_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": uint8_t(-1)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_byte_value(self): + props = {"key": uchar_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_byte_max_value(self): + props = {"key": uchar_t(2 ** 8 - 1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_byte_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": uchar_t(2 ** 8)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_byte_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": uchar_t(-1)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_int64_value(self): + props = {"key": int64_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int64_max_value(self): + props = {"key": int64_t(2 ** 63 - 1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int64_min_value(self): + props = {"key": int64_t(-(2 ** 63))} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int64_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": int64_t(2 ** 63)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_int64_too_small_value(self): + with self.assertRaises(OverflowError): + props = {"key": int64_t(-(2 ** 63) - 1)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_int32_value(self): + props = {"key": int32_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int32_max_value(self): + props = {"key": int32_t(2 ** 31 - 1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int32_min_value(self): + props = {"key": int32_t(-(2 ** 31))} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int32_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": int32_t(2 ** 31)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_int32_too_small_value(self): + with self.assertRaises(OverflowError): + props = {"key": int32_t(-(2 ** 31) - 1)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_int16_value(self): + props = {"key": int16_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int16_max_value(self): + props = {"key": int16_t(2 ** 15 - 1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int16_min_value(self): + props = {"key": int16_t(-(2 ** 15))} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int16_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": int16_t(2 ** 15)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_int16_too_small_value(self): + with self.assertRaises(OverflowError): + props = {"key": int16_t(-(2 ** 15) - 1)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_int8_value(self): + props = {"key": int8_t(1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int8_max_value(self): + props = {"key": int8_t(2 ** 7 - 1)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int8_min_value(self): + props = {"key": int8_t(-(2 ** 7))} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_explicit_int8_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": int8_t(2 ** 7)} + self._dict_to_nvlist_to_dict(props) + + def test_explicit_int8_too_small_value(self): + with self.assertRaises(OverflowError): + props = {"key": int8_t(-(2 ** 7) - 1)} + self._dict_to_nvlist_to_dict(props) + + def test_nested_dict(self): + props = {"key": {}} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_nested_nested_dict(self): + props = {"key": {"key": {}}} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_mismatching_values_array(self): + props = {"key": [1, "string"]} + with self.assertRaises(TypeError): + self._dict_to_nvlist_to_dict(props) + + def test_mismatching_values_array2(self): + props = {"key": [True, 10]} + with self.assertRaises(TypeError): + self._dict_to_nvlist_to_dict(props) + + def test_mismatching_values_array3(self): + props = {"key": [1, False]} + with self.assertRaises(TypeError): + self._dict_to_nvlist_to_dict(props) + + def test_string_array(self): + props = {"key": ["value", "value2"]} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_boolean_array(self): + props = {"key": [True, False]} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_explicit_boolean_array(self): + props = {"key": [boolean_t(False), boolean_t(True)]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_uint64_array(self): + props = {"key": [0, 1, 2 ** 64 - 1]} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_uint64_array_too_large_value(self): + props = {"key": [0, 2 ** 64]} + with self.assertRaises(OverflowError): + self._dict_to_nvlist_to_dict(props) + + def test_uint64_array_negative_value(self): + props = {"key": [0, -1]} + with self.assertRaises(OverflowError): + self._dict_to_nvlist_to_dict(props) + + def test_mixed_explict_int_array(self): + with self.assertRaises(TypeError): + props = {"key": [uint64_t(0), uint32_t(0)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_uint64_array(self): + props = {"key": [uint64_t(0), uint64_t(1), uint64_t(2 ** 64 - 1)]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_explict_uint64_array_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uint64_t(0), uint64_t(2 ** 64)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_uint64_array_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uint64_t(0), uint64_t(-1)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_uint32_array(self): + props = {"key": [uint32_t(0), uint32_t(1), uint32_t(2 ** 32 - 1)]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_explict_uint32_array_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uint32_t(0), uint32_t(2 ** 32)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_uint32_array_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uint32_t(0), uint32_t(-1)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_uint16_array(self): + props = {"key": [uint16_t(0), uint16_t(1), uint16_t(2 ** 16 - 1)]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_explict_uint16_array_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uint16_t(0), uint16_t(2 ** 16)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_uint16_array_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uint16_t(0), uint16_t(-1)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_uint8_array(self): + props = {"key": [uint8_t(0), uint8_t(1), uint8_t(2 ** 8 - 1)]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_explict_uint8_array_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uint8_t(0), uint8_t(2 ** 8)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_uint8_array_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uint8_t(0), uint8_t(-1)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_byte_array(self): + props = {"key": [uchar_t(0), uchar_t(1), uchar_t(2 ** 8 - 1)]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_explict_byte_array_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uchar_t(0), uchar_t(2 ** 8)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_byte_array_negative_value(self): + with self.assertRaises(OverflowError): + props = {"key": [uchar_t(0), uchar_t(-1)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_int64_array(self): + props = {"key": [int64_t(0), int64_t(1), int64_t(2 ** 63 - 1), int64_t(-(2 ** 63))]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_explict_int64_array_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": [int64_t(0), int64_t(2 ** 63)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_int64_array_too_small_value(self): + with self.assertRaises(OverflowError): + props = {"key": [int64_t(0), int64_t(-(2 ** 63) - 1)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_int32_array(self): + props = {"key": [int32_t(0), int32_t(1), int32_t(2 ** 31 - 1), int32_t(-(2 ** 31))]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_explict_int32_array_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": [int32_t(0), int32_t(2 ** 31)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_int32_array_too_small_value(self): + with self.assertRaises(OverflowError): + props = {"key": [int32_t(0), int32_t(-(2 ** 31) - 1)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_int16_array(self): + props = {"key": [int16_t(0), int16_t(1), int16_t(2 ** 15 - 1), int16_t(-(2 ** 15))]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_explict_int16_array_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": [int16_t(0), int16_t(2 ** 15)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_int16_array_too_small_value(self): + with self.assertRaises(OverflowError): + props = {"key": [int16_t(0), int16_t(-(2 ** 15) - 1)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_int8_array(self): + props = {"key": [int8_t(0), int8_t(1), int8_t(2 ** 7 - 1), int8_t(-(2 ** 7))]} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntArrayDictsEqual(props, res) + + def test_explict_int8_array_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"key": [int8_t(0), int8_t(2 ** 7)]} + self._dict_to_nvlist_to_dict(props) + + def test_explict_int8_array_too_small_value(self): + with self.assertRaises(OverflowError): + props = {"key": [int8_t(0), int8_t(-(2 ** 7) - 1)]} + self._dict_to_nvlist_to_dict(props) + + def test_dict_array(self): + props = {"key": [{"key": 1}, {"key": None}, {"key": {}}]} + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + def test_implicit_uint32_value(self): + props = {"rewind-request": 1} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_implicit_uint32_max_value(self): + props = {"rewind-request": 2 ** 32 - 1} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_implicit_uint32_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"rewind-request": 2 ** 32} + self._dict_to_nvlist_to_dict(props) + + def test_implicit_uint32_negative_value(self): + with self.assertRaises(OverflowError): + props = {"rewind-request": -1} + self._dict_to_nvlist_to_dict(props) + + def test_implicit_int32_value(self): + props = {"pool_context": 1} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_implicit_int32_max_value(self): + props = {"pool_context": 2 ** 31 - 1} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_implicit_int32_min_value(self): + props = {"pool_context": -(2 ** 31)} + res = self._dict_to_nvlist_to_dict(props) + self._assertIntDictsEqual(props, res) + + def test_implicit_int32_too_large_value(self): + with self.assertRaises(OverflowError): + props = {"pool_context": 2 ** 31} + self._dict_to_nvlist_to_dict(props) + + def test_implicit_int32_too_small_value(self): + with self.assertRaises(OverflowError): + props = {"pool_context": -(2 ** 31) - 1} + self._dict_to_nvlist_to_dict(props) + + def test_complex_dict(self): + props = { + "key1": "str", + "key2": 10, + "key3": { + "skey1": True, + "skey2": None, + "skey3": [ + True, + False, + True + ] + }, + "key4": [ + "ab", + "bc" + ], + "key5": [ + 2 ** 64 - 1, + 1, + 2, + 3 + ], + "key6": [ + { + "skey71": "a", + "skey72": "b", + }, + { + "skey71": "c", + "skey72": "d", + }, + { + "skey71": "e", + "skey72": "f", + } + + ], + "type": 2 ** 32 - 1, + "pool_context": -(2 ** 31) + } + res = self._dict_to_nvlist_to_dict(props) + self.assertEqual(props, res) + + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/contrib/pyzfs/setup.py b/contrib/pyzfs/setup.py new file mode 100644 index 000000000..f86f3c1bd --- /dev/null +++ b/contrib/pyzfs/setup.py @@ -0,0 +1,40 @@ +# Copyright 2015 ClusterHQ. See LICENSE file for details. + +from setuptools import setup, find_packages + +setup( + name="pyzfs", + version="0.2.3", + description="Wrapper for libzfs_core", + author="ClusterHQ", + author_email="[email protected]", + url="http://pyzfs.readthedocs.org", + license="Apache License, Version 2.0", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 2 :: Only", + "Programming Language :: Python :: 2.7", + "Topic :: System :: Filesystems", + "Topic :: Software Development :: Libraries", + ], + keywords=[ + "ZFS", + "OpenZFS", + "libzfs_core", + ], + + packages=find_packages(), + include_package_data=True, + install_requires=[ + "cffi", + ], + setup_requires=[ + "cffi", + ], + zip_safe=False, + test_suite="libzfs_core.test", +) + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 |