Retire python-ironic-inspector-client
Removed content per infra-manual in preparation for retirement. Change-Id: I74038877fd0dd291f5c8b8e800ddbf7d822e3681 Signed-Off-By: Jay Faulkner <jay@jvf.cc>
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_path=${TESTS_DIR:-./ironic_inspector_client/tests/unit/}
|
||||
top_dir=./
|
||||
202
LICENSE
202
LICENSE
@@ -1,202 +0,0 @@
|
||||
|
||||
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.
|
||||
38
README.rst
38
README.rst
@@ -1,29 +1,15 @@
|
||||
Ironic Inspector Client
|
||||
=======================
|
||||
This project is no longer maintained.
|
||||
|
||||
.. image:: https://governance.openstack.org/tc/badges/python-ironic-inspector-client.svg
|
||||
:target: https://governance.openstack.org/tc/reference/tags/index.html
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
Overview
|
||||
--------
|
||||
Please use `built-in in-band inspection in ironic
|
||||
<https://docs.openstack.org/ironic/latest/admin/inspection/index.html>`_
|
||||
instead. For existing deployments, see the `migration guide
|
||||
<https://docs.openstack.org/ironic/latest/admin/inspection/migration.html>`_.
|
||||
|
||||
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
|
||||
For any further questions, please email
|
||||
openstack-discuss@lists.openstack.org or join #openstack-dev on
|
||||
OFTC.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# libsrvg2 is needed for sphinxcontrib-svg2pdfconverter in docs builds.
|
||||
librsvg2-tools [doc platform:rpm]
|
||||
librsvg2-bin [doc platform:dpkg]
|
||||
@@ -1,5 +0,0 @@
|
||||
sphinx>=2.0.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
|
||||
@@ -1,233 +0,0 @@
|
||||
Command 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 <JSON FILE>
|
||||
|
||||
* ``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>
|
||||
|
||||
* ``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=<field>] [--vlan=<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=<field>]
|
||||
|
||||
* ``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/
|
||||
@@ -1,77 +0,0 @@
|
||||
# -- 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 = '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',
|
||||
'Python Ironic Inspector Client Documentation',
|
||||
'OpenStack Foundation',
|
||||
'manual'
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
==================================
|
||||
Welcome to Ironic Inspector Client
|
||||
==================================
|
||||
|
||||
This is a client library and tool for `Ironic Inspector`_.
|
||||
|
||||
.. warning::
|
||||
Just as ironic-inspector itself, this project is now in the maintenance
|
||||
mode, and its usage is discouraged. Use ironicclient_ and openstacksdk_.
|
||||
|
||||
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/
|
||||
.. _ironicclient: https://docs.openstack.org/python-ironicclient/latest/
|
||||
.. _openstacksdk: https://docs.openstack.org/openstacksdk/latest/
|
||||
@@ -1,43 +0,0 @@
|
||||
Library 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
|
||||
@@ -1,4 +0,0 @@
|
||||
# NOTE(jroll) these are pinned to the same SHA, update when needed.
|
||||
# Last updated: January 29, 2024 (Caracal cycle).
|
||||
git+https://opendev.org/openstack/ironic-inspector@8cf0b3a9f8b765fe4dab62f740168bb42972a17b#egg=ironic-inspector
|
||||
-r https://opendev.org/openstack/ironic-inspector/raw/commit/8cf0b3a9f8b765fe4dab62f740168bb42972a17b/test-requirements.txt
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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']
|
||||
@@ -1,240 +0,0 @@
|
||||
# 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://<current host>:5050/v<MAJOR>``.
|
||||
: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
|
||||
@@ -1,26 +0,0 @@
|
||||
# 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
|
||||
@@ -1,99 +0,0 @@
|
||||
|
||||
# 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()
|
||||
@@ -1,378 +0,0 @@
|
||||
# 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="<filename>",
|
||||
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='<field>',
|
||||
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='<field>',
|
||||
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())))
|
||||
@@ -1,463 +0,0 @@
|
||||
# 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
|
||||
# TODO(iurygregory): We need to figure out why we can't
|
||||
# use self.uuid, my current assumption is that previous
|
||||
# tests executed introspection for the given uuid and
|
||||
# introspection finished, so we don't get the error when
|
||||
# we ask to abort.
|
||||
self.assertRaises(client.ClientError, self.client.abort,
|
||||
"2e31df61-84b1-5856-bfb6-6b5f2cd3dd11")
|
||||
|
||||
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)
|
||||
@@ -1,219 +0,0 @@
|
||||
# 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')
|
||||
@@ -1,29 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,608 +0,0 @@
|
||||
# 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(('uuid', 'description', 'conditions', 'actions'),
|
||||
cols)
|
||||
self.assertEqual(('uuid1', 'd', [{}], [{}]), 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)
|
||||
@@ -1,498 +0,0 @@
|
||||
# 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"])
|
||||
@@ -1,491 +0,0 @@
|
||||
# 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
|
||||
<https://docs.openstack.org/keystoneauth/latest/using-sessions.html>`_.
|
||||
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')
|
||||
@@ -1,16 +0,0 @@
|
||||
# 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."""
|
||||
@@ -1,3 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["pbr>=5.7.0", "setuptools>=64.0.0", "wheel"]
|
||||
build-backend = "pbr.build"
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
the introspection status returns the ``error``, ``finished``,
|
||||
``finished_at``, ``links``, ``started_at`` and ``uuid`` fields
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Introduced command "openstack baremetal introspection abort <UUID>"
|
||||
to abort running introspection for a node.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Fixed MAX_API_VERSION incorrectly set to (1, 0) while API 1.2 is actually
|
||||
fully supported.
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
other:
|
||||
- Bumped supported API version to 1.5.
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
other:
|
||||
- Bumped supported API version to 1.6.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Add client.get_data() call for getting stored introspection data.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Added "introspection data save" command to retrieve stored introspection
|
||||
data for the node.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
deprecations:
|
||||
- |
|
||||
Just as ironic-inspector itself, this project is now in the maintenance
|
||||
mode, and its usage is discouraged. Use ironicclient_ and openstacksdk_.
|
||||
|
||||
.. _ironicclient: https://docs.openstack.org/python-ironicclient/latest/
|
||||
.. _openstacksdk: https://docs.openstack.org/openstacksdk/latest/
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
Fixes regression in 3.6.0 that caused the deprecated ``uuid`` argument
|
||||
to various calls to stop working.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
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]
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
Experimental setting IPMI credentials feature was removed.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Exposes ``switch_mgmt_addresses`` and ``switch_system_description`` in
|
||||
interface details.
|
||||
@@ -1,6 +0,0 @@
|
||||
deprecations: >
|
||||
The indepedent Ironic Inspector project and client has been in
|
||||
maintenance mode since 2024. All inspection functionality has been
|
||||
enhanced and moved into Ironic. Deployers should not expect further
|
||||
releases of an independent python-ironic-inspector-client.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Allow multiple UUID's in the 'introspection start' CLI command.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
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``.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
Provides a clear error message when trying to access an ironic URL with
|
||||
ironic-inspector-client.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add support for listing introspection statuses both for the API and the CLI
|
||||
upgrade:
|
||||
- |
|
||||
Service max API version bumped to 1.8
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Adds Python library support for passing ``manage_boot``
|
||||
to the introspection API.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
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``
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
upgrade:
|
||||
- osc-lib is a package of common support modules for writing OSC plugins.
|
||||
So use osc-lib instead of OpenStackClient.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
upgrade:
|
||||
- command `openstack baremetal introspection rule import` prints
|
||||
created rules to stdout.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
The deprecated module ``ironic_inspector_client.client`` was removed,
|
||||
please use ``ironic_inspector_client.ClientV1`` instead.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
Support for Python 3.8 has been removed. Now the minimum python version
|
||||
supported is 3.9 .
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
Support for Python 3.9 has been removed. Now Python 3.10 is the minimum
|
||||
version supported.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
other:
|
||||
- |
|
||||
The tox ``func`` environment has been renamed to ``functional``.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Introduced command "openstack baremetal introspection reprocess <UUID>"
|
||||
to reprocess stored introspection data
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Supports importing introspection rules from YAML files (in addition to
|
||||
already supported JSON format).
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Inspector service URL can now be fetched from the service catalog.
|
||||
deprecations:
|
||||
- Using default service URL of localhost:5050 is deprecated now. Either use
|
||||
the service catalog or provide an explicit URL.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
deprecations:
|
||||
- Parameter uuid is deprecated in ClientV1 and will be removed in future
|
||||
releases, please use node_id instead.
|
||||
@@ -1,6 +0,0 @@
|
||||
===========================
|
||||
2023.1 Series Release Notes
|
||||
===========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: unmaintained/2023.1
|
||||
@@ -1,6 +0,0 @@
|
||||
===========================
|
||||
2023.2 Series Release Notes
|
||||
===========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/2023.2
|
||||
@@ -1,6 +0,0 @@
|
||||
===========================
|
||||
2024.1 Series Release Notes
|
||||
===========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/2024.1
|
||||
@@ -1,6 +0,0 @@
|
||||
===========================
|
||||
2024.2 Series Release Notes
|
||||
===========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/2024.2
|
||||
@@ -1,6 +0,0 @@
|
||||
===========================
|
||||
2025.1 Series Release Notes
|
||||
===========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/2025.1
|
||||
@@ -1,6 +0,0 @@
|
||||
===========================
|
||||
2025.2 Series Release Notes
|
||||
===========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/2025.2
|
||||
@@ -1,268 +0,0 @@
|
||||
# -*- 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 = '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
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
# html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# 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',
|
||||
'Ironic Inspector Client Release Notes Documentation',
|
||||
'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',
|
||||
'Ironic Inspector Client Release Notes Documentation',
|
||||
['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',
|
||||
'Ironic Inspector Client Release Notes Documentation',
|
||||
'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/']
|
||||
@@ -1,29 +0,0 @@
|
||||
======================================
|
||||
Ironic Inspector Client Release Notes
|
||||
======================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
unreleased
|
||||
2025.2
|
||||
2025.1
|
||||
2024.2
|
||||
2024.1
|
||||
2023.2
|
||||
2023.1
|
||||
zed
|
||||
yoga
|
||||
xena
|
||||
wallaby
|
||||
victoria
|
||||
ussuri
|
||||
train
|
||||
stein
|
||||
rocky
|
||||
queens
|
||||
pike
|
||||
ocata
|
||||
newton
|
||||
mitaka
|
||||
liberty
|
||||
@@ -1,6 +0,0 @@
|
||||
============================
|
||||
Liberty Series Release Notes
|
||||
============================
|
||||
|
||||
.. release-notes::
|
||||
:branch: origin/stable/liberty
|
||||
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Mitaka Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: origin/stable/mitaka
|
||||
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Newton Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: origin/stable/newton
|
||||
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Ocata Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: origin/stable/ocata
|
||||
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Pike Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/pike
|
||||
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Queens Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/queens
|
||||
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Rocky Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/rocky
|
||||
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Stein Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/stein
|
||||
@@ -1,6 +0,0 @@
|
||||
===========================================
|
||||
Train Series (3.6.0 - 3.6.x) Release Notes
|
||||
===========================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/train
|
||||
@@ -1,5 +0,0 @@
|
||||
============================
|
||||
Current Series Release Notes
|
||||
============================
|
||||
|
||||
.. release-notes::
|
||||
@@ -1,6 +0,0 @@
|
||||
===========================
|
||||
Ussuri Series Release Notes
|
||||
===========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: stable/ussuri
|
||||
@@ -1,6 +0,0 @@
|
||||
=============================
|
||||
Victoria Series Release Notes
|
||||
=============================
|
||||
|
||||
.. release-notes::
|
||||
:branch: unmaintained/victoria
|
||||
@@ -1,6 +0,0 @@
|
||||
============================
|
||||
Wallaby Series Release Notes
|
||||
============================
|
||||
|
||||
.. release-notes::
|
||||
:branch: unmaintained/wallaby
|
||||
@@ -1,6 +0,0 @@
|
||||
=========================
|
||||
Xena Series Release Notes
|
||||
=========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: unmaintained/xena
|
||||
@@ -1,6 +0,0 @@
|
||||
=========================
|
||||
Yoga Series Release Notes
|
||||
=========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: unmaintained/yoga
|
||||
@@ -1,6 +0,0 @@
|
||||
========================
|
||||
Zed Series Release Notes
|
||||
========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: unmaintained/zed
|
||||
@@ -1,5 +0,0 @@
|
||||
cliff>=2.8.0 # Apache-2.0
|
||||
keystoneauth1>=3.4.0 # Apache-2.0
|
||||
pbr>=2.0.0 # Apache-2.0
|
||||
PyYAML>=3.13 # MIT
|
||||
requests>=2.14.2 # Apache-2.0
|
||||
52
setup.cfg
52
setup.cfg
@@ -1,52 +0,0 @@
|
||||
[metadata]
|
||||
name = python-ironic-inspector-client
|
||||
summary = Python client for Ironic Inspector
|
||||
description_file = README.rst
|
||||
license = Apache-2.0
|
||||
author = OpenStack
|
||||
author_email = openstack-discuss@lists.openstack.org
|
||||
home_page = https://docs.openstack.org/python-ironic-inspector-client/latest/
|
||||
python_requires = >=3.10
|
||||
classifier =
|
||||
Environment :: Console
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Developers
|
||||
Intended Audience :: Information Technology
|
||||
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.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
|
||||
[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
|
||||
|
||||
[codespell]
|
||||
quiet-level = 4
|
||||
# Words to ignore:
|
||||
ignore-words-list = example
|
||||
20
setup.py
20
setup.py
@@ -1,20 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,7 +0,0 @@
|
||||
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
|
||||
132
tox.ini
132
tox.ini
@@ -1,132 +0,0 @@
|
||||
[tox]
|
||||
minversion = 3.18.0
|
||||
envlist = py3,pep8,functional-py3
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
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~=6.1.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-py310]
|
||||
basepython = python3.10
|
||||
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:TOX_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:TOX_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:TOX_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
|
||||
|
||||
# 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
|
||||
|
||||
[testenv:codespell]
|
||||
description =
|
||||
Run codespell to check spelling
|
||||
deps = codespell
|
||||
# note(JayF): {posargs} lets us run `tox -ecodespell -- -w` to get codespell
|
||||
# to correct spelling issues in our code it's aware of.
|
||||
commands =
|
||||
codespell {posargs}
|
||||
@@ -1,17 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- openstack-python3-jobs
|
||||
- publish-openstack-docs-pti
|
||||
- check-requirements
|
||||
- release-notes-jobs-python3
|
||||
- openstackclient-plugin-jobs
|
||||
check:
|
||||
jobs:
|
||||
- openstack-tox-functional-py310
|
||||
- python-ironic-inspector-client-tempest
|
||||
- python-ironic-inspector-client-tox-codespell:
|
||||
voting: false
|
||||
gate:
|
||||
jobs:
|
||||
- openstack-tox-functional-py310
|
||||
- python-ironic-inspector-client-tempest
|
||||
@@ -1,25 +0,0 @@
|
||||
- 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
|
||||
|
||||
- job:
|
||||
name: python-ironic-inspector-client-tox-codespell
|
||||
parent: openstack-tox
|
||||
timeout: 7200
|
||||
vars:
|
||||
tox_envlist: codespell
|
||||
Reference in New Issue
Block a user