From 6abf922574f39ad597ae122fa43d2fa811970720 Mon Sep 17 00:00:00 2001 From: Andriy Gapon Date: Sat, 3 Mar 2018 11:10:34 +0100 Subject: [PATCH] Import pyzfs source code from ClusterHQ 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. Original-patch-by: Andriy Gapon Reviewed-by: Brian Behlendorf Ported-by: loli10K Signed-off-by: loli10K Closes #7230 --- contrib/pyzfs/LICENSE | 201 + contrib/pyzfs/README | 28 + contrib/pyzfs/docs/source/conf.py | 304 ++ contrib/pyzfs/docs/source/index.rst | 44 + contrib/pyzfs/libzfs_core/__init__.py | 100 + contrib/pyzfs/libzfs_core/_constants.py | 10 + .../pyzfs/libzfs_core/_error_translation.py | 629 +++ contrib/pyzfs/libzfs_core/_libzfs_core.py | 1270 ++++++ contrib/pyzfs/libzfs_core/_nvlist.py | 259 ++ .../pyzfs/libzfs_core/bindings/__init__.py | 45 + .../pyzfs/libzfs_core/bindings/libnvpair.py | 117 + .../pyzfs/libzfs_core/bindings/libzfs_core.py | 99 + contrib/pyzfs/libzfs_core/ctypes.py | 56 + contrib/pyzfs/libzfs_core/exceptions.py | 443 ++ contrib/pyzfs/libzfs_core/test/__init__.py | 0 .../libzfs_core/test/test_libzfs_core.py | 3708 +++++++++++++++++ contrib/pyzfs/libzfs_core/test/test_nvlist.py | 612 +++ contrib/pyzfs/setup.py | 40 + 18 files changed, 7965 insertions(+) create mode 100644 contrib/pyzfs/LICENSE create mode 100644 contrib/pyzfs/README create mode 100644 contrib/pyzfs/docs/source/conf.py create mode 100644 contrib/pyzfs/docs/source/index.rst create mode 100644 contrib/pyzfs/libzfs_core/__init__.py create mode 100644 contrib/pyzfs/libzfs_core/_constants.py create mode 100644 contrib/pyzfs/libzfs_core/_error_translation.py create mode 100644 contrib/pyzfs/libzfs_core/_libzfs_core.py create mode 100644 contrib/pyzfs/libzfs_core/_nvlist.py create mode 100644 contrib/pyzfs/libzfs_core/bindings/__init__.py create mode 100644 contrib/pyzfs/libzfs_core/bindings/libnvpair.py create mode 100644 contrib/pyzfs/libzfs_core/bindings/libzfs_core.py create mode 100644 contrib/pyzfs/libzfs_core/ctypes.py create mode 100644 contrib/pyzfs/libzfs_core/exceptions.py create mode 100644 contrib/pyzfs/libzfs_core/test/__init__.py create mode 100644 contrib/pyzfs/libzfs_core/test/test_libzfs_core.py create mode 100644 contrib/pyzfs/libzfs_core/test/test_nvlist.py create mode 100644 contrib/pyzfs/setup.py diff --git a/contrib/pyzfs/LICENSE b/contrib/pyzfs/LICENSE new file mode 100644 index 0000000000..370c9bc6f9 --- /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 0000000000..bb3a7f0ffc --- /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 0000000000..511c9b2bc7 --- /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 +# " v 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 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 0000000000..36c227a499 --- /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 0000000000..60e0c25148 --- /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 `_. + +.. 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 0000000000..45016b4313 --- /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 0000000000..64ce870ab7 --- /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 0000000000..00824f5f6b --- /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 0000000000..1f1c39bbf2 --- /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 0000000000..d6fd2b8bae --- /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 0000000000..d3f3adf4ba --- /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 +""" + +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 0000000000..d0bf570c31 --- /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 +""" + +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 0000000000..bd168f22ad --- /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 0000000000..c52d437719 --- /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 0000000000..e69de29bb2 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 0000000000..b6c971c9c1 --- /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 + + +@contextlib.contextmanager +def suppress(exceptions=None): + try: + yield + except BaseException as e: + if exceptions is None or isinstance(e, exceptions): + pass + else: + raise + + +@contextlib.contextmanager +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 = {} + + +@contextlib.contextmanager +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) + + +@contextlib.contextmanager +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) + + +@contextlib.contextmanager +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 + + +@contextlib.contextmanager +def cleanup_fd(): + fd = os.open('/dev/zfs', os.O_EXCL) + try: + yield fd + finally: + os.close(fd) + + +@contextlib.contextmanager +def os_open(name, mode): + fd = os.open(name, mode) + try: + yield fd + finally: + os.close(fd) + + +@contextlib.contextmanager +def dev_null(): + with os_open('/dev/null', os.O_WRONLY) as fd: + yield fd + + +@contextlib.contextmanager +def dev_zero(): + with os_open('/dev/zero', os.O_RDONLY) as fd: + yield fd + + +@contextlib.contextmanager +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)) + + +@contextlib.contextmanager +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 0000000000..61a4b69c26 --- /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 0000000000..f86f3c1bd9 --- /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="support@clusterhq.com", + 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