pax_global_header00006660000000000000000000000064134174463440014524gustar00rootroot0000000000000052 comment=97f143134911598b3364e4ba125744cefb8543af python-pyxs_0.4.2~git20190115.97f14313/000077500000000000000000000000001341744634400166605ustar00rootroot00000000000000python-pyxs_0.4.2~git20190115.97f14313/.travis.yml000066400000000000000000000001511341744634400207660ustar00rootroot00000000000000language: python python: - "2.6" - "2.7" - "3.4" - "3.5" - "pypy" script: python setup.py test python-pyxs_0.4.2~git20190115.97f14313/AUTHORS000066400000000000000000000002421341744634400177260ustar00rootroot00000000000000Authors ======= - Sergei Lebedev - Fedor Gogolev Contributors ------------ - Sandeep Murthy python-pyxs_0.4.2~git20190115.97f14313/CHANGES000066400000000000000000000077561341744634400176720ustar00rootroot00000000000000pyxs Changelog ============== Here you can see the full list of changes between each pyxs release. Version 0.4.2-dev ------------- - Allowed values to be empty ``b""``. Thanks to Stephen Czetty. See PR #13 on GitHub. Version 0.4.1 ------------- Bugfix release, released on May 11th, 2016 - Fixed a bug in ``XenBusConnection.create_transport`` which failed on attribute lookup. See PR #7 on GitHub. Version 0.4.0 ------------- Released on March 6th, 2016 - Fixed a bug in ``Client.set_permissions`` which coerced permission lists (e.g. ``["b0"]``) to repr-strings prior to validation. - The API is now based around ``bytes``, which means that all methods which used to accept ``str`` (or text) now require ``bytes``. XenStore paths and values are specified to be 7-bit ASCII, thus it makes little sense to allow any Unicode string as input and then validate if it matches the spec. - Removed ``transaction`` argument from ``Client`` constructor. The user is advised to use the corresponding methods explicitly. - Removed ``connection`` argument from ``Client`` constructor. The user should now wrap it in a ``Router``. - Renamed some of the ``Client`` methods to more human-readable names: ================= =================== Old name New name ================= =================== ls list rm delete get_permissions get_perms set_permissions set_perms transaction_start transaction transaction_end commit and rollback ================= =================== - Removed ``Client.transaction`` context manager, because it didn't provide a way to handle possible commit failure. - Added ``Client.exists`` for one-line path existence checks. See PR #6 on GitHub. Thanks to Sandeep Murthy. - Removed implicit reconnection logic from ``FileDescriptorConnection``. The user is now expected to connect manually. - Changed ``XenBusConnection`` to prefer ``/dev/xen/xenbus`` on Linux due to a possible deadlock in XenBus backend. - Changed ``UnixSocketConnection`` to use ``socket.socket`` instead of the corresponding file descriptor. - Disallowed calling ``Client.monitor`` over ``XenBusConnection``. See http://lists.xen.org/archives/html/xen-devel/2016-02/msg03816 for details. Version 0.3.1 ------------- Released on November 29th 2012 - Added ``default`` argument to ``Client.read()``, which acts similar to ``dict.get()``. - Fixed a lot of minor quirks so ``pyxs`` can be Debianized. Version 0.3 ----------- Released on September 12th 2011 - Moved all PUBSUB functionality into a separate ``Monitor`` class, which uses a *separate* connection. That way, we'll never have to worry about mixing incoming XenStore events and command replies. - Fixed a couple of nasty bugs in concurrent use of ``Client.wait()`` with other commands (see above). Version 0.2 ----------- Released on August 18th 2011 - Completely refactored validation -- no more `@spec` magic, everything is checked explicitly inside ``Client.execute_command()``. - Added a compatibility interface, which mimics `xen.lowlevel.xs` behaviour, using ``pyxs`` as a backend, see pyxs/_compat.py. - Restricted `SET_TARGET`, `INTRODUCE` and `RELEASE` operations to Dom0 only -- ``/proc/xen/capabilities`` is used to check domain role. - Fixed a bug in ``Client.wait()`` -- queued watch events weren't wrapped in ``pyxs._internal.Event`` class, unlike the received ones. - Added ``Client.walk()`` method for walking XenStore tree -- similar to ``os.walk()`` Version 0.1 ----------- Initial release, released on July 16th 2011 - Added a complete implementation of XenStore protocol, including transactions and path watching, see ``pyxs.Client`` for details. - Added generic validation helper -- `@spec`, which forces arguments to match the scheme from the wire protocol specification. - Added two connection backends -- ``XenBusConnection`` for connecting from DomU through a block device and ``UnixSocketConnection``, communicating with XenStore via a Unix domain socket. python-pyxs_0.4.2~git20190115.97f14313/LICENSE000066400000000000000000000167431341744634400177000ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. python-pyxs_0.4.2~git20190115.97f14313/MANIFEST.in000066400000000000000000000002141341744634400204130ustar00rootroot00000000000000recursive-include examples * recursive-include pyxs *.py include CHANGES include README include AUTHORS include LICENSE include MANIFEST.in python-pyxs_0.4.2~git20190115.97f14313/README000066400000000000000000000023111341744634400175350ustar00rootroot00000000000000.. -*- mode: rst -*- :: .---. .-..-..-.,-. .--. : .; `: :; :`. .'`._-.' : ._.'`._. ;:_,._;`.__.' : : .-. : :_; `._.' 0.4.2-dev -- XenStore access the Python way! What is ``pyxs``? ----------------- It's a pure Python XenStore client implementation, which covers all of the ``libxs`` features and adds some nice Pythonic sugar on top. Here's a shortlist: * ``pyxs`` supports both Python 2 and 3, * works over a Unix socket or XenBus, * has a clean and well-documented API, * is written in easy to understand Python, * can be used with `gevent `_ or `eventlet `_. Installation ------------ If you have `pip `_ you can do the usual:: pip install --user pyxs Otherwise, download the source from `GitHub `_ and run:: python setup.py install Fedora users can install the package from the system repository:: dnf install python2-pyxs dnf install python3-pyxs RHEL/CentOS users can install the package from the `EPEL `_ repository:: yum install python2-pyxs yum install python34-pyxs python-pyxs_0.4.2~git20190115.97f14313/README.rst000077700000000000000000000000001341744634400212222READMEustar00rootroot00000000000000python-pyxs_0.4.2~git20190115.97f14313/docs/000077500000000000000000000000001341744634400176105ustar00rootroot00000000000000python-pyxs_0.4.2~git20190115.97f14313/docs/Makefile000066400000000000000000000071361341744634400212570ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex latexpdf changes linkcheck doctest all: html help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* -rm -rf generated/* -rm -rf auto_examples/* html: # These two lines make the build a bit more lengthy, and the # the embedding of images more robust rm -rf $(BUILDDIR)/html/_images #rm -rf _build/doctrees/ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/hmmlearn.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/hmmlearn.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." python-pyxs_0.4.2~git20190115.97f14313/docs/api.rst000066400000000000000000000014411341744634400211130ustar00rootroot00000000000000.. _api: API reference ============= Client and Monitor ------------------ .. autoclass:: pyxs.client.Client :members: .. autoclass:: pyxs.client.Monitor :members: .. autofunction:: pyxs.monitor Exceptions ---------- .. autoclass:: pyxs.exceptions.PyXSError .. autoclass:: pyxs.exceptions.InvalidOperation .. autoclass:: pyxs.exceptions.InvalidPayload .. autoclass:: pyxs.exceptions.InvalidPath .. autoclass:: pyxs.exceptions.InvalidPermission .. autoclass:: pyxs.exceptions.ConnectionError .. autoclass:: pyxs.exceptions.UnexpectedPacket Internals --------- .. autoclass:: pyxs.client.Router :members: .. autoclass:: pyxs.connection.XenBusConnection .. autoclass:: pyxs.connection.UnixSocketConnection .. autoclass:: pyxs._internal.Packet .. autodata:: pyxs._internal.Op python-pyxs_0.4.2~git20190115.97f14313/docs/changelog.rst000066400000000000000000000000301341744634400222620ustar00rootroot00000000000000.. include:: ../CHANGES python-pyxs_0.4.2~git20190115.97f14313/docs/conf.py000066400000000000000000000205601341744634400211120ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # pyxs documentation build configuration file, created by # sphinx-quickstart on Mon Jul 11 20:45:14 2011. # # 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, os # 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', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.linkcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. 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'pyxs' copyright = u'2011-2012, Selectel' # 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.4.2' # The full version, including alpha/beta/rc tags. release = '0.4.2-dev' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #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 = ['_build'] # 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 = [] linkcode_base_url = "https://github.com/selectel/pyxs/tree/" def resolve_tag(): from urllib.request import urlopen from urllib.error import HTTPError try: urlopen(linkcode_base_url + release) except HTTPError: return "master" else: return release tag = resolve_tag() # Resolve function for the linkcode extension. def linkcode_resolve(domain, info): def find_source(): # try to find the file and line number, based on code from numpy: # https://github.com/numpy/numpy/blob/master/doc/source/conf.py#L286 obj = sys.modules[info['module']] for part in info['fullname'].split('.'): obj = getattr(obj, part) import inspect import os import pyxs fn = inspect.getsourcefile(obj) fn = os.path.relpath(fn, os.path.dirname(pyxs.__file__)) source, lineno = inspect.getsourcelines(obj) return fn, lineno, lineno + len(source) - 1 try: filename = 'pyxs/%s#L%d-L%d' % find_source() except Exception: return None # Failed to resolve source or line numbers. return linkcode_base_url + "%s/%s" % (tag, filename) # -- Options for HTML output --------------------------------------------------- ## Read the docs style: try: import sphinx_rtd_theme except ImportError: html_theme = 'classic' else: html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_theme = 'sphinx_rtd_theme' # 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'] # 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 # Output file base name for HTML help builder. htmlhelp_basename = 'pyxsdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'pyxs.tex', u'pyxs Documentation', u'Sergei Lebedev, Fedor Gogolev', '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 # Additional stuff for the LaTeX preamble. #latex_preamble = '' # 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 = [ ('index', 'pyxs', u'pyxs Documentation', [u'Sergei Lebedev, Fedor Gogolev'], 3) ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} autodoc_member_order = 'bysource' python-pyxs_0.4.2~git20190115.97f14313/docs/contributing.rst000066400000000000000000000025611341744634400230550ustar00rootroot00000000000000Contributing ============ Submitting a bug report ----------------------- In case you experience issues using ``pyxs``, do not hesitate to report it to the `Bug Tracker `_ on GitHub. Setting up development environment ---------------------------------- Writing a XenStore client library without having access to a running XenStore instance can be troublesome. Luckily, there is a way to setup a development using VirtualBox. 1. Create a VM running Ubuntu 14.04 or later. 2. `Install `_ Xen hypervisor: ``sudo apt-get install xen-hypervisor-4.4-amd64``. 3. `Configure `_ VM for SSH access. 4. Done! You can now ``rsync`` your changes to the VM and run the tests. Running the tests ----------------- Only ``root`` is allowed to access XenStore, so the tests require ``sudo``:: $ sudo python setup.py test ``pyxs`` strives to work across a range of Python versions. Use ``tox`` to run the tests on all supported versions:: $ cat tox.ini [tox] envlist = py26,py27,py34,py35,pypy [testenv] commands = python setup.py test $ sudo tox Style guide ----------- ``pyxs`` follows Pocoo style guide. Please `read it `_ before you start implementing your changes. python-pyxs_0.4.2~git20190115.97f14313/docs/index.rst000066400000000000000000000005711341744634400214540ustar00rootroot00000000000000.. include:: ../README Show me the code! ----------------- Head over to our brief :ref:`tutorial` or, if you're feeling brave, dive right into the :ref:`api`; ``pyxs`` also has a couple of examples in the `examples `_ directory. .. toctree:: :hidden: :maxdepth: 2 tutorial api contributing changelog python-pyxs_0.4.2~git20190115.97f14313/docs/tutorial.rst000066400000000000000000000117271341744634400222150ustar00rootroot00000000000000.. _tutorial: Tutorial ======== Basics ------ Using :mod:`pyxs` is easy! The only class you need to import is :class:`~pyxs.client.Client`. It provides a simple straightforward API to XenStore content with a bit of Python's syntactic sugar here and there. Generally, if you just need to fetch or update some XenStore items you can do:: >>> from pyxs import Client >>> with Client() as c: ... c[b"/local/domain/0/name"] = b"Ziggy" ... c[b"/local/domain/0/name"] b'Ziggy' Using :class:`~pyxs.client.Client` without the ``with`` statement is possible, albeit, not recommended: >>> c = Client() >>> c.connect() >>> c[b"/local/domain/0/name"] = b"It works!" >>> c.close() The reason for preferring a context manager is simple: you don't have to DIY. The context manager will make sure that a started transaction was either rolled back or committed and close the underlying XenStore connection afterwards. Connections ----------- :mod:`pyxs` supports two ways of communicating with XenStore: * over a Unix socket with :class:`~pyxs.connection.UnixSocketConnection`; * over XenBus_ with :class:`~pyxs.connection.XenBusConnection`. Connection type is determined from the arguments passed to :class:`~pyxs.client.Client` constructor. For example, the following code creates a :class:`~pyxs.client.Client` instance, operating over a Unix socket:: >>> Client(unix_socket_path="/var/run/xenstored/socket_ro") Client(UnixSocketConnection('/var/run/xenstored/socket_ro')) >>> Client() Client(UnixSocketConnection('/var/run/xenstored/socket')) Use ``xen_bus_path`` argument to initialize a :class:`~pyxs.client.Client` with :class:`~pyxs.connection.XenBusConnection`:: >>> Client(xen_bus_path="/dev/xen/xenbus") Client(XenBusConnection('/dev/xen/xenbus')) .. _XenBus: http://wiki.xensource.com/xenwiki/XenBus Transactions ------------ Transactions allow you to operate on an isolated copy of XenStore tree and merge your changes back atomically on commit. Keep in mind, however, that changes made within a transaction become available to other XenStore clients only if and when committed. Here's an example:: >>> with Client() as c: ... c.transaction() ... c[b"/foo/bar"] = b"baz" ... c.commit() # ! ... print(c[b"/foo/bar"]) b'baz' The line with an exclamation mark is a bit careless, because it ignores the fact that committing a transaction might fail. A more robust way to commit a transaction is by using a loop:: >>> with Client() as c: ... success = False ... while not success: ... c.transaction() ... c[b"/foo/bar"] = b"baz" ... success = c.commit() You can also abort the current transaction by calling :meth:`~pyxs.client.Client.rollback`. Events ------ When a new path is created or an existing path is modified, XenStore fires an event, notifying all watching clients that a change has been made. :mod:`pyxs` implements watching via the :class:`Monitor` class. To watch a path create a monitor :meth:`~pyxs.client.Client.monitor` and call :meth:`~pyxs.client.Monitor.watch` with a path you want to watch and a unique token. Right after that the monitor will start to accumulate incoming events. You can iterate over them via :meth:`~pyxs.client.Monitor.wait`:: >>> with Client() as c: ... m = c.monitor() ... m.watch(b"/foo/bar", b"a unique token") ... next(m.wait()) Event(b"/foo/bar", b"a unique token") XenStore has a notion of *special* paths, which start with ``@`` and are reserved for special occasions: ================ ================================================ Path Description ---------------- ------------------------------------------------ @introduceDomain Fired when a **new** domain is introduced to XenStore -- you can also introduce domains yourself with a :meth:`~pyxs.client.Client.introduce_domain` call, but in most of the cases, ``xenstored`` will do that for you. @releaseDomain Fired when XenStore is no longer communicating with a domain, see :meth:`~pyxs.client.Client.release_domain`. ================ ================================================ Events for both special and ordinary paths are simple two element tuples, where the first element is always `event target` -- a path which triggered the event and second is a token passed to :meth:`~pyxs.client.Monitor.watch`. A rather unfortunate consequence of this is that you can't get `domid` of the domain, which triggered @introduceDomain or @releaseDomain from the received event. Compatibility API ----------------- :mod:`pyxs` also provides a compatibility interface, which mimics that of ``xen.lowlevel.xs`` --- so you don't have to change anything in the code to switch to :mod:`pyxs`:: >>> from pyxs import xs >>> handle = xs() >>> handle.read("0", b"/local/domain/0/name") b'Domain-0' >>> handle.close() python-pyxs_0.4.2~git20190115.97f14313/examples/000077500000000000000000000000001341744634400204765ustar00rootroot00000000000000python-pyxs_0.4.2~git20190115.97f14313/examples/helloworld.py000066400000000000000000000035351341744634400232310ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ helloworld ~~~~~~~~~~ A minimal working example, showing :mod:`pyxs` API usage. :copyright: (c) 2016 by pyxs authors and contributors, see AUTHORS for more details. """ from __future__ import print_function from pyxs import Client, PyXSError if __name__ == "__main__": with Client() as c: # a) write-read. c[b"/foo/bar"] = b"baz" print(c[b"/foo/bar"]) # ==> "baz" # b) exceptions! let's try to read a non-existant path. try: c[b"/path/to/something/useless"] except PyXSError as e: print(e) # c) okay, time to delete that /foo/bar path. del c[b"/foo/bar"] try: c[b"/foo/bar"] except PyXSError as e: print("`/foo/bar` is no moar!") # d) directory listing and permissions. c.mkdir(b"/foo/bar") c.mkdir(b"/foo/baz") c.mkdir(b"/foo/boo") print("Here's what `/foo` has: ", c.list(b"/foo")) print(c.get_perms(b"/foo")) # e) let's watch some paths! c.write(b"/foo/bar", b"baz") with c.monitor() as m: m.watch(b"/foo/bar", b"baz") print("Watching ... do ``xenstore-write /foo/bar ``.") print(next(m.wait())) # 1st. print(next(m.wait())) # 2nd. m.unwatch(b"/foo/bar", b"baz") # f) domain management commands. print(c.get_domain_path(0)) print("domain 0 introduced: ", c.is_domain_introduced(0)) # g) transactions. print("Creating a `/bar/foo` within a transaction.") while True: c.transaction() c[b"/bar/foo"] = b"baz" if c.commit(): break print("Transaction committed. Let's check it: " "/bar/foo =", c[b"/bar/foo"]) python-pyxs_0.4.2~git20190115.97f14313/examples/ls.py000066400000000000000000000015341341744634400214710ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ ls ~~ ``xenstore-ls`` implementation with :mod:`pyxs`. :copyright: (c) 2011 by Selectel, see AUTHORS for more details. """ from __future__ import print_function import posixpath import sys from pyxs.client import Client def main(client, top): # ``xenstore-ls`` doesn't render top level. depth = top.count(b"/") + (top != b"/") for path, value, children in client.walk(top): if path == top: continue node = posixpath.basename(path) indent = " " * (path.count(b"/") - depth) print("{0}{1} = \"{2}\"".format(indent, node, value)) if __name__ == "__main__": try: [path] = sys.argv[1:] or ["/"] except ValueError: sys.exit("usage: %prog PATH") with Client() as client: main(client, posixpath.normpath(path).encode()) python-pyxs_0.4.2~git20190115.97f14313/examples/monitor.py000066400000000000000000000012571341744634400225440ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ monitor ~~~~~~~ A simple monitor, which fires a callback each time a new domains is introduced or released from XenStore. :copyright: (c) 2011 by Selectel, see AUTHORS for more details. """ import pyxs with pyxs.monitor() as m: m.watch(b"@introduceDomain", b"unused") m.watch(b"@releaseDomain", b"unused") for wpath, _token in m.wait(): # Funny thing is -- XenStored doesn't send us domid of the # event target, so we have to get it manually, via ``xc``. if wpath == b"@introduceDomain": print("Hey, we got a new domain here!") else: print("Ooops, we lost him ...") python-pyxs_0.4.2~git20190115.97f14313/pyxs/000077500000000000000000000000001341744634400176635ustar00rootroot00000000000000python-pyxs_0.4.2~git20190115.97f14313/pyxs/__init__.py000066400000000000000000000017061341744634400220000ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pyxs ~~~~ Pure Python bindings for communicating with XenStore. :copyright: (c) 2011 by Selectel, see AUTHORS for more details. :license: LGPL, see LICENSE for more details. """ __all__ = ["Router", "Client", "Monitor", "PyXSError", "ConnectionError", "UnexpectedPacket", "InvalidOperation", "InvalidPath", "InvalidPayload", "xs", "Error"] from contextlib import contextmanager from .client import Router, Client, Monitor from .exceptions import PyXSError, ConnectionError, UnexpectedPacket, \ InvalidOperation, InvalidPath, InvalidPayload from ._compat import xs, Error @contextmanager def monitor(*args, **kwargs): """A simple shortcut for creating :class:`~pyxs.client.Monitor` instances. All arguments are forwared to :class:`~pyxs.client.Client` constructor. """ with Client(*args, **kwargs) as c: with c.monitor() as m: yield m python-pyxs_0.4.2~git20190115.97f14313/pyxs/_compat.py000066400000000000000000000066651341744634400216740ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pyxs._compat ~~~~~~~~~~~~ This module implements compatibility interface for scripts, using ``xen.lowlevel.xs``. :copyright: (c) 2011 by Selectel. :copyright: (c) 2016 by pyxs authors and contributors, see AUTHORS for more details. :license: LGPL, see LICENSE for more details. """ from __future__ import absolute_import __all__ = ["xs", "Error"] import errno from .client import Client from .exceptions import PyXSError as Error class xs(object): """XenStore client with a backward compatible interface, useful for switching from ``xen.lowlevel.xs``. """ def __init__(self): self.client = Client() self.client.connect() self.monitor = self.client.monitor() self.token_aliases = {} def close(self): self.client.close() def get_permissions(self, tx_id, path): self.client.tx_id = int(tx_id or 0) self.client.get_perms(path) def set_permissions(self, tx_id, path, perms): self.client.tx_id = int(tx_id or 0) self.client.set_perms(path, perms) def ls(self, tx_id, path): self.client.tx_id = int(tx_id or 0) try: return self.client.list(path) except Error as e: if e.args[0] == errno.ENOENT: return raise def mkdir(self, tx_id, path): self.client.tx_id = int(tx_id or 0) self.client.mkdir(path) def rm(self, tx_id, path): self.client.tx_id = int(tx_id or 0) self.client.delete(path) def read(self, tx_id, path): self.client.tx_id = int(tx_id or 0) return self.client.read(path) def write(self, tx_id, path, value): self.client.tx_id = int(tx_id or 0) return self.client.write(path, value) def get_domain_path(self, domid): return self.client.get_domain_path(domid) def introduce_domain(self, domid, mfn, eventchn): self.client.introduce_domain(domid, mfn, eventchn) def release_domain(self, domid): self.client.release_domain(domid) def resume_domain(self, domid): self.client.resume_domain(domid) def set_target(self, domid, target): self.client.set_target(domid, target) def transaction_start(self): return str(self.client.transaction()).encode() def transaction_end(self, tx_id, abort=0): self.client.tx_id = int(tx_id or 0) if abort: self.client.rollback() else: try: self.client.commit() except Error as e: if e.args[0] == errno.EAGAIN: return False raise return True def watch(self, path, token): # Even though ``xs.watch`` docstring states that token should be # a string, it in fact can be any Python object. We mimic this # behaviour here, but it can be a source of hard to find bugs. # See http://lists.xen.org/archives/html/xen-devel/2016-03/msg00228 # for discussion. stub = str(id(token)).encode() self.token_aliases[stub] = token return self.monitor.watch(path, stub) def unwatch(self, path, token): stub = str(id(token)).encode() self.monitor.unwatch(path, stub) del self.token_aliases[stub] def read_watch(self): event = next(self.monitor.wait()) return event._replace(token=self.token_aliases[event.token]) python-pyxs_0.4.2~git20190115.97f14313/pyxs/_internal.py000066400000000000000000000057301341744634400222150ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pyxs._internal ~~~~~~~~~~~~~~ A place for secret stuff, not available in the public API. :copyright: (c) 2011 by Selectel. :copyright: (c) 2016 by pyxs authors and contributors, see AUTHORS for more details. :license: LGPL, see LICENSE for more details. """ from __future__ import absolute_import __all__ = ["NUL", "Event", "Op", "Packet", "next_rq_id"] import struct import sys from collections import namedtuple from .exceptions import InvalidOperation, InvalidPayload #: NUL byte. NUL = b"\x00" #: Operations supported by XenStore. Operations = Op = namedtuple("Operations", [ "DEBUG", # 0 "DIRECTORY", # 1 "READ", # 2 "GET_PERMS", # 3 "WATCH", # 4 "UNWATCH", # 5 "TRANSACTION_START", # 6 "TRANSACTION_END", # 7 "INTRODUCE", # 8 "RELEASE", # 9 "GET_DOMAIN_PATH", # 10 "WRITE", # 11 "MKDIR", # 12 "RM", # 13 "SET_PERMS", # 14 "WATCH_EVENT", # 15 "ERROR", # 16 "IS_DOMAIN_INTRODUCED", # 17 "RESUME", # 18 "SET_TARGET", # 19 "RESTRICT" # 128 ])(*(list(range(20)) + [128])) Event = namedtuple("Event", "path token") class Packet(namedtuple("_Packet", "op rq_id tx_id size payload")): """A message to or from XenStore. :param int op: an item from :data:`~pyxs._internal.Op`, representing operation, performed by this packet. :param bytes payload: packet payload, should be a valid ASCII-string with characters between ``[0x20; 0x7f]``. :param int rq_id: request id -- hopefully a **unique** identifier for this packet, XenStore simply echoes this value back in response. :param int tx_id: transaction id, defaults to ``0`` , which means no transaction is running. .. versionchanged:: 0.4.0 ``rq_id`` no longer defaults to ``0`` and should be provided explicitly. """ #: ``xsd_sockmsg`` struct see ``xen/include/public/io/xs_wire.h`` #: for details. _struct = struct.Struct(b"IIII") def __new__(cls, op, payload, rq_id, tx_id=None): # Checking restrictions: # a) payload is limited to 4096 bytes. if len(payload) > 4096: raise InvalidPayload(payload) # b) operation requested is present in ``xsd_sockmsg_type``. if op not in Op: raise InvalidOperation(op) return super(Packet, cls).__new__( cls, op, rq_id, tx_id or 0, len(payload), payload) _rq_id = -1 def next_rq_id(): """Returns the next available request id.""" # XXX we don't need a mutex because of the GIL. global _rq_id _rq_id += 1 _rq_id %= sys.maxsize return _rq_id python-pyxs_0.4.2~git20190115.97f14313/pyxs/client.py000066400000000000000000000562271341744634400215270ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pyxs.client ~~~~~~~~~~~ This module implements XenStore client, which can communicate with XenStore either via: :class:`~pyxs.connection.UnixSocketConnection` or :class:`~pyxs.connection.XenBusConnection`. :copyright: (c) 2011 by Selectel. :copyright: (c) 2016 by pyxs authors and contributors, see AUTHORS for more details. :license: LGPL, see LICENSE for more details. """ from __future__ import absolute_import __all__ = ["Router", "Client", "Monitor"] import copy import errno import posixpath import re import socket import select import sys import threading from collections import defaultdict from functools import partial try: import Queue as queue except ImportError: import queue # XXX see ``Router`` docstring for motivation. if sys.version_info[:2] < (3, 2): _condition_wait = partial(threading._Condition.wait, timeout=1) else: _condition_wait = threading.Condition.wait from ._internal import NUL, Event, Packet, Op, next_rq_id from .connection import UnixSocketConnection, XenBusConnection from .exceptions import UnexpectedPacket, ConnectionError, PyXSError from .helpers import check_path, check_watch_path, check_perms, error _re_7bit_ascii = re.compile(b"^[\x00\x20-\x7f]*$") class Router(object): """Router. The goal of the router is to multiplex XenStore connection between multiple clients and monitors. .. versionadded: 0.4.0 :param connection FileDescriptorConnection: owned by the router. The connection is open when the router is started and remains open until the router is terminated. .. note:: Python lacks API for interrupting a thread from another thread. This means that when a router cannot be stopped when it is blocked in :func:`select.select` or :meth:`~threading.Condition.wait`. The following two "hacks" are used to ensure prompt termination. 1. A router is equipped with a :func:`socket.socketpair`. The reader-end of the pair is selected in the mainloop alongside the XenStore connection, while the writer-end is used in :meth:`~pyxs.client.Router.terminate` to force-stop the mainloop. 2. All operations with :class:`threading.Condition` variables user a 1 second timeout. This "hack" is only relevant for Python prior to 3.2 which didn't allow one to interrupt lock acquisitions. See `issue8844`_ on CPython issue tracker for details. On Python 3.2 and later no timeout is used. .. _issue8844: https://bugs.python.org/issue8844 """ def __init__(self, connection): self.r_terminator, self.w_terminator = socket.socketpair() self.connection = connection self.send_lock = threading.Lock() self.rvars = {} self.monitors = defaultdict(list) # Router thread is daemonic to prevent blocking in case # the client wasn't finilzed properly, e.g. unhandled # exception outside of ``with``. As a result, we cannot # guarantee data integrity unless either ``close`` or # ``__exit__`` was closed. self.thread = threading.Thread(target=self) self.thread.daemon = True def __repr__(self): return "Router({0})".format(self.connection) def __call__(self): try: while True: rlist, _wlist, _xlist = select.select( [self.connection, self.r_terminator], [], []) if not rlist: continue elif self.r_terminator in rlist: break packet = self.connection.recv() if packet.op == Op.WATCH_EVENT: event = Event(*packet.payload.split(NUL)[:-1]) for monitor in self.monitors[event.token]: monitor.events.put(event) else: rvar = self.rvars.pop(packet.rq_id, None) if rvar is None: raise UnexpectedPacket(packet) else: rvar.set(packet) finally: self.connection.close() self.r_terminator.close() self.w_terminator.close() @property def is_connected(self): """Checks if the underlying connection is active.""" return self.connection.is_connected def subscribe(self, token, monitor): """Subscribes a ``monitor`` from events with a given ``token``.""" self.monitors[token].append(monitor) def unsubscribe(self, token, monitor): """Unsubscribes a ``monitor`` to events with a given ``token``.""" self.monitors[token].remove(monitor) def send(self, packet): """Sends a packet to XenStore. :returns RVar: a reference to the XenStore response. """ with self.send_lock: # The order here matters. XenStore might reply to the packet # *before* the ``rvar`` is registered. self.rvars[packet.rq_id] = rvar = RVar() self.connection.send(packet) return rvar def start(self): """Starts the router thread. Does nothing if the router is already started. """ # Connection is deliberately done in the calling thread so that # ``ConnectionError`` could be handled. See issue #8 on GitHub # for details. self.connection.connect() if not self.thread.is_alive(): self.thread.start() while not self.is_connected: if not self.thread.is_alive(): raise ConnectionError("router died") def terminate(self): """Terminates the router. After termination the router can no longer send or receive packets. Does nothing if the router was already terminated. """ if self.is_connected: self.w_terminator.sendall(NUL) if self.thread.is_alive(): self.thread.join() class RVar(object): """A thread-safe shared mutable reference. .. versionadded:: 0.4.0 """ __slots__ = ["condition", "target"] def __init__(self): self.condition = threading.Condition() self.target = None def __repr__(self): return "RVar({0})".format(self.target) def get(self): """Blocks until the value is :meth:`set`` and then returns the value. .. note:: The returned value is guaranteed never to be ``None``. """ with self.condition: while self.target is None: _condition_wait(self.condition) return self.target def set(self, target): """Sets the value, which effectively unblocks all :meth:`get` calls.""" with self.condition: self.target = target self.condition.notify_all() class Client(object): """XenStore client. :param str unix_socket_path: path to XenStore Unix domain socket. :param str xen_bus_path: path to XenBus device. If ``unix_socket_path`` is given or :class:`~pyxs.client.Client` was created with no arguments, XenStore is accessed via :class:`~pyxs.connection.UnixSocketConnection`; otherwise, :class:`~pyxs.connection.XenBusConnection` is used. Each client has a :class:`~pyxs.client.Router` thread running in the background. The goal of the router is to multiplex requests from different transaction through a single XenStore connection. .. versionchanged:: 0.4.0 The constructor no longer accepts ``connection`` argument. If you wan't to force the use of a specific connection class, wrap it in a :class:`~pyxs.client.Router`:: from pyxs import Router, Client from pyxs.connection import XenBusConnection router = Router(XenBusConnection()) with Client(router=router) as c: do_something(c) .. warning:: Always finalize the client either explicitly by calling :meth:`~pyxs.client.Client.close` or implicitly via a context manager to prevent data loss. .. seealso:: `Xenstore protocol specification \ `_ for a description of the protocol, implemented by ``Client``. """ #: A flag, which is ``True`` if we're operating on control domain #: and else otherwise. try: SU = open("/proc/xen/capabilities", "rb").read() == b"control_d\n" except (IOError, OSError): SU = False def __init__(self, unix_socket_path=None, xen_bus_path=None, router=None): if router is None: if unix_socket_path or not xen_bus_path: connection = UnixSocketConnection(unix_socket_path) else: connection = XenBusConnection(xen_bus_path) router = Router(connection) self.router = router self.tx_id = 0 def __repr__(self): return "Client({0})".format(self.router.connection) def __copy__(self): return self.__class__(router=self.router) def __enter__(self): self.connect() return self def __exit__(self, *exc_info): self.close() if self.tx_id and not any(exc_info): raise PyXSError("uncommitted transaction") # Private API. # ............ def execute_command(self, op, *args, **kwargs): if not all(map(_re_7bit_ascii.match, args)): raise ValueError(args) kwargs.update(tx_id=self.tx_id, rq_id=next_rq_id()) rvar = self.router.send(Packet(op, b"".join(args), **kwargs)) packet = rvar.get() if packet.op == Op.ERROR: # Erroneous responses are POSIX error code ending with a # ``NUL`` byte. raise error(packet.payload[:-1]) elif packet.op != op or packet.tx_id != self.tx_id: raise UnexpectedPacket(packet) return packet.payload.rstrip(NUL) def ack(self, *args): payload = self.execute_command(*args) if payload != b"OK": raise PyXSError(payload) # Public API. # ........... def connect(self): """Connects to the XenStore daemon. :raises pyxs.exceptions.ConnectionError: if the connection could not be opened. This could happen either because XenStore is not running on the machine or due to the lack of permissions. .. versionadded: 0.4.0 .. warning:: This method is unsafe. Please use client as a context manager to ensure it is properly finalized. """ self.router.start() def close(self): """Finalizes the client. .. warning:: This method is unsafe. Please use client as a context manager to ensure it is properly finalized. """ self.router.terminate() def read(self, path, default=None): """Reads data from a given path. :param bytes path: a path to read from. :param bytes default: default value, to be used if `path` doesn't exist. """ check_path(path) try: return self.execute_command(Op.READ, path + NUL) except PyXSError as e: if e.args[0] == errno.ENOENT and default is not None: return default raise __getitem__ = read def write(self, path, value): """Writes data to a given path. :param bytes value: data to write. :param bytes path: a path to write to. """ check_path(path) self.ack(Op.WRITE, path + NUL, value) __setitem__ = write def mkdir(self, path): """Ensures that a given path exists, by creating it and any missing parents with empty values. If `path` or any parent already exist, its value is left unchanged. :param bytes path: path to directory to create. """ check_path(path) self.ack(Op.MKDIR, path + NUL) def delete(self, path): """Ensures that a given does not exist, by deleting it and all of its children. It is not an error if `path` doesn't exist, but it **is** an error if `path`'s immediate parent does not exist either. :param bytes path: path to directory to remove. """ check_path(path) self.ack(Op.RM, path + NUL) __delitem__ = delete def list(self, path): """Returns a list of names of the immediate children of `path`. :param bytes path: path to list. """ check_path(path) payload = self.execute_command(Op.DIRECTORY, path + NUL) return [] if not payload else payload.split(NUL) def exists(self, path): """Checks if a given `path` exists. :param bytes path: path to check. """ try: self.list(path) except PyXSError as e: if e.args[0] == errno.ENOENT: return False raise else: return True def get_perms(self, path): """Returns a list of permissions for a given `path`, see :exc:`~pyxs.exceptions.InvalidPermission` for details on permission format. :param bytes path: path to get permissions for. """ check_path(path) payload = self.execute_command(Op.GET_PERMS, path + NUL) return payload.split(NUL) def set_perms(self, path, perms): """Sets a access permissions for a given `path`, see :exc:`~pyxs.exceptions.InvalidPermission` for details on permission format. :param bytes path: path to set permissions for. :param list perms: a list of permissions to set. """ check_path(path) check_perms(perms) self.ack(Op.SET_PERMS, path + NUL, *(perm + NUL for perm in perms)) def walk(self, top, topdown=True): """Walk XenStore, yielding 3-tuples ``(path, value, children)`` for each node in the tree, rooted at node `top`. :param bytes top: node to start from. :param bool topdown: see :func:`os.walk` for details. """ children = self.list(top) try: value = self.read(top) except PyXSError: value = b"" # '/' or no read permissions? if topdown: yield top, value, children for child in children: for x in self.walk(posixpath.join(top, child)): yield x if not topdown: yield top, value, children def get_domain_path(self, domid): """Returns the domain's base path, as used for relative requests: e.g. ``b"/local/domain/"``. If a given `domid` doesn't exists the answer is undefined. :param int domid: domain to get base path for. """ return self.execute_command(Op.GET_DOMAIN_PATH, str(domid).encode() + NUL) def is_domain_introduced(self, domid): """Returns ``True`` if ``xenstored`` is in communication with the domain; that is when `INTRODUCE` for the domain has not yet been followed by domain destruction or explicit `RELEASE`; and ``False`` otherwise. :param int domid: domain to check status for. """ payload = self.execute_command(Op.IS_DOMAIN_INTRODUCED, str(domid).encode() + NUL) return {b"T": True, b"F": False}[payload] def introduce_domain(self, domid, mfn, eventchn): """Tells ``xenstored`` to communicate with this domain. :param int domid: a real domain id, (``0`` is forbidden). :param int mfn: address of xenstore page in `domid`. :param int eventchn: an unbound event chanel in `domid`. """ if not domid: raise ValueError("domain 0 cannot be introduced.") self.ack(Op.INTRODUCE, str(domid).encode() + NUL, str(mfn).encode() + NUL, str(eventchn).encode() + NUL) def release_domain(self, domid): """Manually requests ``xenstored`` to disconnect from the domain. :param int domid: domain to disconnect. .. note:: ``xenstored`` will in any case detect domain destruction and disconnect by itself. """ if not self.SU: raise error(errno.EPERM) self.ack(Op.RELEASE, str(domid).encode() + NUL) def resume_domain(self, domid): """Tells ``xenstored`` to clear its shutdown flag for a domain. This ensures that a subsequent shutdown will fire the appropriate watches. :param int domid: domain to resume. """ if not self.SU: raise error(errno.EPERM) self.ack(Op.RESUME, str(domid).encode() + NUL) def set_target(self, domid, target): """Tells ``xenstored`` that a domain is targetting another one, so it should let it tinker with it. This grants domain `domid` full access to paths owned by `target`. Domain `domid` also inherits all permissions granted to `target` on all other paths. :param int domid: domain to set target for. :param int target: target domain (yours truly, Captain). """ if not self.SU: raise error(errno.EPERM) self.ack(Op.SET_TARGET, str(domid).encode() + NUL, str(target).encode() + NUL) def transaction(self): """Starts a new transaction. :returns int: transaction handle. :raises pyxs.exceptions.PyXSError: with :data:`errno.EALREADY` if this client is already in a transaction. .. warning:: Currently ``xenstored`` has a bug that after 2**32 transactions it will allocate id 0 for an actual transaction. """ if self.tx_id: raise error(errno.EALREADY) payload = self.execute_command(Op.TRANSACTION_START, NUL) self.tx_id = int(payload) return self.tx_id def rollback(self): """Rolls back a transaction currently in progress. .. versionchanged: 0.4.0 In previous versions the method gracefully handled attempts to end a transaction when no transaction was running. This is no longer the case. The method will send the corresponding command to XenStore. """ self.ack(Op.TRANSACTION_END, b"F" + NUL) self.tx_id = 0 def commit(self): """Commits a transaction currently in progress. :returns bool: ``False`` if commit failed because of the intervening writes and ``True`` otherwise. In any case transaction is invalidated. The caller is responsible for starting a new transaction, repeating all of the operations a re-committing. .. versionchanged: 0.4.0 In previous versions the method gracefully handled attempts to end a transaction when no transaction was running. This is no longer the case. The method will send the corresponding command to XenStore. """ try: self.ack(Op.TRANSACTION_END, b"T" + NUL) except PyXSError as e: if e.args[0] == errno.EAGAIN: return False raise else: return True finally: self.tx_id = 0 def monitor(self): """Returns a new :class:`Monitor` instance, which is currently the only way of doing PUBSUB. The monitor shares the router with its parent client. Thus closing the client invalidates the monitor. Closing the monitor, on the other hand, had no effect on the router state. .. note:: Using :meth:`monitor` over :class:`~pyxs.connection.XenBusConnection` is currently unsupported, because XenBus does not obey XenStore protocol specification. See `xen-devel`_ discussion for details. .. _xen-devel: \ http://lists.xen.org/archives/html/xen-devel/2016-02/msg03737 """ if isinstance(self.router.connection, XenBusConnection): raise PyXSError("using ``Monitor`` over XenBus is not supported", UserWarning) return Monitor(copy.copy(self)) class Monitor(object): """Monitor implements minimal PUBSUB functionality on top of XenStore. >>> with Client() as c: ... m = c.monitor(): ... m.watch("foo/bar") ... print(next(c.wait())) Event(...) :param Client client: a reference to the parent client. .. note:: When used as a context manager the monitor will try to unwatch all watched paths. """ def __init__(self, client): self.client = client self.events = queue.Queue() self.unwatch_queue = set() def __enter__(self): return self def __exit__(self, *exc_info): self.close() @property def watched(self): """A set of paths currently watched by the monitor.""" return set(wpath for wpath, token in self.unwatch_queue) def close(self): """Finalizes the monitor by unwatching all watched paths.""" for wpath, token in list(self.unwatch_queue): self.unwatch(wpath, token) def watch(self, wpath, token): """Adds a watch. Any alteration to the watched path generates an event. This includes path creation, removal, contents change or permission change. An event can also be triggered spuriously. Changes made in transactions cause an event only if and when committed. :param bytes wpath: path to watch. :param bytes token: watch token, returned in watch notification. """ check_watch_path(wpath) self.client.router.subscribe(token, self) self.client.ack(Op.WATCH, wpath + NUL, token + NUL) self.unwatch_queue.add((wpath, token)) def unwatch(self, wpath, token): """Removes a previously added watch. :param bytes wpath: path to unwatch. :param bytes token: watch token, passed to :meth:`watch`. """ check_watch_path(wpath) self.client.ack(Op.UNWATCH, wpath + NUL, token + NUL) self.client.router.unsubscribe(token, self) self.unwatch_queue.discard((wpath, token)) def wait(self, unwatched=False): """Yields events for all of the watched paths. An event is a ``(path, token)`` pair, where the first element is event path, i.e. the actual path that was modified, and the second -- a token, passed to :meth:`watch`. :param bool unwatched: if ``True`` :meth:`wait` might yield spurious unwatched packets, otherwise these are dropped. Defaults to ``False``. """ while True: with self.events.not_empty: while not self.events._qsize(): _condition_wait(self.events.not_empty) event = wpath, token = self.events.get_nowait() # Check that event path or its parent is watched. while wpath and (wpath, token) not in self.unwatch_queue: wpath = posixpath.dirname(wpath) if wpath or unwatched: yield event python-pyxs_0.4.2~git20190115.97f14313/pyxs/connection.py000066400000000000000000000163641341744634400224060ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pyxs.connection ~~~~~~~~~~~~~~~ This module implements two connection backends for :class:`~pyxs.client.Client`. :copyright: (c) 2011 by Selectel. :copyright: (c) 2016 by pyxs authors and contributors, see AUTHORS for more details. :license: LGPL, see LICENSE for more details. """ from __future__ import absolute_import __all__ = ["UnixSocketConnection", "XenBusConnection"] import errno import os import platform import socket import sys from .exceptions import ConnectionError from ._internal import Packet class PacketConnection(object): """A connection which operates in terms of XenStore packets. Subclasses are expected to define :meth:`create_transport` and set :attr:`path` attribute. """ path = transport = None def __repr__(self): return "{0}({1!r})".format(self.__class__.__name__, self.path) @property def is_connected(self): return self.transport is not None def fileno(self): return self.transport.fileno() def connect(self): """Connects to XenStore.""" if self.is_connected: return self.transport = self.create_transport() def close(self, silent=True): """Disconnects from XenStore. :param bool silent: if ``True`` (default), any errors raised while closing the file descriptor are suppressed. """ if not self.is_connected: return try: self.transport.close() except OSError as e: if not silent: raise ConnectionError(e.args) finally: self.transport = None def send(self, packet): """Sends a given packet to XenStore. :param pyxs._internal.Packet packet: a packet to send, is expected to be validated, since no checks are done at that point. """ if not self.is_connected: raise ConnectionError("not connected") header = Packet._struct.pack(packet.op, packet.rq_id, packet.tx_id, packet.size) try: self.transport.send(header) self.transport.send(packet.payload) except OSError as e: if e.args[0] in [errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE]: self.close() raise ConnectionError("error while writing to {0!r}: {1}" .format(self.path, e.args)) def recv(self): """Receives a packet from XenStore.""" if not self.is_connected: raise ConnectionError("not connected") try: header = self.transport.recv(Packet._struct.size) except OSError as e: if e.args[0] in [errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE]: self.close() raise ConnectionError("error while reading from {0!r}: {1}" .format(self.path, e.args)) else: op, rq_id, tx_id, size = Packet._struct.unpack(header) # On Linux XenBus blocks on ``os.read(fd, 0)``, so we have # to check the size before reading. See # http://lists.xen.org/archives/html/xen-devel/2016-03/msg00229 # for discussion. payload = b"" if not size else self.transport.recv(size) return Packet(op, payload, rq_id, tx_id) def _get_unix_socket_path(): """Returns default path to ``xenstored`` Unix domain socket.""" return (os.getenv("XENSTORED_PATH") or os.path.join(os.getenv("XENSTORED_RUNDIR", "/var/run/xenstored"), "socket")) class _UnixSocketTransport(object): def __init__(self, path): try: self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(path) except socket.error as e: raise ConnectionError("error connecting to {0!r}: {1}" .format(path, e.args)) def fileno(self): return self.sock.fileno() if sys.version_info[:2] < (2, 7): def recv(self, size): chunks = [] while size: chunks.append(self.sock.recv(size)) size -= len(chunks[-1]) return b"".join(chunks) else: def recv(self, size): view = memoryview(bytearray(size)) while size: received = self.sock.recv_into(view[-size:]) if not received: raise socket.error(errno.ECONNRESET) size -= received return view.tobytes() def send(self, data): self.sock.sendall(data) def close(self): self.sock.shutdown(socket.SHUT_RDWR) self.sock.close() class UnixSocketConnection(PacketConnection): """XenStore connection through Unix domain socket. :param str path: path to XenStore unix domain socket, if not provided explicitly is restored from process environment -- similar to what ``libxs`` does. """ def __init__(self, path=None): self.path = path or _get_unix_socket_path() def create_transport(self): return _UnixSocketTransport(self.path) def _get_xenbus_path(): """Returns OS-specific path to XenBus.""" system = platform.system() if system == "Linux" and not os.access("/dev/xen/xenbus", os.R_OK): # See commit 9c89dc95201ffed5fead17b35754bf9440fdbdc0 in # http://xenbits.xen.org/gitweb/?p=xen.git for details on the # ``os.access`` check. return "/proc/xen/xenbus" elif system == "NetBSD": return "/kern/xen/xenbus" else: return "/dev/xen/xenbus" class _XenBusTransport(object): def __init__(self, path): try: self.fd = os.open(path, os.O_RDWR) except OSError as e: raise ConnectionError("error while opening {0!r}: {1}" .format(path, e.args)) def fileno(self): return self.fd def recv(self, size): chunks = [] while size: read = os.read(self.fd, size) if not read: raise OSError(errno.ECONNRESET) chunks.append(read) size -= len(read) return b"".join(chunks) if sys.version_info[:2] < (2, 7): def send(self, data): size = len(data) while size: size -= os.write(self.fd, data[-size:]) else: def send(self, data): size = len(data) view = memoryview(data) while size: size -= os.write(self.fd, view[-size:]) def close(self): return os.close(self.fd) class XenBusConnection(PacketConnection): """XenStore connection through XenBus. :param str path: path to XenBus. A predefined OS-specific constant is used, if a value isn't provided explicitly. """ def __init__(self, path=None): self.path = path or _get_xenbus_path() def create_transport(self): return _XenBusTransport(self.path) python-pyxs_0.4.2~git20190115.97f14313/pyxs/exceptions.py000066400000000000000000000042641341744634400224240ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pyxs.exceptions ~~~~~~~~~~~~~~~ This module implements a number of Python exceptions used by :mod:`pyxs` classes. :copyright: (c) 2011 by Selectel, see AUTHORS for more details. :license: LGPL, see LICENSE for more details. """ __all__ = [ "InvalidOperation", "InvalidPayload", "InvalidPath", "InvalidPermission", "ConnectionError", "UnexpectedPacket" ] class PyXSError(Exception): """Base class for all :mod:`pyxs` exceptions.""" class InvalidOperation(ValueError, PyXSError): """Exception raised when :class:`~pyxs._internal.Packet` is passed an operation, which isn't listed in :data:`~pyxs._internal.Op`. :param int operation: invalid operation value. """ class InvalidPayload(ValueError, PyXSError): """Exception raised when :class:`~pyxs.Packet` is initialized with payload, which exceeds 4096 bytes restriction or contains a trailing ``NULL``. :param bytes operation: invalid payload value. """ class InvalidPath(ValueError, PyXSError): """Exception raised when a path proccessed by a command doesn't match the following constraints: * its length should not exceed 3072 or 2048 for absolute and relative path respectively. * it should only consist of ASCII alphanumerics and the four punctuation characters ``-/_@`` -- `hyphen`, `slash`, `underscore` and `atsign`. * it shouldn't have a trailing ``/``, except for the root path. :param bytes path: invalid path value. """ class InvalidPermission(ValueError, PyXSError): """Exception raised for permission which don't match the following format:: w write only r read only b both read and write n no access :param bytes perm: invalid permission value. """ class ConnectionError(PyXSError): """Exception raised for failures during socket operations.""" class UnexpectedPacket(ConnectionError): """Exception raised when received packet header doesn't match the header of the packet sent, for example if outgoing packet has ``op = Op.READ`` the incoming packet is expected to have ``op = Op.READ`` as well. """ python-pyxs_0.4.2~git20190115.97f14313/pyxs/helpers.py000066400000000000000000000053321341744634400217020ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pyxs.helpers ~~~~~~~~~~~~ Implements various helpers. :copyright: (c) 2011 by Selectel, see AUTHORS for more details. :license: LGPL, see LICENSE for more details. """ from __future__ import absolute_import __all__ = ["check_path", "check_watch_path", "check_perms", "error"] import errno import re import os import posixpath from .exceptions import InvalidPath, InvalidPermission, PyXSError #: A reverse mapping for :data:`errno.errorcode`. _codeerror = dict((message, code) for code, message in errno.errorcode.items()) def error(smth): """Returns a :class:`~pyxs.exceptions.PyXSError` matching a given errno or error name. >>> error(22) pyxs.exceptions.PyXSError: (22, 'Invalid argument') >>> error(b"EINVAL") pyxs.exceptions.PyXSError: (22, 'Invalid argument') """ if isinstance(smth, bytes): smth = _codeerror.get(smth.decode(), 0) return PyXSError(smth, os.strerror(smth)) _re_path = re.compile(br"^[a-zA-Z0-9-/_@]+\x00?$") def check_path(path): """Checks if a given path is valid, see :exc:`~pyxs.exceptions.InvalidPath` for details. :param bytes path: path to check. :raises pyxs.exceptions.InvalidPath: when path fails to validate. """ # Paths longer than 3072 bytes are forbidden; clients specifying # relative paths should keep them to within 2048 bytes. max_len = 3072 if posixpath.abspath(path) else 2048 if not _re_path.match(path) or len(path) > max_len: raise InvalidPath(path) # A path is not allowed to have a trailing /, except for the # root path and shouldn't have double //'s. if (len(path) > 1 and path.endswith(b"/")) or b"//" in path: raise InvalidPath(path) return path _re_watch_path = re.compile(br"^@(?:introduceDomain|releaseDomain)\x00?$") def check_watch_path(wpath): """Checks if a given watch path is valid -- it should either be a valid path or a special, starting with ``@`` character. :param bytes wpath: watch path to check. :raises pyxs.exceptions.InvalidPath: when path fails to validate. """ if wpath.startswith(b"@") and not _re_watch_path.match(wpath): raise InvalidPath(wpath) else: check_path(wpath) return wpath _re_perms = re.compile(br"^[wrbn]\d+$") def check_perms(perms): """Checks if a given list of permision follows the format described in :exc:`~pyxs.exceptions.InvalidPermissions`. :param list perms: permissions to check. :raises pyxs.exceptions.InvalidPermissions: when any of the permissions fails to validate. """ for perm in perms: if not _re_perms.match(perm): raise InvalidPermission(perm) return perms python-pyxs_0.4.2~git20190115.97f14313/setup.cfg000066400000000000000000000000261341744634400204770ustar00rootroot00000000000000[aliases] test=pytest python-pyxs_0.4.2~git20190115.97f14313/setup.py000077500000000000000000000022571341744634400204030ustar00rootroot00000000000000#! /usr/bin/env python # -*- coding: utf-8 -*- import os from setuptools import setup here = os.path.abspath(os.path.dirname(__file__)) DESCRIPTION = "Pure Python bindings to XenStore." try: LONG_DESCRIPTION = open(os.path.join(here, "README")).read() except IOError: LONG_DESCRIPTION = "" CLASSIFIERS = ( "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", ) setup(name="pyxs", version="0.4.2-dev", packages=["pyxs"], setup_requires=["pytest-runner"], tests_require=["pytest"], platforms=["any"], author="Sergei Lebedev", author_email="superbobry@gmail.com", description=DESCRIPTION, long_description=LONG_DESCRIPTION, classifiers=CLASSIFIERS, keywords=["xen", "xenstore", "virtualization"], url="https://github.com/selectel/pyxs") python-pyxs_0.4.2~git20190115.97f14313/tests/000077500000000000000000000000001341744634400200225ustar00rootroot00000000000000python-pyxs_0.4.2~git20190115.97f14313/tests/__init__.py000066400000000000000000000003661341744634400221400ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import pytest from pyxs import Client # XXX we don't always need 'SU'. _virtualized = not os.path.exists('/dev/xen') or not Client.SU virtualized = pytest.mark.skipif(_virtualized, reason="not virtualized") python-pyxs_0.4.2~git20190115.97f14313/tests/test__compat.py000066400000000000000000000033611341744634400230600ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import import pytest from pyxs._compat import xs, Error from . import virtualized @virtualized def setup_function(f): try: handle = xs() except Error: return # Failed to connect. try: handle.rm(0, b"/foo") except Error: pass finally: handle.close() @pytest.yield_fixture def handle(): handle = xs() try: yield handle finally: handle.close() def test_api_completeness(): try: from xen.lowlevel.xs import xs as cxs except ImportError: pytest.skip("xen.lowlevel.xs not available") def public_methods(target): return set(attr for attr in dir(target) if not attr.startswith("_")) assert public_methods(cxs).issubset(public_methods(xs)) @virtualized def test_ls(handle): assert handle.ls(0, b"/missing/path") is None @virtualized def test_transaction_start(handle): assert isinstance(handle.transaction_start(), bytes) @virtualized def test_transaction_end_rollback(handle): assert handle.ls(0, b"/foo") is None tx_id = handle.transaction_start() handle.write(tx_id, b"/foo/bar", b"boo") handle.transaction_end(tx_id, abort=1) assert handle.ls(0, b"/foo") is None @virtualized def test_transaction_end_commit(handle): assert handle.ls(0, b"/foo") is None tx_id = handle.transaction_start() handle.write(tx_id, b"/foo/bar", b"boo") handle.transaction_end(tx_id, abort=0) assert handle.ls(0, b"/foo") == [b"bar"] @virtualized def test_watch_unwatch(handle): token = object() handle.watch(b"/foo/bar", token) assert handle.read_watch() == (b"/foo/bar", token) handle.unwatch(b"/foo/bar", token) python-pyxs_0.4.2~git20190115.97f14313/tests/test__init__.py000066400000000000000000000003201341744634400230260ustar00rootroot00000000000000from __future__ import absolute_import import pyxs from . import virtualized @virtualized def test_monitor(): with pyxs.monitor() as m: # FAILS! m.watch(b"@introduceDomain", b"token") python-pyxs_0.4.2~git20190115.97f14313/tests/test__internal.py000066400000000000000000000005611341744634400234100ustar00rootroot00000000000000import pytest from pyxs.exceptions import InvalidOperation, InvalidPayload from pyxs._internal import Op, Packet def test_packet(): # a) invalid operation. with pytest.raises(InvalidOperation): Packet(-1, b"", 0) # b) invalid payload -- maximum size exceeded. with pytest.raises(InvalidPayload): Packet(Op.DEBUG, b"hello" * 4096, 0) python-pyxs_0.4.2~git20190115.97f14313/tests/test_client.py000066400000000000000000000264761341744634400227300ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import import errno import sys from itertools import islice from threading import Timer, Thread, current_thread import pytest from pyxs.client import RVar, Router, Client from pyxs.connection import UnixSocketConnection, XenBusConnection from pyxs.exceptions import InvalidPath, InvalidPermission, \ UnexpectedPacket, PyXSError from pyxs._internal import NUL, Op, Event, Packet from . import virtualized def setup_function(f): try: with Client() as c: c.delete(b"/foo") except PyXSError: pass def test_init(): # a) UnixSocketConnection c = Client() assert c.tx_id == 0 assert isinstance(c.router.connection, UnixSocketConnection) assert not c.router.thread.is_alive() c = Client(unix_socket_path="/var/run/xenstored/socket") assert isinstance(c.router.connection, UnixSocketConnection) assert not c.router.thread.is_alive() # b) XenBusConnection c = Client(xen_bus_path="/dev/xen/xenbus") assert isinstance(c.router.connection, XenBusConnection) assert not c.router.thread.is_alive() @virtualized def test_context_manager(): # a) no transaction is running c = Client() assert not c.router.thread.is_alive() with c: assert c.router.thread.is_alive() assert not c.router.thread.is_alive() @virtualized def test_execute_command_invalid_characters(): with Client() as c: c.execute_command(Op.WRITE, b"/foo/bar" + NUL, b"baz") with pytest.raises(ValueError): c.execute_command(Op.DEBUG, b"\x07foo" + NUL) @virtualized def test_execute_command_error(): with Client() as c: with pytest.raises(PyXSError): c.execute_command(Op.READ, b"/unexisting/path" + NUL) with pytest.raises(PyXSError): c.execute_command(-42, b"/unexisting/path" + NUL) def monkeypatch_router(client, response_packet): class FakeRouter: def send(self, packet): rvar = RVar() rvar.set(response_packet) return rvar def terminate(self): pass client.close() client.router = FakeRouter() @virtualized def test_execute_command_invalid_op(): with Client() as c: monkeypatch_router(c, Packet(Op.DEBUG, b"/local" + NUL, rq_id=0)) with pytest.raises(UnexpectedPacket): c.execute_command(Op.READ, b"/local" + NUL) @virtualized def test_execute_command_invalid_tx_id(): with Client() as c: monkeypatch_router(c, Packet(Op.READ, b"/local" + NUL, rq_id=0, tx_id=42)) with pytest.raises(UnexpectedPacket): c.execute_command(Op.READ, b"/local" + NUL) @virtualized def test_close_idempotent(): c = Client() c.connect() c.close() c.close() @pytest.mark.parametrize("op", [ "read", "mkdir", "delete", "list", "exists", "get_perms" ]) def test_check_path(op): with pytest.raises(InvalidPath): getattr(Client(), op)(b"INVALID%PATH!") @pytest.yield_fixture(params=[UnixSocketConnection, XenBusConnection]) def client(request): with Client(router=Router(request.param())) as c: yield c @virtualized def test_read(client): # a) non-existant path. try: client.read(b"/foo/bar") except PyXSError as e: assert e.args[0] == errno.ENOENT # b) using a default. client.read(b"/foo/bar", b"baz") == b"baz" # c) OK-case (`/local` is allways in place). assert client.read(b"/local") == b"" assert client[b"/local"] == b"" # d) No read perms (should be ran in DomU)? @virtualized def test_write(client): client.write(b"/foo/bar", b"baz") assert client.read(b"/foo/bar") == b"baz" client[b"/foo/bar"] = b"boo" assert client[b"/foo/bar"] == b"boo" # b) No write perms (should be ran in DomU)? def test_write_invalid(): with pytest.raises(InvalidPath): Client().write(b"INVALID%PATH!", b"baz") @virtualized def test_mkdir(client): client.mkdir(b"/foo/bar") assert client.list(b"/foo") == [b"bar"] assert client.read(b"/foo/bar") == b"" @virtualized def test_delete(client): client.mkdir(b"/foo/bar") client.delete(b"/foo/bar") try: client.read(b"/foo/bar") except PyXSError as e: assert e.args[0] == errno.ENOENT assert client.read(b"/foo") == b"" @virtualized def test_list(client): client.mkdir(b"/foo/bar") # a) OK-case. assert client.list(b"/foo") == [b"bar"] assert client.list(b"/foo/bar") == [] # b) directory doesn't exist. try: client.list(b"/path/to/something") except PyXSError as e: assert e.args[0] == errno.ENOENT # c) No list perms (should be ran in DomU)? @virtualized def test_exists(client): # a) Path exists. client.mkdir(b"/foo/bar") assert client.exists(b"/foo/bar") # b) Path does not exist. client.delete(b"/foo/bar") assert not client.exists(b"/foo/bar") # c) No list perms (should be ran in DomU)? @virtualized def test_perms(client): client.delete(b"/foo") client.mkdir(b"/foo/bar") # a) checking default perms -- full access. assert client.get_perms(b"/foo/bar") == [b"n0"] # b) setting new perms, and making sure it worked. client.set_perms(b"/foo/bar", [b"b0"]) assert client.get_perms(b"/foo/bar") == [b"b0"] # c) conflicting perms -- XenStore doesn't care. client.set_perms(b"/foo/bar", [b"b0", b"n0", b"r0"]) assert client.get_perms(b"/foo/bar") == [b"b0", b"n0", b"r0"] # d) invalid permission format. with pytest.raises(InvalidPermission): client.set_perms(b"/foo/bar", [b"x0"]) def test_set_perms_invalid(): with pytest.raises(InvalidPath): Client().set_perms(b"INVALID%PATH!", []) with pytest.raises(InvalidPermission): Client().set_perms(b"/foo/bar", [b"z"]) @virtualized def test_get_domain_path(client): # Note, that XenStore doesn't care if a domain exists, but # according to the spec we shouldn't really count on a *valid* # reply in that case. assert client.get_domain_path(0) == b"/local/domain/0" assert client.get_domain_path(999) == b"/local/domain/999" @virtualized def test_is_domain_introduced(client): for domid in map(int, client.list(b"/local/domain")): assert client.is_domain_introduced(domid) assert not client.is_domain_introduced(999) @virtualized def test_transaction(client): assert client.tx_id == 0 client.transaction() assert client.tx_id != 0 @virtualized def test_nested_transaction(client): client.transaction() with pytest.raises(PyXSError): client.transaction() @virtualized def test_transaction_rollback(client): assert not client.exists(b"/foo/bar") client.transaction() client[b"/foo/bar"] = b"boo" client.rollback() assert client.tx_id == 0 assert not client.exists(b"/foo/bar") @virtualized def test_transaction_commit_ok(client): assert not client.exists(b"/foo/bar") client.transaction() client[b"/foo/bar"] = b"boo" assert client.commit() assert client.tx_id == 0 assert client[b"/foo/bar"] == b"boo" @virtualized def test_transaction_commit_retry(client): def writer(): with Client() as other: other[b"/foo/bar"] = b"unexpected write" assert not client.exists(b"/foo/bar") client.transaction() writer() client[b"/foo/bar"] = b"boo" assert not client.commit() assert client.tx_id == 0 @virtualized def test_transaction_exception(): try: with Client() as c: assert not c.exists(b"/foo/bar") c.transaction() c[b"/foo/bar"] = b"boo" raise ValueError except ValueError: pass with Client() as c: assert not c.exists(b"/foo/bar") @virtualized def test_uncommitted_transaction(): with pytest.raises(PyXSError): with Client() as c: c.transaction() def xfail_if_xenbus(client): if isinstance(client.router.connection, XenBusConnection): # http://lists.xen.org/archives/html/xen-users/2016-02/msg00159.html pytest.xfail("unsupported connection") @virtualized def test_monitor(client): xfail_if_xenbus(client) client.write(b"/foo/bar", b"baz") m = client.monitor() m.watch(b"/foo/bar", b"boo") waiter = m.wait() # a) we receive the first event immediately, so `next` doesn't # block. assert next(waiter) == (b"/foo/bar", b"boo") # b) before the second call we have to make sure someone # will change the path being watched. Thread(target=lambda: client.write(b"/foo/bar", b"baz")).start() assert next(waiter) == (b"/foo/bar", b"boo") # c) changing a children of the watched path triggers watch # event as well. Thread(target=lambda: client.write(b"/foo/bar/baz", b"???")).start() assert next(waiter) == (b"/foo/bar/baz", b"boo") @pytest.mark.parametrize("op", ["watch", "unwatch"]) def test_check_watch_path(op): with pytest.raises(InvalidPath): getattr(Client().monitor(), op)(b"INVALID%PATH", b"token") with pytest.raises(InvalidPath): getattr(Client().monitor(), op)(b"@arbitraryPath", b"token") @virtualized def test_monitor_leftover_events(client): xfail_if_xenbus(client) with client.monitor() as m: m.watch(b"/foo/bar", b"boo") def writer(): for i in range(128): client[b"/foo/bar"] = str(i).encode() t = Timer(.25, writer) t.start() m.unwatch(b"/foo/bar", b"boo") assert not m.events.empty() t.join() @virtualized def test_monitor_different_tokens(client): xfail_if_xenbus(client) with client.monitor() as m: m.watch(b"/foo/bar", b"boo") m.watch(b"/foo/bar", b"baz") def writer(): client[b"/foo/bar"] = str(i).encode() t = Timer(.25, lambda: client.write(b"/foo/bar", "???")) t.start() t.join() events = list(islice(m.wait(), 2)) assert len(events) == 2 assert set(token for wpath, token in events) == set([b"boo", b"baz"]) class Latch(object): def __init__(self, initial): self.value = initial def ready(self): self.value -= 1 while self.value: pass # Spin. @virtualized def test_multiple_monitors(client): xfail_if_xenbus(client) n_events = 5 events = {} latch = Latch(2 + 1) def monitor_and_check(token): with client.monitor() as m: m.watch(b"/foo/bar", token) latch.ready() events[current_thread().ident] = list(islice(m.wait(), n_events)) t1 = Thread(target=monitor_and_check, args=(b"boo", )) t1.start() t2 = Thread(target=monitor_and_check, args=(b"baz", )) t2.start() latch.ready() for i in range(n_events): client[b"/foo/bar"] = str(i).encode() t1.join() t2.join() assert len(events) == 2 events1 = events[t1.ident] events2 = events[t2.ident] assert len(events1) == len(events2) == n_events assert set(events1) == set([Event(b"/foo/bar", b"boo")]) assert set(events2) == set([Event(b"/foo/bar", b"baz")]) @virtualized def test_header_decode_error(client): # The following packet's header cannot be decoded to UTF-8, but # we still need to handle it somehow. p = Packet(Op.WRITE, b"/foo", rq_id=0, tx_id=128) client.router.send(p) python-pyxs_0.4.2~git20190115.97f14313/tests/test_connection.py000066400000000000000000000006501341744634400235730ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import import pytest from pyxs.connection import _XenBusTransport, _UnixSocketTransport from pyxs.exceptions import ConnectionError @pytest.mark.parametrize("_transport", [ _XenBusTransport, _UnixSocketTransport ]) def test_transport_init_failed(tmpdir, _transport): with pytest.raises(ConnectionError): _transport(str(tmpdir.join("unexisting"))) python-pyxs_0.4.2~git20190115.97f14313/tests/test_helpers.py000066400000000000000000000035561341744634400231060ustar00rootroot00000000000000import string import pytest from pyxs.exceptions import InvalidPath, InvalidPermission from pyxs.helpers import check_path, check_watch_path, check_perms def test_check_path(): # a) max length is bounded by 3072 for absolute path and 2048 for # relative ones. with pytest.raises(InvalidPath): check_path(b"/foo/bar" * 3072) with pytest.raises(InvalidPath): check_path(b"foo/bar" * 2048) # b) ASCII alphanumerics and -/_@ only! for char in string.punctuation: if char in "-/_@": continue with pytest.raises(InvalidPath): check_path(b"/foo" + char.encode()) # c) no trailing / -- except for root path. with pytest.raises(InvalidPath): check_path(b"/foo/") # d) no //'s!. with pytest.raises(InvalidPath): check_path(b"/foo//bar") # e) OK-case. check_path(b"/") def test_check_watch_path(): # a) ordinary path should be checked with `check_path()` with pytest.raises(InvalidPath): check_watch_path(b"/foo/") with pytest.raises(InvalidPath): check_watch_path(b"/fo\x07o") with pytest.raises(InvalidPath): check_watch_path(b"/$/foo") # b) special path options are limited to `@introduceDomain` and # `@releaseDomain`. with pytest.raises(InvalidPath): check_watch_path(b"@foo") # c) OK-case. check_watch_path(b"@introduceDomain") check_watch_path(b"@releaseDomain") def test_check_perms(): # A valid permission has a form `[wrbn]:digits:`. with pytest.raises(InvalidPermission): check_perms([b"foo"]) with pytest.raises(InvalidPermission): check_perms([b"f20"]) with pytest.raises(InvalidPermission): check_perms([b"r-20"]) # OK-case check_perms(b"w0 r0 b0 n0".split()) check_perms([b"w999999"]) # valid, even though it overflows int32. python-pyxs_0.4.2~git20190115.97f14313/tox.ini000066400000000000000000000001241341744634400201700ustar00rootroot00000000000000[tox] envlist = py26,py27,py34,py35,pypy [testenv] commands = python setup.py test