summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--contrib/pyzfs/LICENSE201
-rw-r--r--contrib/pyzfs/README28
-rw-r--r--contrib/pyzfs/docs/source/conf.py304
-rw-r--r--contrib/pyzfs/docs/source/index.rst44
-rw-r--r--contrib/pyzfs/libzfs_core/__init__.py100
-rw-r--r--contrib/pyzfs/libzfs_core/_constants.py10
-rw-r--r--contrib/pyzfs/libzfs_core/_error_translation.py629
-rw-r--r--contrib/pyzfs/libzfs_core/_libzfs_core.py1270
-rw-r--r--contrib/pyzfs/libzfs_core/_nvlist.py259
-rw-r--r--contrib/pyzfs/libzfs_core/bindings/__init__.py45
-rw-r--r--contrib/pyzfs/libzfs_core/bindings/libnvpair.py117
-rw-r--r--contrib/pyzfs/libzfs_core/bindings/libzfs_core.py99
-rw-r--r--contrib/pyzfs/libzfs_core/ctypes.py56
-rw-r--r--contrib/pyzfs/libzfs_core/exceptions.py443
-rw-r--r--contrib/pyzfs/libzfs_core/test/__init__.py0
-rw-r--r--contrib/pyzfs/libzfs_core/test/test_libzfs_core.py3708
-rw-r--r--contrib/pyzfs/libzfs_core/test/test_nvlist.py612
-rw-r--r--contrib/pyzfs/setup.py40
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