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:
Jay Faulkner
2025-10-08 14:12:24 -07:00
parent 25d51f3c67
commit 6ad8733c0d
99 changed files with 12 additions and 4616 deletions

View File

@@ -1,3 +0,0 @@
[DEFAULT]
test_path=${TESTS_DIR:-./ironic_inspector_client/tests/unit/}
top_dir=./

202
LICENSE
View File

@@ -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.

View File

@@ -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.

View File

@@ -1,3 +0,0 @@
# libsrvg2 is needed for sphinxcontrib-svg2pdfconverter in docs builds.
librsvg2-tools [doc platform:rpm]
librsvg2-bin [doc platform:dpkg]

View File

@@ -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

View File

@@ -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/

View File

@@ -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'
),
]

View File

@@ -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/

View File

@@ -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

View File

@@ -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

View File

@@ -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']

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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())))

View File

@@ -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)

View File

@@ -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')

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"])

View File

@@ -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')

View File

@@ -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."""

View File

@@ -1,3 +0,0 @@
[build-system]
requires = ["pbr>=5.7.0", "setuptools>=64.0.0", "wheel"]
build-backend = "pbr.build"

View File

@@ -1,5 +0,0 @@
---
features:
- |
the introspection status returns the ``error``, ``finished``,
``finished_at``, ``links``, ``started_at`` and ``uuid`` fields

View File

@@ -1,4 +0,0 @@
---
features:
- Introduced command "openstack baremetal introspection abort <UUID>"
to abort running introspection for a node.

View File

@@ -1,4 +0,0 @@
---
fixes:
- Fixed MAX_API_VERSION incorrectly set to (1, 0) while API 1.2 is actually
fully supported.

View File

@@ -1,3 +0,0 @@
---
other:
- Bumped supported API version to 1.5.

View File

@@ -1,3 +0,0 @@
---
other:
- Bumped supported API version to 1.6.

View File

@@ -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.

View File

@@ -1,3 +0,0 @@
---
features:
- Add client.get_data() call for getting stored introspection data.

View File

@@ -1,4 +0,0 @@
---
features:
- Added "introspection data save" command to retrieve stored introspection
data for the node.

View File

@@ -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/

View File

@@ -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.

View File

@@ -1,5 +0,0 @@
---
fixes:
- |
Fixes regression in 3.6.0 that caused the deprecated ``uuid`` argument
to various calls to stop working.

View File

@@ -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]

View File

@@ -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.

View File

@@ -1,4 +0,0 @@
---
upgrade:
- |
Experimental setting IPMI credentials feature was removed.

View File

@@ -1,5 +0,0 @@
---
features:
- |
Exposes ``switch_mgmt_addresses`` and ``switch_system_description`` in
interface details.

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,3 +0,0 @@
---
features:
- Allow multiple UUID's in the 'introspection start' CLI command.

View File

@@ -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``.

View File

@@ -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.

View File

@@ -1,5 +0,0 @@
---
fixes:
- |
Provides a clear error message when trying to access an ironic URL with
ironic-inspector-client.

View File

@@ -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.

View File

@@ -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

View File

@@ -1,4 +0,0 @@
---
features:
- Adds Python library support for passing ``manage_boot``
to the introspection API.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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``

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,4 +0,0 @@
---
upgrade:
- command `openstack baremetal introspection rule import` prints
created rules to stdout.

View File

@@ -1,5 +0,0 @@
---
upgrade:
- |
The deprecated module ``ironic_inspector_client.client`` was removed,
please use ``ironic_inspector_client.ClientV1`` instead.

View File

@@ -1,5 +0,0 @@
---
upgrade:
- |
Support for Python 3.8 has been removed. Now the minimum python version
supported is 3.9 .

View File

@@ -1,5 +0,0 @@
---
upgrade:
- |
Support for Python 3.9 has been removed. Now Python 3.10 is the minimum
version supported.

View File

@@ -1,4 +0,0 @@
---
other:
- |
The tox ``func`` environment has been renamed to ``functional``.

View File

@@ -1,4 +0,0 @@
---
features:
- Introduced command "openstack baremetal introspection reprocess <UUID>"
to reprocess stored introspection data

View File

@@ -1,5 +0,0 @@
---
features:
- |
Supports importing introspection rules from YAML files (in addition to
already supported JSON format).

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,4 +0,0 @@
---
deprecations:
- Parameter uuid is deprecated in ClientV1 and will be removed in future
releases, please use node_id instead.

View File

@@ -1,6 +0,0 @@
===========================
2023.1 Series Release Notes
===========================
.. release-notes::
:branch: unmaintained/2023.1

View File

@@ -1,6 +0,0 @@
===========================
2023.2 Series Release Notes
===========================
.. release-notes::
:branch: stable/2023.2

View File

@@ -1,6 +0,0 @@
===========================
2024.1 Series Release Notes
===========================
.. release-notes::
:branch: stable/2024.1

View File

@@ -1,6 +0,0 @@
===========================
2024.2 Series Release Notes
===========================
.. release-notes::
:branch: stable/2024.2

View File

@@ -1,6 +0,0 @@
===========================
2025.1 Series Release Notes
===========================
.. release-notes::
:branch: stable/2025.1

View File

@@ -1,6 +0,0 @@
===========================
2025.2 Series Release Notes
===========================
.. release-notes::
:branch: stable/2025.2

View File

@@ -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/']

View File

@@ -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

View File

@@ -1,6 +0,0 @@
============================
Liberty Series Release Notes
============================
.. release-notes::
:branch: origin/stable/liberty

View File

@@ -1,6 +0,0 @@
===================================
Mitaka Series Release Notes
===================================
.. release-notes::
:branch: origin/stable/mitaka

View File

@@ -1,6 +0,0 @@
===================================
Newton Series Release Notes
===================================
.. release-notes::
:branch: origin/stable/newton

View File

@@ -1,6 +0,0 @@
===================================
Ocata Series Release Notes
===================================
.. release-notes::
:branch: origin/stable/ocata

View File

@@ -1,6 +0,0 @@
===================================
Pike Series Release Notes
===================================
.. release-notes::
:branch: stable/pike

View File

@@ -1,6 +0,0 @@
===================================
Queens Series Release Notes
===================================
.. release-notes::
:branch: stable/queens

View File

@@ -1,6 +0,0 @@
===================================
Rocky Series Release Notes
===================================
.. release-notes::
:branch: stable/rocky

View File

@@ -1,6 +0,0 @@
===================================
Stein Series Release Notes
===================================
.. release-notes::
:branch: stable/stein

View File

@@ -1,6 +0,0 @@
===========================================
Train Series (3.6.0 - 3.6.x) Release Notes
===========================================
.. release-notes::
:branch: stable/train

View File

@@ -1,5 +0,0 @@
============================
Current Series Release Notes
============================
.. release-notes::

View File

@@ -1,6 +0,0 @@
===========================
Ussuri Series Release Notes
===========================
.. release-notes::
:branch: stable/ussuri

View File

@@ -1,6 +0,0 @@
=============================
Victoria Series Release Notes
=============================
.. release-notes::
:branch: unmaintained/victoria

View File

@@ -1,6 +0,0 @@
============================
Wallaby Series Release Notes
============================
.. release-notes::
:branch: unmaintained/wallaby

View File

@@ -1,6 +0,0 @@
=========================
Xena Series Release Notes
=========================
.. release-notes::
:branch: unmaintained/xena

View File

@@ -1,6 +0,0 @@
=========================
Yoga Series Release Notes
=========================
.. release-notes::
:branch: unmaintained/yoga

View File

@@ -1,6 +0,0 @@
========================
Zed Series Release Notes
========================
.. release-notes::
:branch: unmaintained/zed

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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
View File

@@ -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}

View File

@@ -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

View File

@@ -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