pax_global_header00006660000000000000000000000064136672330470014525gustar00rootroot0000000000000052 comment=3d1e378795ffed0d61b730e72bdf16f5cc38476c pypuppetdb-2.2.0/000077500000000000000000000000001366723304700137225ustar00rootroot00000000000000pypuppetdb-2.2.0/.bandit000066400000000000000000000000301366723304700151550ustar00rootroot00000000000000[bandit] exclude: /venv pypuppetdb-2.2.0/.coveragerc000066400000000000000000000000541366723304700160420ustar00rootroot00000000000000[report] exclude_lines = pragma: notest pypuppetdb-2.2.0/.gitignore000066400000000000000000000006441366723304700157160ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Pytest .cache __pycache__ # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml htmlcov # Translations *.mo # Virtualenv / pyenv .python-version .venv # Sphinx docs/_build # Mr Developer .mr.developer.cfg .project .pydevproject # OS X .DS_* pypuppetdb-2.2.0/.travis.yml000066400000000000000000000021331366723304700160320ustar00rootroot00000000000000dist: xenial language: python python: - 3.6 - 3.7 - &latest_py3 3.8 jobs: fast_finish: true include: - python: *latest_py3 env: LANG=C - stage: deploy (to PyPI for tagged commits) if: tag IS present python: *latest_py3 install: skip script: skip after_success: true before_deploy: - python setup.py sdist bdist_wheel deploy: provider: pypi user: voxpupuliorg distributions: sdist bdist_wheel skip_cleanup: true skip_upload_docs: true password: secure: "bWWCYcDfGPNbg1YjCHqdLDZjpDwiU7i7VsobHC+iTP+Kur7rt7gfHY4daGp5K8Xc/Er4fIJt5wcxpvdnivxce+W1lpVvGOOnYvHxl06Jd41Nj6wfYfUn058G0e5jyT8hfc/NllJKq12gqrOKKFO9m1vLelgscVlaO7S3XyPVvBI=" on: tags: true all_branches: true cache: pip install: - pip install -r requirements-test.txt script: - py.test --pep8 --mypy --strict - bandit -r pypuppetdb - bandit -r tests -s B101 after_success: - coveralls notifications: email: false irc: on_success: always on_failure: always channels: - "chat.freenode.org#voxpupuli-notifications" pypuppetdb-2.2.0/CHANGELOG.rst000066400000000000000000000225401366723304700157460ustar00rootroot00000000000000######### Changelog ######### 2.2.0 ===== * Loosen requirements and drop six 2.1.0 ===== * Bugfix: Fixed `metric()` function to query the new v2 endpoint based on Jolokia (https://jolokia.org/reference/html/protocol.html) * Added a new parameter `metric_api_version` to the `BaseAPI()` constructor that allows changing the version of the `metric` API being queried. Valid values are `'v1'` for PuppetDB <= 6.9.0, `'v2'` for PuppetDB >= 6.9.1 or `None` which defaults to `'v2'`. * Added a mew parameter `version` to the `metric()` function that allows overriding the version of the metric API being queried for that individual call. If nothing is specified it will default to the `self.metric_api_version` of the class, else it expects a value of `'v1'` or `'v2'` same as the `metric_api_version` class parameter. * Added new `payload` parameter to `_query()` to allow users to send arbitrary payloads with their queries (useful for debugging). 2.0.0 ===== * Dropping old python 2.7/3.5 and ensuring 3 latest versions are supported * Adding mypy + cleanup + further removal of python2 code * Bugfix: Httpretty is not used outside tests and breaks install on newer python versions for some systems 1.2.0 ===== * Add option to get nodes without using event-counts * define the project status as stable * bundle requirements-test.txt in python package 1.1.0 ===== * deduplicate dependencylist * QueryBuilder.py: Use native data structures for internal representation * Add support for the Command API, /pdb/cmd/v1. Added _cmd alongside _query to minimise changes to original code 1.0.0 ===== * Bump dependencies * QueryBuilder: Added support for FromOperator, arrays and FromOperator * New endpoint: status * POST query in request body * Simplify JSON encoding for POST * Upload and publish is built in to setuptools 0.3.3 ===== * Add support for authentication with tokens * Fix bug with parsing results from inventory endpoint 0.3.2 ===== * Fixed noop puppet runs reporting unchanged instead of noop. * Fixed unreported nodes shown as 'noop' in puppetdb > 4.1.0. * Add Inventory API endpoint for PuppetDB 4.2.0. * Support for producer field on catalogs, facts and report types. 0.3.1 ===== * Fixed a datetime related bug in `pypuppetdb.api.nodes()` that caused all returned nodes to be an unreported status 0.3.0 ===== * New QueryBuilder module allows users to build PuppetDB queries in an Object-Oriented fashion. * Adding support for new fields provided in PuppetDB 4.1.0. 0.2.3 ===== * Removed deprecation of `pypuppetdb.types.Report.events()`. Expanded resource events data timestamps are not parseable. * Escaping additional path parameters passed to _url() with urllib.quote 0.2.2 ===== * Fixed URL Encoding found when querying the specific value of a macaddress fact. * Adding support for PuppetDB 4.0.0 information. Namely Adding a catalog_uuid attribute to the Catalog type object. Adding code_id, catalog_uuid and cached_catalog_status attributes to the Report type object. * Removing unneeded sudo option from .travis.yml, this gave unnecessary warning in the test environment. * Updating the files under docs/ so https://pypuppetdb.readthedocs.org/en/latest/ can be updated * Deprecating `pypuppetdb.types.Report.events()` in favour of the new events list variable. * Renaming test-requirements.txt to requirements.txt 0.2.1 ===== * Adding a version comparison utility function using examples provided in http://stackoverflow.com/questions/1714027/version-number-comparison * Adding a new variable latest_report_hash to the Node object. Default None but is given a real value from the field of the same name in the Nodes endpoint available in PuppetDB 3.2 or higher. * Allowing support for 'GET' AND 'POST' requests in the api _query() function. This will allow clients to send requests to the PuppetDB that are too long for a GEt request query string * Adding a node field, code_id, to the Catalog object using the field of the same name from the Catalogs endpoint (currently unused as of PuppetDB 3.2.2) * Adding test cases for new features EXCEPT the GET and POST update. 0.2.0 ===== * Version bump to 0.2.0 * Adding support for v4 of the Query API * Removing v2 and v3 api functions as per changelog * pypuppetdb will no longer support multiple API versions, removing the api_version attribute from pypuppetdb.connect() * All clients must remove the api_version attribute from the connect function, or the starting number, since it is no longer supported * Removing all NotImplemented errors in the function of BaseAPI and filled them with the real code New Features ------------ New endpoints: * ``environments``: ``environments()`` * ``factsets``: ``factsets()`` * ``fact-paths``: ``fact_paths()`` * ``fact-contents``: ``fact_contents()`` * ``edges``: ``edges()`` Changes to Types: * ``pypupperdb.types.Report`` now requires ``api`` to be passed as the second argument, this allows to directly query for any events that occurred in this report object. This functionality was proposed and denied because of backward compatability reasons, since the previous versions are now removed this is no longer a problem. * All ``pypupperdb.types.*`` accept the v4 API information as optional parameters. These parameters are primarily environment related but may include additional information if provided from that endpoint. * Functions appearing inside ``pypuppetdb.types`` that run queries against the PuppetDB now accept and passing additional keyword arguments to the query. * All ``pypuppetdb.BaseAPI`` functions pass any received keyword arguments to the ``pypuppetdb.api.__init__._query()`` function. This allows for easy integration with paging functions and parameters. 0.1.1 ===== * Fix the license in our ``setup.py``. The license shouldn't be longer than 200 characters. We were including the full license tripping up tools like bdist_rpm. 0.1.0 ===== Significant changes have been made in this release. The complete v3 API is now supported except for query pagination. Most changes are backwards compatible except for a change in the SSL configuration. The previous behaviour was buggy and slightly misleading in the names the options took: * ``ssl`` has been renamed to ``ssl_verify`` and now defaults to ``True``. * Automatically use HTTPS if ``ssl_key`` and ``ssl_cert`` are provided. For additional instructions about getting SSL to work see the Quickstart in the documentation. Deprecation ------------ Support for API v2 will be dropped in the 0.2.x release series. New features ------------ The following features are **only** supported for **API v3**. The ``node()`` and ``nodes()`` function have gained the following options: * ``with_status=False`` * ``unreported=2`` When ``with_status`` is set to ``True`` an additional query will be made using the ``events-count`` endpoint scoped to the latest report. This will result in an additional ``events`` and ``status`` keys on the node object. ``status`` will be either of ``changed``, ``unchanged`` or ``failed`` depending on if ``events`` contains ``successes`` or ``failures`` or none. By default ``unreported`` is set to ``2``. This is only in effect when ``with_status`` is set to ``True``. It means that if a node hasn't checked in for two hours it will get a ``status`` of ``unreported`` instead. New endpoints: * ``events-count``: ``events_count()`` * ``aggregate-event-counts``: ``aggregate_event_counts()`` * ``server-time``: ``server_time()`` * ``version``: ``current_version()`` * ``catalog``: ``catalog()`` New types: * ``pypuppetdb.types.Catalog`` * ``pypuppetdb.types.Edge`` Changes to types: * ``pypuppetdb.types.Node`` now has: * ``status`` defaulting to ``None`` * ``events`` defaulting to ``None`` * ``unreported_time`` defaulting to ``None`` 0.0.4 ===== Due to a fairly serious bug 0.0.3 was pulled from PyPi minutes after release. When a bug was fixed to be able to query for all facts we accidentally introduced a different bug that caused the ``facts()`` call on a node to query for all facts because we were resetting the query. * Fix a bug where ``node.facts()`` was causing us to query all facts because the query to scope our request was being reset. 0.0.3 ===== With the introduction of PuppetDB 1.5 a new API version, v3, was also introduced. In that same release the old ``/experimental`` endpoints were removed, meaning that as of PuppetDB 1.5 with the v2 API you can no longer get access to reports or events. In light of this the support for the experimental endpoints has been completely removed from pypuppetdb. As of this release you can only get to reports and/or events through v3 of the API. This release includes preliminary support for the v3 API. Everything that could be done with v2 plus the experimental endpoints is now possible on v3. However, more advanced funtionality has not yet been implemented. That will be the focus of the next release. * Removed dependency on pytz. * Fixed the behaviour of ``facts()`` and ``resources()``. We can now correctly query for all facts or resources. * Fixed an issue with catalog timestampless nodes. * Pass along the ``timeout`` option to ``connect()``. * Added preliminary PuppetDB API v3 support. * Removed support for the experimental endpoints. * The ``connect()`` method defaults to API v3 now. 0.0.2 ===== * Fix a bug in ``setup.py`` preventing successful installation. 0.0.1 ===== Initial release. Implements most of the v2 API. pypuppetdb-2.2.0/LICENSE000066400000000000000000000240411366723304700147300ustar00rootroot00000000000000Apache 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: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and 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 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 [yyyy] [name of copyright owner] 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. pypuppetdb-2.2.0/MANIFEST.in000066400000000000000000000001421366723304700154550ustar00rootroot00000000000000include README.rst include CHANGELOG.rst include LICENSE include requirements.txt include version pypuppetdb-2.2.0/README.rst000066400000000000000000000252671366723304700154250ustar00rootroot00000000000000########## pypuppetdb ########## .. image:: https://api.travis-ci.org/voxpupuli/pypuppetdb.png :target: https://travis-ci.org/voxpupuli/pypuppetdb .. image:: https://coveralls.io/repos/voxpupuli/pypuppetdb/badge.png :target: https://coveralls.io/repos/voxpupuli/pypuppetdb pypuppetdtb is a library to work with PuppetDB's REST API. It is implemented using the `requests` library. .. _requests: http://docs.python-requests.org/en/latest/ **pypuppetdb >= 0.2.0 requires PuppetDB 3.0.0 or later. There is no support for previous versions beyond 0.1.1** **pypuppetdb >= 0.2.2 supports PuppetDB 4.0.0. Backwards compatibility with 3.x is available.** This library is a thin wrapper around the REST API providing some convenience functions and objects to request and hold data from PuppetDB. To use this library you will need: * Python 3.6, 3.7, 3.8 Installation ============ You can install this package from source or from PyPi. .. code-block:: bash $ pip install pypuppetdb .. code-block:: bash $ git clone https://github.com/voxpupuli/pypuppetdb $ python setup.py install If you wish to hack on it clone the repository but after that run: .. code-block:: bash $ pip install -r requirements.txt This will install all the runtime requirements of pypuppetdb and the dependencies for the test suite and generation of documentation. Packages -------- Native packages for your operating system will be provided in the near future. +------------------+-----------+--------------------------------------------+ | OS | Status | | +==================+===========+============================================+ | Debian 6/Squeeze | planned | Requires Backports | +------------------+-----------+--------------------------------------------+ | Debian 7/Wheezy | planned | | +------------------+-----------+--------------------------------------------+ | Ubuntu 13.04 | planned | | +------------------+-----------+--------------------------------------------+ | Ubuntu 13.10 | planned | | +------------------+-----------+--------------------------------------------+ | CentOS/RHEL 5 | n/a | Python 2.4 | +------------------+-----------+--------------------------------------------+ | CentOS/RHEL 6 | planned | | +------------------+-----------+--------------------------------------------+ | CentOS/RHEL 7 | planned | | +------------------+-----------+--------------------------------------------+ | `ArchLinux`_ | available | Maintained by `Tim Meusel`_ | +------------------+-----------+--------------------------------------------+ | `OpenBSD`_ | available | Maintained by `Jasper Lievisse Adriaanse`_ | +------------------+-----------+--------------------------------------------+ .. _ArchLinux: https://aur.archlinux.org/packages/?O=0&SeB=nd&K=puppetdb&outdated=&SB=n&SO=a&PP=50&do_Search=Go .. _Tim Meusel: https://github.com/bastelfreak .. _Jasper Lievisse Adriaanse: https://github.com/jasperla .. _OpenBSD: http://www.openbsd.org/cgi-bin/cvsweb/ports/databases/py-puppetdb/ Usage ===== Once you have pypuppetdb installed you can configure it to connect to PuppetDB and take it from there. Connecting ---------- The first thing you need to do is to connect with PuppetDB: .. code-block:: python >>> from pypuppetdb import connect >>> db = connect() Nodes ----- The following will return a generator object yielding Node objects for every returned node from PuppetDB. .. code-block:: python >>> nodes = db.nodes() >>> for node in nodes: >>> print(node) host1 host2 ... To query a single node the singular `node()` can be used: .. code-block:: python >>> node = db.node('hostname') >>> print(node) hostname Node scope ~~~~~~~~~~ The Node objects are a bit more special in that they can query for facts and resources themselves. Using those methods from a node object will automatically add a query to the request scoping the request to the node. .. code-block:: python >>> node = db.node('hostname') >>> print(node.fact('osfamily')) osfamily/hostname Facts ----- .. code-block:: python >>> facts = db.facts('osfamily') >>> for fact in facts: >>> print(fact) osfamily/host1 osfamily/host2 That queries PuppetDB for the 'osfamily' fact and will yield Fact objects, one per node this fact is known for. Resources --------- .. code-block:: python >>> resources = db.resources('file') Will return a generator object containing all file resources you're managing across your infrastructure. This is probably a bad idea if you have a big number of nodes as the response will be huge. Catalogs --------- .. code-block:: python >>> catalog = db.catalog('hostname') >>> for res in catalog.get_resources(): >>> print(res) Will return a Catalog object with the latest Catalog of the definded host. This catalog contains the defined Resources and Edges. .. code-block:: python >>> catalog = db.catalog('hostname') >>> resource = catalog.get_resource('Service','ntp') >>> for rel in resource.relationships: >>> print(rel) Class[Ntp] - contains - Service[ntp] File[/etc/ntp.conf] - notifies - Service[ntp] File[/etc/ntp.conf] - required-by - Service[ntp] Will return all Relationships of a given Resource defined by type and title. This will list all linked other Resources and the type of relationship. Query Builder ------------- Starting with version 0.3.0 pypuppetdb comes shipped with a QueryBuilder component that, as the name suggests, allows users to build PuppetDB AST queries in an Object-Oriented fashion. Vastly superior to constructing long strings than adding additional clauses to fulfill new requirements. The following code will build a query for the Nodes endpoint to find all nodes belonging to the production environment. .. code-block:: python >>> from pypuppetdb.QueryBuilder import * >>> op = AndOperator() >>> op.add(EqualsOperator('catalog_environment', 'production')) >>> op.add(EqualsOperator('facts_environment', 'production')) >>> print(op) ["and",["=", "catalog_environment", "production"],["=", "facts_environment", "production"]] This functionality is based on the PuppetDB AST query string syntax documented `here`_. .. _here: https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html Subqueries are implemented using corresponding operators (like documented). * SubqueryOperator * InOperator * ExtractOperator .. code-block:: python >>> from pypuppetdb.QueryBuilder import * >>> op = InOperator('certname') >>> ex = ExtractOperator() >>> ex.add_field(str('certname')) >>> sub = SubqueryOperator('events') >>> sub.add_query(EqualsOperator('status', 'noop')) >>> ex.add_query(sub) >>> op.add_query(ex) >>> print(op) ["in","certname",["extract",["certname"],["select_events",["=", "status", "noop"]]]] Or using [in ] querying: .. code-block:: python >>> from pypuppetdb.QueryBuilder import * >>> op = InOperator('certname') >>> op.add_array(["prod1.server.net", "prod2.server.net"]) >>> print(op) ["in","certname",["array", ['prod1.server.net', 'prod2.server.net']]] You can also access different entities from a single query on the root endpoint with the FromOperator: .. code-block:: python >>> op = InOperator('certname') >>> ex = ExtractOperator() >>> ex.add_field('certname') >>> fr = FromOperator('fact_contents') >>> nd = AndOperator() >>> nd.add(EqualsOperator("path", ["networking", "eth0", "macaddresses", 0])) >>> nd.add(EqualsOperator("value", "aa:bb:cc:dd:ee:00")) >>> ex.add_query(nd) >>> fr.add_query(ex) >>> op.add_query(fr) >>> print(op) ["in","certname",["from","fact_contents",["extract",["certname"],["and",["=", "path", ['networking', 'eth0', 'macaddresses', 0]],["=", "value", "aa:bb:cc:dd:ee:00"]]]]] Getting Help ============ This project is still very new so it's not inconceivable you'll run into issues. For bug reports you can file an `issue`_. If you need help with something feel free to pop by #voxpupuli on `Freenode`_ or the #puppetboard channel. .. _issue: https://github.com/voxpupuli/pypuppetdb/issues .. _Freenode: http://freenode.net Documentation ============= API documentation is automatically generated from the docstrings using Sphinx's autodoc feature. Documentation will automatically be rebuilt on every push thanks to the Read The Docs webhook. You can `find it here`_. .. _find it here: https://pypuppetdb.readthedocs.org/en/latest/ You can build the documentation manually by doing: .. code-block:: bash $ cd docs $ make html Doing so will only work if you have Sphinx installed, which you can achieve through: .. code-block:: bash $ pip install -r requirements.txt Contributing ============ We welcome contributions to this library. However, there are a few ground rules contributors should be aware of. License ------- This project is licensed under the Apache v2.0 License. As such, your contributions, once accepted, are automatically covered by this license. Copyright (c) 2013-2014 Daniele Sluijters Commit messages --------------- Write decent commit messages. Don't use swear words and refrain from uninformative commit messages as 'fixed typo'. The preferred format of a commit message: :: docs/quickstart: Fixed a typo in the Nodes section. If needed, elaborate further on this commit. Feel free to write a complete blog post here if that helps us understand what this is all about. Fixes #4 and resolves #2. If you'd like a more elaborate guide on how to write and format your commit messages have a look at this post by `Tim Pope`_. .. _Tim Pope: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html Tests ----- Commits are expected to contain tests or updates to tests if they add to or modify the current behavior. The test suite is powered by `pytest`_ and requires `pytest`_, `pytest-pep8`_, `httpretty`_ and `pytest-httpretty`_ which will be installed for you if you run: .. code-block:: bash $ pip install -r requirements.txt .. _pytest: http://pytest.org/latest/ .. _pytest-pep8: https://pypi.python.org/pypi/pytest-pep8 .. _httpretty: https://pypi.python.org/pypi/httpretty/ .. _pytest-httpretty: https://github.com/papaeye/pytest-httpretty To run the unit tests (the ones that don't require a live PuppetDB): .. code-block:: bash $ py.test -v -m unit If the tests pass, you're golden. If not we'll have to figure out why and fix that. Feel free to ask for help on this. pypuppetdb-2.2.0/conftest.py000066400000000000000000000004721366723304700161240ustar00rootroot00000000000000import pytest import pypuppetdb # Set up our API objects @pytest.fixture def baseapi(): return pypuppetdb.api.BaseAPI() @pytest.fixture def token_baseapi(): return pypuppetdb.api.BaseAPI(token='tokenstring') @pytest.fixture def utc(): """Create a UTC object.""" return pypuppetdb.utils.UTC() pypuppetdb-2.2.0/docs/000077500000000000000000000000001366723304700146525ustar00rootroot00000000000000pypuppetdb-2.2.0/docs/Makefile000066400000000000000000000127141366723304700163170ustar00rootroot00000000000000# 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) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 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 " singlehtml to make a single large HTML file" @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 " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @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 " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @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)/* html: $(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." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 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/PyPuppetDB.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyPuppetDB.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PyPuppetDB" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyPuppetDB" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 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." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 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." pypuppetdb-2.2.0/docs/api.rst000066400000000000000000000122651366723304700161630ustar00rootroot00000000000000.. _api: Developer Interface =================== .. module:: pypuppetdb This part of the documentation covers all the interfaces of PyPuppetDB. It will cover how the API is set up and how to configure which version of the API to use. Lazy objects ------------ .. note:: Reading in the response from PuppetDB is currently greedy, it will read in the complete response no matter the size. This will change once streaming and pagination support are added to PuppetDB's endpoints. In order for pypuppetdb to be able to deal with big datasets those functions that are expected to return more than a single item are implemented as generators. This is usually the case for functions with a plural name like :func:`~pypuppetdb.api.BaseAPI.nodes` or :func:`~pypuppetdb.api.BaseAPI.facts`. Because of this we'll only query PuppetDB once you start iterating over the generator object. Until that time not a single request is fired at PuppetDB. Most singular functions are implemented by calling their plural counterpart and then iterating over the generator, immediately exhausting the generator and returning a single/the first object. Main Interface -------------- What you'll usually need to do is use the :func:`connect` method to set up a connection with PuppetDB and indicate which version of the API you want to talk. .. autofunction:: connect API objects ----------- The PuppetDB API is no longer versioned. This was changed in v0.2.0 because it started to become too difficult to maintain multiple API versions. All the functions of the v1, v2, and v3 APIs have been moved to :class:`BaseAPI ` which now only supports API version 4 of PuppetDB. .. data:: API_VERSIONS :obj:`dict` of :obj:`int`::obj:`string` pairs representing the API version and it's URL prefix. We currently only handle API version 2 though it should be fairly easy to support version 1 should we want to. BaseAPI ^^^^^^^ .. autoclass:: pypuppetdb.api.BaseAPI :members: :private-members: Types ----- In order to facilitate working with the API most methods like :meth:`~pypuppetdb.api.BaseAPI.nodes` don't return the decoded JSON response but return an object representation of the querried endpoints data. .. autoclass:: pypuppetdb.types.Node :members: .. autoclass:: pypuppetdb.types.Fact .. autoclass:: pypuppetdb.types.Resource .. autoclass:: pypuppetdb.types.Event .. autoclass:: pypuppetdb.types.Report :members: .. autoclass:: pypuppetdb.types.Catalog :members: .. autoclass:: pypuppetdb.types.Edge Errors ------ Unfortunately things can go haywire. PuppetDB might not be reachable or complain about our query, requests might have to wait too long to recieve a response or the body is just too big to handle. In that case, we'll throw an exception at you. .. autoexception:: pypuppetdb.errors.APIError .. autoexception:: pypuppetdb.errors.ImproperlyConfiguredError :show-inheritance: .. autoexception:: pypuppetdb.errors.DoesNotComputeError :show-inheritance: .. autoexception:: pypuppetdb.errors.EmptyResponseError :show-inheritance: Query Builder ------------- With the increasing complexities of some queries it can be very difficult to maintain query strings as string objects. The 0.3.0 release introduces a new feature called the Query Builder which removes the need of string object concatenation into an Object-Oriented fashion. In order to create the following query: ``'["and", ["=", "facts_environment", "production"], ["=", "catalog_environment", "production"]]'`` Users can use the following code block to create the same thing: .. code-block:: python >>> from pypuppetdb.QueryBuilder import * >>> op = AndOperator() >>> op.add(EqualOperator("facts_environment", "production")) >>> op.add(EqualOperator("catalog_environment", "production")) The ``op`` object can then be directly input to the query parameter of any PuppetDB call. .. code-block:: python >>> pypuppetdb.nodes(query=op) .. autoclass:: pypuppetdb.QueryBuilder.BinaryOperator :members: .. autoclass:: pypuppetdb.QueryBuilder.EqualsOperator .. autoclass:: pypuppetdb.QueryBuilder.GreaterOperator .. autoclass:: pypuppetdb.QueryBuilder.LessOperator .. autoclass:: pypuppetdb.QueryBuilder.GreaterEqualOperator .. autoclass:: pypuppetdb.QueryBuilder.LessEqualOperator .. autoclass:: pypuppetdb.QueryBuilder.RegexOperator .. autoclass:: pypuppetdb.QueryBuilder.RegexArrayOperator .. autoclass:: pypuppetdb.QueryBuilder.NullOperator .. autoclass:: pypuppetdb.QueryBuilder.BooleanOperator :members: .. autoclass:: pypuppetdb.QueryBuilder.AndOperator .. autoclass:: pypuppetdb.QueryBuilder.OrOperator .. autoclass:: pypuppetdb.QueryBuilder.NotOperator .. autoclass:: pypuppetdb.QueryBuilder.ExtractOperator :members: .. autoclass:: pypuppetdb.QueryBuilder.FunctionOperator :members: .. autoclass:: pypuppetdb.QueryBuilder.SubqueryOperator :members: .. autoclass:: pypuppetdb.QueryBuilder.InOperator :members: .. autoclass:: pypuppetdb.QueryBuilder.FromOperator :members: Utilities --------- A few functions that are used across this library have been put into their own :mod:`utils` module. .. autoclass:: pypuppetdb.utils.UTC .. autofunction:: pypuppetdb.utils.json_to_datetime .. autofunction:: pypuppetdb.utils.versioncmp pypuppetdb-2.2.0/docs/conf.py000066400000000000000000000031371366723304700161550ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys import os import pypuppetdb.package pypuppetdb_root = os.path.dirname(os.path.abspath(".")) sys.path.insert(0, pypuppetdb_root) # -- General configuration ---------------------------------------------------- extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] templates_path = ["_templates"] source_suffix = ".rst" master_doc = "index" project = pypuppetdb.package.__title__ copyright = "{0}, {1}".format( pypuppetdb.package.__year__, pypuppetdb.package.__author__ ) version = pypuppetdb.package.__version__ release = version language = "en" exclude_patterns = ["_build"] pygments_style = "sphinx" # -- Options for HTML output -------------------------------------------------- html_theme = "default" html_static_path = ["_static"] htmlhelp_basename = "pypuppetdbdoc" # -- Options for LaTeX output ------------------------------------------------- latex_documents = [ ( "index", "pypuppetdb.tex", u"pypuppetdb Documentation", u"Daniele Sluijters", "manual", ), ] # -- Options for manual page output ------------------------------------------- man_pages = [ ( "index", "pypuppetdb", u"pypuppetdb Documentation", [u"Daniele Sluijters"], 1, ) ] # -- Options for Texinfo output ----------------------------------------------- texinfo_documents = [ ( "index", "pypuppetdb", u"pypuppetdb Documentation", u"Daniele Sluijters", "pypuppetdb", "Library to work with the PuppetDB REST API.", "Miscellaneous", ), ] pypuppetdb-2.2.0/docs/index.rst000066400000000000000000000014421366723304700165140ustar00rootroot00000000000000Welcome to pypuppetdb's documentation! ====================================== .. note:: This is a very new project and still changing at a rapid pace. As such the only documentation currently available is the API documentation and a brief Getting Started guide. Once this settles down tutorials and other documentation will be added over time. Getting started --------------- The quickstart should get you up and running with pypuppetdb and familiarise you with how this library works. .. toctree:: :maxdepth: 2 quickstart API Documentation ----------------- This part of the documentation focusses on the classes, methods and functions that make up this library. .. toctree:: :maxdepth: 2 api Indices and tables ================== * :ref:`genindex` * :ref:`search` pypuppetdb-2.2.0/docs/make.bat000066400000000000000000000117601366723304700162640ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :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. singlehtml to make a single large HTML file 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. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over 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 goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyPuppetDB.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyPuppetDB.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end pypuppetdb-2.2.0/docs/quickstart.rst000066400000000000000000000106371366723304700176050ustar00rootroot00000000000000.. _quickstart: Quickstart ========== Once you have pypuppetdb installed you can configure it to connect to PuppetDB and take it from there. Connecting ---------- The first thing you need to do is to connect with PuppetDB: .. code-block:: python >>> from pypuppetdb import connect >>> db = connect() Nodes ----- The following will return a generator object yielding Node objects for every returned node from PuppetDB. .. code-block:: python >>> nodes = db.nodes() >>> for node in nodes: >>> print(node) host1 host2 ... To query a single node the singular `node()` can be used: .. code-block:: python >>> node = db.node('hostname') >>> print(node) hostname Node scope ~~~~~~~~~~ The Node objects are a bit more special in that they can query for facts and resources themselves. Using those methods from a node object will automatically add a query to the request scoping the request to the node. .. code-block:: python >>> node = db.node('hostname') >>> print(node.fact('osfamily')) osfamily/hostname Facts ----- .. code-block:: python >>> facts = db.facts('osfamily') >>> for fact in facts: >>> print(fact) osfamily/host1 osfamily/host2 That queries PuppetDB for the 'osfamily' fact and will yield Fact objects, one per node this fact is known for. Resources --------- .. code-block:: python >>> resources = db.resources('file') Will return a generator object containing all file resources you're managing across your infrastructure. This is probably a bad idea if you have a big number of nodes as the response will be huge. SSL --- If PuppetDB and the tool that's using pypuppetdb aren't located on the same machine you will have to connect securely to PuppetDB using client certificates according to PuppetDB's default configuration. You can also tell PuppetDB to accept plain connections from anywhere instead of just the local machine but **don't do that**. Pypuppetdb can handle this easily for you. It requires two things: * Generate with your Puppet CA a key pair that you want to use * Tell pypuppetdb to use this keypair. Generate keypair ~~~~~~~~~~~~~~~~ On your Puppet Master or dedicated Puppet CA server: .. code-block:: console $ puppet cert generate Once that's done you'll need to get the public and private keyfile and copy them over. You can find those in Puppet's ``$ssldir``, usually ``/var/lib/puppet/ssl``: * private key: ``$ssldir/private_keys/.pem`` * public key: ``$ssldir/ca/signed/.pem`` Configure pypuppetdb for SSL ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once you have those you can pass them to pypuppetdb's ``connect()``: .. code-block:: python >>> db = connect(ssl_key='/path/to/private.pem', ssl_cert='/path/to/public.pem') If both ``ssl_key`` and ``ssl_cert`` are provided pypuppetdb will automatically switch over to using HTTPS instead. By default pypuppetdb will also verify the certificate PuppetDB is serving. This means that the authority that signed PuppetDB's server certificate, most likely your Puppet Master, must be part of the trusted set of certificates for your OS or must be added to that set. Those certificates are usually found in ``/etc/ssl/certs`` on Linux-y machines. For Debian, install your Puppet Master's certificate in ``/usr/local/share/ca-certificates`` with a ``.crt`` extension and then run ``dpkg-reconfigure ca-certificates`` as per ``/usr/share/doc/ca-certificates/README.Debian``. This of course requires the ``ca-certificates`` package to be installed. If you do not wish to do so or for whatever reason want to disable the verification of PuppetDB's certificate you can pass in ``ssl_verify=False``. RBAC Token Authentication ~~~~~~~~~~~~~~~~~~~~~~~~~ If you are using Puppet Enterprise Puppetdb >=4.0.2 - an RBAC token can be passed to pypuppetdb's ``connect()``: .. code-block:: python >>> db = connect(token='tokenstring') If this argument is passed, pypuppetdb will automatically switch over to using HTTPS. This is handled via the addition of ``X-Authentication`` to the session headers If you need to disable validation of the certificate PuppetDB is serving, please follow the steps documented in the ``Configure pypuppetdb for SSL`` section It should also be noted that when using RBAC token authentication, the ``ssl_key`` and ``ssl_cert`` options should not be used and are not required If you require more information regarding RBAC token generation in PuppetDB, pypuppetdb-2.2.0/packaging/000077500000000000000000000000001366723304700156465ustar00rootroot00000000000000pypuppetdb-2.2.0/packaging/rpm/000077500000000000000000000000001366723304700164445ustar00rootroot00000000000000pypuppetdb-2.2.0/packaging/rpm/python-pypuppetdb.spec000066400000000000000000000103601366723304700230330ustar00rootroot00000000000000%if 0%{?fedora} > 12 %global with_python3 1 %else %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} %endif # use "--with tests" to run tests. # tests not yet working in the rpm environment, so disabled by default %bcond_with tests %global srcname pypuppetdb %global srcname_init %(c=%{srcname}; echo ${c:0:1}) %global srcversion 0.1.1 Name: python-%{srcname} Version: 0.1.1 Release: 1%{?dist} Epoch: 1 Summary: A python library for working with the PuppetDB REST API Group: Development/Libraries License: Apache License 2.0 URL: https://github.com/puppet-community/pypuppetdb Source0: http://pypi.python.org/packages/source/%{srcname_init}/%{srcname}/%{srcname}-%{srcversion}.tar.gz BuildArch: noarch Requires: python-requests BuildRequires: python-setuptools %if 0%{?with tests} BuildRequires: python-requests BuildRequires: pytest BuildRequires: python-mock BuildRequires: python-httpretty BuildRequires: python-pytest-pep8 BuildRequires: python-coverage %endif %description pypuppetdtb is a library to work with PuppetDB's REST API. It is implemented using the requests library. This library is a thin wrapper around the REST API providing some convinience functions and objects to request and hold data from PuppetDB. %package doc Summary: Documentation for %{name} Group: Documentation Requires: %{name} = %{epoch}:%{version}-%{release} %description doc Documentation and examples for %{name}. %if 0%{?with_python3} %package -n python3-%{srcname} Summary: A python library for working with the PuppetDB REST API BuildRequires: python3-requests %description -n python3-%{srcname} pypuppetdtb is a library to work with PuppetDB's REST API. It is implemented using the requests library. This library is a thin wrapper around the REST API providing some convinience functions and objects to request and hold data from PuppetDB. %package -n python3-%{srcname}-doc Summary: Documentation for python3-%{srcname} Group: Documentation Requires: python3-%{srcname} = %{epoch}:%{version}-%{release} %description -n python3-%{srcname}-doc Documentation and examples for python3-%{srcname}. %endif %prep %setup -q -n %{srcname}-%{srcversion} %if 0%{?with_python3} rm -rf %{py3dir} cp -a . %{py3dir} find %{py3dir} -name '*.py' | xargs sed -i '1s|^#!python|#!%{__python3}|' %endif %build %{__python} setup.py build %if 0%{?with_python3} pushd %{py3dir} %{__python3} setup.py build popd %endif %install %{__python} setup.py install -O1 --skip-build --root %{buildroot} # The docs are not included in the pypuppetdb tarballs that have been uploaded # to pypi so we can't build them here #make -C docs html rm -rf %{buildroot}%{python_sitelib}/site.py rm -rf %{buildroot}%{python_sitelib}/site.py[co] rm -rf %{buildroot}%{python_sitelib}/easy-install.pth # uncomment this when docs are being built #rm -rf docs/_build/html/.buildinfo %if 0%{?with_python3} pushd %{py3dir} %{__python3} setup.py install -O1 --skip-build --root %{buildroot} # The docs are not included in the pypuppetdb tarballs that have been uploaded # to pypi so we can't build them here #make -C docs html rm -rf %{buildroot}%{python3_sitelib}/site.py rm -rf %{buildroot}%{python3_sitelib}/site.py[co] rm -rf %{buildroot}%{python3_sitelib}/easy-install.pth rm -rf %{buildroot}%{python3_sitelib}/__pycache__/site.cpython-3?.pyc # uncomment this when docs are being built #rm -rf docs/_build/html/.buildinfo popd %endif %check %if 0%{?with tests} %{__python} setup.py test %if 0%{?with_python3} pushd %{py3dir} %{__python3} setup.py test popd %endif %endif %files %doc LICENSE PKG-INFO CHANGELOG.rst README.rst %{python_sitelib}/*.egg-info %{python_sitelib}/%{srcname} %files doc # uncomment this when docs are being built #%doc docs/_build/html %if 0%{?with_python3} %files -n python3-%{srcname} %doc LICENSE PKG-INFO CHANGELOG.rst README.rst %{python3_sitelib}/*.egg-info %{python3_sitelib}/%{srcname} %files -n python3-%{srcname}-doc # uncomment this when docs are being built #%doc docs/_build/html %endif %changelog * Fri Jan 02 2015 Robin Bowes - 1:0.1.1-1 - Initial packaging pypuppetdb-2.2.0/pypuppetdb/000077500000000000000000000000001366723304700161165ustar00rootroot00000000000000pypuppetdb-2.2.0/pypuppetdb/QueryBuilder.py000066400000000000000000000541331366723304700211120ustar00rootroot00000000000000from __future__ import absolute_import from __future__ import unicode_literals import datetime import json import logging from pypuppetdb.errors import APIError log = logging.getLogger(__name__) class BinaryOperator(object): """ This is a parent helper class used to create PuppetDB AST queries for single key-value pairs for the available operators. It is possible to directly declare the various types of queries from this class. For instance the code BinaryOperator('=', 'certname', 'node1.example.com') generates the PuppetDB query '["=", "certname", "node1.example.com"]'. It is preferred to use the child classes as they may have restrictions specific to that operator. See https://docs.puppet.com/puppetdb/4.0/api/query/v4/ast.html#binary-operators for more information. :param operator: The binary query operation performed. There is no value checking on this field. :type operator: :obj:`string` :param field: The PuppetDB endpoint query field. See endpoint documentation for valid values. :type field: any :param value: The values of the field to match, or not match. :type value: any """ def __init__(self, operator, field, value): if isinstance(value, datetime.datetime): value = str(value) self.data = [operator, field, value] def __repr__(self): return 'Query: {0}'.format(self) def __str__(self): return json.dumps(self.json_data()) def json_data(self): return self.data class BooleanOperator(object): """ This is a parent helper class used to create PuppetDB AST queries for available boolean queries. It is possible to directly declare a boolean query from this class. For instance the code BooleanOperator("and") will create an empty query '["and",]'. An error will be raised if there are no queries added via :func:`~pypuppetdb.QueryBuilder.BooleanOperator.add` See https://docs.puppet.com/puppetdb/4.0/api/query/v4/ast.html#binary-operators for more information. :param operator: The boolean query operation to perform. :type operator: :obj:`string` """ def __init__(self, operator): self.operator = operator self.operations = [] def add(self, query): if type(query) == list: for i in query: self.add(i) elif type(query) == str: self.operations.append(json.loads(query)) elif isinstance(query, (BinaryOperator, InOperator, BooleanOperator)): self.operations.append(query.json_data()) else: raise APIError("Can only accept fixed-string queries, arrays " + "or operator objects") def __repr__(self): return 'Query: {0}'.format(self) def __str__(self): return json.dumps(self.json_data()) def json_data(self): if len(self.operations) == 0: raise APIError("At least one query operation is required") return [self.operator] + self.operations class ExtractOperator(object): """ Queries that either do not or cannot require all the key-value pairs from an endpoint can use the Extract Operator as described in https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#projection-operators. The syntax of this operator requires a function and/or a list of fields, an optional standard query and an optional group by clause including a list of fields. """ def __init__(self): self.fields = [] self.query = None self.group_by = [] def add_field(self, field): if isinstance(field, list): for i in field: self.add_field(i) elif isinstance(field, str): self.fields.append(field) elif isinstance(field, FunctionOperator): self.fields.append(field.json_data()) else: raise APIError("ExtractOperator.add_field only supports " "lists and strings") def add_query(self, query): if self.query is not None: raise APIError("Only one query is supported by ExtractOperator") elif isinstance(query, str): self.query = json.loads(query) elif isinstance(query, (BinaryOperator, SubqueryOperator, BooleanOperator)): self.query = query.json_data() else: raise APIError("ExtractOperator.add_query only supports " "strings, BinaryOperator, BooleanOperator " "and SubqueryOperator objects") def add_group_by(self, field): if isinstance(field, list): for i in field: self.add_group_by(i) elif isinstance(field, str): if len(self.group_by) == 0: self.group_by.append('group_by') self.group_by.append(field) elif isinstance(field, FunctionOperator): if len(self.group_by) == 0: self.group_by.append('group_by') self.group_by.append(field.json_data()) else: raise APIError("ExtractOperator.add_group_by only supports " "lists, strings, and FunctionOperator objects") def __repr__(self): return 'Query: {0}'.format(self) def __str__(self): return json.dumps(self.json_data()) def json_data(self): if len(self.fields) == 0: raise APIError("ExtractOperator needs at least one field") arr = ['extract', self.fields] if self.query is not None: arr.append(self.query) if len(self.group_by) > 0: arr.append(self.group_by) return arr class FunctionOperator(object): """ Performs an aggregate function on the result of a subquery, full documentation is available at https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#function. This object can only be used in the field list or group by list of an ExtractOperator object. :param function: The name of the function to perform. :type function: :obj:`str` :param field: The name of the field to perform the function on. All functions with the exception of count require this value. :type field: :obj:`str` """ def __init__(self, function, field=None, fmt=None): if function not in ['count', 'avg', 'sum', 'min', 'max', 'to_string']: raise APIError("Unsupport function: {0}".format(function)) elif function != "count" and field is None: raise APIError("Function {0} requires a field value".format( function)) elif function == 'to_string' and fmt is None: raise APIError("Function {0} requires an extra 'fmt' parameter") self.arr = ['function', function] if field is not None: self.arr.append(field) if function == 'to_string': self.arr.append(fmt) def __repr__(self): return 'Query: {0}'.format(self) def __str__(self): return json.dumps(self.json_data()) def json_data(self): return self.arr class SubqueryOperator(object): """ Performs a subquery to another puppetDB object, full documentation is available at https://docs.puppet.com/puppetdb/3.2/api/query/v4/operators.html#subquery-operators This object must be used in combination with the InOperator according to documentation. :param endpoint: The name of the subquery object :type function: :obj:`str` """ def __init__(self, endpoint): if endpoint not in ['catalogs', 'edges', 'environments', 'events', 'facts', 'fact_contents', 'fact_paths', 'nodes', 'reports', 'resources']: raise APIError("Unsupported endpoint: {0}".format(endpoint)) self.query = None self.arr = ['select_{0}'.format(endpoint)] def add_query(self, query): if self.query is not None: raise APIError("Only one query is supported by ExtractOperator") else: self.query = True self.arr.append(query.json_data()) def __repr__(self): return 'Query: {0}'.format(self) def __str__(self): return json.dumps(self.json_data()) def json_data(self): return self.arr class InOperator(object): """ Performs boolean compare between a field a subquery result https://docs.puppet.com/puppetdb/3.2/api/query/v4/operators.html#subquery-operators This object must be used in combination with the SubqueryOperator according to documentation. :param field: The name of the subquery object :type function: :obj:`str` """ def __init__(self, field): self.query = None self.arr = ['in', field] def add_query(self, query): if self.query is not None: raise APIError("Only one query is supported by ExtractOperator") elif isinstance(query, str): self.query = True self.arr.append(json.loads(query)) elif isinstance(query, (ExtractOperator, FromOperator)): self.query = True self.arr.append(query.json_data()) else: raise APIError("InOperator.add_query only supports " "strings, ExtractOperator, and" "FromOperator objects") def add_array(self, values): if self.query is not None: raise APIError("Only one array is supported by the InOperator") elif isinstance(values, list): def depth(l): return (isinstance(l, list) and len(l) != 0) \ and max(map(depth, l)) + 1 if depth(values) == 1: self.query = True self.arr.append(['array', values]) else: raise APIError("InOperator.add_array: cannot pass in " "nested arrays (or empty arrays)") else: raise APIError("InOperator.add_array: Ill-formatted array, " "must be of the format: " "['array', []]") def __repr__(self): return 'Query: {0}'.format(self) def __str__(self): return json.dumps(self.json_data()) def json_data(self): return self.arr class FromOperator(object): """ From contextual operator that allows for queries on the root endpoint or subqueries into other entities: https://puppet.com/docs/puppetdb/5.1/api/query/v4/ast.html#from Ex.) fr = FromOperator("facts") fr.add_query(EqualsOperator("foo", "bar")) fr.add_order_by(["certname"]) fr.add_limit(10) note: only supports single entity From operations """ def __init__(self, endpoint): valid_entities = ["aggregate_event_counts", "catalogs", "edges", "environments", "event_counts", "events", "facts", "fact_contents", "fact_names", "fact_paths", "nodes", "producers", "reports", "resources"] if endpoint in valid_entities: self.endpoint = endpoint else: raise APIError("Endpoint is invalid. Must be " "one of the following : %s" % valid_entities) self.query = None self.order_by = [] self.limit = None self.offset = None def add_query(self, query): if self.query is not None: raise APIError("Only one main query is supported by FromOperator") elif isinstance(query, (InOperator, ExtractOperator, BinaryOperator, BooleanOperator, FunctionOperator)): self.query = query.json_data() else: raise APIError("FromOperator.add_field only supports " "Operator Objects") def add_order_by(self, fields): def depth(l): return isinstance(l, list) and max(map(depth, l)) + 1 fields_depth = depth(fields) if isinstance(fields, list): if fields_depth == 1 or fields_depth == 2: self.order_by = fields else: raise APIError("ExtractOperator.add_order_by only " "supports lists of fields of depth " "one or two: [value, ] or " "[value]") else: raise APIError("ExtractOperator.add_group_by only supports " "lists of one or more fields") def add_limit(self, lim): if isinstance(lim, int): self.limit = lim else: raise APIError("ExtractOperator.add_limit only supports ints") def add_offset(self, off): if isinstance(off, int): self.offset = off else: raise APIError("ExtractOperator.add_offset only supports ints") def __repr__(self): return 'Query: {0}'.format(self) def __str__(self): return json.dumps(self.json_data()) def json_data(self): if self.query is None: raise APIError("FromOperator needs one main query") arr = ['from', self.endpoint, self.query] if len(self.order_by) > 0: arr.append(['order_by', self.order_by]) if self.limit is not None: arr.append(['limit', self.limit]) if self.offset is not None: arr.append(['offset', self.offset]) return arr class EqualsOperator(BinaryOperator): """ Builds an equality filter based on the supplied field-value pair as described https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#equality. In order to create the following query: ["=", "environment", "production"] The following code can be used. EqualsOperator('environment', 'production') :param field: The PuppetDB endpoint query field. See endpoint documentation for valid values. :type field: any :param value: The value of the field to match, or not match. :type value: any """ def __init__(self, field, value): super(EqualsOperator, self).__init__("=", field, value) class GreaterOperator(BinaryOperator): """ Builds a greater-than filter based on the supplied field-value pair as described https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#greater-than. In order to create the following query: [">", "catalog_timestamp", "2016-06-01 00:00:00"] The following code can be used. GreaterOperator('catalog_timestamp', datetime.datetime(2016, 06, 01)) :param field: The PuppetDB endpoint query field. See endpoint documentation for valid values. :type field: any :param value: Matches if the field is greater than this value. :type value: Number, timestamp or array """ def __init__(self, field, value): super(GreaterOperator, self).__init__(">", field, value) class LessOperator(BinaryOperator): """ Builds a less-than filter based on the supplied field-value pair as described https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#less-than. In order to create the following query: ["<", "catalog_timestamp", "2016-06-01 00:00:00"] The following code can be used. LessOperator('catalog_timestamp', datetime.datetime(2016, 06, 01)) :param field: The PuppetDB endpoint query field. See endpoint documentation for valid values. :type field: any :param value: Matches if the field is less than this value. :type value: Number, timestamp or array """ def __init__(self, field, value): super(LessOperator, self).__init__("<", field, value) class GreaterEqualOperator(BinaryOperator): """ Builds a greater-than or equal-to filter based on the supplied field-value pair as described https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#greater-than-or-equal-to. In order to create the following query: [">=", "facts_timestamp", "2016-06-01 00:00:00"] The following code can be used. GreaterEqualOperator('facts_timestamp', datetime.datetime(2016, 06, 01)) :param field: The PuppetDB endpoint query field. See endpoint documentation for valid values. :type field: any :param value: Matches if the field is greater than or equal to\ this value. :type value: Number, timestamp or array """ def __init__(self, field, value): super(GreaterEqualOperator, self).__init__(">=", field, value) class LessEqualOperator(BinaryOperator): """ Builds a less-than or equal-to filter based on the supplied field-value pair as described https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#less-than-or-equal-to. In order to create the following query: ["<=", "facts_timestamp", "2016-06-01 00:00:00"] The following code can be used. LessEqualOperator('facts_timestamp', datetime.datetime(2016, 06, 01)) :param field: The PuppetDB endpoint query field. See endpoint documentation for valid values. :type field: any :param value: Matches if the field is less than or equal to\ this value. :type value: Number, timestamp or array """ def __init__(self, field, value): super(LessEqualOperator, self).__init__("<=", field, value) class RegexOperator(BinaryOperator): """ Builds a regular expression filter based on the supplied field-value pair as described https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#regexp-match. In order to create the following query: ["~", "certname", "www\\d+\\.example\\.com"] The following code can be used. RegexOperator('certname', 'www\\d+\\.example\\.com') :param field: The PuppetDB endpoint query field. See endpoint documentation for valid values. :type field: any :param value: Matches if the field matches this regular expression. :type value: :obj:`string` """ def __init__(self, field, value): super(RegexOperator, self).__init__("~", field, value) class RegexArrayOperator(BinaryOperator): """ Builds a regular expression array filter based on the supplied field-value pair. This query only works on fields with paths as described https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#regexp-array-match. In order to create the following query: ["~", "path", ["networking", "eth.*", "macaddress"]] The following code can be used. RegexArrayOperator('path', ["networking", "eth.*", "macaddress"]) :param field: The PuppetDB endpoint query field. See endpoint documentation for valid values. :type field: any :param value: Matches if the field matches this regular expression. :type value: :obj:`list` """ def __init__(self, field, value): super(RegexArrayOperator, self).__init__("~>", field, value) class NullOperator(BinaryOperator): """ Builds a null filter based on the field and boolean value pair as described https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#null-is-null. This filter only works on field that may be null. Value may only be True or False. In order to create the following query: ["null?", "deactivated", true] The following code can be used. NullOperator('deactivated', True) :param field: The PuppetDB endpoint query field. See endpoint documentation for valid values. :type field: any :param value: Matches if the field value is null (if True) or\ not null (if False) :type value: :obj:`bool` """ def __init__(self, field, value): if type(value) != bool: raise APIError("NullOperator value must be boolean") super(NullOperator, self).__init__("null?", field, value) class AndOperator(BooleanOperator): """ Builds an AND boolean filter. Only results that match ALL criteria from the included query strings will be returned from PuppetDB. Full documentation is available https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#and In order to create the following query: ["and", ["=", "catalog_environment", "production"], ["=", "facts_environment", "production"]] The following code can be used: op = AndOperator() op.add(EqualsOperator("catalog_environment", "production")) op.add(EqualsOperator("facts_environment", "production")) """ def __init__(self): super(AndOperator, self).__init__("and") class OrOperator(BooleanOperator): """ Builds an OR boolean filter. Only results that match ANY criteria from the included query strings will be returned from PuppetDB. Full documentation is available https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#or. In order to create the following query: ["or", ["=", "name", "hostname"], ["=", "name", "architecture"]] The following code can be used: op = OrOperator() op.add(EqualsOperator("name", "hostname")) op.add(EqualsOperator("name", "architecture")) """ def __init__(self): super(OrOperator, self).__init__("or") class NotOperator(BooleanOperator): """ Builds a NOT boolean filter. Only results that DO NOT match criteria from the included query strings will be returned from PuppetDB. Full documentation is available https://docs.puppet.com/puppetdb/4.1/api/query/v4/ast.html#not Unlike the other Boolean Operator objects this operator only accepts a single query string. In order to create the following query: ["not", ["=", "osfamily", "RedHat"]] The following code can be used. op = NotOperator() op.add(EqualsOperator("osfamily", "RedHat")) """ def __init__(self): super(NotOperator, self).__init__("not") def add(self, query): if len(self.operations) > 0: raise APIError("This operator only accept one query string") elif isinstance(query, list) and len(query) > 1: raise APIError("This operator only accept one query string") super(NotOperator, self).add(query) pypuppetdb-2.2.0/pypuppetdb/__init__.py000066400000000000000000000075101366723304700202320ustar00rootroot00000000000000from __future__ import absolute_import from __future__ import unicode_literals """ pypuppetdb PuppetDB API library ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ pypuppetdb is a library to work with PuppetDB's REST API. It provides a way to query PuppetDB and a set of additional methods and objects to make working with PuppetDB's API and the responses easier: >>> from pypuppetdb import connect >>> db = connect() >>> nodes = db.nodes() >>> print(nodes) >>> for node in nodes: >>> print(node) host1 host2 ... This will return a generator object yielding Node objects for every returned node from PuppetDB. To query a single node the singular db.node() can be used: >>> node = db.node('hostname') >>> print(node) hostname The Node objects are a bit more special in that they can query for facts and resources themselves. Using those methods from a node object will automatically add a query to the request scoping the request to the node. >>> node = db.node('hostname') >>> print node.fact('osfamily') osfamily/hostname We can also query for facts: >>> facts = db.facts('osfamily') >>> print(facts) >> for fact in facts: >>> print(fact) osfamily/host1 osfamily/host2 That queries PuppetDB for the 'osfamily' fact and will yield Fact objects, one per node this fact is found on. >>> resources = db.resources('file') Will return a generator object containing all file resources you're managing across your infrastructure. This is probably a bad idea if you have a big number of nodes as the response will be huge. """ import logging from pypuppetdb.api import BaseAPI logging.getLogger(__name__).addHandler(logging.NullHandler()) def connect(host='localhost', port=8080, ssl_verify=False, ssl_key=None, ssl_cert=None, timeout=10, protocol=None, url_path='/', username=None, password=None, token=None): """Connect with PuppetDB. This will return an object allowing you to query the API through its methods. :param host: (Default: 'localhost;) Hostname or IP of PuppetDB. :type host: :obj:`string` :param port: (Default: '8080') Port on which to talk to PuppetDB. :type port: :obj:`int` :param ssl_verify: (optional) Verify PuppetDB server certificate. :type ssl_verify: :obj:`bool` or :obj:`string` True, False or filesystem \ path to CA certificate. :param ssl_key: (optional) Path to our client secret key. :type ssl_key: :obj:`None` or :obj:`string` representing a filesystem\ path. :param ssl_cert: (optional) Path to our client certificate. :type ssl_cert: :obj:`None` or :obj:`string` representing a filesystem\ path. :param timeout: (Default: 10) Number of seconds to wait for a response. :type timeout: :obj:`int` :param protocol: (optional) Explicitly specify the protocol to be used (especially handy when using HTTPS with ssl_verify=False and without certs) :type protocol: :obj:`None` or :obj:`string` :param url_path: (Default: '/') The URL path where PuppetDB is served :type url_path: :obj:`None` or :obj:`string` :param username: (optional) The username to use for HTTP basic authentication :type username: :obj:`None` or :obj:`string` :param password: (optional) The password to use for HTTP basic authentication :type password: :obj:`None` or :obj:`string` :param token: (optional) The x-auth token to use for X-Authentication :type token: :obj:`None` or :obj:`string` """ return BaseAPI(host=host, port=port, timeout=timeout, ssl_verify=ssl_verify, ssl_key=ssl_key, ssl_cert=ssl_cert, protocol=protocol, url_path=url_path, username=username, password=password, token=token) pypuppetdb-2.2.0/pypuppetdb/api.py000066400000000000000000001207711366723304700172510ustar00rootroot00000000000000from __future__ import absolute_import from __future__ import unicode_literals import hashlib import json import logging from datetime import datetime, timedelta from urllib.parse import quote import requests from pypuppetdb.QueryBuilder import (EqualsOperator) from pypuppetdb.errors import (APIError, DoesNotComputeError, EmptyResponseError) from pypuppetdb.types import (Catalog, Edge, Event, Fact, Inventory, Node, Report, Resource) from pypuppetdb.utils import json_to_datetime log = logging.getLogger(__name__) ENDPOINTS = { 'facts': 'pdb/query/v4/facts', 'fact-names': 'pdb/query/v4/fact-names', 'nodes': 'pdb/query/v4/nodes', 'resources': 'pdb/query/v4/resources', 'catalogs': 'pdb/query/v4/catalogs', 'mbean': 'metrics/v1/mbeans', # metrics v2 endpoint is now the jolokia library and all of its operations # https://jolokia.org/reference/html/protocol.html#jolokia-operations 'metrics': 'metrics/v2/read', 'metrics-base': 'metrics/v2', 'metrics-exec': 'metrics/v2/exec', 'metrics-list': 'metrics/v2/list', 'metrics-search': 'metrics/v2/search', 'metrics-write': 'metrics/v2/write', 'metrics-version': 'metrics/v2/version', 'reports': 'pdb/query/v4/reports', 'events': 'pdb/query/v4/events', 'event-counts': 'pdb/query/v4/event-counts', 'aggregate-event-counts': 'pdb/query/v4/aggregate-event-counts', 'server-time': 'pdb/meta/v1/server-time', 'version': 'pdb/meta/v1/version', 'environments': 'pdb/query/v4/environments', 'factsets': 'pdb/query/v4/factsets', 'fact-paths': 'pdb/query/v4/fact-paths', 'fact-contents': 'pdb/query/v4/fact-contents', 'edges': 'pdb/query/v4/edges', 'pql': 'pdb/query/v4', 'inventory': 'pdb/query/v4/inventory', 'status': 'status/v1/services/puppetdb-status', 'cmd': 'pdb/cmd/v1' } PARAMETERS = { 'order_by': 'order_by', 'include_total': 'include_total', 'count_by': 'count_by', 'counts_filter': 'counts_filter', 'summarize_by': 'summarize_by', 'server_time': 'server_time', } COMMAND_VERSION = { "deactivate node": 3, "replace catalog": 9, "replace facts": 5, "store report": 8 } ERROR_STRINGS = { 'timeout': 'Connection to PuppetDB timed out on', 'refused': 'Could not reach PuppetDB on', } class BaseAPI(object): """This is a Base or Abstract class and is not meant to be instantiated or used directly. The BaseAPI object defines a set of methods that can be reused across different versions of the PuppetDB API. If querying for a certain resource is done in an identical fashion across different versions it will be implemented here and should be overridden in their respective versions if they deviate. If :attr:`ssl` is set to `True` but either :attr:`ssl_key` or\ :attr:`ssl_cert` are `None` this will raise an error. :param api_version: (Default 4) Version of the API we're initialising. :type api_version: :obj:`int` :param host: (optional) Hostname or IP of PuppetDB. :type host: :obj:`string` :param port: (optional) Port on which to talk to PuppetDB. :type port: :obj:`int` :param ssl_verify: (optional) Verify PuppetDB server certificate. :type ssl_verify: :obj:`bool` or :obj:`string` :param ssl_key: (optional) Path to our client secret key. :type ssl_key: :obj:`None` or :obj:`string` representing a filesystem\ path. :param ssl_cert: (optional) Path to our client certificate. :type ssl_cert: :obj:`None` or :obj:`string` representing a filesystem\ path. :param timeout: (optional) Number of seconds to wait for a response. :type timeout: :obj:`int` :param protocol: (optional) Explicitly specify the protocol to be used (especially handy when using HTTPS with ssl_verify=False and without certs) :type protocol: :obj:`None` or :obj:`string` :param url_path: (optional) The URL path where PuppetDB is served (if not at the root / path) :type url_path: :obj:`None` or :obj:`string` :param username: (optional) The username to use for HTTP basic authentication :type username: :obj:`None` or :obj:`string` :param password: (optional) The password to use for HTTP basic authentication :type password: :obj:`None` or :obj:`string` :param token: (optional) The X-auth token to use for X-Authentication :type token: :obj:`None` or :obj:`string` :param metric_api_version: (Default 'v2') Version of the metric API we're initialising. :type metric_api_version: :obj:`None` or :obj:`string` :raises: :class:`~pypuppetdb.errors.ImproperlyConfiguredError` """ def __init__(self, host='localhost', port=8080, ssl_verify=True, ssl_key=None, ssl_cert=None, timeout=10, protocol=None, url_path=None, username=None, password=None, token=None, metric_api_version=None): """Initialises our BaseAPI object passing the parameters needed in order to be able to create the connection strings, set up SSL and timeouts and so forth.""" self.api_version = 'v4' if metric_api_version is not None and metric_api_version not in ['v1', 'v2']: raise ValueError("metric_api_version specified must be None, 'v1' or 'v2'," " was given: '{}'".format(metric_api_version)) self.metric_api_version = metric_api_version if metric_api_version else 'v2' self.host = host self.port = port self.ssl_verify = ssl_verify self.ssl_key = ssl_key self.ssl_cert = ssl_cert self.timeout = timeout self.token = token # Standardise the URL path to a format similar to /puppetdb if url_path: if not url_path.startswith('/'): url_path = '/' + url_path if url_path.endswith('/'): url_path = url_path[:-1] else: url_path = '' self.url_path = url_path if username and password: self.username = username self.password = password else: self.username = None self.password = None self._session = requests.Session() self._session.headers = { 'content-type': 'application/json', 'accept': 'application/json', 'accept-charset': 'utf-8' } if self.token: self._session.headers['X-Authentication'] = self.token if protocol is not None: protocol = protocol.lower() if protocol not in ['http', 'https']: raise ValueError('Protocol specified must be http or https') self.protocol = protocol elif self.ssl_key is not None and self.ssl_cert is not None: self.protocol = 'https' elif self.token is not None: self.protocol = 'https' else: self.protocol = 'http' @property def version(self): """The version of the API we're querying against. :returns: Current API version. :rtype: :obj:`string`""" return self.api_version @property def base_url(self): """A base_url that will be used to construct the final URL we're going to query against. :returns: A URL of the form: ``proto://host:port``. :rtype: :obj:`string` """ return '{proto}://{host}:{port}{url_path}'.format( proto=self.protocol, host=self.host, port=self.port, url_path=self.url_path, ) @property def total(self): """The total-count of the last request to PuppetDB if enabled as parameter in _query method :returns Number of total results :rtype :obj:`int` """ if self.last_total is not None: return int(self.last_total) @staticmethod def _normalize_resource_type(type_): """Normalizes the type passed to the api by capitalizing each part of the type. For example: sysctl::value -> Sysctl::Value user -> User """ return '::'.join([s.capitalize() for s in type_.split('::')]) def _url(self, endpoint, path=None): """The complete URL we will end up querying. Depending on the endpoint we pass in this will result in different URL's with different prefixes. :param endpoint: The PuppetDB API endpoint we want to query. :type endpoint: :obj:`string` :param path: An additional path if we don't wish to query the\ bare endpoint. :type path: :obj:`string` :returns: A URL constructed from :func:`base_url` with the\ apropraite API version/prefix and the rest of the path added\ to it. :rtype: :obj:`string` """ log.debug('_url called with endpoint: {0} and path: {1}'.format( endpoint, path)) try: endpoint = ENDPOINTS[endpoint] except KeyError: # If we reach this we're trying to query an endpoint that doesn't # exist. This shouldn't happen unless someone made a booboo. raise APIError url = '{base_url}/{endpoint}'.format( base_url=self.base_url, endpoint=endpoint, ) if path is not None: url = '{0}/{1}'.format(url, quote(path)) return url def _query(self, endpoint, path=None, query=None, order_by=None, limit=None, offset=None, include_total=False, summarize_by=None, count_by=None, count_filter=None, payload=None, request_method='GET'): """This method actually querries PuppetDB. Provided an endpoint and an optional path and/or query it will fire a request at PuppetDB. If PuppetDB can be reached and answers within the timeout we'll decode the response and give it back or raise for the HTTP Status Code PuppetDB gave back. :param endpoint: The PuppetDB API endpoint we want to query. :type endpoint: :obj:`string` :param path: An additional path if we don't wish to query the\ bare endpoint. :type path: :obj:`string` :param query: (optional) A query to further narrow down the resultset. :type query: :obj:`string` :param order_by: (optional) Set the order parameters for the resultset. :type order_by: :obj:`string` :param limit: (optional) Tell PuppetDB to limit it's response to this\ number of objects. :type limit: :obj:`int` :param offset: (optional) Tell PuppetDB to start it's response from\ the given offset. This is useful for implementing pagination\ but is not supported just yet. :type offset: :obj:`string` :param include_total: (optional) Include the total number of results :type order_by: :obj:`bool` :param summarize_by: (optional) Specify what type of object you'd like\ to see counts at the event-counts and aggregate-event-counts \ endpoints :type summarize_by: :obj:`string` :param count_by: (optional) Specify what type of object is counted :type count_by: :obj:`string` :param count_filter: (optional) Specify a filter for the results :type count_filter: :obj:`string` :param payload: (optional) Arbitrary payload to send as part of the request. :type payload: :obj:`dict` :raises: :class:`~pypuppetdb.errors.EmptyResponseError` :returns: The decoded response from PuppetDB :rtype: :obj:`dict` or :obj:`list` """ log.debug('_query called with endpoint: {0}, path: {1}, query: {2}, ' 'limit: {3}, offset: {4}, summarize_by {5}, count_by {6}, ' 'count_filter: {7}, payload: {8}' .format(endpoint, path, query, limit, offset, summarize_by, count_by, count_filter, payload)) url = self._url(endpoint, path=path) if payload is None: payload = {} if query is not None: payload['query'] = query if order_by is not None: payload[PARAMETERS['order_by']] = order_by if limit is not None: payload['limit'] = limit if include_total is True: payload[PARAMETERS['include_total']] = \ json.dumps(include_total) if offset is not None: payload['offset'] = offset if summarize_by is not None: payload[PARAMETERS['summarize_by']] = summarize_by if count_by is not None: payload[PARAMETERS['count_by']] = count_by if count_filter is not None: payload[PARAMETERS['counts_filter']] = count_filter if not payload: payload = None if not self.token: auth = (self.username, self.password) else: auth = None try: if request_method.upper() == 'GET': r = self._session.get(url, params=payload, verify=self.ssl_verify, cert=(self.ssl_cert, self.ssl_key), timeout=self.timeout, auth=auth) elif request_method.upper() == 'POST': r = self._session.post(url, data=json.dumps(payload, default=str), verify=self.ssl_verify, cert=(self.ssl_cert, self.ssl_key), timeout=self.timeout, auth=auth) else: log.error("Only GET or POST supported, {0} unsupported".format( request_method)) raise APIError r.raise_for_status() # get total number of results if requested with include-total # just a quick hack - needs improvement if 'X-Records' in r.headers: self.last_total = r.headers['X-Records'] else: self.last_total = None json_body = r.json() if json_body is not None: return json_body else: del json_body raise EmptyResponseError except requests.exceptions.Timeout: log.error("{0} {1}:{2} over {3}.".format(ERROR_STRINGS['timeout'], self.host, self.port, self.protocol.upper())) raise except requests.exceptions.ConnectionError: log.error("{0} {1}:{2} over {3}.".format(ERROR_STRINGS['refused'], self.host, self.port, self.protocol.upper())) raise except requests.exceptions.HTTPError as err: log.error("{0} {1}:{2} over {3}.".format(err.response.text, self.host, self.port, self.protocol.upper())) raise def _cmd(self, command, payload): """This method posts commands to PuppetDB. Provided a command and payload it will fire a request at PuppetDB. If PuppetDB can be reached and answers within the timeout we'll decode the response and give it back or raise for the HTTP Status Code yesPuppetDB gave back. :param command: The PuppetDB Command we want to execute. :type command: :obj:`string` :param command: The payload, in wire format, specific to the command. :type path: :obj:`dict` :raises: :class:`~pypuppetdb.errors.EmptyResponseError` :returns: The decoded response from PuppetDB :rtype: :obj:`dict` or :obj:`list` """ log.debug('_cmd called with command: {0}, data: {1}'.format( command, payload)) url = self._url('cmd') if command not in COMMAND_VERSION: log.error("Only {0} supported, {1} unsupported".format( list(COMMAND_VERSION.keys()), command)) raise APIError params = { "command": command, "version": COMMAND_VERSION[command], "certname": payload['certname'], "checksum": hashlib.sha1(str(payload) # nosec .encode('utf-8')).hexdigest() } if not self.token: auth = (self.username, self.password) else: auth = None try: r = self._session.post(url, params=params, data=json.dumps(payload, default=str), verify=self.ssl_verify, cert=(self.ssl_cert, self.ssl_key), timeout=self.timeout, auth=auth) r.raise_for_status() json_body = r.json() if json_body is not None: return json_body else: del json_body raise EmptyResponseError except requests.exceptions.Timeout: log.error("{0} {1}:{2} over {3}.".format(ERROR_STRINGS['timeout'], self.host, self.port, self.protocol.upper())) raise except requests.exceptions.ConnectionError: log.error("{0} {1}:{2} over {3}.".format(ERROR_STRINGS['refused'], self.host, self.port, self.protocol.upper())) raise except requests.exceptions.HTTPError as err: log.error("{0} {1}:{2} over {3}.".format(err.response.text, self.host, self.port, self.protocol.upper())) raise # Method stubs def nodes(self, unreported=2, with_status=False, with_event_numbers=True, **kwargs): """Query for nodes by either name or query. If both aren't provided this will return a list of all nodes. This method also (optionally) fetches the nodes status and (optionally) event counts of the latest report from puppetdb. :param with_status: (optional) include the node status in the\ returned nodes :type with_status: :bool: :param unreported: (optional) amount of hours when a node gets marked as unreported :type unreported: :obj:`None` or integer :param with_event_numbers: (optional) include the exact number of\ changed/unchanged/failed/noop events when\ with_status is set to True. If set to False only "some" string is provided if there are resources with such status in the last report. This provides performance benefits as potentially slow event-counts query is omitted completely. :type with_event_numbers: :bool: :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function :returns: A generator yieling Nodes. :rtype: :class:`pypuppetdb.types.Node` """ nodes = self._query('nodes', **kwargs) now = datetime.utcnow() # If we happen to only get one node back it # won't be inside a list so iterating over it # goes boom. Therefor we wrap a list around it. if type(nodes) == dict: nodes = [nodes, ] if with_status and with_event_numbers: latest_events = self.event_counts( query=EqualsOperator("latest_report?", True), summarize_by='certname' ) for node in nodes: node['status_report'] = None node['events'] = None if with_status: if with_event_numbers: status = [s for s in latest_events if s['subject']['title'] == node['certname']] try: node['status_report'] = node['latest_report_status'] if status: node['events'] = status[0] except KeyError: if status: node['events'] = status = status[0] if status['successes'] > 0: node['status_report'] = 'changed' if status['noops'] > 0: node['status_report'] = 'noop' if status['failures'] > 0: node['status_report'] = 'failed' else: node['status_report'] = 'unchanged' else: node['status_report'] = node['latest_report_status'] node['events'] = { 'successes': 0, 'failures': 0, 'noops': 0, } if node['status_report'] == 'changed': node['events']['successes'] = 'some' elif node['status_report'] == 'noop': node['events']['noops'] = 'some' elif node['status_report'] == 'failed': node['events']['failures'] = 'some' # node report age if node['report_timestamp'] is not None: try: last_report = json_to_datetime( node['report_timestamp']) last_report = last_report.replace(tzinfo=None) unreported_border = now - timedelta(hours=unreported) if last_report < unreported_border: delta = (now - last_report) node['unreported'] = True node['unreported_time'] = '{0}d {1}h {2}m'.format( delta.days, int(delta.seconds / 3600), int((delta.seconds % 3600) / 60) ) except AttributeError: node['unreported'] = True if not node['report_timestamp']: node['unreported'] = True yield Node(self, name=node['certname'], deactivated=node['deactivated'], expired=node['expired'], report_timestamp=node['report_timestamp'], catalog_timestamp=node['catalog_timestamp'], facts_timestamp=node['facts_timestamp'], status_report=node['status_report'], noop=node.get('latest_report_noop'), noop_pending=node.get('latest_report_noop_pending'), events=node['events'], unreported=node.get('unreported'), unreported_time=node.get('unreported_time'), report_environment=node['report_environment'], catalog_environment=node['catalog_environment'], facts_environment=node['facts_environment'], latest_report_hash=node.get('latest_report_hash'), cached_catalog_status=node.get('cached_catalog_status') ) def node(self, name): """Gets a single node from PuppetDB. :param name: The name of the node search. :type name: :obj:`string` :return: An instance of Node :rtype: :class:`pypuppetdb.types.Node` """ nodes = self.nodes(path=name) return next(node for node in nodes) def edges(self, **kwargs): """Get the known catalog edges, formed between two resources. :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function. :returns: A generating yielding Edges. :rtype: :class:`pypuppetdb.types.Edge` """ edges = self._query('edges', **kwargs) for edge in edges: identifier_source = edge['source_type'] + '[' + edge['source_title'] + ']' identifier_target = edge['target_type'] + '[' + edge['target_title'] + ']' yield Edge(source=self.resources[identifier_source], target=self.resources[identifier_target], relationship=edge['relationship'], node=edge['certname']) def environments(self, **kwargs): """Get all known environments from Puppetdb. :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function. :returns: A list of dictionaries containing the results. :rtype: :obj:`list` of :obj:`dict` """ return self._query('environments', **kwargs) def facts(self, name=None, value=None, **kwargs): """Query for facts limited by either name, value and/or query. :param name: (Optional) Only return facts that match this name. :type name: :obj:`string` :param value: (Optional) Only return facts of `name` that\ match this value. Use of this parameter requires the `name`\ parameter be set. :type value: :obj:`string` :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function :returns: A generator yielding Facts. :rtype: :class:`pypuppetdb.types.Fact` """ if name is not None and value is not None: path = '{0}/{1}'.format(name, value) elif name is not None and value is None: path = name else: path = None facts = self._query('facts', path=path, **kwargs) for fact in facts: yield Fact( node=fact['certname'], name=fact['name'], value=fact['value'], environment=fact['environment'] ) def factsets(self, **kwargs): """Returns a set of all facts or for a single certname. :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function. :returns: A list of dictionaries containg the results. :rtype: :obj:`list` of :obj:`dict` """ return self._query('factsets', **kwargs) def fact_contents(self, **kwargs): """To complement fact_paths(), this endpoint provides the capability to descend into structured facts and retreive the values associated with fact paths. :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function. :returns: A list of dictionaries containg the results. :rtype: :obj:`list` of :obj:`dict` """ return self._query('fact-contents', **kwargs) def fact_paths(self, **kwargs): """Fact Paths are intended to be a counter-part of the fact-names endpoint. It provides increased granularity around structured facts and may be used for building GUI autocompletions or other applications that require a basic top-level view of fact paths. :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function. :returns: A list of dictionaries containg the results. :rtype: :obj:`list` of :obj:`dict` """ return self._query('fact-paths', **kwargs) def resources(self, type_=None, title=None, **kwargs): """Query for resources limited by either type and/or title or query. This will yield a Resources object for every returned resource. :param type_: (Optional) The resource type. This can be any resource type referenced in\ 'https://docs.puppetlabs.com/references/latest/type.html' :type type_: :obj:`string` :param title: (Optional) The name of the resource as declared as the 'namevar' in the Puppet Manifests. This parameter requires the\ `type_` parameter be set. :type title: :obj:`string` :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function :returns: A generator yielding Resources :rtype: :class:`pypuppetdb.types.Resource` """ path = None if type_ is not None: type_ = self._normalize_resource_type(type_) if title is not None: path = '{0}/{1}'.format(type_, title) elif title is None: path = type_ resources = self._query('resources', path=path, **kwargs) for resource in resources: yield Resource( node=resource['certname'], name=resource['title'], type_=resource['type'], tags=resource['tags'], exported=resource['exported'], sourcefile=resource['file'], sourceline=resource['line'], parameters=resource['parameters'], environment=resource['environment'], ) def catalog(self, node): """Get the available catalog for a given node. :param node: (Required) The name of the PuppetDB node. :type: :obj:`string` :returns: An instance of Catalog :rtype: :class:`pypuppetdb.types.Catalog` """ catalogs = self.catalogs(path=node) return next(x for x in catalogs) def catalogs(self, **kwargs): """Get the catalog information from the infrastructure based on path and/or query results. It is strongly recommended to include query and/or paging parameters for this endpoint to prevent large result sets or PuppetDB performance bottlenecks. :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function. :returns: A generator yielding Catalogs :rtype: :class:`pypuppetdb.types.Catalog` """ catalogs = self._query('catalogs', **kwargs) if type(catalogs) == dict: catalogs = [catalogs, ] for catalog in catalogs: yield Catalog(node=catalog['certname'], edges=catalog['edges']['data'], resources=catalog['resources']['data'], version=catalog['version'], transaction_uuid=catalog['transaction_uuid'], environment=catalog['environment'], code_id=catalog.get('code_id'), catalog_uuid=catalog.get('catalog_uuid')) def events(self, **kwargs): """A report is made up of events which can be queried either individually or based on their associated report hash. It is strongly recommended to include query and/or paging parameters for this endpoint to prevent large result sets or PuppetDB performance bottlenecks. :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function :returns: A generator yielding Events :rtype: :class:`pypuppetdb.types.Event` """ events = self._query('events', **kwargs) for event in events: yield Event( node=event['certname'], status=event['status'], timestamp=event['timestamp'], hash_=event['report'], title=event['resource_title'], property_=event['property'], message=event['message'], new_value=event['new_value'], old_value=event['old_value'], type_=event['resource_type'], class_=event['containing_class'], execution_path=event['containment_path'], source_file=event['file'], line_number=event['line'], ) def event_counts(self, summarize_by, **kwargs): """Get event counts from puppetdb. :param summarize_by: (Required) The object type to be counted on. Valid values are 'containing_class', 'resource' and 'certname'. :type summarize_by: :obj:`string` :param count_by: (Optional) The object type that is counted when building the counts of 'successes', 'failures', 'noops' and 'skips'. Support values are 'certname' and 'resource' (default) :type count_by: :obj:`string` :param count_filter: (Optional) A JSON query that is applied to the event-counts output but before the results are aggregated. Supported operators are `=`, `>`, `<`, `>=`, and `<=`. Supported fields are `failures`, `successes`, `noops`, and `skips`. :type count_filter: :obj:`string` :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function. :returns: A list of dictionaries containing the results. :rtype: :obj:`list` """ return self._query('event-counts', summarize_by=summarize_by, **kwargs) def aggregate_event_counts(self, summarize_by, query=None, count_by=None, count_filter=None): """Get event counts from puppetdb aggregated into a single map. :param summarize_by: (Required) The object type to be counted on. Valid values are 'containing_class', 'resource' and 'certname' or any comma-separated value thereof. :type summarize_by: :obj:`string` :param query: (Optional) The PuppetDB query to filter the results. This query is passed to the `events` endpoint. :type query: :obj:`string` :param count_by: (Optional) The object type that is counted when building the counts of 'successes', 'failures', 'noops' and 'skips'. Support values are 'certname' and 'resource' (default) :type count_by: :obj:`string` :param count_filter: (Optional) A JSON query that is applied to the event-counts output but before the results are aggregated. Supported operators are `=`, `>`, `<`, `>=`, and `<=`. Supported fields are `failures`, `successes`, `noops`, and `skips`. :type count_filter: :obj:`string` :returns: A dictionary of name/value results. :rtype: :obj:`dict` """ return self._query('aggregate-event-counts', query=query, summarize_by=summarize_by, count_by=count_by, count_filter=count_filter) def server_time(self): """Get the current time of the clock on the PuppetDB server. :returns: An ISO-8091 formatting timestamp. :rtype: :obj:`string` """ return self._query('server-time')[self.parameters['server_time']] def current_version(self): """Get version information about the running PuppetDB server. :returns: A string representation of the PuppetDB version. :rtype: :obj:`string` """ return self._query('version')['version'] def fact_names(self): """Get a list of all known facts.""" return self._query('fact-names') def metric(self, metric=None, version=None): """Query for a specific metrc. :param metric: The name of the metric we want. :type metric: :obj:`string` :param version: The version of the metric API to query. Valid values: 'v1', 'v2' If not specified, then the value of self.metric_api_version will be used. :type version: :obj:`string` :returns: The return of :meth:`~pypuppetdb.api.BaseAPI._query`. """ version = version if version else self.metric_api_version if version is None or version == 'v2': if metric is None: res = self._query('metrics-list') else: res = self._query('metrics', path=self.escape_metric_name(metric)) if 'error' in res: raise DoesNotComputeError(res['error']) return res['value'] elif version == 'v1': return self._query('mbean', path=metric) else: raise ValueError("Version specified must be 'v1' or 'v2', was given: '{}'" .format(version)) def escape_metric_name(self, metric): """Escapes metric names so they can be used in GET requests as part of the URL. The new (as of v2) metrics API is backed by the Jolokia library. The escpaing rules for Jolokia GET requests can be found here: https://jolokia.org/reference/html/protocol.html#escape-rules :param metric: The name of the metric we want to escape. :type metric: :obj:`string` :returns: The escaped version of the metric name, safe for use in metric GET queries. """ metric = metric.replace('!', r'!!') metric = metric.replace('/', r'!/') metric = metric.replace('"', r'!"') return metric def reports(self, **kwargs): """Get reports for our infrastructure. It is strongly recommended to include query and/or paging parameters for this endpoint to prevent large result sets and potential PuppetDB performance bottlenecks. :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function :returns: A generating yielding Reports :rtype: :class:`pypuppetdb.types.Report` """ reports = self._query('reports', **kwargs) for report in reports: yield Report( api=self, node=report['certname'], hash_=report['hash'], start=report['start_time'], end=report['end_time'], received=report['receive_time'], version=report['configuration_version'], format_=report['report_format'], agent_version=report['puppet_version'], transaction=report['transaction_uuid'], environment=report['environment'], status=report['status'], noop=report.get('noop'), noop_pending=report.get('noop_pending'), metrics=report['metrics']['data'], logs=report['logs']['data'], code_id=report.get('code_id'), catalog_uuid=report.get('catalog_uuid'), cached_catalog_status=report.get('cached_catalog_status') ) def inventory(self, **kwargs): """Get Node and Fact information with an alternative query syntax for structured facts instead of using the facts, fact-contents and factsets endpoints for many fact-related queries. :param \*\*kwargs: The rest of the keyword arguments are passed to the _query function. :returns: A generator yielding Inventory :rtype: :class:`pypuppetdb.types.Inventory` """ inventory = self._query('inventory', **kwargs) for inv in inventory: yield Inventory( node=inv['certname'], time=inv['timestamp'], environment=inv['environment'], facts=inv['facts'], trusted=inv['trusted'] ) def status(self): """Get PuppetDB server status. :returns: A dict with the PuppetDB status information :rtype: :obj:`dict` """ return self._query('status') def command(self, command, payload): return self._cmd(command, payload) pypuppetdb-2.2.0/pypuppetdb/errors.py000066400000000000000000000012661366723304700200110ustar00rootroot00000000000000class APIError(Exception): """Our base exception the other errors inherit from.""" pass class ImproperlyConfiguredError(APIError): """This exception is thrown when the API is initialised and it detects incompatible configuration such as SSL turned on but no certificates provided.""" pass class EmptyResponseError(APIError): """Will be thrown when we did receive a response but the response is empty.""" pass class DoesNotComputeError(APIError): """This error will be thrown when a function is called with an incompatible set of optional parameters. This is the 'you are being a naughty developer, go read the docs' error. """ pass pypuppetdb-2.2.0/pypuppetdb/package.py000066400000000000000000000005641366723304700200700ustar00rootroot00000000000000from __future__ import unicode_literals __title__ = 'pypuppetdb' __version_info__ = (1, 2, 0) # notest __version__ = '.'.join("{0}".format(num) for num in __version_info__) # notest __author__ = 'Daniele Sluijters' __license__ = 'Apache License 2.0' __year__ = '2013, 2014, 2015, 2016, 2017, 2018, 2019' __copyright__ = 'Copyright {0} {1}'.format(__year__, __author__) pypuppetdb-2.2.0/pypuppetdb/types.py000066400000000000000000000700011366723304700176320ustar00rootroot00000000000000from __future__ import absolute_import from __future__ import unicode_literals import logging from pypuppetdb.QueryBuilder import (EqualsOperator) from pypuppetdb.utils import json_to_datetime log = logging.getLogger(__name__) class Event(object): """This object represents an event. Unless otherwise specified all parameters are required. :param node: The hostname of the node this event fired on. :type node: :obj:`string` :param status: The status for the event. :type status: :obj:`string` :param timestamp: A timestamp of when this event occured. :type timestamp: :obj:`string` formatted as ``%Y-%m-%dT%H:%M:%S.%fZ`` :param hash\_: The hash of the report that contains this event. :type hash\_: :obj:`string` :param title: The resource title this event was fired for. :type title: :obj:`string` :param property\_: The property of the resource this event was fired for. :type property\_: :obj:`string` :param message: A message associated with this event. :type message: :obj:`string` :param new_value: The new value/state of the resource. :type new_value: :obj:`string` :param old_value: The old value/state of the resource. :type old_value: :obj:`string` :param type\_: The type of the resource this event fired for. :type type\_: :obj:`string` :param class\_: The class responsible for running this event. :type class\_: :obj:`string` :param execution_path: The path used to reach this particular resource. :type execution_path: :obj:`string` :param source_file: The puppet source code file containing the class. :type source_file: :obj:`string` :param line_number: The line number in the source file containing the definition responsible for triggering this event. :type line_number: :obj:`int` :ivar node: A :obj:`string` of this event's node certname. :ivar status: A :obj:`string` of this event's status. :ivar failed: The :obj:`bool` equivalent of `status`. :ivar timestamp: A :obj:`datetime.datetime` of when this event happend. :ivar node: The hostname of the machine this event\ occured on. :ivar hash\_: The hash of this event. :ivar item: :obj:`dict` with information about the item/resource this\ event was triggered for. """ def __init__(self, node, status, timestamp, hash_, title, property_, message, new_value, old_value, type_, class_, execution_path, source_file, line_number): self.node = node self.status = status if self.status == 'failure': self.failed = True else: self.failed = False self.timestamp = json_to_datetime(timestamp) self.hash_ = hash_ self.item = {'title': title, 'type': type_, 'property': property_, 'message': message, 'old': old_value, 'new': new_value, 'class': class_, 'execution_path': execution_path, 'source_file': source_file, 'line_number': line_number} self.__string = '{0}[{1}]/{2}'.format(self.item['type'], self.item['title'], self.hash_) def __repr__(self): return str('Event: {0}'.format(self.__string)) def __str__(self): return str('{0}').format(self.__string) class Report(object): """This object represents a report. Unless otherwise specified all parameters are required. :param api: API object :type api: :class:`pypuppetdb.api.BaseAPI` :param node: The hostname of the node this report originated on. :type node: :obj:`string` :param hash\_: A string uniquely identifying this report. :type hash\_: :obj:`string` :param start: The start time of the agent run. :type start: :obj:`string` formatted as ``%Y-%m-%dT%H:%M:%S.%fZ`` :param end: The time the agent finished its run. :type end: :obj:`string` formatted as ``%Y-%m-%dT%H:%M:%S.%fZ`` :param received: The time PuppetDB received the report. :type received: :obj:`string` formatted as ``%Y-%m-%dT%H:%M:%S.%fZ`` :param version: The catalog / configuration version. :type version: :obj:`string` :param format\_: The catalog format version. :type format\_: :obj:`int` :param agent_version: The Puppet agent version. :type agent_version: :obj:`string` :param transaction: The UUID of this transaction. :type transaction: :obj:`string` :param environment: (Optional) The environment assigned to the node that\ submitted this report. :type environment: :obj:`string` :param status: (Optional) The status associated to this report's node. :type status: :obj:`string` :param noop: (Default `False`) A flag indicating weather the report was\ produced by a noop run. :type noop: :obj:`bool` :param noop_pending: (Default `False`) A flag indicating weather the \ report had pending changes produced by a noop run. :type noop_pending: :obj:`bool` :param metrics: (Optional) All metrics associated with this report. :type metrics: :obj:`list` containing :obj:`dict` with Metrics :param logs: (Optional) All logs associated with this report. :type logs: :obj:`list` containing :obj:`dict` of logs :param code_id: (Optional) Ties the catalog to the Puppet Code that\ generated the catalog. :type code_id: :obj:`string` :param catalog_uuid: (Optional) Ties the report to the catalog used\ from that Puppet run. :type catalog_uuid: :obj:`string` :param cached_catalog_status: (Optional) Identifies if the Puppet run\ used a cached catalog and weather or not it was used due to an\ error. Can be one of 'explicitly_requested', 'on_failure',\ 'not_used' not 'null'. :type cached_catalog_status: :obj:`string` :param producer: (Optional) The certname of the Puppet Master that\ sent the report to PuppetDB :type producer: :obj:`string` :ivar node: The hostname this report originated from. :ivar hash\_: Unique identifier of this report. :ivar start: :obj:`datetime.datetime` when the Puppet agent run started. :ivar end: :obj:`datetime.datetime` when the Puppet agent run ended. :ivar received: :obj:`datetime.datetime` when the report finished\ uploading. :ivar version: :obj:`string` catalog configuration version. :ivar format\_: :obj:`int` catalog format version. :ivar agent_version: :obj:`string` Puppet Agent version. :ivar run_time: :obj:`datetime.timedelta` of **end** - **start**. :ivar transaction: UUID identifying this transaction. :ivar environment: The environment assigned to the node that submitted\ this report. :ivar status: The status associated to this report's node. :ivar metrics: :obj:`list` containing :obj:`dict` of all metrics\ associated with this report. :ivar logs: :obj:`list` containing :obj:`dict` of all logs\ associated with this report. :ivar code_id: :obj:`string` used to tie a catalog to the Puppet Code\ which generated the catalog. :ivar catalog_uuid: :obj:`string` used to tie this report to the catalog\ used on this Puppet run. :ivar cached_catalog_status: :obj:`string` identifying if this Puppet run\ used a cached catalog, if so weather it was a result of an error or\ otherwise. :ivar producer: :obj:`string` representing the certname of the Puppet\ Master that sent the report to PuppetDB """ def __init__(self, api, node, hash_, start, end, received, version, format_, agent_version, transaction, status=None, metrics={}, logs={}, environment=None, noop=False, noop_pending=False, code_id=None, catalog_uuid=None, cached_catalog_status=None, producer=None): self.node = node self.hash_ = hash_ self.start = json_to_datetime(start) self.end = json_to_datetime(end) self.received = json_to_datetime(received) self.version = version self.format_ = format_ self.agent_version = agent_version self.run_time = self.end - self.start self.transaction = transaction self.environment = environment self.status = 'noop' if noop and noop_pending else status self.metrics = metrics self.logs = logs self.code_id = code_id self.catalog_uuid = catalog_uuid self.cached_catalog_status = cached_catalog_status self.producer = producer self.__string = '{0}'.format(self.hash_) self.__api = api def __repr__(self): return str('Report: {0}'.format(self.__string)) def __str__(self): return str('{0}').format(self.__string) def events(self, **kwargs): """Get all events for this report. Additional arguments may also be specified that will be passed to the query function. """ return self.__api.events(query=EqualsOperator("report", self.hash_), **kwargs) class Fact(object): """This object represents a fact. Unless otherwise specified all parameters are required. :param node: The hostname this fact was collected from. :type node: :obj:`string` :param name: The fact's name, such as 'osfamily' :type name: :obj:`string` :param value: The fact's value, such as 'Debian' :type value: :obj:`string` or :obj:`int` or :obj:`dict` :param environment: (Optional) The fact's environment, such as\ 'production' :type environment: :obj:`string` :ivar node: :obj:`string` holding the hostname. :ivar name: :obj:`string` holding the fact's name. :ivar value: :obj:`string` or :obj:`int` or :obj:`dict` holding the\ fact's value. :ivar environment: :obj:`string` holding the fact's environment """ def __init__(self, node, name, value, environment=None): self.node = node self.name = name self.value = value self.environment = environment self.__string = '{0}/{1}'.format(self.name, self.node) def __repr__(self): return str('Fact: {0}'.format(self.__string)) def __str__(self): return str('{0}').format(self.__string) class Resource(object): """This object represents a resource. Unless otherwise specified all parameters are required. :param node: The hostname this resource is located on. :type noode: :obj:`string` :param name: The name of the resource in the Puppet manifest. :type name: :obj:`string` :param type\_: Type of the Puppet resource. :type type\_: :obj:`string` :param tags: Tags associated with this resource. :type tags: :obj:`list` :param exported: If it's an exported resource. :type exported: :obj:`bool` :param sourcefile: The Puppet manifest this resource is declared in. :type sourcefile: :obj:`string` :param sourceline: The line this resource is declared at. :type sourceline: :obj:`int` :param parameters: (Optional) The parameters this resource has been\ declared with. :type parameters: :obj:`dict` :param environment: (Optional) The environment of the node associated\ with this resource. :type environment: :obj:`string` :ivar node: The hostname this resources is located on. :ivar name: The name of the resource in the Puppet manifest. :ivar type\_: The type of Puppet resource. :ivar exported: :obj:`bool` if the resource is exported. :ivar sourcefile: The Puppet manifest this resource is declared in. :ivar sourceline: The line this resource is declared at. :ivar parameters: :obj:`dict` with key:value pairs of parameters. :ivar relationships: :obj:`list` Contains all relationships to other\ resources :ivar environment: :obj:`string` The environment of the node associated\ with this resource. """ def __init__(self, node, name, type_, tags, exported, sourcefile, sourceline, environment=None, parameters={}): self.node = node self.name = name self.type_ = type_ self.tags = tags self.exported = exported self.sourcefile = sourcefile self.sourceline = sourceline self.parameters = parameters self.relationships = [] self.environment = environment self.__string = '{0}[{1}]'.format(self.type_, self.name) def __repr__(self): return str('').format(self.__string) def __str__(self): return str('{0}').format(self.__string) class Node(object): """This object represents a node. It additionally has some helper methods so that you can query for resources or facts directly from the node scope. Unless otherwise specified all parameters are required. :param api: API object. :type api: :class:`pypuppetdb.api.BaseAPI` :param name: Hostname of this node. :type name: :obj:`string` :param deactivated: (default `None`) Time this node was deactivated at. :type deactivated: :obj:`string` formatted as ``%Y-%m-%dT%H:%M:%S.%fZ`` :param report_timestamp: (default `None`) Time of the last report. :type report_timestamp: :obj:`string` formatted as\ ``%Y-%m-%dT%H:%M:%S.%fZ`` :param catalog_timestamp: (default `None`) Time the last time a catalog\ was compiled. :type catalog_timestamp: :obj:`string` formatted as\ ``%Y-%m-%dT%H:%M:%S.%fZ`` :param facts_timestamp: (default `None`) Time the last time facts were\ collected. :type facts_timestamp: :obj:`string` formatted as\ ``%Y-%m-%dT%H:%M:%S.%fZ`` :param status_report: (default `None`) Status of latest report \ from the node changed | unchanged | failed :type status: :obj:`string` :param noop: (Default `False`) A flag indicating whether the latest \ report of the node was produced by a noop run. :type noop: :obj:`bool` :param noop_pending: (Default `False`) A flag indicating whether \ the latest report of the node had pending changes \ produced by a noop run. :type noop_pending: :obj:`bool` :param events: (default `None`) Counted events from latest Report :type events: :obj:`dict` :param unreported: (default `False`) if node is considered unreported :type unreported_time: :obj:`bool` :param unreported_time: (default `None`) Time since last report :type unreported_time: :obj:`string` :param report_environment: (default 'production') The environment of the\ last received report for this node :type report_environment: :obj:`string` :param catalog_environment: (default 'production') The environment of the\ last received catalog for this node :type catalog_environment: :obj:`string` :param facts_environment: (default 'production') The environment of the\ last received fact set for this node :type facts_environment: :obj:`string` :param latest_report_hash: The hash of the latest report from this node,\ is only available in PuppetDB 3.2 and later :type latest_report_hash: :obj:`string` :param cached_catalog_status: Cached catalog status of the last puppet run\ on this node, possible values are 'explicitly_requested',\ 'on_failure', 'not_used' or None. :type cache_catalog_status: :obj:`string` :ivar name: Hostname of this node. :ivar deactivated: :obj:`datetime.datetime` when this host was\ deactivated or `False`. :ivar report_timestamp: :obj:`datetime.datetime` when the last run\ occured or `None`. :ivar catalog_timestamp: :obj:`datetime.datetime` last time a catalog was\ compiled or `None`. :ivar facts_timestamp: :obj:`datetime.datetime` last time when facts were\ collected or `None`. :ivar report_environment: :obj:`string` the environment of the last\ received report for this node. :ivar catalog_environment: :obj:`string` the environment of the last\ received catalog for this node. :ivar facts_environment: :obj:`string` the environment of the last\ received fact set for this node. :ivar latest_report_hash: :obj:`string` the hash value of the latest\ report the current node reported. Available in PuppetDB 3.2\ and later. :ivar cached_catalog_status: :obj:`string` the status of the cached\ catalog from the last puppet run. """ def __init__(self, api, name, deactivated=None, expired=None, report_timestamp=None, catalog_timestamp=None, facts_timestamp=None, status_report=None, noop=False, noop_pending=False, events=None, unreported=False, unreported_time=None, report_environment='production', catalog_environment='production', facts_environment='production', latest_report_hash=None, cached_catalog_status=None): self.name = name self.events = events self.unreported_time = unreported_time self.report_timestamp = report_timestamp self.catalog_timestamp = catalog_timestamp self.facts_timestamp = facts_timestamp self.report_environment = report_environment self.catalog_environment = catalog_environment self.facts_environment = facts_environment self.latest_report_hash = latest_report_hash self.cached_catalog_status = cached_catalog_status if unreported: self.status = 'unreported' elif noop and noop_pending: self.status = 'noop' else: self.status = status_report if deactivated is not None: self.deactivated = json_to_datetime(deactivated) else: self.deactivated = False if expired is not None: self.expired = json_to_datetime(expired) else: self.expired = False if report_timestamp is not None: self.report_timestamp = json_to_datetime(report_timestamp) else: self.report_timestamp = report_timestamp if facts_timestamp is not None: self.facts_timestamp = json_to_datetime(facts_timestamp) else: self.facts_timestamp = facts_timestamp if catalog_timestamp is not None: self.catalog_timestamp = json_to_datetime(catalog_timestamp) else: self.catalog_timestamp = catalog_timestamp self.__api = api self.__string = self.name def __repr__(self): return str('').format(self.__string) def __str__(self): return str('{0}').format(self.__string) def facts(self, **kwargs): """Get all facts of this node. Additional arguments may also be specified that will be passed to the query function. """ return self.__api.facts(query=EqualsOperator("certname", self.name), **kwargs) def fact(self, name): """Get a single fact from this node.""" facts = self.facts(name=name) return next(fact for fact in facts) def resources(self, type_=None, title=None, **kwargs): """Get all resources of this node or all resources of the specified type. Additional arguments may also be specified that will be passed to the query function. """ if type_ is None: resources = self.__api.resources( query=EqualsOperator("certname", self.name), **kwargs) elif type_ is not None and title is None: resources = self.__api.resources( type_=type_, query=EqualsOperator("certname", self.name), **kwargs) else: resources = self.__api.resources( type_=type_, title=title, query=EqualsOperator("certname", self.name), **kwargs) return resources def resource(self, type_, title, **kwargs): """Get a resource matching the supplied type and title. Additional arguments may also be specified that will be passed to the query function. """ resources = self.__api.resources( type_=type_, title=title, query=EqualsOperator("certname", self.name), **kwargs) return next(resource for resource in resources) def reports(self, **kwargs): """Get all reports for this node. Additional arguments may also be specified that will be passed to the query function. """ return self.__api.reports( query=EqualsOperator("certname", self.name), **kwargs) class Catalog(object): """ This object represents a compiled catalog from puppet. It contains\ Resource and Edge object that represent the dependency graph. Unless\ otherwise specified all parameters are required. :param node: Name of the host :type node: :obj:`string` :param edges: Edges returned from Catalog data :type edges: :obj:`list` containing :obj:`dict` of\ :class:`pypuppetdb.types.Edge` :param resources: :class:`pypuppetdb.types.Resource` managed as of this\ Catalog. :type resources: :obj:`dict` of :class:`pypuppetdb.types.Resource` :param version: Catalog version from Puppet (unique for each node) :type version: :obj:`string` :param transaction_uuid: A string used to match the catalog with the\ corresponding report that was issued during the same puppet run :type transaction_uuid: :obj:`string` :param environment: The environment associated with the catalog's\ certname. :type environment: :obj:`string` :param code_id: The string used to tie this catalog to the Puppet code\ which generated the catalog. :type code_id: :obj:`string` :param catalog_uuid: Universally unique identifier of this catalog. :type catalog_uuid: :obj:`string` :param producer: The certname of the Puppet Master that sent the catalog\ to PuppetDB :type producer: :obj:`string` :ivar node: :obj:`string` Name of the host :ivar version: :obj:`string` Catalog version from Puppet (unique for each node) :ivar transaction_uuid: :obj:`string` used to match the catalog with corresponding report :ivar edges: :obj:`list` of :obj:`Edge` The source Resource object\ of the relationship :ivar resources: :obj:`dict` of :class:`pypuppetdb.types.Resource` The\ source Resource object of the relationship :ivar environment: :obj:`string` Environment associated with the catalog's certname :ivar code_id: :obj:`string` ties the catalog to the Puppet code that\ generated the catalog :ivar catalog_uuid: :obj:`string` uniquely identifying this catalog. :ivar producer: :obj:`string` of the Puppet Master that sent the catalog\ to PuppetDB """ def __init__(self, node, edges, resources, version, transaction_uuid, environment=None, code_id=None, catalog_uuid=None, producer=None): self.node = node self.version = version self.transaction_uuid = transaction_uuid self.environment = environment self.code_id = code_id self.catalog_uuid = catalog_uuid self.producer = producer self.resources = dict() for resource in resources: if 'file' not in resource: resource['file'] = None if 'line' not in resource: resource['line'] = None identifier = resource['type'] + '[' + resource['title'] + ']' res = Resource(node=node, name=resource['title'], type_=resource['type'], tags=resource['tags'], exported=resource['exported'], sourcefile=resource['file'], sourceline=resource['line'], parameters=resource['parameters'], environment=self.environment) self.resources[identifier] = res self.edges = [] for edge in edges: identifier_source = edge['source_type'] + '[' + edge['source_title'] + ']' identifier_target = edge['target_type'] + '[' + edge['target_title'] + ']' e = Edge(source=self.resources[identifier_source], target=self.resources[identifier_target], relationship=edge['relationship'], node=self.node) self.edges.append(e) self.resources[identifier_source].relationships.append(e) self.resources[identifier_target].relationships.append(e) self.__string = '{0}/{1}'.format(self.node, self.transaction_uuid) def __repr__(self): return str('').format(self.__string) def __str__(self): return str('{0}').format(self.__string) def get_resources(self): return self.resources.values() def get_resource(self, resource_type, resource_title): identifier = resource_type + \ '[' + resource_title + ']' return self.resources[identifier] def get_edges(self): return iter(self.edges) class Edge(object): """ This object represents the connection between two Resource objects. Unless otherwise specified all parameters are required. :param source: The source Resource object of the relationship :type source: :class:`pypuppetdb.Resource` :param target: The target Resource object of the relationship :type target: :class:`pypuppetdb.Resource` :param relaptionship: Name of the Puppet Ressource Relationship :type relationship: :obj:`string` :param node: The certname of the node that owns this Relationship :type node: :obj:`string` :ivar source: :obj:`Resource` The source Resource object :ivar target: :obj:`Resource` The target Resource object :ivar relationship: :obj:`string` Name of the Puppet Resource relationship :ivar node: :obj:`string` The name of the node that owns this relationship """ def __init__(self, source, target, relationship, node=None): self.source = source self.target = target self.relationship = relationship self.node = node self.__string = '{0} - {1} - {2}'.format(self.source, self.relationship, self.target) def __repr__(self): return str('').format(self.__string) def __str__(self): return str('{0}').format(self.__string) class Inventory(object): """This object represents a Node Inventory entry returned from the Inventory endpoint. :param node: The certname of the node associated with the inventory. :type node: :obj:`string` :param time: The time at which PuppetDB received the facts in the inventory. :type time: :obj:`string` formatted as ``%Y-%m-%dT%H:%M:%S.%fZ`` :param environment: The environment associated with the inventory's certname. :type environment: :obj:`string` :param facts: The dictionary of key-value pairs for the nodes assosciated facts. :type facts: :obj:`dict` :param trusted: The trusted data from the node. :type trusted: :obj:`dict` :ivar node: The certname of the node associated with the inventory. :ivar time: The time at which PuppetDB received the facts in the inventory. :ivar environment: The environment associated with the inventory's certname. :ivar facts: The dictionary of key-value pairs for the nodes assosciated facts. :ivar trusted: The trusted data from the node. """ def __init__(self, node, time, environment, facts, trusted): self.node = node self.time = json_to_datetime(time) self.environment = environment self.facts = facts self.trusted = trusted self.__string = self.node def __repr__(self): return str('').format(self.__string) def __str__(self): return str("{0}").format(self.__string) pypuppetdb-2.2.0/pypuppetdb/utils.py000066400000000000000000000034371366723304700176370ustar00rootroot00000000000000from __future__ import absolute_import from __future__ import unicode_literals import datetime import re # A UTC class, see: # http://docs.python.org/2/library/datetime.html#tzinfo-objects class UTC(datetime.tzinfo): """UTC""" def utcoffset(self, dt): return datetime.timedelta(0) def tzname(self, dt): return str('UTC') def dst(self, dt): return datetime.timedelta(0) def __repr__(self): return str('') def __str__(self): return str('UTC') def __unicode__(self): return 'UTC' def json_to_datetime(date): """Tranforms a JSON datetime string into a timezone aware datetime object with a UTC tzinfo object. :param date: The datetime representation. :type date: :obj:`string` :returns: A timezone aware datetime object. :rtype: :class:`datetime.datetime` """ return datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%fZ').replace( tzinfo=UTC()) def versioncmp(v1, v2): """Compares two objects, x and y, and returns an integer according to the outcome. The return value is negative if x < y, zero if x == y and positive if x > y. :param v1: The first object to compare. :param v2: The second object to compare. :returns: -1, 0 or 1. :rtype: :obj:`int` """ def normalize(v): """Removes leading zeroes from right of a decimal point from v and returns an array of values separated by '.' :param v: The data to normalize. :returns: An list representation separated by '.' with all leading zeroes stripped. :rtype: :obj:`list` """ return [int(x) for x in re.sub(r'(\.0+)*$', '', v).split(".")] return (normalize(v1) > normalize(v2)) - ( normalize(v1) < normalize(v2)) pypuppetdb-2.2.0/pytest.ini000066400000000000000000000005121366723304700157510ustar00rootroot00000000000000[pytest] filterwarnings = ignore::DeprecationWarning ignore::PendingDeprecationWarning markers = pep8: workaround for https://bitbucket.org/pytest-dev/pytest-pep8/issues/23/ pep8maxlinelength = 100 addopts = --cov=pypuppetdb --cov-report=term-missing norecursedirs = docs .tox venv .eggs lib python_files = tests/*.py pypuppetdb-2.2.0/python-pypuppetdb.spec000066400000000000000000000027161366723304700203170ustar00rootroot00000000000000%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} Name: python-pypuppetdb Version: 2.2.0 Release: 2%{?dist} Summary: A Python puppetdb API Group: Development/Languages License: Apache URL: https://github.com/nedap/pypuppetdb Source0: http://pypi.python.org/packages/source/p/pypuppetdb/pypuppetdb-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release} BuildArch: noarch BuildRequires: python-setuptools Requires: python-requests >= 1.1.0 Requires: python >= 2.6 %description pypuppetdb is a library to work with PuppetDB's REST API. It is implemented using the requests library. This library is a thin wrapper around the REST API providing some convinience functions and objects to request and hold data from PuppetDB. To use this library you will need: Python 2.6 or 2.7 or Python 3.3. %prep %setup -q -n pypuppetdb-%{version} %{__rm} -rf *.egg-info %{__sed} -i 's,^#!.*env python.*$,#!/usr/bin/python,' setup.py %build %install rm -rf %{buildroot} %{__python} setup.py install -O1 --root %{buildroot} %clean rm -rf %{buildroot} %files %defattr(-,root,root,-) %doc %{python_sitelib}/* %changelog * Tue Oct 15 2013 Klavs Klavsen - 0.0.4-2 - Add requirements, description and other small cleanups. * Mon Oct 14 2013 Klavs Klavsen - 0.0.4-1 - Initial release. pypuppetdb-2.2.0/requirements-test.txt000066400000000000000000000002061366723304700201610ustar00rootroot00000000000000-r requirements.txt coveralls bandit coverage pep8 mock pytest pytest-cov pytest-pep8 pytest-mock pytest-mypy cov-core mypy httpretty pypuppetdb-2.2.0/requirements.txt000066400000000000000000000000211366723304700171770ustar00rootroot00000000000000requests>=2.22.0 pypuppetdb-2.2.0/setup.cfg000066400000000000000000000006031366723304700155420ustar00rootroot00000000000000[pep8] max-line-length=100 exclude=venv,dist,build ignore=E402 [nosetests] with-coverage = 1 with-xunit = 1 cover-package = pypuppetdb [flake8] exclude=venv [mypy] python_version = 3.8 ignore_missing_imports=True ignore_errors=False pretty=True [mypy-setup] ignore_errors = True [bdist_wheel] universal = 1 [bdist_rpm] build_requires = python-setuptools requires = python-requests pypuppetdb-2.2.0/setup.py000066400000000000000000000044751366723304700154460ustar00rootroot00000000000000import codecs import os import sys from setuptools import find_packages, setup from setuptools.command.test import test as TestCommand def rc_value(): with open('version') as fp: val = fp.read().rstrip() return '{}rc0'.format(val) def version(): return os.getenv('TRAVIS_TAG', rc_value()) class PyTest(TestCommand): user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")] def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = '--cov=pypuppetdb --cov-report=term-missing' def run_tests(self): import shlex import pytest errno = pytest.main(shlex.split(self.pytest_args)) sys.exit(errno) with codecs.open('README.rst', encoding='utf-8') as f: README = f.read() with codecs.open('CHANGELOG.rst', encoding='utf-8') as f: CHANGELOG = f.read() requirements = None with open('requirements.txt', 'r') as f: requirements = [line.rstrip() for line in f.readlines() if not line.startswith('-')] requirements_test = None with open('requirements-test.txt', 'r') as f: requirements_test = [line.rstrip() for line in f.readlines() if not line.startswith('-')] setup( name='pypuppetdb', version=version(), author='Vox Pupuli', author_email='voxpupuli@groups.io', packages=find_packages(), url='https://github.com/voxpupuli/pypuppetdb', license='Apache License 2.0', description='Library for working with the PuppetDB REST API.', long_description='\n'.join((README, CHANGELOG)), long_description_content_type='text/x-rst', keywords='puppet puppetdb', tests_require=requirements_test, data_files=[('requirements_for_tests', ['requirements-test.txt'])], cmdclass={'test': PyTest}, install_requires=requirements, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Natural Language :: English', 'License :: OSI Approved :: Apache Software License', 'Operating System :: POSIX', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Libraries' ], ) pypuppetdb-2.2.0/tests/000077500000000000000000000000001366723304700150645ustar00rootroot00000000000000pypuppetdb-2.2.0/tests/test_baseapi.py000066400000000000000000000643311366723304700201100ustar00rootroot00000000000000import base64 import json import mock import httpretty import pytest import requests import pypuppetdb def stub_request(url, data=None, method=httpretty.GET, status=200, **kwargs): if data is None: body = '[]' else: with open(data, 'r') as d: body = json.load(d.read()) return httpretty.register_uri(method, url, body=body, status=status, **kwargs) @pytest.fixture(params=['string', 'QueryBuilder']) def query(request): key = 'certname' value = 'node1' if request.param == 'string': return '["{0}", "=", "{1}"]'.format(key, value) elif request.param == 'QueryBuilder': return pypuppetdb.QueryBuilder.EqualsOperator(key, value) class TestBaseAPIVersion(object): def test_init_defaults(self): v4 = pypuppetdb.api.BaseAPI() assert v4.api_version == 'v4' class TestBaseAPIInitOptions(object): def test_defaults(self, baseapi): assert baseapi.host == 'localhost' assert baseapi.port == 8080 assert baseapi.ssl_verify is True assert baseapi.ssl_key is None assert baseapi.ssl_cert is None assert baseapi.timeout == 10 assert baseapi.token is None assert baseapi.protocol == 'http' assert baseapi.url_path == '' assert baseapi.username is None assert baseapi.password is None assert baseapi.metric_api_version is 'v2' def test_host(self): api = pypuppetdb.api.BaseAPI(host='127.0.0.1') assert api.host == '127.0.0.1' def test_port(self): api = pypuppetdb.api.BaseAPI(port=8081) assert api.port == 8081 def test_ssl_verify(self): api = pypuppetdb.api.BaseAPI(ssl_verify=False) assert api.ssl_verify is False assert api.protocol == 'http' def test_token(self): test_token = 'tokenstring' # nosec api = pypuppetdb.api.BaseAPI(token=test_token) assert api.token == test_token assert api.protocol == 'https' def test_ssl_key(self): api = pypuppetdb.api.BaseAPI(ssl_key='/a/b/c.pem') assert api.ssl_key == '/a/b/c.pem' assert api.protocol == 'http' def test_ssl_cert(self): api = pypuppetdb.api.BaseAPI(ssl_cert='/d/e/f.pem') assert api.ssl_cert == '/d/e/f.pem' assert api.protocol == 'http' def test_ssl_key_and_cert(self): api = pypuppetdb.api.BaseAPI(ssl_cert='/d/e/f.pem', ssl_key='/a/b/c.pem') assert api.ssl_key == '/a/b/c.pem' assert api.ssl_cert == '/d/e/f.pem' assert api.protocol == 'https' def test_timeout(self): api = pypuppetdb.api.BaseAPI(timeout=20) assert api.timeout == 20 def test_protocol(self): api = pypuppetdb.api.BaseAPI(protocol='https') assert api.protocol == 'https' def test_uppercase_protocol(self): api = pypuppetdb.api.BaseAPI(protocol='HTTP') assert api.protocol == 'http' def test_override_protocol(self): api = pypuppetdb.api.BaseAPI(protocol='http', ssl_cert='/d/e/f.pem', ssl_key='/a/b/c.pem') assert api.protocol == 'http' def test_invalid_protocol(self): with pytest.raises(ValueError): api = pypuppetdb.api.BaseAPI(protocol='ftp') def test_url_path(self): api = pypuppetdb.api.BaseAPI(url_path='puppetdb') assert api.url_path == '/puppetdb' def test_url_path_leading_slash(self): api = pypuppetdb.api.BaseAPI(url_path='/puppetdb') assert api.url_path == '/puppetdb' def test_url_path_trailing_slash(self): api = pypuppetdb.api.BaseAPI(url_path='puppetdb/') assert api.url_path == '/puppetdb' def test_url_path_longer_with_both_slashes(self): api = pypuppetdb.api.BaseAPI(url_path='/puppet/db/') assert api.url_path == '/puppet/db' def test_username(self): api = pypuppetdb.api.BaseAPI(username='puppetdb') assert api.username is None assert api.password is None def test_password(self): api = pypuppetdb.api.BaseAPI(password='password123') # nosec assert api.username is None assert api.password is None def test_username_and_password(self): api = pypuppetdb.api.BaseAPI(username='puppetdb', # nosec password='password123') assert api.username == 'puppetdb' assert api.password == 'password123' def test_metric_api_version_v1(self): api = pypuppetdb.api.BaseAPI(metric_api_version='v1') assert api.metric_api_version == 'v1' def test_metric_api_version_v2(self): api = pypuppetdb.api.BaseAPI(metric_api_version='v2') assert api.metric_api_version == 'v2' def test_metric_api_version_invalid_raises(self): with pytest.raises(ValueError): pypuppetdb.api.BaseAPI(metric_api_version='bad') class TestBaseAPIProperties(object): def test_version(self, baseapi): assert baseapi.version == 'v4' def test_base_url(self, baseapi): assert baseapi.base_url == 'http://localhost:8080' def test_base_url_ssl(self, baseapi): baseapi.protocol = 'https' # slightly evil assert baseapi.base_url == 'https://localhost:8080' def test_total(self, baseapi): baseapi.last_total = 10 # slightly evil assert baseapi.total == 10 class TestBaseAPIURL(object): def test_without_path(self, baseapi): assert baseapi._url('nodes') == \ 'http://localhost:8080/pdb/query/v4/nodes' def test_with_invalid_endpoint(self, baseapi): with pytest.raises(pypuppetdb.errors.APIError): baseapi._url('this_will-Never+Ex1s7') def test_with_path(self, baseapi): url = baseapi._url('nodes', path='node1.example.com') assert url == \ 'http://localhost:8080/pdb/query/v4/nodes/node1.example.com' def test_quote(self, baseapi): url = baseapi._url("facts", path="macaddress/02:42:ec:94:80:f0") assert url == \ 'http://localhost:8080/pdb/query/v4/' \ + 'facts/macaddress/02%3A42%3Aec%3A94%3A80%3Af0' class TestAPIQuery(object): @mock.patch.object(requests.Session, 'request') def test_timeout(self, get, baseapi): get.side_effect = requests.exceptions.Timeout with pytest.raises(requests.exceptions.Timeout): baseapi._query('nodes') with pytest.raises(requests.exceptions.Timeout): baseapi._cmd('deactivate node', {'certname': ''}) @mock.patch.object(requests.Session, 'request') def test_connectionerror(self, get, baseapi): get.side_effect = requests.exceptions.ConnectionError with pytest.raises(requests.exceptions.ConnectionError): baseapi._query('nodes') with pytest.raises(requests.exceptions.ConnectionError): baseapi._cmd('deactivate node', {'certname': ''}) @mock.patch.object(requests.Session, 'request') def test_httperror(self, get, baseapi): get.side_effect = requests.exceptions.HTTPError( response=requests.Response()) with pytest.raises(requests.exceptions.HTTPError): baseapi._query('nodes') with pytest.raises(requests.exceptions.HTTPError): baseapi._cmd('deactivate node', {'certname': ''}) def test_setting_headers_without_token(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes') # need to query some endpoint request_headers = dict(httpretty.last_request().headers) assert request_headers['accept'] == 'application/json' assert request_headers['content-type'] == 'application/json' assert request_headers['accept-charset'] == 'utf-8' host_val = request_headers.get('host', request_headers.get('Host')) assert host_val == 'localhost:8080' assert httpretty.last_request().path == '/pdb/query/v4/nodes' httpretty.disable() httpretty.reset() def test_setting_headers_with_token(self, token_baseapi): httpretty.enable() stub_request('https://localhost:8080/pdb/query/v4/nodes') token_baseapi._query('nodes') # need to query some endpoint request_headers = dict(httpretty.last_request().headers) print(request_headers) assert request_headers['accept'] == 'application/json' assert request_headers['content-type'] == 'application/json' assert request_headers['accept-charset'] == 'utf-8' host_val = request_headers.get('host', request_headers.get('Host')) assert host_val == 'localhost:8080' x_val = request_headers.get('X-Authentication', request_headers.get('x-authentication')) assert x_val == 'tokenstring' assert httpretty.last_request().path == '/pdb/query/v4/nodes' httpretty.disable() httpretty.reset() def test_with_path(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes/node1') baseapi._query('nodes', path='node1') assert httpretty.last_request().path == '/pdb/query/v4/nodes/node1' httpretty.disable() httpretty.reset() def test_with_url_path(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/puppetdb/pdb/query/v4/nodes') baseapi.url_path = '/puppetdb' baseapi._query('nodes') assert httpretty.last_request().path == '/puppetdb/pdb/query/v4/nodes' httpretty.disable() httpretty.reset() def test_with_password_authorization(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi.username = 'puppetdb' baseapi.password = 'password123' baseapi._query('nodes') assert httpretty.last_request().path == '/pdb/query/v4/nodes' encoded_cred = 'puppetdb:password123'.encode('utf-8') bs_authheader = base64.b64encode(encoded_cred).decode('utf-8') assert httpretty.last_request().headers['Authorization'] == \ 'Basic {0}'.format(bs_authheader) httpretty.disable() httpretty.reset() def test_with_token_authorization(self, token_baseapi): httpretty.enable() stub_request('https://localhost:8080/pdb/query/v4/nodes') token_baseapi._query('nodes') assert httpretty.last_request().path == '/pdb/query/v4/nodes' assert httpretty.last_request().headers['X-Authentication'] == \ 'tokenstring' def test_with_query(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes', query='["certname", "=", "node1"]') assert httpretty.last_request().querystring == { 'query': ['["certname", "=", "node1"]']} httpretty.disable() httpretty.reset() def test_with_order(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes', order_by='ted') assert httpretty.last_request().querystring == { 'order_by': ['ted']} httpretty.disable() httpretty.reset() def test_with_limit(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes', limit=1) assert httpretty.last_request().querystring == { 'limit': ['1']} httpretty.disable() httpretty.reset() def test_with_include_total(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes', include_total=True) assert httpretty.last_request().querystring == { 'include_total': ['true']} httpretty.disable() httpretty.reset() def test_with_offset(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes', offset=1) assert httpretty.last_request().querystring == { 'offset': ['1']} httpretty.disable() httpretty.reset() def test_with_summarize_by(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes', summarize_by=1) assert httpretty.last_request().querystring == { 'summarize_by': ['1']} httpretty.disable() httpretty.reset() def test_with_count_by(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes', count_by=1) assert httpretty.last_request().querystring == { 'count_by': ['1']} httpretty.disable() httpretty.reset() def test_with_count_filter(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes', count_filter=1) assert httpretty.last_request().querystring == { 'counts_filter': ['1']} httpretty.disable() httpretty.reset() def test_with_payload_get(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') baseapi._query('nodes', payload={'foo': 'bar'}, count_by=1) assert httpretty.last_request().querystring == { 'foo': ['bar'], 'count_by': ['1']} httpretty.disable() httpretty.reset() def test_with_payload_post(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes', method=httpretty.POST) baseapi._query('nodes', payload={'foo': 'bar'}, count_by=1, request_method='POST') assert httpretty.last_request().body == json.dumps({'foo': 'bar', 'count_by': 1}).encode("latin-1") httpretty.disable() httpretty.reset() def test_response_empty(self, baseapi): httpretty.enable() httpretty.register_uri(httpretty.GET, 'http://localhost:8080/pdb/query/v4/nodes', body=json.dumps(None)) with pytest.raises(pypuppetdb.errors.EmptyResponseError): baseapi._query('nodes') def test_response_x_records(self, baseapi): httpretty.enable() httpretty.register_uri(httpretty.GET, 'http://localhost:8080/pdb/query/v4/nodes', adding_headers={ 'X-Records': 256}, body='[]', ) baseapi._query('nodes', include_total=True) assert baseapi.total == 256 def test_query_bad_request_type(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes') with pytest.raises(pypuppetdb.errors.APIError): baseapi._query('nodes', query='["certname", "=", "node1"]', request_method='DELETE') httpretty.disable() httpretty.reset() def test_query_with_post(self, baseapi, query): httpretty.reset() httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/nodes', method=httpretty.POST) baseapi._query('nodes', query=query, count_by=1, request_method='POST') last_request = httpretty.last_request() assert last_request.querystring == {} assert last_request.headers['Content-Type'] == 'application/json' assert last_request.method == 'POST' assert last_request.body == json.dumps({'query': str(query), 'count_by': 1}).encode("latin-1") httpretty.disable() httpretty.reset() def test_cmd(self, baseapi, query): httpretty.reset() httpretty.enable() stub_request('http://localhost:8080/pdb/cmd/v1', method=httpretty.POST) node_name = 'testnode' baseapi._cmd('deactivate node', {'certname': node_name}) last_request = httpretty.last_request() assert last_request.querystring == { "certname": [node_name], "command": ['deactivate node'], "version": ['3'], "checksum": ['b93d474970e54943aec050ee399dfb85d21e143a'] } assert last_request.headers['Content-Type'] == 'application/json' assert last_request.method == 'POST' assert last_request.body == json.dumps({'certname': node_name}).encode("latin-1") httpretty.disable() httpretty.reset() def test_cmd_bad_command(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/cmd/v1') with pytest.raises(pypuppetdb.errors.APIError): baseapi._cmd('incorrect command', {}) httpretty.disable() httpretty.reset() def test_cmd_with_token_authorization(self, token_baseapi): httpretty.enable() stub_request('https://localhost:8080/pdb/cmd/v1', method=httpretty.POST) token_baseapi._cmd('deactivate node', {'certname': ''}) assert httpretty.last_request().path.startswith('/pdb/cmd/v1') assert httpretty.last_request().headers['X-Authentication'] == \ 'tokenstring' class TestAPIMethods(object): def test_metric_v1(self, baseapi): httpretty.enable() httpretty.enable() stub_request('http://localhost:8080/metrics/v1/mbeans/test') baseapi.metric('test', version='v1') assert httpretty.last_request().path == '/metrics/v1/mbeans/test' def test_metric_v1_list(self, baseapi): httpretty.enable() httpretty.enable() stub_request('http://localhost:8080/metrics/v1/mbeans') baseapi.metric(version='v1') assert httpretty.last_request().path == '/metrics/v1/mbeans' def test_metric_v1_version_constructor(self): api = pypuppetdb.api.BaseAPI(metric_api_version='v1') httpretty.enable() httpretty.enable() stub_request('http://localhost:8080/metrics/v1/mbeans/test') api.metric('test') assert httpretty.last_request().path == '/metrics/v1/mbeans/test' def test_metric_v2(self, baseapi): metrics_body = { 'request': { 'mbean': 'test:name=Num', 'type': 'read' }, 'value': { 'Value': 0 }, 'timestamp': 0, 'status': 200 } httpretty.enable() httpretty.register_uri(httpretty.GET, 'http://localhost:8080/metrics/v2/read/test:name=Num', body=json.dumps(metrics_body)) metric = baseapi.metric('test:name=Num') assert httpretty.last_request().path == '/metrics/v2/read/test%3Aname%3DNum' assert metric['Value'] == 0 httpretty.disable() httpretty.reset() def test_metric_v2_version_constructor(self): api = pypuppetdb.api.BaseAPI(metric_api_version='v2') metrics_body = { 'request': { 'mbean': 'test:name=Num', 'type': 'read' }, 'value': { 'Value': 0 }, 'timestamp': 0, 'status': 200 } httpretty.enable() httpretty.register_uri(httpretty.GET, 'http://localhost:8080/metrics/v2/read/test:name=Num', body=json.dumps(metrics_body)) metric = api.metric('test:name=Num') assert httpretty.last_request().path == '/metrics/v2/read/test%3Aname%3DNum' assert metric['Value'] == 0 httpretty.disable() httpretty.reset() def test_metric_v2_version_string(self, baseapi): metrics_body = { 'request': { 'mbean': 'test:name=Num', 'type': 'read' }, 'value': { 'Value': 0 }, 'timestamp': 0, 'status': 200 } httpretty.enable() httpretty.register_uri(httpretty.GET, 'http://localhost:8080/metrics/v2/read/test:name=Num', body=json.dumps(metrics_body)) metric = baseapi.metric('test:name=Num', version='v2') assert httpretty.last_request().path == '/metrics/v2/read/test%3Aname%3DNum' assert metric['Value'] == 0 httpretty.disable() httpretty.reset() def test_metric_v2_error(self, baseapi): metrics_body = { 'request': { 'mbean': 'test:name=Num', 'type': 'read' }, 'error_type': 'javax.management.InstanceNotFoundException', 'error': 'javax.management.InstanceNotFoundException : test:name=Num', 'status': 404 } httpretty.enable() httpretty.register_uri(httpretty.GET, 'http://localhost:8080/metrics/v2/read/test:name=Num', body=json.dumps(metrics_body)) with pytest.raises(pypuppetdb.errors.APIError): baseapi.metric('test:name=Num') assert httpretty.last_request().path == '/metrics/v2/read/test%3Aname%3DNum' httpretty.disable() httpretty.reset() def test_metric_v2_escape_special_characters(self, baseapi): metrics_body = { 'request': { 'mbean': 'test:name=Num', 'type': 'read' }, 'value': { 'Value': 0 }, 'timestamp': 0, 'status': 200 } httpretty.enable() metric_name = 'test:special/chars!metric"name' metric_escaped = 'test:special!/chars!!metric!"name' metric_escaped_urlencoded = 'test%3Aspecial%21/chars%21%21metric%21%22name' httpretty.register_uri(httpretty.GET, ('http://localhost:8080/metrics/v2/read/' + metric_escaped), body=json.dumps(metrics_body)) metric = baseapi.metric(metric_name) assert httpretty.last_request().path == ('/metrics/v2/read/' + metric_escaped_urlencoded) assert metric['Value'] == 0 httpretty.disable() httpretty.reset() def test_metric_v2_list(self, baseapi): # test metric() (no arguments) metrics_body = { 'request': { 'type': 'list' }, 'value': { 'java.util.logging': { 'type=Logging': { } }, }, 'timestamp': 0, 'status': 200 } httpretty.enable() httpretty.register_uri(httpretty.GET, 'http://localhost:8080/metrics/v2/list', body=json.dumps(metrics_body)) metric = baseapi.metric() assert httpretty.last_request().path == '/metrics/v2/list' assert metric == {'java.util.logging': {'type=Logging': {}}} httpretty.disable() httpretty.reset() def test_metric_bad_version(self, baseapi): with pytest.raises(ValueError): baseapi.metric('test', version='bad') def test_facts(self, baseapi): facts_body = [{ 'certname': 'test_certname', 'name': 'test_name', 'value': 'test_value', 'environment': 'test_environment', }] facts_url = 'http://localhost:8080/pdb/query/v4/facts' httpretty.enable() httpretty.register_uri(httpretty.GET, facts_url, body=json.dumps(facts_body)) for fact in baseapi.facts(): pass assert httpretty.last_request().path == '/pdb/query/v4/facts' httpretty.disable() httpretty.reset() def test_fact_names(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/fact-names') baseapi.fact_names() assert httpretty.last_request().path == '/pdb/query/v4/fact-names' httpretty.disable() httpretty.reset() def test_normalize_resource_type(self, baseapi): assert baseapi._normalize_resource_type('sysctl::value') == \ 'Sysctl::Value' assert baseapi._normalize_resource_type('user') == 'User' def test_environments(self, baseapi): httpretty.enable() stub_request('http://localhost:8080/pdb/query/v4/environments') baseapi.environments() assert httpretty.last_request().path == '/pdb/query/v4/environments' httpretty.disable() httpretty.reset() def test_inventory(self, baseapi): inventory_body = [{ 'certname': 'test_certname', 'timestamp': '2017-06-05T20:18:23.374Z', 'environment': 'test_environment', 'facts': 'test_facts', 'trusted': 'test_trusted' }] inventory_url = 'http://localhost:8080/pdb/query/v4/inventory' httpretty.enable() httpretty.register_uri(httpretty.GET, inventory_url, body=json.dumps(inventory_body)) for inv in baseapi.inventory(): pass assert httpretty.last_request().path == '/pdb/query/v4/inventory' httpretty.disable() httpretty.reset() def test_status(self, baseapi): httpretty.enable() stub_request( 'http://localhost:8080/status/v1/services/puppetdb-status' ) baseapi.status() assert httpretty.last_request().path == \ '/status/v1/services/puppetdb-status' httpretty.disable() httpretty.reset() def test_command(self, baseapi): httpretty.enable() stub_request( 'http://localhost:8080/pdb/cmd/v1', method=httpretty.POST ) baseapi.command('deactivate node', {'certname': 'testnode'}) assert httpretty.last_request().path.startswith( '/pdb/cmd/v1' ) httpretty.disable() httpretty.reset() pypuppetdb-2.2.0/tests/test_connect.py000066400000000000000000000001641366723304700201270ustar00rootroot00000000000000import pypuppetdb def test_connect_api(): puppetdb = pypuppetdb.connect() assert puppetdb.version == 'v4' pypuppetdb-2.2.0/tests/test_package.py000066400000000000000000000006031366723304700200670ustar00rootroot00000000000000from pypuppetdb.package import (__author__, __copyright__, __license__, __title__, __year__) def test_package(): assert __title__ == 'pypuppetdb' assert __author__ == 'Daniele Sluijters' assert __license__ == 'Apache License 2.0' assert __year__ == '2013, 2014, 2015, 2016, 2017, 2018, 2019' assert __copyright__ == 'Copyright {0} {1}'.format(__year__, __author__) pypuppetdb-2.2.0/tests/test_querybuilder.py000066400000000000000000000551261366723304700212220ustar00rootroot00000000000000import datetime import pytest from pypuppetdb.QueryBuilder import (AndOperator, EqualsOperator, ExtractOperator, FromOperator, FunctionOperator, GreaterEqualOperator, GreaterOperator, InOperator, LessEqualOperator, LessOperator, NotOperator, NullOperator, OrOperator, RegexArrayOperator, RegexOperator, SubqueryOperator) from pypuppetdb.api import APIError class TestBinaryOperator(object): """ Test the BinaryOperator object and all sub-classes. """ def test_equal_operator(self): op = EqualsOperator("certname", "test01") assert str(op) == '["=", "certname", "test01"]' assert repr(op) == 'Query: ["=", "certname", "test01"]' assert str(op) == '["=", "certname", "test01"]' assert str(EqualsOperator("clientversion", 91))\ == '["=", "clientversion", 91]' assert str(EqualsOperator("start_time", "2016-05-11T23:22:48.709Z"))\ == '["=", "start_time", "2016-05-11T23:22:48.709Z"]' assert str(EqualsOperator("is_virtual", True))\ == '["=", "is_virtual", true]' assert str(EqualsOperator("bios_version", ["6.00", 5.00]))\ == '["=", "bios_version", ["6.00", 5.0]]' assert str(EqualsOperator(['parameter', 'ensure'], "present"))\ == '["=", ["parameter", "ensure"], "present"]' assert str(EqualsOperator(u"latest_report?", True))\ == '["=", "latest_report?", true]' assert str(EqualsOperator("report_timestamp", datetime.datetime(2016, 6, 11)))\ == '["=", "report_timestamp", "2016-06-11 00:00:00"]' def test_greater_operator(self): assert str(GreaterOperator("uptime", 150))\ == '[">", "uptime", 150]' assert str(GreaterOperator("end_time", '2016-05-11T23:22:48.709Z'))\ == '[">", "end_time", "2016-05-11T23:22:48.709Z"]' assert str(GreaterOperator(['parameter', 'version'], 4.0))\ == '[">", ["parameter", "version"], 4.0]' assert str(GreaterOperator("report_timestamp", datetime.datetime(2016, 6, 11)))\ == '[">", "report_timestamp", "2016-06-11 00:00:00"]' def test_less_operator(self): assert str(LessOperator("uptime_seconds", 300))\ == '["<", "uptime_seconds", 300]' assert str(LessOperator( "producer_timestamp", "2016-05-11T23:53:29.962Z"))\ == '["<", "producer_timestamp", "2016-05-11T23:53:29.962Z"]' assert str(LessOperator(['parameter', 'version'], 4.0))\ == '["<", ["parameter", "version"], 4.0]' assert str(LessOperator("report_timestamp", datetime.datetime(2016, 6, 11)))\ == '["<", "report_timestamp", "2016-06-11 00:00:00"]' def test_greater_equal_operator(self): assert str(GreaterEqualOperator("uptime_days", 3))\ == '[">=", "uptime_days", 3]' assert str(GreaterEqualOperator( "start_time", "2016-05-11T23:53:29.962Z"))\ == '[">=", "start_time", "2016-05-11T23:53:29.962Z"]' assert str(GreaterEqualOperator(['parameter', 'version'], 4.0))\ == '[">=", ["parameter", "version"], 4.0]' assert str(GreaterEqualOperator("report_timestamp", datetime.datetime(2016, 6, 11)))\ == '[">=", "report_timestamp", "2016-06-11 00:00:00"]' def test_less_equal_operator(self): assert str(LessEqualOperator("kernelmajversion", 4))\ == '["<=", "kernelmajversion", 4]' assert str(LessEqualOperator("end_time", "2016-05-11T23:53:29.962Z"))\ == '["<=", "end_time", "2016-05-11T23:53:29.962Z"]' assert str(LessEqualOperator(['parameter', 'version'], 4.0))\ == '["<=", ["parameter", "version"], 4.0]' assert str(LessEqualOperator("report_timestamp", datetime.datetime(2016, 6, 11)))\ == '["<=", "report_timestamp", "2016-06-11 00:00:00"]' def test_regex_operator(self): assert str(RegexOperator("certname", "www\\d+\\.example\\.com"))\ == '["~", "certname", "www\\\\d+\\\\.example\\\\.com"]' assert str(RegexOperator(['parameter', 'version'], "4\\.\\d+"))\ == '["~", ["parameter", "version"], "4\\\\.\\\\d+"]' def test_regex_array_operator(self): assert str(RegexArrayOperator( "networking", ["interfaces", "eno.*", "netmask"]))\ == '["~>", "networking", ["interfaces", "eno.*", "netmask"]]' def test_null_operator(self): assert str(NullOperator("expired", True))\ == '["null?", "expired", true]' assert str(NullOperator("report_environment", False))\ == '["null?", "report_environment", false]' with pytest.raises(APIError): NullOperator("environment", "test") class TestBooleanOperator(object): """ Test the BooleanOperator object and all sub-classes. """ def test_and_operator(self): op = AndOperator() op.add(EqualsOperator("operatingsystem", "CentOS")) op.add([EqualsOperator("architecture", "x86_64"), GreaterOperator("operatingsystemmajrelease", 6)]) assert str(op) == '["and", ["=", "operatingsystem", "CentOS"], '\ '["=", "architecture", "x86_64"], '\ '[">", "operatingsystemmajrelease", 6]]' assert repr(op) == 'Query: ["and", '\ '["=", "operatingsystem", "CentOS"], '\ '["=", "architecture", "x86_64"], '\ '[">", "operatingsystemmajrelease", 6]]' assert str(op) == '["and", ["=", "operatingsystem", "CentOS"], ' \ '["=", "architecture", "x86_64"], '\ '[">", "operatingsystemmajrelease", 6]]' with pytest.raises(APIError): op.add({"query1": '["=", "catalog_environment", "production"]'}) def test_or_operator(self): op = OrOperator() op.add(EqualsOperator("operatingsystem", "CentOS")) op.add([EqualsOperator("architecture", "x86_64"), GreaterOperator("operatingsystemmajrelease", 6)]) assert str(op) == '["or", ["=", "operatingsystem", "CentOS"], '\ '["=", "architecture", "x86_64"], '\ '[">", "operatingsystemmajrelease", 6]]' assert repr(op) == 'Query: ["or", '\ '["=", "operatingsystem", "CentOS"], '\ '["=", "architecture", "x86_64"], '\ '[">", "operatingsystemmajrelease", 6]]' assert str(op) == '["or", ["=", "operatingsystem", "CentOS"], ' \ '["=", "architecture", "x86_64"], '\ '[">", "operatingsystemmajrelease", 6]]' with pytest.raises(APIError): op.add({"query1": '["=", "catalog_environment", "production"]'}) def test_not_operator(self): op = NotOperator() op.add(EqualsOperator("operatingsystem", "CentOS")) assert str(op) == '["not", ["=", "operatingsystem", "CentOS"]]' assert repr(op) == 'Query: ["not", ["=", "operatingsystem", "CentOS"]]' assert str(op) == '["not", ["=", "operatingsystem", "CentOS"]]' with pytest.raises(APIError): op.add(GreaterOperator("operatingsystemmajrelease", 6)) with pytest.raises(APIError): op.add([EqualsOperator("architecture", "x86_64"), GreaterOperator("operatingsystemmajrelease", 6)]) def test_and_with_no_operations(self): op = AndOperator() with pytest.raises(APIError): repr(op) with pytest.raises(APIError): str(op) with pytest.raises(APIError): str(op) def test_or_with_no_operations(self): op = OrOperator() with pytest.raises(APIError): repr(op) with pytest.raises(APIError): str(op) with pytest.raises(APIError): str(op) def test_not_with_no_operations(self): op = NotOperator() with pytest.raises(APIError): repr(op) with pytest.raises(APIError): str(op) with pytest.raises(APIError): str(op) def test_not_with_list(self): op = NotOperator() with pytest.raises(APIError): str(op.add([EqualsOperator('clientversion', '4.5.1'), EqualsOperator('facterversion', '3.2.0')])) class TestExtractOperator(object): """ Test the ExtractOperator object and all sub-classes. """ def test_with_add_field(self): op = ExtractOperator() with pytest.raises(APIError): repr(op) with pytest.raises(APIError): str(op) with pytest.raises(APIError): str(op) op.add_field("certname") op.add_field(['fact_environment', 'catalog_environment']) assert repr(op) == 'Query: ["extract", '\ '["certname", "fact_environment", "catalog_environment"]]' assert str(op) == '["extract", '\ '["certname", "fact_environment", "catalog_environment"]]' assert str(op) == '["extract", ' \ '["certname", "fact_environment", "catalog_environment"]]' with pytest.raises(APIError): op.add_field({'equal': 'operatingsystemrelease'}) def test_with_add_query(self): op = ExtractOperator() op.add_field(['certname', 'fact_environment', 'catalog_environment']) with pytest.raises(APIError): op.add_query({'less': 42, 'greater': 50}) op.add_query(EqualsOperator('domain', 'example.com')) assert repr(op) == 'Query: ["extract", '\ '["certname", "fact_environment", "catalog_environment"], '\ '["=", "domain", "example.com"]]' assert str(op) == '["extract", '\ '["certname", "fact_environment", "catalog_environment"], '\ '["=", "domain", "example.com"]]' assert str(op) == '["extract", ' \ '["certname", "fact_environment", "catalog_environment"], '\ '["=", "domain", "example.com"]]' with pytest.raises(APIError): op.add_query(GreaterOperator("processorcount", 1)) def test_with_add_group_by(self): op = ExtractOperator() op.add_field(['certname', 'fact_environment', 'catalog_environment']) op.add_query(EqualsOperator('domain', 'example.com')) op.add_group_by(["fact_environment", "catalog_environment"]) with pytest.raises(APIError): op.add_group_by({"deactivated": False}) assert repr(op) == 'Query: ["extract", '\ '["certname", "fact_environment", "catalog_environment"], '\ '["=", "domain", "example.com"], '\ '["group_by", "fact_environment", "catalog_environment"]]' assert str(op) == '["extract", '\ '["certname", "fact_environment", "catalog_environment"], '\ '["=", "domain", "example.com"], '\ '["group_by", "fact_environment", "catalog_environment"]]' assert str(op) == '["extract", ' \ '["certname", "fact_environment", "catalog_environment"], '\ '["=", "domain", "example.com"], '\ '["group_by", "fact_environment", "catalog_environment"]]' def test_with_add_function_operator(self): op = ExtractOperator() op.add_field(FunctionOperator('to_string', 'producer_timestamp', 'FMDAY')) op.add_field(FunctionOperator('count')) op.add_group_by(FunctionOperator('to_string', 'producer_timestamp', 'FMDAY')) assert str(op) == '["extract", '\ '[["function", "to_string", "producer_timestamp", "FMDAY"], '\ '["function", "count"]], '\ '["group_by", '\ '["function", "to_string", "producer_timestamp", "FMDAY"]]]' assert repr(op) == 'Query: ["extract", '\ '[["function", "to_string", "producer_timestamp", "FMDAY"], '\ '["function", "count"]], '\ '["group_by", '\ '["function", "to_string", "producer_timestamp", "FMDAY"]]]' assert str(op) == '["extract", ' \ '[["function", "to_string", "producer_timestamp", "FMDAY"], '\ '["function", "count"]], '\ '["group_by", '\ '["function", "to_string", "producer_timestamp", "FMDAY"]]]' class TestFunctionOperator(object): """ Test the FunctionOperator object and all sub-classes. """ def test_count_function(self): assert str(FunctionOperator('count')) == \ '["function", "count"]' assert repr(FunctionOperator('count')) == \ 'Query: ["function", "count"]' assert str(FunctionOperator('count')) == \ '["function", "count"]' assert str(FunctionOperator('count', 'domain')) == \ '["function", "count", "domain"]' assert repr(FunctionOperator('count', 'domain')) == \ 'Query: ["function", "count", "domain"]' assert str(FunctionOperator('count', 'domain')) == \ '["function", "count", "domain"]' def test_avg_function(self): assert str(FunctionOperator('avg', 'uptime')) == \ '["function", "avg", "uptime"]' assert repr(FunctionOperator('avg', 'uptime')) == \ 'Query: ["function", "avg", "uptime"]' assert str(FunctionOperator('avg', 'uptime')) == \ '["function", "avg", "uptime"]' with pytest.raises(APIError): FunctionOperator("avg") def test_sum_function(self): assert str(FunctionOperator('sum', 'memoryfree_mb')) == \ '["function", "sum", "memoryfree_mb"]' assert repr(FunctionOperator('sum', 'memoryfree_mb')) == \ 'Query: ["function", "sum", "memoryfree_mb"]' assert str(FunctionOperator('sum', 'memoryfree_mb')) == \ '["function", "sum", "memoryfree_mb"]' with pytest.raises(APIError): FunctionOperator("sum") def test_min_function(self): assert str(FunctionOperator('min', 'kernelversion')) == \ '["function", "min", "kernelversion"]' assert repr(FunctionOperator('min', 'kernelversion')) == \ 'Query: ["function", "min", "kernelversion"]' assert str(FunctionOperator('min', 'kernelversion')) == \ '["function", "min", "kernelversion"]' with pytest.raises(APIError): FunctionOperator("min") def test_max_function(self): assert str(FunctionOperator('max', 'facterversion')) == \ '["function", "max", "facterversion"]' assert repr(FunctionOperator('max', 'facterversion')) == \ 'Query: ["function", "max", "facterversion"]' assert str(FunctionOperator('max', 'facterversion')) == \ '["function", "max", "facterversion"]' with pytest.raises(APIError): FunctionOperator("max") def test_to_string_function(self): assert str(FunctionOperator("to_string", 'producer_timestamp', 'FMDAY')) == \ '["function", "to_string", "producer_timestamp", "FMDAY"]' assert repr(FunctionOperator("to_string", 'producer_timestamp', 'FMDAY')) == \ 'Query: ["function", "to_string", "producer_timestamp", "FMDAY"]' assert str(FunctionOperator("to_string", 'producer_timestamp', 'FMDAY')) == \ '["function", "to_string", "producer_timestamp", "FMDAY"]' with pytest.raises(APIError): FunctionOperator("to_string") with pytest.raises(APIError): FunctionOperator("to_string", 'receive_time') def test_unknown_function(self): with pytest.raises(APIError): FunctionOperator("std_dev") with pytest.raises(APIError): FunctionOperator("last") class TestSubqueryOperator(object): """ Test the SubqueryOperator object """ def test_events_endpoint(self): assert str(SubqueryOperator('events')) == \ '["select_events"]' op = SubqueryOperator('events') op.add_query(EqualsOperator('status', 'noop')) assert repr(op) == 'Query: ["select_events", '\ '["=", "status", "noop"]]' def test_multiple_add_query(self): with pytest.raises(APIError): op = SubqueryOperator('events') op.add_query(EqualsOperator('status', 'noop')) op.add_query(EqualsOperator('status', 'changed')) def test_unknown_endpoint(self): with pytest.raises(APIError): SubqueryOperator('cats') class TestInOperator(object): """ Test the InOperator object """ def test_events_endpoint(self): assert str(InOperator('certname')) == \ '["in", "certname"]' op = InOperator('certname') ex = ExtractOperator() ex.add_field("certname") op.add_query(ex) assert repr(op) == 'Query: ["in", "certname", ' \ '["extract", ["certname"]]]' def test_multiple_add_query(self): with pytest.raises(APIError): op = InOperator('certname') op.add_query(ExtractOperator()) op.add_query(ExtractOperator()) def test_add_array(self): arr = [1, "2", 3] op = InOperator('certname') op.add_array(arr) assert repr(op) == 'Query: ["in", "certname", ' \ '["array", [1, "2", 3]]]' def test_invalid_add_array(self): arr = [1, 2, 3] inv1 = [1, [2, 3]] inv2 = [] with pytest.raises(APIError): op = InOperator('certname') op.add_array(inv1) with pytest.raises(APIError): op = InOperator('certname') op.add_array(inv2) with pytest.raises(APIError): op = InOperator('certname') op.add_array(arr) op.add_array(arr) with pytest.raises(APIError): op = InOperator('certname') op.add_array(arr) ex = ExtractOperator() ex.add_field("certname") op.add_query(ex) def test_fromoperator(self): op = InOperator('certname') ex = ExtractOperator() ex.add_field(["certname", "facts"]) fr = FromOperator("facts") fr.add_query(ex) fr.add_offset(10) op.add_query(fr) assert repr(op) == 'Query: ["in", "certname", ' \ '["from", "facts", ["extract", ' \ '["certname", "facts"]], ["offset", 10]]]' # last example on page # https://puppet.com/docs/puppetdb/5.1/api/query/v4/ast.html op = InOperator('certname') ex = ExtractOperator() ex.add_field('certname') fr = FromOperator('fact_contents') nd = AndOperator() nd.add(EqualsOperator("path", ["networking", "eth0", "macaddresses", 0])) nd.add(EqualsOperator("value", "aa:bb:cc:dd:ee:00")) ex.add_query(nd) fr.add_query(ex) op.add_query(fr) assert str(op) == '["in", "certname", ' \ '["from", "fact_contents", ' \ '["extract", ["certname"], ["and", ["=", "path", ' \ '["networking", "eth0", "macaddresses", 0]], ' \ '["=", "value", "aa:bb:cc:dd:ee:00"]]]]]' class TestFromOperator(object): """ Test the FromOperator object """ def test_init_from(self): fr = FromOperator("facts") with pytest.raises(APIError): str(fr) == "unimportant_no_query" with pytest.raises(APIError): fr2 = FromOperator('invalid_entity') def test_add_query(self): fr = FromOperator("facts") op = EqualsOperator("certname", "test01") fr.add_query(op) assert str(fr) == '["from", "facts", ["=", "certname", "test01"]]' fr2 = FromOperator("facts") op2 = "test, test, test" with pytest.raises(APIError): fr2.add_query(op2) fr2.add_query(op) with pytest.raises(APIError): fr2.add_query(op) fr3 = FromOperator("facts") op3 = ExtractOperator() op3.add_field(['certname', 'fact_environment', 'catalog_environment']) fr3.add_query(op3) assert str(fr3) == \ '["from", "facts", ["extract", '\ '["certname", "fact_environment", "catalog_environment"]]]' def test_limit_offset(self): fr = FromOperator("facts") op = EqualsOperator("certname", "test01") fr.add_query(op) fr.add_offset(10) assert str(fr) == \ '["from", "facts", ["=", "certname", "test01"], ["offset", 10]]' fr.add_limit(5) assert str(fr) == \ '["from", "facts", ["=", "certname",' \ ' "test01"], ["limit", 5], ["offset", 10]]' fr.add_limit(15) assert str(fr) == \ '["from", "facts", ["=", "certname",' \ ' "test01"], ["limit", 15], ["offset", 10]]' assert repr(fr) == \ 'Query: ["from", "facts", ["=", "certname",' \ ' "test01"], ["limit", 15], ["offset", 10]]' with pytest.raises(APIError): fr.add_offset("invalid") with pytest.raises(APIError): fr.add_limit(["invalid"]) def test_order_by(self): fr = FromOperator("facts") op = EqualsOperator("certname", "test01") fr.add_query(op) o1 = ["certname"] o2 = ["certname", ["timestamp", "desc"], "facts"] o3inv = ['certname', ['timestamp', 'desc', ['oops']]] fr.add_order_by(o1) assert str(fr) == \ '["from", "facts", ["=", "certname", ' \ '"test01"], ["order_by", ["certname"]]]' fr.add_order_by(o2) assert repr(fr) == \ 'Query: ["from", "facts", ' \ '["=", "certname", "test01"], ' \ '["order_by", ["certname", ' \ '["timestamp", "desc"], "facts"]]]' assert str(fr) == \ '["from", "facts", ' \ '["=", "certname", "test01"], ' \ '["order_by", ["certname", ' \ '["timestamp", "desc"], "facts"]]]' assert str(fr) == \ '["from", "facts", ' \ '["=", "certname", "test01"], ' \ '["order_by", ["certname", ' \ '["timestamp", "desc"], "facts"]]]' with pytest.raises(APIError): fr.add_order_by(o3inv) pypuppetdb-2.2.0/tests/test_types.py000066400000000000000000000710011366723304700176400ustar00rootroot00000000000000from pypuppetdb.types import (Catalog, Edge, Event, Fact, Inventory, Node, Report, Resource) from pypuppetdb.utils import json_to_datetime class TestNode(object): """Test the Node object.""" def test_without_status(self): node = Node('_', 'node', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z',) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_with_status_unreported(self): node = Node('_', 'node', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z', status_report='unchanged', unreported=True, unreported_time='0d 5h 20m',) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.status == 'unreported' assert node.unreported_time is '0d 5h 20m' assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_with_status_unreported_from_noop(self): node = Node('_', 'node', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z', status_report='noop', unreported=True, unreported_time='0d 5h 20m',) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.status == 'unreported' assert node.unreported_time is '0d 5h 20m' assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_with_status_unreported_from_failed(self): node = Node('_', 'node', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z', status_report='failed', unreported=True, unreported_time='0d 5h 20m',) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.status == 'unreported' assert node.unreported_time is '0d 5h 20m' assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_apiv4_with_failed_status(self): node = Node('_', 'node', status_report='failed', report_environment='development', catalog_environment='development', facts_environment='development', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z', ) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_environment == 'development' assert node.catalog_environment == 'development' assert node.facts_environment == 'development' assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.status == 'failed' assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_apiv4_with_unchanged_status(self): node = Node('_', 'node', status_report='unchanged', report_environment='development', catalog_environment='development', facts_environment='development', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z', ) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_environment == 'development' assert node.catalog_environment == 'development' assert node.facts_environment == 'development' assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.status == 'unchanged' assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_apiv4_with_unchanged_noop_status(self): node = Node('_', 'node', status_report='unchanged', noop=True, noop_pending=False, report_environment='development', catalog_environment='development', facts_environment='development', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z', ) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_environment == 'development' assert node.catalog_environment == 'development' assert node.facts_environment == 'development' assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.status == 'unchanged' assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_apiv4_with_pending_noop_status(self): node = Node('_', 'node', status_report='unchanged', noop=True, noop_pending=True, report_environment='development', catalog_environment='development', facts_environment='development', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z', ) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_environment == 'development' assert node.catalog_environment == 'development' assert node.facts_environment == 'development' assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.status == 'noop' assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_apiv4_with_failed_noop_status(self): node = Node('_', 'node', status_report='failed', noop=True, noop_pending=False, report_environment='development', catalog_environment='development', facts_environment='development', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z', ) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_environment == 'development' assert node.catalog_environment == 'development' assert node.facts_environment == 'development' assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.status == 'failed' assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_apiv4_without_status(self): node = Node('_', 'node', report_environment='development', catalog_environment='development', facts_environment='development', report_timestamp='2013-08-01T09:57:00.000Z', catalog_timestamp='2013-08-01T09:57:00.000Z', facts_timestamp='2013-08-01T09:57:00.000Z',) assert node.name == 'node' assert node.deactivated is False assert node.expired is False assert node.report_environment == 'development' assert node.catalog_environment == 'development' assert node.facts_environment == 'development' assert node.report_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.facts_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert node.catalog_timestamp == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_deactivated(self): node = Node('_', 'node', deactivated='2013-08-01T09:57:00.000Z',) assert node.name == 'node' assert node.deactivated == \ json_to_datetime('2013-08-01T09:57:00.000Z') assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_expired(self): node = Node('_', 'node', expired='2013-08-01T09:57:00.000Z',) assert node.name == 'node' assert node.expired == json_to_datetime('2013-08-01T09:57:00.000Z') assert str(node) == str('node') assert str(node) == str('node') assert repr(node) == str('') def test_with_latest_report_hash(self): node = Node('_', 'node', latest_report_hash='hash#1') assert node.name == 'node' assert node.latest_report_hash == 'hash#1' def test_with_cached_catalog_status(self): node1 = Node('_', 'node', cached_catalog_status='explicitly_requested') node2 = Node('_', 'node', cached_catalog_status='on_failure') node3 = Node('_', 'node', cached_catalog_status='not_used') assert node1.name == 'node' assert node1.cached_catalog_status == 'explicitly_requested' assert node2.name == 'node' assert node2.cached_catalog_status == 'on_failure' assert node3.name == 'node' assert node3.cached_catalog_status == 'not_used' class TestFact(object): """Test the Fact object.""" def test_fact(self): fact = Fact('node', 'osfamily', 'Debian', 'production') assert fact.node == 'node' assert fact.name == 'osfamily' assert fact.value == 'Debian' assert fact.environment == 'production' assert str(fact) == str('osfamily/node') assert str(fact) == str('osfamily/node') assert repr(fact) == str('Fact: osfamily/node') class TestResource(object): "Test the Resource object.""" def test_resource(self): resource = Resource('node', '/etc/ssh/sshd_config', 'file', ['class', 'ssh'], False, '/ssh/manifests/init.pp', 15, 'production', parameters={ 'ensure': 'present', 'owner': 'root', 'group': 'root', 'mode': '0600', }) assert resource.node == 'node' assert resource.name == '/etc/ssh/sshd_config' assert resource.type_ == 'file' assert resource.tags == ['class', 'ssh'] assert resource.exported is False assert resource.sourcefile == '/ssh/manifests/init.pp' assert resource.sourceline == 15 assert resource.environment == 'production' assert resource.parameters['ensure'] == 'present' assert resource.parameters['owner'] == 'root' assert resource.parameters['group'] == 'root' assert resource.parameters['mode'] == '0600' assert str(resource) == str('file[/etc/ssh/sshd_config]') assert str(resource) == str('file[/etc/ssh/sshd_config]') assert repr(resource) == str( '') class TestReport(object): """Test the Report object.""" def test_report(self): report = Report('_', 'node1.puppet.board', 'hash#', '2013-08-01T09:57:00.000Z', '2013-08-01T10:57:00.000Z', '2013-08-01T10:58:00.000Z', '1351535883', 3, '3.2.1', 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3', status='success') assert report.node == 'node1.puppet.board' assert report.hash_ == 'hash#' assert report.start == json_to_datetime('2013-08-01T09:57:00.000Z') assert report.end == json_to_datetime('2013-08-01T10:57:00.000Z') assert report.received == json_to_datetime('2013-08-01T10:58:00.000Z') assert report.version == '1351535883' assert report.format_ == 3 assert report.agent_version == '3.2.1' assert report.run_time == report.end - report.start assert report.transaction == 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3' assert report.status == 'success' assert str(report) == str('hash#') assert str(report) == str('hash#') assert repr(report) == str('Report: hash#') def test_report_with_noop(self): report = Report('_', 'node2.puppet.board', 'hash#', '2015-08-31T21:07:00.000Z', '2015-08-31T21:09:00.000Z', '2015-08-31T21:10:00.000Z', '1482347613', 4, '4.2.1', 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3', status='unchanged', noop=True, noop_pending=False) assert report.node == 'node2.puppet.board' assert report.hash_ == 'hash#' assert report.start == json_to_datetime('2015-08-31T21:07:00.000Z') assert report.end == json_to_datetime('2015-08-31T21:09:00.000Z') assert report.received == json_to_datetime('2015-08-31T21:10:00.000Z') assert report.version == '1482347613' assert report.format_ == 4 assert report.agent_version == '4.2.1' assert report.run_time == report.end - report.start assert report.transaction == 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3' assert report.status == 'unchanged' assert str(report) == str('hash#') assert str(report) == str('hash#') assert repr(report) == str('Report: hash#') def test_report_with_failed_noop(self): report = Report('_', 'node2.puppet.board', 'hash#', '2015-08-31T21:07:00.000Z', '2015-08-31T21:09:00.000Z', '2015-08-31T21:10:00.000Z', '1482347613', 4, '4.2.1', 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3', status='failed', noop=True, noop_pending=False) assert report.node == 'node2.puppet.board' assert report.hash_ == 'hash#' assert report.start == json_to_datetime('2015-08-31T21:07:00.000Z') assert report.end == json_to_datetime('2015-08-31T21:09:00.000Z') assert report.received == json_to_datetime('2015-08-31T21:10:00.000Z') assert report.version == '1482347613' assert report.format_ == 4 assert report.agent_version == '4.2.1' assert report.run_time == report.end - report.start assert report.transaction == 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3' assert report.status == 'failed' assert str(report) == str('hash#') assert str(report) == str('hash#') assert repr(report) == str('Report: hash#') def test_report_with_pending_noop(self): report = Report('_', 'node2.puppet.board', 'hash#', '2015-08-31T21:07:00.000Z', '2015-08-31T21:09:00.000Z', '2015-08-31T21:10:00.000Z', '1482347613', 4, '4.2.1', 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3', status='unchanged', noop=True, noop_pending=True) assert report.node == 'node2.puppet.board' assert report.hash_ == 'hash#' assert report.start == json_to_datetime('2015-08-31T21:07:00.000Z') assert report.end == json_to_datetime('2015-08-31T21:09:00.000Z') assert report.received == json_to_datetime('2015-08-31T21:10:00.000Z') assert report.version == '1482347613' assert report.format_ == 4 assert report.agent_version == '4.2.1' assert report.run_time == report.end - report.start assert report.transaction == 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3' assert report.status == 'noop' assert str(report) == str('hash#') assert str(report) == str('hash#') assert repr(report) == str('Report: hash#') def test_report_with_cataloguuid_codeid(self): report = Report('_', 'node2.puppet.board', 'hash#', '2015-08-31T21:07:00.000Z', '2015-08-31T21:09:00.000Z', '2015-08-31T21:10:00.000Z', '1482347613', 4, '4.2.1', 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3', code_id=None, catalog_uuid="0b3a4943-a164-4cea-bbf0-91d0ee931326", cached_catalog_status="not_used") assert report.node == 'node2.puppet.board' assert report.hash_ == 'hash#' assert report.start == json_to_datetime('2015-08-31T21:07:00.000Z') assert report.end == json_to_datetime('2015-08-31T21:09:00.000Z') assert report.received == json_to_datetime('2015-08-31T21:10:00.000Z') assert report.version == '1482347613' assert report.format_ == 4 assert report.agent_version == '4.2.1' assert report.run_time == report.end - report.start assert report.transaction == 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3' assert report.catalog_uuid == "0b3a4943-a164-4cea-bbf0-91d0ee931326" assert report.cached_catalog_status == "not_used" assert str(report) == str('hash#') assert str(report) == str('hash#') assert repr(report) == str('Report: hash#') def test_report_with_producer(self): report = Report('_', "test.test.com", "hash#", '2015-08-31T21:07:00.000Z', '2015-08-31T21:09:00.000Z', '2015-08-31T21:10:00.000Z', '1482347613', 4, '4.2.1', 'af9f16e3-75f6-4f90-acc6-f83d6524a6f3', producer="puppet01.test.com") assert report.node == "test.test.com" assert report.hash_ == 'hash#' assert report.start == json_to_datetime('2015-08-31T21:07:00.000Z') assert report.end == json_to_datetime('2015-08-31T21:09:00.000Z') assert report.received == json_to_datetime('2015-08-31T21:10:00.000Z') assert report.version == '1482347613' assert report.format_ == 4 assert report.agent_version == '4.2.1' assert report.run_time == report.end - report.start assert report.producer == "puppet01.test.com" assert str(report) == str('hash#') assert str(report) == str('hash#') assert repr(report) == str('Report: hash#') class TestEvent(object): """Test the Event object.""" def test_event(self): event = Event('node', 'failure', '2013-08-01T10:57:00.000Z', 'hash#', '/etc/ssh/sshd_config', 'ensure', 'Nothing to say', 'present', 'absent', 'file', 'Ssh::Server', ['Stage[main]', 'Ssh::Server', 'File[/etc/ssh/sshd_config]'], '/etc/puppet/modules/ssh/manifests/server.pp', 80) assert event.node == 'node' assert event.status == 'failure' assert event.failed is True assert event.timestamp == json_to_datetime('2013-08-01T10:57:00.000Z') assert event.hash_ == 'hash#' assert event.item['title'] == '/etc/ssh/sshd_config' assert event.item['type'] == 'file' assert event.item['property'] == 'ensure' assert event.item['message'] == 'Nothing to say' assert event.item['old'] == 'absent' assert event.item['new'] == 'present' assert str(event) == str('file[/etc/ssh/sshd_config]/hash#') assert str(event) == str('file[/etc/ssh/sshd_config]/hash#') assert repr(event) == str('Event: file[/etc/ssh/sshd_config]/hash#') def test_event_failed(self): event = Event('node', 'success', '2013-08-01T10:57:00.000Z', 'hash#', '/etc/ssh/sshd_config', 'ensure', 'Nothing to say', 'present', 'absent', 'file', 'Ssh::Server', ['Stage[main]', 'Ssh::Server', 'File[/etc/ssh/sshd_config]'], '/etc/puppet/modules/ssh/manifests/server.pp', 80) assert event.status == 'success' assert event.failed is False class TestCatalog(object): """Test the Catalog object.""" def test_catalog(self): catalog = Catalog('node', [], [], 'unique', None) assert catalog.node == 'node' assert catalog.version == 'unique' assert catalog.transaction_uuid is None assert catalog.resources == {} assert catalog.edges == [] assert str(catalog) == str('node/None') assert str(catalog) == str('node/None') assert repr(catalog) == str( '') def test_catalog_codeid(self): catalog = Catalog('node', [], [], 'unique', None, code_id='somecodeid') assert catalog.node == 'node' assert catalog.version == 'unique' assert catalog.transaction_uuid is None assert catalog.resources == {} assert catalog.edges == [] assert str(catalog) == str('node/None') assert str(catalog) == str('node/None') assert repr(catalog) == str( '') assert catalog.code_id == 'somecodeid' def test_catalog_uuid(self): catalog = Catalog('node', [], [], 'unique', None, catalog_uuid='univerallyuniqueidentifier') assert catalog.node == 'node' assert catalog.version == 'unique' assert catalog.transaction_uuid is None assert catalog.resources == {} assert catalog.edges == [] assert str(catalog) == str('node/None') assert str(catalog) == str('node/None') assert repr(catalog) == str( '') assert catalog.catalog_uuid == 'univerallyuniqueidentifier' def test_catalog_producer(self): catalog = Catalog('node', [], [], 'unique', None, producer="puppet01.test.com") assert catalog.node == 'node' assert catalog.version == 'unique' assert catalog.transaction_uuid is None assert catalog.resources == {} assert catalog.edges == [] assert catalog.producer == 'puppet01.test.com' assert str(catalog) == str('node/None') assert str(catalog) == str('node/None') assert repr(catalog) == str( '') class TestEdge(object): """Test the Edge object.""" def test_edge(self): resource_a = Resource('node', '/etc/ssh/sshd_config', 'file', ['class', 'ssh'], False, '/ssh/manifests/init.pp', 15, 'production', parameters={}) resource_b = Resource('node', 'sshd', 'service', ['class', 'ssh'], False, '/ssh/manifests/init.pp', 30, 'production', parameters={}) edge = Edge(resource_a, resource_b, 'notify') assert edge.source == resource_a assert edge.target == resource_b assert edge.relationship == 'notify' assert str(edge) == str( 'file[/etc/ssh/sshd_config] - notify - service[sshd]') assert str(edge) == str( 'file[/etc/ssh/sshd_config] - notify - service[sshd]') assert repr(edge) == str( '') class TestInventory(object): def test_inventory(self): inv = Inventory(node="test1.test.com", environment="production", time='2016-08-18T21:00:00.000Z', facts={ "hostname": "test1.test.com", "domain": "test.com", "puppetversion": "4.6.0" }, trusted={ "authenticated": "remote", "domain": "test.com", "certname": "test1.test.com", "extensions": {}, "hostname": "test1" }) assert inv.node == "test1.test.com" assert inv.environment == "production" assert inv.time == json_to_datetime('2016-08-18T21:00:00.000Z') assert inv.facts == { "hostname": "test1.test.com", "domain": "test.com", "puppetversion": "4.6.0" } assert inv.trusted == { "authenticated": "remote", "domain": "test.com", "certname": "test1.test.com", "extensions": {}, "hostname": "test1" } assert str(inv) == str("test1.test.com") assert str(inv) == str("test1.test.com") assert repr(inv) == str("") pypuppetdb-2.2.0/tests/test_utils.py000066400000000000000000000047041366723304700176420ustar00rootroot00000000000000from __future__ import unicode_literals import datetime import pytest import pypuppetdb class TestUTC(object): """Test the UTC class.""" def test_utc_offset(self, utc): assert datetime.timedelta(0) == utc.utcoffset(300) def test_tzname(self, utc): assert str('UTC') == utc.tzname(300) def test_dst(self, utc): assert datetime.timedelta(0) == utc.dst(300) def test_magic_str(self, utc): assert str('UTC') == str(utc) def test_magic_unicode(self, utc): assert 'UTC' == str(utc) def test_magic_repr(self, utc): assert str('') == repr(utc) class TestJSONToDateTime(object): """Test the json_to_datetime function.""" def test_json_to_datetime(self): json_datetime = '2013-08-01T09:57:00.000Z' python_datetime = pypuppetdb.utils.json_to_datetime(json_datetime) assert python_datetime.dst() == datetime.timedelta(0) assert python_datetime.date() == datetime.date(2013, 8, 1) assert python_datetime.tzname() == 'UTC' assert python_datetime.utcoffset() == datetime.timedelta(0) assert python_datetime.dst() == datetime.timedelta(0) def test_json_to_datetime_invalid(self): with pytest.raises(ValueError): pypuppetdb.utils.json_to_datetime('2013-08-0109:57:00.000Z') class TestVersionCmp(object): """Test the versioncmp function using different criteria.""" def test_versioncmp(self): assert pypuppetdb.utils.versioncmp('1', '1') == 0 assert pypuppetdb.utils.versioncmp('2.1', '2.2') < 0 assert pypuppetdb.utils.versioncmp('3.0.4.10', '3.0.4.2') > 0 assert pypuppetdb.utils.versioncmp('4.08', '4.08.1') < 0 assert pypuppetdb.utils.versioncmp('3.2.1.9.8144', '3.2') > 0 assert pypuppetdb.utils.versioncmp('3.2', '3.2.1.9.8144') < 0 assert pypuppetdb.utils.versioncmp('1.2', '2.1') < 0 assert pypuppetdb.utils.versioncmp('2.1', '1.2') > 0 assert pypuppetdb.utils.versioncmp('5.6.7', '5.6.7') == 0 assert pypuppetdb.utils.versioncmp('1.01.1', '1.1.1') == 0 assert pypuppetdb.utils.versioncmp('1.1.1', '1.01.1') == 0 assert pypuppetdb.utils.versioncmp('1', '1.0') == 0 assert pypuppetdb.utils.versioncmp('1.0', '1') == 0 assert pypuppetdb.utils.versioncmp('1.0', '1.0.1') < 0 assert pypuppetdb.utils.versioncmp('1.0.1', '1.0') > 0 assert pypuppetdb.utils.versioncmp('1.0.2.0', '1.0.2') == 0 pypuppetdb-2.2.0/tox.ini000066400000000000000000000002551366723304700152370ustar00rootroot00000000000000[tox] envlist = py{36,37,38} [testenv] deps= -rrequirements-test.txt bandit commands= py.test --cov=pypuppetdb --pep8 -v py{36,37,38}: bandit -r pypuppetdb pypuppetdb-2.2.0/version000066400000000000000000000000061366723304700153260ustar00rootroot000000000000002.2.0