././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4138756 python-ironic-inspector-client-4.7.1/0000775000175000017500000000000000000000000017630 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/.stestr.conf0000664000175000017500000000012300000000000022075 0ustar00zuulzuul00000000000000[DEFAULT] test_path=${TESTS_DIR:-./ironic_inspector_client/tests/unit/} top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/AUTHORS0000664000175000017500000000364700000000000020712 0ustar00zuulzuul0000000000000098k <18552437190@163.com> Andreas Jaeger Anton Arefiev Bob Fournier Cao Xuan Hoang Clenimar Filemon Corey Bryant Dmitry Tantsur Dmitry Tantsur Dmitry Tantsur Doug Hellmann Flavio Percoco Ghanshyam Mann Ian Wienand Iury Gregory Melo Ferreira Iury Gregory Melo Ferreira James E. Blair Janonymous Jay Faulkner Jim Rollenhagen John L. Villalovos Julia Kreger Kaifeng Wang Lucas Alvares Gomes Luong Anh Tuan Madhuri Kumari Mark Goddard Monty Taylor Nguyen Hai Truong Nguyen Hung Phuong Nisha Brahmankar OpenStack Release Bot Radosław Piliszek Riccardo Pittau Ruby Loo Sean McGinnis Takashi Kajinami Tang Chen Tao Li Yuiko Takada chenxing dparalen fpxie gengchc2 ghanshyam jacky06 jinxingfang melissaml pengyuesheng sonu.kumar wu.chunyang ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/ChangeLog0000664000175000017500000002751100000000000021410 0ustar00zuulzuul00000000000000CHANGES ======= 4.7.1 ----- * setup.cfg: Replace dashes by underscores * Re-add python 3.6/3.7 in classifier * Updating python testing as per Yoga testing runtime 4.7.0 ----- * Improve testing * Add Python3 yoga unit tests * Update master for stable/xena * Replace deprecated assertDictContainsSubset * Update min version of tox to use allowlist 4.6.0 ----- * [doc] Fix lldp\_basic name * Expose more interface fields * Add Python3 xena unit tests * Update master for stable/wallaby 4.5.0 ----- * Remove redundant items from CI jobs * Update minversion of tox * Move pep8 dependencies from test-requirements to tox.ini * Remove lower-constraints job * Set safe version of hacking * Use python 3.8 in functional tests * Fix l-c job * Add Python3 wallaby unit tests * Update master for stable/victoria 4.3.0 ----- * Provide a clear error message when trying to access ironic (not inspector) * Support retrieving unprocessed data * Tempest job to use the inherited devstack\_services value * Set min version of tox to 3.2.1 4.2.0 ----- * Use unittest.mock instead of third party mock * Use only one job for tempest * Change parent for base job to ironic-inspector-base * Update releasenotes conf * Update lower-constraints.txt * Switch to newer openstackdocstheme and reno versions * Convert job to dib * Follow up to update functest-requirements * Fix pep8 job and functional job * Upgrade flake8-import-order version to 0.17.1 * Hacking: enforce usage of autospec=True in tests * Restore default netboot boot option * Add py38 package metadata * No need to import print function anymore * Document the new standalone CLI * Add Python3 victoria unit tests * Update master for stable/ussuri * Remove python3 from job name 4.1.0 ----- * Move osc-lib to test-requirements * Get rid of the oslo.utils requirement * Make oslo.i18n an optional dependency * Cleanup py27 support * Bump hacking to 3.0.0 4.0.0 ----- * Add bindep env and fix pdf doc generation * Enforce running tox with correct python version based on env * Stop using six library * Drop python 2.7 support and testing * Fixing base job * Switch jobs to python3 * Switch to Ussuri job * Recover the functional test * Add versions to release notes series * Update the constraints url * Update master for stable/train 3.7.0 ----- * Build pdf doc * Blacklist sphinx 2.1.0 (autodoc bug) * Bump the openstackdocstheme extension to 1.20 * CI: clean up required projects * Allow running a specific functional test via CLI 3.6.1 ----- * Repair the deprecated uuid/uuids arguments * Switch base job to ironic-base 3.6.0 ----- * Update Python 3 test runtimes for Train * OSC: try fetching ironic-inspector URL from osc-lib * Update sphinx requirements * Fetch requirements from opendev * OpenDev Migration Patch * Dropping the py35 testing * Replace openstack.org git:// URLs with https:// * Deprecates "uuid" parameters to Python calls when node is expected * Update master for stable/stein * Find misteriously missing pep8 import check 3.5.0 ----- * add python 3.7 unit test job * Remove dsvm from zuulv3 job names * Zuul migration * Update tox.ini to support Python 3.x unit tests * Change openstack-dev to openstack-discuss * Follow-up check-errors flag * Add check-errors flag to Introspection * Update min tox version to 2.0 3.4.0 ----- * Change basepython to python3 * Support loading introspection rules from YAML files * add python 3.6 unit test job * switch documentation job to new PTI * import zuul job settings from project-config * CI: stop trying to pull the tempest plugin from ironic-inspector * Update reno for stable/rocky * Fix setup.cfg for release 3.3.0 ----- * Fix errors in package metadata * Update the home-page link * Provide proper error message if interface name is invalid * Add release note link in README * Support passing manage\_boot argument in Python API * Follow the new PTI for document build * fix tox python3 overrides * add lower-constraints job * Trivial: Update pypi url to new url 3.2.0 ----- * Gate fix: Cap hacking to avoid gate failure * Update bug tracker URL * Updated from global requirements * Switch the CI to hardware types and clean up playbook * Use the regular inspector tests in the CI * Update links in README * Add the api help information * Clean imports in code * Updated from global requirements * Zuul: Remove project name * Updated from global requirements * Update reno for stable/queens 3.1.0 ----- * Updated from global requirements * Updated from global requirements * Updated from global requirements * Use the tempest plugin from openstack/ironic-tempest-plugin 3.0.0 ----- * Avoid tox\_install.sh for constraints support * Add missing test on session + explicit URL * Documentation clean up * Drop default URI, make either a session or inspector\_url required * Remove support for passing auth\_token to ClientV1 * Add missing python-openstackclient to test-requirements * Remove setting of version/release from releasenotes * Centralize list of irrelevant files * Updated from global requirements * Updated from global requirements * Using --option ARGUMENT * Use functional tox environment * Move legacy python-ironic-inspector-client jobs * Remove deprecated setting ipmi creds feature * Use default tox environments for docs and releasenotes * Remove deprecated ironic\_inspector\_client.client * Functional tests: pin \*inspector\* to HEAD * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Update Inspector docs links in README * Updated from global requirements * Update reno for stable/pike 2.0.0 ----- * Make recent release note more readable * Updated from global requirements * Updated from global requirements * Update the documentation link for doc migration * Introducing warning-is-error to docs * Introducing cli and contributor content * Updated from global requirements * Switch from oslosphinx to openstackdocstheme * Replace http with https * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Functional tests: pin \*inspector\* to HEAD * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Do not depend on python-openstackclient * Updated from global requirements * Remove log translations 1.12.0 ------ * Updated from global requirements * Updated from global requirements * Add new introspection commands for interface data including lldp * Updated from global requirements * Updated from global requirements * Remove support for py34 * Update reno for stable/ocata 1.11.0 ------ * Updated from global requirements * Clarify that node names can be used in addition to UUIDs * Deprecate setting IPMI credentials * Updated from global requirements * List introspection statuses support * Updated from global requirements * Pin functest-requirements to a specific SHA * Show team and repo badges on README * Updated from global requirements * Rework handling requirements for functional tests * Update to newer hacking * UUID, started\_at, finished\_at in the status * Updated from global requirements * Updated from global requirements * Updated from global requirements * Fix functional tests after recent API changes * Updated from global requirements * Bump hacking to 0.11.0 in test-requirements * Updated from global requirements * Updated from global requirements * Enable release notes translation * Updated from global requirements * Updated from global requirements * Updated from global requirements * Move documentation from README to Sphinx * Add simple Sphinx documentation * TrivialFix: Remove logging import unused * Add oslotest to test-requirements * Update reno for stable/newton 1.9.0 ----- * Sync tools/tox\_install.sh * Using assertIsNone() is preferred over assertIs(None,..) * Switch to osc-lib instead of cliff * Add functional tests for CLI * Updated from global requirements * Increase verbosity for functional tests * Add functional test for wait\_for\_finish * Use constraints for all the things * Fix functional tests broken by the latest refactoring * Deprecate global functions in favor of ClientV1 methods * Updated from global requirements * Updated from global requirements * Add Python 3.5 tox env and detailed setup.cfg classifiers * Updated from global requirements * Add a test dependency on requests-mock 1.8.0 ----- * Updated from global requirements * Print rule import results * Updated from global requirements * Use osc-lib instead of openstackclient * Updated from global requirements * Updated from global requirements * Switch to keystoneauth * Bump max api version to 1.6 * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements 1.7.0 ----- * Updated from global requirements * Bump supported API version to 1.5 * Introspection on stored data * Updated from global requirements * Update reno for stable/mitaka * Updated from global requirements 1.5.0 ----- * Bump MAX\_API\_VERSION to actually supported 1.3 * Try to fetch inspector URL from the service catalog * Updated from global requirements * Update README: \`start\` cli doesn't rely on Ironic * Updated from global requirements * Introduce command to abort introspection * Updated from global requirements * Add a missing unit test on 'introspection status' command * Update --wait release note with an upgrade notice * Set MAX\_API\_VERSION to actually supported 1.2 * Updated from global requirements * Add --wait flag to 'introspection start' to wait for results * Updated from global requirements * Switch to accepting keystone session objects in client constructor 1.4.0 ----- * Updated from global requirements * Implement 'introspection data save' command * Stop relying on deprecated and removed support for maintenance mode in tests * Put py34 first in the env order of tox * Removes MANIFEST.in as it is not needed explicitely by PBR * Updated from global requirements * Updated from global requirements * Updated from global requirements * Fixed links to the new ironic-inspector documentation 1.3.0 ----- * Updated from global requirements * Add missing release notes * Use Reno for release notes management * Allow several UUID's in 'introspection start' * Updated from global requirements * Updated from global requirements * Updated from global requirements * Copy gitignore file from ironic-inspector * Stop assuming that all shell unit tests are for rules API * Support for getting introspection data in Python library * Updated from global requirements 1.2.0 ----- * Add a simple 'introspection rule show' command * Support for introspection rules * Fix func test job broken by Inspector func tests changes * Add a proper client object 1.1.0 ----- * Updated from global requirements * Make our README friendly to OpenStack release-tools * Updated from global requirements * Make sure we expose all API elements in the top-level package * Drop comment about changing functional tests to use released inspector * Fix error message for unsupported API version * Updated from global requirements * Updated from global requirements * Implement optional API versioning * Create own functional tests for the client * Updated from global requirements * Updated from global requirements * Updated from global requirements * Use released ironic-inspector for functional testing * Don't repeat requirements in tox.ini * Add functional test * Updated from global requirements * Change to Capital letters 1.0.1 ----- * Explicitly depend on pbr * Add venv environment for tox * Use pbr without explicit version 1.0.0 ----- * Manual update from global requirements * Switch to pbr * Basic support for API versions * After-moving code reorganization * Sync remaining changes from ironic-inspector tree * Add gitreview file * Initial import ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/LICENSE0000664000175000017500000002613600000000000020645 0ustar00zuulzuul00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4138756 python-ironic-inspector-client-4.7.1/PKG-INFO0000664000175000017500000000505300000000000020730 0ustar00zuulzuul00000000000000Metadata-Version: 2.1 Name: python-ironic-inspector-client Version: 4.7.1 Summary: Python client for Ironic Inspector Home-page: https://docs.openstack.org/python-ironic-inspector-client/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: Apache-2 Description: Ironic Inspector Client ======================= .. image:: https://governance.openstack.org/tc/badges/python-ironic-inspector-client.svg :target: https://governance.openstack.org/tc/reference/tags/index.html Overview -------- This is a client library and tool for `Ironic Inspector`_. * Free software: Apache license * Source: https://opendev.org/openstack/python-ironic-inspector-client/ * Documentation: https://docs.openstack.org/python-ironic-inspector-client/latest/ * Bugs: https://storyboard.openstack.org/#!/project/958 * Downloads: https://pypi.org/project/python-ironic-inspector-client * Release Notes: https://docs.openstack.org/releasenotes/python-ironic-inspector-client/ Please follow usual OpenStack `Gerrit Workflow`_ to submit a patch, see `Inspector contributing guide`_ for more detail. Refer to the `HTTP API reference`_ for information on the **Ironic Inspector** HTTP API. .. _Gerrit Workflow: https://docs.openstack.org/infra/manual/developers.html#development-workflow .. _Ironic Inspector: https://docs.openstack.org/ironic-inspector/latest/ .. _Inspector contributing guide: https://docs.openstack.org/ironic-inspector/latest/contributor/index.html .. _HTTP API reference: https://docs.openstack.org/ironic-inspector/latest/user/http-api.html Platform: UNKNOWN Classifier: Environment :: Console Classifier: Environment :: OpenStack Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Requires-Python: >=3.6 Provides-Extra: cli Provides-Extra: test ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/README.rst0000664000175000017500000000240200000000000021315 0ustar00zuulzuul00000000000000Ironic Inspector Client ======================= .. image:: https://governance.openstack.org/tc/badges/python-ironic-inspector-client.svg :target: https://governance.openstack.org/tc/reference/tags/index.html Overview -------- This is a client library and tool for `Ironic Inspector`_. * Free software: Apache license * Source: https://opendev.org/openstack/python-ironic-inspector-client/ * Documentation: https://docs.openstack.org/python-ironic-inspector-client/latest/ * Bugs: https://storyboard.openstack.org/#!/project/958 * Downloads: https://pypi.org/project/python-ironic-inspector-client * Release Notes: https://docs.openstack.org/releasenotes/python-ironic-inspector-client/ Please follow usual OpenStack `Gerrit Workflow`_ to submit a patch, see `Inspector contributing guide`_ for more detail. Refer to the `HTTP API reference`_ for information on the **Ironic Inspector** HTTP API. .. _Gerrit Workflow: https://docs.openstack.org/infra/manual/developers.html#development-workflow .. _Ironic Inspector: https://docs.openstack.org/ironic-inspector/latest/ .. _Inspector contributing guide: https://docs.openstack.org/ironic-inspector/latest/contributor/index.html .. _HTTP API reference: https://docs.openstack.org/ironic-inspector/latest/user/http-api.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/bindep.txt0000664000175000017500000000021300000000000021626 0ustar00zuulzuul00000000000000# libsrvg2 is needed for sphinxcontrib-svg2pdfconverter in docs builds. librsvg2-tools [doc platform:rpm] librsvg2-bin [doc platform:dpkg] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.3978746 python-ironic-inspector-client-4.7.1/doc/0000775000175000017500000000000000000000000020375 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/doc/requirements.txt0000664000175000017500000000025300000000000023661 0ustar00zuulzuul00000000000000sphinx>=2.0.0,!=2.1.0 # BSD openstackdocstheme>=2.2.0 # Apache-2.0 reno>=3.1.0 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.3978746 python-ironic-inspector-client-4.7.1/doc/source/0000775000175000017500000000000000000000000021675 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.3978746 python-ironic-inspector-client-4.7.1/doc/source/cli/0000775000175000017500000000000000000000000022444 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/doc/source/cli/index.rst0000664000175000017500000001436400000000000024315 0ustar00zuulzuul00000000000000Command Line Reference ====================== Integration for two command lines tools are provided: ironicclient_ and OpenStackClient_. Integration with ``baremetal`` CLI ---------------------------------- The ``baremetal`` command is provided by ironicclient_, so it has to be installed, e.g.: .. code-block:: shell pip install 'python-ironicclient>=4.1.0' All commands are prefixed with ``baremetal introspection``. See `standalone ironic CLI documentation`_ for details on how to use it. .. _ironicclient: https://docs.openstack.org/python-ironicclient/ .. _standalone ironic CLI documentation: https://docs.openstack.org/python-ironicclient/latest/cli/standalone.html Integration with ``openstack baremetal`` CLI -------------------------------------------- The ``openstack`` command is provided by ironicclient_, so it has to be installed, e.g.: .. code-block:: shell pip install python-openstackclient. All commands are prefixed with ``baremetal introspection``. Common arguments ~~~~~~~~~~~~~~~~ All commands accept the following arguments: * ``--inspector-url`` the **Ironic Inspector** API endpoint. If missing, the endpoint will be fetched from the service catalog. * ``--inspector-api-version`` requested API version, see :ref:`api-versioning` for details. Start introspection on a node ----------------------------- :: $ openstack baremetal introspection start [--wait] [--check-errors] NODE_ID [NODE_ID ...] * ``NODE_ID`` - Ironic node UUID or name; Note that the CLI call accepts several UUID's and will stop on the first error. .. note:: This CLI call doesn't rely on Ironic, and the introspected node will be left in ``MANAGEABLE`` state. This means that the Ironic node is not protected from other operations being performed by Ironic, which could cause inconsistency in the node's state, and lead to operational errors. With ``--wait`` flag it waits until introspection ends for all given nodes, then displays the results as a table. The ``--check-errors`` flag verifies if any error occurred during the introspection of the selected nodes while waiting for the results. If any error has occurred in the introspection result of selected nodes no output is displayed, otherwise it shows the result as a table. .. note:: ``--check-errors`` can only be used with ``--wait`` Query introspection status -------------------------- :: $ openstack baremetal introspection status NODE_ID * ``NODE_ID`` - Ironic node UUID or name. Returns following information about a node introspection status: * ``error``: an error string or ``None`` * ``finished``: ``True/False`` * ``finished_at``: an ISO8601 timestamp or ``None`` if not finished * ``started_at``: an ISO8601 timestamp * ``uuid``: node UUID List introspection statuses --------------------------- This command supports pagination. :: $ openstack baremetal introspection list [--marker] [--limit] * ``--marker`` the last item on the previous page, a UUID * ``--limit`` the amount of items to list, an integer, 50 by default Shows a table with the following columns: * ``Error``: an error string or ``None`` * ``Finished at``: an ISO8601 timestamp or ``None`` if not finished * ``Started at``: and ISO8601 timestamp * ``UUID``: node UUID .. note:: The server orders the introspection status items according to the ``Started at`` column, newer items first. Retrieving introspection data ----------------------------- :: $ openstack baremetal introspection data save [--file file_name] [--unprocessed] NODE_ID * ``NODE_ID`` - Ironic node UUID or name; * ``file_name`` - file name to save data to. If file name is not provided, the data is dumped to stdout. * ``--unprocessed`` - if set, retrieves the unprocessed data received from the ramdisk. .. note:: This feature requires Swift support to be enabled in **Ironic Inspector** by setting ``[processing]store_data`` configuration option to ``swift``. Aborting introspection ---------------------- :: $ openstack baremetal introspection abort NODE_ID * ``NODE_ID`` - Ironic node UUID or name. Reprocess stored introspection data ----------------------------------- :: $ openstack baremetal introspection reprocess NODE_ID * ``NODE_ID`` - Ironic node UUID or name. .. note:: This feature requires Swift store to be enabled for **Ironic Inspector** by setting ``[processing]store_data`` configuration option to ``swift``. Introspection Rules API ----------------------- Creating a rule ~~~~~~~~~~~~~~~ :: $ openstack baremetal introspection rule import * ``rule_json`` dictionary with a rule representation, see :py:meth:`ironic_inspector_client.RulesAPI.from_json` for details. Listing all rules ~~~~~~~~~~~~~~~~~ :: $ openstack baremetal introspection rule list Returns list of short rule representations, containing only description, UUID and links. Deleting all rules ~~~~~~~~~~~~~~~~~~ :: $ openstack baremetal introspection rule purge Deleting a rule ~~~~~~~~~~~~~~~ :: $ openstack baremetal introspection rule delete * ``UUID`` rule UUID. Using names instead of UUID --------------------------- Starting with baremetal introspection API 1.5 (provided by **Ironic Inspector** 3.3.0) it's possible to use node names instead of UUIDs in all Python and CLI calls. .. _introspection rules documentation: https://docs.openstack.org/ironic-inspector/latest/usage.html#introspection-rules List interface data ------------------- :: $ openstack baremetal introspection interface list NODE_IDENT [--fields=] [--vlan=] * ``NODE_IDENT`` - Ironic node UUID or name * ``fields`` - name of one or more interface columns to display. * ``vlan`` - list only interfaces configured for this vlan id Returns a list of interface data, including attached switch information, for each interface on the node. Show interface data ------------------- :: $ openstack baremetal introspection interface show NODE_IDENT INTERFACE [--fields=] * ``NODE_IDENT`` - Ironic node UUID or name * ``INTERFACE`` - interface name on this node * ``fields`` - name of one or more interface rows to display. Show interface data, including attached switch information, for a particular node and interface. .. _OpenStackClient: https://docs.openstack.org/python-openstackclient/latest/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/doc/source/conf.py0000664000175000017500000000434100000000000023176 0ustar00zuulzuul00000000000000# -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinxcontrib.apidoc', 'sphinx.ext.viewcode', 'openstackdocstheme', 'cliff.sphinxext' ] # sphinxcontrib.apidoc options apidoc_module_dir = '../../ironic_inspector_client' apidoc_output_dir = 'reference/api' apidoc_excluded_paths = [ 'tests', 'common/i18n*', 'shell*' ] apidoc_separate_modules = True # openstackdocstheme options openstackdocs_repo_name = 'openstack/python-ironic-inspector-client' openstackdocs_pdf_link = True openstackdocs_use_storyboard = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. copyright = u'OpenStack Foundation' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['ironic_inspector_client'] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'openstackdocs' #html_theme_path = ["."] #html_theme = '_theme' #html_static_path = ['_static'] # Output file base name for HTML help builder. htmlhelp_basename = 'python-ironic-inspector-clientdoc' latex_use_xindy = False # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ( 'index', 'doc-python-ironic-inspector-client.tex', u'Python Ironic Inspector Client Documentation', u'OpenStack Foundation', 'manual' ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/doc/source/index.rst0000664000175000017500000000063700000000000023544 0ustar00zuulzuul00000000000000================================== Welcome to Ironic Inspector Client ================================== This is a client library and tool for `Ironic Inspector`_. Contents ======== .. toctree:: :maxdepth: 2 cli/index reference/index Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _Ironic Inspector: https://docs.openstack.org/ironic-inspector/latest/ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.3978746 python-ironic-inspector-client-4.7.1/doc/source/reference/0000775000175000017500000000000000000000000023633 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/doc/source/reference/index.rst0000664000175000017500000000216500000000000025500 0ustar00zuulzuul00000000000000Library User Reference ====================== To use Python API first create a ``ClientV1`` object:: import ironic_inspector_client client = ironic_inspector_client.ClientV1(session=keystone_session) This code creates a client with API version *1.0* and a given `Keystone session`_. The service URL is fetched from the service catalog in this case. See :py:class:`ironic_inspector_client.ClientV1` documentation for details. .. _api-versioning: API Versioning -------------- Starting with version 2.1.0 **Ironic Inspector** supports optional API versioning. Version is a tuple (X, Y), where X is always 1 for now. The server has maximum and minimum supported versions. If no version is requested, the server assumes the maximum it's supported. Two constants are exposed for convenience: * :py:const:`ironic_inspector_client.DEFAULT_API_VERSION` * :py:const:`ironic_inspector_client.MAX_API_VERSION` API Reference ------------- .. toctree:: :maxdepth: 2 api/ironic_inspector_client .. toctree:: :hidden: api/modules .. _Keystone session: https://docs.openstack.org/keystoneauth/latest/using-sessions.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/functest-requirements.txt0000664000175000017500000000054200000000000024746 0ustar00zuulzuul00000000000000# NOTE(jroll) these are pinned to the same SHA, update when needed. # Last updated: October 27, 2020 (Wallaby cycle). git+https://opendev.org/openstack/ironic-inspector@5678f219cd55345633c1478c3cad22460bef4429#egg=ironic-inspector -r https://opendev.org/openstack/ironic-inspector/raw/commit/5678f219cd55345633c1478c3cad22460bef4429/test-requirements.txt././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.3978746 python-ironic-inspector-client-4.7.1/ironic_inspector_client/0000775000175000017500000000000000000000000024537 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/__init__.py0000664000175000017500000000150500000000000026651 0ustar00zuulzuul00000000000000# 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. from .v1 import ClientV1, DEFAULT_API_VERSION, MAX_API_VERSION # noqa from .common.http import ClientError, EndpointNotFound, VersionNotSupported # noqa __all__ = ['ClientV1', 'DEFAULT_API_VERSION', 'MAX_API_VERSION', 'ClientError', 'EndpointNotFound', 'VersionNotSupported'] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.3978746 python-ironic-inspector-client-4.7.1/ironic_inspector_client/common/0000775000175000017500000000000000000000000026027 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/common/__init__.py0000664000175000017500000000000000000000000030126 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/common/http.py0000664000175000017500000002333700000000000027370 0ustar00zuulzuul00000000000000# 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. """Generic code for inspector client.""" import json import logging from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import session as ks_session import requests from ironic_inspector_client.common.i18n import _ _ERROR_ENCODING = 'utf-8' LOG = logging.getLogger('ironic_inspector_client') _MIN_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Minimum-Version' _MAX_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Maximum-Version' _VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Version' _AUTH_TOKEN_HEADER = 'X-Auth-Token' def _parse_version(api_version): try: return tuple(int(x) for x in api_version.split('.')) except (ValueError, TypeError): raise ValueError(_("Malformed API version: expect tuple, string " "in form of X.Y or integer")) class ClientError(requests.HTTPError): """Error returned from a server.""" def __init__(self, response): # inspector returns error message in body msg = response.content.decode(_ERROR_ENCODING) try: msg = json.loads(msg) except ValueError: LOG.debug('Old style error response returned, assuming ' 'ironic-discoverd') except TypeError: LOG.exception('Bad error response from Ironic Inspector') else: try: msg = msg['error']['message'] except KeyError as exc: LOG.error('Invalid error response from Ironic Inspector: ' '%(msg)s (missing key %(key)s)', {'msg': msg, 'key': exc}) # It's surprisingly common to try accessing ironic URL with # ironic-inspector-client, handle this case try: msg = msg['error_message'] except KeyError: pass else: msg = _('Received Ironic-style response %s. Are you ' 'trying to access Ironic URL instead of Ironic ' 'Inspector?') % msg except TypeError: LOG.exception('Bad error response from Ironic Inspector') LOG.debug('Inspector returned error "%(msg)s" (HTTP %(code)s)', {'msg': msg, 'code': response.status_code}) super(ClientError, self).__init__(msg, response=response) @classmethod def raise_if_needed(cls, response): """Raise exception if response contains error.""" if response.status_code >= 400: raise cls(response) class VersionNotSupported(Exception): """Denotes that requested API versions is not supported by the server. :ivar expected: requested version. :ivar supported: sequence with two items: minimum and maximum actually supported versions. """ def __init__(self, expected, supported): msg = (_('Version %(expected)s is not supported by the server, ' 'supported range is %(supported)s') % {'expected': expected, 'supported': ' to '.join(str(x) for x in supported)}) self.expected_version = expected self.supported_versions = supported super(Exception, self).__init__(msg) class EndpointNotFound(Exception): """Denotes that endpoint for the introspection service was not found. :ivar service_type: requested service type """ def __init__(self, service_type): self.service_type = service_type msg = _('Endpoint of type %s was not found in the service catalog ' 'and was not provided explicitly') % service_type super(Exception, self).__init__(msg) class BaseClient(object): """Base class for clients, provides common HTTP code.""" def __init__(self, api_version, inspector_url=None, session=None, service_type='baremetal-introspection', interface=None, region_name=None): """Create a client. :param api_version: minimum API version that must be supported by the server :param inspector_url: *Ironic Inspector* URL in form: http://host:port[/ver]. When session is provided, defaults to service URL from the catalog. As a last resort defaults to ``http://:5050/v``. :param session: existing keystone session. A session without authentication is created if this is set to None. :param service_type: service type to use when looking up the URL :param interface: interface type (public, internal, etc) to use when looking up the URL :param region_name: region name to use when looking up the URL :raises: EndpointNotFound if the introspection service endpoint was not provided via inspector_url and was not found in the service catalog. """ self._base_url = inspector_url if session is None: self._session = ks_session.Session(None) else: self._session = session if not inspector_url: try: self._base_url = session.get_endpoint( service_type=service_type, interface=interface, region_name=region_name) except ks_exc.CatalogException as exc: LOG.error('%(iface)s endpoint for %(stype)s in region ' '%(region)s was not found in the service ' 'catalog: %(error)s', {'iface': interface, 'stype': service_type, 'region': region_name, 'error': exc}) raise EndpointNotFound(service_type=service_type) if not self._base_url: # This handles the case when session=None and no inspector_url is # provided, as well as keystoneauth plugins that may return None. raise EndpointNotFound(service_type=service_type) self._base_url = self._base_url.rstrip('/') self._api_version = self._check_api_version(api_version) self._version_str = '%d.%d' % self._api_version ver_postfix = '/v%d' % self._api_version[0] if not self._base_url.endswith(ver_postfix): self._base_url += ver_postfix def _add_headers(self, headers): headers[_VERSION_HEADER] = self._version_str return headers def _check_api_version(self, api_version): if isinstance(api_version, int): api_version = (api_version, 0) if isinstance(api_version, str): api_version = _parse_version(api_version) api_version = tuple(api_version) if not all(isinstance(x, int) for x in api_version): raise TypeError(_("All API version components should be integers")) if len(api_version) == 1: api_version += (0,) elif len(api_version) > 2: raise ValueError(_("API version should be of length 1 or 2")) minv, maxv = self.server_api_versions() if api_version < minv or api_version > maxv: raise VersionNotSupported(api_version, (minv, maxv)) return api_version def request(self, method, url, **kwargs): """Make an HTTP request. :param method: HTTP method :param endpoint: relative endpoint :param kwargs: arguments to pass to 'requests' library """ headers = self._add_headers(kwargs.pop('headers', {})) url = self._base_url + '/' + url.lstrip('/') LOG.debug('Requesting %(method)s %(url)s (API version %(ver)s) ' 'with %(args)s', {'method': method.upper(), 'url': url, 'ver': self._version_str, 'args': kwargs}) res = self._session.request(url, method, headers=headers, raise_exc=False, **kwargs) LOG.debug('Got response for %(method)s %(url)s with status code ' '%(code)s', {'url': url, 'method': method.upper(), 'code': res.status_code}) ClientError.raise_if_needed(res) return res def server_api_versions(self): """Get minimum and maximum supported API versions from a server. :return: tuple (minimum version, maximum version) each version is returned as a tuple (X, Y) :raises: *requests* library exception on connection problems. :raises: ValueError if returned version cannot be parsed """ res = self._session.get(self._base_url, authenticated=False, raise_exc=False) # HTTP Not Found is a valid response for older (2.0.0) servers if res.status_code >= 400 and res.status_code != 404: ClientError.raise_if_needed(res) min_ver = res.headers.get(_MIN_VERSION_HEADER, '1.0') max_ver = res.headers.get(_MAX_VERSION_HEADER, '1.0') res = (_parse_version(min_ver), _parse_version(max_ver)) LOG.debug('Supported API version range for %(url)s is ' '[%(min)s, %(max)s]', {'url': self._base_url, 'min': min_ver, 'max': max_ver}) return res ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/common/i18n.py0000664000175000017500000000156700000000000027171 0ustar00zuulzuul00000000000000# Copyright 2015 NEC Corporation # All Rights Reserved. # # 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. try: import oslo_i18n except ImportError: def _(msg): return msg else: _translators = oslo_i18n.TranslatorFactory( domain='python-ironic-inspector-client') # The primary translation function using the well-known name "_" _ = _translators.primary ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/resource.py0000664000175000017500000000773100000000000026750 0ustar00zuulzuul00000000000000 # 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. class InterfaceResource(object): """InterfaceResource class This class is used to manage the fields including Link Layer Discovery Protocols (LLDP) fields, that an interface contains. An individual field consists of a 'field_id' (key) and a 'label' (value). """ FIELDS = { 'interface': 'Interface', 'mac': 'MAC Address', 'node_ident': 'Node', 'switch_capabilities_enabled': 'Switch Capabilities Enabled', 'switch_capabilities_support': 'Switch Capabilities Supported', 'switch_chassis_id': 'Switch Chassis ID', 'switch_mgmt_addresses': 'Switch Management Addresses', 'switch_port_autonegotiation_enabled': 'Switch Port Autonegotiation Enabled', 'switch_port_autonegotiation_support': 'Switch Port Autonegotiation Supported', 'switch_port_description': 'Switch Port Description', 'switch_port_id': 'Switch Port ID', 'switch_port_link_aggregation_enabled': 'Switch Port Link Aggregation Enabled', 'switch_port_link_aggregation_support': 'Switch Port Link Aggregation Supported', 'switch_port_link_aggregation_id': 'Switch Port Link Aggregation ID', 'switch_port_management_vlan_id': 'Switch Port Mgmt VLAN ID', 'switch_port_mau_type': 'Switch Port Mau Type', 'switch_port_mtu': 'Switch Port MTU', 'switch_port_physical_capabilities': 'Switch Port Physical Capabilities', 'switch_port_protocol_vlan_enabled': 'Switch Port Protocol VLAN Enabled', 'switch_port_protocol_vlan_support': 'Switch Port Protocol VLAN Supported', 'switch_port_protocol_vlan_ids': 'Switch Port Protocol VLAN IDs', 'switch_port_untagged_vlan_id': 'Switch Port Untagged VLAN', 'switch_port_vlans': 'Switch Port VLANs', 'switch_port_vlan_ids': 'Switch Port VLAN IDs', 'switch_protocol_identities': 'Switch Protocol Identities', 'switch_system_description': 'Switch System Description', 'switch_system_name': 'Switch System Name' } """A mapping of all known interface fields to their descriptions.""" DEFAULT_FIELD_IDS = ['interface', 'mac', 'switch_port_vlan_ids', 'switch_chassis_id', 'switch_port_id'] """Interface fields displayed by default.""" def __init__(self, field_ids=None, detailed=False): """Create an InterfaceResource object :param field_ids: A list of strings that the Resource object will contain. Each string must match an existing key in FIELDS. :param detailed: If True, use the all of the keys in FIELDS instead of input field_ids """ if field_ids is None: # Default field set in logical format, so don't sort field_ids = self.DEFAULT_FIELD_IDS if detailed: field_ids = sorted(self.FIELDS.keys()) self._fields = tuple(field_ids) self._labels = tuple(self.FIELDS[x] for x in field_ids) @property def fields(self): """List of fields displayed for this resource.""" return self._fields @property def labels(self): """List of labels for fields displayed for this resource.""" return self._labels INTERFACE_DEFAULT = InterfaceResource() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/shell.py0000664000175000017500000003366300000000000026233 0ustar00zuulzuul00000000000000# 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. """OpenStackClient plugin for Ironic Inspector.""" import json import os import sys from cliff import command from cliff import lister from cliff import show import yaml import ironic_inspector_client from ironic_inspector_client.common.i18n import _ from ironic_inspector_client import resource as res API_NAME = 'baremetal_introspection' API_VERSION_OPTION = 'inspector_api_version' DEFAULT_API_VERSION = '1' API_VERSIONS = { "1": "ironic_inspector.shell", } for mversion in range(ironic_inspector_client.MAX_API_VERSION[1] + 1): API_VERSIONS["1.%d" % mversion] = API_VERSIONS["1"] def make_client(instance): url = instance.get_configuration().get('inspector_url') if not url: url = instance.get_endpoint_for_service_type( 'baremetal-introspection', interface=instance.interface, region_name=instance._region_name ) return ironic_inspector_client.ClientV1( inspector_url=url, session=instance.session, api_version=instance._api_version[API_NAME], interface=instance._interface, region_name=instance._region_name) def build_option_parser(parser): # TODO(dtantsur): deprecate these options in favor of more generic OS_* parser.add_argument('--inspector-api-version', default=(os.environ.get('INSPECTOR_VERSION') or DEFAULT_API_VERSION), help='inspector API version, only 1 is supported now ' '(env: INSPECTOR_VERSION).') parser.add_argument('--inspector-url', default=os.environ.get('INSPECTOR_URL'), help='inspector URL, defaults to localhost ' '(env: INSPECTOR_URL).') return parser class StartCommand(lister.Lister): """Start the introspection.""" COLUMNS = ('UUID', 'Error') def get_parser(self, prog_name): parser = super(StartCommand, self).get_parser(prog_name) parser.add_argument('node', help='baremetal node UUID(s) or name(s)', nargs='+') parser.add_argument('--wait', action='store_true', help='wait for introspection to finish; the result' ' will be displayed in the end') parser.add_argument('--check-errors', action='store_true', help='check if errors occurred during the' ' introspection; if any error occurs only the' ' errors are displayed; can only be used with' ' --wait') return parser def take_action(self, parsed_args): if parsed_args.check_errors and not parsed_args.wait: raise RuntimeError( _("--check-errors can only be used with --wait")) client = self.app.client_manager.baremetal_introspection for uuid in parsed_args.node: client.introspect(uuid) if parsed_args.wait: print('Waiting for introspection to finish...', file=sys.stderr) result = client.wait_for_finish(parsed_args.node) result = [(uuid, s.get('error')) for uuid, s in result.items()] if parsed_args.check_errors: uuids_errors = "\n".join("%s (%s)" % node_info for node_info in result if node_info[1] is not None) if uuids_errors: raise Exception( _("Introspection failed for some nodes: %s") % uuids_errors) else: result = [] return self.COLUMNS, result class ReprocessCommand(command.Command): """Reprocess stored introspection data""" def get_parser(self, prog_name): parser = super(ReprocessCommand, self).get_parser(prog_name) parser.add_argument('node', help='baremetal node UUID or name') return parser def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection client.reprocess(parsed_args.node) class StatusCommand(show.ShowOne): """Get introspection status.""" hidden_status_items = {'links'} @classmethod def status_attributes(cls, client_item): """Get status attributes from an API client dict. Filters the status fields according to the cls.hidden_status_items :param client_item: an item returned from either the get_status or the list_statuses client method :return: introspection status as a list of name, value pairs """ return [item for item in client_item.items() if item[0] not in cls.hidden_status_items] def get_parser(self, prog_name): parser = super(StatusCommand, self).get_parser(prog_name) parser.add_argument('node', help='baremetal node UUID or name') return parser def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection status = client.get_status(parsed_args.node) return zip(*sorted(self.status_attributes(status))) class StatusListCommand(lister.Lister): """List introspection statuses""" COLUMNS = ('UUID', 'Started at', 'Finished at', 'Error') MAPPING = dict(zip(COLUMNS, ['uuid', 'started_at', 'finished_at', 'error'])) @classmethod def status_row(cls, client_item): """Get a row from a client_item. The row columns are filtered&sorted according to cls.COLUMNS. :param client_item: an item returned from either the get_status or the list_statuses client method. :return: a list of client_item attributes as the row """ status = dict(StatusCommand.status_attributes(client_item)) return tuple(status.get(cls.MAPPING[item]) for item in cls.COLUMNS) def get_parser(self, prog_name): parser = super(StatusListCommand, self).get_parser(prog_name) parser.add_argument('--marker', help='UUID of the last item on the ' 'previous page', default=None) parser.add_argument('--limit', help='the amount of items to return', type=int, default=None) return parser def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection statuses = client.list_statuses(marker=parsed_args.marker, limit=parsed_args.limit) rows = [self.status_row(status) for status in statuses] return self.COLUMNS, rows class AbortCommand(command.Command): """Abort running introspection for node.""" def get_parser(self, prog_name): parser = super(AbortCommand, self).get_parser(prog_name) parser.add_argument('node', help='baremetal node UUID or name') return parser def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection client.abort(parsed_args.node) class RuleImportCommand(lister.Lister): """Import one or several introspection rules from a JSON/YAML file.""" COLUMNS = ("UUID", "Description") def get_parser(self, prog_name): parser = super(RuleImportCommand, self).get_parser(prog_name) parser.add_argument('file', help='JSON or YAML file to import, may ' 'contain one or several rules') return parser def take_action(self, parsed_args): with open(parsed_args.file, 'r') as fp: rules = yaml.safe_load(fp) if not isinstance(rules, list): rules = [rules] client = self.app.client_manager.baremetal_introspection result = [] for rule in rules: result.append(client.rules.from_json(rule)) result = [tuple(rule.get(col.lower()) for col in self.COLUMNS) for rule in result] return self.COLUMNS, result class RuleListCommand(lister.Lister): """List all introspection rules.""" COLUMNS = ("UUID", "Description") def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection rules = client.rules.get_all() rules = [tuple(rule.get(col.lower()) for col in self.COLUMNS) for rule in rules] return self.COLUMNS, rules class RuleShowCommand(show.ShowOne): """Show an introspection rule.""" def get_parser(self, prog_name): parser = super(RuleShowCommand, self).get_parser(prog_name) parser.add_argument('uuid', help='rule UUID') return parser def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection rule = client.rules.get(parsed_args.uuid) del rule['links'] return self.dict2columns(rule) class RuleDeleteCommand(command.Command): """Delete an introspection rule.""" def get_parser(self, prog_name): parser = super(RuleDeleteCommand, self).get_parser(prog_name) parser.add_argument('uuid', help='rule UUID') return parser def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection client.rules.delete(parsed_args.uuid) class RulePurgeCommand(command.Command): """Drop all introspection rules.""" def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection client.rules.delete_all() class DataSaveCommand(command.Command): """Save or display raw introspection data.""" def get_parser(self, prog_name): parser = super(DataSaveCommand, self).get_parser(prog_name) parser.add_argument("--file", metavar="", help="downloaded introspection data filename " "(default: stdout)") parser.add_argument('--unprocessed', action='store_true', help="download the unprocessed data") parser.add_argument('node', help='baremetal node UUID or name') return parser def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection data = client.get_data(parsed_args.node, raw=bool(parsed_args.file), processed=not parsed_args.unprocessed) if parsed_args.file: with open(parsed_args.file, 'wb') as fp: fp.write(data) else: json.dump(data, sys.stdout) class InterfaceListCommand(lister.Lister): """List interface data including attached switch port information.""" def get_parser(self, prog_name): parser = super(InterfaceListCommand, self).get_parser(prog_name) parser.add_argument('node_ident', help='baremetal node UUID or name') parser.add_argument("--vlan", action='append', default=[], type=int, help="List only interfaces configured " "for this vlan id, can be repeated") display_group = parser.add_mutually_exclusive_group() display_group.add_argument( '--long', dest='detail', action='store_true', default=False, help="Show detailed information about interfaces.") display_group.add_argument( '--fields', nargs='+', dest='fields', metavar='', choices=sorted(res.InterfaceResource(detailed=True).fields), help="Display one or more fields. " "Can not be used when '--long' is specified") return parser def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection # If --long defined, use all fields interface_res = res.InterfaceResource(parsed_args.fields, parsed_args.detail) rows = client.get_all_interface_data(parsed_args.node_ident, interface_res.fields, vlan=parsed_args.vlan) return interface_res.labels, rows class InterfaceShowCommand(show.ShowOne): """Show interface data including attached switch port information.""" COLUMNS = ("Field", "Value") def get_parser(self, prog_name): parser = super(InterfaceShowCommand, self).get_parser(prog_name) parser.add_argument('node_ident', help='baremetal node UUID or name') parser.add_argument('interface', help='interface name') parser.add_argument( '--fields', nargs='+', dest='fields', metavar='', choices=sorted(res.InterfaceResource(detailed=True).fields), help="Display one or more fields.") return parser def take_action(self, parsed_args): client = self.app.client_manager.baremetal_introspection if parsed_args.fields: interface_res = res.InterfaceResource(parsed_args.fields) else: # Show all fields in detailed resource interface_res = res.InterfaceResource(detailed=True) iface_dict = client.get_interface_data(parsed_args.node_ident, parsed_args.interface, interface_res.fields) return tuple(zip(*(iface_dict.items()))) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.3978746 python-ironic-inspector-client-4.7.1/ironic_inspector_client/tests/0000775000175000017500000000000000000000000025701 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/tests/__init__.py0000664000175000017500000000000000000000000030000 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/tests/functional.py0000664000175000017500000004464100000000000030426 0ustar00zuulzuul00000000000000# 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. import json import os import sys import tempfile import unittest from unittest import mock import eventlet eventlet.monkey_patch() # noqa from ironic_inspector import introspection_state as istate from ironic_inspector import process from ironic_inspector.test import functional from keystoneauth1 import session as ks_session from keystoneauth1 import token_endpoint from oslo_concurrency import processutils import ironic_inspector_client as client from ironic_inspector_client import shell class TestV1PythonAPI(functional.Base): def setUp(self): super(TestV1PythonAPI, self).setUp() self.auth = token_endpoint.Token(endpoint='http://127.0.0.1:5050', token='token') self.session = ks_session.Session(self.auth) self.client = client.ClientV1(session=self.session) def my_status_index(self, statuses): my_status = self._fake_status() return statuses.index(my_status) def test_introspect_get_status(self): self.client.introspect(self.uuid) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.cli.set_node_power_state.assert_called_once_with(self.uuid, 'rebooting') status = self.client.get_status(self.uuid) self.check_status(status, finished=False, state=istate.States.waiting) res = self.call_continue(self.data) self.assertEqual({'uuid': self.uuid}, res) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.assertCalledWithPatch(self.patch, self.cli.patch_node) self.cli.create_port.assert_called_once_with( node_uuid=self.uuid, address='11:22:33:44:55:66', is_pxe_enabled=True, extra={}) status = self.client.get_status(self.uuid) self.check_status(status, finished=True, state=istate.States.finished) def test_introspect_list_statuses(self): self.client.introspect(self.uuid) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.cli.set_node_power_state.assert_called_once_with(self.uuid, 'rebooting') statuses = self.client.list_statuses() my_status = statuses[self.my_status_index(statuses)] self.check_status(my_status, finished=False, state=istate.States.waiting) res = self.call_continue(self.data) self.assertEqual({'uuid': self.uuid}, res) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.assertCalledWithPatch(self.patch, self.cli.patch_node) self.cli.create_port.assert_called_once_with( node_uuid=self.uuid, address='11:22:33:44:55:66', is_pxe_enabled=True, extra={}) statuses = self.client.list_statuses() my_status = statuses[self.my_status_index(statuses)] self.check_status(my_status, finished=True, state=istate.States.finished) def test_wait_for_finish(self): shared = [0] # mutable structure to hold number of retries def fake_waiter(delay): shared[0] += 1 if shared[0] == 2: # On the second wait simulate data arriving res = self.call_continue(self.data) self.assertEqual({'uuid': self.uuid}, res) elif shared[0] > 2: # Just wait afterwards eventlet.greenthread.sleep(delay) self.client.introspect(self.uuid) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) status = self.client.get_status(self.uuid) self.check_status(status, finished=False, state=istate.States.waiting) self.client.wait_for_finish([self.uuid], sleep_function=fake_waiter, retry_interval=functional.DEFAULT_SLEEP) status = self.client.get_status(self.uuid) self.check_status(status, finished=True, state=istate.States.finished) def test_reprocess_stored_introspection_data(self): port_create_call = mock.call(node_uuid=self.uuid, address='11:22:33:44:55:66', is_pxe_enabled=True, extra={}) # assert reprocessing doesn't work before introspection self.assertRaises(client.ClientError, self.client.reprocess, self.uuid) self.client.introspect(self.uuid) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.cli.set_node_power_state.assert_called_once_with(self.uuid, 'rebooting') status = self.client.get_status(self.uuid) self.check_status(status, finished=False, state=istate.States.waiting) res = self.call_continue(self.data) self.assertEqual({'uuid': self.uuid}, res) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) status = self.client.get_status(self.uuid) self.check_status(status, finished=True, state=istate.States.finished) self.cli.create_port.assert_has_calls([port_create_call], any_order=True) res = self.client.reprocess(self.uuid) self.assertEqual(202, res.status_code) self.assertEqual('{}\n', res.text) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.check_status(status, finished=True, state=istate.States.finished) self.cli.create_port.assert_has_calls([port_create_call, port_create_call], any_order=True) def test_abort_introspection(self): # assert abort doesn't work before introspect request self.assertRaises(client.ClientError, self.client.abort, self.uuid) self.client.introspect(self.uuid) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.cli.set_node_power_state.assert_called_once_with(self.uuid, 'rebooting') status = self.client.get_status(self.uuid) self.check_status(status, finished=False, state=istate.States.waiting) res = self.client.abort(self.uuid) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.assertEqual(202, res.status_code) self.assertEqual('{}\n', res.text) status = self.client.get_status(self.uuid) self.check_status(status, finished=True, state=istate.States.error, error='Canceled by operator') # assert continue doesn't work after abort self.call_continue(self.data, expect_error=400) def test_api_versions(self): minv, maxv = self.client.server_api_versions() self.assertEqual((1, 0), minv) self.assertGreaterEqual(maxv, (1, 0)) self.assertLess(maxv, (2, 0)) def test_client_init(self): self.assertRaises(client.VersionNotSupported, client.ClientV1, session=self.session, api_version=(1, 999)) self.assertRaises(client.VersionNotSupported, client.ClientV1, session=self.session, api_version=2) self.assertTrue(client.ClientV1( api_version=1, session=self.session).server_api_versions()) self.assertTrue(client.ClientV1( api_version='1.0', session=self.session).server_api_versions()) self.assertTrue(client.ClientV1( api_version=(1, 0), session=self.session).server_api_versions()) self.assertTrue( client.ClientV1(inspector_url='http://127.0.0.1:5050') .server_api_versions()) self.assertTrue( client.ClientV1(inspector_url='http://127.0.0.1:5050/v1') .server_api_versions()) def test_rules_api(self): res = self.client.rules.get_all() self.assertEqual([], res) rule = {'conditions': [], 'actions': [{'action': 'fail', 'message': 'boom'}], 'description': 'Cool actions', 'scope': None, 'uuid': self.uuid} res = self.client.rules.from_json(rule) self.assertEqual(self.uuid, res['uuid']) rule['links'] = res['links'] self.assertEqual(rule, res) res = self.client.rules.get(self.uuid) self.assertEqual(rule, res) res = self.client.rules.get_all() self.assertEqual(rule['links'], res[0].pop('links')) self.assertEqual([{'uuid': self.uuid, 'description': 'Cool actions', 'scope': None}], res) self.client.rules.delete(self.uuid) res = self.client.rules.get_all() self.assertEqual([], res) for _ in range(3): res = self.client.rules.create(conditions=rule['conditions'], actions=rule['actions'], description=rule['description']) self.assertTrue(res['uuid']) for key in ('conditions', 'actions', 'description'): self.assertEqual(rule[key], res[key]) res = self.client.rules.get_all() self.assertEqual(3, len(res)) self.client.rules.delete_all() res = self.client.rules.get_all() self.assertEqual([], res) self.assertRaises(client.ClientError, self.client.rules.get, self.uuid) self.assertRaises(client.ClientError, self.client.rules.delete, self.uuid) BASE_CMD = [os.path.join(sys.prefix, 'bin', 'openstack'), '--os-auth-type', 'none', '--os-endpoint', 'http://127.0.0.1:5050'] class BaseCLITest(functional.Base): def openstack(self, cmd, expect_error=False, parse_json=False): real_cmd = BASE_CMD + cmd if parse_json: real_cmd += ['-f', 'json'] try: out, _err = processutils.execute(*real_cmd) except processutils.ProcessExecutionError as exc: if expect_error: return exc.stderr else: raise else: if expect_error: raise AssertionError('Command %s returned unexpected success' % cmd) elif parse_json: return json.loads(out) else: return out def run_cli(self, *cmd, **kwargs): return self.openstack(['baremetal', 'introspection'] + list(cmd), **kwargs) class TestCLI(BaseCLITest): def setup_lldp(self): self.all_interfaces = { 'eth1': {'mac': self.macs[0], 'ip': self.ips[0], 'client_id': None, 'lldp_processed': {'switch_chassis_id': "11:22:33:aa:bb:cc", 'switch_port_vlans': [{"name": "vlan101", "id": 101}, {"name": "vlan102", "id": 102}, {"name": "vlan104", "id": 104}, {"name": "vlan201", "id": 201}, {"name": "vlan203", "id": 203}], 'switch_port_id': "554", 'switch_port_mtu': 1514}}, 'eth3': {'mac': self.macs[1], 'ip': None, 'client_id': None, 'lldp_processed': {'switch_chassis_id': "11:22:33:aa:bb:cc", 'switch_port_vlans': [{"name": "vlan101", "id": 101}, {"name": "vlan102", "id": 102}, {"name": "vlan104", "id": 106}], 'switch_port_id': "557", 'switch_port_mtu': 9216}} } self.data['all_interfaces'] = self.all_interfaces def _fake_status(self, **kwargs): # to remove the hidden fields hidden_status_items = shell.StatusCommand.hidden_status_items fake_status = super(TestCLI, self)._fake_status(**kwargs) fake_status = dict(item for item in fake_status.items() if item[0] not in hidden_status_items) return fake_status def test_cli_negative(self): msg_missing_param = 'the following arguments are required' err = self.run_cli('start', expect_error=True) self.assertIn(msg_missing_param, err) err = self.run_cli('status', expect_error=True) self.assertIn(msg_missing_param, err) err = self.run_cli('rule', 'show', 'uuid', expect_error=True) self.assertIn('not found', err) err = self.run_cli('rule', 'delete', 'uuid', expect_error=True) self.assertIn('not found', err) err = self.run_cli('interface', 'list', expect_error=True) self.assertIn(msg_missing_param, err) def test_introspect_get_status(self): self.run_cli('start', self.uuid) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.cli.set_node_power_state.assert_called_once_with(self.uuid, 'rebooting') status = self.run_cli('status', self.uuid, parse_json=True) self.check_status(status, finished=False, state=istate.States.waiting) res = self.call_continue(self.data) self.assertEqual({'uuid': self.uuid}, res) eventlet.greenthread.sleep(functional.DEFAULT_SLEEP) self.assertCalledWithPatch(self.patch, self.cli.patch_node) self.cli.create_port.assert_called_once_with( node_uuid=self.uuid, address='11:22:33:44:55:66', is_pxe_enabled=True, extra={}) status = self.run_cli('status', self.uuid, parse_json=True) self.check_status(status, finished=True, state=istate.States.finished) def test_rules_api(self): res = self.run_cli('rule', 'list', parse_json=True) self.assertEqual([], res) rule = {'conditions': [], 'actions': [{'action': 'fail', 'message': 'boom'}], 'description': 'Cool actions', 'scope': None, 'uuid': self.uuid} with tempfile.NamedTemporaryFile(mode='w') as fp: json.dump(rule, fp) fp.flush() res = self.run_cli('rule', 'import', fp.name, parse_json=True) self.assertEqual([{'UUID': self.uuid, 'Description': 'Cool actions'}], res) res = self.run_cli('rule', 'show', self.uuid, parse_json=True) self.assertEqual(rule, res) res = self.run_cli('rule', 'list', parse_json=True) self.assertEqual([{'UUID': self.uuid, 'Description': 'Cool actions'}], res) self.run_cli('rule', 'delete', self.uuid) res = self.run_cli('rule', 'list', parse_json=True) self.assertEqual([], res) with tempfile.NamedTemporaryFile(mode='w') as fp: rule.pop('uuid') json.dump([rule, rule], fp) fp.flush() res = self.run_cli('rule', 'import', fp.name, parse_json=True) self.run_cli('rule', 'purge') res = self.run_cli('rule', 'list', parse_json=True) self.assertEqual([], res) @mock.patch.object(process, 'get_introspection_data', autospec=True) def test_interface_list(self, get_mock): self.setup_lldp() get_mock.return_value = json.dumps(self.data) expected_eth1 = {u'Interface': u'eth1', u'MAC Address': u'11:22:33:44:55:66', u'Switch Chassis ID': u'11:22:33:aa:bb:cc', u'Switch Port ID': u'554', u'Switch Port VLAN IDs': [101, 102, 104, 201, 203]} expected_eth3 = {u'Interface': u'eth3', u'MAC Address': u'66:55:44:33:22:11', u'Switch Chassis ID': u'11:22:33:aa:bb:cc', u'Switch Port ID': u'557', u'Switch Port VLAN IDs': [101, 102, 106]} res = self.run_cli('interface', 'list', self.uuid, parse_json=True) self.assertIn(expected_eth1, res) self.assertIn(expected_eth3, res) # Filter on vlan res = self.run_cli('interface', 'list', self.uuid, '--vlan', '106', parse_json=True) self.assertIn(expected_eth3, res) # Select fields res = self.run_cli('interface', 'list', self.uuid, '--fields', 'switch_port_mtu', parse_json=True) self.assertIn({u'Switch Port MTU': 1514}, res) self.assertIn({u'Switch Port MTU': 9216}, res) @mock.patch.object(process, 'get_introspection_data', autospec=True) def test_interface_show(self, get_mock): self.setup_lldp() get_mock.return_value = json.dumps(self.data) res = self.run_cli('interface', 'show', self.uuid, "eth1", parse_json=True) expected = {u'interface': u'eth1', u'mac': u'11:22:33:44:55:66', u'switch_chassis_id': u'11:22:33:aa:bb:cc', u'switch_port_id': u'554', u'switch_port_mtu': 1514, u'switch_port_vlan_ids': [101, 102, 104, 201, 203], u'switch_port_vlans': [{u'id': 101, u'name': u'vlan101'}, {u'id': 102, u'name': u'vlan102'}, {u'id': 104, u'name': u'vlan104'}, {u'id': 201, u'name': u'vlan201'}, {u'id': 203, u'name': u'vlan203'}]} self.assertLessEqual(expected.items(), res.items()) if __name__ == '__main__': if len(sys.argv) > 1: test_name = sys.argv[1] else: test_name = None with functional.mocked_server(): unittest.main(verbosity=2, defaultTest=test_name) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4018748 python-ironic-inspector-client-4.7.1/ironic_inspector_client/tests/unit/0000775000175000017500000000000000000000000026660 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/tests/unit/__init__.py0000664000175000017500000000000000000000000030757 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/tests/unit/test_common_http.py0000664000175000017500000002003200000000000032615 0ustar00zuulzuul00000000000000# 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. import json import unittest from unittest import mock from keystoneauth1 import exceptions from keystoneauth1 import session from ironic_inspector_client.common import http class TestCheckVersion(unittest.TestCase): @mock.patch.object(http.BaseClient, 'server_api_versions', lambda *args, **kwargs: ((1, 0), (1, 99))) def _check(self, version): cli = http.BaseClient(1, inspector_url='http://127.0.0.1:5050') return cli._check_api_version(version) def test_tuple(self): self.assertEqual((1, 0), self._check((1, 0))) def test_small_tuple(self): self.assertEqual((1, 0), self._check((1,))) def test_int(self): self.assertEqual((1, 0), self._check(1)) def test_str(self): self.assertEqual((1, 0), self._check("1.0")) def test_invalid_tuple(self): self.assertRaises(TypeError, self._check, (1, "x")) self.assertRaises(ValueError, self._check, (1, 2, 3)) def test_invalid_str(self): self.assertRaises(ValueError, self._check, "a.b") self.assertRaises(ValueError, self._check, "1.2.3") self.assertRaises(ValueError, self._check, "foo") def test_unsupported(self): self.assertRaises(http.VersionNotSupported, self._check, (99, 42)) FAKE_HEADERS = { http._MIN_VERSION_HEADER: '1.0', http._MAX_VERSION_HEADER: '1.9' } @mock.patch.object(session.Session, 'get', autospec=True, **{'return_value.status_code': 200, 'return_value.headers': FAKE_HEADERS}) class TestServerApiVersions(unittest.TestCase): def _check(self, current=1): return http.BaseClient( api_version=current, inspector_url='http://127.0.0.1:5050').server_api_versions() def test_no_headers(self, mock_get): mock_get.return_value.headers = {} minv, maxv = self._check() self.assertEqual((1, 0), minv) self.assertEqual((1, 0), maxv) def test_with_headers(self, mock_get): mock_get.return_value.headers = { 'X-OpenStack-Ironic-Inspector-API-Minimum-Version': '1.1', 'X-OpenStack-Ironic-Inspector-API-Maximum-Version': '1.42', } minv, maxv = self._check(current=(1, 2)) self.assertEqual((1, 1), minv) self.assertEqual((1, 42), maxv) def test_with_404(self, mock_get): mock_get.return_value.status_code = 404 mock_get.return_value.headers = {} minv, maxv = self._check() self.assertEqual((1, 0), minv) self.assertEqual((1, 0), maxv) def test_with_other_error(self, mock_get): mock_get.return_value.status_code = 500 mock_get.return_value.headers = {} self.assertRaises(http.ClientError, self._check) class TestRequest(unittest.TestCase): base_url = 'http://127.0.0.1:5050/v1' def setUp(self): super(TestRequest, self).setUp() self.headers = {http._VERSION_HEADER: '1.0'} self.session = mock.Mock(spec=session.Session) self.session.get_endpoint.return_value = self.base_url self.req = self.session.request self.req.return_value.status_code = 200 @mock.patch.object(http.BaseClient, 'server_api_versions', lambda self: ((1, 0), (1, 42))) def get_client(self, version=1, inspector_url=None, use_session=True): if use_session: return http.BaseClient(version, session=self.session, inspector_url=inspector_url) else: return http.BaseClient(version, inspector_url=inspector_url) def test_ok(self): res = self.get_client().request('get', '/foo/bar') self.assertIs(self.req.return_value, res) self.req.assert_called_once_with(self.base_url + '/foo/bar', 'get', raise_exc=False, headers=self.headers) self.session.get_endpoint.assert_called_once_with( service_type='baremetal-introspection', interface=None, region_name=None) def test_no_endpoint(self): self.session.get_endpoint.return_value = None self.assertRaises(http.EndpointNotFound, self.get_client) self.session.get_endpoint.assert_called_once_with( service_type='baremetal-introspection', interface=None, region_name=None) def test_endpoint_not_found(self): self.session.get_endpoint.side_effect = exceptions.EndpointNotFound() self.assertRaises(http.EndpointNotFound, self.get_client) self.session.get_endpoint.assert_called_once_with( service_type='baremetal-introspection', interface=None, region_name=None) @mock.patch.object(session.Session, 'request', autospec=True, **{'return_value.status_code': 200}) def test_ok_no_auth(self, mock_req): res = self.get_client( use_session=False, inspector_url='http://some/host').request('get', '/foo/bar') self.assertIs(mock_req.return_value, res) mock_req.assert_called_once_with(mock.ANY, 'http://some/host/v1/foo/bar', 'get', raise_exc=False, headers=self.headers) def test_ok_with_session_and_url(self): res = self.get_client( use_session=True, inspector_url='http://some/host').request('get', '/foo/bar') self.assertIs(self.req.return_value, res) self.req.assert_called_once_with('http://some/host/v1/foo/bar', 'get', raise_exc=False, headers=self.headers) def test_explicit_version(self): res = self.get_client(version='1.2').request('get', '/foo/bar') self.assertIs(self.req.return_value, res) self.headers[http._VERSION_HEADER] = '1.2' self.req.assert_called_once_with(self.base_url + '/foo/bar', 'get', raise_exc=False, headers=self.headers) def test_error(self): self.req.return_value.status_code = 400 self.req.return_value.content = json.dumps( {'error': {'message': 'boom'}}).encode('utf-8') self.assertRaisesRegex(http.ClientError, 'boom', self.get_client().request, 'get', 'url') def test_error_discoverd_style(self): self.req.return_value.status_code = 400 self.req.return_value.content = b'boom' self.assertRaisesRegex(http.ClientError, 'boom', self.get_client().request, 'get', 'url') def test_accessing_ironic(self): self.req.return_value.status_code = 400 self.req.return_value.content = json.dumps( {"error_message": "{\"code\": 404, \"title\": \"Not Found\", " "\"description\": \"\"}"}).encode('utf-8') self.assertRaisesRegex(http.ClientError, 'Ironic-style response.*Not Found', self.get_client().request, 'get', 'url') def test_error_non_sense(self): self.req.return_value.status_code = 400 self.req.return_value.content = json.dumps( {'hello': 'world'}).encode('utf-8') self.assertRaisesRegex(http.ClientError, 'hello', self.get_client().request, 'get', 'url') def test_error_non_sense2(self): self.req.return_value.status_code = 400 self.req.return_value.content = b'42' self.assertRaisesRegex(http.ClientError, '42', self.get_client().request, 'get', 'url') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/tests/unit/test_init.py0000664000175000017500000000220300000000000031231 0ustar00zuulzuul00000000000000# 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. import types import unittest import ironic_inspector_client class TestExposedAPI(unittest.TestCase): def test_only_client_all_exposed(self): exposed = {x for x in dir(ironic_inspector_client) if not x.startswith('__') and not isinstance(getattr(ironic_inspector_client, x), types.ModuleType)} self.assertEqual({'ClientV1', 'ClientError', 'EndpointNotFound', 'VersionNotSupported', 'MAX_API_VERSION', 'DEFAULT_API_VERSION'}, exposed) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/tests/unit/test_shell.py0000664000175000017500000005564300000000000031415 0ustar00zuulzuul00000000000000# 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. import collections import io import sys import tempfile from unittest import mock from osc_lib.tests import utils from ironic_inspector_client import shell from ironic_inspector_client import v1 class BaseTest(utils.TestCommand): def setUp(self): super(BaseTest, self).setUp() self.client = mock.Mock(spec=v1.ClientV1) self.rules_api = mock.Mock(spec=v1.RulesAPI) self.client.rules = self.rules_api self.app.client_manager.baremetal_introspection = self.client class TestIntrospect(BaseTest): def test_introspect_one(self): arglist = ['uuid1'] verifylist = [('node', arglist)] cmd = shell.StartCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) result = cmd.take_action(parsed_args) self.assertEqual((shell.StartCommand.COLUMNS, []), result) self.client.introspect.assert_called_once_with('uuid1') def test_introspect_many(self): arglist = ['uuid1', 'uuid2', 'uuid3'] verifylist = [('node', arglist)] cmd = shell.StartCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cmd.take_action(parsed_args) calls = [mock.call(node) for node in arglist] self.assertEqual(calls, self.client.introspect.call_args_list) def test_introspect_many_fails(self): arglist = ['uuid1', 'uuid2', 'uuid3'] verifylist = [('node', arglist)] self.client.introspect.side_effect = (None, RuntimeError()) cmd = shell.StartCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) self.assertRaises(RuntimeError, cmd.take_action, parsed_args) calls = [mock.call(node) for node in arglist[:2]] self.assertEqual(calls, self.client.introspect.call_args_list) def test_reprocess(self): node = 'uuid1' arglist = [node] verifylist = [('node', node)] response_mock = mock.Mock(status_code=202, content=b'') self.client.reprocess.return_value = response_mock cmd = shell.ReprocessCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) result = cmd.take_action(parsed_args) self.client.reprocess.assert_called_once_with(node) self.assertIsNone(result) def test_wait(self): nodes = ['uuid1', 'uuid2', 'uuid3'] arglist = ['--wait'] + nodes verifylist = [('node', nodes), ('wait', True)] self.client.wait_for_finish.return_value = { 'uuid1': {'finished': True, 'error': None}, 'uuid2': {'finished': True, 'error': 'boom'}, 'uuid3': {'finished': True, 'error': None}, } cmd = shell.StartCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) _c, values = cmd.take_action(parsed_args) calls = [mock.call(node) for node in nodes] self.assertEqual(calls, self.client.introspect.call_args_list) self.assertEqual([('uuid1', None), ('uuid2', 'boom'), ('uuid3', None)], sorted(values)) def test_wait_with_check_errors_no_raise_exception(self): nodes = ['uuid1', 'uuid2', 'uuid3'] arglist = ['--wait'] + ['--check-errors'] + nodes verifylist = [('node', nodes), ('wait', True), ('check_errors', True)] self.client.wait_for_finish.return_value = { 'uuid1': {'finished': True, 'error': None}, 'uuid2': {'finished': True, 'error': None}, 'uuid3': {'finished': True, 'error': None}, } cmd = shell.StartCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) _c, values = cmd.take_action(parsed_args) calls = [mock.call(node) for node in nodes] self.assertEqual(calls, self.client.introspect.call_args_list) self.assertEqual([('uuid1', None), ('uuid2', None), ('uuid3', None)], sorted(values)) def test_wait_with_check_errors(self): nodes = ['uuid1', 'uuid2', 'uuid3'] arglist = ['--wait'] + ['--check-errors'] + nodes verifylist = [('node', nodes), ('wait', True), ('check_errors', True)] self.client.wait_for_finish.return_value = { 'uuid1': {'finished': True, 'error': None}, 'uuid2': {'finished': True, 'error': 'boom'}, 'uuid3': {'finished': True, 'error': None}, } cmd = shell.StartCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) msg = "Introspection failed for" self.assertRaisesRegex(Exception, msg, cmd.take_action, parsed_args) def test_check_errors_alone(self): nodes = ['uuid1', 'uuid2', 'uuid3'] arglist = ['--check-errors'] + nodes verifylist = [('node', nodes), ('check_errors', True)] self.client.wait_for_finish.return_value = { 'uuid1': {'finished': True, 'error': None}, 'uuid2': {'finished': True, 'error': 'boom'}, 'uuid3': {'finished': True, 'error': None}, } cmd = shell.StartCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) msg = "--check-errors can only be used with --wait" self.assertRaisesRegex(RuntimeError, msg, cmd.take_action, parsed_args) def test_abort(self): node = 'uuid1' arglist = [node] verifylist = [('node', node)] response_mock = mock.Mock(status_code=202, content=b'') self.client.abort.return_value = response_mock cmd = shell.AbortCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) result = cmd.take_action(parsed_args) self.client.abort.assert_called_once_with(node) self.assertIsNone(result) class TestGetStatus(BaseTest): def test_get_status(self): arglist = ['uuid1'] verifylist = [('node', 'uuid1')] self.client.get_status.return_value = {'finished': True, 'error': 'boom'} cmd = shell.StatusCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) result = cmd.take_action(parsed_args) self.assertEqual([('error', 'finished'), ('boom', True)], list(result)) self.client.get_status.assert_called_once_with('uuid1') class TestStatusList(BaseTest): def setUp(self): super(TestStatusList, self).setUp() self.COLUMNS = ('UUID', 'Started at', 'Finished at', 'Error') self.status1 = { 'error': None, 'finished': True, 'finished_at': '1970-01-01T00:10', 'links': None, 'started_at': '1970-01-01T00:00', 'uuid': 'uuid1' } self.status2 = { 'error': None, 'finished': False, 'finished_at': None, 'links': None, 'started_at': '1970-01-01T00:01', 'uuid': 'uuid2' } def status_row(self, status): status = dict(item for item in status.items() if item[0] != 'links') return (status['uuid'], status['started_at'], status['finished_at'], status['error']) def test_list_statuses(self): status_list = [self.status1, self.status2] self.client.list_statuses.return_value = status_list arglist = [] verifylist = [] cmd = shell.StatusListCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) result = cmd.take_action(parsed_args) self.assertEqual((self.COLUMNS, [self.status_row(status) for status in status_list]), result) self.client.list_statuses.assert_called_once_with(limit=None, marker=None) def test_list_statuses_marker_limit(self): self.client.list_statuses.return_value = [] arglist = ['--marker', 'uuid1', '--limit', '42'] verifylist = [('marker', 'uuid1'), ('limit', 42)] cmd = shell.StatusListCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) result = cmd.take_action(parsed_args) self.assertEqual((self.COLUMNS, []), result) self.client.list_statuses.assert_called_once_with(limit=42, marker='uuid1') class TestRules(BaseTest): def test_import_single(self): f = tempfile.NamedTemporaryFile() self.addCleanup(f.close) f.write(b'{"foo": "bar"}') f.flush() arglist = [f.name] verifylist = [('file', f.name)] self.rules_api.from_json.return_value = { 'uuid': '1', 'description': 'd', 'links': []} cmd = shell.RuleImportCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) self.assertEqual(('UUID', 'Description'), cols) self.assertEqual([('1', 'd')], values) self.rules_api.from_json.assert_called_once_with({'foo': 'bar'}) def test_import_multiple(self): f = tempfile.NamedTemporaryFile() self.addCleanup(f.close) f.write(b'[{"foo": "bar"}, {"answer": 42}]') f.flush() arglist = [f.name] verifylist = [('file', f.name)] self.rules_api.from_json.side_effect = iter([ {'uuid': '1', 'description': 'd1', 'links': []}, {'uuid': '2', 'description': 'd2', 'links': []} ]) cmd = shell.RuleImportCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) self.assertEqual(('UUID', 'Description'), cols) self.assertEqual([('1', 'd1'), ('2', 'd2')], values) self.rules_api.from_json.assert_any_call({'foo': 'bar'}) self.rules_api.from_json.assert_any_call({'answer': 42}) def test_import_yaml(self): f = tempfile.NamedTemporaryFile() self.addCleanup(f.close) f.write(b"""--- - foo: bar - answer: 42 """) f.flush() arglist = [f.name] verifylist = [('file', f.name)] self.rules_api.from_json.side_effect = iter([ {'uuid': '1', 'description': 'd1', 'links': []}, {'uuid': '2', 'description': 'd2', 'links': []} ]) cmd = shell.RuleImportCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) self.assertEqual(('UUID', 'Description'), cols) self.assertEqual([('1', 'd1'), ('2', 'd2')], values) self.rules_api.from_json.assert_any_call({'foo': 'bar'}) self.rules_api.from_json.assert_any_call({'answer': 42}) def test_list(self): self.rules_api.get_all.return_value = [ {'uuid': '1', 'description': 'd1', 'links': []}, {'uuid': '2', 'description': 'd2', 'links': []} ] cmd = shell.RuleListCommand(self.app, None) parsed_args = self.check_parser(cmd, [], []) cols, values = cmd.take_action(parsed_args) self.assertEqual(('UUID', 'Description'), cols) self.assertEqual([('1', 'd1'), ('2', 'd2')], values) self.rules_api.get_all.assert_called_once_with() def test_show(self): self.rules_api.get.return_value = { 'uuid': 'uuid1', 'links': [], 'description': 'd', 'conditions': [{}], 'actions': [{}] } arglist = ['uuid1'] verifylist = [('uuid', 'uuid1')] cmd = shell.RuleShowCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) self.assertEqual(('actions', 'conditions', 'description', 'uuid'), cols) self.assertEqual(([{}], [{}], 'd', 'uuid1'), values) self.rules_api.get.assert_called_once_with('uuid1') def test_delete(self): arglist = ['uuid1'] verifylist = [('uuid', 'uuid1')] cmd = shell.RuleDeleteCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cmd.take_action(parsed_args) self.rules_api.delete.assert_called_once_with('uuid1') def test_purge(self): cmd = shell.RulePurgeCommand(self.app, None) parsed_args = self.check_parser(cmd, [], []) cmd.take_action(parsed_args) self.rules_api.delete_all.assert_called_once_with() class TestDataSave(BaseTest): def test_stdout(self): self.client.get_data.return_value = {'answer': 42} buf = io.StringIO() arglist = ['uuid1'] verifylist = [('node', 'uuid1')] cmd = shell.DataSaveCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) with mock.patch.object(sys, 'stdout', buf): cmd.take_action(parsed_args) self.assertEqual('{"answer": 42}', buf.getvalue()) self.client.get_data.assert_called_once_with('uuid1', raw=False, processed=True) def test_unprocessed(self): self.client.get_data.return_value = {'answer': 42} buf = io.StringIO() arglist = ['uuid1', '--unprocessed'] verifylist = [('node', 'uuid1'), ('unprocessed', True)] cmd = shell.DataSaveCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) with mock.patch.object(sys, 'stdout', buf): cmd.take_action(parsed_args) self.assertEqual('{"answer": 42}', buf.getvalue()) self.client.get_data.assert_called_once_with('uuid1', raw=False, processed=False) def test_file(self): self.client.get_data.return_value = b'{"answer": 42}' with tempfile.NamedTemporaryFile() as fp: arglist = ['--file', fp.name, 'uuid1'] verifylist = [('node', 'uuid1'), ('file', fp.name)] cmd = shell.DataSaveCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cmd.take_action(parsed_args) content = fp.read() self.assertEqual(b'{"answer": 42}', content) self.client.get_data.assert_called_once_with('uuid1', raw=True, processed=True) class TestInterfaceCmds(BaseTest): def setUp(self): super(TestInterfaceCmds, self).setUp() self.inspector_db = { "all_interfaces": { 'em1': {'mac': "00:11:22:33:44:55", 'ip': "10.10.1.1", "lldp_processed": { "switch_chassis_id": "99:aa:bb:cc:dd:ff", "switch_port_id": "555", "switch_port_vlans": [{"id": 101, "name": "vlan101"}, {"id": 102, "name": "vlan102"}, {"id": 104, "name": "vlan104"}, {"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}], "switch_port_mtu": 1514 } } } } def test_list(self): self.client.get_all_interface_data.return_value = [ ["em1", "00:11:22:33:44:55", [101, 102, 104, 201, 203], "99:aa:bb:cc:dd:ff", "555"], ["em2", "00:11:22:66:77:88", [201, 203], "99:aa:bb:cc:dd:ff", "777"], ["em3", "00:11:22:aa:bb:cc", '', '', '']] arglist = ['uuid1'] verifylist = [('node_ident', 'uuid1')] cmd = shell.InterfaceListCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) expected_cols = ("Interface", "MAC Address", "Switch Port VLAN IDs", "Switch Chassis ID", "Switch Port ID") # Note that em3 has no lldp data expected_rows = [["em1", "00:11:22:33:44:55", [101, 102, 104, 201, 203], "99:aa:bb:cc:dd:ff", "555"], ["em2", "00:11:22:66:77:88", [201, 203], "99:aa:bb:cc:dd:ff", "777"], ["em3", "00:11:22:aa:bb:cc", '', '', '']] self.assertEqual(expected_cols, cols) self.assertEqual(expected_rows, values) def test_list_field(self): self.client.get_all_interface_data.return_value = [ ["em1", 1514], ["em2", 9216], ["em3", '']] arglist = ['uuid1', '--fields', 'interface', "switch_port_mtu"] verifylist = [('node_ident', 'uuid1'), ('fields', ["interface", "switch_port_mtu"])] cmd = shell.InterfaceListCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) expected_cols = ("Interface", "Switch Port MTU") expected_rows = [["em1", 1514], ["em2", 9216], ["em3", '']] self.assertEqual(expected_cols, cols) self.assertEqual(expected_rows, values) def test_list_filtered(self): self.client.get_all_interface_data.return_value = [ ["em1", "00:11:22:33:44:55", [101, 102, 104, 201, 203], "99:aa:bb:cc:dd:ff", "555"]] arglist = ['uuid1', '--vlan', '104'] verifylist = [('node_ident', 'uuid1'), ('vlan', [104])] cmd = shell.InterfaceListCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) expected_cols = ("Interface", "MAC Address", "Switch Port VLAN IDs", "Switch Chassis ID", "Switch Port ID") expected_rows = [["em1", "00:11:22:33:44:55", [101, 102, 104, 201, 203], "99:aa:bb:cc:dd:ff", "555"]] self.assertEqual(expected_cols, cols) self.assertEqual(expected_rows, values) def test_list_no_data(self): self.client.get_all_interface_data.return_value = [[]] arglist = ['uuid1'] verifylist = [('node_ident', 'uuid1')] cmd = shell.InterfaceListCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) expected_cols = ("Interface", "MAC Address", "Switch Port VLAN IDs", "Switch Chassis ID", "Switch Port ID") expected_rows = [[]] self.assertEqual(expected_cols, cols) self.assertEqual(expected_rows, values) def test_show(self): self.client.get_data.return_value = self.inspector_db data = collections.OrderedDict( [('node_ident', "uuid1"), ('interface', "em1"), ('mac', "00:11:22:33:44:55"), ('switch_chassis_id', "99:aa:bb:cc:dd:ff"), ('switch_port_id', "555"), ('switch_port_mtu', 1514), ('switch_port_vlans', [{"id": 101, "name": "vlan101"}, {"id": 102, "name": "vlan102"}, {"id": 104, "name": "vlan104"}, {"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}])] ) self.client.get_interface_data.return_value = data arglist = ['uuid1', 'em1'] verifylist = [('node_ident', 'uuid1'), ('interface', 'em1')] cmd = shell.InterfaceShowCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) expected_cols = ("node_ident", "interface", "mac", "switch_chassis_id", "switch_port_id", "switch_port_mtu", "switch_port_vlans") expected_rows = ("uuid1", "em1", "00:11:22:33:44:55", "99:aa:bb:cc:dd:ff", "555", 1514, [{"id": 101, "name": "vlan101"}, {"id": 102, "name": "vlan102"}, {"id": 104, "name": "vlan104"}, {"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}]) self.assertEqual(expected_cols, cols) self.assertEqual(expected_rows, values) def test_show_field(self): self.client.get_data.return_value = self.inspector_db data = collections.OrderedDict([('node_ident', "uuid1"), ('interface', "em1"), ('switch_port_vlans', [{"id": 101, "name": "vlan101"}, {"id": 102, "name": "vlan102"}, {"id": 104, "name": "vlan104"}, {"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}]) ]) self.client.get_interface_data.return_value = data arglist = ['uuid1', 'em1', '--fields', 'node_ident', 'interface', "switch_port_vlans"] verifylist = [('node_ident', 'uuid1'), ('interface', 'em1'), ('fields', ["node_ident", "interface", "switch_port_vlans"])] cmd = shell.InterfaceShowCommand(self.app, None) parsed_args = self.check_parser(cmd, arglist, verifylist) cols, values = cmd.take_action(parsed_args) expected_cols = ("node_ident", "interface", "switch_port_vlans") expected_rows = ("uuid1", "em1", [{"id": 101, "name": "vlan101"}, {"id": 102, "name": "vlan102"}, {"id": 104, "name": "vlan104"}, {"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}]) self.assertEqual(expected_cols, cols) self.assertEqual(expected_rows, values) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/tests/unit/test_v1.py0000664000175000017500000004560300000000000030627 0ustar00zuulzuul00000000000000# 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. import collections import unittest from unittest import mock import uuid from keystoneauth1 import session import ironic_inspector_client from ironic_inspector_client.common import http from ironic_inspector_client import v1 FAKE_HEADERS = { http._MIN_VERSION_HEADER: '1.0', http._MAX_VERSION_HEADER: '1.9' } @mock.patch.object(session.Session, 'get', return_value=mock.Mock(headers=FAKE_HEADERS, status_code=200), autospec=True) class TestInit(unittest.TestCase): my_ip = 'http://127.0.0.1:5050' def get_client(self, **kwargs): kwargs.setdefault('inspector_url', self.my_ip) return ironic_inspector_client.ClientV1(**kwargs) def test_ok(self, mock_get): self.get_client() mock_get.assert_called_once_with(mock.ANY, self.my_ip, authenticated=False, raise_exc=False) def test_explicit_version(self, mock_get): self.get_client(api_version=(1, 2)) self.get_client(api_version=1) self.get_client(api_version='1.3') def test_unsupported_version(self, mock_get): self.assertRaises(ironic_inspector_client.VersionNotSupported, self.get_client, api_version=(1, 99)) self.assertRaises(ironic_inspector_client.VersionNotSupported, self.get_client, api_version=2) self.assertRaises(ironic_inspector_client.VersionNotSupported, self.get_client, api_version='1.42') def test_explicit_url(self, mock_get): self.get_client(inspector_url='http://host:port') mock_get.assert_called_once_with(mock.ANY, 'http://host:port', authenticated=False, raise_exc=False) class BaseTest(unittest.TestCase): def setUp(self): super(BaseTest, self).setUp() self.uuid = str(uuid.uuid4()) self.my_ip = 'http://127.0.0.1:5050' @mock.patch.object(http.BaseClient, 'server_api_versions', lambda self: ((1, 0), (1, 99))) def get_client(self, **kwargs): kwargs.setdefault('inspector_url', self.my_ip) return ironic_inspector_client.ClientV1(**kwargs) @mock.patch.object(http.BaseClient, 'request', autospec=True) class TestIntrospect(BaseTest): def test(self, mock_req): self.get_client().introspect(self.uuid) mock_req.assert_called_once_with( mock.ANY, 'post', '/introspection/%s' % self.uuid, params={}) def test_deprecated_uuid(self, mock_req): self.get_client().introspect(uuid=self.uuid) mock_req.assert_called_once_with( mock.ANY, 'post', '/introspection/%s' % self.uuid, params={}) def test_invalid_input(self, mock_req): self.assertRaises(TypeError, self.get_client().introspect, 42) def test_manage_boot(self, mock_req): self.get_client().introspect(self.uuid, manage_boot=False) mock_req.assert_called_once_with( mock.ANY, 'post', '/introspection/%s' % self.uuid, params={'manage_boot': '0'}) @mock.patch.object(http.BaseClient, 'request', autospec=True) class TestReprocess(BaseTest): def test(self, mock_req): self.get_client().reprocess(self.uuid) mock_req.assert_called_once_with( mock.ANY, 'post', '/introspection/%s/data/unprocessed' % self.uuid ) def test_deprecated_uuid(self, mock_req): self.get_client().reprocess(uuid=self.uuid) mock_req.assert_called_once_with( mock.ANY, 'post', '/introspection/%s/data/unprocessed' % self.uuid ) def test_invalid_input(self, mock_req): self.assertRaises(TypeError, self.get_client().reprocess, 42) self.assertFalse(mock_req.called) @mock.patch.object(http.BaseClient, 'request', autospec=True) class TestGetStatus(BaseTest): def test(self, mock_req): mock_req.return_value.json.return_value = 'json' self.get_client().get_status(self.uuid) mock_req.assert_called_once_with( mock.ANY, 'get', '/introspection/%s' % self.uuid) def test_deprecated_uuid(self, mock_req): mock_req.return_value.json.return_value = 'json' self.get_client().get_status(uuid=self.uuid) mock_req.assert_called_once_with( mock.ANY, 'get', '/introspection/%s' % self.uuid) def test_invalid_input(self, _): self.assertRaises(TypeError, self.get_client().get_status, 42) @mock.patch.object(http.BaseClient, 'request', autospec=True) class TestListStatuses(BaseTest): def test_default(self, mock_req): mock_req.return_value.json.return_value = { 'introspection': None } params = { 'marker': None, 'limit': None } self.get_client().list_statuses() mock_req.assert_called_once_with(mock.ANY, 'get', '/introspection', params=params) def test_nondefault(self, mock_req): mock_req.return_value.json.return_value = { 'introspection': None } params = { 'marker': 'uuid', 'limit': 42 } self.get_client().list_statuses(**params) mock_req.assert_called_once_with(mock.ANY, 'get', '/introspection', params=params) def test_invalid_marker(self, _): self.assertRaisesRegex(TypeError, 'Expected a string value.*', self.get_client().list_statuses, marker=42) def test_invalid_limit(self, _): self.assertRaisesRegex(TypeError, 'Expected an integer.*', self.get_client().list_statuses, limit='42') @mock.patch.object(ironic_inspector_client.ClientV1, 'get_status', autospec=True) class TestWaitForFinish(BaseTest): def setUp(self): super(TestWaitForFinish, self).setUp() self.sleep = mock.Mock(spec=[]) def test_ok(self, mock_get_st): mock_get_st.side_effect = ( [{'finished': False, 'error': None}] * 5 + [{'finished': True, 'error': None}] ) res = self.get_client().wait_for_finish(['uuid1'], sleep_function=self.sleep) self.assertEqual({'uuid1': {'finished': True, 'error': None}}, res) self.sleep.assert_called_with(v1.DEFAULT_RETRY_INTERVAL) self.assertEqual(5, self.sleep.call_count) def test_deprecated_uuids(self, mock_get_st): mock_get_st.side_effect = ( [{'finished': False, 'error': None}] * 5 + [{'finished': True, 'error': None}] ) res = self.get_client().wait_for_finish(uuids=['uuid1'], sleep_function=self.sleep) self.assertEqual({'uuid1': {'finished': True, 'error': None}}, res) self.sleep.assert_called_with(v1.DEFAULT_RETRY_INTERVAL) self.assertEqual(5, self.sleep.call_count) def test_timeout(self, mock_get_st): mock_get_st.return_value = {'finished': False, 'error': None} self.assertRaises(v1.WaitTimeoutError, self.get_client().wait_for_finish, ['uuid1'], sleep_function=self.sleep) self.sleep.assert_called_with(v1.DEFAULT_RETRY_INTERVAL) self.assertEqual(v1.DEFAULT_MAX_RETRIES, self.sleep.call_count) def test_multiple(self, mock_get_st): mock_get_st.side_effect = [ # attempt 1 {'finished': False, 'error': None}, {'finished': False, 'error': None}, {'finished': False, 'error': None}, # attempt 2 {'finished': True, 'error': None}, {'finished': False, 'error': None}, {'finished': True, 'error': 'boom'}, # attempt 3 (only uuid2) {'finished': True, 'error': None}, ] res = self.get_client().wait_for_finish(['uuid1', 'uuid2', 'uuid3'], sleep_function=self.sleep) self.assertEqual({'uuid1': {'finished': True, 'error': None}, 'uuid2': {'finished': True, 'error': None}, 'uuid3': {'finished': True, 'error': 'boom'}}, res) self.sleep.assert_called_with(v1.DEFAULT_RETRY_INTERVAL) self.assertEqual(2, self.sleep.call_count) def test_no_arguments(self, mock_get_st): self.assertRaises(TypeError, self.get_client().wait_for_finish) @mock.patch.object(http.BaseClient, 'request', autospec=True) class TestGetData(BaseTest): def test_json(self, mock_req): mock_req.return_value.json.return_value = 'json' self.assertEqual('json', self.get_client().get_data(self.uuid)) mock_req.assert_called_once_with( mock.ANY, 'get', '/introspection/%s/data' % self.uuid) def test_unprocessed(self, mock_req): mock_req.return_value.json.return_value = 'json' self.assertEqual('json', self.get_client().get_data(self.uuid, processed=False)) mock_req.assert_called_once_with( mock.ANY, 'get', '/introspection/%s/data/unprocessed' % self.uuid) def test_deprecated_uuid(self, mock_req): mock_req.return_value.json.return_value = 'json' self.assertEqual('json', self.get_client().get_data(uuid=self.uuid)) mock_req.assert_called_once_with( mock.ANY, 'get', '/introspection/%s/data' % self.uuid) def test_raw(self, mock_req): mock_req.return_value.content = b'json' self.assertEqual(b'json', self.get_client().get_data(self.uuid, raw=True)) mock_req.assert_called_once_with( mock.ANY, 'get', '/introspection/%s/data' % self.uuid) def test_invalid_input(self, _): self.assertRaises(TypeError, self.get_client().get_data, 42) @mock.patch.object(http.BaseClient, 'request', autospec=True) class TestRules(BaseTest): def get_rules(self, **kwargs): return self.get_client(**kwargs).rules def test_create(self, mock_req): self.get_rules().create([{'cond': 'cond'}], [{'act': 'act'}]) mock_req.assert_called_once_with( mock.ANY, 'post', '/rules', json={'conditions': [{'cond': 'cond'}], 'actions': [{'act': 'act'}], 'uuid': None, 'description': None}) def test_create_all_fields(self, mock_req): self.get_rules().create([{'cond': 'cond'}], [{'act': 'act'}], uuid='u', description='d') mock_req.assert_called_once_with( mock.ANY, 'post', '/rules', json={'conditions': [{'cond': 'cond'}], 'actions': [{'act': 'act'}], 'uuid': 'u', 'description': 'd'}) def test_create_invalid_input(self, mock_req): self.assertRaises(TypeError, self.get_rules().create, {}, [{'act': 'act'}]) self.assertRaises(TypeError, self.get_rules().create, [{'cond': 'cond'}], {}) self.assertRaises(TypeError, self.get_rules().create, [{'cond': 'cond'}], [{'act': 'act'}], uuid=42) self.assertFalse(mock_req.called) def test_from_json(self, mock_req): self.get_rules().from_json({'foo': 'bar'}) mock_req.assert_called_once_with( mock.ANY, 'post', '/rules', json={'foo': 'bar'}) def test_get_all(self, mock_req): mock_req.return_value.json.return_value = {'rules': ['rules']} res = self.get_rules().get_all() self.assertEqual(['rules'], res) mock_req.assert_called_once_with(mock.ANY, 'get', '/rules') def test_get(self, mock_req): mock_req.return_value.json.return_value = {'answer': 42} res = self.get_rules().get('uuid1') self.assertEqual({'answer': 42}, res) mock_req.assert_called_once_with(mock.ANY, 'get', '/rules/uuid1') def test_get_invalid_input(self, mock_req): self.assertRaises(TypeError, self.get_rules().get, 42) self.assertFalse(mock_req.called) def test_delete(self, mock_req): self.get_rules().delete('uuid1') mock_req.assert_called_once_with(mock.ANY, 'delete', '/rules/uuid1') def test_delete_invalid_input(self, mock_req): self.assertRaises(TypeError, self.get_rules().delete, 42) self.assertFalse(mock_req.called) def test_delete_all(self, mock_req): self.get_rules().delete_all() mock_req.assert_called_once_with(mock.ANY, 'delete', '/rules') @mock.patch.object(http.BaseClient, 'request', autospec=True) class TestAbort(BaseTest): def test(self, mock_req): self.get_client().abort(self.uuid) mock_req.assert_called_once_with(mock.ANY, 'post', '/introspection/%s/abort' % self.uuid) def test_invalid_input(self, _): self.assertRaises(TypeError, self.get_client().abort, 42) @mock.patch.object(http.BaseClient, 'request', autospec=True) class TestInterfaceApi(BaseTest): def setUp(self): super(TestInterfaceApi, self).setUp() self.inspector_db = { "all_interfaces": { 'em1': {'mac': "00:11:22:33:44:55", 'ip': "10.10.1.1", "lldp_processed": { "switch_chassis_id": "99:aa:bb:cc:dd:ff", "switch_port_id": "555", "switch_port_vlans": [{"id": 101, "name": "vlan101"}, {"id": 102, "name": "vlan102"}, {"id": 104, "name": "vlan104"}, {"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}], "switch_port_mtu": 1514} }, 'em2': {'mac': "00:11:22:66:77:88", 'ip': "10.10.1.2", "lldp_processed": { "switch_chassis_id": "99:aa:bb:cc:dd:ff", "switch_port_id": "777", "switch_port_vlans": [{"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}], "switch_port_mtu": 9216} }, 'em3': {'mac': "00:11:22:aa:bb:cc", 'ip': "10.10.1.2"} } } def test_all_interfaces(self, mock_req): mock_req.return_value.json.return_value = self.inspector_db fields = ['interface', 'mac', 'switch_chassis_id', 'switch_port_id', 'switch_port_vlans'] expected = [['em1', '00:11:22:33:44:55', '99:aa:bb:cc:dd:ff', '555', [{"id": 101, "name": "vlan101"}, {"id": 102, "name": "vlan102"}, {"id": 104, "name": "vlan104"}, {"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}]], ['em2', '00:11:22:66:77:88', '99:aa:bb:cc:dd:ff', '777', [{"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}]], ['em3', '00:11:22:aa:bb:cc', None, None, None]] actual = self.get_client().get_all_interface_data(self.uuid, fields) self.assertEqual(sorted(expected), sorted(actual)) # Change fields fields = ['interface', 'switch_port_mtu'] expected = [ ['em1', 1514], ['em2', 9216], ['em3', None]] actual = self.get_client().get_all_interface_data(self.uuid, fields) self.assertEqual(expected, sorted(actual)) def test_all_interfaces_filtered(self, mock_req): mock_req.return_value.json.return_value = self.inspector_db fields = ['interface', 'mac', 'switch_chassis_id', 'switch_port_id', 'switch_port_vlan_ids'] expected = [['em1', '00:11:22:33:44:55', '99:aa:bb:cc:dd:ff', '555', [101, 102, 104, 201, 203]]] # Filter on expected VLAN vlan = [104] actual = self.get_client().get_all_interface_data(self.uuid, fields, vlan=vlan) self.assertEqual(expected, actual) # VLANs don't match existing vlans vlan = [111, 555] actual = self.get_client().get_all_interface_data(self.uuid, fields, vlan=vlan) self.assertEqual([], actual) def test_one_interface(self, mock_req): mock_req.return_value.json.return_value = self.inspector_db # Note that a value for 'switch_foo' will not be found fields = ["node_ident", "interface", "mac", "switch_port_vlan_ids", "switch_chassis_id", "switch_port_id", "switch_port_mtu", "switch_port_vlans", "switch_foo"] expected_values = collections.OrderedDict( [('node_ident', self.uuid), ('interface', "em1"), ('mac', "00:11:22:33:44:55"), ('switch_port_vlan_ids', [101, 102, 104, 201, 203]), ('switch_chassis_id', "99:aa:bb:cc:dd:ff"), ('switch_port_id', "555"), ('switch_port_mtu', 1514), ('switch_port_vlans', [{"id": 101, "name": "vlan101"}, {"id": 102, "name": "vlan102"}, {"id": 104, "name": "vlan104"}, {"id": 201, "name": "vlan201"}, {"id": 203, "name": "vlan203"}]), ("switch_foo", None)]) iface_dict = self.get_client().get_interface_data( self.uuid, "em1", fields) self.assertEqual(expected_values, iface_dict) def test_invalid_interface(self, mock_req): mock_req.return_value.json.return_value = self.inspector_db self.assertRaises(ValueError, self.get_client().get_interface_data, self.uuid, "em55", ["node_ident", "interface"]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/v1.py0000664000175000017500000004740500000000000025451 0ustar00zuulzuul00000000000000# 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. """Client for V1 API.""" import collections import logging import time import warnings from ironic_inspector_client.common import http from ironic_inspector_client.common.i18n import _ DEFAULT_API_VERSION = (1, 0) """Server API version used by default.""" MAX_API_VERSION = (1, 13) """Maximum API version this client was designed to work with. This does not mean that other versions won't work at all - the server might still support them. """ # using huge timeout by default, as precise timeout should be set in # ironic-inspector settings DEFAULT_RETRY_INTERVAL = 10 """Default interval (in seconds) between retries when waiting for introspection to finish.""" DEFAULT_MAX_RETRIES = 3600 """Default number of retries when waiting for introspection to finish.""" LOG = logging.getLogger(__name__) class WaitTimeoutError(Exception): """Timeout while waiting for nodes to finish introspection.""" class ClientV1(http.BaseClient): """Client for API v1. Create this object to use Python API, for example:: import ironic_inspector_client client = ironic_inspector_client.ClientV1(session=keystone_session) This code creates a client with API version *1.0* and a given Keystone `session `_. The service URL is fetched from the service catalog in this case. Optional arguments ``service_type``, ``interface`` and ``region_name`` can be provided to modify how the URL is looked up. If the catalog lookup fails, the local host with port 5050 is tried. However, this behaviour is deprecated and should not be relied on. Also an explicit ``inspector_url`` can be passed to bypass service catalog. Optional ``api_version`` argument is a minimum API version that a server must support. It can be a tuple (MAJ, MIN), string "MAJ.MIN" or integer (only major, minimum supported minor version is assumed). :ivar rules: Reference to the introspection rules API. Instance of :py:class:`ironic_inspector_client.v1.RulesAPI`. """ def __init__(self, **kwargs): """Create a client. See :py:class:`ironic_inspector_client.common.http.HttpClient` for the list of acceptable arguments. :param kwargs: arguments to pass to the BaseClient constructor. api_version is set to DEFAULT_API_VERSION by default. """ kwargs.setdefault('api_version', DEFAULT_API_VERSION) super(ClientV1, self).__init__(**kwargs) self.rules = RulesAPI(self.request) def _check_parameters(self, node_id, uuid): """Deprecate uuid parameters. Check the parameters and return a deprecation warning if the uuid parameter is present. """ node_id = node_id or uuid if not isinstance(node_id, str): raise TypeError( _("Expected string for node_id argument, got %r") % node_id) if uuid: warnings.warn("Parameter uuid is deprecated and will be " "removed in future releases, please use " "node_id instead.", DeprecationWarning) return node_id def introspect(self, node_id=None, manage_boot=None, uuid=None): """Start introspection for a node. :param uuid: node UUID or name, deprecated :param node_id: node node_id or name :param manage_boot: whether to manage boot during introspection of this node. If it is None (the default), then this argument is not passed to API and the server default is used instead. :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported :raises: *requests* library exception on connection problems. """ node_id = self._check_parameters(node_id, uuid) params = {} if manage_boot is not None: params['manage_boot'] = str(int(manage_boot)) self.request('post', '/introspection/%s' % node_id, params=params) def reprocess(self, node_id=None, uuid=None): """Reprocess stored introspection data. If swift support is disabled, introspection data won't be stored, this request will return error response with 404 code. :param uuid: node UUID or name, deprecated :param node_id: node node_id or name :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported :raises: *requests* library exception on connection problems. :raises: TypeError if uuid is not a string. """ node_id = self._check_parameters(node_id, uuid) return self.request('post', '/introspection/%s/data/unprocessed' % node_id) def list_statuses(self, marker=None, limit=None): """List introspection statuses. Supports pagination via the marker and limit params. The items are sorted by the server according to the `started_at` attribute, newer items first. :param marker: pagination maker, UUID or None :param limit: pagination limit, int or None :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported :raises: *requests* library exception on connection problems. :return: a list of status dictionaries with the keys: * `error` an error string or None, * `finished` whether introspection was finished, * `finished_at` an ISO8601 timestamp or None, * `links` with a self-link URL, * `started_at` an ISO8601 timestamp, * `uuid` the node UUID """ if not (marker is None or isinstance(marker, str)): raise TypeError(_('Expected a string value of the marker, got ' '%s instead') % marker) if not (limit is None or isinstance(limit, int)): raise TypeError(_('Expected an integer value of the limit, got ' '%s instead') % limit) params = { 'marker': marker, 'limit': limit, } response = self.request('get', '/introspection', params=params) return response.json()['introspection'] def get_status(self, node_id=None, uuid=None): """Get introspection status for a node. :param uuid: node UUID or name, deprecated :param node_id: node node_id or name :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported :raises: *requests* library exception on connection problems. :return: dictionary with the keys: * `error` an error string or None, * `finished` whether introspection was finished, * `finished_at` an ISO8601 timestamp or None, * `links` with a self-link URL, * `started_at` an ISO8601 timestamp, * `uuid` the node UUID """ node_id = self._check_parameters(node_id, uuid) return self.request('get', '/introspection/%s' % node_id).json() def wait_for_finish(self, node_ids=None, retry_interval=DEFAULT_RETRY_INTERVAL, max_retries=DEFAULT_MAX_RETRIES, sleep_function=time.sleep, uuids=None): """Wait for introspection finishing for given nodes. :param uuids: collection of node UUIDs or names, deprecated :param node_ids: collection of node node_ids or names :param retry_interval: sleep interval between retries. :param max_retries: maximum number of retries. :param sleep_function: function used for sleeping between retries. :raises: :py:class:`ironic_inspector_client.WaitTimeoutError` on timeout :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported :raises: *requests* library exception on connection problems. :return: dictionary UUID -> status (the same as in get_status). """ result = {} node_ids = node_ids or uuids if uuids: warnings.warn("Parameter uuid is deprecated and will be " "removed in future releases, please use " "node_id instead.", DeprecationWarning) elif not node_ids: raise TypeError("The node_ids argument is required") # Number of attempts = number of retries + first attempt for attempt in range(max_retries + 1): new_active_node_ids = [] for node_id in node_ids: status = self.get_status(node_id) if status.get('finished'): result[node_id] = status else: new_active_node_ids.append(node_id) if new_active_node_ids: if attempt != max_retries: node_ids = new_active_node_ids LOG.debug('Still waiting for introspection results for ' '%(count)d nodes, attempt %(attempt)d of ' '%(total)d', {'count': len(new_active_node_ids), 'attempt': attempt + 1, 'total': max_retries + 1}) sleep_function(retry_interval) else: return result raise WaitTimeoutError(_("Timeout while waiting for introspection " "of nodes %s") % new_active_node_ids) def get_data(self, node_id=None, raw=False, uuid=None, processed=True): """Get introspection data from the last introspection of a node. If swift support is disabled, introspection data won't be stored, this request will return error response with 404 code. :param uuid: node UUID or name, deprecated :param node_id: node node_id or name :param raw: whether to return raw binary data or parsed JSON data :param processed: whether to return the final processed data or the raw unprocessed data received from the ramdisk. :returns: bytes or a dict depending on the 'raw' argument :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported :raises: *requests* library exception on connection problems. :raises: TypeError if uuid is not a string """ node_id = self._check_parameters(node_id, uuid) url = ('/introspection/%s/data' if processed else '/introspection/%s/data/unprocessed') resp = self.request('get', url % node_id) if raw: return resp.content else: return resp.json() def abort(self, node_id=None, uuid=None): """Abort running introspection for a node. :param uuid: node UUID or name, deprecated :param node_id: node node_id or name :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported :raises: *requests* library exception on connection problems. :raises: TypeError if uuid is not a string. """ node_id = self._check_parameters(node_id, uuid) return self.request('post', '/introspection/%s/abort' % node_id) def get_interface_data(self, node_ident, interface, field_sel): """Get interface data for the input node and interface To get LLDP data, collection must be enabled by the kernel parameter ``ipa-collect-lldp=1``, and a relevant inspector plugin must be enabled, e.g., ``lldp_basic``, ``local_link_connection``. :param node_ident: node UUID or name :param interface: interface name :param field_sel: list of all fields for which to get data :returns: interface data in OrderedDict :raises: ValueError if interface is not found. """ # Use OrderedDict to maintain order of user-entered fields iface_data = collections.OrderedDict() data = self.get_data(node_ident) all_interfaces = data.get('all_interfaces', []) # Make sure interface name is valid if interface not in all_interfaces: raise ValueError( _("Interface %s was not found on this node") % interface) # If lldp data not available this will still return interface, # mac, node_ident etc. lldp_proc = all_interfaces[interface].get('lldp_processed', {}) for f in field_sel: if f == 'node_ident': iface_data[f] = node_ident elif f == 'interface': iface_data[f] = interface elif f == 'mac': iface_data[f] = all_interfaces[interface].get(f) elif f == 'switch_port_vlan_ids': iface_data[f] = [item['id'] for item in lldp_proc.get('switch_port_vlans', [])] else: iface_data[f] = lldp_proc.get(f) return iface_data def get_all_interface_data(self, node_ident, field_sel, vlan=None): """Get interface data for all of the interfaces on this node :param node_ident: node UUID or name :param field_sel: list of all fields for which to get data :param vlan: list of vlans used to filter the lists returned :returns: list of interface data, each interface in a list """ # Get inventory data for this node data = self.get_data(node_ident) all_interfaces = data.get('all_interfaces', []) rows = [] if vlan: vlan = set(vlan) # walk all interfaces, appending data to row if not filtered for interface in all_interfaces: iface_dict = self.get_interface_data(node_ident, interface, field_sel) values = list(iface_dict.values()) # Use (optional) vlans to filter row if not vlan: rows.append(values) continue # curr_vlans may be None curr_vlans = iface_dict.get('switch_port_vlan_ids', []) if curr_vlans and (vlan & set(curr_vlans)): rows.append(values) # vlan matches, display this row return rows class RulesAPI(object): """Introspection rules API. Do not create instances of this class directly, use :py:attr:`ironic_inspector_client.v1.ClientV1.rules` instead. """ def __init__(self, requester): self._request = requester def create(self, conditions, actions, uuid=None, description=None): """Create a new introspection rule. :param conditions: list of rule conditions :param actions: list of rule actions :param uuid: rule UUID, will be generated if not specified :param description: optional rule description :returns: rule representation :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported """ if uuid is not None and not isinstance(uuid, str): raise TypeError( _("Expected string for uuid argument, got %r") % uuid) for name, arg in [('conditions', conditions), ('actions', actions)]: if not isinstance(arg, list) or not all(isinstance(x, dict) for x in arg): raise TypeError(_("Expected list of dicts for %(arg)s " "argument, got %(real)r"), {'arg': name, 'real': arg}) body = {'uuid': uuid, 'conditions': conditions, 'actions': actions, 'description': description} return self.from_json(body) def from_json(self, json_rule): """Import an introspection rule from JSON data. :param json_rule: rule information as a dict with keys matching arguments of :py:meth:`RulesAPI.create`. :returns: rule representation :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported """ return self._request('post', '/rules', json=json_rule).json() def get_all(self): """List all introspection rules. :returns: list of short rule representations (uuid, description and links) :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported """ return self._request('get', '/rules').json()['rules'] def get(self, uuid): """Get detailed information about an introspection rule. :param uuid: rule UUID :returns: rule representation :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported """ if not isinstance(uuid, str): raise TypeError( _("Expected string for uuid argument, got %r") % uuid) return self._request('get', '/rules/%s' % uuid).json() def delete(self, uuid): """Delete an introspection rule. :param uuid: rule UUID :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported """ if not isinstance(uuid, str): raise TypeError( _("Expected string for uuid argument, got %r") % uuid) self._request('delete', '/rules/%s' % uuid) def delete_all(self): """Delete all introspection rules. :raises: :py:class:`ironic_inspector_client.ClientError` on error reported from a server :raises: :py:class:`ironic_inspector_client.VersionNotSupported` if requested api_version is not supported """ self._request('delete', '/rules') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/ironic_inspector_client/version.py0000664000175000017500000000127300000000000026601 0ustar00zuulzuul00000000000000# 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. import pbr.version version_info = pbr.version.VersionInfo('python-ironic-inspector-client') """Installed package version.""" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4018748 python-ironic-inspector-client-4.7.1/python_ironic_inspector_client.egg-info/0000775000175000017500000000000000000000000027632 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/python_ironic_inspector_client.egg-info/PKG-INFO0000664000175000017500000000505300000000000030732 0ustar00zuulzuul00000000000000Metadata-Version: 2.1 Name: python-ironic-inspector-client Version: 4.7.1 Summary: Python client for Ironic Inspector Home-page: https://docs.openstack.org/python-ironic-inspector-client/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: Apache-2 Description: Ironic Inspector Client ======================= .. image:: https://governance.openstack.org/tc/badges/python-ironic-inspector-client.svg :target: https://governance.openstack.org/tc/reference/tags/index.html Overview -------- This is a client library and tool for `Ironic Inspector`_. * Free software: Apache license * Source: https://opendev.org/openstack/python-ironic-inspector-client/ * Documentation: https://docs.openstack.org/python-ironic-inspector-client/latest/ * Bugs: https://storyboard.openstack.org/#!/project/958 * Downloads: https://pypi.org/project/python-ironic-inspector-client * Release Notes: https://docs.openstack.org/releasenotes/python-ironic-inspector-client/ Please follow usual OpenStack `Gerrit Workflow`_ to submit a patch, see `Inspector contributing guide`_ for more detail. Refer to the `HTTP API reference`_ for information on the **Ironic Inspector** HTTP API. .. _Gerrit Workflow: https://docs.openstack.org/infra/manual/developers.html#development-workflow .. _Ironic Inspector: https://docs.openstack.org/ironic-inspector/latest/ .. _Inspector contributing guide: https://docs.openstack.org/ironic-inspector/latest/contributor/index.html .. _HTTP API reference: https://docs.openstack.org/ironic-inspector/latest/user/http-api.html Platform: UNKNOWN Classifier: Environment :: Console Classifier: Environment :: OpenStack Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Requires-Python: >=3.6 Provides-Extra: cli Provides-Extra: test ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/python_ironic_inspector_client.egg-info/SOURCES.txt0000664000175000017500000001000100000000000031506 0ustar00zuulzuul00000000000000.stestr.conf AUTHORS ChangeLog LICENSE README.rst bindep.txt functest-requirements.txt requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/requirements.txt doc/source/conf.py doc/source/index.rst doc/source/cli/index.rst doc/source/reference/index.rst ironic_inspector_client/__init__.py ironic_inspector_client/resource.py ironic_inspector_client/shell.py ironic_inspector_client/v1.py ironic_inspector_client/version.py ironic_inspector_client/common/__init__.py ironic_inspector_client/common/http.py ironic_inspector_client/common/i18n.py ironic_inspector_client/tests/__init__.py ironic_inspector_client/tests/functional.py ironic_inspector_client/tests/unit/__init__.py ironic_inspector_client/tests/unit/test_common_http.py ironic_inspector_client/tests/unit/test_init.py ironic_inspector_client/tests/unit/test_shell.py ironic_inspector_client/tests/unit/test_v1.py python_ironic_inspector_client.egg-info/PKG-INFO python_ironic_inspector_client.egg-info/SOURCES.txt python_ironic_inspector_client.egg-info/dependency_links.txt python_ironic_inspector_client.egg-info/entry_points.txt python_ironic_inspector_client.egg-info/not-zip-safe python_ironic_inspector_client.egg-info/pbr.json python_ironic_inspector_client.egg-info/requires.txt python_ironic_inspector_client.egg-info/top_level.txt releasenotes/notes/.placeholder releasenotes/notes/UUID-started_at-finished_at-in-the-status-cafa48aeb5653412.yaml releasenotes/notes/abort-introspection-428ba16c991af207.yaml releasenotes/notes/api-1.2-33f0e1956b924447.yaml releasenotes/notes/api-1.5-d5c64e5265fe56d3.yaml releasenotes/notes/api-1.6-a020f6ee5756a7ab.yaml releasenotes/notes/change-error-msg-invalid-interface-4b6b70b92c27d6f6.yaml releasenotes/notes/client-get-data-7002c1e22f14cefd.yaml releasenotes/notes/data-save-9d9d4b3ac7c9851f.yaml releasenotes/notes/deprecate-setting-ipmi-creds-1581ddc63b273811.yaml releasenotes/notes/deprecated-uuid-d0c8e8980106e7f9.yaml releasenotes/notes/drop-osc-client-requirements-efb31b432ddbb370.yaml releasenotes/notes/drop-py-2-7-c2707af650df18c8.yaml releasenotes/notes/drop-setting-ipmi-creds-feature-4965aaba75a40326.yaml releasenotes/notes/expose-more-interface-fields-de82d634c2af05e0.yaml releasenotes/notes/interface-list-show-39cedaca3cd9db9b.yaml releasenotes/notes/introspect-multiple-uuids-0790d57e0a0b9292.yaml releasenotes/notes/introspection-check-errors-587ebb2c00038b5a.yaml releasenotes/notes/introspection-wait-a7e8fe832c3aaff9.yaml releasenotes/notes/ironic-error-657d352b31ba77ed.yaml releasenotes/notes/ks-session-ac614a9abda3e228.yaml releasenotes/notes/list-introspection-statuses-4ad9e7e56823e754.yaml releasenotes/notes/manage-boot-3d77762952b354a1.yaml releasenotes/notes/no-auth-token-c486915a6168d4a3.yaml releasenotes/notes/no-default-uri-861f675ccb75e05d.yaml releasenotes/notes/old-functions-80ddae9eaa1e7e1d.yaml releasenotes/notes/osc-lib-162db03fed2bc40c.yaml releasenotes/notes/oslo-i18n-optional-194293e37274a901.yaml releasenotes/notes/print-import-rule-result-b5c19e9b8679849e.yaml releasenotes/notes/remove-client-64778b2011c26f6b.yaml releasenotes/notes/rename-func-427aa11c60c2838b.yaml releasenotes/notes/reprocess-stored-introspection-data-c4910325254426c5.yaml releasenotes/notes/rules-import-yaml-815ebc6ca6fe28b9.yaml releasenotes/notes/service-catalog-45466d1cfd330231.yaml releasenotes/notes/unprocessed-7ebb1c48427bfee4.yaml releasenotes/notes/version-uuid-b7f9e57589314fe9.yaml releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/liberty.rst releasenotes/source/mitaka.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder zuul.d/project.yaml zuul.d/python-ironic-inspector-client-jobs.yaml././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/python_ironic_inspector_client.egg-info/dependency_links.txt0000664000175000017500000000000100000000000033700 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/python_ironic_inspector_client.egg-info/entry_points.txt0000664000175000017500000000225600000000000033135 0ustar00zuulzuul00000000000000[openstack.baremetal_introspection.v1] baremetal_introspection_abort = ironic_inspector_client.shell:AbortCommand baremetal_introspection_data_save = ironic_inspector_client.shell:DataSaveCommand baremetal_introspection_interface_list = ironic_inspector_client.shell:InterfaceListCommand baremetal_introspection_interface_show = ironic_inspector_client.shell:InterfaceShowCommand baremetal_introspection_list = ironic_inspector_client.shell:StatusListCommand baremetal_introspection_reprocess = ironic_inspector_client.shell:ReprocessCommand baremetal_introspection_rule_delete = ironic_inspector_client.shell:RuleDeleteCommand baremetal_introspection_rule_import = ironic_inspector_client.shell:RuleImportCommand baremetal_introspection_rule_list = ironic_inspector_client.shell:RuleListCommand baremetal_introspection_rule_purge = ironic_inspector_client.shell:RulePurgeCommand baremetal_introspection_rule_show = ironic_inspector_client.shell:RuleShowCommand baremetal_introspection_start = ironic_inspector_client.shell:StartCommand baremetal_introspection_status = ironic_inspector_client.shell:StatusCommand [openstack.cli.extension] baremetal-introspection = ironic_inspector_client.shell ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/python_ironic_inspector_client.egg-info/not-zip-safe0000664000175000017500000000000100000000000032060 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/python_ironic_inspector_client.egg-info/pbr.json0000664000175000017500000000005600000000000031311 0ustar00zuulzuul00000000000000{"git_version": "61a323f", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/python_ironic_inspector_client.egg-info/requires.txt0000664000175000017500000000042100000000000032227 0ustar00zuulzuul00000000000000PyYAML>=3.13 cliff!=2.9.0,>=2.8.0 keystoneauth1>=3.4.0 pbr!=2.1.0,>=2.0.0 requests>=2.14.2 [cli] python-openstackclient>=3.12.0 [test] coverage>=5.0 fixtures>=3.0.0 osc-lib>=2.1.0 oslo.concurrency>=3.25.0 python-openstackclient>=3.12.0 requests-mock>=1.2.0 stestr>=2.0.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793596.0 python-ironic-inspector-client-4.7.1/python_ironic_inspector_client.egg-info/top_level.txt0000664000175000017500000000003000000000000032355 0ustar00zuulzuul00000000000000ironic_inspector_client ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.3938742 python-ironic-inspector-client-4.7.1/releasenotes/0000775000175000017500000000000000000000000022321 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4098752 python-ironic-inspector-client-4.7.1/releasenotes/notes/0000775000175000017500000000000000000000000023451 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/.placeholder0000664000175000017500000000000000000000000025722 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000022700000000000011456 xustar0000000000000000129 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/UUID-started_at-finished_at-in-the-status-cafa48aeb5653412.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/UUID-started_at-finished_at-in-the-status-ca0000664000175000017500000000022700000000000033632 0ustar00zuulzuul00000000000000--- features: - | the introspection status returns the ``error``, ``finished``, ``finished_at``, ``links``, ``started_at`` and ``uuid`` fields ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/abort-introspection-428ba16c991af207.yaml0000664000175000017500000000020500000000000032472 0ustar00zuulzuul00000000000000--- features: - Introduced command "openstack baremetal introspection abort " to abort running introspection for a node. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/api-1.2-33f0e1956b924447.yaml0000664000175000017500000000015600000000000027411 0ustar00zuulzuul00000000000000--- fixes: - Fixed MAX_API_VERSION incorrectly set to (1, 0) while API 1.2 is actually fully supported. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/api-1.5-d5c64e5265fe56d3.yaml0000664000175000017500000000006400000000000027636 0ustar00zuulzuul00000000000000--- other: - Bumped supported API version to 1.5. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/api-1.6-a020f6ee5756a7ab.yaml0000664000175000017500000000006300000000000027677 0ustar00zuulzuul00000000000000--- other: - Bumped supported API version to 1.6.././@PaxHeader0000000000000000000000000000022000000000000011447 xustar0000000000000000122 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/change-error-msg-invalid-interface-4b6b70b92c27d6f6.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/change-error-msg-invalid-interface-4b6b70b920000664000175000017500000000036200000000000033336 0ustar00zuulzuul00000000000000--- fixes: - The error message returned when running the `openstack baremetal introspection interface show` command with an interface not associated with the node has been fixed. It now indicates that the interface was invalid. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/client-get-data-7002c1e22f14cefd.yaml0000664000175000017500000000012400000000000031554 0ustar00zuulzuul00000000000000--- features: - Add client.get_data() call for getting stored introspection data. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/data-save-9d9d4b3ac7c9851f.yaml0000664000175000017500000000016200000000000030514 0ustar00zuulzuul00000000000000--- features: - Added "introspection data save" command to retrieve stored introspection data for the node. ././@PaxHeader0000000000000000000000000000021200000000000011450 xustar0000000000000000116 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/deprecate-setting-ipmi-creds-1581ddc63b273811.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/deprecate-setting-ipmi-creds-1581ddc63b273810000664000175000017500000000034000000000000033021 0ustar00zuulzuul00000000000000--- deprecations: - | Support for setting IPMI credentials via ironic-inspector is deprecated and will be removed completely in Pike. For reasoning see https://bugs.launchpad.net/ironic-inspector/+bug/1654318. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/deprecated-uuid-d0c8e8980106e7f9.yaml0000664000175000017500000000020200000000000031547 0ustar00zuulzuul00000000000000--- fixes: - | Fixes regression in 3.6.0 that caused the deprecated ``uuid`` argument to various calls to stop working. ././@PaxHeader0000000000000000000000000000021200000000000011450 xustar0000000000000000116 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/drop-osc-client-requirements-efb31b432ddbb370.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/drop-osc-client-requirements-efb31b432ddbb370000664000175000017500000000061500000000000033400 0ustar00zuulzuul00000000000000--- upgrade: - | The ``python-openstackclient`` package is no longer a requirement for ``python-ironic-inspector-client`` to be installed. If you wish to use the **ironic-inspector** cli, you should install ``python-openstackclient`` manually or use setuptools "extra" option to install both packages automatically:: pip install python-ironic-inspector-client[cli] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/drop-py-2-7-c2707af650df18c8.yaml0000664000175000017500000000037600000000000030460 0ustar00zuulzuul00000000000000--- upgrade: - | Python 2.7 support has been dropped. Last release of python-ironic-inspector-client to support Python 2.7 is OpenStack Train. The minimum version of Python now supported by python-ironic-inspector-client is Python 3.6. ././@PaxHeader0000000000000000000000000000021500000000000011453 xustar0000000000000000119 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/drop-setting-ipmi-creds-feature-4965aaba75a40326.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/drop-setting-ipmi-creds-feature-4965aaba75a40000664000175000017500000000012200000000000033304 0ustar00zuulzuul00000000000000--- upgrade: - | Experimental setting IPMI credentials feature was removed. ././@PaxHeader0000000000000000000000000000021200000000000011450 xustar0000000000000000116 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/expose-more-interface-fields-de82d634c2af05e0.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/expose-more-interface-fields-de82d634c2af05e0000664000175000017500000000016600000000000033253 0ustar00zuulzuul00000000000000--- features: - | Exposes ``switch_mgmt_addresses`` and ``switch_system_description`` in interface details. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/interface-list-show-39cedaca3cd9db9b.yaml0000664000175000017500000000060200000000000033020 0ustar00zuulzuul00000000000000--- features: - Add ``introspection interface list`` and ``introspection interface show`` commands to display stored introspection data for the node including attached switch port information. In order to get switch data, LLDP data collection must be enabled by the kernel parameter ``ipa-collect-lldp=1`` and the inspector plugin ``basic_lldp`` must be enabled. ././@PaxHeader0000000000000000000000000000020700000000000011454 xustar0000000000000000113 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/introspect-multiple-uuids-0790d57e0a0b9292.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/introspect-multiple-uuids-0790d57e0a0b9292.y0000664000175000017500000000012200000000000033063 0ustar00zuulzuul00000000000000--- features: - Allow multiple UUID's in the 'introspection start' CLI command. ././@PaxHeader0000000000000000000000000000021000000000000011446 xustar0000000000000000114 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/introspection-check-errors-587ebb2c00038b5a.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/introspection-check-errors-587ebb2c00038b5a.0000664000175000017500000000053000000000000033137 0ustar00zuulzuul00000000000000--- features: - | Adds ``--check-errors`` flag to verify if any error occurred when waiting for the introspection to finish for the selected nodes. If any error occurs no output is displayed and the exit status for the command is different from 0 (success). The ``--check-errors`` option can only be used with ``--wait``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/introspection-wait-a7e8fe832c3aaff9.yaml0000664000175000017500000000042000000000000032642 0ustar00zuulzuul00000000000000--- upgrade: - When setting IPMI credentials is requested, the introspection commands prints a notice to stdout. This was not desired, and it is not printed to stderr instead. features: - Introspection command got --wait flag to wait for introspection finish. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/ironic-error-657d352b31ba77ed.yaml0000664000175000017500000000017200000000000031164 0ustar00zuulzuul00000000000000--- fixes: - | Provides a clear error message when trying to access an ironic URL with ironic-inspector-client. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/ks-session-ac614a9abda3e228.yaml0000664000175000017500000000034500000000000030777 0ustar00zuulzuul00000000000000--- features: - Python API now supports Keystone sessions instead of only authentication token. deprecations: - Passing auth_token directly to the client object constructor is deprecated, please pass session instead. ././@PaxHeader0000000000000000000000000000021100000000000011447 xustar0000000000000000115 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/list-introspection-statuses-4ad9e7e56823e754.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/list-introspection-statuses-4ad9e7e56823e7540000664000175000017500000000023300000000000033271 0ustar00zuulzuul00000000000000--- features: - | Add support for listing introspection statuses both for the API and the CLI upgrade: - | Service max API version bumped to 1.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/manage-boot-3d77762952b354a1.yaml0000664000175000017500000000015000000000000030523 0ustar00zuulzuul00000000000000--- features: - Adds Python library support for passing ``manage_boot`` to the introspection API. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/no-auth-token-c486915a6168d4a3.yaml0000664000175000017500000000041400000000000031110 0ustar00zuulzuul00000000000000--- upgrade: - | Support for passing ``auth_token`` to ``ClientV1`` was removed. Please create a **keystoneauth** session and pass it via the ``session`` argument instead. If no ``session`` is passed, a new session without authentication is created. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/no-default-uri-861f675ccb75e05d.yaml0000664000175000017500000000070600000000000031421 0ustar00zuulzuul00000000000000--- upgrade: - | There is no longer a default introspection API endpoint. Previously, if no endpoint was requested and no endpoint found in the service catalog, ``127.0.0.1:5050`` was used by default. other: - | The ``ClientV1`` constructor now raises a new ``EndpointNotFound`` exception when no introspection API endpoint can be detected. Previously this condition was ignored and ``127.0.0.1:5050`` was used as a fallback. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/old-functions-80ddae9eaa1e7e1d.yaml0000664000175000017500000000036600000000000031640 0ustar00zuulzuul00000000000000--- deprecations: - | The following functions are deprecated in favor of ``ClientV1`` methods: * ``ironic_inspector_client.introspect`` * ``ironic_inspector_client.get_status`` * ``ironic_inspector_client.server_api_versions`` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/osc-lib-162db03fed2bc40c.yaml0000664000175000017500000000021000000000000030212 0ustar00zuulzuul00000000000000--- upgrade: - osc-lib is a package of common support modules for writing OSC plugins. So use osc-lib instead of OpenStackClient. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/oslo-i18n-optional-194293e37274a901.yaml0000664000175000017500000000027500000000000031635 0ustar00zuulzuul00000000000000--- upgrade: - | The dependency on ``oslo.i18n`` is now optional. If you would like messages from ironic-inspector-client to be translated, you need to install it explicitly. ././@PaxHeader0000000000000000000000000000020600000000000011453 xustar0000000000000000112 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/print-import-rule-result-b5c19e9b8679849e.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/print-import-rule-result-b5c19e9b8679849e.ya0000664000175000017500000000015500000000000033123 0ustar00zuulzuul00000000000000--- upgrade: - command `openstack baremetal introspection rule import` prints created rules to stdout. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/remove-client-64778b2011c26f6b.yaml0000664000175000017500000000023200000000000031157 0ustar00zuulzuul00000000000000--- upgrade: - | The deprecated module ``ironic_inspector_client.client`` was removed, please use ``ironic_inspector_client.ClientV1`` instead. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/rename-func-427aa11c60c2838b.yaml0000664000175000017500000000012500000000000030654 0ustar00zuulzuul00000000000000--- other: - | The tox ``func`` environment has been renamed to ``functional``.././@PaxHeader0000000000000000000000000000022100000000000011450 xustar0000000000000000123 path=python-ironic-inspector-client-4.7.1/releasenotes/notes/reprocess-stored-introspection-data-c4910325254426c5.yaml 22 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/reprocess-stored-introspection-data-c49103250000664000175000017500000000020500000000000033471 0ustar00zuulzuul00000000000000--- features: - Introduced command "openstack baremetal introspection reprocess " to reprocess stored introspection data ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/rules-import-yaml-815ebc6ca6fe28b9.yaml0000664000175000017500000000020300000000000032317 0ustar00zuulzuul00000000000000--- features: - | Supports importing introspection rules from YAML files (in addition to already supported JSON format). ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/service-catalog-45466d1cfd330231.yaml0000664000175000017500000000034400000000000031452 0ustar00zuulzuul00000000000000--- features: - Inspector service URL can now be fetched from the service catalog. deprecations: - Using default service URL of locahost:5050 is deprecated now. Either use the service catalog or provide an explicit URL. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/unprocessed-7ebb1c48427bfee4.yaml0000664000175000017500000000030600000000000031246 0ustar00zuulzuul00000000000000--- features: - | Adds support for retrieving unprocessed introspection data via the new ``processed`` boolean argument to ``get_data``, as well as the new ``--unprocessed`` CLI flag. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/notes/version-uuid-b7f9e57589314fe9.yaml0000664000175000017500000000020700000000000031155 0ustar00zuulzuul00000000000000--- deprecations: - Parameter uuid is deprecated in ClientV1 and will be removed in future releases, please use node_id instead. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4138756 python-ironic-inspector-client-4.7.1/releasenotes/source/0000775000175000017500000000000000000000000023621 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4138756 python-ironic-inspector-client-4.7.1/releasenotes/source/_static/0000775000175000017500000000000000000000000025247 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/_static/.placeholder0000664000175000017500000000000000000000000027520 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4138756 python-ironic-inspector-client-4.7.1/releasenotes/source/_templates/0000775000175000017500000000000000000000000025756 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/_templates/.placeholder0000664000175000017500000000000000000000000030227 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/conf.py0000664000175000017500000002134700000000000025127 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # 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. # Ironic Inspector Release Notes documentation build configuration file, # created by sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # openstackdocstheme options openstackdocs_repo_name = 'openstack/python-ironic-inspector-client' openstackdocs_use_storyboard = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. copyright = u'2015, Ironic Inspector Developers' # Release notes are version independent. # The full version, including alpha/beta/rc tags. release = '' # The short X.Y version. version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'IronicInspectorClientReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'IronicInspectorClientReleaseNotes.tex', u'Ironic Inspector Client Release Notes Documentation', u'Ironic Inspector Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'ironicinspectorclientreleasenotes', u'Ironic Inspector Client Release Notes Documentation', [u'Ironic Inspector Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'IronicInspectorClientReleaseNotes', u'Ironic Inspector Client Release Notes Documentation', u'Ironic Inspector Developers', 'IronicInspectorClientReleaseNotes', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/index.rst0000664000175000017500000000044000000000000025460 0ustar00zuulzuul00000000000000====================================== Ironic Inspector Client Release Notes ====================================== .. toctree:: :maxdepth: 1 unreleased xena wallaby victoria ussuri train stein rocky queens pike ocata newton mitaka liberty ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/liberty.rst0000664000175000017500000000021500000000000026023 0ustar00zuulzuul00000000000000============================ Liberty Series Release Notes ============================ .. release-notes:: :branch: origin/stable/liberty ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/mitaka.rst0000664000175000017500000000023200000000000025616 0ustar00zuulzuul00000000000000=================================== Mitaka Series Release Notes =================================== .. release-notes:: :branch: origin/stable/mitaka ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/newton.rst0000664000175000017500000000023200000000000025662 0ustar00zuulzuul00000000000000=================================== Newton Series Release Notes =================================== .. release-notes:: :branch: origin/stable/newton ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/ocata.rst0000664000175000017500000000023000000000000025435 0ustar00zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/pike.rst0000664000175000017500000000021700000000000025303 0ustar00zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/queens.rst0000664000175000017500000000022300000000000025650 0ustar00zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/rocky.rst0000664000175000017500000000022100000000000025475 0ustar00zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/stein.rst0000664000175000017500000000022100000000000025470 0ustar00zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/train.rst0000664000175000017500000000026100000000000025467 0ustar00zuulzuul00000000000000=========================================== Train Series (3.6.0 - 3.6.x) Release Notes =========================================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/unreleased.rst0000664000175000017500000000015300000000000026501 0ustar00zuulzuul00000000000000============================ Current Series Release Notes ============================ .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/ussuri.rst0000664000175000017500000000020200000000000025677 0ustar00zuulzuul00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/victoria.rst0000664000175000017500000000021200000000000026166 0ustar00zuulzuul00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/wallaby.rst0000664000175000017500000000020600000000000026004 0ustar00zuulzuul00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/releasenotes/source/xena.rst0000664000175000017500000000017200000000000025306 0ustar00zuulzuul00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: stable/xena ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/requirements.txt0000664000175000017500000000054700000000000023122 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. cliff!=2.9.0,>=2.8.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0 PyYAML>=3.13 # MIT requests>=2.14.2 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4138756 python-ironic-inspector-client-4.7.1/setup.cfg0000664000175000017500000000431500000000000021454 0ustar00zuulzuul00000000000000[metadata] name = python-ironic-inspector-client summary = Python client for Ironic Inspector description_file = README.rst author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/python-ironic-inspector-client/latest/ license = Apache-2 python_requires = >=3.6 classifier = Environment :: Console Environment :: OpenStack Intended Audience :: Developers Intended Audience :: Information Technology License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [files] packages = ironic_inspector_client [entry_points] openstack.cli.extension = baremetal-introspection = ironic_inspector_client.shell openstack.baremetal_introspection.v1 = baremetal_introspection_start = ironic_inspector_client.shell:StartCommand baremetal_introspection_status = ironic_inspector_client.shell:StatusCommand baremetal_introspection_list = ironic_inspector_client.shell:StatusListCommand baremetal_introspection_reprocess = ironic_inspector_client.shell:ReprocessCommand baremetal_introspection_abort = ironic_inspector_client.shell:AbortCommand baremetal_introspection_data_save = ironic_inspector_client.shell:DataSaveCommand baremetal_introspection_rule_import = ironic_inspector_client.shell:RuleImportCommand baremetal_introspection_rule_list = ironic_inspector_client.shell:RuleListCommand baremetal_introspection_rule_show = ironic_inspector_client.shell:RuleShowCommand baremetal_introspection_rule_delete = ironic_inspector_client.shell:RuleDeleteCommand baremetal_introspection_rule_purge = ironic_inspector_client.shell:RulePurgeCommand baremetal_introspection_interface_list = ironic_inspector_client.shell:InterfaceListCommand baremetal_introspection_interface_show = ironic_inspector_client.shell:InterfaceShowCommand [extras] cli = python-openstackclient>=3.12.0 # Apache-2.0 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/setup.py0000664000175000017500000000127100000000000021343 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # 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. import setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/test-requirements.txt0000664000175000017500000000067000000000000024074 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. coverage>=5.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.2.0 # Apache-2.0 oslo.concurrency>=3.25.0 # Apache-2.0 osc-lib>=2.1.0 # Apache-2.0 python-openstackclient>=3.12.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/tox.ini0000664000175000017500000001000700000000000021141 0ustar00zuulzuul00000000000000[tox] minversion = 3.18.0 envlist = py3,pep8,functional-py3 ignore_basepython_conflict=true [testenv] usedevelop = True basepython = python3 setenv = VIRTUAL_ENV={envdir} PYTHONDONTWRITEBYTECODE = 1 LANGUAGE=en_US LC_ALL=en_US.UTF-8 PYTHONWARNINGS=default::DeprecationWarning deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = stestr run --slowest {posargs} passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY [testenv:pep8] deps = hacking>=4.1.0,<5.0.0 # Apache-2.0 doc8>=0.6.0 # Apache-2.0 flake8-import-order>=0.17.1 # LGPLv3 pycodestyle>=2.0.0,<3.0.0 # MIT Pygments>=2.2.0 # BSD commands = flake8 ironic_inspector_client doc8 README.rst doc/source [testenv:functional-py3] deps = {[testenv]deps} -r{toxinidir}/functest-requirements.txt commands = python -m ironic_inspector_client.tests.functional {posargs} [testenv:functional-py39] basepython = python3.9 deps = {[testenv:functional-py3]deps} commands = {[testenv:functional-py3]commands} [testenv:func] # Replaced in CI with "functional" environment but kept here as a # backwards-compatibility shim for transition deps = {[testenv:functional-py3]deps} commands = {[testenv:functional-py3]commands} [testenv:venv] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = {posargs} [testenv:cover] setenv = {[testenv]setenv} PYTHON=coverage run --source ironic_inspector_client --omit='*tests*' --parallel-mode commands = coverage erase stestr run {posargs} coverage combine coverage report --omit='*tests*' coverage html -d ./cover --omit='*tests*' [testenv:releasenotes] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:docs] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/functest-requirements.txt -r{toxinidir}/test-requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html [testenv:pdf-docs] allowlist_externals = make deps = {[testenv:docs]deps} commands = sphinx-build -W -b latex doc/source doc/build/pdf make -C doc/build/pdf [flake8] # [E741] ambiguous variable name # [W503] Line break before binary operator. ignore = E741,W503 exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build import-order-style = pep8 application-import-names = ironic_inspector_client max-complexity=15 # [H106] Don't put vim configuration in source files. # [H203] Use assertIs(Not)None to check for None. # [H204] Use assert(Not)Equal to check for equality. # [H205] Use assert(Greater|Less)(Equal) for comparison. # [H210] Require ‘autospec’, ‘spec’, or ‘spec_set’ in mock.patch/mock.patch.object calls # [H904] Delay string interpolations at logging calls. enable-extensions=H106,H203,H204,H205,H210,H904 per-file-ignores = ironic_inspector_client/tests/functional.py:E402 [hacking] import_exceptions = ironic_inspector_client.common.i18n [testenv:lower-constraints] deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt # This environment can be used to quickly validate that all needed system # packages required to successfully execute test targets are installed [testenv:bindep] # Do not install any requirements. We want this to be fast and work even if # system dependencies are missing, since it's used to tell you what system # dependencies are missing! This also means that bindep must be installed # separately, outside of the requirements files. deps = bindep commands = bindep test ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793596.4138756 python-ironic-inspector-client-4.7.1/zuul.d/0000775000175000017500000000000000000000000021051 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/zuul.d/project.yaml0000664000175000017500000000064200000000000023405 0ustar00zuulzuul00000000000000- project: templates: - openstack-python3-yoga-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 - openstackclient-plugin-jobs check: jobs: - openstack-tox-functional-py39 - python-ironic-inspector-client-tempest gate: jobs: - openstack-tox-functional-py39 - python-ironic-inspector-client-tempest ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793563.0 python-ironic-inspector-client-4.7.1/zuul.d/python-ironic-inspector-client-jobs.yaml0000664000175000017500000000113700000000000030754 0ustar00zuulzuul00000000000000- job: name: python-ironic-inspector-client-tempest description: Devstack/tempest based python-ironic-inspector-client job. parent: ironic-inspector-base required-projects: - openstack/python-ironic-inspector-client irrelevant-files: - ^(func|)test-requirements.txt$ - ^.*\.rst$ - ^doc/.*$ - ^ironic_inspector_client/tests/.*$ - ^releasenotes/.*$ - ^setup.cfg$ - ^tox.ini$ vars: tempest_test_regex: ironic_tempest_plugin.tests.scenario.test_introspection_basic devstack_localrc: IRONIC_DEFAULT_BOOT_OPTION: netboot