Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: Ia85425594e6fb170783a498ca41a2cb35e1ce051
This commit is contained in:
parent
72fe817a25
commit
c269683c0e
42
.gitignore
vendored
42
.gitignore
vendored
@ -1,42 +0,0 @@
|
||||
# Compiled files
|
||||
*.py[co]
|
||||
*.a
|
||||
*.o
|
||||
*.so
|
||||
|
||||
# Sphinx
|
||||
_build
|
||||
doc/source/contributor/api/
|
||||
|
||||
# release notes build
|
||||
releasenotes/build
|
||||
|
||||
# Packages/installer info
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
.eggs/
|
||||
|
||||
# Other
|
||||
*.DS_Store
|
||||
.idea
|
||||
.testrepository
|
||||
.tox
|
||||
.venv
|
||||
.*.swp
|
||||
.coverage
|
||||
cover
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
*.sqlite
|
||||
*~
|
||||
|
||||
# Vagrant
|
||||
.vagrant
|
@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/ironic-inspector.git
|
367
CONTRIBUTING.rst
367
CONTRIBUTING.rst
@ -1,367 +0,0 @@
|
||||
=================
|
||||
How To Contribute
|
||||
=================
|
||||
|
||||
Basics
|
||||
~~~~~~
|
||||
|
||||
* Our source code is hosted on `OpenStack GitHub`_, but please do not send pull
|
||||
requests there.
|
||||
|
||||
* Please follow usual OpenStack `Gerrit Workflow`_ to submit a patch.
|
||||
|
||||
* Update change log in README.rst on any significant change.
|
||||
|
||||
* It goes without saying that any code change should by accompanied by unit
|
||||
tests.
|
||||
|
||||
* Note the branch you're proposing changes to. ``master`` is the current focus
|
||||
of development, use ``stable/VERSION`` for proposing an urgent fix, where
|
||||
``VERSION`` is the current stable series. E.g. at the moment of writing the
|
||||
stable branch is ``stable/1.0``.
|
||||
|
||||
* Please file a launchpad_ blueprint for any significant code change and a bug
|
||||
for any significant bug fix.
|
||||
|
||||
.. _OpenStack GitHub: https://github.com/openstack/ironic-inspector
|
||||
.. _Gerrit Workflow: http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
.. _launchpad: https://bugs.launchpad.net/ironic-inspector
|
||||
|
||||
Development Environment
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
First of all, install *tox* utility. It's likely to be in your distribution
|
||||
repositories under name of ``python-tox``. Alternatively, you can install it
|
||||
from PyPI.
|
||||
|
||||
Next checkout and create environments::
|
||||
|
||||
git clone https://github.com/openstack/ironic-inspector.git
|
||||
cd ironic-inspector
|
||||
tox
|
||||
|
||||
Repeat *tox* command each time you need to run tests. If you don't have Python
|
||||
interpreter of one of supported versions (currently 2.7 and 3.4), use
|
||||
``-e`` flag to select only some environments, e.g.
|
||||
|
||||
::
|
||||
|
||||
tox -e py27
|
||||
|
||||
.. note::
|
||||
Support for Python 3 is highly experimental, stay with Python 2 for the
|
||||
production environment for now.
|
||||
|
||||
.. note::
|
||||
This command also runs tests for database migrations. By default the sqlite
|
||||
backend is used. For testing with mysql or postgresql, you need to set up
|
||||
a db named 'openstack_citest' with user 'openstack_citest' and password
|
||||
'openstack_citest' on localhost. Use the script
|
||||
``tools/test_setup.sh`` to set the database up the same way as
|
||||
done in the OpenStack CI environment.
|
||||
|
||||
.. note::
|
||||
Users of Fedora <= 23 will need to run "sudo dnf --releasever=24 update
|
||||
python-virtualenv" to run unit tests
|
||||
|
||||
To run the functional tests, use::
|
||||
|
||||
tox -e func
|
||||
|
||||
Once you have added new state or transition into inspection state machine, you
|
||||
should regenerate :ref:`State machine diagram <state_machine_diagram>` with::
|
||||
|
||||
tox -e genstates
|
||||
|
||||
Run the service with::
|
||||
|
||||
.tox/py27/bin/ironic-inspector --config-file example.conf
|
||||
|
||||
Of course you may have to modify ``example.conf`` to match your OpenStack
|
||||
environment.
|
||||
|
||||
You can develop and test **ironic-inspector** using DevStack - see
|
||||
`Deploying Ironic Inspector with DevStack`_ for the current status.
|
||||
|
||||
Deploying Ironic Inspector with DevStack
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
`DevStack <http://docs.openstack.org/developer/devstack/>`_ provides a way to
|
||||
quickly build a full OpenStack development environment with requested
|
||||
components. There is a plugin for installing **ironic-inspector** in DevStack.
|
||||
Installing **ironic-inspector** requires a machine running Ubuntu 14.04 (or
|
||||
later) or Fedora 23 (or later). Make sure this machine is fully up to date and
|
||||
has the latest packages installed before beginning this process.
|
||||
|
||||
Download DevStack::
|
||||
|
||||
git clone https://git.openstack.org/openstack-dev/devstack.git
|
||||
cd devstack
|
||||
|
||||
|
||||
Create ``local.conf`` file with minimal settings required to
|
||||
enable both the **ironic** and the **ironic-inspector**. You can start with the
|
||||
`Example local.conf`_ and extend it as needed.
|
||||
|
||||
|
||||
Example local.conf
|
||||
------------------
|
||||
|
||||
.. literalinclude:: ../../../devstack/example.local.conf
|
||||
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
* Set IRONIC_INSPECTOR_BUILD_RAMDISK to True if you want to build ramdisk.
|
||||
Default value is False and ramdisk will be downloaded instead of building.
|
||||
|
||||
* 1024 MiB of RAM is a minimum required for the default build of IPA based on
|
||||
CoreOS. If you plan to use another operating system and build IPA with
|
||||
diskimage-builder 2048 MiB is recommended.
|
||||
|
||||
* Network configuration is pretty sensitive, better not to touch it
|
||||
without deep understanding.
|
||||
|
||||
* This configuration disables **horizon**, **heat**, **cinder** and
|
||||
**tempest**, adjust it if you need these services.
|
||||
|
||||
Start the install::
|
||||
|
||||
./stack.sh
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
After installation is complete, you can source ``openrc`` in your shell, and
|
||||
then use the OpenStack CLI to manage your DevStack::
|
||||
|
||||
source openrc admin demo
|
||||
|
||||
Show DevStack screens::
|
||||
|
||||
screen -x stack
|
||||
|
||||
To exit screen, hit ``CTRL-a d``.
|
||||
|
||||
List baremetal nodes::
|
||||
|
||||
openstack baremetal node list
|
||||
|
||||
Bring the node to manageable state::
|
||||
|
||||
openstack baremetal node manage <NodeID>
|
||||
|
||||
Inspect the node::
|
||||
|
||||
openstack baremetal node inspect <NodeID>
|
||||
|
||||
.. note::
|
||||
The deploy driver used must support the inspect interface. See also the
|
||||
`Ironic Python Agent
|
||||
<http://docs.openstack.org/developer/ironic/drivers/ipa.html#ipa>`_.
|
||||
|
||||
A node can also be inspected using the following command. However, this will
|
||||
not affect the provision state of the node::
|
||||
|
||||
openstack baremetal introspection start <NodeID>
|
||||
|
||||
Check inspection status::
|
||||
|
||||
openstack baremetal introspection status <NodeID>
|
||||
|
||||
Optionally, get the inspection data::
|
||||
|
||||
openstack baremetal introspection data save <NodeID>
|
||||
|
||||
|
||||
Writing a Plugin
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* **ironic-inspector** allows you to hook code into the data processing chain
|
||||
after introspection. Inherit ``ProcessingHook`` class defined in
|
||||
ironic_inspector.plugins.base_ module and overwrite any or both of
|
||||
the following methods:
|
||||
|
||||
``before_processing(introspection_data,**)``
|
||||
called before any data processing, providing the raw data. Each plugin in
|
||||
the chain can modify the data, so order in which plugins are loaded
|
||||
matters here. Returns nothing.
|
||||
``before_update(introspection_data,node_info,**)``
|
||||
called after node is found and ports are created, but before data is
|
||||
updated on a node. Please refer to the docstring for details
|
||||
and examples.
|
||||
|
||||
You can optionally define the following attribute:
|
||||
|
||||
``dependencies``
|
||||
a list of entry point names of the hooks this hook depends on. These
|
||||
hooks are expected to be enabled before the current hook.
|
||||
|
||||
Make your plugin a setuptools entry point under
|
||||
``ironic_inspector.hooks.processing`` namespace and enable it in the
|
||||
configuration file (``processing.processing_hooks`` option).
|
||||
|
||||
* **ironic-inspector** allows plugins to override the action when node is not
|
||||
found in node cache. Write a callable with the following signature:
|
||||
|
||||
``(introspection_data,**)``
|
||||
called when node is not found in cache, providing the processed data.
|
||||
Should return a ``NodeInfo`` class instance.
|
||||
|
||||
Make your plugin a setuptools entry point under
|
||||
``ironic_inspector.hooks.node_not_found`` namespace and enable it in the
|
||||
configuration file (``processing.node_not_found_hook`` option).
|
||||
|
||||
* **ironic-inspector** allows more condition types to be added for
|
||||
`Introspection Rules`_. Inherit ``RuleConditionPlugin`` class defined in
|
||||
ironic_inspector.plugins.base_ module and overwrite at least the following
|
||||
method:
|
||||
|
||||
``check(node_info,field,params,**)``
|
||||
called to check that condition holds for a given field. Field value is
|
||||
provided as ``field`` argument, ``params`` is a dictionary defined
|
||||
at the time of condition creation. Returns boolean value.
|
||||
|
||||
The following methods and attributes may also be overridden:
|
||||
|
||||
``validate(params,**)``
|
||||
called to validate parameters provided during condition creating.
|
||||
Default implementation requires keys listed in ``REQUIRED_PARAMS`` (and
|
||||
only them).
|
||||
|
||||
``REQUIRED_PARAMS``
|
||||
contains set of required parameters used in the default implementation
|
||||
of ``validate`` method, defaults to ``value`` parameter.
|
||||
|
||||
``ALLOW_NONE``
|
||||
if it's set to ``True``, missing fields will be passed as ``None``
|
||||
values instead of failing the condition. Defaults to ``False``.
|
||||
|
||||
Make your plugin a setuptools entry point under
|
||||
``ironic_inspector.rules.conditions`` namespace.
|
||||
|
||||
* **ironic-inspector** allows more action types to be added for `Introspection
|
||||
Rules`_. Inherit ``RuleActionPlugin`` class defined in
|
||||
ironic_inspector.plugins.base_ module and overwrite at least the following
|
||||
method:
|
||||
|
||||
``apply(node_info,params,**)``
|
||||
called to apply the action.
|
||||
|
||||
The following methods and attributes may also be overridden:
|
||||
|
||||
``validate(params,**)``
|
||||
called to validate parameters provided during actions creating.
|
||||
Default implementation requires keys listed in ``REQUIRED_PARAMS`` (and
|
||||
only them).
|
||||
|
||||
``REQUIRED_PARAMS``
|
||||
contains set of required parameters used in the default implementation
|
||||
of ``validate`` method, defaults to no parameters.
|
||||
|
||||
Make your plugin a setuptools entry point under
|
||||
``ironic_inspector.rules.conditions`` namespace.
|
||||
|
||||
.. note::
|
||||
``**`` argument is needed so that we can add optional arguments without
|
||||
breaking out-of-tree plugins. Please make sure to include and ignore it.
|
||||
|
||||
.. _ironic_inspector.plugins.base: http://docs.openstack.org/developer/ironic-inspector/api/ironic_inspector.plugins.base.html
|
||||
.. _Introspection Rules: http://docs.openstack.org/developer/ironic-inspector/usage.html#introspection-rules
|
||||
|
||||
Making changes to the database
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In order to make a change to the ironic-inspector database you must update the
|
||||
database models found in ironic_inspector.db_ and then create a migration to
|
||||
reflect that change.
|
||||
|
||||
There are two ways to create a migration which are described below, both of
|
||||
these generate a new migration file. In this file there is only one function:
|
||||
|
||||
* ``upgrade`` - The function to run when
|
||||
``ironic-inspector-dbsync upgrade`` is run, and should be populated with
|
||||
code to bring the database up to its new state from the state it was in
|
||||
after the last migration.
|
||||
|
||||
For further information on creating a migration, refer to
|
||||
`Create a Migration Script`_ from the alembic documentation.
|
||||
|
||||
Autogenerate
|
||||
------------
|
||||
|
||||
This is the simplest way to create a migration. Alembic will compare the models
|
||||
to an up to date database, and then attempt to write a migration based on the
|
||||
differences. This should generate correct migrations in most cases however
|
||||
there are some cases when it can not detect some changes and may require
|
||||
manual modification, see `What does Autogenerate Detect (and what does it not
|
||||
detect?)`_ from the alembic documentation.
|
||||
|
||||
::
|
||||
|
||||
ironic-inspector-dbsync upgrade
|
||||
ironic-inspector-dbsync revision -m "A short description" --autogenerate
|
||||
|
||||
Manual
|
||||
------
|
||||
|
||||
This will generate an empty migration file, with the correct revision
|
||||
information already included. However the upgrade function is left empty
|
||||
and must be manually populated in order to perform the correct actions on
|
||||
the database::
|
||||
|
||||
ironic-inspector-dbsync revision -m "A short description"
|
||||
|
||||
.. _Create a Migration Script: http://alembic.zzzcomputing.com/en/latest/tutorial.html#create-a-migration-script
|
||||
.. _ironic_inspector.db: http://docs.openstack.org/developer/ironic-inspector/api/ironic_inspector.db.html
|
||||
.. _What does Autogenerate Detect (and what does it not detect?): http://alembic.zzzcomputing.com/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect
|
||||
|
||||
Implementing PXE Filter Drivers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Background
|
||||
----------
|
||||
|
||||
**inspector** in-band introspection PXE-boots the Ironic Python Agent "live"
|
||||
image, to inspect the baremetal server. **ironic** also PXE-boots IPA to
|
||||
perform tasks on a node, such as deploying an image. **ironic** uses
|
||||
**neutron** to provide DHCP, however **neutron** does not provide DHCP for
|
||||
unknown MAC addresses so **inspector** has to use its own DHCP/TFTP stack for
|
||||
discovery and inspection.
|
||||
|
||||
When **ironic** and **inspector** are operating in the same L2 network, there
|
||||
is a potential for the two DHCPs to race, which could result in a node being
|
||||
deployed by **ironic** being PXE booted by **inspector**.
|
||||
|
||||
To prevent DHCP races between the **inspector** DHCP and **ironic** DHCP,
|
||||
**inspector** has to be able to filter which nodes can get a DHCP lease from
|
||||
the **inspector** DHCP server. These filters can then be used to prevent
|
||||
node's enrolled in **ironic** inventory from being PXE-booted unless they are
|
||||
explicitly moved into the ``inspected`` state.
|
||||
|
||||
Filter Interface
|
||||
----------------
|
||||
|
||||
.. py:currentmodule:: ironic_inspector.pxe_filter.interface
|
||||
|
||||
The contract between **inspector** and a PXE filter driver is described in the
|
||||
:class:`FilterDriver` interface. The methods a driver has to implement are:
|
||||
|
||||
* :meth:`~FilterDriver.init_filter` called on the service start to initialize
|
||||
internal driver state
|
||||
|
||||
* :meth:`~FilterDriver.sync` called both periodically and when a node starts or
|
||||
finishes introspection to white or blacklist its ports MAC addresses in the
|
||||
driver
|
||||
|
||||
* :meth:`~FilterDriver.tear_down_filter` called on service exit to reset the
|
||||
internal driver state
|
||||
|
||||
.. py:currentmodule:: ironic_inspector.pxe_filter.base
|
||||
|
||||
The driver-specific configuration is suggested to be parsed during
|
||||
instantiation. There's also a convenience generic interface implementation
|
||||
:class:`BaseFilter` that provides base locking and initialization
|
||||
implementation. If required, a driver can opt-out from the periodic
|
||||
synchronization by overriding the :meth:`~BaseFilter.get_periodic_sync_task`.
|
202
LICENSE
202
LICENSE
@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
14
README
Normal file
14
README
Normal file
@ -0,0 +1,14 @@
|
||||
This project is no longer maintained.
|
||||
|
||||
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".
|
||||
|
||||
For ongoing work on maintaining OpenStack packages in the Debian
|
||||
distribution, please see the Debian OpenStack packaging team at
|
||||
https://wiki.debian.org/OpenStack/.
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
37
README.rst
37
README.rst
@ -1,37 +0,0 @@
|
||||
===============================================
|
||||
Hardware introspection for OpenStack Bare Metal
|
||||
===============================================
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
.. image:: http://governance.openstack.org/badges/ironic-inspector.svg
|
||||
:target: http://governance.openstack.org/reference/tags/index.html
|
||||
|
||||
This is an auxiliary service for discovering hardware properties for a
|
||||
node managed by `Ironic`_. Hardware introspection or hardware
|
||||
properties discovery is a process of getting hardware parameters required for
|
||||
scheduling from a bare metal node, given it's power management credentials
|
||||
(e.g. IPMI address, user name and password).
|
||||
|
||||
* Free software: Apache license
|
||||
* Source: http://git.openstack.org/cgit/openstack/ironic-inspector
|
||||
* Bugs: http://bugs.launchpad.net/ironic-inspector
|
||||
* Downloads: https://pypi.python.org/pypi/ironic-inspector
|
||||
* Documentation: http://docs.openstack.org/developer/ironic-inspector
|
||||
* Python client library and CLI tool: `python-ironic-inspector-client
|
||||
<https://pypi.python.org/pypi/python-ironic-inspector-client>`_
|
||||
(`documentation
|
||||
<http://docs.openstack.org/developer/python-ironic-inspector-client>`_).
|
||||
|
||||
.. _Ironic: https://wiki.openstack.org/wiki/Ironic
|
||||
|
||||
.. note::
|
||||
**ironic-inspector** was called *ironic-discoverd* before version 2.0.0.
|
||||
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
For information on any current or prior version, see `the release notes`_.
|
||||
|
||||
.. _the release notes: http://docs.openstack.org/releasenotes/ironic-inspector/
|
@ -1,12 +0,0 @@
|
||||
[DEFAULT]
|
||||
output_file = example.conf
|
||||
namespace = ironic_inspector
|
||||
namespace = ironic_inspector.common.ironic
|
||||
namespace = ironic_inspector.common.swift
|
||||
namespace = ironic_inspector.plugins.capabilities
|
||||
namespace = ironic_inspector.plugins.discovery
|
||||
namespace = ironic_inspector.plugins.pci_devices
|
||||
namespace = keystonemiddleware.auth_token
|
||||
namespace = oslo.db
|
||||
namespace = oslo.log
|
||||
namespace = oslo.middleware.cors
|
@ -1,61 +0,0 @@
|
||||
[[local|localrc]]
|
||||
# Credentials
|
||||
# Reference: http://docs.openstack.org/developer/devstack/configuration.html
|
||||
ADMIN_PASSWORD=password
|
||||
DATABASE_PASSWORD=$ADMIN_PASSWORD
|
||||
RABBIT_PASSWORD=$ADMIN_PASSWORD
|
||||
SERVICE_PASSWORD=$ADMIN_PASSWORD
|
||||
SERVICE_TOKEN=$ADMIN_PASSWORD
|
||||
SWIFT_HASH=$ADMIN_PASSWORD
|
||||
|
||||
# Enable Neutron which is required by Ironic and disable nova-network.
|
||||
disable_service n-net n-novnc
|
||||
enable_service neutron q-svc q-agt q-dhcp q-l3 q-meta
|
||||
|
||||
# Enable Swift for agent_* drivers
|
||||
enable_service s-proxy s-object s-container s-account
|
||||
|
||||
# Enable Ironic, Ironic Inspector plugins
|
||||
enable_plugin ironic https://github.com/openstack/ironic
|
||||
enable_plugin ironic-inspector https://github.com/openstack/ironic-inspector
|
||||
|
||||
# Disable services
|
||||
disable_service horizon
|
||||
disable_service heat h-api h-api-cfn h-api-cw h-eng
|
||||
disable_service cinder c-sch c-api c-vol
|
||||
disable_service tempest
|
||||
|
||||
# Swift temp URL's are required for agent_* drivers.
|
||||
SWIFT_ENABLE_TEMPURLS=True
|
||||
|
||||
# Create 2 virtual machines to pose as Ironic's baremetal nodes.
|
||||
IRONIC_VM_COUNT=2
|
||||
IRONIC_VM_SPECS_RAM=1024
|
||||
IRONIC_VM_SPECS_DISK=10
|
||||
IRONIC_BAREMETAL_BASIC_OPS=True
|
||||
DEFAULT_INSTANCE_TYPE=baremetal
|
||||
|
||||
# Enable Ironic drivers.
|
||||
IRONIC_ENABLED_DRIVERS=fake,agent_ipmitool,pxe_ipmitool
|
||||
|
||||
# This driver should be in the enabled list above.
|
||||
IRONIC_DEPLOY_DRIVER=agent_ipmitool
|
||||
|
||||
IRONIC_BUILD_DEPLOY_RAMDISK=False
|
||||
IRONIC_INSPECTOR_BUILD_RAMDISK=False
|
||||
|
||||
VIRT_DRIVER=ironic
|
||||
|
||||
TEMPEST_ALLOW_TENANT_ISOLATION=False
|
||||
|
||||
# By default, DevStack creates a 10.0.0.0/24 network for instances.
|
||||
# If this overlaps with the hosts network, you may adjust with the
|
||||
# following.
|
||||
NETWORK_GATEWAY=10.1.0.1
|
||||
FIXED_RANGE=10.1.0.0/24
|
||||
|
||||
# Log all output to files
|
||||
LOGDAYS=1
|
||||
LOGFILE=$HOME/logs/stack.sh.log
|
||||
SCREEN_LOGDIR=$HOME/logs/screen
|
||||
IRONIC_VM_LOG_DIR=$HOME/ironic-bm-logs
|
@ -1,354 +0,0 @@
|
||||
IRONIC_INSPECTOR_DEBUG=${IRONIC_INSPECTOR_DEBUG:-True}
|
||||
IRONIC_INSPECTOR_DIR=$DEST/ironic-inspector
|
||||
IRONIC_INSPECTOR_DATA_DIR=$DATA_DIR/ironic-inspector
|
||||
IRONIC_INSPECTOR_BIN_DIR=$(get_python_exec_prefix)
|
||||
IRONIC_INSPECTOR_BIN_FILE=$IRONIC_INSPECTOR_BIN_DIR/ironic-inspector
|
||||
IRONIC_INSPECTOR_DBSYNC_BIN_FILE=$IRONIC_INSPECTOR_BIN_DIR/ironic-inspector-dbsync
|
||||
IRONIC_INSPECTOR_CONF_DIR=${IRONIC_INSPECTOR_CONF_DIR:-/etc/ironic-inspector}
|
||||
IRONIC_INSPECTOR_CONF_FILE=$IRONIC_INSPECTOR_CONF_DIR/inspector.conf
|
||||
IRONIC_INSPECTOR_CMD="$IRONIC_INSPECTOR_BIN_FILE --config-file $IRONIC_INSPECTOR_CONF_FILE"
|
||||
IRONIC_INSPECTOR_DHCP_CONF_FILE=$IRONIC_INSPECTOR_CONF_DIR/dnsmasq.conf
|
||||
IRONIC_INSPECTOR_ROOTWRAP_CONF_FILE=$IRONIC_INSPECTOR_CONF_DIR/rootwrap.conf
|
||||
IRONIC_INSPECTOR_ADMIN_USER=${IRONIC_INSPECTOR_ADMIN_USER:-ironic-inspector}
|
||||
IRONIC_INSPECTOR_AUTH_CACHE_DIR=${IRONIC_INSPECTOR_AUTH_CACHE_DIR:-/var/cache/ironic-inspector}
|
||||
IRONIC_INSPECTOR_MANAGE_FIREWALL=$(trueorfalse True IRONIC_INSPECTOR_MANAGE_FIREWALL)
|
||||
IRONIC_INSPECTOR_HOST=$HOST_IP
|
||||
IRONIC_INSPECTOR_PORT=5050
|
||||
IRONIC_INSPECTOR_URI="http://$IRONIC_INSPECTOR_HOST:$IRONIC_INSPECTOR_PORT"
|
||||
IRONIC_INSPECTOR_BUILD_RAMDISK=$(trueorfalse False IRONIC_INSPECTOR_BUILD_RAMDISK)
|
||||
IRONIC_AGENT_KERNEL_URL=${IRONIC_AGENT_KERNEL_URL:-http://tarballs.openstack.org/ironic-python-agent/coreos/files/coreos_production_pxe.vmlinuz}
|
||||
IRONIC_AGENT_RAMDISK_URL=${IRONIC_AGENT_RAMDISK_URL:-http://tarballs.openstack.org/ironic-python-agent/coreos/files/coreos_production_pxe_image-oem.cpio.gz}
|
||||
IRONIC_INSPECTOR_COLLECTORS=${IRONIC_INSPECTOR_COLLECTORS:-default,logs,pci-devices}
|
||||
IRONIC_INSPECTOR_RAMDISK_LOGDIR=${IRONIC_INSPECTOR_RAMDISK_LOGDIR:-$IRONIC_INSPECTOR_DATA_DIR/ramdisk-logs}
|
||||
IRONIC_INSPECTOR_ALWAYS_STORE_RAMDISK_LOGS=${IRONIC_INSPECTOR_ALWAYS_STORE_RAMDISK_LOGS:-True}
|
||||
IRONIC_INSPECTOR_TIMEOUT=${IRONIC_INSPECTOR_TIMEOUT:-600}
|
||||
IRONIC_INSPECTOR_CLEAN_UP_PERIOD=${IRONIC_INSPECTOR_CLEAN_UP_PERIOD:-}
|
||||
# These should not overlap with other ranges/networks
|
||||
IRONIC_INSPECTOR_INTERNAL_IP=${IRONIC_INSPECTOR_INTERNAL_IP:-172.24.42.254}
|
||||
IRONIC_INSPECTOR_INTERNAL_SUBNET_SIZE=${IRONIC_INSPECTOR_INTERNAL_SUBNET_SIZE:-24}
|
||||
IRONIC_INSPECTOR_DHCP_RANGE=${IRONIC_INSPECTOR_DHCP_RANGE:-172.24.42.100,172.24.42.253}
|
||||
IRONIC_INSPECTOR_INTERFACE=${IRONIC_INSPECTOR_INTERFACE:-br-inspector}
|
||||
IRONIC_INSPECTOR_INTERFACE_PHYSICAL=$(trueorfalse False IRONIC_INSPECTOR_INTERFACE_PHYSICAL)
|
||||
IRONIC_INSPECTOR_INTERNAL_URI="http://$IRONIC_INSPECTOR_INTERNAL_IP:$IRONIC_INSPECTOR_PORT"
|
||||
IRONIC_INSPECTOR_INTERNAL_IP_WITH_NET="$IRONIC_INSPECTOR_INTERNAL_IP/$IRONIC_INSPECTOR_INTERNAL_SUBNET_SIZE"
|
||||
# Whether DevStack will be setup for bare metal or VMs
|
||||
IRONIC_IS_HARDWARE=$(trueorfalse False IRONIC_IS_HARDWARE)
|
||||
IRONIC_INSPECTOR_NODE_NOT_FOUND_HOOK=${IRONIC_INSPECTOR_NODE_NOT_FOUND_HOOK:-""}
|
||||
IRONIC_INSPECTOR_OVS_PORT=${IRONIC_INSPECTOR_OVS_PORT:-brbm-inspector}
|
||||
|
||||
GITDIR["python-ironic-inspector-client"]=$DEST/python-ironic-inspector-client
|
||||
GITREPO["python-ironic-inspector-client"]=${IRONIC_INSPECTOR_CLIENT_REPO:-${GIT_BASE}/openstack/python-ironic-inspector-client.git}
|
||||
GITBRANCH["python-ironic-inspector-client"]=${IRONIC_INSPECTOR_CLIENT_BRANCH:-master}
|
||||
|
||||
### Utilities
|
||||
|
||||
function mkdir_chown_stack {
|
||||
if [[ ! -d "$1" ]]; then
|
||||
sudo mkdir -p "$1"
|
||||
fi
|
||||
sudo chown $STACK_USER "$1"
|
||||
}
|
||||
|
||||
function inspector_iniset {
|
||||
iniset "$IRONIC_INSPECTOR_CONF_FILE" $1 $2 $3
|
||||
}
|
||||
|
||||
### Install-start-stop
|
||||
|
||||
function install_inspector {
|
||||
setup_develop $IRONIC_INSPECTOR_DIR
|
||||
}
|
||||
|
||||
function install_inspector_dhcp {
|
||||
install_package dnsmasq
|
||||
}
|
||||
|
||||
function install_inspector_client {
|
||||
if use_library_from_git python-ironic-inspector-client; then
|
||||
git_clone_by_name python-ironic-inspector-client
|
||||
setup_dev_lib python-ironic-inspector-client
|
||||
else
|
||||
pip_install_gr python-ironic-inspector-client
|
||||
fi
|
||||
}
|
||||
|
||||
function start_inspector {
|
||||
run_process ironic-inspector "$IRONIC_INSPECTOR_CMD"
|
||||
}
|
||||
|
||||
function start_inspector_dhcp {
|
||||
# NOTE(dtantsur): USE_SYSTEMD requires an absolute path
|
||||
run_process ironic-inspector-dhcp \
|
||||
"$(which dnsmasq) --conf-file=$IRONIC_INSPECTOR_DHCP_CONF_FILE" \
|
||||
"" root
|
||||
}
|
||||
|
||||
function stop_inspector {
|
||||
stop_process ironic-inspector
|
||||
}
|
||||
|
||||
function stop_inspector_dhcp {
|
||||
stop_process ironic-inspector-dhcp
|
||||
}
|
||||
|
||||
### Configuration
|
||||
|
||||
function prepare_tftp {
|
||||
IRONIC_INSPECTOR_IMAGE_PATH="$TOP_DIR/files/ironic-inspector"
|
||||
IRONIC_INSPECTOR_KERNEL_PATH="$IRONIC_INSPECTOR_IMAGE_PATH.kernel"
|
||||
IRONIC_INSPECTOR_INITRAMFS_PATH="$IRONIC_INSPECTOR_IMAGE_PATH.initramfs"
|
||||
IRONIC_INSPECTOR_CALLBACK_URI="$IRONIC_INSPECTOR_INTERNAL_URI/v1/continue"
|
||||
|
||||
IRONIC_INSPECTOR_KERNEL_CMDLINE="ipa-inspection-callback-url=$IRONIC_INSPECTOR_CALLBACK_URI systemd.journald.forward_to_console=yes"
|
||||
IRONIC_INSPECTOR_KERNEL_CMDLINE="$IRONIC_INSPECTOR_KERNEL_CMDLINE vga=normal console=tty0 console=ttyS0"
|
||||
IRONIC_INSPECTOR_KERNEL_CMDLINE="$IRONIC_INSPECTOR_KERNEL_CMDLINE ipa-inspection-collectors=$IRONIC_INSPECTOR_COLLECTORS"
|
||||
IRONIC_INSPECTOR_KERNEL_CMDLINE="$IRONIC_INSPECTOR_KERNEL_CMDLINE ipa-debug=1"
|
||||
if [[ "$IRONIC_INSPECTOR_BUILD_RAMDISK" == "True" ]]; then
|
||||
if [ ! -e "$IRONIC_INSPECTOR_KERNEL_PATH" -o ! -e "$IRONIC_INSPECTOR_INITRAMFS_PATH" ]; then
|
||||
build_ipa_ramdisk "$IRONIC_INSPECTOR_KERNEL_PATH" "$IRONIC_INSPECTOR_INITRAMFS_PATH"
|
||||
fi
|
||||
else
|
||||
# download the agent image tarball
|
||||
if [ ! -e "$IRONIC_INSPECTOR_KERNEL_PATH" -o ! -e "$IRONIC_INSPECTOR_INITRAMFS_PATH" ]; then
|
||||
if [ -e "$IRONIC_DEPLOY_KERNEL" -a -e "$IRONIC_DEPLOY_RAMDISK" ]; then
|
||||
cp $IRONIC_DEPLOY_KERNEL $IRONIC_INSPECTOR_KERNEL_PATH
|
||||
cp $IRONIC_DEPLOY_RAMDISK $IRONIC_INSPECTOR_INITRAMFS_PATH
|
||||
else
|
||||
wget "$IRONIC_AGENT_KERNEL_URL" -O $IRONIC_INSPECTOR_KERNEL_PATH
|
||||
wget "$IRONIC_AGENT_RAMDISK_URL" -O $IRONIC_INSPECTOR_INITRAMFS_PATH
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$IRONIC_IPXE_ENABLED" == "True" ]] ; then
|
||||
cp $IRONIC_INSPECTOR_KERNEL_PATH $IRONIC_HTTP_DIR/ironic-inspector.kernel
|
||||
cp $IRONIC_INSPECTOR_INITRAMFS_PATH $IRONIC_HTTP_DIR
|
||||
|
||||
cat > "$IRONIC_HTTP_DIR/ironic-inspector.ipxe" <<EOF
|
||||
#!ipxe
|
||||
|
||||
dhcp
|
||||
|
||||
kernel http://$IRONIC_HTTP_SERVER:$IRONIC_HTTP_PORT/ironic-inspector.kernel BOOTIF=\${mac} $IRONIC_INSPECTOR_KERNEL_CMDLINE
|
||||
initrd http://$IRONIC_HTTP_SERVER:$IRONIC_HTTP_PORT/ironic-inspector.initramfs
|
||||
boot
|
||||
EOF
|
||||
else
|
||||
mkdir_chown_stack "$IRONIC_TFTPBOOT_DIR/pxelinux.cfg"
|
||||
cp $IRONIC_INSPECTOR_KERNEL_PATH $IRONIC_TFTPBOOT_DIR/ironic-inspector.kernel
|
||||
cp $IRONIC_INSPECTOR_INITRAMFS_PATH $IRONIC_TFTPBOOT_DIR
|
||||
|
||||
cat > "$IRONIC_TFTPBOOT_DIR/pxelinux.cfg/default" <<EOF
|
||||
default inspect
|
||||
|
||||
label inspect
|
||||
kernel ironic-inspector.kernel
|
||||
append initrd=ironic-inspector.initramfs $IRONIC_INSPECTOR_KERNEL_CMDLINE
|
||||
|
||||
ipappend 3
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
function inspector_configure_auth_for {
|
||||
inspector_iniset $1 auth_type password
|
||||
inspector_iniset $1 auth_url "$KEYSTONE_SERVICE_URI"
|
||||
inspector_iniset $1 username $IRONIC_INSPECTOR_ADMIN_USER
|
||||
inspector_iniset $1 password $SERVICE_PASSWORD
|
||||
inspector_iniset $1 project_name $SERVICE_PROJECT_NAME
|
||||
inspector_iniset $1 user_domain_id default
|
||||
inspector_iniset $1 project_domain_id default
|
||||
inspector_iniset $1 cafile $SSL_BUNDLE_FILE
|
||||
inspector_iniset $1 os_region $REGION_NAME
|
||||
}
|
||||
|
||||
function configure_inspector {
|
||||
mkdir_chown_stack "$IRONIC_INSPECTOR_CONF_DIR"
|
||||
mkdir_chown_stack "$IRONIC_INSPECTOR_DATA_DIR"
|
||||
|
||||
create_service_user "$IRONIC_INSPECTOR_ADMIN_USER" "admin"
|
||||
|
||||
cp "$IRONIC_INSPECTOR_DIR/example.conf" "$IRONIC_INSPECTOR_CONF_FILE"
|
||||
inspector_iniset DEFAULT debug $IRONIC_INSPECTOR_DEBUG
|
||||
inspector_configure_auth_for ironic
|
||||
configure_auth_token_middleware $IRONIC_INSPECTOR_CONF_FILE $IRONIC_INSPECTOR_ADMIN_USER $IRONIC_INSPECTOR_AUTH_CACHE_DIR/api
|
||||
|
||||
inspector_iniset DEFAULT listen_port $IRONIC_INSPECTOR_PORT
|
||||
inspector_iniset DEFAULT listen_address 0.0.0.0 # do not change
|
||||
|
||||
inspector_iniset firewall manage_firewall $IRONIC_INSPECTOR_MANAGE_FIREWALL
|
||||
inspector_iniset firewall dnsmasq_interface $IRONIC_INSPECTOR_INTERFACE
|
||||
inspector_iniset database connection `database_connection_url ironic_inspector`
|
||||
|
||||
is_service_enabled swift && configure_inspector_swift
|
||||
|
||||
iniset "$IRONIC_CONF_FILE" inspector enabled True
|
||||
iniset "$IRONIC_CONF_FILE" inspector service_url $IRONIC_INSPECTOR_URI
|
||||
|
||||
setup_logging $IRONIC_INSPECTOR_CONF_FILE DEFAULT
|
||||
|
||||
cp "$IRONIC_INSPECTOR_DIR/rootwrap.conf" "$IRONIC_INSPECTOR_ROOTWRAP_CONF_FILE"
|
||||
cp -r "$IRONIC_INSPECTOR_DIR/rootwrap.d" "$IRONIC_INSPECTOR_CONF_DIR"
|
||||
local ironic_inspector_rootwrap=$(get_rootwrap_location ironic-inspector)
|
||||
local rootwrap_sudoer_cmd="$ironic_inspector_rootwrap $IRONIC_INSPECTOR_CONF_DIR/rootwrap.conf *"
|
||||
|
||||
# Set up the rootwrap sudoers for ironic-inspector
|
||||
local tempfile=`mktemp`
|
||||
echo "$STACK_USER ALL=(root) NOPASSWD: $rootwrap_sudoer_cmd" >$tempfile
|
||||
chmod 0640 $tempfile
|
||||
sudo chown root:root $tempfile
|
||||
sudo mv $tempfile /etc/sudoers.d/ironic-inspector-rootwrap
|
||||
|
||||
inspector_iniset DEFAULT rootwrap_config $IRONIC_INSPECTOR_ROOTWRAP_CONF_FILE
|
||||
|
||||
mkdir_chown_stack "$IRONIC_INSPECTOR_RAMDISK_LOGDIR"
|
||||
inspector_iniset processing ramdisk_logs_dir "$IRONIC_INSPECTOR_RAMDISK_LOGDIR"
|
||||
inspector_iniset processing always_store_ramdisk_logs "$IRONIC_INSPECTOR_ALWAYS_STORE_RAMDISK_LOGS"
|
||||
if [ -n "$IRONIC_INSPECTOR_NODE_NOT_FOUND_HOOK" ]; then
|
||||
inspector_iniset processing node_not_found_hook "$IRONIC_INSPECTOR_NODE_NOT_FOUND_HOOK"
|
||||
fi
|
||||
inspector_iniset DEFAULT timeout $IRONIC_INSPECTOR_TIMEOUT
|
||||
if [ -n "$IRONIC_INSPECTOR_CLEAN_UP_PERIOD" ]; then
|
||||
inspector_iniset DEFAULT clean_up_period "$IRONIC_INSPECTOR_CLEAN_UP_PERIOD"
|
||||
fi
|
||||
get_or_create_service "ironic-inspector" "baremetal-introspection" "Ironic Inspector baremetal introspection service"
|
||||
get_or_create_endpoint "baremetal-introspection" "$REGION_NAME" \
|
||||
"$IRONIC_INSPECTOR_URI" "$IRONIC_INSPECTOR_URI" "$IRONIC_INSPECTOR_URI"
|
||||
|
||||
}
|
||||
|
||||
function configure_inspector_swift {
|
||||
inspector_configure_auth_for swift
|
||||
inspector_iniset processing store_data swift
|
||||
}
|
||||
|
||||
function configure_inspector_dhcp {
|
||||
mkdir_chown_stack "$IRONIC_INSPECTOR_CONF_DIR"
|
||||
|
||||
if [[ "$IRONIC_IPXE_ENABLED" == "True" ]] ; then
|
||||
cat > "$IRONIC_INSPECTOR_DHCP_CONF_FILE" <<EOF
|
||||
no-daemon
|
||||
port=0
|
||||
interface=$IRONIC_INSPECTOR_INTERFACE
|
||||
bind-interfaces
|
||||
dhcp-range=$IRONIC_INSPECTOR_DHCP_RANGE
|
||||
dhcp-match=ipxe,175
|
||||
dhcp-boot=tag:!ipxe,undionly.kpxe
|
||||
dhcp-boot=tag:ipxe,http://$IRONIC_HTTP_SERVER:$IRONIC_HTTP_PORT/ironic-inspector.ipxe
|
||||
dhcp-sequential-ip
|
||||
EOF
|
||||
else
|
||||
cat > "$IRONIC_INSPECTOR_DHCP_CONF_FILE" <<EOF
|
||||
no-daemon
|
||||
port=0
|
||||
interface=$IRONIC_INSPECTOR_INTERFACE
|
||||
bind-interfaces
|
||||
dhcp-range=$IRONIC_INSPECTOR_DHCP_RANGE
|
||||
dhcp-boot=pxelinux.0
|
||||
dhcp-sequential-ip
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
function prepare_environment {
|
||||
prepare_tftp
|
||||
create_ironic_inspector_cache_dir
|
||||
|
||||
if [[ "$IRONIC_BAREMETAL_BASIC_OPS" == "True" && "$IRONIC_IS_HARDWARE" == "False" ]]; then
|
||||
sudo ip link add $IRONIC_INSPECTOR_OVS_PORT type veth peer name $IRONIC_INSPECTOR_INTERFACE
|
||||
sudo ip link set dev $IRONIC_INSPECTOR_OVS_PORT up
|
||||
sudo ip link set dev $IRONIC_INSPECTOR_OVS_PORT mtu $PUBLIC_BRIDGE_MTU
|
||||
sudo ovs-vsctl add-port $IRONIC_VM_NETWORK_BRIDGE $IRONIC_INSPECTOR_OVS_PORT
|
||||
fi
|
||||
sudo ip link set dev $IRONIC_INSPECTOR_INTERFACE up
|
||||
sudo ip link set dev $IRONIC_INSPECTOR_INTERFACE mtu $PUBLIC_BRIDGE_MTU
|
||||
sudo ip addr add $IRONIC_INSPECTOR_INTERNAL_IP_WITH_NET dev $IRONIC_INSPECTOR_INTERFACE
|
||||
|
||||
sudo iptables -I INPUT -i $IRONIC_INSPECTOR_INTERFACE -p udp \
|
||||
--dport 69 -j ACCEPT
|
||||
sudo iptables -I INPUT -i $IRONIC_INSPECTOR_INTERFACE -p tcp \
|
||||
--dport $IRONIC_INSPECTOR_PORT -j ACCEPT
|
||||
}
|
||||
|
||||
# create_ironic_inspector_cache_dir() - Part of the prepare_environment() process
|
||||
function create_ironic_inspector_cache_dir {
|
||||
# Create cache dir
|
||||
mkdir_chown_stack $IRONIC_INSPECTOR_AUTH_CACHE_DIR/api
|
||||
rm -f $IRONIC_INSPECTOR_AUTH_CACHE_DIR/api/*
|
||||
mkdir_chown_stack $IRONIC_INSPECTOR_AUTH_CACHE_DIR/registry
|
||||
rm -f $IRONIC_INSPECTOR_AUTH_CACHE_DIR/registry/*
|
||||
}
|
||||
|
||||
function cleanup_inspector {
|
||||
if [[ "$IRONIC_IPXE_ENABLED" == "True" ]] ; then
|
||||
rm -f $IRONIC_HTTP_DIR/ironic-inspector.*
|
||||
else
|
||||
rm -f $IRONIC_TFTPBOOT_DIR/pxelinux.cfg/default
|
||||
rm -f $IRONIC_TFTPBOOT_DIR/ironic-inspector.*
|
||||
fi
|
||||
sudo rm -f /etc/sudoers.d/ironic-inspector-rootwrap
|
||||
sudo rm -rf $IRONIC_INSPECTOR_AUTH_CACHE_DIR
|
||||
sudo rm -rf "$IRONIC_INSPECTOR_RAMDISK_LOGDIR"
|
||||
|
||||
# Try to clean up firewall rules
|
||||
sudo iptables -D INPUT -i $IRONIC_INSPECTOR_INTERFACE -p udp \
|
||||
--dport 69 -j ACCEPT | true
|
||||
sudo iptables -D INPUT -i $IRONIC_INSPECTOR_INTERFACE -p tcp \
|
||||
--dport $IRONIC_INSPECTOR_PORT -j ACCEPT | true
|
||||
sudo iptables -D INPUT -i $IRONIC_INSPECTOR_INTERFACE -p udp \
|
||||
--dport 67 -j ironic-inspector | true
|
||||
sudo iptables -F ironic-inspector | true
|
||||
sudo iptables -X ironic-inspector | true
|
||||
|
||||
if [[ $IRONIC_INSPECTOR_INTERFACE != $OVS_PHYSICAL_BRIDGE && "$IRONIC_INSPECTOR_INTERFACE_PHYSICAL" == "False" ]]; then
|
||||
sudo ip link show $IRONIC_INSPECTOR_INTERFACE && sudo ip link delete $IRONIC_INSPECTOR_INTERFACE
|
||||
fi
|
||||
sudo ip link show $IRONIC_INSPECTOR_OVS_PORT && sudo ip link delete $IRONIC_INSPECTOR_OVS_PORT
|
||||
sudo ovs-vsctl --if-exists del-port $IRONIC_INSPECTOR_OVS_PORT
|
||||
}
|
||||
|
||||
function sync_inspector_database {
|
||||
recreate_database ironic_inspector
|
||||
$IRONIC_INSPECTOR_DBSYNC_BIN_FILE --config-file $IRONIC_INSPECTOR_CONF_FILE upgrade
|
||||
}
|
||||
|
||||
### Entry points
|
||||
|
||||
if [[ "$1" == "stack" && "$2" == "install" ]]; then
|
||||
echo_summary "Installing ironic-inspector"
|
||||
if [[ "$IRONIC_INSPECTOR_MANAGE_FIREWALL" == "True" ]]; then
|
||||
install_inspector_dhcp
|
||||
fi
|
||||
install_inspector
|
||||
install_inspector_client
|
||||
elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
|
||||
echo_summary "Configuring ironic-inspector"
|
||||
cleanup_inspector
|
||||
if [[ "$IRONIC_INSPECTOR_MANAGE_FIREWALL" == "True" ]]; then
|
||||
configure_inspector_dhcp
|
||||
fi
|
||||
configure_inspector
|
||||
sync_inspector_database
|
||||
elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
|
||||
echo_summary "Initializing ironic-inspector"
|
||||
prepare_environment
|
||||
if [[ "$IRONIC_INSPECTOR_MANAGE_FIREWALL" == "True" ]]; then
|
||||
start_inspector_dhcp
|
||||
fi
|
||||
start_inspector
|
||||
elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then
|
||||
if is_service_enabled tempest; then
|
||||
echo_summary "Configuring Tempest for Ironic Inspector"
|
||||
if [ -n "$IRONIC_INSPECTOR_NODE_NOT_FOUND_HOOK" ]; then
|
||||
iniset $TEMPEST_CONFIG baremetal_introspection auto_discovery_feature True
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$1" == "unstack" ]]; then
|
||||
stop_inspector
|
||||
if [[ "$IRONIC_INSPECTOR_MANAGE_FIREWALL" == "True" ]]; then
|
||||
stop_inspector_dhcp
|
||||
fi
|
||||
cleanup_inspector
|
||||
fi
|
@ -1 +0,0 @@
|
||||
enable_service ironic-inspector ironic-inspector-dhcp
|
@ -1,77 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2016 Intel Corporation
|
||||
# Copyright 2016 Red Hat, Inc.
|
||||
#
|
||||
# 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.
|
||||
## based on Ironic/devstack/upgrade/resources.sh
|
||||
|
||||
set -o errexit
|
||||
|
||||
source $GRENADE_DIR/grenaderc
|
||||
source $GRENADE_DIR/functions
|
||||
|
||||
source $TOP_DIR/openrc admin admin
|
||||
|
||||
# Inspector relies on a couple of Ironic variables
|
||||
source $TARGET_RELEASE_DIR/ironic/devstack/lib/ironic
|
||||
|
||||
INSPECTOR_DEVSTACK_DIR=$(cd $(dirname "$0")/.. && pwd)
|
||||
source $INSPECTOR_DEVSTACK_DIR/plugin.sh
|
||||
|
||||
set -o xtrace
|
||||
|
||||
|
||||
function early_create {
|
||||
:
|
||||
}
|
||||
|
||||
function create {
|
||||
:
|
||||
}
|
||||
|
||||
function verify {
|
||||
:
|
||||
}
|
||||
|
||||
function verify_noapi {
|
||||
:
|
||||
}
|
||||
|
||||
function destroy {
|
||||
:
|
||||
}
|
||||
|
||||
# Dispatcher
|
||||
case $1 in
|
||||
"early_create")
|
||||
early_create
|
||||
;;
|
||||
"create")
|
||||
create
|
||||
;;
|
||||
"verify_noapi")
|
||||
verify_noapi
|
||||
;;
|
||||
"verify")
|
||||
verify
|
||||
;;
|
||||
"destroy")
|
||||
destroy
|
||||
;;
|
||||
"force_destroy")
|
||||
set +o errexit
|
||||
destroy
|
||||
;;
|
||||
esac
|
@ -1,4 +0,0 @@
|
||||
# Enabling Inspector grenade plug-in
|
||||
# Based on Ironic/devstack/grenade/settings
|
||||
register_project_for_upgrade ironic-inspector
|
||||
register_db_to_save ironic_inspector
|
@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# based on Ironic/devstack/upgrade/shutdown.sh
|
||||
|
||||
set -o errexit
|
||||
|
||||
source $GRENADE_DIR/grenaderc
|
||||
source $GRENADE_DIR/functions
|
||||
|
||||
# We need base DevStack functions for this
|
||||
source $BASE_DEVSTACK_DIR/functions
|
||||
source $BASE_DEVSTACK_DIR/stackrc # needed for status directory
|
||||
source $BASE_DEVSTACK_DIR/lib/tls
|
||||
source $BASE_DEVSTACK_DIR/lib/apache
|
||||
|
||||
# Inspector relies on a couple of Ironic variables
|
||||
source $TARGET_RELEASE_DIR/ironic/devstack/lib/ironic
|
||||
|
||||
# Keep track of the DevStack directory
|
||||
INSPECTOR_DEVSTACK_DIR=$(cd $(dirname "$0")/.. && pwd)
|
||||
source $INSPECTOR_DEVSTACK_DIR/plugin.sh
|
||||
|
||||
|
||||
set -o xtrace
|
||||
|
||||
stop_inspector
|
||||
if [[ "$IRONIC_INSPECTOR_MANAGE_FIREWALL" == "True" ]]; then
|
||||
stop_inspector_dhcp
|
||||
fi
|
@ -1,106 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
## based on Ironic/devstack/upgrade/upgrade.sh
|
||||
|
||||
# ``upgrade-inspector``
|
||||
|
||||
echo "*********************************************************************"
|
||||
echo "Begin $0"
|
||||
echo "*********************************************************************"
|
||||
|
||||
# Clean up any resources that may be in use
|
||||
cleanup() {
|
||||
set +o errexit
|
||||
|
||||
echo "*********************************************************************"
|
||||
echo "ERROR: Abort $0"
|
||||
echo "*********************************************************************"
|
||||
|
||||
# Kill ourselves to signal any calling process
|
||||
trap 2; kill -2 $$
|
||||
}
|
||||
|
||||
trap cleanup SIGHUP SIGINT SIGTERM
|
||||
|
||||
# Keep track of the grenade directory
|
||||
RUN_DIR=$(cd $(dirname "$0") && pwd)
|
||||
|
||||
# Source params
|
||||
source $GRENADE_DIR/grenaderc
|
||||
|
||||
# Import common functions
|
||||
source $GRENADE_DIR/functions
|
||||
|
||||
# This script exits on an error so that errors don't compound and you see
|
||||
# only the first error that occurred.
|
||||
set -o errexit
|
||||
|
||||
# Upgrade Inspector
|
||||
# =================
|
||||
|
||||
# Duplicate some setup bits from target DevStack
|
||||
source $TARGET_DEVSTACK_DIR/stackrc
|
||||
source $TARGET_DEVSTACK_DIR/lib/tls
|
||||
source $TARGET_DEVSTACK_DIR/lib/nova
|
||||
source $TARGET_DEVSTACK_DIR/lib/neutron-legacy
|
||||
source $TARGET_DEVSTACK_DIR/lib/apache
|
||||
source $TARGET_DEVSTACK_DIR/lib/keystone
|
||||
source $TARGET_DEVSTACK_DIR/lib/database
|
||||
|
||||
# Inspector relies on couple of Ironic variables
|
||||
source $TARGET_RELEASE_DIR/ironic/devstack/lib/ironic
|
||||
|
||||
# Keep track of the DevStack directory
|
||||
INSPECTOR_DEVSTACK_DIR=$(cd $(dirname "$0")/.. && pwd)
|
||||
INSPECTOR_PLUGIN=$INSPECTOR_DEVSTACK_DIR/plugin.sh
|
||||
source $INSPECTOR_PLUGIN
|
||||
|
||||
# Print the commands being run so that we can see the command that triggers
|
||||
# an error. It is also useful for following allowing as the install occurs.
|
||||
set -o xtrace
|
||||
|
||||
initialize_database_backends
|
||||
|
||||
function wait_for_keystone {
|
||||
if ! wait_for_service $SERVICE_TIMEOUT ${KEYSTONE_AUTH_URI}/v$IDENTITY_API_VERSION/; then
|
||||
die $LINENO "keystone did not start"
|
||||
fi
|
||||
}
|
||||
|
||||
# Save current config files for posterity
|
||||
if [[ -d $IRONIC_INSPECTOR_CONF_DIR ]] && [[ ! -d $SAVE_DIR/etc.inspector ]] ; then
|
||||
cp -pr $IRONIC_INSPECTOR_CONF_DIR $SAVE_DIR/etc.inspector
|
||||
fi
|
||||
|
||||
# This call looks for install_<NAME>, which is install_inspector in our case:
|
||||
# https://github.com/openstack-dev/devstack/blob/dec121114c3ea6f9e515a452700e5015d1e34704/lib/stack#L32
|
||||
stack_install_service inspector
|
||||
|
||||
if [[ "$IRONIC_INSPECTOR_MANAGE_FIREWALL" == "True" ]]; then
|
||||
stack_install_service inspector_dhcp
|
||||
fi
|
||||
|
||||
$IRONIC_INSPECTOR_DBSYNC_BIN_FILE --config-file $IRONIC_INSPECTOR_CONF_FILE upgrade
|
||||
|
||||
# calls upgrade inspector for specific release
|
||||
upgrade_project ironic-inspector $RUN_DIR $BASE_DEVSTACK_BRANCH $TARGET_DEVSTACK_BRANCH
|
||||
|
||||
|
||||
start_inspector
|
||||
|
||||
if [[ "$IRONIC_INSPECTOR_MANAGE_FIREWALL" == "True" ]]; then
|
||||
start_inspector_dhcp
|
||||
fi
|
||||
|
||||
# Don't succeed unless the services come up
|
||||
ensure_services_started ironic-inspector
|
||||
ensure_logs_exist ironic-inspector
|
||||
|
||||
if [[ "$IRONIC_INSPECTOR_MANAGE_FIREWALL" == "True" ]]; then
|
||||
ensure_services_started dnsmasq
|
||||
ensure_logs_exist ironic-inspector-dhcp
|
||||
fi
|
||||
|
||||
set +o xtrace
|
||||
echo "*********************************************************************"
|
||||
echo "SUCCESS: End $0"
|
||||
echo "*********************************************************************"
|
159
doc/Makefile
159
doc/Makefile
@ -1,159 +0,0 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Heat.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Heat.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Heat"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Heat"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The xml files are in $(BUILDDIR)/xml."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
2
doc/source/.gitignore
vendored
2
doc/source/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
target/
|
||||
build/
|
@ -1,10 +0,0 @@
|
||||
Administrator Guide
|
||||
===================
|
||||
|
||||
How to upgrade Ironic Inspector
|
||||
-------------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
upgrade
|
@ -1,28 +0,0 @@
|
||||
Upgrade Guide
|
||||
-------------
|
||||
|
||||
The `release notes <http://docs.openstack.org/releasenotes/ironic-inspector/>`_
|
||||
should always be read carefully when upgrading the ironic-inspector service.
|
||||
Starting with the Mitaka series, specific upgrade steps and considerations are
|
||||
well-documented in the release notes.
|
||||
|
||||
Upgrades are only supported one series at a time, or within a series.
|
||||
Only offline (with downtime) upgrades are currently supported.
|
||||
|
||||
When upgrading ironic-inspector, the following steps should always be taken:
|
||||
|
||||
* Update ironic-inspector code, without restarting the service yet.
|
||||
|
||||
* Stop the ironic-inspector service.
|
||||
|
||||
* Run database migrations::
|
||||
|
||||
ironic-inspector-dbsync --config-file <PATH-TO-INSPECTOR.CONF> upgrade
|
||||
|
||||
* Start the ironic-inspector service.
|
||||
|
||||
* Upgrade the ironic-python-agent image used for introspection.
|
||||
|
||||
.. note::
|
||||
There is no implicit upgrade order between ironic and ironic-inspector,
|
||||
unless the `release notes`_ say otherwise.
|
@ -1,103 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
# -- 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 = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
try:
|
||||
import openstackdocstheme
|
||||
extensions.append('openstackdocstheme')
|
||||
except ImportError:
|
||||
openstackdocstheme = None
|
||||
|
||||
repository_name = 'openstack/ironic-inspector'
|
||||
bug_project = 'ironic-inspector'
|
||||
bug_tag = ''
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
wsme_protocols = ['restjson']
|
||||
|
||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
||||
# text edit cycles.
|
||||
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
||||
|
||||
# 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.
|
||||
project = u'Ironic Inspector'
|
||||
copyright = u'OpenStack Foundation'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
#from ironic import version as ironic_version
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
#release = ironic_version.version_info.release_string()
|
||||
# The short X.Y version.
|
||||
#version = ironic_version.version_info.version_string()
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
modindex_common_prefix = ['ironic.']
|
||||
|
||||
# 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 = 'sphinx'
|
||||
|
||||
# NOTE(cinerama): mock out nova modules so docs can build without warnings
|
||||
#import mock
|
||||
#import sys
|
||||
#MOCK_MODULES = ['nova', 'nova.compute', 'nova.context']
|
||||
#for module in MOCK_MODULES:
|
||||
# sys.modules[module] = mock.Mock()
|
||||
|
||||
# -- 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'.
|
||||
if openstackdocstheme is not None:
|
||||
html_theme = 'openstackdocs'
|
||||
else:
|
||||
html_theme = 'default'
|
||||
#html_theme_path = ["."]
|
||||
#html_theme = '_theme'
|
||||
#html_static_path = ['_static']
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = '%sdoc' % project
|
||||
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass
|
||||
# [howto/manual]).
|
||||
latex_documents = [
|
||||
(
|
||||
'index',
|
||||
'%s.tex' % project,
|
||||
u'%s Documentation' % project,
|
||||
u'OpenStack Foundation',
|
||||
'manual'
|
||||
),
|
||||
]
|
||||
|
||||
# -- Options for seqdiag ------------------------------------------------------
|
||||
|
||||
seqdiag_html_image_format = "SVG"
|
@ -1,11 +0,0 @@
|
||||
.. _contributing_link:
|
||||
|
||||
.. include:: ../../../CONTRIBUTING.rst
|
||||
|
||||
Python API
|
||||
~~~~~~~~~~
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api/autoindex
|
@ -1,230 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: Ironic Inspector states Pages: 1 -->
|
||||
<svg width="842pt" height="383pt"
|
||||
viewBox="0.00 0.00 841.68 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 379)">
|
||||
<title>Ironic Inspector states</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-379 837.6827,-379 837.6827,4 -4,4"/>
|
||||
<!-- enrolling -->
|
||||
<g id="node1" class="node">
|
||||
<title>enrolling</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="32.7967" cy="-223" rx="32.5946" ry="18"/>
|
||||
<text text-anchor="middle" x="32.7967" y="-219.7" font-family="Times,serif" font-size="11.00" fill="#c0c0c0">enrolling</text>
|
||||
</g>
|
||||
<!-- error -->
|
||||
<g id="node2" class="node">
|
||||
<title>error</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="159.1451" cy="-190" rx="27" ry="18"/>
|
||||
<text text-anchor="middle" x="159.1451" y="-186.7" font-family="Times,serif" font-size="11.00" fill="#ff0000">error</text>
|
||||
</g>
|
||||
<!-- enrolling->error -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>enrolling->error</title>
|
||||
<path fill="none" stroke="#000000" d="M62.4198,-215.263C80.9292,-210.4286 104.8518,-204.1805 124.2748,-199.1075"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="125.1781,-202.4891 133.969,-196.5756 123.4091,-195.7163 125.1781,-202.4891"/>
|
||||
<text text-anchor="middle" x="98.8693" y="-211" font-family="Times,serif" font-size="10.00" fill="#ff0000">error</text>
|
||||
</g>
|
||||
<!-- enrolling->error -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>enrolling->error</title>
|
||||
<path fill="none" stroke="#000000" d="M50.793,-207.6437C59.9733,-200.8122 71.6953,-193.5255 83.5934,-190 95.8748,-186.3609 109.7805,-185.4983 122.284,-185.8233"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="122.2248,-189.3248 132.3907,-186.3408 122.5828,-182.3339 122.2248,-189.3248"/>
|
||||
<text text-anchor="middle" x="98.8693" y="-192" font-family="Times,serif" font-size="10.00" fill="#ff0000">timeout</text>
|
||||
</g>
|
||||
<!-- processing -->
|
||||
<g id="node3" class="node">
|
||||
<title>processing</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="796.5702" cy="-294" rx="37.2253" ry="18"/>
|
||||
<text text-anchor="middle" x="796.5702" y="-290.7" font-family="Times,serif" font-size="11.00" fill="#c0c0c0">processing</text>
|
||||
</g>
|
||||
<!-- enrolling->processing -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>enrolling->processing</title>
|
||||
<path fill="none" stroke="#000000" d="M38.9232,-240.6973C53.3543,-278.7629 93.3124,-365 159.1451,-365 159.1451,-365 159.1451,-365 664.6261,-365 705.9926,-365 746.88,-337.6031 771.9214,-316.8465"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="774.4666,-319.2748 779.784,-310.111 769.9125,-313.9587 774.4666,-319.2748"/>
|
||||
<text text-anchor="middle" x="423.3931" y="-367" font-family="Times,serif" font-size="10.00" fill="#000000">process</text>
|
||||
</g>
|
||||
<!-- error->error -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>error->error</title>
|
||||
<path fill="none" stroke="#000000" d="M153.7522,-207.7817C152.8358,-217.3149 154.6334,-226 159.1451,-226 161.8945,-226 163.636,-222.7749 164.3696,-218.0981"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="167.8742,-217.8376 164.5381,-207.7817 160.8752,-217.7232 167.8742,-217.8376"/>
|
||||
<text text-anchor="middle" x="159.1451" y="-228" font-family="Times,serif" font-size="10.00" fill="#ff0000">abort</text>
|
||||
</g>
|
||||
<!-- error->error -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>error->error</title>
|
||||
<path fill="none" stroke="#000000" d="M150.3671,-207.1418C145.2035,-224.585 148.1295,-244 159.1451,-244 168.0953,-244 171.7051,-231.183 169.9745,-217.0206"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="173.3833,-216.2213 167.9232,-207.1418 166.5295,-217.6445 173.3833,-216.2213"/>
|
||||
<text text-anchor="middle" x="159.1451" y="-246" font-family="Times,serif" font-size="10.00" fill="#ff0000">error</text>
|
||||
</g>
|
||||
<!-- reapplying -->
|
||||
<g id="node4" class="node">
|
||||
<title>reapplying</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="289.8062" cy="-207" rx="37.219" ry="18"/>
|
||||
<text text-anchor="middle" x="289.8062" y="-203.7" font-family="Times,serif" font-size="11.00" fill="#c0c0c0">reapplying</text>
|
||||
</g>
|
||||
<!-- error->reapplying -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>error->reapplying</title>
|
||||
<path fill="none" stroke="#000000" d="M170.8266,-206.6885C178.8931,-216.6767 190.5869,-228.473 204.1451,-234 222.0093,-241.2823 242.6135,-235.2198 259.137,-227.0102"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="261.2238,-229.8599 268.3427,-222.0131 257.8843,-223.7078 261.2238,-229.8599"/>
|
||||
<text text-anchor="middle" x="219.421" y="-239" font-family="Times,serif" font-size="10.00" fill="#000000">reapply</text>
|
||||
</g>
|
||||
<!-- starting -->
|
||||
<g id="node5" class="node">
|
||||
<title>starting</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="548.712" cy="-106" rx="28.6835" ry="18"/>
|
||||
<text text-anchor="middle" x="548.712" y="-102.7" font-family="Times,serif" font-size="11.00" fill="#c0c0c0">starting</text>
|
||||
</g>
|
||||
<!-- error->starting -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>error->starting</title>
|
||||
<path fill="none" stroke="#000000" d="M185.7778,-186.305C191.8127,-185.5078 198.1957,-184.6962 204.1451,-184 220.6565,-182.0679 486.8945,-161.2162 501.8707,-154 512.9535,-148.6598 522.867,-139.5514 530.6885,-130.6948"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="533.5841,-132.6853 537.257,-122.7474 528.1884,-128.2258 533.5841,-132.6853"/>
|
||||
<text text-anchor="middle" x="360.1914" y="-174" font-family="Times,serif" font-size="10.00" fill="#000000">start</text>
|
||||
</g>
|
||||
<!-- processing->error -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>processing->error</title>
|
||||
<path fill="none" stroke="#000000" d="M764.8941,-303.4514C738.3628,-310.531 699.3647,-319 664.6261,-319 289.8062,-319 289.8062,-319 289.8062,-319 250.2686,-319 233.8818,-321.0567 204.1451,-295 181.4063,-275.0751 169.7917,-241.6479 164.1159,-217.864"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="167.5185,-217.0393 161.9695,-208.0138 160.6789,-218.5297 167.5185,-217.0393"/>
|
||||
<text text-anchor="middle" x="486.5948" y="-321" font-family="Times,serif" font-size="10.00" fill="#ff0000">error</text>
|
||||
</g>
|
||||
<!-- processing->error -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>processing->error</title>
|
||||
<path fill="none" stroke="#000000" d="M760.2154,-290.0289C722.0298,-286.2059 660.2037,-281 606.6691,-281 289.8062,-281 289.8062,-281 289.8062,-281 249.4674,-281 236.5959,-274.9619 204.1451,-251 191.6594,-241.7805 181.1271,-228.1091 173.3984,-216.0446"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="176.2299,-213.9642 168.0487,-207.2323 170.2462,-217.5968 176.2299,-213.9642"/>
|
||||
<text text-anchor="middle" x="486.5948" y="-283" font-family="Times,serif" font-size="10.00" fill="#ff0000">timeout</text>
|
||||
</g>
|
||||
<!-- finished -->
|
||||
<g id="node6" class="node">
|
||||
<title>finished</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="423.3931" cy="-207" rx="29.8518" ry="18"/>
|
||||
<text text-anchor="middle" x="423.3931" y="-203.7" font-family="Times,serif" font-size="11.00" fill="#c0c0c0">finished</text>
|
||||
</g>
|
||||
<!-- processing->finished -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>processing->finished</title>
|
||||
<path fill="none" stroke="#000000" d="M762.8931,-286.1487C693.4478,-269.9587 534.7324,-232.9569 461.5916,-215.9053"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="461.9756,-212.4011 451.4421,-213.5391 460.3862,-219.2183 461.9756,-212.4011"/>
|
||||
<text text-anchor="middle" x="606.6691" y="-253" font-family="Times,serif" font-size="10.00" fill="#000000">finish</text>
|
||||
</g>
|
||||
<!-- reapplying->error -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>reapplying->error</title>
|
||||
<path fill="none" stroke="#000000" d="M252.5474,-205.7684C237.4566,-204.9118 219.906,-203.46 204.1451,-201 200.8871,-200.4915 197.5185,-199.8637 194.1582,-199.1679"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="194.7462,-195.7127 184.2212,-196.9277 193.2066,-202.5413 194.7462,-195.7127"/>
|
||||
<text text-anchor="middle" x="219.421" y="-206" font-family="Times,serif" font-size="10.00" fill="#ff0000">error</text>
|
||||
</g>
|
||||
<!-- reapplying->error -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>reapplying->error</title>
|
||||
<path fill="none" stroke="#000000" d="M260.1535,-196.0759C252.015,-193.5688 243.1124,-191.2685 234.6969,-190 222.3581,-188.1401 208.7564,-187.6625 196.5344,-187.7968"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="196.1254,-184.3057 186.2154,-188.0528 196.2992,-191.3036 196.1254,-184.3057"/>
|
||||
<text text-anchor="middle" x="219.421" y="-192" font-family="Times,serif" font-size="10.00" fill="#ff0000">timeout</text>
|
||||
</g>
|
||||
<!-- reapplying->reapplying -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>reapplying->reapplying</title>
|
||||
<path fill="none" stroke="#000000" d="M277.4022,-224.0373C274.8708,-233.8579 279.0054,-243 289.8062,-243 296.5567,-243 300.7033,-239.4289 302.2458,-234.3529"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="305.7448,-234.0251 302.2103,-224.0373 298.7449,-234.0494 305.7448,-234.0251"/>
|
||||
<text text-anchor="middle" x="289.8062" y="-245" font-family="Times,serif" font-size="10.00" fill="#000000">reapply</text>
|
||||
</g>
|
||||
<!-- reapplying->finished -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>reapplying->finished</title>
|
||||
<path fill="none" stroke="#000000" d="M325.6382,-201.6062C340.9792,-199.936 359.097,-198.8108 375.4673,-200 378.3764,-200.2113 381.3771,-200.4939 384.3911,-200.8245"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="384.2187,-204.3302 394.5761,-202.1002 385.0886,-197.3845 384.2187,-204.3302"/>
|
||||
<text text-anchor="middle" x="360.1914" y="-202" font-family="Times,serif" font-size="10.00" fill="#000000">finish</text>
|
||||
</g>
|
||||
<!-- starting->error -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>starting->error</title>
|
||||
<path fill="none" stroke="#000000" d="M520.9066,-111.2751C462.18,-122.5338 321.2756,-150.1669 204.1451,-178 200.9199,-178.7664 197.5735,-179.5951 194.2274,-180.4465"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="193.1069,-177.1215 184.312,-183.0292 194.8713,-183.8955 193.1069,-177.1215"/>
|
||||
<text text-anchor="middle" x="360.1914" y="-149" font-family="Times,serif" font-size="10.00" fill="#ff0000">error</text>
|
||||
</g>
|
||||
<!-- starting->error -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>starting->error</title>
|
||||
<path fill="none" stroke="#000000" d="M519.8505,-106.9285C466.0809,-109.3456 347.44,-117.9303 252.6969,-148 231.0771,-154.8617 207.8053,-165.2964 189.8809,-174.048"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="187.9134,-171.1174 180.5174,-178.7035 191.0299,-177.3854 187.9134,-171.1174"/>
|
||||
<text text-anchor="middle" x="360.1914" y="-128" font-family="Times,serif" font-size="10.00" fill="#ff0000">timeout</text>
|
||||
</g>
|
||||
<!-- starting->starting -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>starting->starting</title>
|
||||
<path fill="none" stroke="#000000" d="M538.7888,-123.0373C536.7637,-132.8579 540.0714,-142 548.712,-142 554.1124,-142 557.4296,-138.4289 558.6637,-133.3529"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="562.1629,-133.0276 558.6352,-123.0373 555.163,-133.047 562.1629,-133.0276"/>
|
||||
<text text-anchor="middle" x="548.712" y="-144" font-family="Times,serif" font-size="10.00" fill="#000000">start</text>
|
||||
</g>
|
||||
<!-- waiting -->
|
||||
<g id="node7" class="node">
|
||||
<title>waiting</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="664.6261" cy="-58" rx="28.6835" ry="18"/>
|
||||
<text text-anchor="middle" x="664.6261" y="-54.7" font-family="Times,serif" font-size="11.00" fill="#c0c0c0">waiting</text>
|
||||
</g>
|
||||
<!-- starting->waiting -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>starting->waiting</title>
|
||||
<path fill="none" stroke="#000000" d="M573.0114,-95.9376C589.8579,-88.9615 612.5106,-79.581 631.0755,-71.8933"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="632.4586,-75.1088 640.3587,-68.0491 629.7804,-68.6414 632.4586,-75.1088"/>
|
||||
<text text-anchor="middle" x="606.6691" y="-89" font-family="Times,serif" font-size="10.00" fill="#000000">wait</text>
|
||||
</g>
|
||||
<!-- finished->reapplying -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>finished->reapplying</title>
|
||||
<path fill="none" stroke="#000000" d="M393.7612,-209.8611C387.7142,-210.3357 381.3902,-210.7533 375.4673,-211 362.9165,-211.5229 349.3336,-211.2665 336.7624,-210.6879"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="336.6587,-207.1773 326.4853,-210.136 336.2832,-214.1673 336.6587,-207.1773"/>
|
||||
<text text-anchor="middle" x="360.1914" y="-213" font-family="Times,serif" font-size="10.00" fill="#000000">reapply</text>
|
||||
</g>
|
||||
<!-- finished->starting -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>finished->starting</title>
|
||||
<path fill="none" stroke="#000000" d="M441.3483,-192.5292C462.6588,-175.3541 498.3641,-146.5776 522.6991,-126.965"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="525.2004,-129.4443 530.7902,-120.444 520.8078,-123.994 525.2004,-129.4443"/>
|
||||
<text text-anchor="middle" x="486.5948" y="-168" font-family="Times,serif" font-size="10.00" fill="#000000">start</text>
|
||||
</g>
|
||||
<!-- finished->finished -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>finished->finished</title>
|
||||
<path fill="none" stroke="#000000" d="M412.4067,-224.0373C410.1646,-233.8579 413.8267,-243 423.3931,-243 429.3721,-243 433.0448,-239.4289 434.4111,-234.3529"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="437.9102,-234.0265 434.3795,-224.0373 430.9102,-234.048 437.9102,-234.0265"/>
|
||||
<text text-anchor="middle" x="423.3931" y="-245" font-family="Times,serif" font-size="10.00" fill="#000000">finish</text>
|
||||
</g>
|
||||
<!-- waiting->error -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>waiting->error</title>
|
||||
<path fill="none" stroke="#000000" d="M635.8244,-59.3578C567.489,-63.3 390.9294,-77.7349 252.6969,-126 229.7823,-134.0008 224.248,-137.3998 204.1451,-151 196.6367,-156.0797 188.9922,-162.2552 182.186,-168.1799"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="179.7533,-165.6601 174.6393,-174.939 184.4235,-170.8745 179.7533,-165.6601"/>
|
||||
<text text-anchor="middle" x="423.3931" y="-91" font-family="Times,serif" font-size="10.00" fill="#ff0000">abort</text>
|
||||
</g>
|
||||
<!-- waiting->error -->
|
||||
<g id="edge25" class="edge">
|
||||
<title>waiting->error</title>
|
||||
<path fill="none" stroke="#000000" d="M647.5018,-43.0636C625.9836,-25.8786 587.1915,0 548.712,0 289.8062,0 289.8062,0 289.8062,0 212.0914,0 177.1061,-109.2549 164.7343,-162.0677"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="161.252,-161.6033 162.4928,-172.1253 168.0844,-163.1262 161.252,-161.6033"/>
|
||||
<text text-anchor="middle" x="423.3931" y="-2" font-family="Times,serif" font-size="10.00" fill="#ff0000">timeout</text>
|
||||
</g>
|
||||
<!-- waiting->processing -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>waiting->processing</title>
|
||||
<path fill="none" stroke="#000000" d="M674.3283,-75.3536C697.2037,-116.2695 754.6279,-218.9806 781.8345,-267.6433"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="778.9188,-269.6004 786.8538,-276.6209 785.0287,-266.1844 778.9188,-269.6004"/>
|
||||
<text text-anchor="middle" x="726.4625" y="-192" font-family="Times,serif" font-size="10.00" fill="#000000">process</text>
|
||||
</g>
|
||||
<!-- waiting->starting -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>waiting->starting</title>
|
||||
<path fill="none" stroke="#000000" d="M635.7298,-56.4637C622.955,-56.7905 608.0343,-58.5932 595.5533,-64 585.985,-68.1451 577.0389,-75.0396 569.5816,-82.0564"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="566.6659,-80.0265 562.1149,-89.5942 571.639,-84.9528 566.6659,-80.0265"/>
|
||||
<text text-anchor="middle" x="606.6691" y="-66" font-family="Times,serif" font-size="10.00" fill="#000000">start</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 15 KiB |
@ -1,26 +0,0 @@
|
||||
.. include:: ../../README.rst
|
||||
|
||||
Using Ironic Inspector
|
||||
======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
install/index
|
||||
user/index
|
||||
admin/index
|
||||
|
||||
Contributor Docs
|
||||
================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
contributor/index
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
@ -1,361 +0,0 @@
|
||||
Install Guide
|
||||
=============
|
||||
|
||||
Install from PyPI_ (you may want to use virtualenv to isolate your
|
||||
environment)::
|
||||
|
||||
pip install ironic-inspector
|
||||
|
||||
Also there is a `DevStack <http://docs.openstack.org/developer/devstack/>`_
|
||||
plugin for **ironic-inspector** - see :ref:`contributing_link` for the
|
||||
current status.
|
||||
|
||||
Finally, some distributions (e.g. Fedora) provide **ironic-inspector**
|
||||
packaged, some of them - under its old name *ironic-discoverd*.
|
||||
|
||||
There are several projects you can use to set up **ironic-inspector** in
|
||||
production. `puppet-ironic
|
||||
<http://git.openstack.org/cgit/openstack/puppet-ironic/>`_ provides Puppet
|
||||
manifests, while `bifrost <http://docs.openstack.org/developer/bifrost/>`_
|
||||
provides an Ansible-based standalone installer. Refer to Configuration_
|
||||
if you plan on installing **ironic-inspector** manually.
|
||||
|
||||
.. _PyPI: https://pypi.python.org/pypi/ironic-inspector
|
||||
|
||||
.. note::
|
||||
Please beware of :ref:`possible DNS issues <ubuntu-dns>` when installing
|
||||
**ironic-inspector** on Ubuntu.
|
||||
|
||||
Version Support Matrix
|
||||
----------------------
|
||||
|
||||
**ironic-inspector** currently requires the Bare Metal API version
|
||||
``1.11`` to be provided by **ironic**. This version is available starting
|
||||
with the Liberty release of **ironic**.
|
||||
|
||||
Here is a mapping between the ironic versions and the supported
|
||||
ironic-inspector versions. The Standalone column shows which
|
||||
ironic-inspector versions can be used in standalone mode with each
|
||||
ironic version. The Inspection Interface column shows which
|
||||
ironic-inspector versions can be used with the inspection interface in
|
||||
each version of **ironic**.
|
||||
|
||||
============== ============ ====================
|
||||
Ironic Version Standalone Inspection Interface
|
||||
============== ============ ====================
|
||||
Juno 1.0 N/A
|
||||
Kilo 1.0 - 2.2 1.0 - 1.1
|
||||
Liberty 1.1 - 2.2.7 2.0 - 2.2.7
|
||||
Mitaka 2.3 - 3.X 2.3 - 3.X
|
||||
Newton 3.3 - 4.X 3.3 - 4.X
|
||||
Ocata+ 5.0 - 5.X 5.0 - 5.X
|
||||
============== ============ ====================
|
||||
|
||||
.. note::
|
||||
``3.X`` means there are no specific plans to deprecate support for this
|
||||
ironic version. This does not imply that it will be supported forever.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Copy ``example.conf`` to some permanent place
|
||||
(e.g. ``/etc/ironic-inspector/inspector.conf``).
|
||||
Fill in these minimum configuration values:
|
||||
|
||||
* The ``keystone_authtoken`` section - credentials to use when checking user
|
||||
authentication.
|
||||
|
||||
* The ``ironic`` section - credentials to use when accessing **ironic**
|
||||
API.
|
||||
|
||||
* ``connection`` in the ``database`` section - SQLAlchemy connection string
|
||||
for the database.
|
||||
|
||||
* ``dnsmasq_interface`` in the ``firewall`` section - interface on which
|
||||
``dnsmasq`` (or another DHCP service) listens for PXE boot requests
|
||||
(defaults to ``br-ctlplane`` which is a sane default for **tripleo**-based
|
||||
installations but is unlikely to work for other cases).
|
||||
|
||||
See comments inside `example.conf
|
||||
<https://github.com/openstack/ironic-inspector/blob/master/example.conf>`_
|
||||
for other possible configuration options.
|
||||
|
||||
.. note::
|
||||
Configuration file contains a password and thus should be owned by ``root``
|
||||
and should have access rights like ``0600``.
|
||||
|
||||
Here is an example *inspector.conf* (adapted from a gate run)::
|
||||
|
||||
[DEFAULT]
|
||||
debug = false
|
||||
rootwrap_config = /etc/ironic-inspector/rootwrap.conf
|
||||
|
||||
[database]
|
||||
connection = mysql+pymysql://root:<PASSWORD>@127.0.0.1/ironic_inspector?charset=utf8
|
||||
|
||||
[firewall]
|
||||
dnsmasq_interface = br-ctlplane
|
||||
|
||||
[ironic]
|
||||
os_region = RegionOne
|
||||
project_name = service
|
||||
password = <PASSWORD>
|
||||
username = ironic-inspector
|
||||
auth_url = http://127.0.0.1/identity
|
||||
auth_type = password
|
||||
|
||||
[keystone_authtoken]
|
||||
auth_uri = http://127.0.0.1/identity
|
||||
project_name = service
|
||||
password = <PASSWORD>
|
||||
username = ironic-inspector
|
||||
auth_url = http://127.0.0.1/identity_v2_admin
|
||||
auth_type = password
|
||||
|
||||
[processing]
|
||||
ramdisk_logs_dir = /var/log/ironic-inspector/ramdisk
|
||||
store_data = swift
|
||||
|
||||
[swift]
|
||||
os_region = RegionOne
|
||||
project_name = service
|
||||
password = <PASSWORD>
|
||||
username = ironic-inspector
|
||||
auth_url = http://127.0.0.1/identity
|
||||
auth_type = password
|
||||
|
||||
.. note::
|
||||
Set ``debug = true`` if you want to see complete logs.
|
||||
|
||||
**ironic-inspector** requires root rights for managing ``iptables``. It
|
||||
gets them by running ``ironic-inspector-rootwrap`` utility with ``sudo``.
|
||||
To allow it, copy file ``rootwrap.conf`` and directory ``rootwrap.d`` to the
|
||||
configuration directory (e.g. ``/etc/ironic-inspector/``) and create file
|
||||
``/etc/sudoers.d/ironic-inspector-rootwrap`` with the following content::
|
||||
|
||||
Defaults:stack !requiretty
|
||||
stack ALL=(root) NOPASSWD: /usr/bin/ironic-inspector-rootwrap /etc/ironic-inspector/rootwrap.conf *
|
||||
|
||||
.. DANGER::
|
||||
Be very careful about typos in ``/etc/sudoers.d/ironic-inspector-rootwrap``
|
||||
as any typo will break sudo for **ALL** users on the system. Especially,
|
||||
make sure there is a new line at the end of this file.
|
||||
|
||||
.. note::
|
||||
``rootwrap.conf`` and all files in ``rootwrap.d`` must be writeable
|
||||
only by root.
|
||||
|
||||
.. note::
|
||||
If you store ``rootwrap.d`` in a different location, make sure to update
|
||||
the *filters_path* option in ``rootwrap.conf`` to reflect the change.
|
||||
|
||||
If your ``rootwrap.conf`` is in a different location, then you need
|
||||
to update the *rootwrap_config* option in ``ironic-inspector.conf``
|
||||
to point to that location.
|
||||
|
||||
Replace ``stack`` with whatever user you'll be using to run
|
||||
**ironic-inspector**.
|
||||
|
||||
Configuring IPA
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
ironic-python-agent_ is a ramdisk developed for **ironic** and support
|
||||
for **ironic-inspector** was added during the Liberty cycle. This is the
|
||||
default ramdisk starting with the Mitaka release.
|
||||
|
||||
.. note::
|
||||
You need at least 1.5 GiB of RAM on the machines to use IPA built with
|
||||
diskimage-builder_ and at least 384 MiB to use the *TinyIPA*.
|
||||
|
||||
To build an **ironic-python-agent** ramdisk, do the following:
|
||||
|
||||
* Get the new enough version of diskimage-builder_::
|
||||
|
||||
sudo pip install -U "diskimage-builder>=1.1.2"
|
||||
|
||||
* Build the ramdisk::
|
||||
|
||||
disk-image-create ironic-agent fedora -o ironic-agent
|
||||
|
||||
.. note::
|
||||
Replace "fedora" with your distribution of choice.
|
||||
|
||||
* Use the resulting files ``ironic-agent.kernel`` and
|
||||
``ironic-agent.initramfs`` in the following instructions to set PXE or iPXE.
|
||||
|
||||
Alternatively, you can download a `prebuilt TinyIPA image
|
||||
<http://tarballs.openstack.org/ironic-python-agent/tinyipa/files/>`_ or use
|
||||
the `other builders
|
||||
<http://docs.openstack.org/developer/ironic-python-agent/#image-builders>`_.
|
||||
|
||||
.. _diskimage-builder: https://docs.openstack.org/developer/diskimage-builder/
|
||||
.. _ironic-python-agent: https://docs.openstack.org/developer/ironic-python-agent/
|
||||
|
||||
Configuring PXE
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
For the PXE boot environment, you'll need:
|
||||
|
||||
* TFTP server running and accessible (see below for using *dnsmasq*).
|
||||
Ensure ``pxelinux.0`` is present in the TFTP root.
|
||||
|
||||
Copy ``ironic-agent.kernel`` and ``ironic-agent.initramfs`` to the TFTP
|
||||
root as well.
|
||||
|
||||
* Next, setup ``$TFTPROOT/pxelinux.cfg/default`` as follows::
|
||||
|
||||
default introspect
|
||||
|
||||
label introspect
|
||||
kernel ironic-agent.kernel
|
||||
append initrd=ironic-agent.initramfs ipa-inspection-callback-url=http://{IP}:5050/v1/continue systemd.journald.forward_to_console=yes
|
||||
|
||||
ipappend 3
|
||||
|
||||
Replace ``{IP}`` with IP of the machine (do not use loopback interface, it
|
||||
will be accessed by ramdisk on a booting machine).
|
||||
|
||||
.. note::
|
||||
While ``systemd.journald.forward_to_console=yes`` is not actually
|
||||
required, it will substantially simplify debugging if something
|
||||
goes wrong. You can also enable IPA debug logging by appending
|
||||
``ipa-debug=1``.
|
||||
|
||||
IPA is pluggable: you can insert introspection plugins called
|
||||
*collectors* into it. For example, to enable a very handy ``logs`` collector
|
||||
(sending ramdisk logs to **ironic-inspector**), modify the ``append``
|
||||
line in ``$TFTPROOT/pxelinux.cfg/default``::
|
||||
|
||||
append initrd=ironic-agent.initramfs ipa-inspection-callback-url=http://{IP}:5050/v1/continue ipa-inspection-collectors=default,logs systemd.journald.forward_to_console=yes
|
||||
|
||||
.. note::
|
||||
You probably want to always keep the ``default`` collector, as it provides
|
||||
the basic information required for introspection.
|
||||
|
||||
* You need PXE boot server (e.g. *dnsmasq*) running on **the same** machine as
|
||||
**ironic-inspector**. Don't do any firewall configuration:
|
||||
**ironic-inspector** will handle it for you. In **ironic-inspector**
|
||||
configuration file set ``dnsmasq_interface`` to the interface your
|
||||
PXE boot server listens on. Here is an example *dnsmasq.conf*::
|
||||
|
||||
port=0
|
||||
interface={INTERFACE}
|
||||
bind-interfaces
|
||||
dhcp-range={DHCP IP RANGE, e.g. 192.168.0.50,192.168.0.150}
|
||||
enable-tftp
|
||||
tftp-root={TFTP ROOT, e.g. /tftpboot}
|
||||
dhcp-boot=pxelinux.0
|
||||
dhcp-sequential-ip
|
||||
|
||||
.. note::
|
||||
``dhcp-sequential-ip`` is used because otherwise a lot of nodes booting
|
||||
simultaneously cause conflicts - the same IP address is suggested to
|
||||
several nodes.
|
||||
|
||||
Configuring iPXE
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
iPXE allows better scaling as it primarily uses the HTTP protocol instead of
|
||||
slow and unreliable TFTP. You still need a TFTP server as a fallback for
|
||||
nodes not supporting iPXE. To use iPXE, you'll need:
|
||||
|
||||
* TFTP server running and accessible (see above for using *dnsmasq*).
|
||||
Ensure ``undionly.kpxe`` is present in the TFTP root. If any of your nodes
|
||||
boot with UEFI, you'll also need ``ipxe.efi`` there.
|
||||
|
||||
* You also need an HTTP server capable of serving static files.
|
||||
Copy ``ironic-agent.kernel`` and ``ironic-agent.initramfs`` there.
|
||||
|
||||
* Create a file called ``inspector.ipxe`` in the HTTP root (you can name and
|
||||
place it differently, just don't forget to adjust the *dnsmasq.conf* example
|
||||
below)::
|
||||
|
||||
#!ipxe
|
||||
|
||||
:retry_dhcp
|
||||
dhcp || goto retry_dhcp
|
||||
|
||||
:retry_boot
|
||||
imgfree
|
||||
kernel --timeout 30000 http://{IP}:8088/ironic-agent.kernel ipa-inspection-callback-url=http://{IP}>:5050/v1/continue systemd.journald.forward_to_console=yes BOOTIF=${mac} initrd=agent.ramdisk || goto retry_boot
|
||||
initrd --timeout 30000 http://{IP}:8088/ironic-agent.ramdisk || goto retry_boot
|
||||
boot
|
||||
|
||||
.. note::
|
||||
Older versions of the iPXE ROM tend to misbehave on unreliable network
|
||||
connection, thus we use the timeout option with retries.
|
||||
|
||||
Just like with PXE, you can customize the list of collectors by appending
|
||||
the ``ipa-inspector-collectors`` kernel option. For example::
|
||||
|
||||
ipa-inspection-collectors=default,logs,extra_hardware
|
||||
|
||||
* Just as with PXE, you'll need a PXE boot server. The configuration, however,
|
||||
will be different. Here is an example *dnsmasq.conf*::
|
||||
|
||||
port=0
|
||||
interface={INTERFACE}
|
||||
bind-interfaces
|
||||
dhcp-range={DHCP IP RANGE, e.g. 192.168.0.50,192.168.0.150}
|
||||
enable-tftp
|
||||
tftp-root={TFTP ROOT, e.g. /tftpboot}
|
||||
dhcp-sequential-ip
|
||||
dhcp-match=ipxe,175
|
||||
dhcp-match=set:efi,option:client-arch,7
|
||||
dhcp-boot=tag:ipxe,http://{IP}:8088/inspector.ipxe
|
||||
dhcp-boot=tag:efi,ipxe.efi
|
||||
dhcp-boot=undionly.kpxe,localhost.localdomain,{IP}
|
||||
|
||||
First, we configure the same common parameters as with PXE. Then we define
|
||||
``ipxe`` and ``efi`` tags. Nodes already supporting iPXE are ordered to
|
||||
download and execute ``inspector.ipxe``. Nodes without iPXE booted with UEFI
|
||||
will get ``ipxe.efi`` firmware to execute, while the remaining will get
|
||||
``undionly.kpxe``.
|
||||
|
||||
Managing the **ironic-inspector** Database
|
||||
------------------------------------------
|
||||
|
||||
**ironic-inspector** provides a command line client for managing its
|
||||
database. This client can be used for upgrading, and downgrading the database
|
||||
using `alembic <https://alembic.readthedocs.org/>`_ migrations.
|
||||
|
||||
If this is your first time running **ironic-inspector** to migrate the
|
||||
database, simply run:
|
||||
::
|
||||
|
||||
ironic-inspector-dbsync --config-file /etc/ironic-inspector/inspector.conf upgrade
|
||||
|
||||
If you have previously run a version of **ironic-inspector** earlier than
|
||||
2.2.0, the safest thing is to delete the existing SQLite database and run
|
||||
``upgrade`` as shown above. However, if you want to save the existing
|
||||
database, to ensure your database will work with the migrations, you'll need to
|
||||
run an extra step before upgrading the database. You only need to do this the
|
||||
first time running version 2.2.0 or later.
|
||||
|
||||
If you are upgrading from **ironic-inspector** version 2.1.0 or lower:
|
||||
::
|
||||
|
||||
ironic-inspector-dbsync --config-file /etc/ironic-inspector/inspector.conf stamp --revision 578f84f38d
|
||||
ironic-inspector-dbsync --config-file /etc/ironic-inspector/inspector.conf upgrade
|
||||
|
||||
If you are upgrading from a git master install of the **ironic-inspector**
|
||||
after :ref:`rules <introspection_rules>` were introduced:
|
||||
::
|
||||
|
||||
ironic-inspector-dbsync --config-file /etc/ironic-inspector/inspector.conf stamp --revision d588418040d
|
||||
ironic-inspector-dbsync --config-file /etc/ironic-inspector/inspector.conf upgrade
|
||||
|
||||
Other available commands can be discovered by running::
|
||||
|
||||
ironic-inspector-dbsync --help
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
::
|
||||
|
||||
ironic-inspector --config-file /etc/ironic-inspector/inspector.conf
|
||||
|
||||
A good starting point for writing your own *systemd* unit should be `one used
|
||||
in Fedora <http://pkgs.fedoraproject.org/cgit/openstack-ironic-discoverd.git/plain/openstack-ironic-discoverd.service>`_
|
||||
(note usage of old name).
|
@ -1,392 +0,0 @@
|
||||
HTTP API
|
||||
--------
|
||||
|
||||
.. _http_api:
|
||||
|
||||
By default **ironic-inspector** listens on ``0.0.0.0:5050``, port
|
||||
can be changed in configuration. Protocol is JSON over HTTP.
|
||||
|
||||
Start Introspection
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``POST /v1/introspection/<Node ID>`` initiate hardware introspection for node
|
||||
``<Node ID>``. All power management configuration for this node needs to be
|
||||
done prior to calling the endpoint.
|
||||
|
||||
Requires X-Auth-Token header with Keystone token for authentication.
|
||||
|
||||
Response:
|
||||
|
||||
* 202 - accepted introspection request
|
||||
* 400 - bad request
|
||||
* 401, 403 - missing or invalid authentication
|
||||
* 404 - node cannot be found
|
||||
|
||||
Get Introspection Status
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``GET /v1/introspection/<Node ID>`` get hardware introspection status.
|
||||
|
||||
Requires X-Auth-Token header with Keystone token for authentication.
|
||||
|
||||
Response:
|
||||
|
||||
* 200 - OK
|
||||
* 400 - bad request
|
||||
* 401, 403 - missing or invalid authentication
|
||||
* 404 - node cannot be found
|
||||
|
||||
Response body: JSON dictionary with keys:
|
||||
|
||||
* ``finished`` (boolean) whether introspection is finished
|
||||
(``true`` on introspection completion or if it ends because of an error)
|
||||
* ``state`` state of the introspection
|
||||
* ``error`` error string or ``null``; ``Canceled by operator`` in
|
||||
case introspection was aborted
|
||||
* ``uuid`` node UUID
|
||||
* ``started_at`` a UTC ISO8601 timestamp
|
||||
* ``finished_at`` a UTC ISO8601 timestamp or ``null``
|
||||
* ``links`` containing a self URL
|
||||
|
||||
Get All Introspection Statuses
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``GET /v1/introspection`` get all hardware introspection statuses.
|
||||
|
||||
Requires X-Auth-Token header with Keystone token for authentication.
|
||||
|
||||
Returned status list is sorted by the ``started_at, uuid`` attribute pair,
|
||||
newer items first, and is paginated with these query string fields:
|
||||
|
||||
* ``marker`` the UUID of the last node returned previously
|
||||
* ``limit`` default, max: ``CONF.api_max_limit``
|
||||
|
||||
Response:
|
||||
|
||||
* 200 - OK
|
||||
* 400 - bad request
|
||||
* 401, 403 - missing or invalid authentication
|
||||
|
||||
Response body: a JSON object containing a list of status objects::
|
||||
|
||||
{
|
||||
'introspection': [
|
||||
{
|
||||
'finished': false,
|
||||
'state': 'waiting',
|
||||
'error': null,
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
Each status object contains these keys:
|
||||
|
||||
* ``finished`` (boolean) whether introspection is finished
|
||||
(``true`` on introspection completion or if it ends because of an error)
|
||||
* ``state`` state of the introspection
|
||||
* ``error`` error string or ``null``; ``Canceled by operator`` in
|
||||
case introspection was aborted
|
||||
* ``uuid`` node UUID
|
||||
* ``started_at`` an UTC ISO8601 timestamp
|
||||
* ``finished_at`` an UTC ISO8601 timestamp or ``null``
|
||||
|
||||
|
||||
Abort Running Introspection
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``POST /v1/introspection/<Node ID>/abort`` abort running introspection.
|
||||
|
||||
Requires X-Auth-Token header with Keystone token for authentication.
|
||||
|
||||
Response:
|
||||
|
||||
* 202 - accepted
|
||||
* 400 - bad request
|
||||
* 401, 403 - missing or invalid authentication
|
||||
* 404 - node cannot be found
|
||||
* 409 - inspector has locked this node for processing
|
||||
|
||||
|
||||
Get Introspection Data
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``GET /v1/introspection/<Node ID>/data`` get stored data from successful
|
||||
introspection.
|
||||
|
||||
Requires X-Auth-Token header with Keystone token for authentication.
|
||||
|
||||
Response:
|
||||
|
||||
* 200 - OK
|
||||
* 400 - bad request
|
||||
* 401, 403 - missing or invalid authentication
|
||||
* 404 - data cannot be found or data storage not configured
|
||||
|
||||
Response body: JSON dictionary with introspection data
|
||||
|
||||
.. note::
|
||||
We do not provide any backward compatibility guarantees regarding the
|
||||
format and contents of the stored data. Notably, it depends on the ramdisk
|
||||
used and plugins enabled both in the ramdisk and in inspector itself.
|
||||
|
||||
Reapply introspection on stored data
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``POST /v1/introspection/<Node ID>/data/unprocessed`` to trigger
|
||||
introspection on stored unprocessed data. No data is allowed to be
|
||||
sent along with the request.
|
||||
|
||||
Requires X-Auth-Token header with Keystone token for authentication.
|
||||
Requires enabling Swift store in processing section of the
|
||||
configuration file.
|
||||
|
||||
Response:
|
||||
|
||||
* 202 - accepted
|
||||
* 400 - bad request or store not configured
|
||||
* 401, 403 - missing or invalid authentication
|
||||
* 404 - node not found for Node ID
|
||||
* 409 - inspector locked node for processing
|
||||
|
||||
Introspection Rules
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
See :ref:`rules <introspection_rules>` for details.
|
||||
|
||||
All these API endpoints require X-Auth-Token header with Keystone token for
|
||||
authentication.
|
||||
|
||||
* ``POST /v1/rules`` create a new introspection rule.
|
||||
|
||||
Request body: JSON dictionary with keys:
|
||||
|
||||
* ``conditions`` rule conditions, see :ref:`rules <introspection_rules>`
|
||||
* ``actions`` rule actions, see :ref:`rules <introspection_rules>`
|
||||
* ``description`` (optional) human-readable description
|
||||
* ``uuid`` (optional) rule UUID, autogenerated if missing
|
||||
|
||||
Response
|
||||
|
||||
* 200 - OK for API version < 1.6
|
||||
* 201 - OK for API version 1.6 and higher
|
||||
* 400 - bad request
|
||||
|
||||
Response body: JSON dictionary with introspection rule representation (the
|
||||
same as above with UUID filled in).
|
||||
|
||||
* ``GET /v1/rules`` list all introspection rules.
|
||||
|
||||
Response
|
||||
|
||||
* 200 - OK
|
||||
|
||||
Response body: JSON dictionary with key ``rules`` - list of short rule
|
||||
representations. Short rule representation is a JSON dictionary with keys:
|
||||
|
||||
* ``uuid`` rule UUID
|
||||
* ``description`` human-readable description
|
||||
* ``links`` list of HTTP links, use one with ``rel=self`` to get the full
|
||||
rule details
|
||||
|
||||
* ``DELETE /v1/rules`` delete all introspection rules.
|
||||
|
||||
Response
|
||||
|
||||
* 204 - OK
|
||||
|
||||
* ``GET /v1/rules/<UUID>`` get one introspection rule by its ``<UUID>``.
|
||||
|
||||
Response
|
||||
|
||||
* 200 - OK
|
||||
* 404 - not found
|
||||
|
||||
Response body: JSON dictionary with introspection rule representation
|
||||
(see ``POST /v1/rules`` above).
|
||||
|
||||
* ``DELETE /v1/rules/<UUID>`` delete one introspection rule by its ``<UUID>``.
|
||||
|
||||
Response
|
||||
|
||||
* 204 - OK
|
||||
* 404 - not found
|
||||
|
||||
Ramdisk Callback
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. _ramdisk_callback:
|
||||
|
||||
``POST /v1/continue`` internal endpoint for the ramdisk to post back
|
||||
discovered data. Should not be used for anything other than implementing
|
||||
the ramdisk. Request body: JSON dictionary with at least these keys:
|
||||
|
||||
* ``inventory`` full `hardware inventory`_ from the ironic-python-agent with at
|
||||
least the following keys:
|
||||
|
||||
* ``memory`` memory information containing at least key ``physical_mb`` -
|
||||
physical memory size as reported by dmidecode,
|
||||
|
||||
* ``cpu`` CPU information containing at least keys ``count`` (CPU count) and
|
||||
``architecture`` (CPU architecture, e.g. ``x86_64``),
|
||||
|
||||
* ``bmc_address`` IP address of the node's BMC,
|
||||
|
||||
* ``interfaces`` list of dictionaries with the following keys:
|
||||
|
||||
* ``name`` interface name,
|
||||
|
||||
* ``ipv4_address`` IPv4 address of the interface,
|
||||
|
||||
* ``mac_address`` MAC (physical) address of the interface.
|
||||
|
||||
* ``client_id`` InfiniBand Client-ID, for Ethernet is None.
|
||||
|
||||
* ``disks`` list of disk block devices containing at least ``name`` and
|
||||
``size`` (in bytes) keys. In case ``disks`` are not provided
|
||||
**ironic-inspector** assumes that this is a disk-less node.
|
||||
|
||||
* ``root_disk`` default deployment root disk as calculated by the
|
||||
ironic-python-agent algorithm.
|
||||
|
||||
.. note::
|
||||
**ironic-inspector** default plugin ``root_disk_selection`` may change
|
||||
``root_disk`` based on root device hints if node specify hints via
|
||||
properties ``root_device`` key. See `Specifying the disk for deployment
|
||||
root device hints`_ for more details.
|
||||
|
||||
* ``boot_interface`` MAC address of the NIC that the machine PXE booted from
|
||||
either in standard format ``11:22:33:44:55:66`` or in *PXELinux* ``BOOTIF``
|
||||
format ``01-11-22-33-44-55-66``. Strictly speaking, this key is optional,
|
||||
but some features will now work as expected, if it is not provided.
|
||||
|
||||
Optionally the following keys might be provided:
|
||||
|
||||
* ``error`` error happened during ramdisk run, interpreted by
|
||||
``ramdisk_error`` plugin.
|
||||
|
||||
* ``logs`` base64-encoded logs from the ramdisk.
|
||||
|
||||
.. note::
|
||||
This list highly depends on enabled plugins, provided above are
|
||||
expected keys for the default set of plugins. See
|
||||
:ref:`plugins <introspection_plugins>` for details.
|
||||
|
||||
.. note::
|
||||
This endpoint is not expected to be versioned, though versioning will work
|
||||
on it.
|
||||
|
||||
Response:
|
||||
|
||||
* 200 - OK
|
||||
* 400 - bad request
|
||||
* 403 - node is not on introspection
|
||||
* 404 - node cannot be found or multiple nodes found
|
||||
|
||||
Response body: JSON dictionary with ``uuid`` key.
|
||||
|
||||
.. _hardware inventory: http://docs.openstack.org/developer/ironic-python-agent/#hardware-inventory
|
||||
.. _Specifying the disk for deployment root device hints:
|
||||
http://docs.openstack.org/project-install-guide/baremetal/draft/advanced.html#specifying-the-disk-for-deployment-root-device-hints
|
||||
|
||||
Error Response
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
If an error happens during request processing, **Ironic Inspector** returns
|
||||
a response with an appropriate HTTP code set, e.g. 400 for bad request or
|
||||
404 when something was not found (usually node in cache or node in ironic).
|
||||
The following JSON body is returned::
|
||||
|
||||
{
|
||||
"error": {
|
||||
"message": "Full error message"
|
||||
}
|
||||
}
|
||||
|
||||
This body may be extended in the future to include details that are more error
|
||||
specific.
|
||||
|
||||
API Versioning
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The API supports optional API versioning. You can query for minimum and
|
||||
maximum API version supported by the server. You can also declare required API
|
||||
version in your requests, so that the server rejects request of unsupported
|
||||
version.
|
||||
|
||||
.. note::
|
||||
Versioning was introduced in **Ironic Inspector 2.1.0**.
|
||||
|
||||
All versions must be supplied as string in form of ``X.Y``, where ``X`` is a
|
||||
major version and is always ``1`` for now, ``Y`` is a minor version.
|
||||
|
||||
* If ``X-OpenStack-Ironic-Inspector-API-Version`` header is sent with request,
|
||||
the server will check if it supports this version. HTTP error 406 will be
|
||||
returned for unsupported API version.
|
||||
|
||||
* All HTTP responses contain
|
||||
``X-OpenStack-Ironic-Inspector-API-Minimum-Version`` and
|
||||
``X-OpenStack-Ironic-Inspector-API-Maximum-Version`` headers with minimum
|
||||
and maximum API versions supported by the server.
|
||||
|
||||
.. note::
|
||||
Maximum is server API version used by default.
|
||||
|
||||
|
||||
API Discovery
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The API supports API discovery. You can query different parts of the API to
|
||||
discover what other endpoints are avaliable.
|
||||
|
||||
* ``GET /`` List API Versions
|
||||
|
||||
Response:
|
||||
|
||||
* 200 - OK
|
||||
|
||||
Response body: JSON dictionary containing a list of ``versions``, each
|
||||
version contains:
|
||||
|
||||
* ``status`` Either CURRENT or SUPPORTED
|
||||
* ``id`` The version identifier
|
||||
* ``links`` A list of links to this version endpoint containing:
|
||||
|
||||
* ``href`` The URL
|
||||
* ``rel`` The relationship between the version and the href
|
||||
|
||||
* ``GET /v1`` List API v1 resources
|
||||
|
||||
Response:
|
||||
|
||||
* 200 - OK
|
||||
|
||||
Response body: JSON dictionary containing a list of ``resources``, each
|
||||
resource contains:
|
||||
|
||||
* ``name`` The name of this resources
|
||||
* ``links`` A list of link to this resource containing:
|
||||
|
||||
* ``href`` The URL
|
||||
* ``rel`` The relationship between the resource and the href
|
||||
|
||||
Version History
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
* **1.0** version of API at the moment of introducing versioning.
|
||||
* **1.1** adds endpoint to retrieve stored introspection data.
|
||||
* **1.2** endpoints for manipulating introspection rules.
|
||||
* **1.3** endpoint for canceling running introspection
|
||||
* **1.4** endpoint for reapplying the introspection over stored data.
|
||||
* **1.5** support for Ironic node names.
|
||||
* **1.6** endpoint for rules creating returns 201 instead of 200 on success.
|
||||
* **1.7** UUID, started_at, finished_at in the introspection status API.
|
||||
* **1.8** support for listing all introspection statuses.
|
||||
* **1.9** de-activate setting IPMI credentials, if IPMI credentials
|
||||
are requested, API gets HTTP 400 response.
|
||||
* **1.10** adds node state to the GET /v1/introspection/<Node ID> and
|
||||
GET /v1/introspection API response data.
|
||||
* **1.11** adds invert&multiple fields into rules response data
|
||||
* **1.12** this version indicates that support for setting IPMI credentials
|
||||
was completely removed from API (all versions).
|
@ -1,34 +0,0 @@
|
||||
User Guide
|
||||
==========
|
||||
|
||||
How Ironic Inspector Works
|
||||
--------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
workflow
|
||||
|
||||
How to use Ironic Inspector
|
||||
---------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
usage
|
||||
|
||||
HTTP API Reference
|
||||
------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
http-api
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
troubleshooting
|
@ -1,149 +0,0 @@
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
Errors when starting introspection
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* *Invalid provision state "available"*
|
||||
|
||||
In Kilo release with *python-ironicclient* 0.5.0 or newer Ironic defaults to
|
||||
reporting provision state ``AVAILABLE`` for newly enrolled nodes.
|
||||
**ironic-inspector** will refuse to conduct introspection in this state, as
|
||||
such nodes are supposed to be used by Nova for scheduling. See :ref:`node
|
||||
states <node_states>` for instructions on how to put nodes into the correct
|
||||
state.
|
||||
|
||||
Introspection times out
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There may be 3 reasons why introspection can time out after some time
|
||||
(defaulting to 60 minutes, altered by ``timeout`` configuration option):
|
||||
|
||||
#. Fatal failure in processing chain before node was found in the local cache.
|
||||
See `Troubleshooting data processing`_ for the hints.
|
||||
|
||||
#. Failure to load the ramdisk on the target node. See `Troubleshooting
|
||||
PXE boot`_ for the hints.
|
||||
|
||||
#. Failure during ramdisk run. See `Troubleshooting ramdisk run`_ for the
|
||||
hints.
|
||||
|
||||
Troubleshooting data processing
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
In this case **ironic-inspector** logs should give a good idea what went wrong.
|
||||
E.g. for RDO or Fedora the following command will output the full log::
|
||||
|
||||
sudo journalctl -u openstack-ironic-inspector
|
||||
|
||||
(use ``openstack-ironic-discoverd`` for version < 2.0.0).
|
||||
|
||||
.. note::
|
||||
Service name and specific command might be different for other Linux
|
||||
distributions (and for old version of **ironic-inspector**).
|
||||
|
||||
If ``ramdisk_error`` plugin is enabled and ``ramdisk_logs_dir`` configuration
|
||||
option is set, **ironic-inspector** will store logs received from the ramdisk
|
||||
to the ``ramdisk_logs_dir`` directory. This depends, however, on the ramdisk
|
||||
implementation.
|
||||
|
||||
Troubleshooting PXE boot
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
PXE booting most often becomes a problem for bare metal environments with
|
||||
several physical networks. If the hardware vendor provides a remote console
|
||||
(e.g. iDRAC for DELL), use it to connect to the machine and see what is going
|
||||
on. You may need to restart introspection.
|
||||
|
||||
Another source of information is DHCP and TFTP server logs. Their location
|
||||
depends on how the servers were installed and run. For RDO or Fedora use::
|
||||
|
||||
$ sudo journalctl -u openstack-ironic-inspector-dnsmasq
|
||||
|
||||
(use ``openstack-ironic-discoverd-dnsmasq`` for version < 2.0.0).
|
||||
|
||||
The last resort is ``tcpdump`` utility. Use something like
|
||||
::
|
||||
|
||||
$ sudo tcpdump -i any port 67 or port 68 or port 69
|
||||
|
||||
to watch both DHCP and TFTP traffic going through your machine. Replace
|
||||
``any`` with a specific network interface to check that DHCP and TFTP
|
||||
requests really reach it.
|
||||
|
||||
If you see node not attempting PXE boot or attempting PXE boot on the wrong
|
||||
network, reboot the machine into BIOS settings and make sure that only one
|
||||
relevant NIC is allowed to PXE boot.
|
||||
|
||||
If you see node attempting PXE boot using the correct NIC but failing, make
|
||||
sure that:
|
||||
|
||||
#. network switches configuration does not prevent PXE boot requests from
|
||||
propagating,
|
||||
|
||||
#. there is no additional firewall rules preventing access to port 67 on the
|
||||
machine where *ironic-inspector* and its DHCP server are installed.
|
||||
|
||||
If you see node receiving DHCP address and then failing to get kernel and/or
|
||||
ramdisk or to boot them, make sure that:
|
||||
|
||||
#. TFTP server is running and accessible (use ``tftp`` utility to verify),
|
||||
|
||||
#. no firewall rules prevent access to TFTP port,
|
||||
|
||||
#. DHCP server is correctly set to point to the TFTP server,
|
||||
|
||||
#. ``pxelinux.cfg/default`` within TFTP root contains correct reference to the
|
||||
kernel and ramdisk.
|
||||
|
||||
.. note::
|
||||
If using iPXE instead of PXE, check the HTTP server logs and the iPXE
|
||||
configuration instead.
|
||||
|
||||
Troubleshooting ramdisk run
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
First, check if the ramdisk logs were stored locally as described in the
|
||||
`Troubleshooting data processing`_ section. If not, ensure that the ramdisk
|
||||
actually booted as described in the `Troubleshooting PXE boot`_ section.
|
||||
|
||||
Finally, you can try connecting to the IPA ramdisk. If you have any remote
|
||||
console access to the machine, you can check the logs as they appear on the
|
||||
screen. Otherwise, you can rebuild the IPA image with your SSH key to be able
|
||||
to log into it. Use the `dynamic-login`_ or `devuser`_ element for a DIB-based
|
||||
build or put an authorized_keys file in ``/usr/share/oem/`` for a CoreOS-based
|
||||
one.
|
||||
|
||||
.. _devuser: http://docs.openstack.org/developer/diskimage-builder/elements/devuser/README.html
|
||||
.. _dynamic-login: http://docs.openstack.org/developer/diskimage-builder/elements/dynamic-login/README.html
|
||||
|
||||
Troubleshooting DNS issues on Ubuntu
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. _ubuntu-dns:
|
||||
|
||||
Ubuntu uses local DNS caching, so tries localhost for DNS results first
|
||||
before calling out to an external DNS server. When DNSmasq is installed and
|
||||
configured for use with ironic-inspector, it can cause problems by interfering
|
||||
with the local DNS cache. To fix this issue ensure that ``/etc/resolve.conf``
|
||||
points to your external DNS servers and not to ``127.0.0.1``.
|
||||
|
||||
On Ubuntu 14.04 this can be done by editing your
|
||||
``/etc/resolvconf/resolv.conf.d/head`` and adding your nameservers there.
|
||||
This will ensure they will come up first when ``/etc/resolv.conf``
|
||||
is regenerated.
|
||||
|
||||
Running Inspector in a VirtualBox environment
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default VirtualBox does not expose a DMI table to the guest. This prevents
|
||||
ironic-inspector from being able to discover the properties of the a node. In
|
||||
order to run ironic-inspector on a VirtualBox guest the host must be configured
|
||||
to expose DMI data inside the guest. To do this run the following command on
|
||||
the VirtualBox host::
|
||||
|
||||
VBoxManage setextradata {NodeName} "VBoxInternal/Devices/pcbios/0/Config/DmiExposeMemoryTable" 1
|
||||
|
||||
.. note::
|
||||
Replace `{NodeName}` with the name of the guest you wish to expose the DMI
|
||||
table on. This command will need to be run once per host to enable this
|
||||
functionality.
|
@ -1,395 +0,0 @@
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. _usage_guide:
|
||||
|
||||
Refer to :ref:`api <http_api>` for information on the HTTP API.
|
||||
Refer to the `client documentation`_ for information on how to use CLI and
|
||||
Python library.
|
||||
|
||||
.. _client documentation: http://docs.openstack.org/developer/python-ironic-inspector-client
|
||||
|
||||
Using from Ironic API
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Ironic Kilo introduced support for hardware introspection under name of
|
||||
"inspection". **ironic-inspector** introspection is supported for some generic
|
||||
drivers, please refer to `Ironic inspection documentation`_ for details.
|
||||
|
||||
.. _Ironic inspection documentation: http://docs.openstack.org/developer/ironic/deploy/inspection.html
|
||||
|
||||
Node States
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. _node_states:
|
||||
|
||||
* The nodes should be moved to ``MANAGEABLE`` provision state before
|
||||
introspection (requires *python-ironicclient* of version 0.5.0 or newer)::
|
||||
|
||||
ironic node-set-provision-state <UUID> manage
|
||||
|
||||
* After successful introspection and before deploying nodes should be made
|
||||
available to Nova, by moving them to ``AVAILABLE`` state::
|
||||
|
||||
ironic node-set-provision-state <UUID> provide
|
||||
|
||||
.. note::
|
||||
Due to how Nova interacts with Ironic driver, you should wait 1 minute
|
||||
before Nova becomes aware of available nodes after issuing this command.
|
||||
Use ``nova hypervisor-stats`` command output to check it.
|
||||
|
||||
Introspection Rules
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. _introspection_rules:
|
||||
|
||||
Inspector supports a simple JSON-based DSL to define rules to run during
|
||||
introspection. Inspector provides an API to manage such rules, and will run
|
||||
them automatically after running all processing hooks.
|
||||
|
||||
A rule consists of conditions to check, and actions to run. If conditions
|
||||
evaluate to true on the introspection data, then actions are run on a node.
|
||||
|
||||
Available conditions and actions are defined by plugins, and can be extended,
|
||||
see :ref:`contributing_link` for details. See :ref:`api <http_api>` for
|
||||
specific calls to define introspection rules.
|
||||
|
||||
Conditions
|
||||
^^^^^^^^^^
|
||||
|
||||
A condition is represented by an object with fields:
|
||||
|
||||
``op`` the type of comparison operation, default available operators include:
|
||||
|
||||
* ``eq``, ``le``, ``ge``, ``ne``, ``lt``, ``gt`` - basic comparison operators;
|
||||
|
||||
* ``in-net`` - checks that an IP address is in a given network;
|
||||
|
||||
* ``matches`` - requires a full match against a given regular expression;
|
||||
|
||||
* ``contains`` - requires a value to contain a given regular expression;
|
||||
|
||||
* ``is-empty`` - checks that field is an empty string, list, dict or
|
||||
None value.
|
||||
|
||||
``field`` a `JSON path <http://goessner.net/articles/JsonPath/>`_ to the field
|
||||
in the introspection data to use in comparison.
|
||||
|
||||
Starting with the Mitaka release, you can also apply conditions to ironic node
|
||||
field. Prefix field with schema (``data://`` or ``node://``) to distinguish
|
||||
between values from introspection data and node. Both schemes use JSON path::
|
||||
|
||||
{"field": "node://property.path", "op": "eq", "value": "val"}
|
||||
{"field": "data://introspection.path", "op": "eq", "value": "val"}
|
||||
|
||||
if scheme (node or data) is missing, condition compares data with
|
||||
introspection data.
|
||||
|
||||
``invert`` boolean value, whether to invert the result of the comparison.
|
||||
|
||||
``multiple`` how to treat situations where the ``field`` query returns multiple
|
||||
results (e.g. the field contains a list), available options are:
|
||||
|
||||
* ``any`` (the default) require any to match,
|
||||
* ``all`` require all to match,
|
||||
* ``first`` requrie the first to match.
|
||||
|
||||
All other fields are passed to the condition plugin, e.g. numeric comparison
|
||||
operations require a ``value`` field to compare against.
|
||||
|
||||
Actions
|
||||
^^^^^^^
|
||||
|
||||
An action is represented by an object with fields:
|
||||
|
||||
``action`` type of action. Possible values are defined by plugins.
|
||||
|
||||
All other fields are passed to the action plugin.
|
||||
|
||||
Default available actions include:
|
||||
|
||||
* ``fail`` fail introspection. Requires a ``message`` parameter for the failure
|
||||
message.
|
||||
|
||||
* ``set-attribute`` sets an attribute on an Ironic node. Requires a ``path``
|
||||
field, which is the path to the attribute as used by ironic (e.g.
|
||||
``/properties/something``), and a ``value`` to set.
|
||||
|
||||
* ``set-capability`` sets a capability on an Ironic node. Requires ``name``
|
||||
and ``value`` fields, which are the name and the value for a new capability
|
||||
accordingly. Existing value for this same capability is replaced.
|
||||
|
||||
* ``extend-attribute`` the same as ``set-attribute``, but treats existing
|
||||
value as a list and appends value to it. If optional ``unique`` parameter is
|
||||
set to ``True``, nothing will be added if given value is already in a list.
|
||||
|
||||
Starting from Mitaka release, ``value`` field in actions supports fetching data
|
||||
from introspection, it's using `python string formatting notation
|
||||
<https://docs.python.org/2/library/string.html#formatspec>`_ ::
|
||||
|
||||
{"action": "set-attribute", "path": "/driver_info/ipmi_address",
|
||||
"value": "{data[inventory][bmc_address]}"}
|
||||
|
||||
Plugins
|
||||
~~~~~~~
|
||||
|
||||
.. _introspection_plugins:
|
||||
|
||||
**ironic-inspector** heavily relies on plugins for data processing. Even the
|
||||
standard functionality is largely based on plugins. Set ``processing_hooks``
|
||||
option in the configuration file to change the set of plugins to be run on
|
||||
introspection data. Note that order does matter in this option, especially
|
||||
for hooks that have dependencies on other hooks.
|
||||
|
||||
These are plugins that are enabled by default and should not be disabled,
|
||||
unless you understand what you're doing:
|
||||
|
||||
``scheduler``
|
||||
validates and updates basic hardware scheduling properties: CPU number and
|
||||
architecture, memory and disk size.
|
||||
|
||||
.. note::
|
||||
|
||||
Diskless nodes have the disk size property ``local_gb == 0``. Always use
|
||||
node driver ``root_device`` hints to prevent unexpected HW failures
|
||||
passing silently.
|
||||
|
||||
``validate_interfaces`` validates network interfaces information. Creates new
|
||||
ports, optionally deletes ports that were not present in the introspection
|
||||
data. Also sets the ``pxe_enabled`` flag for the PXE-booting port and
|
||||
unsets it for all the other ports to avoid **nova** picking a random port
|
||||
to boot the node.
|
||||
|
||||
The following plugins are enabled by default, but can be disabled if not
|
||||
needed:
|
||||
|
||||
``ramdisk_error``
|
||||
reports error, if ``error`` field is set by the ramdisk, also optionally
|
||||
stores logs from ``logs`` field, see :ref:`api <http_api>` for details.
|
||||
``capabilities``
|
||||
detect node capabilities: CPU, boot mode, etc. See `Capabilities
|
||||
Detection`_ for more details.
|
||||
``pci_devices``
|
||||
gathers the list of all PCI devices returned by the ramdisk and compares to
|
||||
those defined in ``alias`` field(s) from ``pci_devices`` section of
|
||||
configuration file. The recognized PCI devices and their count are then
|
||||
stored in node properties. This information can be later used in nova
|
||||
flavors for node scheduling.
|
||||
|
||||
Here are some plugins that can be additionally enabled:
|
||||
|
||||
``example``
|
||||
example plugin logging it's input and output.
|
||||
``raid_device``
|
||||
gathers block devices from ramdisk and exposes root device in multiple
|
||||
runs.
|
||||
``extra_hardware``
|
||||
stores the value of the 'data' key returned by the ramdisk as a JSON
|
||||
encoded string in a Swift object. The plugin will also attempt to convert
|
||||
the data into a format usable by introspection rules. If this is successful
|
||||
then the new format will be stored in the 'extra' key. The 'data' key is
|
||||
then deleted from the introspection data, as unless converted it's assumed
|
||||
unusable by introspection rules.
|
||||
``local_link_connection``
|
||||
Processes LLDP data returned from inspection specifically looking for the
|
||||
port ID and chassis ID, if found it configures the local link connection
|
||||
information on the nodes Ironic ports with that data. To enable LLDP in the
|
||||
inventory from IPA ``ipa-collect-lldp=1`` should be passed as a kernel
|
||||
parameter to the IPA ramdisk. In order to avoid processing the raw LLDP
|
||||
data twice, the ``lldp_basic`` plugin should also be installed and run
|
||||
prior to this plugin.
|
||||
``lldp_basic``
|
||||
Processes LLDP data returned from inspection and parses TLVs from the
|
||||
Basic Management (802.1AB), 802.1Q, and 802.3 sets and stores the
|
||||
processed data back to the Ironic inspector data in Swift.
|
||||
|
||||
Refer to :ref:`contributing_link` for information on how to write your
|
||||
own plugin.
|
||||
|
||||
Discovery
|
||||
~~~~~~~~~
|
||||
|
||||
Starting from Mitaka, **ironic-inspector** is able to register new nodes
|
||||
in Ironic.
|
||||
|
||||
The existing ``node-not-found-hook`` handles what happens if
|
||||
**ironic-inspector** receives inspection data from a node it can not identify.
|
||||
This can happen if a node is manually booted without registering it with
|
||||
Ironic first.
|
||||
|
||||
For discovery, the configuration file option ``node_not_found_hook`` should be
|
||||
set to load the hook called ``enroll``. This hook will enroll the unidentified
|
||||
node into Ironic using the ``fake`` driver (this driver is a configurable
|
||||
option, set ``enroll_node_driver`` in the **ironic-inspector** configuration
|
||||
file, to the Ironic driver you want).
|
||||
|
||||
The ``enroll`` hook will also set the ``ipmi_address`` property on the new
|
||||
node, if its available in the introspection data we received,
|
||||
see :ref:`ramdisk_callback <ramdisk_callback>`.
|
||||
|
||||
Once the ``enroll`` hook is finished, **ironic-inspector** will process the
|
||||
introspection data in the same way it would for an identified node. It runs
|
||||
the processing :ref:`plugins <introspection_plugins>`, and after that it runs
|
||||
introspection rules, which would allow for more customisable node
|
||||
configuration, see :ref:`rules <introspection_rules>`.
|
||||
|
||||
A rule to set a node's Ironic driver to the ``agent_ipmitool`` driver and
|
||||
populate the required driver_info for that driver would look like::
|
||||
|
||||
[{
|
||||
"description": "Set IPMI driver_info if no credentials",
|
||||
"actions": [
|
||||
{"action": "set-attribute", "path": "driver", "value": "agent_ipmitool"},
|
||||
{"action": "set-attribute", "path": "driver_info/ipmi_username",
|
||||
"value": "username"},
|
||||
{"action": "set-attribute", "path": "driver_info/ipmi_password",
|
||||
"value": "password"}
|
||||
],
|
||||
"conditions": [
|
||||
{"op": "is-empty", "field": "node://driver_info.ipmi_password"},
|
||||
{"op": "is-empty", "field": "node://driver_info.ipmi_username"}
|
||||
]
|
||||
},{
|
||||
"description": "Set deploy info if not already set on node",
|
||||
"actions": [
|
||||
{"action": "set-attribute", "path": "driver_info/deploy_kernel",
|
||||
"value": "<glance uuid>"},
|
||||
{"action": "set-attribute", "path": "driver_info/deploy_ramdisk",
|
||||
"value": "<glance uuid>"}
|
||||
],
|
||||
"conditions": [
|
||||
{"op": "is-empty", "field": "node://driver_info.deploy_ramdisk"},
|
||||
{"op": "is-empty", "field": "node://driver_info.deploy_kernel"}
|
||||
]
|
||||
}]
|
||||
|
||||
All nodes discovered and enrolled via the ``enroll`` hook, will contain an
|
||||
``auto_discovered`` flag in the introspection data, this flag makes it
|
||||
possible to distinguish between manually enrolled nodes and auto-discovered
|
||||
nodes in the introspection rules using the rule condition ``eq``::
|
||||
|
||||
{
|
||||
"description": "Enroll auto-discovered nodes with fake driver",
|
||||
"actions": [
|
||||
{"action": "set-attribute", "path": "driver", "value": "fake"}
|
||||
],
|
||||
"conditions": [
|
||||
{"op": "eq", "field": "data://auto_discovered", "value": true}
|
||||
]
|
||||
}
|
||||
|
||||
Reapplying introspection on stored data
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To allow correcting mistakes in introspection rules the API provides
|
||||
an entry point that triggers the introspection over stored data. The
|
||||
data to use for processing is kept in Swift separately from the data
|
||||
already processed. Reapplying introspection overwrites processed data
|
||||
in the store. Updating the introspection data through the endpoint
|
||||
isn't supported yet. Following preconditions are checked before
|
||||
reapplying introspection:
|
||||
|
||||
* no data is being sent along with the request
|
||||
* Swift store is configured and enabled
|
||||
* introspection data is stored in Swift for the node UUID
|
||||
* node record is kept in database for the UUID
|
||||
* introspection is not ongoing for the node UUID
|
||||
|
||||
Should the preconditions fail an immediate response is given to the
|
||||
user:
|
||||
|
||||
* ``400`` if the request contained data or in case Swift store is not
|
||||
enabled in configuration
|
||||
* ``404`` in case Ironic doesn't keep track of the node UUID
|
||||
* ``409`` if an introspection is already ongoing for the node
|
||||
|
||||
If the preconditions are met a background task is executed to carry
|
||||
out the processing and a ``202 Accepted`` response is returned to the
|
||||
endpoint user. As requested, these steps are performed in the
|
||||
background task:
|
||||
|
||||
* preprocessing hooks
|
||||
* post processing hooks, storing result in Swift
|
||||
* introspection rules
|
||||
|
||||
These steps are avoided, based on the feature requirements:
|
||||
|
||||
* ``node_not_found_hook`` is skipped
|
||||
* power operations
|
||||
* roll-back actions done by hooks
|
||||
|
||||
Limitations:
|
||||
|
||||
* there's no way to update the unprocessed data atm.
|
||||
* the unprocessed data is never cleaned from the store
|
||||
* check for stored data presence is performed in background;
|
||||
missing data situation still results in a ``202`` response
|
||||
|
||||
Capabilities Detection
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Starting with the Newton release, **Ironic Inspector** can optionally discover
|
||||
several node capabilities. A recent (Newton or newer) IPA image is required
|
||||
for it to work.
|
||||
|
||||
Boot mode
|
||||
^^^^^^^^^
|
||||
|
||||
The current boot mode (BIOS or UEFI) can be detected and recorded as
|
||||
``boot_mode`` capability in Ironic. It will make some drivers to change their
|
||||
behaviour to account for this capability. Set the ``[capabilities]boot_mode``
|
||||
configuration option to ``True`` to enable.
|
||||
|
||||
CPU capabilities
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Several CPU flags are detected by default and recorded as following
|
||||
capabilities:
|
||||
|
||||
* ``cpu_aes`` AES instructions.
|
||||
|
||||
* ``cpu_vt`` virtualization support.
|
||||
|
||||
* ``cpu_txt`` TXT support.
|
||||
|
||||
* ``cpu_hugepages`` huge pages (2 MiB) support.
|
||||
|
||||
* ``cpu_hugepages_1g`` huge pages (1 GiB) support.
|
||||
|
||||
It is possible to define your own rules for detecting CPU capabilities.
|
||||
Set the ``[capabilities]cpu_flags`` configuration option to a mapping between
|
||||
a CPU flag and a capability, for example::
|
||||
|
||||
cpu_flags = aes:cpu_aes,svm:cpu_vt,vmx:cpu_vt
|
||||
|
||||
See the default value of this option for a more detail example.
|
||||
|
||||
InfiniBand support
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
Starting with the Ocata release, **Ironic Inspector** supports detection of
|
||||
InfiniBand network interfaces. A recent (Ocata or newer) IPA image is required
|
||||
for that to work. When an InfiniBand network interface is discovered, the
|
||||
**Ironic Inspector** adds a ``client-id`` attribute to the ``extra`` attribute
|
||||
in the ironic port. The **Ironic Inspector** should be configured with
|
||||
``firewall.ethoib_interfaces`` to indicate the Ethernet Over InfiniBand (EoIB)
|
||||
which are used for physical access access to the DHCP network.
|
||||
For example if **Ironic Inspector** DHCP server is using ``br-inspector`` and
|
||||
the ``br-inspector`` has EoIB port e.g. ``eth0``,
|
||||
the ``firewall.ethoib_interfaces`` should be set to ``eth0``.
|
||||
The ``firewall.ethoib_interfaces`` allows to map the baremetal GUID to it's
|
||||
EoIB MAC based on the neighs files. This is needed for blocking DHCP traffic
|
||||
of the nodes (MACs) which are not part of the introspection.
|
||||
|
||||
The format of the ``/sys/class/net/<ethoib>/eth/neighs`` file::
|
||||
|
||||
# EMAC=<ethernet mac of the ethoib> IMAC=<qp number:lid:GUID>
|
||||
# For example:
|
||||
IMAC=97:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:26:52
|
||||
qp number=97:fe
|
||||
lid=80:00:00:00:00:00:00
|
||||
GUID=7c:fe:90:03:00:29:26:52
|
||||
|
||||
Example of content::
|
||||
|
||||
EMAC=02:00:02:97:00:01 IMAC=97:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:26:52
|
||||
EMAC=02:00:00:61:00:02 IMAC=61:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:24:4f
|
@ -1,83 +0,0 @@
|
||||
How Ironic Inspector Works
|
||||
==========================
|
||||
|
||||
Workflow
|
||||
--------
|
||||
|
||||
Usual hardware introspection flow is as follows:
|
||||
|
||||
* Operator enrolls nodes into Ironic_ e.g. via ironic CLI command.
|
||||
Power management credentials should be provided to Ironic at this step.
|
||||
|
||||
* Nodes are put in the correct state for introspection as described in
|
||||
:ref:`node states <node_states>`.
|
||||
|
||||
* Operator sends nodes on introspection using **ironic-inspector** API or CLI
|
||||
(see :ref:`usage <usage_guide>`).
|
||||
|
||||
* On receiving node UUID **ironic-inspector**:
|
||||
|
||||
* validates node power credentials, current power and provisioning states,
|
||||
* allows firewall access to PXE boot service for the nodes,
|
||||
* issues reboot command for the nodes, so that they boot the ramdisk.
|
||||
|
||||
* The ramdisk collects the required information and posts it back to
|
||||
**ironic-inspector**.
|
||||
|
||||
* On receiving data from the ramdisk, **ironic-inspector**:
|
||||
|
||||
* validates received data,
|
||||
* finds the node in Ironic database using it's BMC address (MAC address in
|
||||
case of SSH driver),
|
||||
* fills missing node properties with received data and creates missing ports.
|
||||
|
||||
.. note::
|
||||
**ironic-inspector** is responsible to create Ironic ports for some or all
|
||||
NIC's found on the node. **ironic-inspector** is also capable of
|
||||
deleting ports that should not be present. There are two important
|
||||
configuration options that affect this behavior: ``add_ports`` and
|
||||
``keep_ports`` (please refer to ``example.conf`` for detailed explanation).
|
||||
|
||||
Default values as of **ironic-inspector** 1.1.0 are ``add_ports=pxe``,
|
||||
``keep_ports=all``, which means that only one port will be added, which is
|
||||
associated with NIC the ramdisk PXE booted from. No ports will be deleted.
|
||||
This setting ensures that deploying on introspected nodes will succeed
|
||||
despite `Ironic bug 1405131
|
||||
<https://bugs.launchpad.net/ironic/+bug/1405131>`_.
|
||||
|
||||
Ironic inspection feature by default requires different settings:
|
||||
``add_ports=all``, ``keep_ports=present``, which means that ports will be
|
||||
created for all detected NIC's, and all other ports will be deleted.
|
||||
Refer to the `Ironic inspection documentation`_ for details.
|
||||
|
||||
Ironic inspector can also be configured to not create any ports. This is
|
||||
done by setting ``add_ports=disabled``. If setting ``add_ports`` to disabled
|
||||
the ``keep_ports`` option should be also set to ``all``. This will ensure
|
||||
no manually added ports will be deleted.
|
||||
|
||||
.. _Ironic inspection documentation: http://docs.openstack.org/developer/ironic/deploy/inspection.html
|
||||
|
||||
* Separate API (see :ref:`usage <usage_guide>` and :ref:`api <http_api>`) can
|
||||
be used to query introspection results for a given node.
|
||||
|
||||
* Nodes are put in the correct state for deploying as described in
|
||||
:ref:`node states <node_states>`.
|
||||
|
||||
Starting DHCP server and configuring PXE boot environment is not part of this
|
||||
package and should be done separately.
|
||||
|
||||
State machine diagram
|
||||
---------------------
|
||||
|
||||
.. _state_machine_diagram:
|
||||
|
||||
The diagram below shows the introspection states that an **ironic-inspector**
|
||||
FSM goes through during the node introspection, discovery and reprocessing.
|
||||
The diagram also shows events that trigger state transitions.
|
||||
|
||||
.. figure:: ../images/states.svg
|
||||
:width: 660px
|
||||
:align: center
|
||||
:alt: ironic-inspector state machine diagram
|
||||
|
||||
.. _Ironic: https://wiki.openstack.org/wiki/Ironic
|
915
example.conf
915
example.conf
@ -1,915 +0,0 @@
|
||||
[DEFAULT]
|
||||
|
||||
#
|
||||
# From ironic_inspector
|
||||
#
|
||||
|
||||
# IP to listen on. (string value)
|
||||
#listen_address = 0.0.0.0
|
||||
|
||||
# Port to listen on. (port value)
|
||||
# Minimum value: 0
|
||||
# Maximum value: 65535
|
||||
#listen_port = 5050
|
||||
|
||||
# Authentication method used on the ironic-inspector API. Either
|
||||
# "noauth" or "keystone" are currently valid options. "noauth" will
|
||||
# disable all authentication. (string value)
|
||||
# Allowed values: keystone, noauth
|
||||
#auth_strategy = keystone
|
||||
|
||||
# Timeout after which introspection is considered failed, set to 0 to
|
||||
# disable. (integer value)
|
||||
#timeout = 3600
|
||||
|
||||
# DEPRECATED: For how much time (in seconds) to keep status
|
||||
# information about nodes after introspection was finished for them.
|
||||
# Set to 0 (the default) to disable the timeout. (integer value)
|
||||
# This option is deprecated for removal.
|
||||
# Its value may be silently ignored in the future.
|
||||
#node_status_keep_time = 0
|
||||
|
||||
# Amount of time in seconds, after which repeat clean up of timed out
|
||||
# nodes and old nodes status information. (integer value)
|
||||
#clean_up_period = 60
|
||||
|
||||
# SSL Enabled/Disabled (boolean value)
|
||||
#use_ssl = false
|
||||
|
||||
# Path to SSL certificate (string value)
|
||||
#ssl_cert_path =
|
||||
|
||||
# Path to SSL key (string value)
|
||||
#ssl_key_path =
|
||||
|
||||
# The green thread pool size. (integer value)
|
||||
# Minimum value: 2
|
||||
#max_concurrency = 1000
|
||||
|
||||
# Delay (in seconds) between two introspections. (integer value)
|
||||
#introspection_delay = 5
|
||||
|
||||
# DEPRECATED: Only node with drivers matching this regular expression
|
||||
# will be affected by introspection_delay setting. (string value)
|
||||
# This option is deprecated for removal.
|
||||
# Its value may be silently ignored in the future.
|
||||
#introspection_delay_drivers = .*
|
||||
|
||||
# Ironic driver_info fields that are equivalent to ipmi_address. (list
|
||||
# value)
|
||||
#ipmi_address_fields = ilo_address,drac_host,drac_address,cimc_address
|
||||
|
||||
# Path to the rootwrap configuration file to use for running commands
|
||||
# as root (string value)
|
||||
#rootwrap_config = /etc/ironic-inspector/rootwrap.conf
|
||||
|
||||
# Limit the number of elements an API list-call returns (integer
|
||||
# value)
|
||||
# Minimum value: 1
|
||||
#api_max_limit = 1000
|
||||
|
||||
#
|
||||
# From oslo.log
|
||||
#
|
||||
|
||||
# If set to true, the logging level will be set to DEBUG instead of
|
||||
# the default INFO level. (boolean value)
|
||||
# Note: This option can be changed without restarting.
|
||||
#debug = false
|
||||
|
||||
# The name of a logging configuration file. This file is appended to
|
||||
# any existing logging configuration files. For details about logging
|
||||
# configuration files, see the Python logging module documentation.
|
||||
# Note that when logging configuration files are used then all logging
|
||||
# configuration is set in the configuration file and other logging
|
||||
# configuration options are ignored (for example,
|
||||
# logging_context_format_string). (string value)
|
||||
# Note: This option can be changed without restarting.
|
||||
# Deprecated group/name - [DEFAULT]/log_config
|
||||
#log_config_append = <None>
|
||||
|
||||
# Defines the format string for %%(asctime)s in log records. Default:
|
||||
# %(default)s . This option is ignored if log_config_append is set.
|
||||
# (string value)
|
||||
#log_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
# (Optional) Name of log file to send logging output to. If no default
|
||||
# is set, logging will go to stderr as defined by use_stderr. This
|
||||
# option is ignored if log_config_append is set. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/logfile
|
||||
#log_file = <None>
|
||||
|
||||
# (Optional) The base directory used for relative log_file paths.
|
||||
# This option is ignored if log_config_append is set. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/logdir
|
||||
#log_dir = <None>
|
||||
|
||||
# Uses logging handler designed to watch file system. When log file is
|
||||
# moved or removed this handler will open a new log file with
|
||||
# specified path instantaneously. It makes sense only if log_file
|
||||
# option is specified and Linux platform is used. This option is
|
||||
# ignored if log_config_append is set. (boolean value)
|
||||
#watch_log_file = false
|
||||
|
||||
# Use syslog for logging. Existing syslog format is DEPRECATED and
|
||||
# will be changed later to honor RFC5424. This option is ignored if
|
||||
# log_config_append is set. (boolean value)
|
||||
#use_syslog = false
|
||||
|
||||
# Enable journald for logging. If running in a systemd environment you
|
||||
# may wish to enable journal support. Doing so will use the journal
|
||||
# native protocol which includes structured metadata in addition to
|
||||
# log messages.This option is ignored if log_config_append is set.
|
||||
# (boolean value)
|
||||
#use_journal = false
|
||||
|
||||
# Syslog facility to receive log lines. This option is ignored if
|
||||
# log_config_append is set. (string value)
|
||||
#syslog_log_facility = LOG_USER
|
||||
|
||||
# Log output to standard error. This option is ignored if
|
||||
# log_config_append is set. (boolean value)
|
||||
#use_stderr = false
|
||||
|
||||
# Format string to use for log messages with context. (string value)
|
||||
#logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s
|
||||
|
||||
# Format string to use for log messages when context is undefined.
|
||||
# (string value)
|
||||
#logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s
|
||||
|
||||
# Additional data to append to log message when logging level for the
|
||||
# message is DEBUG. (string value)
|
||||
#logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d
|
||||
|
||||
# Prefix each line of exception output with this format. (string
|
||||
# value)
|
||||
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s
|
||||
|
||||
# Defines the format string for %(user_identity)s that is used in
|
||||
# logging_context_format_string. (string value)
|
||||
#logging_user_identity_format = %(user)s %(tenant)s %(domain)s %(user_domain)s %(project_domain)s
|
||||
|
||||
# List of package logging levels in logger=LEVEL pairs. This option is
|
||||
# ignored if log_config_append is set. (list value)
|
||||
#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,oslo_messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN,taskflow=WARN,keystoneauth=WARN,oslo.cache=INFO,dogpile.core.dogpile=INFO
|
||||
|
||||
# Enables or disables publication of error events. (boolean value)
|
||||
#publish_errors = false
|
||||
|
||||
# The format for an instance that is passed with the log message.
|
||||
# (string value)
|
||||
#instance_format = "[instance: %(uuid)s] "
|
||||
|
||||
# The format for an instance UUID that is passed with the log message.
|
||||
# (string value)
|
||||
#instance_uuid_format = "[instance: %(uuid)s] "
|
||||
|
||||
# Interval, number of seconds, of log rate limiting. (integer value)
|
||||
#rate_limit_interval = 0
|
||||
|
||||
# Maximum number of logged messages per rate_limit_interval. (integer
|
||||
# value)
|
||||
#rate_limit_burst = 0
|
||||
|
||||
# Log level name used by rate limiting: CRITICAL, ERROR, INFO,
|
||||
# WARNING, DEBUG or empty string. Logs with level greater or equal to
|
||||
# rate_limit_except_level are not filtered. An empty string means that
|
||||
# all levels are filtered. (string value)
|
||||
#rate_limit_except_level = CRITICAL
|
||||
|
||||
# Enables or disables fatal status of deprecations. (boolean value)
|
||||
#fatal_deprecations = false
|
||||
|
||||
|
||||
[capabilities]
|
||||
|
||||
#
|
||||
# From ironic_inspector.plugins.capabilities
|
||||
#
|
||||
|
||||
# Whether to store the boot mode (BIOS or UEFI). (boolean value)
|
||||
#boot_mode = false
|
||||
|
||||
# Mapping between a CPU flag and a capability to set if this flag is
|
||||
# present. (dict value)
|
||||
#cpu_flags = aes:cpu_aes,pdpe1gb:cpu_hugepages_1g,pse:cpu_hugepages,smx:cpu_txt,svm:cpu_vt,vmx:cpu_vt
|
||||
|
||||
|
||||
[cors]
|
||||
|
||||
#
|
||||
# From oslo.middleware.cors
|
||||
#
|
||||
|
||||
# Indicate whether this resource may be shared with the domain
|
||||
# received in the requests "origin" header. Format:
|
||||
# "<protocol>://<host>[:<port>]", no trailing slash. Example:
|
||||
# https://horizon.example.com (list value)
|
||||
#allowed_origin = <None>
|
||||
|
||||
# Indicate that the actual request can include user credentials
|
||||
# (boolean value)
|
||||
#allow_credentials = true
|
||||
|
||||
# Indicate which headers are safe to expose to the API. Defaults to
|
||||
# HTTP Simple Headers. (list value)
|
||||
#expose_headers =
|
||||
|
||||
# Maximum cache age of CORS preflight requests. (integer value)
|
||||
#max_age = 3600
|
||||
|
||||
# Indicate which methods can be used during the actual request. (list
|
||||
# value)
|
||||
#allow_methods = GET,POST,PUT,HEAD,PATCH,DELETE,OPTIONS
|
||||
|
||||
# Indicate which header field names may be used during the actual
|
||||
# request. (list value)
|
||||
#allow_headers = X-Auth-Token,X-OpenStack-Ironic-Inspector-API-Minimum-Version,X-OpenStack-Ironic-Inspector-API-Maximum-Version,X-OpenStack-Ironic-Inspector-API-Version
|
||||
|
||||
|
||||
[cors.subdomain]
|
||||
|
||||
#
|
||||
# From oslo.middleware.cors
|
||||
#
|
||||
|
||||
# Indicate whether this resource may be shared with the domain
|
||||
# received in the requests "origin" header. Format:
|
||||
# "<protocol>://<host>[:<port>]", no trailing slash. Example:
|
||||
# https://horizon.example.com (list value)
|
||||
#allowed_origin = <None>
|
||||
|
||||
# Indicate that the actual request can include user credentials
|
||||
# (boolean value)
|
||||
#allow_credentials = true
|
||||
|
||||
# Indicate which headers are safe to expose to the API. Defaults to
|
||||
# HTTP Simple Headers. (list value)
|
||||
#expose_headers =
|
||||
|
||||
# Maximum cache age of CORS preflight requests. (integer value)
|
||||
#max_age = 3600
|
||||
|
||||
# Indicate which methods can be used during the actual request. (list
|
||||
# value)
|
||||
#allow_methods = GET,POST,PUT,HEAD,PATCH,DELETE,OPTIONS
|
||||
|
||||
# Indicate which header field names may be used during the actual
|
||||
# request. (list value)
|
||||
#allow_headers = X-Auth-Token,X-OpenStack-Ironic-Inspector-API-Minimum-Version,X-OpenStack-Ironic-Inspector-API-Maximum-Version,X-OpenStack-Ironic-Inspector-API-Version
|
||||
|
||||
|
||||
[database]
|
||||
|
||||
#
|
||||
# From oslo.db
|
||||
#
|
||||
|
||||
# If True, SQLite uses synchronous mode. (boolean value)
|
||||
#sqlite_synchronous = true
|
||||
|
||||
# The back end to use for the database. (string value)
|
||||
# Deprecated group/name - [DEFAULT]/db_backend
|
||||
#backend = sqlalchemy
|
||||
|
||||
# The SQLAlchemy connection string to use to connect to the database.
|
||||
# (string value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_connection
|
||||
# Deprecated group/name - [DATABASE]/sql_connection
|
||||
# Deprecated group/name - [sql]/connection
|
||||
#connection = <None>
|
||||
|
||||
# The SQLAlchemy connection string to use to connect to the slave
|
||||
# database. (string value)
|
||||
#slave_connection = <None>
|
||||
|
||||
# The SQL mode to be used for MySQL sessions. This option, including
|
||||
# the default, overrides any server-set SQL mode. To use whatever SQL
|
||||
# mode is set by the server configuration, set this to no value.
|
||||
# Example: mysql_sql_mode= (string value)
|
||||
#mysql_sql_mode = TRADITIONAL
|
||||
|
||||
# Timeout before idle SQL connections are reaped. (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_idle_timeout
|
||||
# Deprecated group/name - [DATABASE]/sql_idle_timeout
|
||||
# Deprecated group/name - [sql]/idle_timeout
|
||||
#idle_timeout = 3600
|
||||
|
||||
# Minimum number of SQL connections to keep open in a pool. (integer
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_min_pool_size
|
||||
# Deprecated group/name - [DATABASE]/sql_min_pool_size
|
||||
#min_pool_size = 1
|
||||
|
||||
# Maximum number of SQL connections to keep open in a pool. Setting a
|
||||
# value of 0 indicates no limit. (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_max_pool_size
|
||||
# Deprecated group/name - [DATABASE]/sql_max_pool_size
|
||||
#max_pool_size = 5
|
||||
|
||||
# Maximum number of database connection retries during startup. Set to
|
||||
# -1 to specify an infinite retry count. (integer value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_max_retries
|
||||
# Deprecated group/name - [DATABASE]/sql_max_retries
|
||||
#max_retries = 10
|
||||
|
||||
# Interval between retries of opening a SQL connection. (integer
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_retry_interval
|
||||
# Deprecated group/name - [DATABASE]/reconnect_interval
|
||||
#retry_interval = 10
|
||||
|
||||
# If set, use this value for max_overflow with SQLAlchemy. (integer
|
||||
# value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_max_overflow
|
||||
# Deprecated group/name - [DATABASE]/sqlalchemy_max_overflow
|
||||
#max_overflow = 50
|
||||
|
||||
# Verbosity of SQL debugging information: 0=None, 100=Everything.
|
||||
# (integer value)
|
||||
# Minimum value: 0
|
||||
# Maximum value: 100
|
||||
# Deprecated group/name - [DEFAULT]/sql_connection_debug
|
||||
#connection_debug = 0
|
||||
|
||||
# Add Python stack traces to SQL as comment strings. (boolean value)
|
||||
# Deprecated group/name - [DEFAULT]/sql_connection_trace
|
||||
#connection_trace = false
|
||||
|
||||
# If set, use this value for pool_timeout with SQLAlchemy. (integer
|
||||
# value)
|
||||
# Deprecated group/name - [DATABASE]/sqlalchemy_pool_timeout
|
||||
#pool_timeout = <None>
|
||||
|
||||
# Enable the experimental use of database reconnect on connection
|
||||
# lost. (boolean value)
|
||||
#use_db_reconnect = false
|
||||
|
||||
# Seconds between retries of a database transaction. (integer value)
|
||||
#db_retry_interval = 1
|
||||
|
||||
# If True, increases the interval between retries of a database
|
||||
# operation up to db_max_retry_interval. (boolean value)
|
||||
#db_inc_retry_interval = true
|
||||
|
||||
# If db_inc_retry_interval is set, the maximum seconds between retries
|
||||
# of a database operation. (integer value)
|
||||
#db_max_retry_interval = 10
|
||||
|
||||
# Maximum retries in case of connection error or deadlock error before
|
||||
# error is raised. Set to -1 to specify an infinite retry count.
|
||||
# (integer value)
|
||||
#db_max_retries = 20
|
||||
|
||||
|
||||
[discovery]
|
||||
|
||||
#
|
||||
# From ironic_inspector.plugins.discovery
|
||||
#
|
||||
|
||||
# The name of the Ironic driver used by the enroll hook when creating
|
||||
# a new node in Ironic. (string value)
|
||||
#enroll_node_driver = fake
|
||||
|
||||
|
||||
[firewall]
|
||||
|
||||
#
|
||||
# From ironic_inspector
|
||||
#
|
||||
|
||||
# Whether to manage firewall rules for PXE port. (boolean value)
|
||||
#manage_firewall = true
|
||||
|
||||
# Interface on which dnsmasq listens, the default is for VM's. (string
|
||||
# value)
|
||||
#dnsmasq_interface = br-ctlplane
|
||||
|
||||
# Amount of time in seconds, after which repeat periodic update of
|
||||
# firewall. (integer value)
|
||||
#firewall_update_period = 15
|
||||
|
||||
# iptables chain name to use. (string value)
|
||||
#firewall_chain = ironic-inspector
|
||||
|
||||
# List of Etherent Over InfiniBand interfaces on the Inspector host
|
||||
# which are used for physical access to the DHCP network. Multiple
|
||||
# interfaces would be attached to a bond or bridge specified in
|
||||
# dnsmasq_interface. The MACs of the InfiniBand nodes which are not in
|
||||
# desired state are going to be blacklisted based on the list of
|
||||
# neighbor MACs on these interfaces. (list value)
|
||||
#ethoib_interfaces =
|
||||
|
||||
|
||||
[ironic]
|
||||
|
||||
#
|
||||
# From ironic_inspector.common.ironic
|
||||
#
|
||||
|
||||
# Authentication URL (string value)
|
||||
#auth_url = <None>
|
||||
|
||||
# Method to use for authentication: noauth or keystone. (string value)
|
||||
# Allowed values: keystone, noauth
|
||||
#auth_strategy = keystone
|
||||
|
||||
# Authentication type to load (string value)
|
||||
# Deprecated group/name - [ironic]/auth_plugin
|
||||
#auth_type = <None>
|
||||
|
||||
# PEM encoded Certificate Authority to use when verifying HTTPs
|
||||
# connections. (string value)
|
||||
#cafile = <None>
|
||||
|
||||
# PEM encoded client certificate cert file (string value)
|
||||
#certfile = <None>
|
||||
|
||||
# Optional domain ID to use with v3 and v2 parameters. It will be used
|
||||
# for both the user and project domain in v3 and ignored in v2
|
||||
# authentication. (string value)
|
||||
#default_domain_id = <None>
|
||||
|
||||
# Optional domain name to use with v3 API and v2 parameters. It will
|
||||
# be used for both the user and project domain in v3 and ignored in v2
|
||||
# authentication. (string value)
|
||||
#default_domain_name = <None>
|
||||
|
||||
# Domain ID to scope to (string value)
|
||||
#domain_id = <None>
|
||||
|
||||
# Domain name to scope to (string value)
|
||||
#domain_name = <None>
|
||||
|
||||
# Verify HTTPS connections. (boolean value)
|
||||
#insecure = false
|
||||
|
||||
# Ironic API URL, used to set Ironic API URL when auth_strategy option
|
||||
# is noauth to work with standalone Ironic without keystone. (string
|
||||
# value)
|
||||
#ironic_url = http://localhost:6385/
|
||||
|
||||
# PEM encoded client certificate key file (string value)
|
||||
#keyfile = <None>
|
||||
|
||||
# Maximum number of retries in case of conflict error (HTTP 409).
|
||||
# (integer value)
|
||||
#max_retries = 30
|
||||
|
||||
# Ironic endpoint type. (string value)
|
||||
#os_endpoint_type = internalURL
|
||||
|
||||
# Keystone region used to get Ironic endpoints. (string value)
|
||||
#os_region = <None>
|
||||
|
||||
# Ironic service type. (string value)
|
||||
#os_service_type = baremetal
|
||||
|
||||
# User's password (string value)
|
||||
#password = <None>
|
||||
|
||||
# Domain ID containing project (string value)
|
||||
#project_domain_id = <None>
|
||||
|
||||
# Domain name containing project (string value)
|
||||
#project_domain_name = <None>
|
||||
|
||||
# Project ID to scope to (string value)
|
||||
# Deprecated group/name - [ironic]/tenant_id
|
||||
#project_id = <None>
|
||||
|
||||
# Project name to scope to (string value)
|
||||
# Deprecated group/name - [ironic]/tenant_name
|
||||
#project_name = <None>
|
||||
|
||||
# Interval between retries in case of conflict error (HTTP 409).
|
||||
# (integer value)
|
||||
#retry_interval = 2
|
||||
|
||||
# Tenant ID (string value)
|
||||
#tenant_id = <None>
|
||||
|
||||
# Tenant Name (string value)
|
||||
#tenant_name = <None>
|
||||
|
||||
# Timeout value for http requests (integer value)
|
||||
#timeout = <None>
|
||||
|
||||
# Trust ID (string value)
|
||||
#trust_id = <None>
|
||||
|
||||
# User's domain id (string value)
|
||||
#user_domain_id = <None>
|
||||
|
||||
# User's domain name (string value)
|
||||
#user_domain_name = <None>
|
||||
|
||||
# User id (string value)
|
||||
#user_id = <None>
|
||||
|
||||
# Username (string value)
|
||||
# Deprecated group/name - [ironic]/user_name
|
||||
#username = <None>
|
||||
|
||||
|
||||
[keystone_authtoken]
|
||||
|
||||
#
|
||||
# From keystonemiddleware.auth_token
|
||||
#
|
||||
|
||||
# Complete "public" Identity API endpoint. This endpoint should not be
|
||||
# an "admin" endpoint, as it should be accessible by all end users.
|
||||
# Unauthenticated clients are redirected to this endpoint to
|
||||
# authenticate. Although this endpoint should ideally be unversioned,
|
||||
# client support in the wild varies. If you're using a versioned v2
|
||||
# endpoint here, then this should *not* be the same endpoint the
|
||||
# service user utilizes for validating tokens, because normal end
|
||||
# users may not be able to reach that endpoint. (string value)
|
||||
#auth_uri = <None>
|
||||
|
||||
# API version of the admin Identity API endpoint. (string value)
|
||||
#auth_version = <None>
|
||||
|
||||
# Do not handle authorization requests within the middleware, but
|
||||
# delegate the authorization decision to downstream WSGI components.
|
||||
# (boolean value)
|
||||
#delay_auth_decision = false
|
||||
|
||||
# Request timeout value for communicating with Identity API server.
|
||||
# (integer value)
|
||||
#http_connect_timeout = <None>
|
||||
|
||||
# How many times are we trying to reconnect when communicating with
|
||||
# Identity API Server. (integer value)
|
||||
#http_request_max_retries = 3
|
||||
|
||||
# Request environment key where the Swift cache object is stored. When
|
||||
# auth_token middleware is deployed with a Swift cache, use this
|
||||
# option to have the middleware share a caching backend with swift.
|
||||
# Otherwise, use the ``memcached_servers`` option instead. (string
|
||||
# value)
|
||||
#cache = <None>
|
||||
|
||||
# Required if identity server requires client certificate (string
|
||||
# value)
|
||||
#certfile = <None>
|
||||
|
||||
# Required if identity server requires client certificate (string
|
||||
# value)
|
||||
#keyfile = <None>
|
||||
|
||||
# A PEM encoded Certificate Authority to use when verifying HTTPs
|
||||
# connections. Defaults to system CAs. (string value)
|
||||
#cafile = <None>
|
||||
|
||||
# Verify HTTPS connections. (boolean value)
|
||||
#insecure = false
|
||||
|
||||
# The region in which the identity server can be found. (string value)
|
||||
#region_name = <None>
|
||||
|
||||
# DEPRECATED: Directory used to cache files related to PKI tokens.
|
||||
# This option has been deprecated in the Ocata release and will be
|
||||
# removed in the P release. (string value)
|
||||
# This option is deprecated for removal since Ocata.
|
||||
# Its value may be silently ignored in the future.
|
||||
# Reason: PKI token format is no longer supported.
|
||||
#signing_dir = <None>
|
||||
|
||||
# Optionally specify a list of memcached server(s) to use for caching.
|
||||
# If left undefined, tokens will instead be cached in-process. (list
|
||||
# value)
|
||||
# Deprecated group/name - [keystone_authtoken]/memcache_servers
|
||||
#memcached_servers = <None>
|
||||
|
||||
# In order to prevent excessive effort spent validating tokens, the
|
||||
# middleware caches previously-seen tokens for a configurable duration
|
||||
# (in seconds). Set to -1 to disable caching completely. (integer
|
||||
# value)
|
||||
#token_cache_time = 300
|
||||
|
||||
# DEPRECATED: Determines the frequency at which the list of revoked
|
||||
# tokens is retrieved from the Identity service (in seconds). A high
|
||||
# number of revocation events combined with a low cache duration may
|
||||
# significantly reduce performance. Only valid for PKI tokens. This
|
||||
# option has been deprecated in the Ocata release and will be removed
|
||||
# in the P release. (integer value)
|
||||
# This option is deprecated for removal since Ocata.
|
||||
# Its value may be silently ignored in the future.
|
||||
# Reason: PKI token format is no longer supported.
|
||||
#revocation_cache_time = 10
|
||||
|
||||
# (Optional) If defined, indicate whether token data should be
|
||||
# authenticated or authenticated and encrypted. If MAC, token data is
|
||||
# authenticated (with HMAC) in the cache. If ENCRYPT, token data is
|
||||
# encrypted and authenticated in the cache. If the value is not one of
|
||||
# these options or empty, auth_token will raise an exception on
|
||||
# initialization. (string value)
|
||||
# Allowed values: None, MAC, ENCRYPT
|
||||
#memcache_security_strategy = None
|
||||
|
||||
# (Optional, mandatory if memcache_security_strategy is defined) This
|
||||
# string is used for key derivation. (string value)
|
||||
#memcache_secret_key = <None>
|
||||
|
||||
# (Optional) Number of seconds memcached server is considered dead
|
||||
# before it is tried again. (integer value)
|
||||
#memcache_pool_dead_retry = 300
|
||||
|
||||
# (Optional) Maximum total number of open connections to every
|
||||
# memcached server. (integer value)
|
||||
#memcache_pool_maxsize = 10
|
||||
|
||||
# (Optional) Socket timeout in seconds for communicating with a
|
||||
# memcached server. (integer value)
|
||||
#memcache_pool_socket_timeout = 3
|
||||
|
||||
# (Optional) Number of seconds a connection to memcached is held
|
||||
# unused in the pool before it is closed. (integer value)
|
||||
#memcache_pool_unused_timeout = 60
|
||||
|
||||
# (Optional) Number of seconds that an operation will wait to get a
|
||||
# memcached client connection from the pool. (integer value)
|
||||
#memcache_pool_conn_get_timeout = 10
|
||||
|
||||
# (Optional) Use the advanced (eventlet safe) memcached client pool.
|
||||
# The advanced pool will only work under python 2.x. (boolean value)
|
||||
#memcache_use_advanced_pool = false
|
||||
|
||||
# (Optional) Indicate whether to set the X-Service-Catalog header. If
|
||||
# False, middleware will not ask for service catalog on token
|
||||
# validation and will not set the X-Service-Catalog header. (boolean
|
||||
# value)
|
||||
#include_service_catalog = true
|
||||
|
||||
# Used to control the use and type of token binding. Can be set to:
|
||||
# "disabled" to not check token binding. "permissive" (default) to
|
||||
# validate binding information if the bind type is of a form known to
|
||||
# the server and ignore it if not. "strict" like "permissive" but if
|
||||
# the bind type is unknown the token will be rejected. "required" any
|
||||
# form of token binding is needed to be allowed. Finally the name of a
|
||||
# binding method that must be present in tokens. (string value)
|
||||
#enforce_token_bind = permissive
|
||||
|
||||
# DEPRECATED: If true, the revocation list will be checked for cached
|
||||
# tokens. This requires that PKI tokens are configured on the identity
|
||||
# server. (boolean value)
|
||||
# This option is deprecated for removal since Ocata.
|
||||
# Its value may be silently ignored in the future.
|
||||
# Reason: PKI token format is no longer supported.
|
||||
#check_revocations_for_cached = false
|
||||
|
||||
# DEPRECATED: Hash algorithms to use for hashing PKI tokens. This may
|
||||
# be a single algorithm or multiple. The algorithms are those
|
||||
# supported by Python standard hashlib.new(). The hashes will be tried
|
||||
# in the order given, so put the preferred one first for performance.
|
||||
# The result of the first hash will be stored in the cache. This will
|
||||
# typically be set to multiple values only while migrating from a less
|
||||
# secure algorithm to a more secure one. Once all the old tokens are
|
||||
# expired this option should be set to a single value for better
|
||||
# performance. (list value)
|
||||
# This option is deprecated for removal since Ocata.
|
||||
# Its value may be silently ignored in the future.
|
||||
# Reason: PKI token format is no longer supported.
|
||||
#hash_algorithms = md5
|
||||
|
||||
# A choice of roles that must be present in a service token. Service
|
||||
# tokens are allowed to request that an expired token can be used and
|
||||
# so this check should tightly control that only actual services
|
||||
# should be sending this token. Roles here are applied as an ANY check
|
||||
# so any role in this list must be present. For backwards
|
||||
# compatibility reasons this currently only affects the allow_expired
|
||||
# check. (list value)
|
||||
#service_token_roles = service
|
||||
|
||||
# For backwards compatibility reasons we must let valid service tokens
|
||||
# pass that don't pass the service_token_roles check as valid. Setting
|
||||
# this true will become the default in a future release and should be
|
||||
# enabled if possible. (boolean value)
|
||||
#service_token_roles_required = false
|
||||
|
||||
# Authentication type to load (string value)
|
||||
# Deprecated group/name - [keystone_authtoken]/auth_plugin
|
||||
#auth_type = <None>
|
||||
|
||||
# Config Section from which to load plugin specific options (string
|
||||
# value)
|
||||
#auth_section = <None>
|
||||
|
||||
|
||||
[pci_devices]
|
||||
|
||||
#
|
||||
# From ironic_inspector.plugins.pci_devices
|
||||
#
|
||||
|
||||
# An alias for PCI device identified by 'vendor_id' and 'product_id'
|
||||
# fields. Format: {"vendor_id": "1234", "product_id": "5678", "name":
|
||||
# "pci_dev1"} (multi valued)
|
||||
#alias =
|
||||
|
||||
|
||||
[processing]
|
||||
|
||||
#
|
||||
# From ironic_inspector
|
||||
#
|
||||
|
||||
# Which MAC addresses to add as ports during introspection. Possible
|
||||
# values: all (all MAC addresses), active (MAC addresses of NIC with
|
||||
# IP addresses), pxe (only MAC address of NIC node PXE booted from,
|
||||
# falls back to "active" if PXE MAC is not supplied by the ramdisk).
|
||||
# (string value)
|
||||
# Allowed values: all, active, pxe, disabled
|
||||
#add_ports = pxe
|
||||
|
||||
# Which ports (already present on a node) to keep after introspection.
|
||||
# Possible values: all (do not delete anything), present (keep ports
|
||||
# which MACs were present in introspection data), added (keep only
|
||||
# MACs that we added during introspection). (string value)
|
||||
# Allowed values: all, present, added
|
||||
#keep_ports = all
|
||||
|
||||
# Whether to overwrite existing values in node database. Disable this
|
||||
# option to make introspection a non-destructive operation. (boolean
|
||||
# value)
|
||||
#overwrite_existing = true
|
||||
|
||||
# Comma-separated list of default hooks for processing pipeline. Hook
|
||||
# 'scheduler' updates the node with the minimum properties required by
|
||||
# the Nova scheduler. Hook 'validate_interfaces' ensures that valid
|
||||
# NIC data was provided by the ramdisk. Do not exclude these two
|
||||
# unless you really know what you're doing. (string value)
|
||||
#default_processing_hooks = ramdisk_error,root_disk_selection,scheduler,validate_interfaces,capabilities,pci_devices
|
||||
|
||||
# Comma-separated list of enabled hooks for processing pipeline. The
|
||||
# default for this is $default_processing_hooks, hooks can be added
|
||||
# before or after the defaults like this:
|
||||
# "prehook,$default_processing_hooks,posthook". (string value)
|
||||
#processing_hooks = $default_processing_hooks
|
||||
|
||||
# If set, logs from ramdisk will be stored in this directory. (string
|
||||
# value)
|
||||
#ramdisk_logs_dir = <None>
|
||||
|
||||
# Whether to store ramdisk logs even if it did not return an error
|
||||
# message (dependent upon "ramdisk_logs_dir" option being set).
|
||||
# (boolean value)
|
||||
#always_store_ramdisk_logs = false
|
||||
|
||||
# The name of the hook to run when inspector receives inspection
|
||||
# information from a node it isn't already aware of. This hook is
|
||||
# ignored by default. (string value)
|
||||
#node_not_found_hook = <None>
|
||||
|
||||
# Method for storing introspection data. If set to 'none',
|
||||
# introspection data will not be stored. (string value)
|
||||
# Allowed values: none, swift
|
||||
#store_data = none
|
||||
|
||||
# Name of the key to store the location of stored data in the extra
|
||||
# column of the Ironic database. (string value)
|
||||
#store_data_location = <None>
|
||||
|
||||
# Whether to leave 1 GiB of disk size untouched for partitioning. Only
|
||||
# has effect when used with the IPA as a ramdisk, for older ramdisk
|
||||
# local_gb is calculated on the ramdisk side. (boolean value)
|
||||
#disk_partitioning_spacing = true
|
||||
|
||||
# DEPRECATED: Whether to log node BMC address with every message
|
||||
# during processing. (boolean value)
|
||||
# This option is deprecated for removal.
|
||||
# Its value may be silently ignored in the future.
|
||||
#log_bmc_address = true
|
||||
|
||||
# File name template for storing ramdisk logs. The following
|
||||
# replacements can be used: {uuid} - node UUID or "unknown", {bmc} -
|
||||
# node BMC address or "unknown", {dt} - current UTC date and time,
|
||||
# {mac} - PXE booting MAC or "unknown". (string value)
|
||||
#ramdisk_logs_filename_format = {uuid}_{dt:%Y%m%d-%H%M%S.%f}.tar.gz
|
||||
|
||||
# Whether to power off a node after introspection. (boolean value)
|
||||
#power_off = true
|
||||
|
||||
|
||||
[pxe_filter]
|
||||
|
||||
#
|
||||
# From ironic_inspector
|
||||
#
|
||||
|
||||
# PXE boot filter driver to use, such as iptables (string value)
|
||||
#driver = noop
|
||||
|
||||
# Amount of time in seconds, after which repeat periodic update of the
|
||||
# filter. (integer value)
|
||||
# Minimum value: 0
|
||||
#sync_period = 15
|
||||
|
||||
|
||||
[swift]
|
||||
|
||||
#
|
||||
# From ironic_inspector.common.swift
|
||||
#
|
||||
|
||||
# Authentication URL (string value)
|
||||
#auth_url = <None>
|
||||
|
||||
# Authentication type to load (string value)
|
||||
# Deprecated group/name - [swift]/auth_plugin
|
||||
#auth_type = <None>
|
||||
|
||||
# PEM encoded Certificate Authority to use when verifying HTTPs
|
||||
# connections. (string value)
|
||||
#cafile = <None>
|
||||
|
||||
# PEM encoded client certificate cert file (string value)
|
||||
#certfile = <None>
|
||||
|
||||
# Default Swift container to use when creating objects. (string value)
|
||||
#container = ironic-inspector
|
||||
|
||||
# Optional domain ID to use with v3 and v2 parameters. It will be used
|
||||
# for both the user and project domain in v3 and ignored in v2
|
||||
# authentication. (string value)
|
||||
#default_domain_id = <None>
|
||||
|
||||
# Optional domain name to use with v3 API and v2 parameters. It will
|
||||
# be used for both the user and project domain in v3 and ignored in v2
|
||||
# authentication. (string value)
|
||||
#default_domain_name = <None>
|
||||
|
||||
# Number of seconds that the Swift object will last before being
|
||||
# deleted. (set to 0 to never delete the object). (integer value)
|
||||
#delete_after = 0
|
||||
|
||||
# Domain ID to scope to (string value)
|
||||
#domain_id = <None>
|
||||
|
||||
# Domain name to scope to (string value)
|
||||
#domain_name = <None>
|
||||
|
||||
# Verify HTTPS connections. (boolean value)
|
||||
#insecure = false
|
||||
|
||||
# PEM encoded client certificate key file (string value)
|
||||
#keyfile = <None>
|
||||
|
||||
# Maximum number of times to retry a Swift request, before failing.
|
||||
# (integer value)
|
||||
#max_retries = 2
|
||||
|
||||
# Swift endpoint type. (string value)
|
||||
#os_endpoint_type = internalURL
|
||||
|
||||
# Keystone region to get endpoint for. (string value)
|
||||
#os_region = <None>
|
||||
|
||||
# Swift service type. (string value)
|
||||
#os_service_type = object-store
|
||||
|
||||
# User's password (string value)
|
||||
#password = <None>
|
||||
|
||||
# Domain ID containing project (string value)
|
||||
#project_domain_id = <None>
|
||||
|
||||
# Domain name containing project (string value)
|
||||
#project_domain_name = <None>
|
||||
|
||||
# Project ID to scope to (string value)
|
||||
# Deprecated group/name - [swift]/tenant_id
|
||||
#project_id = <None>
|
||||
|
||||
# Project name to scope to (string value)
|
||||
# Deprecated group/name - [swift]/tenant_name
|
||||
#project_name = <None>
|
||||
|
||||
# Tenant ID (string value)
|
||||
#tenant_id = <None>
|
||||
|
||||
# Tenant Name (string value)
|
||||
#tenant_name = <None>
|
||||
|
||||
# Timeout value for http requests (integer value)
|
||||
#timeout = <None>
|
||||
|
||||
# Trust ID (string value)
|
||||
#trust_id = <None>
|
||||
|
||||
# User's domain id (string value)
|
||||
#user_domain_id = <None>
|
||||
|
||||
# User's domain name (string value)
|
||||
#user_domain_name = <None>
|
||||
|
||||
# User id (string value)
|
||||
#user_id = <None>
|
||||
|
||||
# Username (string value)
|
||||
# Deprecated group/name - [swift]/user_name
|
||||
#username = <None>
|
@ -1,20 +0,0 @@
|
||||
.\" Manpage for ironic-inspector.
|
||||
.TH man 8 "08 Oct 2014" "1.0" "ironic-inspector man page"
|
||||
.SH NAME
|
||||
ironic-inspector \- hardware introspection daemon for OpenStack Ironic.
|
||||
.SH SYNOPSIS
|
||||
ironic-inspector CONFFILE
|
||||
.SH DESCRIPTION
|
||||
This command starts ironic-inspector service, which starts and finishes
|
||||
hardware discovery and maintains firewall rules for nodes accessing PXE
|
||||
boot service (usually dnsmasq).
|
||||
.SH OPTIONS
|
||||
The ironic-inspector does not take any options. However, you should supply
|
||||
path to the configuration file.
|
||||
.SH SEE ALSO
|
||||
README page located at https://pypi.python.org/pypi/ironic-inspector
|
||||
provides some information about how to configure and use the service.
|
||||
.SH BUGS
|
||||
No known bugs.
|
||||
.SH AUTHOR
|
||||
Dmitry Tantsur (divius.inside@gmail.com)
|
@ -1,38 +0,0 @@
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = %(here)s/migrations
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
@ -1,83 +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 Rest Api tools."""
|
||||
|
||||
import flask
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def raises_coercion_exceptions(fn):
|
||||
"""Convert coercion function exceptions to utils.Error.
|
||||
|
||||
:raises: utils.Error when the coercion function raises an
|
||||
AssertionError or a ValueError
|
||||
"""
|
||||
@six.wraps(fn)
|
||||
def inner(*args, **kwargs):
|
||||
try:
|
||||
ret = fn(*args, **kwargs)
|
||||
except (AssertionError, ValueError) as exc:
|
||||
raise utils.Error(_('Bad request: %s') % exc, code=400)
|
||||
return ret
|
||||
return inner
|
||||
|
||||
|
||||
def request_field(field_name):
|
||||
"""Decorate a function that coerces the specified field.
|
||||
|
||||
:param field_name: name of the field to fetch
|
||||
:returns: a decorator
|
||||
"""
|
||||
def outer(fn):
|
||||
@six.wraps(fn)
|
||||
def inner(*args, **kwargs):
|
||||
default = kwargs.pop('default', None)
|
||||
field = flask.request.args.get(field_name, default=default)
|
||||
if field == default:
|
||||
# field not found or the same as the default, just return
|
||||
return default
|
||||
return fn(field, *args, **kwargs)
|
||||
return inner
|
||||
return outer
|
||||
|
||||
|
||||
@request_field('marker')
|
||||
@raises_coercion_exceptions
|
||||
def marker_field(value):
|
||||
"""Fetch the pagination marker field from flask.request.args.
|
||||
|
||||
:returns: an uuid
|
||||
"""
|
||||
assert uuidutils.is_uuid_like(value), _('Marker not UUID-like')
|
||||
return value
|
||||
|
||||
|
||||
@request_field('limit')
|
||||
@raises_coercion_exceptions
|
||||
def limit_field(value):
|
||||
"""Fetch the pagination limit field from flask.request.args.
|
||||
|
||||
:returns: the limit
|
||||
"""
|
||||
# limit of zero means the default limit
|
||||
value = int(value) or CONF.api_max_limit
|
||||
assert value >= 0, _('Limit cannot be negative')
|
||||
assert value <= CONF.api_max_limit, _('Limit over %s') % CONF.api_max_limit
|
||||
return value
|
@ -1,2 +0,0 @@
|
||||
import eventlet # noqa
|
||||
eventlet.monkey_patch()
|
@ -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.
|
||||
|
||||
"""The Ironic Inspector service."""
|
||||
|
||||
import sys
|
||||
|
||||
from ironic_inspector.common import service_utils
|
||||
from ironic_inspector import wsgi_service
|
||||
|
||||
|
||||
def main(args=sys.argv[1:]):
|
||||
# Parse config file and command line options, then start logging
|
||||
service_utils.prepare_service(args)
|
||||
|
||||
server = wsgi_service.WSGIService()
|
||||
server.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
@ -1,21 +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.
|
||||
|
||||
import oslo_i18n
|
||||
|
||||
_translators = oslo_i18n.TranslatorFactory(domain='ironic_inspector')
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
@ -1,188 +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 socket
|
||||
|
||||
from ironicclient import client
|
||||
from ironicclient import exceptions as ironic_exc
|
||||
import netaddr
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import keystone
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
# See http://specs.openstack.org/openstack/ironic-specs/specs/kilo/new-ironic-state-machine.html # noqa
|
||||
VALID_STATES = {'enroll', 'manageable', 'inspecting', 'inspect failed'}
|
||||
|
||||
# 1.19 is API version, which supports port.pxe_enabled
|
||||
DEFAULT_IRONIC_API_VERSION = '1.19'
|
||||
|
||||
IRONIC_GROUP = 'ironic'
|
||||
|
||||
IRONIC_OPTS = [
|
||||
cfg.StrOpt('os_region',
|
||||
help=_('Keystone region used to get Ironic endpoints.')),
|
||||
cfg.StrOpt('auth_strategy',
|
||||
default='keystone',
|
||||
choices=('keystone', 'noauth'),
|
||||
help=_('Method to use for authentication: noauth or '
|
||||
'keystone.')),
|
||||
cfg.StrOpt('ironic_url',
|
||||
default='http://localhost:6385/',
|
||||
help=_('Ironic API URL, used to set Ironic API URL when '
|
||||
'auth_strategy option is noauth to work with standalone '
|
||||
'Ironic without keystone.')),
|
||||
cfg.StrOpt('os_service_type',
|
||||
default='baremetal',
|
||||
help=_('Ironic service type.')),
|
||||
cfg.StrOpt('os_endpoint_type',
|
||||
default='internalURL',
|
||||
help=_('Ironic endpoint type.')),
|
||||
cfg.IntOpt('retry_interval',
|
||||
default=2,
|
||||
help=_('Interval between retries in case of conflict error '
|
||||
'(HTTP 409).')),
|
||||
cfg.IntOpt('max_retries',
|
||||
default=30,
|
||||
help=_('Maximum number of retries in case of conflict error '
|
||||
'(HTTP 409).')),
|
||||
]
|
||||
|
||||
|
||||
CONF.register_opts(IRONIC_OPTS, group=IRONIC_GROUP)
|
||||
keystone.register_auth_opts(IRONIC_GROUP)
|
||||
|
||||
IRONIC_SESSION = None
|
||||
|
||||
|
||||
class NotFound(utils.Error):
|
||||
"""Node not found in Ironic."""
|
||||
|
||||
def __init__(self, node_ident, code=404, *args, **kwargs):
|
||||
msg = _('Node %s was not found in Ironic') % node_ident
|
||||
super(NotFound, self).__init__(msg, code, *args, **kwargs)
|
||||
|
||||
|
||||
def reset_ironic_session():
|
||||
"""Reset the global session variable.
|
||||
|
||||
Mostly useful for unit tests.
|
||||
"""
|
||||
global IRONIC_SESSION
|
||||
IRONIC_SESSION = None
|
||||
|
||||
|
||||
def get_ipmi_address(node):
|
||||
ipmi_fields = ['ipmi_address'] + CONF.ipmi_address_fields
|
||||
# NOTE(sambetts): IPMI Address is useless to us if bridging is enabled so
|
||||
# just ignore it and return None
|
||||
if node.driver_info.get("ipmi_bridging", "no") != "no":
|
||||
return
|
||||
for name in ipmi_fields:
|
||||
value = node.driver_info.get(name)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
try:
|
||||
ip = socket.gethostbyname(value)
|
||||
except socket.gaierror:
|
||||
msg = _('Failed to resolve the hostname (%(value)s)'
|
||||
' for node %(uuid)s')
|
||||
raise utils.Error(msg % {'value': value,
|
||||
'uuid': node.uuid},
|
||||
node_info=node)
|
||||
|
||||
if netaddr.IPAddress(ip).is_loopback():
|
||||
LOG.warning('Ignoring loopback BMC address %s', ip,
|
||||
node_info=node)
|
||||
ip = None
|
||||
|
||||
return ip
|
||||
|
||||
|
||||
def get_client(token=None,
|
||||
api_version=DEFAULT_IRONIC_API_VERSION): # pragma: no cover
|
||||
"""Get Ironic client instance."""
|
||||
# NOTE: To support standalone ironic without keystone
|
||||
if CONF.ironic.auth_strategy == 'noauth':
|
||||
args = {'token': 'noauth',
|
||||
'endpoint': CONF.ironic.ironic_url}
|
||||
else:
|
||||
global IRONIC_SESSION
|
||||
if not IRONIC_SESSION:
|
||||
IRONIC_SESSION = keystone.get_session(IRONIC_GROUP)
|
||||
if token is None:
|
||||
args = {'session': IRONIC_SESSION,
|
||||
'region_name': CONF.ironic.os_region}
|
||||
else:
|
||||
ironic_url = IRONIC_SESSION.get_endpoint(
|
||||
service_type=CONF.ironic.os_service_type,
|
||||
endpoint_type=CONF.ironic.os_endpoint_type,
|
||||
region_name=CONF.ironic.os_region
|
||||
)
|
||||
args = {'token': token,
|
||||
'endpoint': ironic_url}
|
||||
args['os_ironic_api_version'] = api_version
|
||||
args['max_retries'] = CONF.ironic.max_retries
|
||||
args['retry_interval'] = CONF.ironic.retry_interval
|
||||
return client.Client(1, **args)
|
||||
|
||||
|
||||
def check_provision_state(node):
|
||||
state = node.provision_state.lower()
|
||||
if state not in VALID_STATES:
|
||||
msg = _('Invalid provision state for introspection: '
|
||||
'"%(state)s", valid states are "%(valid)s"')
|
||||
raise utils.Error(msg % {'state': state, 'valid': list(VALID_STATES)},
|
||||
node_info=node)
|
||||
|
||||
|
||||
def capabilities_to_dict(caps):
|
||||
"""Convert the Node's capabilities into a dictionary."""
|
||||
if not caps:
|
||||
return {}
|
||||
return dict([key.split(':', 1) for key in caps.split(',')])
|
||||
|
||||
|
||||
def dict_to_capabilities(caps_dict):
|
||||
"""Convert a dictionary into a string with the capabilities syntax."""
|
||||
return ','.join(["%s:%s" % (key, value)
|
||||
for key, value in caps_dict.items()
|
||||
if value is not None])
|
||||
|
||||
|
||||
def get_node(node_id, ironic=None, **kwargs):
|
||||
"""Get a node from Ironic.
|
||||
|
||||
:param node_id: node UUID or name.
|
||||
:param ironic: ironic client instance.
|
||||
:param kwargs: arguments to pass to Ironic client.
|
||||
:raises: Error on failure
|
||||
"""
|
||||
ironic = ironic if ironic is not None else get_client()
|
||||
|
||||
try:
|
||||
return ironic.node.get(node_id, **kwargs)
|
||||
except ironic_exc.NotFound:
|
||||
raise NotFound(node_id)
|
||||
except ironic_exc.HttpError as exc:
|
||||
raise utils.Error(_("Cannot get node %(node)s: %(exc)s") %
|
||||
{'node': node_id, 'exc': exc})
|
||||
|
||||
|
||||
def list_opts():
|
||||
return keystone.add_auth_options(IRONIC_OPTS, IRONIC_GROUP)
|
@ -1,56 +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 copy
|
||||
|
||||
from keystoneauth1 import loading
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def register_auth_opts(group):
|
||||
loading.register_session_conf_options(CONF, group)
|
||||
loading.register_auth_conf_options(CONF, group)
|
||||
CONF.set_default('auth_type', default='password', group=group)
|
||||
|
||||
|
||||
def get_session(group):
|
||||
auth = loading.load_auth_from_conf_options(CONF, group)
|
||||
session = loading.load_session_from_conf_options(
|
||||
CONF, group, auth=auth)
|
||||
return session
|
||||
|
||||
|
||||
def add_auth_options(options, group):
|
||||
|
||||
def add_options(opts, opts_to_add):
|
||||
for new_opt in opts_to_add:
|
||||
for opt in opts:
|
||||
if opt.name == new_opt.name:
|
||||
break
|
||||
else:
|
||||
opts.append(new_opt)
|
||||
|
||||
opts = copy.deepcopy(options)
|
||||
opts.insert(0, loading.get_auth_common_conf_options()[0])
|
||||
# NOTE(dims): There are a lot of auth plugins, we just generate
|
||||
# the config options for a few common ones
|
||||
plugins = ['password', 'v2password', 'v3password']
|
||||
for name in plugins:
|
||||
plugin = loading.get_plugin_loader(name)
|
||||
add_options(opts, loading.get_auth_plugin_conf_options(plugin))
|
||||
add_options(opts, loading.get_session_conf_options())
|
||||
opts.sort(key=lambda x: x.name)
|
||||
return [(group, opts)]
|
@ -1,365 +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.
|
||||
|
||||
""" Names and mapping functions used to map LLDP TLVs to name/value pairs """
|
||||
|
||||
import binascii
|
||||
|
||||
from construct import core
|
||||
import netaddr
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import lldp_tlvs as tlv
|
||||
from ironic_inspector import utils
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
|
||||
# Names used in name/value pair from parsed TLVs
|
||||
LLDP_CHASSIS_ID_NM = 'switch_chassis_id'
|
||||
LLDP_PORT_ID_NM = 'switch_port_id'
|
||||
LLDP_PORT_DESC_NM = 'switch_port_description'
|
||||
LLDP_SYS_NAME_NM = 'switch_system_name'
|
||||
LLDP_SYS_DESC_NM = 'switch_system_description'
|
||||
LLDP_SWITCH_CAP_NM = 'switch_capabilities'
|
||||
LLDP_CAP_SUPPORT_NM = 'switch_capabilities_support'
|
||||
LLDP_CAP_ENABLED_NM = 'switch_capabilities_enabled'
|
||||
LLDP_MGMT_ADDRESSES_NM = 'switch_mgmt_addresses'
|
||||
LLDP_PORT_VLANID_NM = 'switch_port_untagged_vlan_id'
|
||||
LLDP_PORT_PROT_NM = 'switch_port_protocol'
|
||||
LLDP_PORT_PROT_VLAN_ENABLED_NM = 'switch_port_protocol_vlan_enabled'
|
||||
LLDP_PORT_PROT_VLAN_SUPPORT_NM = 'switch_port_protocol_vlan_support'
|
||||
LLDP_PORT_PROT_VLANIDS_NM = 'switch_port_protocol_vlan_ids'
|
||||
LLDP_PORT_VLANS_NM = 'switch_port_vlans'
|
||||
LLDP_PROTOCOL_IDENTITIES_NM = 'switch_protocol_identities'
|
||||
LLDP_PORT_MGMT_VLANID_NM = 'switch_port_management_vlan_id'
|
||||
LLDP_PORT_LINK_AGG_NM = 'switch_port_link_aggregation'
|
||||
LLDP_PORT_LINK_AGG_ENABLED_NM = 'switch_port_link_aggregation_enabled'
|
||||
LLDP_PORT_LINK_AGG_SUPPORT_NM = 'switch_port_link_aggregation_support'
|
||||
LLDP_PORT_LINK_AGG_ID_NM = 'switch_port_link_aggregation_id'
|
||||
LLDP_PORT_MAC_PHY_NM = 'switch_port_mac_phy_config'
|
||||
LLDP_PORT_LINK_AUTONEG_ENABLED_NM = 'switch_port_autonegotiation_enabled'
|
||||
LLDP_PORT_LINK_AUTONEG_SUPPORT_NM = 'switch_port_autonegotiation_support'
|
||||
LLDP_PORT_CAPABILITIES_NM = 'switch_port_physical_capabilities'
|
||||
LLDP_PORT_MAU_TYPE_NM = 'switch_port_mau_type'
|
||||
LLDP_MTU_NM = 'switch_port_mtu'
|
||||
|
||||
|
||||
class LLDPParser(object):
|
||||
"""Base class to handle parsing of LLDP TLVs
|
||||
|
||||
Each class that inherits from this base class must provide a parser map.
|
||||
Parser maps are used to associate a LLDP TLV with a function handler
|
||||
and arguments necessary to parse the TLV and generate one or more
|
||||
name/value pairs. Each LLDP TLV maps to a tuple with the following
|
||||
fields:
|
||||
|
||||
function - handler function to generate name/value pairs
|
||||
|
||||
construct - name of construct definition for TLV
|
||||
|
||||
name - user-friendly name of TLV. For TLVs that generate only one
|
||||
name/value pair this is the name used
|
||||
|
||||
len_check - boolean indicating if length check should be done on construct
|
||||
|
||||
It's valid to have a function handler of None, this is for TLVs that
|
||||
are not mapped to a name/value pair(e.g.LLDP_TLV_TTL).
|
||||
"""
|
||||
|
||||
def __init__(self, node_info, nv=None):
|
||||
"""Create LLDPParser
|
||||
|
||||
:param node_info - node being introspected
|
||||
:param nv - dictionary of name/value pairs to use
|
||||
"""
|
||||
self.nv_dict = nv or {}
|
||||
self.node_info = node_info
|
||||
self.parser_map = {}
|
||||
|
||||
def set_value(self, name, value):
|
||||
"""Set name value pair in dictionary
|
||||
|
||||
The value for a name should not be changed if it exists.
|
||||
"""
|
||||
self.nv_dict.setdefault(name, value)
|
||||
|
||||
def append_value(self, name, value):
|
||||
"""Add value to a list mapped to name"""
|
||||
self.nv_dict.setdefault(name, []).append(value)
|
||||
|
||||
def add_single_value(self, struct, name, data):
|
||||
"""Add a single name/value pair the the nv dict"""
|
||||
self.set_value(name, struct.value)
|
||||
|
||||
def parse_tlv(self, tlv_type, data):
|
||||
"""Parse TLVs from mapping table
|
||||
|
||||
This functions takes the TLV type and the raw data for
|
||||
this TLV and gets a tuple from the parser_map. The
|
||||
construct field in the tuple contains the construct lib
|
||||
definition of the TLV which can be parsed to access
|
||||
individual fields. Once the TLV is parsed, the handler
|
||||
function for each TLV will store the individual fields as
|
||||
name/value pairs in nv_dict.
|
||||
|
||||
If the handler function does not exist, then no name/value pairs
|
||||
will be added to nv_dict, but since the TLV was handled,
|
||||
True will be returned.
|
||||
|
||||
:param: tlv_type - type identifier for TLV
|
||||
:param: data - raw TLV value
|
||||
:returns: True if TLV in parser_map and data is valid, otherwise False.
|
||||
"""
|
||||
|
||||
s = self.parser_map.get(tlv_type)
|
||||
if not s:
|
||||
return False
|
||||
|
||||
func = s[0] # handler
|
||||
|
||||
if not func:
|
||||
return True # TLV is handled
|
||||
|
||||
try:
|
||||
tlv_parser = s[1]
|
||||
name = s[2]
|
||||
check_len = s[3]
|
||||
except KeyError as e:
|
||||
LOG.warning("Key error in TLV table: %s", e,
|
||||
node_info=self.node_info)
|
||||
return False
|
||||
|
||||
# Some constructs require a length validation to ensure the
|
||||
# proper number of bytes has been provided, for example
|
||||
# when a BitStruct is used.
|
||||
if check_len and (tlv_parser.sizeof() != len(data)):
|
||||
LOG.warning("Invalid data for %(name)s expected len %(expect)d, "
|
||||
"got %(actual)d", {'name': name,
|
||||
'expect': tlv_parser.sizeof(),
|
||||
'actual': len(data)},
|
||||
node_info=self.node_info)
|
||||
return False
|
||||
|
||||
# Use the construct parser to parse TLV so that it's
|
||||
# individual fields can be accessed
|
||||
try:
|
||||
struct = tlv_parser.parse(data)
|
||||
except (core.RangeError, core.FieldError, core.MappingError,
|
||||
netaddr.AddrFormatError) as e:
|
||||
LOG.warning("TLV parse error: %s", e,
|
||||
node_info=self.node_info)
|
||||
return False
|
||||
|
||||
# Call functions with parsed structure
|
||||
try:
|
||||
func(struct, name, data)
|
||||
except ValueError as e:
|
||||
LOG.warning("TLV value error: %s", e,
|
||||
node_info=self.node_info)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def add_dot1_link_aggregation(self, struct, name, data):
|
||||
"""Add name/value pairs for TLV Dot1_LinkAggregationId
|
||||
|
||||
This is in base class since it can be used by both dot1 and dot3.
|
||||
"""
|
||||
|
||||
self.set_value(LLDP_PORT_LINK_AGG_ENABLED_NM,
|
||||
struct.status.enabled)
|
||||
self.set_value(LLDP_PORT_LINK_AGG_SUPPORT_NM,
|
||||
struct.status.supported)
|
||||
self.set_value(LLDP_PORT_LINK_AGG_ID_NM, struct.portid)
|
||||
|
||||
|
||||
class LLDPBasicMgmtParser(LLDPParser):
|
||||
"""Class to handle parsing of 802.1AB Basic Management set
|
||||
|
||||
This class will also handle 802.1Q and 802.3 OUI TLVs.
|
||||
"""
|
||||
def __init__(self, nv=None):
|
||||
super(LLDPBasicMgmtParser, self).__init__(nv)
|
||||
|
||||
self.parser_map = {
|
||||
tlv.LLDP_TLV_CHASSIS_ID:
|
||||
(self.add_single_value, tlv.ChassisId,
|
||||
LLDP_CHASSIS_ID_NM, False),
|
||||
tlv.LLDP_TLV_PORT_ID:
|
||||
(self.add_single_value, tlv.PortId, LLDP_PORT_ID_NM, False),
|
||||
tlv.LLDP_TLV_TTL: (None, None, None, False),
|
||||
tlv.LLDP_TLV_PORT_DESCRIPTION:
|
||||
(self.add_single_value, tlv.PortDesc, LLDP_PORT_DESC_NM,
|
||||
False),
|
||||
tlv.LLDP_TLV_SYS_NAME:
|
||||
(self.add_single_value, tlv.SysName, LLDP_SYS_NAME_NM, False),
|
||||
tlv.LLDP_TLV_SYS_DESCRIPTION:
|
||||
(self.add_single_value, tlv.SysDesc, LLDP_SYS_DESC_NM, False),
|
||||
tlv.LLDP_TLV_SYS_CAPABILITIES:
|
||||
(self.add_capabilities, tlv.SysCapabilities,
|
||||
LLDP_SWITCH_CAP_NM, True),
|
||||
tlv.LLDP_TLV_MGMT_ADDRESS:
|
||||
(self.add_mgmt_address, tlv.MgmtAddress,
|
||||
LLDP_MGMT_ADDRESSES_NM, False),
|
||||
tlv.LLDP_TLV_ORG_SPECIFIC:
|
||||
(self.handle_org_specific_tlv, tlv.OrgSpecific, None, False),
|
||||
tlv.LLDP_TLV_END_LLDPPDU: (None, None, None, False)
|
||||
}
|
||||
|
||||
def add_mgmt_address(self, struct, name, data):
|
||||
"""Handle LLDP_TLV_MGMT_ADDRESS
|
||||
|
||||
There can be multiple Mgmt Address TLVs, store in list.
|
||||
"""
|
||||
self.append_value(name, struct.address)
|
||||
|
||||
def _get_capabilities_list(self, caps):
|
||||
"""Get capabilities from bit map"""
|
||||
cap_map = [
|
||||
(caps.repeater, 'Repeater'),
|
||||
(caps.bridge, 'Bridge'),
|
||||
(caps.wlan, 'WLAN'),
|
||||
(caps.router, 'Router'),
|
||||
(caps.telephone, 'Telephone'),
|
||||
(caps.docsis, 'DOCSIS cable device'),
|
||||
(caps.station, 'Station only'),
|
||||
(caps.cvlan, 'C-Vlan'),
|
||||
(caps.svlan, 'S-Vlan'),
|
||||
(caps.tpmr, 'TPMR')]
|
||||
|
||||
return [cap for (bit, cap) in cap_map if bit]
|
||||
|
||||
def add_capabilities(self, struct, name, data):
|
||||
"""Handle LLDP_TLV_SYS_CAPABILITIES"""
|
||||
self.set_value(LLDP_CAP_SUPPORT_NM,
|
||||
self._get_capabilities_list(struct.system))
|
||||
self.set_value(LLDP_CAP_ENABLED_NM,
|
||||
self._get_capabilities_list(struct.enabled))
|
||||
|
||||
def handle_org_specific_tlv(self, struct, name, data):
|
||||
"""Handle Organizationally Unique ID TLVs
|
||||
|
||||
This class supports 802.1Q and 802.3 OUI TLVs.
|
||||
|
||||
See http://www.ieee802.org/1/pages/802.1Q-2014.html, Annex D
|
||||
and http://standards.ieee.org/about/get/802/802.3.html
|
||||
"""
|
||||
oui = binascii.hexlify(struct.oui).decode()
|
||||
subtype = struct.subtype
|
||||
oui_data = data[4:]
|
||||
|
||||
if oui == tlv.LLDP_802dot1_OUI:
|
||||
parser = LLDPdot1Parser(self.node_info, self.nv_dict)
|
||||
if parser.parse_tlv(subtype, oui_data):
|
||||
LOG.debug("Handled 802.1 subtype %d", subtype)
|
||||
else:
|
||||
LOG.debug("Subtype %d not found for 802.1", subtype)
|
||||
elif oui == tlv.LLDP_802dot3_OUI:
|
||||
parser = LLDPdot3Parser(self.node_info, self.nv_dict)
|
||||
if parser.parse_tlv(subtype, oui_data):
|
||||
LOG.debug("Handled 802.3 subtype %d", subtype)
|
||||
else:
|
||||
LOG.debug("Subtype %d not found for 802.3", subtype)
|
||||
else:
|
||||
LOG.warning("Organizationally Unique ID %s not "
|
||||
"recognized", oui, node_info=self.node_info)
|
||||
|
||||
|
||||
class LLDPdot1Parser(LLDPParser):
|
||||
"""Class to handle parsing of 802.1Q TLVs"""
|
||||
def __init__(self, node_info, nv=None):
|
||||
super(LLDPdot1Parser, self).__init__(node_info, nv)
|
||||
|
||||
self.parser_map = {
|
||||
tlv.dot1_PORT_VLANID:
|
||||
(self.add_single_value, tlv.Dot1_UntaggedVlanId,
|
||||
LLDP_PORT_VLANID_NM, False),
|
||||
tlv.dot1_PORT_PROTOCOL_VLANID:
|
||||
(self.add_dot1_port_protocol_vlan, tlv.Dot1_PortProtocolVlan,
|
||||
LLDP_PORT_PROT_NM, True),
|
||||
tlv.dot1_VLAN_NAME:
|
||||
(self.add_dot1_vlans, tlv.Dot1_VlanName, None, False),
|
||||
tlv.dot1_PROTOCOL_IDENTITY:
|
||||
(self.add_dot1_protocol_identities, tlv.Dot1_ProtocolIdentity,
|
||||
LLDP_PROTOCOL_IDENTITIES_NM, False),
|
||||
tlv.dot1_MANAGEMENT_VID:
|
||||
(self.add_single_value, tlv.Dot1_MgmtVlanId,
|
||||
LLDP_PORT_MGMT_VLANID_NM, False),
|
||||
tlv.dot1_LINK_AGGREGATION:
|
||||
(self.add_dot1_link_aggregation, tlv.Dot1_LinkAggregationId,
|
||||
LLDP_PORT_LINK_AGG_NM, True)
|
||||
}
|
||||
|
||||
def add_dot1_port_protocol_vlan(self, struct, name, data):
|
||||
"""Handle dot1_PORT_PROTOCOL_VLANID"""
|
||||
self.set_value(LLDP_PORT_PROT_VLAN_ENABLED_NM, struct.flags.enabled)
|
||||
self.set_value(LLDP_PORT_PROT_VLAN_SUPPORT_NM, struct.flags.supported)
|
||||
|
||||
# There can be multiple port/protocol vlans TLVs, store in list
|
||||
self.append_value(LLDP_PORT_PROT_VLANIDS_NM, struct.vlanid)
|
||||
|
||||
def add_dot1_vlans(self, struct, name, data):
|
||||
"""Handle dot1_VLAN_NAME
|
||||
|
||||
There can be multiple vlan TLVs, add dictionary entry with id/vlan
|
||||
to list.
|
||||
"""
|
||||
vlan_dict = {}
|
||||
vlan_dict['name'] = struct.vlan_name
|
||||
vlan_dict['id'] = struct.vlanid
|
||||
self.append_value(LLDP_PORT_VLANS_NM, vlan_dict)
|
||||
|
||||
def add_dot1_protocol_identities(self, struct, name, data):
|
||||
"""Handle dot1_PROTOCOL_IDENTITY
|
||||
|
||||
There can be multiple protocol ids TLVs, store in list
|
||||
"""
|
||||
self.append_value(LLDP_PROTOCOL_IDENTITIES_NM,
|
||||
binascii.b2a_hex(struct.protocol).decode())
|
||||
|
||||
|
||||
class LLDPdot3Parser(LLDPParser):
|
||||
"""Class to handle parsing of 802.3 TLVs"""
|
||||
def __init__(self, node_info, nv=None):
|
||||
super(LLDPdot3Parser, self).__init__(node_info, nv)
|
||||
|
||||
# Note that 802.3 link Aggregation has been deprecated and moved to
|
||||
# 802.1 spec, but it is in the same format. Use the same function as
|
||||
# dot1 handler.
|
||||
self.parser_map = {
|
||||
tlv.dot3_MACPHY_CONFIG_STATUS:
|
||||
(self.add_dot3_macphy_config, tlv.Dot3_MACPhy_Config_Status,
|
||||
LLDP_PORT_MAC_PHY_NM, True),
|
||||
tlv.dot3_LINK_AGGREGATION:
|
||||
(self.add_dot1_link_aggregation, tlv.Dot1_LinkAggregationId,
|
||||
LLDP_PORT_LINK_AGG_NM, True),
|
||||
tlv.dot3_MTU:
|
||||
(self.add_single_value, tlv.Dot3_MTU, LLDP_MTU_NM, False)
|
||||
}
|
||||
|
||||
def add_dot3_macphy_config(self, struct, name, data):
|
||||
"""Handle dot3_MACPHY_CONFIG_STATUS"""
|
||||
|
||||
try:
|
||||
mau_type = tlv.OPER_MAU_TYPES[struct.mau_type]
|
||||
except KeyError:
|
||||
raise ValueError(_('Invalid index for mau type'))
|
||||
|
||||
self.set_value(LLDP_PORT_LINK_AUTONEG_ENABLED_NM,
|
||||
struct.autoneg.enabled)
|
||||
self.set_value(LLDP_PORT_LINK_AUTONEG_SUPPORT_NM,
|
||||
struct.autoneg.supported)
|
||||
self.set_value(LLDP_PORT_CAPABILITIES_NM,
|
||||
tlv.get_autoneg_cap(struct.pmd_autoneg))
|
||||
self.set_value(LLDP_PORT_MAU_TYPE_NM, mau_type)
|
@ -1,366 +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.
|
||||
|
||||
""" Link Layer Discovery Protocol TLVs """
|
||||
|
||||
import functools
|
||||
|
||||
# See http://construct.readthedocs.io/en/latest/index.html
|
||||
import construct
|
||||
from construct import core
|
||||
import netaddr
|
||||
|
||||
from ironic_inspector import utils
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
# Constants defined according to 802.1AB-2016 LLDP spec
|
||||
# https://standards.ieee.org/findstds/standard/802.1AB-2016.html
|
||||
|
||||
# TLV types
|
||||
LLDP_TLV_END_LLDPPDU = 0
|
||||
LLDP_TLV_CHASSIS_ID = 1
|
||||
LLDP_TLV_PORT_ID = 2
|
||||
LLDP_TLV_TTL = 3
|
||||
LLDP_TLV_PORT_DESCRIPTION = 4
|
||||
LLDP_TLV_SYS_NAME = 5
|
||||
LLDP_TLV_SYS_DESCRIPTION = 6
|
||||
LLDP_TLV_SYS_CAPABILITIES = 7
|
||||
LLDP_TLV_MGMT_ADDRESS = 8
|
||||
LLDP_TLV_ORG_SPECIFIC = 127
|
||||
|
||||
# 802.1Q defines from http://www.ieee802.org/1/pages/802.1Q-2014.html, Annex D
|
||||
LLDP_802dot1_OUI = "0080c2"
|
||||
# subtypes
|
||||
dot1_PORT_VLANID = 1
|
||||
dot1_PORT_PROTOCOL_VLANID = 2
|
||||
dot1_VLAN_NAME = 3
|
||||
dot1_PROTOCOL_IDENTITY = 4
|
||||
dot1_MANAGEMENT_VID = 6
|
||||
dot1_LINK_AGGREGATION = 7
|
||||
|
||||
# 802.3 defines from http://standards.ieee.org/about/get/802/802.3.html,
|
||||
# section 79
|
||||
LLDP_802dot3_OUI = "00120f"
|
||||
# Subtypes
|
||||
dot3_MACPHY_CONFIG_STATUS = 1
|
||||
dot3_LINK_AGGREGATION = 3 # Deprecated, but still in use
|
||||
dot3_MTU = 4
|
||||
|
||||
|
||||
def bytes_to_int(obj):
|
||||
"""Convert bytes to an integer
|
||||
|
||||
:param: obj - array of bytes
|
||||
"""
|
||||
return functools.reduce(lambda x, y: x << 8 | y, obj)
|
||||
|
||||
|
||||
def mapping_for_enum(mapping):
|
||||
"""Return tuple used for keys as a dict
|
||||
|
||||
:param: mapping - dict with tuple as keys
|
||||
"""
|
||||
return dict(mapping.keys())
|
||||
|
||||
|
||||
def mapping_for_switch(mapping):
|
||||
"""Return dict from values
|
||||
|
||||
:param: mapping - dict with tuple as keys
|
||||
"""
|
||||
return {key[0]: value for key, value in mapping.items()}
|
||||
|
||||
|
||||
IPv4Address = core.ExprAdapter(
|
||||
core.Byte[4],
|
||||
encoder=lambda obj, ctx: netaddr.IPAddress(obj).words,
|
||||
decoder=lambda obj, ctx: str(netaddr.IPAddress(bytes_to_int(obj)))
|
||||
)
|
||||
|
||||
IPv6Address = core.ExprAdapter(
|
||||
core.Byte[16],
|
||||
encoder=lambda obj, ctx: netaddr.IPAddress(obj).words,
|
||||
decoder=lambda obj, ctx: str(netaddr.IPAddress(bytes_to_int(obj)))
|
||||
)
|
||||
|
||||
MACAddress = core.ExprAdapter(
|
||||
core.Byte[6],
|
||||
encoder=lambda obj, ctx: netaddr.EUI(obj).words,
|
||||
decoder=lambda obj, ctx: str(netaddr.EUI(bytes_to_int(obj),
|
||||
dialect=netaddr.mac_unix_expanded))
|
||||
)
|
||||
|
||||
IANA_ADDRESS_FAMILY_ID_MAPPING = {
|
||||
('ipv4', 1): IPv4Address,
|
||||
('ipv6', 2): IPv6Address,
|
||||
('mac', 6): MACAddress,
|
||||
}
|
||||
|
||||
IANAAddress = core.Embedded(core.Struct(
|
||||
'family' / core.Enum(core.Int8ub, **mapping_for_enum(
|
||||
IANA_ADDRESS_FAMILY_ID_MAPPING)),
|
||||
'value' / core.Switch(construct.this.family, mapping_for_switch(
|
||||
IANA_ADDRESS_FAMILY_ID_MAPPING))))
|
||||
|
||||
# Note that 'GreedyString()' is used in cases where string len is not defined
|
||||
CHASSIS_ID_MAPPING = {
|
||||
('entPhysAlias_c', 1): core.Struct('value' / core.GreedyString("utf8")),
|
||||
('ifAlias', 2): core.Struct('value' / core.GreedyString("utf8")),
|
||||
('entPhysAlias_p', 3): core.Struct('value' / core.GreedyString("utf8")),
|
||||
('mac_address', 4): core.Struct('value' / MACAddress),
|
||||
('IANA_address', 5): IANAAddress,
|
||||
('ifName', 6): core.Struct('value' / core.GreedyString("utf8")),
|
||||
('local', 7): core.Struct('value' / core.GreedyString("utf8"))
|
||||
}
|
||||
|
||||
#
|
||||
# Basic Management Set TLV field definitions
|
||||
#
|
||||
|
||||
# Chassis ID value is based on the subtype
|
||||
ChassisId = core.Struct(
|
||||
'subtype' / core.Enum(core.Byte, **mapping_for_enum(
|
||||
CHASSIS_ID_MAPPING)),
|
||||
'value' /
|
||||
core.Embedded(core.Switch(construct.this.subtype,
|
||||
mapping_for_switch(CHASSIS_ID_MAPPING)))
|
||||
)
|
||||
|
||||
PORT_ID_MAPPING = {
|
||||
('ifAlias', 1): core.Struct('value' / core.GreedyString("utf8")),
|
||||
('entPhysicalAlias', 2): core.Struct('value' / core.GreedyString("utf8")),
|
||||
('mac_address', 3): core.Struct('value' / MACAddress),
|
||||
('IANA_address', 4): IANAAddress,
|
||||
('ifName', 5): core.Struct('value' / core.GreedyString("utf8")),
|
||||
('local', 7): core.Struct('value' / core.GreedyString("utf8"))
|
||||
}
|
||||
|
||||
# Port ID value is based on the subtype
|
||||
PortId = core.Struct(
|
||||
'subtype' / core.Enum(core.Byte, **mapping_for_enum(
|
||||
PORT_ID_MAPPING)),
|
||||
'value' /
|
||||
core.Embedded(core.Switch(construct.this.subtype,
|
||||
mapping_for_switch(PORT_ID_MAPPING)))
|
||||
)
|
||||
|
||||
PortDesc = core.Struct('value' / core.GreedyString("utf8"))
|
||||
|
||||
SysName = core.Struct('value' / core.GreedyString("utf8"))
|
||||
|
||||
SysDesc = core.Struct('value' / core.GreedyString("utf8"))
|
||||
|
||||
MgmtAddress = core.Struct(
|
||||
'len' / core.Int8ub,
|
||||
'family' / core.Enum(core.Int8ub, **mapping_for_enum(
|
||||
IANA_ADDRESS_FAMILY_ID_MAPPING)),
|
||||
'address' / core.Switch(construct.this.family, mapping_for_switch(
|
||||
IANA_ADDRESS_FAMILY_ID_MAPPING))
|
||||
)
|
||||
|
||||
Capabilities = core.BitStruct(
|
||||
core.Padding(5),
|
||||
'tpmr' / core.Bit,
|
||||
'svlan' / core.Bit,
|
||||
'cvlan' / core.Bit,
|
||||
'station' / core.Bit,
|
||||
'docsis' / core.Bit,
|
||||
'telephone' / core.Bit,
|
||||
'router' / core.Bit,
|
||||
'wlan' / core.Bit,
|
||||
'bridge' / core.Bit,
|
||||
'repeater' / core.Bit,
|
||||
core.Padding(1)
|
||||
)
|
||||
|
||||
SysCapabilities = core.Struct(
|
||||
'system' / Capabilities,
|
||||
'enabled' / Capabilities
|
||||
)
|
||||
|
||||
OrgSpecific = core.Struct(
|
||||
'oui' / core.Bytes(3),
|
||||
'subtype' / core.Int8ub
|
||||
)
|
||||
|
||||
#
|
||||
# 802.1Q TLV field definitions
|
||||
# See http://www.ieee802.org/1/pages/802.1Q-2014.html, Annex D
|
||||
#
|
||||
|
||||
Dot1_UntaggedVlanId = core.Struct('value' / core.Int16ub)
|
||||
|
||||
Dot1_PortProtocolVlan = core.Struct(
|
||||
'flags' / core.BitStruct(
|
||||
core.Padding(5),
|
||||
'enabled' / core.Flag,
|
||||
'supported' / core.Flag,
|
||||
core.Padding(1),
|
||||
),
|
||||
'vlanid' / core.Int16ub
|
||||
)
|
||||
|
||||
Dot1_VlanName = core.Struct(
|
||||
'vlanid' / core.Int16ub,
|
||||
'name_len' / core.Rebuild(core.Int8ub,
|
||||
construct.len_(construct.this.value)),
|
||||
'vlan_name' / core.String(construct.this.name_len, "utf8")
|
||||
)
|
||||
|
||||
Dot1_ProtocolIdentity = core.Struct(
|
||||
'len' / core.Rebuild(core.Int8ub, construct.len_(construct.this.value)),
|
||||
'protocol' / core.Bytes(construct.this.len)
|
||||
)
|
||||
|
||||
Dot1_MgmtVlanId = core.Struct('value' / core.Int16ub)
|
||||
|
||||
Dot1_LinkAggregationId = core.Struct(
|
||||
'status' / core.BitStruct(
|
||||
core.Padding(6),
|
||||
'enabled' / core.Flag,
|
||||
'supported' / core.Flag
|
||||
),
|
||||
'portid' / core.Int32ub
|
||||
)
|
||||
|
||||
#
|
||||
# 802.3 TLV field definitions
|
||||
# See http://standards.ieee.org/about/get/802/802.3.html,
|
||||
# section 79
|
||||
#
|
||||
|
||||
|
||||
def get_autoneg_cap(pmd):
|
||||
"""Get autonegotiated capability strings
|
||||
|
||||
This returns a list of capability strings from the Physical Media
|
||||
Dependent (PMD) capability bits.
|
||||
|
||||
:param pmd: PMD bits
|
||||
:return: Sorted ist containing capability strings
|
||||
"""
|
||||
caps_set = set()
|
||||
|
||||
pmd_map = [
|
||||
(pmd._10base_t_hdx, '10BASE-T hdx'),
|
||||
(pmd._10base_t_hdx, '10BASE-T fdx'),
|
||||
(pmd._10base_t4, '10BASE-T4'),
|
||||
(pmd._100base_tx_hdx, '100BASE-TX hdx'),
|
||||
(pmd._100base_tx_fdx, '100BASE-TX fdx'),
|
||||
(pmd._100base_t2_hdx, '100BASE-T2 hdx'),
|
||||
(pmd._100base_t2_fdx, '100BASE-T2 fdx'),
|
||||
(pmd.pause_fdx, 'PAUSE fdx'),
|
||||
(pmd.asym_pause, 'Asym PAUSE fdx'),
|
||||
(pmd.sym_pause, 'Sym PAUSE fdx'),
|
||||
(pmd.asym_sym_pause, 'Asym and Sym PAUSE fdx'),
|
||||
(pmd._1000base_x_hdx, '1000BASE-X hdx'),
|
||||
(pmd._1000base_x_fdx, '1000BASE-X fdx'),
|
||||
(pmd._1000base_t_hdx, '1000BASE-T hdx'),
|
||||
(pmd._1000base_t_fdx, '1000BASE-T fdx')]
|
||||
|
||||
for bit, cap in pmd_map:
|
||||
if bit:
|
||||
caps_set.add(cap)
|
||||
|
||||
return sorted(caps_set)
|
||||
|
||||
Dot3_MACPhy_Config_Status = core.Struct(
|
||||
'autoneg' / core.BitStruct(
|
||||
core.Padding(6),
|
||||
'enabled' / core.Flag,
|
||||
'supported' / core.Flag,
|
||||
),
|
||||
# See IANAifMauAutoNegCapBits
|
||||
# RFC 4836, Definitions of Managed Objects for IEEE 802.3
|
||||
'pmd_autoneg' / core.BitStruct(
|
||||
core.Padding(1),
|
||||
'_10base_t_hdx' / core.Bit,
|
||||
'_10base_t_fdx' / core.Bit,
|
||||
'_10base_t4' / core.Bit,
|
||||
'_100base_tx_hdx' / core.Bit,
|
||||
'_100base_tx_fdx' / core.Bit,
|
||||
'_100base_t2_hdx' / core.Bit,
|
||||
'_100base_t2_fdx' / core.Bit,
|
||||
'pause_fdx' / core.Bit,
|
||||
'asym_pause' / core.Bit,
|
||||
'sym_pause' / core.Bit,
|
||||
'asym_sym_pause' / core.Bit,
|
||||
'_1000base_x_hdx' / core.Bit,
|
||||
'_1000base_x_fdx' / core.Bit,
|
||||
'_1000base_t_hdx' / core.Bit,
|
||||
'_1000base_t_fdx' / core.Bit
|
||||
),
|
||||
'mau_type' / core.Int16ub
|
||||
)
|
||||
|
||||
# See ifMauTypeList in
|
||||
# RFC 4836, Definitions of Managed Objects for IEEE 802.3
|
||||
OPER_MAU_TYPES = {
|
||||
0: "Unknown",
|
||||
1: "AUI",
|
||||
2: "10BASE-5",
|
||||
3: "FOIRL",
|
||||
4: "10BASE-2",
|
||||
5: "10BASE-T duplex mode unknown",
|
||||
6: "10BASE-FP",
|
||||
7: "10BASE-FB",
|
||||
8: "10BASE-FL duplex mode unknown",
|
||||
9: "10BROAD36",
|
||||
10: "10BASE-T half duplex",
|
||||
11: "10BASE-T full duplex",
|
||||
12: "10BASE-FL half duplex",
|
||||
13: "10BASE-FL full duplex",
|
||||
14: "100 BASE-T4",
|
||||
15: "100BASE-TX half duplex",
|
||||
16: "100BASE-TX full duplex",
|
||||
17: "100BASE-FX half duplex",
|
||||
18: "100BASE-FX full duplex",
|
||||
19: "100BASE-T2 half duplex",
|
||||
20: "100BASE-T2 full duplex",
|
||||
21: "1000BASE-X half duplex",
|
||||
22: "1000BASE-X full duplex",
|
||||
23: "1000BASE-LX half duplex",
|
||||
24: "1000BASE-LX full duplex",
|
||||
25: "1000BASE-SX half duplex",
|
||||
26: "1000BASE-SX full duplex",
|
||||
27: "1000BASE-CX half duplex",
|
||||
28: "1000BASE-CX full duplex",
|
||||
29: "1000BASE-T half duplex",
|
||||
30: "1000BASE-T full duplex",
|
||||
31: "10GBASE-X",
|
||||
32: "10GBASE-LX4",
|
||||
33: "10GBASE-R",
|
||||
34: "10GBASE-ER",
|
||||
35: "10GBASE-LR",
|
||||
36: "10GBASE-SR",
|
||||
37: "10GBASE-W",
|
||||
38: "10GBASE-EW",
|
||||
39: "10GBASE-LW",
|
||||
40: "10GBASE-SW",
|
||||
41: "10GBASE-CX4",
|
||||
42: "2BASE-TL",
|
||||
43: "10PASS-TS",
|
||||
44: "100BASE-BX10D",
|
||||
45: "100BASE-BX10U",
|
||||
46: "100BASE-LX10",
|
||||
47: "1000BASE-BX10D",
|
||||
48: "1000BASE-BX10U",
|
||||
49: "1000BASE-LX10",
|
||||
50: "1000BASE-PX10D",
|
||||
51: "1000BASE-PX10U",
|
||||
52: "1000BASE-PX20D",
|
||||
53: "1000BASE-PX20U",
|
||||
}
|
||||
|
||||
Dot3_MTU = core.Struct('value' / core.Int16ub)
|
@ -1,35 +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 oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def prepare_service(args):
|
||||
log.register_options(CONF)
|
||||
log.set_defaults(default_log_levels=['sqlalchemy=WARNING',
|
||||
'iso8601=WARNING',
|
||||
'requests=WARNING',
|
||||
'urllib3.connectionpool=WARNING',
|
||||
'keystonemiddleware=WARNING',
|
||||
'swiftclient=WARNING',
|
||||
'keystoneauth=WARNING',
|
||||
'ironicclient=WARNING'])
|
||||
CONF(args, project='ironic-inspector')
|
||||
log.setup(CONF, 'ironic_inspector')
|
||||
|
||||
LOG.debug("Configuration:")
|
||||
CONF.log_opt_values(LOG, log.DEBUG)
|
@ -1,172 +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.
|
||||
|
||||
# Mostly copied from ironic/common/swift.py
|
||||
|
||||
import json
|
||||
|
||||
from oslo_config import cfg
|
||||
from swiftclient import client as swift_client
|
||||
from swiftclient import exceptions as swift_exceptions
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import keystone
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
SWIFT_GROUP = 'swift'
|
||||
SWIFT_OPTS = [
|
||||
cfg.IntOpt('max_retries',
|
||||
default=2,
|
||||
help=_('Maximum number of times to retry a Swift request, '
|
||||
'before failing.')),
|
||||
cfg.IntOpt('delete_after',
|
||||
default=0,
|
||||
help=_('Number of seconds that the Swift object will last '
|
||||
'before being deleted. (set to 0 to never delete the '
|
||||
'object).')),
|
||||
cfg.StrOpt('container',
|
||||
default='ironic-inspector',
|
||||
help=_('Default Swift container to use when creating '
|
||||
'objects.')),
|
||||
cfg.StrOpt('os_service_type',
|
||||
default='object-store',
|
||||
help=_('Swift service type.')),
|
||||
cfg.StrOpt('os_endpoint_type',
|
||||
default='internalURL',
|
||||
help=_('Swift endpoint type.')),
|
||||
cfg.StrOpt('os_region',
|
||||
help=_('Keystone region to get endpoint for.')),
|
||||
]
|
||||
|
||||
CONF.register_opts(SWIFT_OPTS, group=SWIFT_GROUP)
|
||||
keystone.register_auth_opts(SWIFT_GROUP)
|
||||
|
||||
OBJECT_NAME_PREFIX = 'inspector_data'
|
||||
SWIFT_SESSION = None
|
||||
|
||||
|
||||
def reset_swift_session():
|
||||
"""Reset the global session variable.
|
||||
|
||||
Mostly useful for unit tests.
|
||||
"""
|
||||
global SWIFT_SESSION
|
||||
SWIFT_SESSION = None
|
||||
|
||||
|
||||
class SwiftAPI(object):
|
||||
"""API for communicating with Swift."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor for creating a SwiftAPI object.
|
||||
|
||||
Authentification is loaded from config file.
|
||||
"""
|
||||
global SWIFT_SESSION
|
||||
if not SWIFT_SESSION:
|
||||
SWIFT_SESSION = keystone.get_session(SWIFT_GROUP)
|
||||
|
||||
self.connection = swift_client.Connection(session=SWIFT_SESSION)
|
||||
|
||||
def create_object(self, object, data, container=CONF.swift.container,
|
||||
headers=None):
|
||||
"""Uploads a given string to Swift.
|
||||
|
||||
:param object: The name of the object in Swift
|
||||
:param data: string data to put in the object
|
||||
:param container: The name of the container for the object.
|
||||
:param headers: the headers for the object to pass to Swift
|
||||
:returns: The Swift UUID of the object
|
||||
:raises: utils.Error, if any operation with Swift fails.
|
||||
"""
|
||||
try:
|
||||
self.connection.put_container(container)
|
||||
except swift_exceptions.ClientException as e:
|
||||
err_msg = (_('Swift failed to create container %(container)s. '
|
||||
'Error was: %(error)s') %
|
||||
{'container': container, 'error': e})
|
||||
raise utils.Error(err_msg)
|
||||
|
||||
if CONF.swift.delete_after > 0:
|
||||
headers = headers or {}
|
||||
headers['X-Delete-After'] = CONF.swift.delete_after
|
||||
|
||||
try:
|
||||
obj_uuid = self.connection.put_object(container,
|
||||
object,
|
||||
data,
|
||||
headers=headers)
|
||||
except swift_exceptions.ClientException as e:
|
||||
err_msg = (_('Swift failed to create object %(object)s in '
|
||||
'container %(container)s. Error was: %(error)s') %
|
||||
{'object': object, 'container': container, 'error': e})
|
||||
raise utils.Error(err_msg)
|
||||
|
||||
return obj_uuid
|
||||
|
||||
def get_object(self, object, container=CONF.swift.container):
|
||||
"""Downloads a given object from Swift.
|
||||
|
||||
:param object: The name of the object in Swift
|
||||
:param container: The name of the container for the object.
|
||||
:returns: Swift object
|
||||
:raises: utils.Error, if the Swift operation fails.
|
||||
"""
|
||||
try:
|
||||
headers, obj = self.connection.get_object(container, object)
|
||||
except swift_exceptions.ClientException as e:
|
||||
err_msg = (_('Swift failed to get object %(object)s in '
|
||||
'container %(container)s. Error was: %(error)s') %
|
||||
{'object': object, 'container': container, 'error': e})
|
||||
raise utils.Error(err_msg)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def store_introspection_data(data, uuid, suffix=None):
|
||||
"""Uploads introspection data to Swift.
|
||||
|
||||
:param data: data to store in Swift
|
||||
:param uuid: UUID of the Ironic node that the data came from
|
||||
:param suffix: optional suffix to add to the underlying swift
|
||||
object name
|
||||
:returns: name of the Swift object that the data is stored in
|
||||
"""
|
||||
swift_api = SwiftAPI()
|
||||
swift_object_name = '%s-%s' % (OBJECT_NAME_PREFIX, uuid)
|
||||
if suffix is not None:
|
||||
swift_object_name = '%s-%s' % (swift_object_name, suffix)
|
||||
swift_api.create_object(swift_object_name, json.dumps(data))
|
||||
return swift_object_name
|
||||
|
||||
|
||||
def get_introspection_data(uuid, suffix=None):
|
||||
"""Downloads introspection data from Swift.
|
||||
|
||||
:param uuid: UUID of the Ironic node that the data came from
|
||||
:param suffix: optional suffix to add to the underlying swift
|
||||
object name
|
||||
:returns: Swift object with the introspection data
|
||||
"""
|
||||
swift_api = SwiftAPI()
|
||||
swift_object_name = '%s-%s' % (OBJECT_NAME_PREFIX, uuid)
|
||||
if suffix is not None:
|
||||
swift_object_name = '%s-%s' % (swift_object_name, suffix)
|
||||
return swift_api.get_object(swift_object_name)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return keystone.add_auth_options(SWIFT_OPTS, SWIFT_GROUP)
|
@ -1,241 +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 oslo_config import cfg
|
||||
from oslo_middleware import cors
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
|
||||
|
||||
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'
|
||||
|
||||
VALID_ADD_PORTS_VALUES = ('all', 'active', 'pxe', 'disabled')
|
||||
VALID_KEEP_PORTS_VALUES = ('all', 'present', 'added')
|
||||
VALID_STORE_DATA_VALUES = ('none', 'swift')
|
||||
|
||||
|
||||
FIREWALL_OPTS = [
|
||||
cfg.BoolOpt('manage_firewall',
|
||||
default=True,
|
||||
help=_('Whether to manage firewall rules for PXE port.')),
|
||||
cfg.StrOpt('dnsmasq_interface',
|
||||
default='br-ctlplane',
|
||||
help=_('Interface on which dnsmasq listens, the default is for '
|
||||
'VM\'s.')),
|
||||
cfg.IntOpt('firewall_update_period',
|
||||
default=15,
|
||||
help=_('Amount of time in seconds, after which repeat periodic '
|
||||
'update of firewall.')),
|
||||
cfg.StrOpt('firewall_chain',
|
||||
default='ironic-inspector',
|
||||
help=_('iptables chain name to use.')),
|
||||
cfg.ListOpt('ethoib_interfaces',
|
||||
default=[],
|
||||
help=_('List of Etherent Over InfiniBand interfaces '
|
||||
'on the Inspector host which are used for physical '
|
||||
'access to the DHCP network. Multiple interfaces would '
|
||||
'be attached to a bond or bridge specified in '
|
||||
'dnsmasq_interface. The MACs of the InfiniBand nodes '
|
||||
'which are not in desired state are going to be '
|
||||
'blacklisted based on the list of neighbor MACs '
|
||||
'on these interfaces.')),
|
||||
]
|
||||
|
||||
PROCESSING_OPTS = [
|
||||
cfg.StrOpt('add_ports',
|
||||
default='pxe',
|
||||
help=_('Which MAC addresses to add as ports during '
|
||||
'introspection. Possible values: all '
|
||||
'(all MAC addresses), active (MAC addresses of NIC with '
|
||||
'IP addresses), pxe (only MAC address of NIC node PXE '
|
||||
'booted from, falls back to "active" if PXE MAC is not '
|
||||
'supplied by the ramdisk).'),
|
||||
choices=VALID_ADD_PORTS_VALUES),
|
||||
cfg.StrOpt('keep_ports',
|
||||
default='all',
|
||||
help=_('Which ports (already present on a node) to keep after '
|
||||
'introspection. Possible values: all (do not delete '
|
||||
'anything), present (keep ports which MACs were present '
|
||||
'in introspection data), added (keep only MACs that we '
|
||||
'added during introspection).'),
|
||||
choices=VALID_KEEP_PORTS_VALUES),
|
||||
cfg.BoolOpt('overwrite_existing',
|
||||
default=True,
|
||||
help=_('Whether to overwrite existing values in node '
|
||||
'database. Disable this option to make '
|
||||
'introspection a non-destructive operation.')),
|
||||
cfg.StrOpt('default_processing_hooks',
|
||||
default='ramdisk_error,root_disk_selection,scheduler,'
|
||||
'validate_interfaces,capabilities,pci_devices',
|
||||
help=_('Comma-separated list of default hooks for processing '
|
||||
'pipeline. Hook \'scheduler\' updates the node with the '
|
||||
'minimum properties required by the Nova scheduler. '
|
||||
'Hook \'validate_interfaces\' ensures that valid NIC '
|
||||
'data was provided by the ramdisk. '
|
||||
'Do not exclude these two unless you really know what '
|
||||
'you\'re doing.')),
|
||||
cfg.StrOpt('processing_hooks',
|
||||
default='$default_processing_hooks',
|
||||
help=_('Comma-separated list of enabled hooks for processing '
|
||||
'pipeline. The default for this is '
|
||||
'$default_processing_hooks, hooks can be added before '
|
||||
'or after the defaults like this: '
|
||||
'"prehook,$default_processing_hooks,posthook".')),
|
||||
cfg.StrOpt('ramdisk_logs_dir',
|
||||
help=_('If set, logs from ramdisk will be stored in this '
|
||||
'directory.')),
|
||||
cfg.BoolOpt('always_store_ramdisk_logs',
|
||||
default=False,
|
||||
help=_('Whether to store ramdisk logs even if it did not '
|
||||
'return an error message (dependent upon '
|
||||
'"ramdisk_logs_dir" option being set).')),
|
||||
cfg.StrOpt('node_not_found_hook',
|
||||
help=_('The name of the hook to run when inspector receives '
|
||||
'inspection information from a node it isn\'t already '
|
||||
'aware of. This hook is ignored by default.')),
|
||||
cfg.StrOpt('store_data',
|
||||
default='none',
|
||||
choices=VALID_STORE_DATA_VALUES,
|
||||
help=_('Method for storing introspection data. If set to \'none'
|
||||
'\', introspection data will not be stored.')),
|
||||
cfg.StrOpt('store_data_location',
|
||||
help=_('Name of the key to store the location of stored data '
|
||||
'in the extra column of the Ironic database.')),
|
||||
cfg.BoolOpt('disk_partitioning_spacing',
|
||||
default=True,
|
||||
help=_('Whether to leave 1 GiB of disk size untouched for '
|
||||
'partitioning. Only has effect when used with the IPA '
|
||||
'as a ramdisk, for older ramdisk local_gb is '
|
||||
'calculated on the ramdisk side.')),
|
||||
cfg.BoolOpt('log_bmc_address',
|
||||
default=True,
|
||||
help=_('Whether to log node BMC address with every message '
|
||||
'during processing.'),
|
||||
deprecated_for_removal=True),
|
||||
cfg.StrOpt('ramdisk_logs_filename_format',
|
||||
default='{uuid}_{dt:%Y%m%d-%H%M%S.%f}.tar.gz',
|
||||
help=_('File name template for storing ramdisk logs. The '
|
||||
'following replacements can be used: '
|
||||
'{uuid} - node UUID or "unknown", '
|
||||
'{bmc} - node BMC address or "unknown", '
|
||||
'{dt} - current UTC date and time, '
|
||||
'{mac} - PXE booting MAC or "unknown".')),
|
||||
cfg.BoolOpt('power_off',
|
||||
default=True,
|
||||
help=_('Whether to power off a node after introspection.')),
|
||||
]
|
||||
|
||||
SERVICE_OPTS = [
|
||||
cfg.StrOpt('listen_address',
|
||||
default='0.0.0.0',
|
||||
help=_('IP to listen on.')),
|
||||
cfg.PortOpt('listen_port',
|
||||
default=5050,
|
||||
help=_('Port to listen on.')),
|
||||
cfg.StrOpt('auth_strategy',
|
||||
default='keystone',
|
||||
choices=('keystone', 'noauth'),
|
||||
help=_('Authentication method used on the ironic-inspector '
|
||||
'API. Either "noauth" or "keystone" are currently valid '
|
||||
'options. "noauth" will disable all authentication.')),
|
||||
cfg.IntOpt('timeout',
|
||||
default=3600,
|
||||
help=_('Timeout after which introspection is considered '
|
||||
'failed, set to 0 to disable.')),
|
||||
cfg.IntOpt('node_status_keep_time',
|
||||
default=0,
|
||||
help=_('For how much time (in seconds) to keep status '
|
||||
'information about nodes after introspection was '
|
||||
'finished for them. Set to 0 (the default) '
|
||||
'to disable the timeout.'),
|
||||
deprecated_for_removal=True),
|
||||
cfg.IntOpt('clean_up_period',
|
||||
default=60,
|
||||
help=_('Amount of time in seconds, after which repeat clean up '
|
||||
'of timed out nodes and old nodes status information.')),
|
||||
cfg.BoolOpt('use_ssl',
|
||||
default=False,
|
||||
help=_('SSL Enabled/Disabled')),
|
||||
cfg.StrOpt('ssl_cert_path',
|
||||
default='',
|
||||
help=_('Path to SSL certificate')),
|
||||
cfg.StrOpt('ssl_key_path',
|
||||
default='',
|
||||
help=_('Path to SSL key')),
|
||||
cfg.IntOpt('max_concurrency',
|
||||
default=1000, min=2,
|
||||
help=_('The green thread pool size.')),
|
||||
cfg.IntOpt('introspection_delay',
|
||||
default=5,
|
||||
help=_('Delay (in seconds) between two introspections.')),
|
||||
cfg.StrOpt('introspection_delay_drivers',
|
||||
default='.*',
|
||||
help=_('Only node with drivers matching this regular '
|
||||
'expression will be affected by introspection_delay '
|
||||
'setting.'),
|
||||
deprecated_for_removal=True),
|
||||
cfg.ListOpt('ipmi_address_fields',
|
||||
default=['ilo_address', 'drac_host', 'drac_address',
|
||||
'cimc_address'],
|
||||
help=_('Ironic driver_info fields that are equivalent '
|
||||
'to ipmi_address.')),
|
||||
cfg.StrOpt('rootwrap_config',
|
||||
default="/etc/ironic-inspector/rootwrap.conf",
|
||||
help=_('Path to the rootwrap configuration file to use for '
|
||||
'running commands as root')),
|
||||
cfg.IntOpt('api_max_limit', default=1000, min=1,
|
||||
help=_('Limit the number of elements an API list-call returns'))
|
||||
]
|
||||
|
||||
PXE_FILTER_OPTS = [
|
||||
cfg.StrOpt('driver', default='noop',
|
||||
help=_('PXE boot filter driver to use, such as iptables')),
|
||||
cfg.IntOpt('sync_period', default=15, min=0,
|
||||
help=_('Amount of time in seconds, after which repeat periodic '
|
||||
'update of the filter.')),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(SERVICE_OPTS)
|
||||
cfg.CONF.register_opts(FIREWALL_OPTS, group='firewall')
|
||||
cfg.CONF.register_opts(PROCESSING_OPTS, group='processing')
|
||||
cfg.CONF.register_opts(PXE_FILTER_OPTS, 'pxe_filter')
|
||||
|
||||
|
||||
def list_opts():
|
||||
return [
|
||||
('', SERVICE_OPTS),
|
||||
('firewall', FIREWALL_OPTS),
|
||||
('processing', PROCESSING_OPTS),
|
||||
('pxe_filter', PXE_FILTER_OPTS),
|
||||
]
|
||||
|
||||
|
||||
def set_config_defaults():
|
||||
"""This method updates all configuration default values."""
|
||||
set_cors_middleware_defaults()
|
||||
|
||||
|
||||
def set_cors_middleware_defaults():
|
||||
"""Update default configuration options for oslo.middleware."""
|
||||
# TODO(krotscheck): Update with https://review.openstack.org/#/c/285368/
|
||||
cfg.set_defaults(
|
||||
cors.CORS_OPTS,
|
||||
allow_headers=['X-Auth-Token',
|
||||
MIN_VERSION_HEADER,
|
||||
MAX_VERSION_HEADER,
|
||||
VERSION_HEADER],
|
||||
allow_methods=['GET', 'POST', 'PUT', 'HEAD',
|
||||
'PATCH', 'DELETE', 'OPTIONS']
|
||||
)
|
@ -1,197 +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.
|
||||
|
||||
"""SQLAlchemy models for inspection data and shared database code."""
|
||||
|
||||
import contextlib
|
||||
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_config import cfg
|
||||
from oslo_db import options as db_opts
|
||||
from oslo_db.sqlalchemy import enginefacade
|
||||
from oslo_db.sqlalchemy import models
|
||||
from oslo_db.sqlalchemy import types as db_types
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Enum, ForeignKey,
|
||||
Integer, String, Text)
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy import orm
|
||||
|
||||
from ironic_inspector import conf # noqa
|
||||
from ironic_inspector import introspection_state as istate
|
||||
|
||||
|
||||
class ModelBase(models.ModelBase):
|
||||
__table_args__ = {'mysql_engine': "InnoDB",
|
||||
'mysql_charset': "utf8"}
|
||||
|
||||
|
||||
Base = declarative_base(cls=ModelBase)
|
||||
CONF = cfg.CONF
|
||||
_DEFAULT_SQL_CONNECTION = 'sqlite:///ironic_inspector.sqlite'
|
||||
_CTX_MANAGER = None
|
||||
|
||||
db_opts.set_defaults(CONF, connection=_DEFAULT_SQL_CONNECTION)
|
||||
|
||||
_synchronized = lockutils.synchronized_with_prefix("ironic-inspector-")
|
||||
|
||||
|
||||
class Node(Base):
|
||||
__tablename__ = 'nodes'
|
||||
uuid = Column(String(36), primary_key=True)
|
||||
version_id = Column(String(36), server_default='')
|
||||
state = Column(Enum(*istate.States.all()), nullable=False,
|
||||
default=istate.States.finished,
|
||||
server_default=istate.States.finished)
|
||||
started_at = Column(DateTime, nullable=True)
|
||||
finished_at = Column(DateTime, nullable=True)
|
||||
error = Column(Text, nullable=True)
|
||||
|
||||
# version_id is being tracked in the NodeInfo object
|
||||
# for the sake of consistency. See also SQLAlchemy docs:
|
||||
# http://docs.sqlalchemy.org/en/latest/orm/versioning.html
|
||||
__mapper_args__ = {
|
||||
'version_id_col': version_id,
|
||||
'version_id_generator': False,
|
||||
}
|
||||
|
||||
|
||||
class Attribute(Base):
|
||||
__tablename__ = 'attributes'
|
||||
uuid = Column(String(36), primary_key=True)
|
||||
node_uuid = Column(String(36), ForeignKey('nodes.uuid',
|
||||
name='fk_node_attribute'))
|
||||
name = Column(String(255), nullable=False)
|
||||
value = Column(String(255), nullable=True)
|
||||
|
||||
|
||||
class Option(Base):
|
||||
__tablename__ = 'options'
|
||||
uuid = Column(String(36), ForeignKey('nodes.uuid'), primary_key=True)
|
||||
name = Column(String(255), primary_key=True)
|
||||
value = Column(Text)
|
||||
|
||||
|
||||
class Rule(Base):
|
||||
__tablename__ = 'rules'
|
||||
uuid = Column(String(36), primary_key=True)
|
||||
created_at = Column(DateTime, nullable=False)
|
||||
description = Column(Text)
|
||||
# NOTE(dtantsur): in the future we might need to temporary disable a rule
|
||||
disabled = Column(Boolean, default=False)
|
||||
|
||||
conditions = orm.relationship('RuleCondition', lazy='joined',
|
||||
order_by='RuleCondition.id',
|
||||
cascade="all, delete-orphan")
|
||||
actions = orm.relationship('RuleAction', lazy='joined',
|
||||
order_by='RuleAction.id',
|
||||
cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class RuleCondition(Base):
|
||||
__tablename__ = 'rule_conditions'
|
||||
id = Column(Integer, primary_key=True)
|
||||
rule = Column(String(36), ForeignKey('rules.uuid'))
|
||||
op = Column(String(255), nullable=False)
|
||||
multiple = Column(String(255), nullable=False)
|
||||
invert = Column(Boolean, default=False)
|
||||
# NOTE(dtantsur): while all operations now require a field, I can also
|
||||
# imagine user-defined operations that do not, thus it's nullable.
|
||||
field = Column(Text)
|
||||
params = Column(db_types.JsonEncodedDict)
|
||||
|
||||
def as_dict(self):
|
||||
res = self.params.copy()
|
||||
res['op'] = self.op
|
||||
res['field'] = self.field
|
||||
res['multiple'] = self.multiple
|
||||
res['invert'] = self.invert
|
||||
return res
|
||||
|
||||
|
||||
class RuleAction(Base):
|
||||
__tablename__ = 'rule_actions'
|
||||
id = Column(Integer, primary_key=True)
|
||||
rule = Column(String(36), ForeignKey('rules.uuid'))
|
||||
action = Column(String(255), nullable=False)
|
||||
params = Column(db_types.JsonEncodedDict)
|
||||
|
||||
def as_dict(self):
|
||||
res = self.params.copy()
|
||||
res['action'] = self.action
|
||||
return res
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the database.
|
||||
|
||||
Method called on service start up, initialize transaction
|
||||
context manager and try to create db session.
|
||||
"""
|
||||
get_writer_session()
|
||||
|
||||
|
||||
def model_query(model, *args, **kwargs):
|
||||
"""Query helper for simpler session usage.
|
||||
|
||||
:param session: if present, the session to use
|
||||
"""
|
||||
session = kwargs.get('session') or get_reader_session()
|
||||
query = session.query(model, *args)
|
||||
return query
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ensure_transaction(session=None):
|
||||
session = session or get_writer_session()
|
||||
with session.begin(subtransactions=True):
|
||||
yield session
|
||||
|
||||
|
||||
@_synchronized("transaction-context-manager")
|
||||
def _create_context_manager():
|
||||
_ctx_mgr = enginefacade.transaction_context()
|
||||
# TODO(aarefiev): enable foreign keys for SQLite once all unit
|
||||
# tests with failed constraint will be fixed.
|
||||
_ctx_mgr.configure(sqlite_fk=False)
|
||||
|
||||
return _ctx_mgr
|
||||
|
||||
|
||||
def get_context_manager():
|
||||
"""Create transaction context manager lazily.
|
||||
|
||||
:returns: The transaction context manager.
|
||||
"""
|
||||
global _CTX_MANAGER
|
||||
if _CTX_MANAGER is None:
|
||||
_CTX_MANAGER = _create_context_manager()
|
||||
|
||||
return _CTX_MANAGER
|
||||
|
||||
|
||||
def get_reader_session():
|
||||
"""Help method to get reader session.
|
||||
|
||||
:returns: The reader session.
|
||||
"""
|
||||
return get_context_manager().reader.get_sessionmaker()()
|
||||
|
||||
|
||||
def get_writer_session():
|
||||
"""Help method to get writer session.
|
||||
|
||||
:returns: The writer session.
|
||||
"""
|
||||
return get_context_manager().writer.get_sessionmaker()()
|
@ -1,94 +0,0 @@
|
||||
# Copyright 2015 Cisco Systems
|
||||
# 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.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from alembic import command as alembic_command
|
||||
from alembic import config as alembic_config
|
||||
from alembic import util as alembic_util
|
||||
import six
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from ironic_inspector import conf # noqa
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def add_alembic_command(subparsers, name):
|
||||
return subparsers.add_parser(
|
||||
name, help=getattr(alembic_command, name).__doc__)
|
||||
|
||||
|
||||
def add_command_parsers(subparsers):
|
||||
for name in ['current', 'history', 'branches', 'heads']:
|
||||
parser = add_alembic_command(subparsers, name)
|
||||
parser.set_defaults(func=do_alembic_command)
|
||||
|
||||
for name in ['stamp', 'show', 'edit']:
|
||||
parser = add_alembic_command(subparsers, name)
|
||||
parser.set_defaults(func=with_revision)
|
||||
parser.add_argument('--revision', nargs='?', required=True)
|
||||
|
||||
parser = add_alembic_command(subparsers, 'upgrade')
|
||||
parser.set_defaults(func=with_revision)
|
||||
parser.add_argument('--revision', nargs='?')
|
||||
|
||||
parser = add_alembic_command(subparsers, 'revision')
|
||||
parser.set_defaults(func=do_revision)
|
||||
parser.add_argument('-m', '--message')
|
||||
parser.add_argument('--autogenerate', action='store_true')
|
||||
|
||||
|
||||
command_opt = cfg.SubCommandOpt('command',
|
||||
title='Command',
|
||||
help='Available commands',
|
||||
handler=add_command_parsers)
|
||||
|
||||
CONF.register_cli_opt(command_opt)
|
||||
|
||||
|
||||
def _get_alembic_config():
|
||||
return alembic_config.Config(os.path.join(os.path.dirname(__file__),
|
||||
'alembic.ini'))
|
||||
|
||||
|
||||
def do_revision(config, cmd, *args, **kwargs):
|
||||
do_alembic_command(config, cmd, message=CONF.command.message,
|
||||
autogenerate=CONF.command.autogenerate)
|
||||
|
||||
|
||||
def with_revision(config, cmd, *args, **kwargs):
|
||||
revision = CONF.command.revision or 'head'
|
||||
do_alembic_command(config, cmd, revision)
|
||||
|
||||
|
||||
def do_alembic_command(config, cmd, *args, **kwargs):
|
||||
try:
|
||||
getattr(alembic_command, cmd)(config, *args, **kwargs)
|
||||
except alembic_util.CommandError as e:
|
||||
alembic_util.err(six.text_type(e))
|
||||
|
||||
|
||||
def main(args=sys.argv[1:]):
|
||||
log.register_options(CONF)
|
||||
CONF(args, project='ironic-inspector')
|
||||
config = _get_alembic_config()
|
||||
config.set_main_option('script_location', "ironic_inspector:migrations")
|
||||
config.ironic_inspector_config = CONF
|
||||
|
||||
CONF.command.func(config, CONF.command.name)
|
@ -1,257 +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 contextlib
|
||||
import os
|
||||
import re
|
||||
|
||||
from eventlet.green import subprocess
|
||||
from eventlet import semaphore
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import node_cache
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger("ironic_inspector.firewall")
|
||||
NEW_CHAIN = None
|
||||
CHAIN = None
|
||||
INTERFACE = None
|
||||
LOCK = semaphore.BoundedSemaphore()
|
||||
BASE_COMMAND = None
|
||||
BLACKLIST_CACHE = None
|
||||
ENABLED = True
|
||||
EMAC_REGEX = 'EMAC=([0-9a-f]{2}(:[0-9a-f]{2}){5}) IMAC=.*'
|
||||
|
||||
|
||||
def _iptables(*args, **kwargs):
|
||||
# NOTE(dtantsur): -w flag makes it wait for xtables lock
|
||||
cmd = BASE_COMMAND + args
|
||||
ignore = kwargs.pop('ignore', False)
|
||||
LOG.debug('Running iptables %s', args)
|
||||
kwargs['stderr'] = subprocess.STDOUT
|
||||
try:
|
||||
subprocess.check_output(cmd, **kwargs)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
output = exc.output.replace('\n', '. ')
|
||||
if ignore:
|
||||
LOG.debug('Ignoring failed iptables %(args)s: %(output)s',
|
||||
{'args': args, 'output': output})
|
||||
else:
|
||||
LOG.error('iptables %(iptables)s failed: %(exc)s',
|
||||
{'iptables': args, 'exc': output})
|
||||
raise
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize firewall management.
|
||||
|
||||
Must be called one on start-up.
|
||||
"""
|
||||
if not CONF.firewall.manage_firewall:
|
||||
return
|
||||
|
||||
global INTERFACE, CHAIN, NEW_CHAIN, BASE_COMMAND, BLACKLIST_CACHE
|
||||
BLACKLIST_CACHE = None
|
||||
INTERFACE = CONF.firewall.dnsmasq_interface
|
||||
CHAIN = CONF.firewall.firewall_chain
|
||||
NEW_CHAIN = CHAIN + '_temp'
|
||||
BASE_COMMAND = ('sudo', 'ironic-inspector-rootwrap',
|
||||
CONF.rootwrap_config, 'iptables',)
|
||||
|
||||
# -w flag makes iptables wait for xtables lock, but it's not supported
|
||||
# everywhere yet
|
||||
try:
|
||||
with open(os.devnull, 'wb') as null:
|
||||
subprocess.check_call(BASE_COMMAND + ('-w', '-h'),
|
||||
stderr=null, stdout=null)
|
||||
except subprocess.CalledProcessError:
|
||||
LOG.warning('iptables does not support -w flag, please update '
|
||||
'it to at least version 1.4.21')
|
||||
else:
|
||||
BASE_COMMAND += ('-w',)
|
||||
|
||||
_clean_up(CHAIN)
|
||||
# Not really needed, but helps to validate that we have access to iptables
|
||||
_iptables('-N', CHAIN)
|
||||
|
||||
|
||||
def _clean_up(chain):
|
||||
_iptables('-D', 'INPUT', '-i', INTERFACE, '-p', 'udp',
|
||||
'--dport', '67', '-j', chain,
|
||||
ignore=True)
|
||||
_iptables('-F', chain, ignore=True)
|
||||
_iptables('-X', chain, ignore=True)
|
||||
|
||||
|
||||
def clean_up():
|
||||
"""Clean up everything before exiting."""
|
||||
if not CONF.firewall.manage_firewall:
|
||||
return
|
||||
|
||||
_clean_up(CHAIN)
|
||||
_clean_up(NEW_CHAIN)
|
||||
|
||||
|
||||
def _should_enable_dhcp():
|
||||
"""Check whether we should enable DHCP at all.
|
||||
|
||||
We won't even open our DHCP if no nodes are on introspection and
|
||||
node_not_found_hook is not set.
|
||||
"""
|
||||
return (node_cache.introspection_active() or
|
||||
CONF.processing.node_not_found_hook)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _temporary_chain(chain, main_chain):
|
||||
"""Context manager to operate on a temporary chain."""
|
||||
# Clean up a bit to account for possible troubles on previous run
|
||||
_clean_up(chain)
|
||||
_iptables('-N', chain)
|
||||
|
||||
yield
|
||||
|
||||
# Swap chains
|
||||
_iptables('-I', 'INPUT', '-i', INTERFACE, '-p', 'udp',
|
||||
'--dport', '67', '-j', chain)
|
||||
_iptables('-D', 'INPUT', '-i', INTERFACE, '-p', 'udp',
|
||||
'--dport', '67', '-j', main_chain,
|
||||
ignore=True)
|
||||
_iptables('-F', main_chain, ignore=True)
|
||||
_iptables('-X', main_chain, ignore=True)
|
||||
_iptables('-E', chain, main_chain)
|
||||
|
||||
|
||||
def _disable_dhcp():
|
||||
"""Disable DHCP completely."""
|
||||
global ENABLED, BLACKLIST_CACHE
|
||||
|
||||
if not ENABLED:
|
||||
LOG.debug('DHCP is already disabled, not updating')
|
||||
return
|
||||
|
||||
LOG.debug('No nodes on introspection and node_not_found_hook is '
|
||||
'not set - disabling DHCP')
|
||||
BLACKLIST_CACHE = None
|
||||
with _temporary_chain(NEW_CHAIN, CHAIN):
|
||||
# Blacklist everything
|
||||
_iptables('-A', NEW_CHAIN, '-j', 'REJECT')
|
||||
|
||||
ENABLED = False
|
||||
|
||||
|
||||
def update_filters(ironic=None):
|
||||
"""Update firewall filter rules for introspection.
|
||||
|
||||
Gives access to PXE boot port for any machine, except for those,
|
||||
whose MAC is registered in Ironic and is not on introspection right now.
|
||||
|
||||
This function is called from both introspection initialization code and
|
||||
from periodic task. This function is supposed to be resistant to unexpected
|
||||
iptables state.
|
||||
|
||||
``init()`` function must be called once before any call to this function.
|
||||
This function is using ``eventlet`` semaphore to serialize access from
|
||||
different green threads.
|
||||
|
||||
Does nothing, if firewall management is disabled in configuration.
|
||||
|
||||
:param ironic: Ironic client instance, optional.
|
||||
"""
|
||||
global BLACKLIST_CACHE, ENABLED
|
||||
|
||||
if not CONF.firewall.manage_firewall:
|
||||
return
|
||||
|
||||
assert INTERFACE is not None
|
||||
ironic = ir_utils.get_client() if ironic is None else ironic
|
||||
with LOCK:
|
||||
if not _should_enable_dhcp():
|
||||
_disable_dhcp()
|
||||
return
|
||||
|
||||
ports_active = ironic.port.list(limit=0, fields=['address', 'extra'])
|
||||
macs_active = set(p.address for p in ports_active)
|
||||
to_blacklist = macs_active - node_cache.active_macs()
|
||||
ib_mac_mapping = (
|
||||
_ib_mac_to_rmac_mapping(to_blacklist, ports_active))
|
||||
|
||||
if (BLACKLIST_CACHE is not None and
|
||||
to_blacklist == BLACKLIST_CACHE and not ib_mac_mapping):
|
||||
LOG.debug('Not updating iptables - no changes in MAC list %s',
|
||||
to_blacklist)
|
||||
return
|
||||
|
||||
LOG.debug('Blacklisting active MAC\'s %s', to_blacklist)
|
||||
# Force update on the next iteration if this attempt fails
|
||||
BLACKLIST_CACHE = None
|
||||
|
||||
with _temporary_chain(NEW_CHAIN, CHAIN):
|
||||
# - Blacklist active macs, so that nova can boot them
|
||||
for mac in to_blacklist:
|
||||
mac = ib_mac_mapping.get(mac) or mac
|
||||
_iptables('-A', NEW_CHAIN, '-m', 'mac',
|
||||
'--mac-source', mac, '-j', 'DROP')
|
||||
# - Whitelist everything else
|
||||
_iptables('-A', NEW_CHAIN, '-j', 'ACCEPT')
|
||||
|
||||
# Cache result of successful iptables update
|
||||
ENABLED = True
|
||||
BLACKLIST_CACHE = to_blacklist
|
||||
|
||||
|
||||
def _ib_mac_to_rmac_mapping(blacklist_macs, ports_active):
|
||||
"""Mapping between host InfiniBand MAC to EthernetOverInfiniBand MAC
|
||||
|
||||
On InfiniBand deployment we need to map between the baremetal host
|
||||
InfiniBand MAC to the EoIB MAC. The EoIB MAC addresses are learned
|
||||
automatically by the EoIB interfaces and those MACs are recorded
|
||||
to the /sys/class/net/<ethoib_interface>/eth/neighs file.
|
||||
The InfiniBand GUID is taken from the ironic port client-id extra
|
||||
attribute. The InfiniBand GUID is the last 8 bytes of the client-id.
|
||||
The file format allows to map the GUID to EoIB MAC. The firewall
|
||||
rules based on those MACs get applied to the dnsmasq_interface by the
|
||||
update_filters function.
|
||||
|
||||
:param blacklist_macs: List of InfiniBand baremetal hosts macs to
|
||||
blacklist.
|
||||
:param ports_active: list of active ironic ports
|
||||
:return: baremetal InfiniBand to remote mac on ironic node mapping
|
||||
"""
|
||||
ethoib_interfaces = CONF.firewall.ethoib_interfaces
|
||||
ib_mac_to_remote_mac = {}
|
||||
for interface in ethoib_interfaces:
|
||||
neighs_file = (
|
||||
os.path.join('/sys/class/net', interface, 'eth/neighs'))
|
||||
try:
|
||||
with open(neighs_file, 'r') as fd:
|
||||
data = fd.read()
|
||||
except IOError:
|
||||
LOG.error('Interface %s is not Ethernet Over InfiniBand; '
|
||||
'Skipping ...', interface)
|
||||
continue
|
||||
for port in ports_active:
|
||||
if port.address in blacklist_macs:
|
||||
client_id = port.extra.get('client-id')
|
||||
if client_id:
|
||||
# Note(moshele): The last 8 bytes in the client-id is
|
||||
# the baremetal node InfiniBand GUID
|
||||
guid = client_id[-23:]
|
||||
p = re.compile(EMAC_REGEX + guid)
|
||||
match = p.search(data)
|
||||
if match:
|
||||
ib_mac_to_remote_mac[port.address] = match.group(1)
|
||||
return ib_mac_to_remote_mac
|
@ -1,184 +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.
|
||||
|
||||
"""Handling introspection request."""
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from eventlet import semaphore
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import firewall
|
||||
from ironic_inspector import introspection_state as istate
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
_LAST_INTROSPECTION_TIME = 0
|
||||
_LAST_INTROSPECTION_LOCK = semaphore.BoundedSemaphore()
|
||||
|
||||
|
||||
def introspect(node_id, token=None):
|
||||
"""Initiate hardware properties introspection for a given node.
|
||||
|
||||
:param node_id: node UUID or name
|
||||
:param token: authentication token
|
||||
:raises: Error
|
||||
"""
|
||||
ironic = ir_utils.get_client(token)
|
||||
node = ir_utils.get_node(node_id, ironic=ironic)
|
||||
|
||||
ir_utils.check_provision_state(node)
|
||||
validation = ironic.node.validate(node.uuid)
|
||||
if not validation.power['result']:
|
||||
msg = _('Failed validation of power interface, reason: %s')
|
||||
raise utils.Error(msg % validation.power['reason'],
|
||||
node_info=node)
|
||||
|
||||
bmc_address = ir_utils.get_ipmi_address(node)
|
||||
node_info = node_cache.start_introspection(node.uuid,
|
||||
bmc_address=bmc_address,
|
||||
ironic=ironic)
|
||||
|
||||
def _handle_exceptions(fut):
|
||||
try:
|
||||
fut.result()
|
||||
except utils.Error as exc:
|
||||
# Logging has already happened in Error.__init__
|
||||
node_info.finished(error=str(exc))
|
||||
except Exception as exc:
|
||||
msg = _('Unexpected exception in background introspection thread')
|
||||
LOG.exception(msg, node_info=node_info)
|
||||
node_info.finished(error=msg)
|
||||
|
||||
future = utils.executor().submit(_background_introspect, ironic, node_info)
|
||||
future.add_done_callback(_handle_exceptions)
|
||||
|
||||
|
||||
def _background_introspect(ironic, node_info):
|
||||
global _LAST_INTROSPECTION_TIME
|
||||
|
||||
if re.match(CONF.introspection_delay_drivers, node_info.node().driver):
|
||||
LOG.debug('Attempting to acquire lock on last introspection time')
|
||||
with _LAST_INTROSPECTION_LOCK:
|
||||
delay = (_LAST_INTROSPECTION_TIME - time.time()
|
||||
+ CONF.introspection_delay)
|
||||
if delay > 0:
|
||||
LOG.debug('Waiting %d seconds before sending the next '
|
||||
'node on introspection', delay)
|
||||
time.sleep(delay)
|
||||
_LAST_INTROSPECTION_TIME = time.time()
|
||||
|
||||
node_info.acquire_lock()
|
||||
try:
|
||||
_background_introspect_locked(node_info, ironic)
|
||||
finally:
|
||||
node_info.release_lock()
|
||||
|
||||
|
||||
@node_cache.fsm_transition(istate.Events.wait)
|
||||
def _background_introspect_locked(node_info, ironic):
|
||||
# TODO(dtantsur): pagination
|
||||
macs = list(node_info.ports())
|
||||
if macs:
|
||||
node_info.add_attribute(node_cache.MACS_ATTRIBUTE, macs)
|
||||
LOG.info('Whitelisting MAC\'s %s on the firewall', macs,
|
||||
node_info=node_info)
|
||||
firewall.update_filters(ironic)
|
||||
|
||||
attrs = node_info.attributes
|
||||
if CONF.processing.node_not_found_hook is None and not attrs:
|
||||
raise utils.Error(
|
||||
_('No lookup attributes were found, inspector won\'t '
|
||||
'be able to find it after introspection, consider creating '
|
||||
'ironic ports or providing an IPMI address'),
|
||||
node_info=node_info)
|
||||
|
||||
LOG.info('The following attributes will be used for look up: %s',
|
||||
attrs, node_info=node_info)
|
||||
|
||||
try:
|
||||
ironic.node.set_boot_device(node_info.uuid, 'pxe',
|
||||
persistent=False)
|
||||
except Exception as exc:
|
||||
LOG.warning('Failed to set boot device to PXE: %s',
|
||||
exc, node_info=node_info)
|
||||
|
||||
try:
|
||||
ironic.node.set_power_state(node_info.uuid, 'reboot')
|
||||
except Exception as exc:
|
||||
raise utils.Error(_('Failed to power on the node, check it\'s '
|
||||
'power management configuration: %s'),
|
||||
exc, node_info=node_info)
|
||||
LOG.info('Introspection started successfully',
|
||||
node_info=node_info)
|
||||
|
||||
|
||||
def abort(node_id, token=None):
|
||||
"""Abort running introspection.
|
||||
|
||||
:param node_id: node UUID or name
|
||||
:param token: authentication token
|
||||
:raises: Error
|
||||
"""
|
||||
LOG.debug('Aborting introspection for node %s', node_id)
|
||||
ironic = ir_utils.get_client(token)
|
||||
node_info = node_cache.get_node(node_id, ironic=ironic, locked=False)
|
||||
|
||||
# check pending operations
|
||||
locked = node_info.acquire_lock(blocking=False)
|
||||
if not locked:
|
||||
# Node busy --- cannot abort atm
|
||||
raise utils.Error(_('Node is locked, please, retry later'),
|
||||
node_info=node_info, code=409)
|
||||
|
||||
utils.executor().submit(_abort, node_info, ironic)
|
||||
|
||||
|
||||
@node_cache.release_lock
|
||||
@node_cache.fsm_transition(istate.Events.abort, reentrant=False)
|
||||
def _abort(node_info, ironic):
|
||||
# runs in background
|
||||
if node_info.finished_at is not None:
|
||||
# introspection already finished; nothing to do
|
||||
LOG.info('Cannot abort introspection as it is already '
|
||||
'finished', node_info=node_info)
|
||||
node_info.release_lock()
|
||||
return
|
||||
|
||||
# finish the introspection
|
||||
LOG.debug('Forcing power-off', node_info=node_info)
|
||||
try:
|
||||
ironic.node.set_power_state(node_info.uuid, 'off')
|
||||
except Exception as exc:
|
||||
LOG.warning('Failed to power off node: %s', exc,
|
||||
node_info=node_info)
|
||||
|
||||
node_info.finished(error=_('Canceled by operator'))
|
||||
|
||||
# block this node from PXE Booting the introspection image
|
||||
try:
|
||||
firewall.update_filters(ironic)
|
||||
except Exception as exc:
|
||||
# Note(mkovacik): this will be retried in firewall update
|
||||
# periodic task; we continue aborting
|
||||
LOG.warning('Failed to update firewall filters: %s', exc,
|
||||
node_info=node_info)
|
||||
LOG.info('Introspection aborted', node_info=node_info)
|
@ -1,148 +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.
|
||||
|
||||
"""Introspection state."""
|
||||
|
||||
from automaton import machines
|
||||
|
||||
|
||||
class States(object):
|
||||
"""States of an introspection."""
|
||||
# received introspection data from a nonexistent node
|
||||
# active - the inspector performs an operation on the node
|
||||
enrolling = 'enrolling'
|
||||
# an error appeared in a previous introspection state
|
||||
# passive - the inspector doesn't perform any operation on the node
|
||||
error = 'error'
|
||||
# introspection finished successfully
|
||||
# passive
|
||||
finished = 'finished'
|
||||
# processing introspection data from the node
|
||||
# active
|
||||
processing = 'processing'
|
||||
# processing stored introspection data from the node
|
||||
# active
|
||||
reapplying = 'reapplying'
|
||||
# received a request to start node introspection
|
||||
# active
|
||||
starting = 'starting'
|
||||
# waiting for node introspection data
|
||||
# passive
|
||||
waiting = 'waiting'
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
"""Return a list of all states."""
|
||||
return [cls.starting, cls.waiting, cls.processing, cls.finished,
|
||||
cls.error, cls.reapplying, cls.enrolling]
|
||||
|
||||
|
||||
class Events(object):
|
||||
"""Events that change introspection state."""
|
||||
# cancel a waiting node introspection
|
||||
# API, user
|
||||
abort = 'abort'
|
||||
# mark an introspection failed
|
||||
# internal
|
||||
error = 'error'
|
||||
# mark an introspection finished
|
||||
# internal
|
||||
finish = 'finish'
|
||||
# process node introspection data
|
||||
# API, introspection image
|
||||
process = 'process'
|
||||
# process stored node introspection data
|
||||
# API, user
|
||||
reapply = 'reapply'
|
||||
# initialize node introspection
|
||||
# API, user
|
||||
start = 'start'
|
||||
# mark an introspection timed-out waiting for data
|
||||
# internal
|
||||
timeout = 'timeout'
|
||||
# mark an introspection waiting for image data
|
||||
# internal
|
||||
wait = 'wait'
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
"""Return a list of all events."""
|
||||
return [cls.process, cls.reapply, cls.timeout, cls.wait, cls.abort,
|
||||
cls.error, cls.finish]
|
||||
|
||||
# Error transition is allowed in any state.
|
||||
State_space = [
|
||||
{
|
||||
'name': States.enrolling,
|
||||
'next_states': {
|
||||
Events.error: States.error,
|
||||
Events.process: States.processing,
|
||||
Events.timeout: States.error,
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': States.error,
|
||||
'next_states': {
|
||||
Events.abort: States.error,
|
||||
Events.error: States.error,
|
||||
Events.reapply: States.reapplying,
|
||||
Events.start: States.starting,
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': States.finished,
|
||||
'next_states': {
|
||||
Events.finish: States.finished,
|
||||
Events.reapply: States.reapplying,
|
||||
Events.start: States.starting
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': States.processing,
|
||||
'next_states': {
|
||||
Events.error: States.error,
|
||||
Events.finish: States.finished,
|
||||
Events.timeout: States.error,
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': States.reapplying,
|
||||
'next_states': {
|
||||
Events.error: States.error,
|
||||
Events.finish: States.finished,
|
||||
Events.reapply: States.reapplying,
|
||||
Events.timeout: States.error,
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': States.starting,
|
||||
'next_states': {
|
||||
Events.error: States.error,
|
||||
Events.start: States.starting,
|
||||
Events.wait: States.waiting,
|
||||
Events.timeout: States.error
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': States.waiting,
|
||||
'next_states': {
|
||||
Events.abort: States.error,
|
||||
Events.process: States.processing,
|
||||
Events.start: States.starting,
|
||||
Events.timeout: States.error,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
FSM = machines.FiniteMachine.build(State_space)
|
||||
FSM.default_start_state = States.finished
|
@ -1,324 +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 functools
|
||||
import os
|
||||
import re
|
||||
|
||||
import flask
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
import werkzeug
|
||||
|
||||
from ironic_inspector import api_tools
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector.common import swift
|
||||
from ironic_inspector import conf # noqa
|
||||
from ironic_inspector import introspect
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector import process
|
||||
from ironic_inspector import rules
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
MINIMUM_API_VERSION = (1, 0)
|
||||
CURRENT_API_VERSION = (1, 12)
|
||||
DEFAULT_API_VERSION = CURRENT_API_VERSION
|
||||
_LOGGING_EXCLUDED_KEYS = ('logs',)
|
||||
|
||||
|
||||
def _get_version():
|
||||
ver = flask.request.headers.get(conf.VERSION_HEADER,
|
||||
_DEFAULT_API_VERSION)
|
||||
try:
|
||||
requested = tuple(int(x) for x in ver.split('.'))
|
||||
except (ValueError, TypeError):
|
||||
return error_response(_('Malformed API version: expected string '
|
||||
'in form of X.Y'), code=400)
|
||||
return requested
|
||||
|
||||
|
||||
def _format_version(ver):
|
||||
return '%d.%d' % ver
|
||||
|
||||
|
||||
_DEFAULT_API_VERSION = _format_version(DEFAULT_API_VERSION)
|
||||
|
||||
|
||||
def error_response(exc, code=500):
|
||||
res = flask.jsonify(error={'message': str(exc)})
|
||||
res.status_code = code
|
||||
LOG.debug('Returning error to client: %s', exc)
|
||||
return res
|
||||
|
||||
|
||||
def convert_exceptions(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except utils.Error as exc:
|
||||
return error_response(exc, exc.http_code)
|
||||
except werkzeug.exceptions.HTTPException as exc:
|
||||
return error_response(exc, exc.code or 400)
|
||||
except Exception as exc:
|
||||
LOG.exception('Internal server error')
|
||||
msg = _('Internal server error')
|
||||
if CONF.debug:
|
||||
msg += ' (%s): %s' % (exc.__class__.__name__, exc)
|
||||
return error_response(msg)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@app.before_request
|
||||
def check_api_version():
|
||||
requested = _get_version()
|
||||
|
||||
if requested < MINIMUM_API_VERSION or requested > CURRENT_API_VERSION:
|
||||
return error_response(_('Unsupported API version %(requested)s, '
|
||||
'supported range is %(min)s to %(max)s') %
|
||||
{'requested': _format_version(requested),
|
||||
'min': _format_version(MINIMUM_API_VERSION),
|
||||
'max': _format_version(CURRENT_API_VERSION)},
|
||||
code=406)
|
||||
|
||||
|
||||
@app.after_request
|
||||
def add_version_headers(res):
|
||||
res.headers[conf.MIN_VERSION_HEADER] = '%s.%s' % MINIMUM_API_VERSION
|
||||
res.headers[conf.MAX_VERSION_HEADER] = '%s.%s' % CURRENT_API_VERSION
|
||||
return res
|
||||
|
||||
|
||||
def create_link_object(urls):
|
||||
links = []
|
||||
for url in urls:
|
||||
links.append({"rel": "self",
|
||||
"href": os.path.join(flask.request.url_root, url)})
|
||||
return links
|
||||
|
||||
|
||||
def generate_resource_data(resources):
|
||||
data = []
|
||||
for resource in resources:
|
||||
item = {}
|
||||
item['name'] = str(resource).split('/')[-1]
|
||||
item['links'] = create_link_object([str(resource)[1:]])
|
||||
data.append(item)
|
||||
return data
|
||||
|
||||
|
||||
def generate_introspection_status(node):
|
||||
"""Return a dict representing current node status.
|
||||
|
||||
:param node: a NodeInfo instance
|
||||
:return: dictionary
|
||||
"""
|
||||
started_at = node.started_at.isoformat()
|
||||
finished_at = node.finished_at.isoformat() if node.finished_at else None
|
||||
|
||||
status = {}
|
||||
status['uuid'] = node.uuid
|
||||
status['finished'] = bool(node.finished_at)
|
||||
status['state'] = node.state
|
||||
status['started_at'] = started_at
|
||||
status['finished_at'] = finished_at
|
||||
status['error'] = node.error
|
||||
status['links'] = create_link_object(
|
||||
["v%s/introspection/%s" % (CURRENT_API_VERSION[0], node.uuid)])
|
||||
return status
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
@convert_exceptions
|
||||
def api_root():
|
||||
versions = [
|
||||
{
|
||||
"status": "CURRENT",
|
||||
"id": '%s.%s' % CURRENT_API_VERSION,
|
||||
},
|
||||
]
|
||||
|
||||
for version in versions:
|
||||
version['links'] = create_link_object(
|
||||
["v%s" % version['id'].split('.')[0]])
|
||||
|
||||
return flask.jsonify(versions=versions)
|
||||
|
||||
|
||||
@app.route('/<version>', methods=['GET'])
|
||||
@convert_exceptions
|
||||
def version_root(version):
|
||||
pat = re.compile("^\/%s\/[^\/]*?$" % version)
|
||||
|
||||
resources = []
|
||||
for url in app.url_map.iter_rules():
|
||||
if pat.match(str(url)):
|
||||
resources.append(url)
|
||||
|
||||
if not resources:
|
||||
raise utils.Error(_('Version not found.'), code=404)
|
||||
|
||||
return flask.jsonify(resources=generate_resource_data(resources))
|
||||
|
||||
|
||||
@app.route('/v1/continue', methods=['POST'])
|
||||
@convert_exceptions
|
||||
def api_continue():
|
||||
data = flask.request.get_json(force=True)
|
||||
if not isinstance(data, dict):
|
||||
raise utils.Error(_('Invalid data: expected a JSON object, got %s') %
|
||||
data.__class__.__name__)
|
||||
|
||||
logged_data = {k: (v if k not in _LOGGING_EXCLUDED_KEYS else '<hidden>')
|
||||
for k, v in data.items()}
|
||||
LOG.debug("Received data from the ramdisk: %s", logged_data,
|
||||
data=data)
|
||||
|
||||
return flask.jsonify(process.process(data))
|
||||
|
||||
|
||||
# TODO(sambetts) Add API discovery for this endpoint
|
||||
@app.route('/v1/introspection/<node_id>', methods=['GET', 'POST'])
|
||||
@convert_exceptions
|
||||
def api_introspection(node_id):
|
||||
utils.check_auth(flask.request)
|
||||
|
||||
if flask.request.method == 'POST':
|
||||
introspect.introspect(node_id,
|
||||
token=flask.request.headers.get('X-Auth-Token'))
|
||||
return '', 202
|
||||
else:
|
||||
node_info = node_cache.get_node(node_id)
|
||||
return flask.json.jsonify(generate_introspection_status(node_info))
|
||||
|
||||
|
||||
@app.route('/v1/introspection', methods=['GET'])
|
||||
@convert_exceptions
|
||||
def api_introspection_statuses():
|
||||
utils.check_auth(flask.request)
|
||||
|
||||
nodes = node_cache.get_node_list(
|
||||
marker=api_tools.marker_field(),
|
||||
limit=api_tools.limit_field(default=CONF.api_max_limit)
|
||||
)
|
||||
data = {
|
||||
'introspection': [generate_introspection_status(node)
|
||||
for node in nodes]
|
||||
}
|
||||
return flask.json.jsonify(data)
|
||||
|
||||
|
||||
@app.route('/v1/introspection/<node_id>/abort', methods=['POST'])
|
||||
@convert_exceptions
|
||||
def api_introspection_abort(node_id):
|
||||
utils.check_auth(flask.request)
|
||||
introspect.abort(node_id, token=flask.request.headers.get('X-Auth-Token'))
|
||||
return '', 202
|
||||
|
||||
|
||||
@app.route('/v1/introspection/<node_id>/data', methods=['GET'])
|
||||
@convert_exceptions
|
||||
def api_introspection_data(node_id):
|
||||
utils.check_auth(flask.request)
|
||||
|
||||
if CONF.processing.store_data == 'swift':
|
||||
if not uuidutils.is_uuid_like(node_id):
|
||||
node = ir_utils.get_node(node_id, fields=['uuid'])
|
||||
node_id = node.uuid
|
||||
res = swift.get_introspection_data(node_id)
|
||||
return res, 200, {'Content-Type': 'application/json'}
|
||||
else:
|
||||
return error_response(_('Inspector is not configured to store data. '
|
||||
'Set the [processing] store_data '
|
||||
'configuration option to change this.'),
|
||||
code=404)
|
||||
|
||||
|
||||
@app.route('/v1/introspection/<node_id>/data/unprocessed', methods=['POST'])
|
||||
@convert_exceptions
|
||||
def api_introspection_reapply(node_id):
|
||||
utils.check_auth(flask.request)
|
||||
|
||||
if flask.request.content_length:
|
||||
return error_response(_('User data processing is not '
|
||||
'supported yet'), code=400)
|
||||
|
||||
if CONF.processing.store_data == 'swift':
|
||||
process.reapply(node_id)
|
||||
return '', 202
|
||||
else:
|
||||
return error_response(_('Inspector is not configured to store'
|
||||
' data. Set the [processing] '
|
||||
'store_data configuration option to '
|
||||
'change this.'), code=400)
|
||||
|
||||
|
||||
def rule_repr(rule, short):
|
||||
result = rule.as_dict(short=short)
|
||||
result['links'] = [{
|
||||
'href': flask.url_for('api_rule', uuid=result['uuid']),
|
||||
'rel': 'self'
|
||||
}]
|
||||
return result
|
||||
|
||||
|
||||
@app.route('/v1/rules', methods=['GET', 'POST', 'DELETE'])
|
||||
@convert_exceptions
|
||||
def api_rules():
|
||||
utils.check_auth(flask.request)
|
||||
|
||||
if flask.request.method == 'GET':
|
||||
res = [rule_repr(rule, short=True) for rule in rules.get_all()]
|
||||
return flask.jsonify(rules=res)
|
||||
elif flask.request.method == 'DELETE':
|
||||
rules.delete_all()
|
||||
return '', 204
|
||||
else:
|
||||
body = flask.request.get_json(force=True)
|
||||
if body.get('uuid') and not uuidutils.is_uuid_like(body['uuid']):
|
||||
raise utils.Error(_('Invalid UUID value'), code=400)
|
||||
|
||||
rule = rules.create(conditions_json=body.get('conditions', []),
|
||||
actions_json=body.get('actions', []),
|
||||
uuid=body.get('uuid'),
|
||||
description=body.get('description'))
|
||||
|
||||
response_code = (200 if _get_version() < (1, 6) else 201)
|
||||
return flask.make_response(
|
||||
flask.jsonify(rule_repr(rule, short=False)), response_code)
|
||||
|
||||
|
||||
@app.route('/v1/rules/<uuid>', methods=['GET', 'DELETE'])
|
||||
@convert_exceptions
|
||||
def api_rule(uuid):
|
||||
utils.check_auth(flask.request)
|
||||
|
||||
if flask.request.method == 'GET':
|
||||
rule = rules.get(uuid)
|
||||
return flask.jsonify(rule_repr(rule, short=False))
|
||||
else:
|
||||
rules.delete(uuid)
|
||||
return '', 204
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def handle_404(error):
|
||||
return error_response(error, code=404)
|
@ -1,82 +0,0 @@
|
||||
# Copyright 2015 Cisco Systems
|
||||
#
|
||||
# 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 logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
|
||||
from ironic_inspector import db
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
ironic_inspector_config = config.ironic_inspector_config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = db.Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = ironic_inspector_config.database.connection
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
session = db.get_writer_session()
|
||||
with session.connection() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
@ -1,32 +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.
|
||||
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
@ -1,63 +0,0 @@
|
||||
# Copyright 2015 Cisco Systems, Inc.
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""inital_db_schema
|
||||
|
||||
Revision ID: 578f84f38d
|
||||
Revises:
|
||||
Create Date: 2015-09-15 14:52:22.448944
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '578f84f38d'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'nodes',
|
||||
sa.Column('uuid', sa.String(36), primary_key=True),
|
||||
sa.Column('started_at', sa.Float, nullable=True),
|
||||
sa.Column('finished_at', sa.Float, nullable=True),
|
||||
sa.Column('error', sa.Text, nullable=True),
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'attributes',
|
||||
sa.Column('name', sa.String(255), primary_key=True),
|
||||
sa.Column('value', sa.String(255), primary_key=True),
|
||||
sa.Column('uuid', sa.String(36), sa.ForeignKey('nodes.uuid')),
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'options',
|
||||
sa.Column('uuid', sa.String(36), sa.ForeignKey('nodes.uuid'),
|
||||
primary_key=True),
|
||||
sa.Column('name', sa.String(255), primary_key=True),
|
||||
sa.Column('value', sa.Text),
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
@ -1,90 +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.
|
||||
|
||||
"""attribute_constraints_relaxing
|
||||
|
||||
Revision ID: 882b2d84cb1b
|
||||
Revises: d00d6e3f38c4
|
||||
Create Date: 2017-01-13 11:27:00.053286
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '882b2d84cb1b'
|
||||
down_revision = 'd00d6e3f38c4'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.engine.reflection import Inspector as insp
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
ATTRIBUTES = 'attributes'
|
||||
NODES = 'nodes'
|
||||
NAME = 'name'
|
||||
VALUE = 'value'
|
||||
UUID = 'uuid'
|
||||
NODE_UUID = 'node_uuid'
|
||||
|
||||
naming_convention = {
|
||||
"pk": 'pk_%(table_name)s',
|
||||
"fk": 'fk_%(table_name)s'
|
||||
}
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
connection = op.get_bind()
|
||||
|
||||
inspector = insp.from_engine(connection)
|
||||
|
||||
pk_constraint = (inspector.get_pk_constraint(ATTRIBUTES).get('name')
|
||||
or naming_convention['pk'] % {'table_name': ATTRIBUTES})
|
||||
fk_constraint = (inspector.get_foreign_keys(ATTRIBUTES)[0].get('name')
|
||||
or naming_convention['fk'] % {'table_name': ATTRIBUTES})
|
||||
|
||||
columns_meta = inspector.get_columns(ATTRIBUTES)
|
||||
name_type = {meta.get('type') for meta in columns_meta
|
||||
if meta['name'] == NAME}.pop()
|
||||
value_type = {meta.get('type') for meta in columns_meta
|
||||
if meta['name'] == VALUE}.pop()
|
||||
|
||||
node_uuid_column = sa.Column(NODE_UUID, sa.String(36))
|
||||
op.add_column(ATTRIBUTES, node_uuid_column)
|
||||
|
||||
attributes = sa.table(ATTRIBUTES, node_uuid_column,
|
||||
sa.Column(UUID, sa.String(36)))
|
||||
|
||||
with op.batch_alter_table(ATTRIBUTES,
|
||||
naming_convention=naming_convention) as batch_op:
|
||||
batch_op.drop_constraint(fk_constraint, type_='foreignkey')
|
||||
|
||||
rows = connection.execute(sa.select([attributes.c.uuid,
|
||||
attributes.c.node_uuid]))
|
||||
|
||||
for row in rows:
|
||||
# move uuid to node_uuid, reuse uuid as a new primary key
|
||||
connection.execute(
|
||||
attributes.update().where(attributes.c.uuid == row.uuid).
|
||||
values(node_uuid=row.uuid, uuid=uuidutils.generate_uuid())
|
||||
)
|
||||
|
||||
with op.batch_alter_table(ATTRIBUTES,
|
||||
naming_convention=naming_convention) as batch_op:
|
||||
batch_op.drop_constraint(pk_constraint, type_='primary')
|
||||
batch_op.create_primary_key(pk_constraint, [UUID])
|
||||
batch_op.create_foreign_key('fk_node_attribute', NODES,
|
||||
[NODE_UUID], [UUID])
|
||||
batch_op.alter_column('name', nullable=False, type_=name_type)
|
||||
batch_op.alter_column('value', nullable=True, type_=value_type)
|
@ -1,69 +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.
|
||||
|
||||
"""Change created|finished_at type to DateTime
|
||||
|
||||
Revision ID: d00d6e3f38c4
|
||||
Revises: d2e48801c8ef
|
||||
Create Date: 2016-12-15 17:18:10.728695
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd00d6e3f38c4'
|
||||
down_revision = 'd2e48801c8ef'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
import datetime
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
started_at = sa.Column('started_at', sa.types.Float, nullable=True)
|
||||
finished_at = sa.Column('finished_at', sa.types.Float, nullable=True)
|
||||
temp_started_at = sa.Column("temp_started_at", sa.types.DateTime,
|
||||
nullable=True)
|
||||
temp_finished_at = sa.Column("temp_finished_at", sa.types.DateTime,
|
||||
nullable=True)
|
||||
uuid = sa.Column("uuid", sa.String(36), primary_key=True)
|
||||
|
||||
op.add_column("nodes", temp_started_at)
|
||||
op.add_column("nodes", temp_finished_at)
|
||||
|
||||
t = sa.table('nodes', started_at, finished_at,
|
||||
temp_started_at, temp_finished_at, uuid)
|
||||
|
||||
conn = op.get_bind()
|
||||
rows = conn.execute(sa.select([t.c.started_at, t.c.finished_at, t.c.uuid]))
|
||||
for row in rows:
|
||||
temp_started = datetime.datetime.utcfromtimestamp(row['started_at'])
|
||||
temp_finished = row['finished_at']
|
||||
# Note(milan) this is just a precaution; sa.null shouldn't happen here
|
||||
if temp_finished is not None:
|
||||
temp_finished = datetime.datetime.utcfromtimestamp(temp_finished)
|
||||
conn.execute(t.update().where(t.c.uuid == row.uuid).values(
|
||||
temp_started_at=temp_started, temp_finished_at=temp_finished))
|
||||
|
||||
with op.batch_alter_table('nodes') as batch_op:
|
||||
batch_op.drop_column('started_at')
|
||||
batch_op.drop_column('finished_at')
|
||||
batch_op.alter_column('temp_started_at',
|
||||
existing_type=sa.types.DateTime,
|
||||
nullable=True,
|
||||
new_column_name='started_at')
|
||||
batch_op.alter_column('temp_finished_at',
|
||||
existing_type=sa.types.DateTime,
|
||||
nullable=True,
|
||||
new_column_name='finished_at')
|
@ -1,49 +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.
|
||||
|
||||
"""Introducing Node.state attribute
|
||||
|
||||
Revision ID: d2e48801c8ef
|
||||
Revises: e169a4a81d88
|
||||
Create Date: 2016-07-29 10:10:32.351661
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd2e48801c8ef'
|
||||
down_revision = 'e169a4a81d88'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import sql
|
||||
|
||||
from ironic_inspector import introspection_state as istate
|
||||
|
||||
Node = sql.table('nodes',
|
||||
sql.column('error', sa.String),
|
||||
sql.column('state', sa.Enum(*istate.States.all())))
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('nodes', sa.Column('version_id', sa.String(36),
|
||||
server_default=''))
|
||||
op.add_column('nodes', sa.Column('state', sa.Enum(*istate.States.all(),
|
||||
name='node_state'),
|
||||
nullable=False,
|
||||
default=istate.States.finished,
|
||||
server_default=istate.States.finished))
|
||||
# correct the state: finished -> error if Node.error is not null
|
||||
stmt = Node.update().where(Node.c.error != sql.null()).values(
|
||||
{'state': op.inline_literal(istate.States.error)})
|
||||
op.execute(stmt)
|
@ -1,64 +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.
|
||||
|
||||
"""Add Rules
|
||||
|
||||
Revision ID: d588418040d
|
||||
Revises: 578f84f38d
|
||||
Create Date: 2015-09-21 14:31:03.048455
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd588418040d'
|
||||
down_revision = '578f84f38d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from oslo_db.sqlalchemy import types
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'rules',
|
||||
sa.Column('uuid', sa.String(36), primary_key=True),
|
||||
sa.Column('created_at', sa.DateTime, nullable=False),
|
||||
sa.Column('description', sa.Text),
|
||||
sa.Column('disabled', sa.Boolean, default=False),
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'rule_conditions',
|
||||
sa.Column('id', sa.Integer, primary_key=True),
|
||||
sa.Column('rule', sa.String(36), sa.ForeignKey('rules.uuid')),
|
||||
sa.Column('op', sa.String(255), nullable=False),
|
||||
sa.Column('multiple', sa.String(255), nullable=False),
|
||||
sa.Column('field', sa.Text),
|
||||
sa.Column('params', types.JsonEncodedDict),
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'rule_actions',
|
||||
sa.Column('id', sa.Integer, primary_key=True),
|
||||
sa.Column('rule', sa.String(36), sa.ForeignKey('rules.uuid')),
|
||||
sa.Column('action', sa.String(255), nullable=False),
|
||||
sa.Column('params', types.JsonEncodedDict),
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
@ -1,33 +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.
|
||||
|
||||
"""Add invert field to rule condition
|
||||
|
||||
Revision ID: e169a4a81d88
|
||||
Revises: d588418040d
|
||||
Create Date: 2016-02-16 11:19:29.715615
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e169a4a81d88'
|
||||
down_revision = 'd588418040d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('rule_conditions', sa.Column('invert', sa.Boolean(),
|
||||
nullable=True, default=False))
|
@ -1,954 +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.
|
||||
|
||||
"""Cache for nodes currently under introspection."""
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from automaton import exceptions as automaton_errors
|
||||
from ironicclient import exceptions
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_config import cfg
|
||||
from oslo_db.sqlalchemy import utils as db_utils
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import reflection
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
from sqlalchemy.orm import exc as orm_errors
|
||||
from sqlalchemy import text
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import db
|
||||
from ironic_inspector import introspection_state as istate
|
||||
from ironic_inspector import utils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
|
||||
MACS_ATTRIBUTE = 'mac'
|
||||
_LOCK_TEMPLATE = 'node-%s'
|
||||
_SEMAPHORES = lockutils.Semaphores()
|
||||
|
||||
|
||||
def _get_lock(uuid):
|
||||
"""Get lock object for a given node UUID."""
|
||||
return lockutils.internal_lock(_LOCK_TEMPLATE % uuid,
|
||||
semaphores=_SEMAPHORES)
|
||||
|
||||
|
||||
def _get_lock_ctx(uuid):
|
||||
"""Get context manager yielding a lock object for a given node UUID."""
|
||||
return lockutils.lock(_LOCK_TEMPLATE % uuid, semaphores=_SEMAPHORES)
|
||||
|
||||
|
||||
class NodeInfo(object):
|
||||
"""Record about a node in the cache.
|
||||
|
||||
This class optionally allows to acquire a lock on a node. Note that the
|
||||
class instance itself is NOT thread-safe, you need to create a new instance
|
||||
for every thread.
|
||||
"""
|
||||
|
||||
def __init__(self, uuid, version_id=None, state=None, started_at=None,
|
||||
finished_at=None, error=None, node=None, ports=None,
|
||||
ironic=None, lock=None):
|
||||
self.uuid = uuid
|
||||
self.started_at = started_at
|
||||
self.finished_at = finished_at
|
||||
self.error = error
|
||||
self.invalidate_cache()
|
||||
self._version_id = version_id
|
||||
self._state = state
|
||||
self._node = node
|
||||
if ports is not None and not isinstance(ports, dict):
|
||||
ports = {p.address: p for p in ports}
|
||||
self._ports = ports
|
||||
self._attributes = None
|
||||
self._ironic = ironic
|
||||
# This is a lock on a node UUID, not on a NodeInfo object
|
||||
self._lock = lock if lock is not None else _get_lock(uuid)
|
||||
# Whether lock was acquired using this NodeInfo object
|
||||
self._locked = lock is not None
|
||||
self._fsm = None
|
||||
|
||||
def __del__(self):
|
||||
if self._locked:
|
||||
LOG.warning('BUG: node lock was not released by the moment '
|
||||
'node info object is deleted')
|
||||
self._lock.release()
|
||||
|
||||
def __str__(self):
|
||||
"""Self represented as an UUID and a state."""
|
||||
parts = [self.uuid]
|
||||
if self._state:
|
||||
parts += [_('state'), self._state]
|
||||
return ' '.join(parts)
|
||||
|
||||
def acquire_lock(self, blocking=True):
|
||||
"""Acquire a lock on the associated node.
|
||||
|
||||
Exits with success if a lock is already acquired using this NodeInfo
|
||||
object.
|
||||
|
||||
:param blocking: if True, wait for lock to be acquired, otherwise
|
||||
return immediately.
|
||||
:returns: boolean value, whether lock was acquired successfully
|
||||
"""
|
||||
if self._locked:
|
||||
return True
|
||||
|
||||
LOG.debug('Attempting to acquire lock', node_info=self)
|
||||
if self._lock.acquire(blocking):
|
||||
self._locked = True
|
||||
LOG.debug('Successfully acquired lock', node_info=self)
|
||||
return True
|
||||
else:
|
||||
LOG.debug('Unable to acquire lock', node_info=self)
|
||||
return False
|
||||
|
||||
def release_lock(self):
|
||||
"""Release a lock on a node.
|
||||
|
||||
Does nothing if lock was not acquired using this NodeInfo object.
|
||||
"""
|
||||
if self._locked:
|
||||
LOG.debug('Successfully released lock', node_info=self)
|
||||
self._lock.release()
|
||||
self._locked = False
|
||||
|
||||
@property
|
||||
def version_id(self):
|
||||
"""Get the version id"""
|
||||
if self._version_id is None:
|
||||
row = db.model_query(db.Node).get(self.uuid)
|
||||
if row is None:
|
||||
raise utils.NotFoundInCacheError(_('Node not found in the '
|
||||
'cache'), node_info=self)
|
||||
self._version_id = row.version_id
|
||||
return self._version_id
|
||||
|
||||
def _set_version_id(self, value, session):
|
||||
row = self._row(session)
|
||||
row.version_id = value
|
||||
row.save(session)
|
||||
self._version_id = value
|
||||
|
||||
def _row(self, session=None):
|
||||
"""Get a row from the database with self.uuid and self.version_id"""
|
||||
try:
|
||||
# race condition if version_id changed outside of this node_info
|
||||
return db.model_query(db.Node, session=session).filter_by(
|
||||
uuid=self.uuid, version_id=self.version_id).one()
|
||||
except (orm_errors.NoResultFound, orm_errors.StaleDataError):
|
||||
raise utils.NodeStateRaceCondition(node_info=self)
|
||||
|
||||
def _commit(self, **fields):
|
||||
"""Commit the fields into the DB."""
|
||||
LOG.debug('Committing fields: %s', fields, node_info=self)
|
||||
with db.ensure_transaction() as session:
|
||||
self._set_version_id(uuidutils.generate_uuid(), session)
|
||||
row = self._row(session)
|
||||
row.update(fields)
|
||||
|
||||
def commit(self):
|
||||
"""Commit current node status into the database."""
|
||||
# state and version_id are updated separately
|
||||
self._commit(started_at=self.started_at, finished_at=self.finished_at,
|
||||
error=self.error)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""State of the node_info object."""
|
||||
if self._state is None:
|
||||
row = self._row()
|
||||
self._state = row.state
|
||||
return self._state
|
||||
|
||||
def _set_state(self, value):
|
||||
self._commit(state=value)
|
||||
self._state = value
|
||||
|
||||
def _get_fsm(self):
|
||||
"""Get an fsm instance initialized with self.state."""
|
||||
if self._fsm is None:
|
||||
self._fsm = istate.FSM.copy(shallow=True)
|
||||
self._fsm.initialize(start_state=self.state)
|
||||
return self._fsm
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _fsm_ctx(self):
|
||||
fsm = self._get_fsm()
|
||||
try:
|
||||
yield fsm
|
||||
finally:
|
||||
if fsm.current_state != self.state:
|
||||
LOG.info('Updating node state: %(current)s --> %(new)s',
|
||||
{'current': self.state, 'new': fsm.current_state},
|
||||
node_info=self)
|
||||
self._set_state(fsm.current_state)
|
||||
|
||||
def fsm_event(self, event, strict=False):
|
||||
"""Update node_info.state based on a fsm.process_event(event) call.
|
||||
|
||||
An AutomatonException triggers an error event.
|
||||
If strict, node_info.finished(error=str(exc)) is called with the
|
||||
AutomatonException instance and a EventError raised.
|
||||
|
||||
:param event: an event to process by the fsm
|
||||
:strict: whether to fail the introspection upon an invalid event
|
||||
:raises: NodeStateInvalidEvent
|
||||
"""
|
||||
with self._fsm_ctx() as fsm:
|
||||
LOG.debug('Executing fsm(%(state)s).process_event(%(event)s)',
|
||||
{'state': fsm.current_state, 'event': event},
|
||||
node_info=self)
|
||||
try:
|
||||
fsm.process_event(event)
|
||||
except automaton_errors.NotFound as exc:
|
||||
msg = _('Invalid event: %s') % exc
|
||||
if strict:
|
||||
LOG.error(msg, node_info=self)
|
||||
# assuming an error event is always possible
|
||||
fsm.process_event(istate.Events.error)
|
||||
self.finished(error=str(exc))
|
||||
else:
|
||||
LOG.warning(msg, node_info=self)
|
||||
raise utils.NodeStateInvalidEvent(str(exc), node_info=self)
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""Node introspection options as a dict."""
|
||||
if self._options is None:
|
||||
rows = db.model_query(db.Option).filter_by(
|
||||
uuid=self.uuid)
|
||||
self._options = {row.name: json.loads(row.value)
|
||||
for row in rows}
|
||||
return self._options
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
"""Node look up attributes as a dict."""
|
||||
if self._attributes is None:
|
||||
self._attributes = {}
|
||||
rows = db.model_query(db.Attribute).filter_by(
|
||||
node_uuid=self.uuid)
|
||||
for row in rows:
|
||||
self._attributes.setdefault(row.name, []).append(row.value)
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def ironic(self):
|
||||
"""Ironic client instance."""
|
||||
if self._ironic is None:
|
||||
self._ironic = ir_utils.get_client()
|
||||
return self._ironic
|
||||
|
||||
def set_option(self, name, value):
|
||||
"""Set an option for a node."""
|
||||
encoded = json.dumps(value)
|
||||
self.options[name] = value
|
||||
with db.ensure_transaction() as session:
|
||||
db.model_query(db.Option, session=session).filter_by(
|
||||
uuid=self.uuid, name=name).delete()
|
||||
db.Option(uuid=self.uuid, name=name, value=encoded).save(
|
||||
session)
|
||||
|
||||
def finished(self, error=None):
|
||||
"""Record status for this node.
|
||||
|
||||
Also deletes look up attributes from the cache.
|
||||
|
||||
:param error: error message
|
||||
"""
|
||||
self.release_lock()
|
||||
|
||||
self.finished_at = timeutils.utcnow()
|
||||
self.error = error
|
||||
|
||||
with db.ensure_transaction() as session:
|
||||
self._commit(finished_at=self.finished_at, error=self.error)
|
||||
db.model_query(db.Attribute, session=session).filter_by(
|
||||
node_uuid=self.uuid).delete()
|
||||
db.model_query(db.Option, session=session).filter_by(
|
||||
uuid=self.uuid).delete()
|
||||
|
||||
def add_attribute(self, name, value, session=None):
|
||||
"""Store look up attribute for a node in the database.
|
||||
|
||||
:param name: attribute name
|
||||
:param value: attribute value or list of possible values
|
||||
:param session: optional existing database session
|
||||
"""
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
with db.ensure_transaction(session) as session:
|
||||
for v in value:
|
||||
db.Attribute(uuid=uuidutils.generate_uuid(), name=name,
|
||||
value=v, node_uuid=self.uuid).save(session)
|
||||
# Invalidate attributes so they're loaded on next usage
|
||||
self._attributes = None
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row, ironic=None, lock=None, node=None):
|
||||
"""Construct NodeInfo from a database row."""
|
||||
fields = {key: row[key]
|
||||
for key in ('uuid', 'version_id', 'state', 'started_at',
|
||||
'finished_at', 'error')}
|
||||
return cls(ironic=ironic, lock=lock, node=node, **fields)
|
||||
|
||||
def invalidate_cache(self):
|
||||
"""Clear all cached info, so that it's reloaded next time."""
|
||||
self._options = None
|
||||
self._node = None
|
||||
self._ports = None
|
||||
self._attributes = None
|
||||
self._ironic = None
|
||||
self._fsm = None
|
||||
self._state = None
|
||||
self._version_id = None
|
||||
|
||||
def node(self, ironic=None):
|
||||
"""Get Ironic node object associated with the cached node record."""
|
||||
if self._node is None:
|
||||
ironic = ironic or self.ironic
|
||||
self._node = ir_utils.get_node(self.uuid, ironic=ironic)
|
||||
return self._node
|
||||
|
||||
def create_ports(self, ports, ironic=None):
|
||||
"""Create one or several ports for this node.
|
||||
|
||||
:param ports: List of ports with all their attributes
|
||||
e.g [{'mac': xx, 'ip': xx, 'client_id': None},
|
||||
{'mac': xx, 'ip': None, 'client_id': None}]
|
||||
It also support the old style of list of macs.
|
||||
A warning is issued if port already exists on a node.
|
||||
|
||||
:param ironic: Ironic client to use instead of self.ironic
|
||||
"""
|
||||
existing_macs = []
|
||||
for port in ports:
|
||||
mac = port
|
||||
extra = {}
|
||||
pxe_enabled = True
|
||||
if isinstance(port, dict):
|
||||
mac = port['mac']
|
||||
client_id = port.get('client_id')
|
||||
if client_id:
|
||||
extra = {'client-id': client_id}
|
||||
pxe_enabled = port.get('pxe', True)
|
||||
|
||||
if mac not in self.ports():
|
||||
self._create_port(mac, ironic=ironic, extra=extra,
|
||||
pxe_enabled=pxe_enabled)
|
||||
else:
|
||||
existing_macs.append(mac)
|
||||
|
||||
if existing_macs:
|
||||
LOG.warning('Did not create ports %s as they already exist',
|
||||
existing_macs, node_info=self)
|
||||
|
||||
def ports(self, ironic=None):
|
||||
"""Get Ironic port objects associated with the cached node record.
|
||||
|
||||
This value is cached as well, use invalidate_cache() to clean.
|
||||
|
||||
:return: dict MAC -> port object
|
||||
"""
|
||||
if self._ports is None:
|
||||
ironic = ironic or self.ironic
|
||||
port_list = ironic.node.list_ports(self.uuid, limit=0, detail=True)
|
||||
self._ports = {p.address: p for p in port_list}
|
||||
return self._ports
|
||||
|
||||
def _create_port(self, mac, ironic=None, **kwargs):
|
||||
ironic = ironic or self.ironic
|
||||
try:
|
||||
port = ironic.port.create(
|
||||
node_uuid=self.uuid, address=mac, **kwargs)
|
||||
LOG.info('Port %(uuid)s was created successfully, MAC: %(mac)s,'
|
||||
'attributes: %(attrs)s',
|
||||
{'uuid': port.uuid, 'mac': port.address,
|
||||
'attrs': kwargs},
|
||||
node_info=self)
|
||||
except exceptions.Conflict:
|
||||
LOG.warning('Port %s already exists, skipping',
|
||||
mac, node_info=self)
|
||||
# NOTE(dtantsur): we didn't get port object back, so we have to
|
||||
# reload ports on next access
|
||||
self._ports = None
|
||||
else:
|
||||
self._ports[mac] = port
|
||||
|
||||
def patch(self, patches, ironic=None):
|
||||
"""Apply JSON patches to a node.
|
||||
|
||||
Refreshes cached node instance.
|
||||
|
||||
:param patches: JSON patches to apply
|
||||
:param ironic: Ironic client to use instead of self.ironic
|
||||
:raises: ironicclient exceptions
|
||||
"""
|
||||
ironic = ironic or self.ironic
|
||||
# NOTE(aarefiev): support path w/o ahead forward slash
|
||||
# as Ironic cli does
|
||||
for patch in patches:
|
||||
if patch.get('path') and not patch['path'].startswith('/'):
|
||||
patch['path'] = '/' + patch['path']
|
||||
|
||||
LOG.debug('Updating node with patches %s', patches, node_info=self)
|
||||
self._node = ironic.node.update(self.uuid, patches)
|
||||
|
||||
def patch_port(self, port, patches, ironic=None):
|
||||
"""Apply JSON patches to a port.
|
||||
|
||||
:param port: port object or its MAC
|
||||
:param patches: JSON patches to apply
|
||||
:param ironic: Ironic client to use instead of self.ironic
|
||||
"""
|
||||
ironic = ironic or self.ironic
|
||||
ports = self.ports()
|
||||
if isinstance(port, six.string_types):
|
||||
port = ports[port]
|
||||
|
||||
LOG.debug('Updating port %(mac)s with patches %(patches)s',
|
||||
{'mac': port.address, 'patches': patches},
|
||||
node_info=self)
|
||||
new_port = ironic.port.update(port.uuid, patches)
|
||||
ports[port.address] = new_port
|
||||
|
||||
def update_properties(self, ironic=None, **props):
|
||||
"""Update properties on a node.
|
||||
|
||||
:param props: properties to update
|
||||
:param ironic: Ironic client to use instead of self.ironic
|
||||
"""
|
||||
ironic = ironic or self.ironic
|
||||
patches = [{'op': 'add', 'path': '/properties/%s' % k, 'value': v}
|
||||
for k, v in props.items()]
|
||||
self.patch(patches, ironic)
|
||||
|
||||
def update_capabilities(self, ironic=None, **caps):
|
||||
"""Update capabilities on a node.
|
||||
|
||||
:param caps: capabilities to update
|
||||
:param ironic: Ironic client to use instead of self.ironic
|
||||
"""
|
||||
existing = ir_utils.capabilities_to_dict(
|
||||
self.node().properties.get('capabilities'))
|
||||
existing.update(caps)
|
||||
self.update_properties(
|
||||
ironic=ironic,
|
||||
capabilities=ir_utils.dict_to_capabilities(existing))
|
||||
|
||||
def delete_port(self, port, ironic=None):
|
||||
"""Delete port.
|
||||
|
||||
:param port: port object or its MAC
|
||||
:param ironic: Ironic client to use instead of self.ironic
|
||||
"""
|
||||
ironic = ironic or self.ironic
|
||||
ports = self.ports()
|
||||
if isinstance(port, six.string_types):
|
||||
port = ports[port]
|
||||
|
||||
ironic.port.delete(port.uuid)
|
||||
del ports[port.address]
|
||||
|
||||
def get_by_path(self, path):
|
||||
"""Get field value by ironic-style path (e.g. /extra/foo).
|
||||
|
||||
:param path: path to a field
|
||||
:returns: field value
|
||||
:raises: KeyError if field was not found
|
||||
"""
|
||||
path = path.strip('/')
|
||||
try:
|
||||
if '/' in path:
|
||||
prop, key = path.split('/', 1)
|
||||
return getattr(self.node(), prop)[key]
|
||||
else:
|
||||
return getattr(self.node(), path)
|
||||
except AttributeError:
|
||||
raise KeyError(path)
|
||||
|
||||
def replace_field(self, path, func, **kwargs):
|
||||
"""Replace a field on ironic node.
|
||||
|
||||
:param path: path to a field as used by the ironic client
|
||||
:param func: function accepting an old value and returning a new one
|
||||
:param kwargs: if 'default' value is passed here, it will be used when
|
||||
no existing value is found.
|
||||
:raises: KeyError if value is not found and default is not set
|
||||
:raises: everything that patch() may raise
|
||||
"""
|
||||
ironic = kwargs.pop("ironic", None) or self.ironic
|
||||
try:
|
||||
value = self.get_by_path(path)
|
||||
op = 'replace'
|
||||
except KeyError:
|
||||
if 'default' in kwargs:
|
||||
value = kwargs['default']
|
||||
op = 'add'
|
||||
else:
|
||||
raise
|
||||
|
||||
ref_value = copy.deepcopy(value)
|
||||
value = func(value)
|
||||
if value != ref_value:
|
||||
self.patch([{'op': op, 'path': path, 'value': value}], ironic)
|
||||
|
||||
|
||||
def triggers_fsm_error_transition(errors=(Exception,),
|
||||
no_errors=(utils.NodeStateInvalidEvent,
|
||||
utils.NodeStateRaceCondition)):
|
||||
"""Trigger an fsm error transition upon certain errors.
|
||||
|
||||
It is assumed the first function arg of the decorated function is always a
|
||||
NodeInfo instance.
|
||||
|
||||
:param errors: a tuple of exceptions upon which an error
|
||||
event is triggered. Re-raised.
|
||||
:param no_errors: a tuple of exceptions that won't trigger the
|
||||
error event.
|
||||
"""
|
||||
def outer(func):
|
||||
@six.wraps(func)
|
||||
def inner(node_info, *args, **kwargs):
|
||||
ret = None
|
||||
try:
|
||||
ret = func(node_info, *args, **kwargs)
|
||||
except no_errors as exc:
|
||||
LOG.debug('Not processing error event for the '
|
||||
'exception: %(exc)s raised by %(func)s',
|
||||
{'exc': exc,
|
||||
'func': reflection.get_callable_name(func)},
|
||||
node_info=node_info)
|
||||
except errors as exc:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Processing the error event because of an '
|
||||
'exception %(exc_type)s: %(exc)s raised by '
|
||||
'%(func)s',
|
||||
{'exc_type': type(exc), 'exc': exc,
|
||||
'func': reflection.get_callable_name(func)},
|
||||
node_info=node_info)
|
||||
# an error event should be possible from all states
|
||||
node_info.fsm_event(istate.Events.error)
|
||||
return ret
|
||||
return inner
|
||||
return outer
|
||||
|
||||
|
||||
def fsm_event_before(event, strict=False):
|
||||
"""Trigger an fsm event before the function execution.
|
||||
|
||||
It is assumed the first function arg of the decorated function is always a
|
||||
NodeInfo instance.
|
||||
|
||||
:param event: the event to process before the function call
|
||||
:param strict: make an invalid fsm event trigger an error event
|
||||
"""
|
||||
def outer(func):
|
||||
@six.wraps(func)
|
||||
def inner(node_info, *args, **kwargs):
|
||||
LOG.debug('Processing event %(event)s before calling '
|
||||
'%(func)s', {'event': event, 'func': func},
|
||||
node_info=node_info)
|
||||
node_info.fsm_event(event, strict=strict)
|
||||
return func(node_info, *args, **kwargs)
|
||||
return inner
|
||||
return outer
|
||||
|
||||
|
||||
def fsm_event_after(event, strict=False):
|
||||
"""Trigger an fsm event after the function execution.
|
||||
|
||||
It is assumed the first function arg of the decorated function is always a
|
||||
NodeInfo instance.
|
||||
|
||||
:param event: the event to process after the function call
|
||||
:param strict: make an invalid fsm event trigger an error event
|
||||
"""
|
||||
def outer(func):
|
||||
@six.wraps(func)
|
||||
def inner(node_info, *args, **kwargs):
|
||||
ret = func(node_info, *args, **kwargs)
|
||||
LOG.debug('Processing event %(event)s after calling '
|
||||
'%(func)s', {'event': event, 'func': func},
|
||||
node_info=node_info)
|
||||
node_info.fsm_event(event, strict=strict)
|
||||
return ret
|
||||
return inner
|
||||
return outer
|
||||
|
||||
|
||||
def fsm_transition(event, reentrant=True, **exc_kwargs):
|
||||
"""Decorate a function to perform a (non-)reentrant transition.
|
||||
|
||||
If True, reentrant transition will be performed at the end of a function
|
||||
call. If False, the transition will be performed before the function call.
|
||||
The function is decorated with the triggers_fsm_error_transition decorator
|
||||
as well.
|
||||
|
||||
:param event: the event to bind the transition to.
|
||||
:param reentrant: whether the transition is reentrant.
|
||||
:param exc_kwargs: passed on to the triggers_fsm_error_transition decorator
|
||||
"""
|
||||
def outer(func):
|
||||
inner = triggers_fsm_error_transition(**exc_kwargs)(func)
|
||||
if not reentrant:
|
||||
return fsm_event_before(event, strict=True)(inner)
|
||||
return fsm_event_after(event)(inner)
|
||||
return outer
|
||||
|
||||
|
||||
def release_lock(func):
|
||||
"""Decorate a node_info-function to release the node_info lock.
|
||||
|
||||
Assumes the first parameter of the function func is always a NodeInfo
|
||||
instance.
|
||||
|
||||
"""
|
||||
@six.wraps(func)
|
||||
def inner(node_info, *args, **kwargs):
|
||||
try:
|
||||
return func(node_info, *args, **kwargs)
|
||||
finally:
|
||||
# FIXME(milan) hacking the test cases to work
|
||||
# with release_lock.assert_called_once...
|
||||
if node_info._locked:
|
||||
node_info.release_lock()
|
||||
return inner
|
||||
|
||||
|
||||
def start_introspection(uuid, **kwargs):
|
||||
"""Start the introspection of a node.
|
||||
|
||||
If a node_info record exists in the DB, a start transition is used rather
|
||||
than dropping the record in order to check for the start transition
|
||||
validity in particular node state.
|
||||
|
||||
:param uuid: Ironic node UUID
|
||||
:param kwargs: passed on to add_node()
|
||||
:raises: NodeStateInvalidEvent in case the start transition is invalid in
|
||||
the current node state
|
||||
:raises: NodeStateRaceCondition if a mismatch was detected between the
|
||||
node_info cache and the DB
|
||||
:returns: NodeInfo
|
||||
"""
|
||||
with db.ensure_transaction():
|
||||
node_info = NodeInfo(uuid)
|
||||
# check that the start transition is possible
|
||||
try:
|
||||
node_info.fsm_event(istate.Events.start)
|
||||
except utils.NotFoundInCacheError:
|
||||
# node not found while in the fsm_event handler
|
||||
LOG.debug('Node missing in the cache; adding it now',
|
||||
node_info=node_info)
|
||||
state = istate.States.starting
|
||||
else:
|
||||
state = node_info.state
|
||||
return add_node(uuid, state, **kwargs)
|
||||
|
||||
|
||||
def add_node(uuid, state, **attributes):
|
||||
"""Store information about a node under introspection.
|
||||
|
||||
All existing information about this node is dropped.
|
||||
Empty values are skipped.
|
||||
|
||||
:param uuid: Ironic node UUID
|
||||
:param state: The initial state of the node
|
||||
:param attributes: attributes known about this node (like macs, BMC etc);
|
||||
also ironic client instance may be passed under 'ironic'
|
||||
:returns: NodeInfo
|
||||
"""
|
||||
started_at = timeutils.utcnow()
|
||||
with db.ensure_transaction() as session:
|
||||
_delete_node(uuid)
|
||||
db.Node(uuid=uuid, state=state, started_at=started_at).save(session)
|
||||
|
||||
node_info = NodeInfo(uuid=uuid, state=state, started_at=started_at,
|
||||
ironic=attributes.pop('ironic', None))
|
||||
for (name, value) in attributes.items():
|
||||
if not value:
|
||||
continue
|
||||
node_info.add_attribute(name, value, session=session)
|
||||
|
||||
return node_info
|
||||
|
||||
|
||||
def delete_nodes_not_in_list(uuids):
|
||||
"""Delete nodes which don't exist in Ironic node UUIDs.
|
||||
|
||||
:param uuids: Ironic node UUIDs
|
||||
"""
|
||||
inspector_uuids = _list_node_uuids()
|
||||
for uuid in inspector_uuids - uuids:
|
||||
LOG.warning('Node %s was deleted from Ironic, dropping from Ironic '
|
||||
'Inspector database', uuid)
|
||||
with _get_lock_ctx(uuid):
|
||||
_delete_node(uuid)
|
||||
|
||||
|
||||
def _delete_node(uuid, session=None):
|
||||
"""Delete information about a node.
|
||||
|
||||
:param uuid: Ironic node UUID
|
||||
:param session: optional existing database session
|
||||
"""
|
||||
with db.ensure_transaction(session) as session:
|
||||
db.model_query(db.Attribute, session=session).filter_by(
|
||||
node_uuid=uuid).delete()
|
||||
for model in (db.Option, db.Node):
|
||||
db.model_query(model,
|
||||
session=session).filter_by(uuid=uuid).delete()
|
||||
|
||||
|
||||
def introspection_active():
|
||||
"""Check if introspection is active for at least one node."""
|
||||
# FIXME(dtantsur): is there a better way to express it?
|
||||
return (db.model_query(db.Node.uuid).filter_by(finished_at=None).first()
|
||||
is not None)
|
||||
|
||||
|
||||
def active_macs():
|
||||
"""List all MAC's that are on introspection right now."""
|
||||
return ({x.value for x in db.model_query(db.Attribute.value).
|
||||
filter_by(name=MACS_ATTRIBUTE)})
|
||||
|
||||
|
||||
def _list_node_uuids():
|
||||
"""Get all nodes' uuid from cache.
|
||||
|
||||
:returns: Set of nodes' uuid.
|
||||
"""
|
||||
return {x.uuid for x in db.model_query(db.Node.uuid)}
|
||||
|
||||
|
||||
def get_node(node_id, ironic=None, locked=False):
|
||||
"""Get node from cache.
|
||||
|
||||
:param node_id: node UUID or name.
|
||||
:param ironic: optional ironic client instance
|
||||
:param locked: if True, get a lock on node before fetching its data
|
||||
:returns: structure NodeInfo.
|
||||
"""
|
||||
if uuidutils.is_uuid_like(node_id):
|
||||
node = None
|
||||
uuid = node_id
|
||||
else:
|
||||
node = ir_utils.get_node(node_id, ironic=ironic)
|
||||
uuid = node.uuid
|
||||
|
||||
if locked:
|
||||
lock = _get_lock(uuid)
|
||||
lock.acquire()
|
||||
else:
|
||||
lock = None
|
||||
|
||||
try:
|
||||
row = db.model_query(db.Node).filter_by(uuid=uuid).first()
|
||||
if row is None:
|
||||
raise utils.Error(_('Could not find node %s in cache') % uuid,
|
||||
code=404)
|
||||
return NodeInfo.from_row(row, ironic=ironic, lock=lock, node=node)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
if lock is not None:
|
||||
lock.release()
|
||||
|
||||
|
||||
def find_node(**attributes):
|
||||
"""Find node in cache.
|
||||
|
||||
Looks up a node based on attributes in a best-match fashion.
|
||||
This function acquires a lock on a node.
|
||||
|
||||
:param attributes: attributes known about this node (like macs, BMC etc)
|
||||
also ironic client instance may be passed under 'ironic'
|
||||
:returns: structure NodeInfo with attributes ``uuid`` and ``created_at``
|
||||
:raises: Error if node is not found or multiple nodes match the attributes
|
||||
"""
|
||||
ironic = attributes.pop('ironic', None)
|
||||
# NOTE(dtantsur): sorting is not required, but gives us predictability
|
||||
found = collections.Counter()
|
||||
|
||||
for (name, value) in sorted(attributes.items()):
|
||||
if not value:
|
||||
LOG.debug('Empty value for attribute %s', name)
|
||||
continue
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
LOG.debug('Trying to use %s of value %s for node look up',
|
||||
name, value)
|
||||
value_list = []
|
||||
for v in value:
|
||||
value_list.append("name='%s' AND value='%s'" % (name, v))
|
||||
stmt = ('select distinct node_uuid from attributes where ' +
|
||||
' OR '.join(value_list))
|
||||
rows = (db.model_query(db.Attribute.node_uuid).from_statement(
|
||||
text(stmt)).all())
|
||||
found.update(row.node_uuid for row in rows)
|
||||
|
||||
if not found:
|
||||
raise utils.NotFoundInCacheError(_(
|
||||
'Could not find a node for attributes %s') % attributes)
|
||||
|
||||
most_common = found.most_common()
|
||||
LOG.debug('The following nodes match the attributes: %(attributes)s, '
|
||||
'scoring: %(most_common)s',
|
||||
{'most_common': ', '.join('%s: %d' % tpl for tpl in most_common),
|
||||
'attributes': ', '.join('%s=%s' % tpl for tpl in
|
||||
attributes.items())})
|
||||
|
||||
# NOTE(milan) most_common is sorted, higher scores first
|
||||
highest_score = most_common[0][1]
|
||||
found = [item[0] for item in most_common if highest_score == item[1]]
|
||||
if len(found) > 1:
|
||||
raise utils.Error(_(
|
||||
'Multiple nodes match the same number of attributes '
|
||||
'%(attr)s: %(found)s')
|
||||
% {'attr': attributes, 'found': found}, code=404)
|
||||
|
||||
uuid = found.pop()
|
||||
node_info = NodeInfo(uuid=uuid, ironic=ironic)
|
||||
node_info.acquire_lock()
|
||||
|
||||
try:
|
||||
row = (db.model_query(db.Node.started_at, db.Node.finished_at).
|
||||
filter_by(uuid=uuid).first())
|
||||
|
||||
if not row:
|
||||
raise utils.Error(_(
|
||||
'Could not find node %s in introspection cache, '
|
||||
'probably it\'s not on introspection now') % uuid, code=404)
|
||||
|
||||
if row.finished_at:
|
||||
raise utils.Error(_(
|
||||
'Introspection for node %(node)s already finished on '
|
||||
'%(finish)s') % {'node': uuid, 'finish': row.finished_at})
|
||||
|
||||
node_info.started_at = row.started_at
|
||||
return node_info
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
node_info.release_lock()
|
||||
|
||||
|
||||
def clean_up():
|
||||
"""Clean up the cache.
|
||||
|
||||
* Finish introspection for timed out nodes.
|
||||
* Drop outdated node status information.
|
||||
|
||||
:return: list of timed out node UUID's
|
||||
"""
|
||||
if CONF.node_status_keep_time > 0:
|
||||
status_keep_threshold = (timeutils.utcnow() - datetime.timedelta(
|
||||
seconds=CONF.node_status_keep_time))
|
||||
with db.ensure_transaction() as session:
|
||||
db.model_query(db.Node, session=session).filter(
|
||||
db.Node.finished_at.isnot(None),
|
||||
db.Node.finished_at < status_keep_threshold).delete()
|
||||
|
||||
timeout = CONF.timeout
|
||||
if timeout <= 0:
|
||||
return []
|
||||
threshold = timeutils.utcnow() - datetime.timedelta(seconds=timeout)
|
||||
uuids = [row.uuid for row in
|
||||
db.model_query(db.Node.uuid).filter(
|
||||
db.Node.started_at < threshold,
|
||||
db.Node.finished_at.is_(None)).all()]
|
||||
|
||||
if not uuids:
|
||||
return []
|
||||
|
||||
LOG.error('Introspection for nodes %s has timed out', uuids)
|
||||
for u in uuids:
|
||||
node_info = get_node(u, locked=True)
|
||||
try:
|
||||
if node_info.finished_at or node_info.started_at > threshold:
|
||||
continue
|
||||
if node_info.state != istate.States.waiting:
|
||||
LOG.error('Something went wrong, timeout occurred '
|
||||
'while introspection in "%s" state',
|
||||
node_info.state,
|
||||
node_info=node_info)
|
||||
node_info.fsm_event(istate.Events.timeout)
|
||||
node_info.finished(error='Introspection timeout')
|
||||
finally:
|
||||
node_info.release_lock()
|
||||
|
||||
return uuids
|
||||
|
||||
|
||||
def create_node(driver, ironic=None, **attributes):
|
||||
"""Create ironic node and cache it.
|
||||
|
||||
* Create new node in ironic.
|
||||
* Cache it in inspector.
|
||||
* Sets node_info state to enrolling.
|
||||
|
||||
:param driver: driver for Ironic node.
|
||||
:param ironic: ronic client instance.
|
||||
:param attributes: dict, additional keyword arguments to pass
|
||||
to the ironic client on node creation.
|
||||
:return: NodeInfo, or None in case error happened.
|
||||
"""
|
||||
if ironic is None:
|
||||
ironic = ir_utils.get_client()
|
||||
try:
|
||||
node = ironic.node.create(driver=driver, **attributes)
|
||||
except exceptions.InvalidAttribute as e:
|
||||
LOG.error('Failed to create new node: %s', e)
|
||||
else:
|
||||
LOG.info('Node %s was created successfully', node.uuid)
|
||||
return add_node(node.uuid, istate.States.enrolling, ironic=ironic)
|
||||
|
||||
|
||||
def get_node_list(ironic=None, marker=None, limit=None):
|
||||
"""Get node list from the cache.
|
||||
|
||||
The list of the nodes is ordered based on the (started_at, uuid)
|
||||
attribute pair, newer items first.
|
||||
|
||||
:param ironic: optional ironic client instance
|
||||
:param marker: pagination marker (an UUID or None)
|
||||
:param limit: pagination limit; None for default CONF.api_max_limit
|
||||
:returns: a list of NodeInfo instances.
|
||||
"""
|
||||
if marker is not None:
|
||||
# uuid marker -> row marker for pagination
|
||||
marker = db.model_query(db.Node).get(marker)
|
||||
if marker is None:
|
||||
raise utils.Error(_('Node not found for marker: %s') % marker,
|
||||
code=404)
|
||||
|
||||
rows = db.model_query(db.Node)
|
||||
# ordered based on (started_at, uuid); newer first
|
||||
rows = db_utils.paginate_query(rows, db.Node, limit,
|
||||
('started_at', 'uuid'),
|
||||
marker=marker, sort_dir='desc')
|
||||
return [NodeInfo.from_row(row, ironic=ironic) for row in rows]
|
@ -1,231 +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.
|
||||
|
||||
"""Base code for plugins support."""
|
||||
|
||||
import abc
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
import six
|
||||
import stevedore
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ProcessingHook(object): # pragma: no cover
|
||||
"""Abstract base class for introspection data processing hooks."""
|
||||
|
||||
dependencies = []
|
||||
"""An ordered list of hooks that must be enabled before this one.
|
||||
|
||||
The items here should be entry point names, not classes.
|
||||
"""
|
||||
|
||||
def before_processing(self, introspection_data, **kwargs):
|
||||
"""Hook to run before any other data processing.
|
||||
|
||||
This hook is run even before sanity checks.
|
||||
|
||||
:param introspection_data: raw information sent by the ramdisk,
|
||||
may be modified by the hook.
|
||||
:param kwargs: used for extensibility without breaking existing hooks
|
||||
:returns: nothing.
|
||||
"""
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
"""Hook to run before Ironic node update.
|
||||
|
||||
This hook is run after node is found and ports are created,
|
||||
just before the node is updated with the data.
|
||||
|
||||
:param introspection_data: processed data from the ramdisk.
|
||||
:param node_info: NodeInfo instance.
|
||||
:param kwargs: used for extensibility without breaking existing hooks.
|
||||
:returns: nothing.
|
||||
|
||||
[RFC 6902] - http://tools.ietf.org/html/rfc6902
|
||||
"""
|
||||
|
||||
|
||||
class WithValidation(object):
|
||||
REQUIRED_PARAMS = set()
|
||||
"""Set with names of required parameters."""
|
||||
|
||||
OPTIONAL_PARAMS = set()
|
||||
"""Set with names of optional parameters."""
|
||||
|
||||
def validate(self, params, **kwargs):
|
||||
"""Validate params passed during creation.
|
||||
|
||||
Default implementation checks for presence of fields from
|
||||
REQUIRED_PARAMS and fails for unexpected fields (not from
|
||||
REQUIRED_PARAMS + OPTIONAL_PARAMS).
|
||||
|
||||
:param params: params as a dictionary
|
||||
:param kwargs: used for extensibility without breaking existing plugins
|
||||
:raises: ValueError on validation failure
|
||||
"""
|
||||
passed = {k for k, v in params.items() if v is not None}
|
||||
missing = self.REQUIRED_PARAMS - passed
|
||||
unexpected = passed - self.REQUIRED_PARAMS - self.OPTIONAL_PARAMS
|
||||
|
||||
msg = []
|
||||
if missing:
|
||||
msg.append(_('missing required parameter(s): %s')
|
||||
% ', '.join(missing))
|
||||
if unexpected:
|
||||
msg.append(_('unexpected parameter(s): %s')
|
||||
% ', '.join(unexpected))
|
||||
|
||||
if msg:
|
||||
raise ValueError('; '.join(msg))
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class RuleConditionPlugin(WithValidation): # pragma: no cover
|
||||
"""Abstract base class for rule condition plugins."""
|
||||
|
||||
REQUIRED_PARAMS = {'value'}
|
||||
|
||||
ALLOW_NONE = False
|
||||
"""Whether this condition accepts None when field is not found."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def check(self, node_info, field, params, **kwargs):
|
||||
"""Check if condition holds for a given field.
|
||||
|
||||
:param node_info: NodeInfo object
|
||||
:param field: field value
|
||||
:param params: parameters as a dictionary, changing it here will change
|
||||
what will be stored in database
|
||||
:param kwargs: used for extensibility without breaking existing plugins
|
||||
:raises ValueError: on unacceptable field value
|
||||
:returns: True if check succeeded, otherwise False
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class RuleActionPlugin(WithValidation): # pragma: no cover
|
||||
"""Abstract base class for rule action plugins."""
|
||||
|
||||
FORMATTED_PARAMS = []
|
||||
"""List of params will be formatted with python format."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply(self, node_info, params, **kwargs):
|
||||
"""Run action on successful rule match.
|
||||
|
||||
:param node_info: NodeInfo object
|
||||
:param params: parameters as a dictionary
|
||||
:param kwargs: used for extensibility without breaking existing plugins
|
||||
:raises: utils.Error on failure
|
||||
"""
|
||||
|
||||
|
||||
_HOOKS_MGR = None
|
||||
_NOT_FOUND_HOOK_MGR = None
|
||||
_CONDITIONS_MGR = None
|
||||
_ACTIONS_MGR = None
|
||||
|
||||
|
||||
def missing_entrypoints_callback(names):
|
||||
"""Raise MissingHookError with comma-separated list of missing hooks"""
|
||||
error = _('The following hook(s) are missing or failed to load: %s')
|
||||
raise RuntimeError(error % ', '.join(names))
|
||||
|
||||
|
||||
def processing_hooks_manager(*args):
|
||||
"""Create a Stevedore extension manager for processing hooks.
|
||||
|
||||
:param args: arguments to pass to the hooks constructor.
|
||||
"""
|
||||
global _HOOKS_MGR
|
||||
if _HOOKS_MGR is None:
|
||||
names = [x.strip()
|
||||
for x in CONF.processing.processing_hooks.split(',')
|
||||
if x.strip()]
|
||||
_HOOKS_MGR = stevedore.NamedExtensionManager(
|
||||
'ironic_inspector.hooks.processing',
|
||||
names=names,
|
||||
invoke_on_load=True,
|
||||
invoke_args=args,
|
||||
on_missing_entrypoints_callback=missing_entrypoints_callback,
|
||||
name_order=True)
|
||||
return _HOOKS_MGR
|
||||
|
||||
|
||||
def validate_processing_hooks():
|
||||
"""Validate the enabled processing hooks.
|
||||
|
||||
:raises: MissingHookError on missing or failed to load hooks
|
||||
:raises: RuntimeError on validation failure
|
||||
:returns: the list of hooks passed validation
|
||||
"""
|
||||
hooks = [ext for ext in processing_hooks_manager()]
|
||||
enabled = set()
|
||||
errors = []
|
||||
for hook in hooks:
|
||||
deps = getattr(hook.obj, 'dependencies', ())
|
||||
missing = [d for d in deps if d not in enabled]
|
||||
if missing:
|
||||
errors.append('Hook %(hook)s requires the following hooks to be '
|
||||
'enabled before it: %(deps)s. The following hooks '
|
||||
'are missing: %(missing)s.' %
|
||||
{'hook': hook.name,
|
||||
'deps': ', '.join(deps),
|
||||
'missing': ', '.join(missing)})
|
||||
enabled.add(hook.name)
|
||||
|
||||
if errors:
|
||||
raise RuntimeError("Some hooks failed to load due to dependency "
|
||||
"problems:\n%s" % "\n".join(errors))
|
||||
|
||||
return hooks
|
||||
|
||||
|
||||
def node_not_found_hook_manager(*args):
|
||||
global _NOT_FOUND_HOOK_MGR
|
||||
if _NOT_FOUND_HOOK_MGR is None:
|
||||
name = CONF.processing.node_not_found_hook
|
||||
if name:
|
||||
_NOT_FOUND_HOOK_MGR = stevedore.DriverManager(
|
||||
'ironic_inspector.hooks.node_not_found',
|
||||
name=name)
|
||||
|
||||
return _NOT_FOUND_HOOK_MGR
|
||||
|
||||
|
||||
def rule_conditions_manager():
|
||||
"""Create a Stevedore extension manager for conditions in rules."""
|
||||
global _CONDITIONS_MGR
|
||||
if _CONDITIONS_MGR is None:
|
||||
_CONDITIONS_MGR = stevedore.ExtensionManager(
|
||||
'ironic_inspector.rules.conditions',
|
||||
invoke_on_load=True)
|
||||
return _CONDITIONS_MGR
|
||||
|
||||
|
||||
def rule_actions_manager():
|
||||
"""Create a Stevedore extension manager for actions in rules."""
|
||||
global _ACTIONS_MGR
|
||||
if _ACTIONS_MGR is None:
|
||||
_ACTIONS_MGR = stevedore.ExtensionManager(
|
||||
'ironic_inspector.rules.actions',
|
||||
invoke_on_load=True)
|
||||
return _ACTIONS_MGR
|
@ -1,101 +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.
|
||||
|
||||
"""Gather capabilities from inventory."""
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.plugins import base
|
||||
from ironic_inspector import utils
|
||||
|
||||
|
||||
DEFAULT_CPU_FLAGS_MAPPING = {
|
||||
'vmx': 'cpu_vt',
|
||||
'svm': 'cpu_vt',
|
||||
'aes': 'cpu_aes',
|
||||
'pse': 'cpu_hugepages',
|
||||
'pdpe1gb': 'cpu_hugepages_1g',
|
||||
'smx': 'cpu_txt',
|
||||
}
|
||||
|
||||
CAPABILITIES_OPTS = [
|
||||
cfg.BoolOpt('boot_mode',
|
||||
default=False,
|
||||
help=_('Whether to store the boot mode (BIOS or UEFI).')),
|
||||
cfg.DictOpt('cpu_flags',
|
||||
default=DEFAULT_CPU_FLAGS_MAPPING,
|
||||
help=_('Mapping between a CPU flag and a capability to set '
|
||||
'if this flag is present.')),
|
||||
]
|
||||
|
||||
|
||||
def list_opts():
|
||||
return [
|
||||
('capabilities', CAPABILITIES_OPTS)
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(CAPABILITIES_OPTS, group='capabilities')
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
|
||||
class CapabilitiesHook(base.ProcessingHook):
|
||||
"""Processing hook for detecting capabilities."""
|
||||
|
||||
def _detect_boot_mode(self, inventory, node_info, data=None):
|
||||
boot_mode = inventory.get('boot', {}).get('current_boot_mode')
|
||||
if boot_mode is not None:
|
||||
LOG.info('Boot mode was %s', boot_mode,
|
||||
data=data, node_info=node_info)
|
||||
return {'boot_mode': boot_mode}
|
||||
else:
|
||||
LOG.warning('No boot mode information available',
|
||||
data=data, node_info=node_info)
|
||||
return {}
|
||||
|
||||
def _detect_cpu_flags(self, inventory, node_info, data=None):
|
||||
flags = inventory['cpu'].get('flags')
|
||||
if not flags:
|
||||
LOG.warning('No CPU flags available, please update your '
|
||||
'introspection ramdisk',
|
||||
data=data, node_info=node_info)
|
||||
return {}
|
||||
|
||||
flags = set(flags)
|
||||
caps = {}
|
||||
for flag, name in CONF.capabilities.cpu_flags.items():
|
||||
if flag in flags:
|
||||
caps[name] = 'true'
|
||||
|
||||
LOG.info('CPU capabilities: %s', list(caps),
|
||||
data=data, node_info=node_info)
|
||||
return caps
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
inventory = utils.get_inventory(introspection_data)
|
||||
caps = {}
|
||||
if CONF.capabilities.boot_mode:
|
||||
caps.update(self._detect_boot_mode(inventory, node_info,
|
||||
introspection_data))
|
||||
|
||||
caps.update(self._detect_cpu_flags(inventory, node_info,
|
||||
introspection_data))
|
||||
|
||||
if caps:
|
||||
LOG.debug('New capabilities: %s', caps, node_info=node_info,
|
||||
data=introspection_data)
|
||||
node_info.update_capabilities(**caps)
|
||||
else:
|
||||
LOG.debug('No new capabilities detected', node_info=node_info,
|
||||
data=introspection_data)
|
@ -1,102 +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.
|
||||
|
||||
"""Enroll node not found hook hook."""
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector import utils
|
||||
|
||||
|
||||
DISCOVERY_OPTS = [
|
||||
cfg.StrOpt('enroll_node_driver',
|
||||
default='fake',
|
||||
help=_('The name of the Ironic driver used by the enroll '
|
||||
'hook when creating a new node in Ironic.')),
|
||||
]
|
||||
|
||||
|
||||
def list_opts():
|
||||
return [
|
||||
('discovery', DISCOVERY_OPTS)
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(DISCOVERY_OPTS, group='discovery')
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
|
||||
def _extract_node_driver_info(introspection_data):
|
||||
node_driver_info = {}
|
||||
ipmi_address = utils.get_ipmi_address_from_data(introspection_data)
|
||||
if ipmi_address:
|
||||
node_driver_info['ipmi_address'] = ipmi_address
|
||||
else:
|
||||
LOG.warning('No BMC address provided, discovered node will be '
|
||||
'created without ipmi address')
|
||||
return node_driver_info
|
||||
|
||||
|
||||
def _check_existing_nodes(introspection_data, node_driver_info, ironic):
|
||||
macs = utils.get_valid_macs(introspection_data)
|
||||
if macs:
|
||||
# verify existing ports
|
||||
for mac in macs:
|
||||
ports = ironic.port.list(address=mac)
|
||||
if not ports:
|
||||
continue
|
||||
raise utils.Error(
|
||||
_('Port %(mac)s already exists, uuid: %(uuid)s') %
|
||||
{'mac': mac, 'uuid': ports[0].uuid}, data=introspection_data)
|
||||
else:
|
||||
LOG.warning('No suitable interfaces found for discovered node. '
|
||||
'Check that validate_interfaces hook is listed in '
|
||||
'[processing]default_processing_hooks config option')
|
||||
|
||||
# verify existing node with discovered ipmi address
|
||||
ipmi_address = node_driver_info.get('ipmi_address')
|
||||
if ipmi_address:
|
||||
# FIXME(aarefiev): it's not effective to fetch all nodes, and may
|
||||
# impact on performance on big clusters
|
||||
nodes = ironic.node.list(fields=('uuid', 'driver_info'), limit=0)
|
||||
for node in nodes:
|
||||
if ipmi_address == ir_utils.get_ipmi_address(node):
|
||||
raise utils.Error(
|
||||
_('Node %(uuid)s already has BMC address '
|
||||
'%(ipmi_address)s, not enrolling') %
|
||||
{'ipmi_address': ipmi_address, 'uuid': node.uuid},
|
||||
data=introspection_data)
|
||||
|
||||
|
||||
def enroll_node_not_found_hook(introspection_data, **kwargs):
|
||||
node_attr = {}
|
||||
ironic = ir_utils.get_client()
|
||||
|
||||
node_driver_info = _extract_node_driver_info(introspection_data)
|
||||
node_attr['driver_info'] = node_driver_info
|
||||
|
||||
node_driver = CONF.discovery.enroll_node_driver
|
||||
|
||||
_check_existing_nodes(introspection_data, node_driver_info, ironic)
|
||||
LOG.debug('Creating discovered node with driver %(driver)s and '
|
||||
'attributes: %(attr)s',
|
||||
{'driver': node_driver, 'attr': node_attr},
|
||||
data=introspection_data)
|
||||
# NOTE(aarefiev): This flag allows to distinguish enrolled manually
|
||||
# and auto-discovered nodes in the introspection rules.
|
||||
introspection_data['auto_discovered'] = True
|
||||
return node_cache.create_node(node_driver, ironic=ironic, **node_attr)
|
@ -1,39 +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.
|
||||
|
||||
"""Example plugin."""
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from ironic_inspector.plugins import base
|
||||
|
||||
|
||||
LOG = log.getLogger('ironic_inspector.plugins.example')
|
||||
|
||||
|
||||
class ExampleProcessingHook(base.ProcessingHook): # pragma: no cover
|
||||
def before_processing(self, introspection_data, **kwargs):
|
||||
LOG.debug('before_processing: %s', introspection_data)
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
LOG.debug('before_update: %s (node %s)', introspection_data,
|
||||
node_info.uuid)
|
||||
|
||||
|
||||
def example_not_found_hook(introspection_data, **kwargs):
|
||||
LOG.debug('Processing node not found %s', introspection_data)
|
||||
|
||||
|
||||
class ExampleRuleAction(base.RuleActionPlugin): # pragma: no cover
|
||||
def apply(self, node_info, params, **kwargs):
|
||||
LOG.debug('apply action to %s: %s', node_info.uuid, params)
|
@ -1,98 +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.
|
||||
|
||||
"""Plugin to store extra hardware information in Swift.
|
||||
|
||||
Stores the value of the 'data' key returned by the ramdisk as a JSON encoded
|
||||
string in a Swift object. The object is named 'extra_hardware-<node uuid>' and
|
||||
is stored in the 'inspector' container.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from ironic_inspector.common import swift
|
||||
from ironic_inspector.plugins import base
|
||||
from ironic_inspector import utils
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
EDEPLOY_ITEM_SIZE = 4
|
||||
|
||||
|
||||
class ExtraHardwareHook(base.ProcessingHook):
|
||||
"""Processing hook for saving extra hardware information in Swift."""
|
||||
|
||||
def _store_extra_hardware(self, name, data):
|
||||
"""Handles storing the extra hardware data from the ramdisk"""
|
||||
swift_api = swift.SwiftAPI()
|
||||
swift_api.create_object(name, data)
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
"""Stores the 'data' key from introspection_data in Swift.
|
||||
|
||||
If the 'data' key exists, updates Ironic extra column
|
||||
'hardware_swift_object' key to the name of the Swift object, and stores
|
||||
the data in the 'inspector' container in Swift.
|
||||
|
||||
Otherwise, it does nothing.
|
||||
"""
|
||||
if 'data' not in introspection_data:
|
||||
LOG.warning('No extra hardware information was received from '
|
||||
'the ramdisk', node_info=node_info,
|
||||
data=introspection_data)
|
||||
return
|
||||
data = introspection_data['data']
|
||||
|
||||
name = 'extra_hardware-%s' % node_info.uuid
|
||||
self._store_extra_hardware(name, json.dumps(data))
|
||||
|
||||
# NOTE(sambetts) If data is edeploy format, convert to dicts for rules
|
||||
# processing, store converted data in introspection_data['extra'].
|
||||
# Delete introspection_data['data'], it is assumed unusable
|
||||
# by rules.
|
||||
if self._is_edeploy_data(data):
|
||||
LOG.debug('Extra hardware data is in eDeploy format, '
|
||||
'converting to usable format',
|
||||
node_info=node_info, data=introspection_data)
|
||||
introspection_data['extra'] = self._convert_edeploy_data(data)
|
||||
else:
|
||||
LOG.warning('Extra hardware data was not in a recognised '
|
||||
'format (eDeploy), and will not be forwarded to '
|
||||
'introspection rules', node_info=node_info,
|
||||
data=introspection_data)
|
||||
|
||||
LOG.debug('Deleting \"data\" key from introspection data as it is '
|
||||
'assumed unusable by introspection rules. Raw data is '
|
||||
'stored in swift',
|
||||
node_info=node_info, data=introspection_data)
|
||||
del introspection_data['data']
|
||||
|
||||
node_info.patch([{'op': 'add', 'path': '/extra/hardware_swift_object',
|
||||
'value': name}])
|
||||
|
||||
def _is_edeploy_data(self, data):
|
||||
return all(isinstance(item, list) and len(item) == EDEPLOY_ITEM_SIZE
|
||||
for item in data)
|
||||
|
||||
def _convert_edeploy_data(self, data):
|
||||
converted = {}
|
||||
for item in data:
|
||||
converted_0 = converted.setdefault(item[0], {})
|
||||
converted_1 = converted_0.setdefault(item[1], {})
|
||||
|
||||
try:
|
||||
item[3] = int(item[3])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
converted_1[item[2]] = item[3]
|
||||
return converted
|
@ -1,87 +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.
|
||||
|
||||
"""LLDP Processing Hook for basic TLVs"""
|
||||
|
||||
import binascii
|
||||
|
||||
from ironic_inspector.common import lldp_parsers
|
||||
from ironic_inspector.plugins import base
|
||||
from ironic_inspector import utils
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
|
||||
class LLDPBasicProcessingHook(base.ProcessingHook):
|
||||
"""Process mandatory and optional LLDP packet fields
|
||||
|
||||
Loop through raw LLDP TLVs and parse those from the
|
||||
basic management, 802.1, and 802.3 TLV sets.
|
||||
Store parsed data back to the ironic-inspector database.
|
||||
"""
|
||||
|
||||
def _parse_lldp_tlvs(self, tlvs, node_info):
|
||||
"""Parse LLDP TLVs into dictionary of name/value pairs
|
||||
|
||||
:param tlvs: list of raw TLVs
|
||||
:param node_info: node being introspected
|
||||
:returns nv: dictionary of name/value pairs. The
|
||||
LLDP user-friendly names, e.g.
|
||||
"switch_port_id" are the keys
|
||||
"""
|
||||
|
||||
# Generate name/value pairs for each TLV supported by this plugin.
|
||||
parser = lldp_parsers.LLDPBasicMgmtParser(node_info)
|
||||
|
||||
for tlv_type, tlv_value in tlvs:
|
||||
try:
|
||||
data = bytearray(binascii.a2b_hex(tlv_value))
|
||||
except TypeError as e:
|
||||
LOG.warning(
|
||||
"TLV value for TLV type %(tlv_type)d not in correct "
|
||||
"format, value must be in hexadecimal: %(msg)s",
|
||||
{'tlv_type': tlv_type, 'msg': e}, node_info=node_info)
|
||||
continue
|
||||
|
||||
if parser.parse_tlv(tlv_type, data):
|
||||
LOG.debug("Handled TLV type %d",
|
||||
tlv_type, node_info=node_info)
|
||||
else:
|
||||
LOG.debug("LLDP TLV type %d not handled",
|
||||
tlv_type, node_info=node_info)
|
||||
|
||||
return parser.nv_dict
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
"""Process LLDP data and update all_interfaces with processed data"""
|
||||
|
||||
inventory = utils.get_inventory(introspection_data)
|
||||
|
||||
for iface in inventory['interfaces']:
|
||||
if_name = iface['name']
|
||||
|
||||
tlvs = iface.get('lldp')
|
||||
if tlvs is None:
|
||||
LOG.warning("No LLDP Data found for interface %s",
|
||||
if_name, node_info=node_info)
|
||||
continue
|
||||
|
||||
LOG.debug("Processing LLDP Data for interface %s",
|
||||
if_name, node_info=node_info)
|
||||
|
||||
nv = self._parse_lldp_tlvs(tlvs, node_info)
|
||||
|
||||
if nv:
|
||||
# Store lldp data per interface in "all_interfaces"
|
||||
iface_to_update = introspection_data['all_interfaces'][if_name]
|
||||
iface_to_update['lldp_processed'] = nv
|
@ -1,149 +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 LLDP Processing Hook"""
|
||||
|
||||
import binascii
|
||||
|
||||
from construct import core
|
||||
import netaddr
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common import lldp_parsers
|
||||
from ironic_inspector.common import lldp_tlvs as tlv
|
||||
from ironic_inspector.plugins import base
|
||||
from ironic_inspector import utils
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
PORT_ID_ITEM_NAME = "port_id"
|
||||
SWITCH_ID_ITEM_NAME = "switch_id"
|
||||
|
||||
LLDP_PROC_DATA_MAPPING =\
|
||||
{lldp_parsers.LLDP_CHASSIS_ID_NM: SWITCH_ID_ITEM_NAME,
|
||||
lldp_parsers.LLDP_PORT_ID_NM: PORT_ID_ITEM_NAME}
|
||||
|
||||
|
||||
class GenericLocalLinkConnectionHook(base.ProcessingHook):
|
||||
"""Process mandatory LLDP packet fields
|
||||
|
||||
Non-vendor specific LLDP packet fields processed for each NIC found for a
|
||||
baremetal node, port ID and chassis ID. These fields if found and if valid
|
||||
will be saved into the local link connection info port id and switch id
|
||||
fields on the Ironic port that represents that NIC.
|
||||
"""
|
||||
|
||||
def _get_local_link_patch(self, tlv_type, tlv_value, port, node_info):
|
||||
try:
|
||||
data = bytearray(binascii.unhexlify(tlv_value))
|
||||
except TypeError:
|
||||
LOG.warning("TLV value for TLV type %d not in correct"
|
||||
"format, ensure TLV value is in "
|
||||
"hexidecimal format when sent to "
|
||||
"inspector", tlv_type, node_info=node_info)
|
||||
return
|
||||
|
||||
item = value = None
|
||||
if tlv_type == tlv.LLDP_TLV_PORT_ID:
|
||||
try:
|
||||
port_id = tlv.PortId.parse(data)
|
||||
except (core.MappingError, netaddr.AddrFormatError) as e:
|
||||
LOG.warning("TLV parse error for Port ID: %s", e,
|
||||
node_info=node_info)
|
||||
return
|
||||
|
||||
item = PORT_ID_ITEM_NAME
|
||||
value = port_id.value
|
||||
elif tlv_type == tlv.LLDP_TLV_CHASSIS_ID:
|
||||
try:
|
||||
chassis_id = tlv.ChassisId.parse(data)
|
||||
except (core.MappingError, netaddr.AddrFormatError) as e:
|
||||
LOG.warning("TLV parse error for Chassis ID: %s", e,
|
||||
node_info=node_info)
|
||||
return
|
||||
|
||||
# Only accept mac address for chassis ID
|
||||
if 'mac_address' in chassis_id.subtype:
|
||||
item = SWITCH_ID_ITEM_NAME
|
||||
value = chassis_id.value
|
||||
|
||||
if item and value:
|
||||
if (not CONF.processing.overwrite_existing and
|
||||
item in port.local_link_connection):
|
||||
return
|
||||
return {'op': 'add',
|
||||
'path': '/local_link_connection/%s' % item,
|
||||
'value': value}
|
||||
|
||||
def _get_lldp_processed_patch(self, name, item, lldp_proc_data, port):
|
||||
|
||||
if 'lldp_processed' not in lldp_proc_data:
|
||||
return
|
||||
|
||||
value = lldp_proc_data['lldp_processed'].get(name)
|
||||
|
||||
if value:
|
||||
if (not CONF.processing.overwrite_existing and
|
||||
item in port.local_link_connection):
|
||||
return
|
||||
return {'op': 'add',
|
||||
'path': '/local_link_connection/%s' % item,
|
||||
'value': value}
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
"""Process LLDP data and patch Ironic port local link connection"""
|
||||
inventory = utils.get_inventory(introspection_data)
|
||||
|
||||
ironic_ports = node_info.ports()
|
||||
|
||||
for iface in inventory['interfaces']:
|
||||
if iface['name'] not in introspection_data['all_interfaces']:
|
||||
continue
|
||||
|
||||
mac_address = iface['mac_address']
|
||||
port = ironic_ports.get(mac_address)
|
||||
if not port:
|
||||
LOG.debug("Skipping LLC processing for interface %s, matching "
|
||||
"port not found in Ironic.", mac_address,
|
||||
node_info=node_info, data=introspection_data)
|
||||
continue
|
||||
|
||||
lldp_data = iface.get('lldp')
|
||||
if lldp_data is None:
|
||||
LOG.warning("No LLDP Data found for interface %s",
|
||||
mac_address, node_info=node_info,
|
||||
data=introspection_data)
|
||||
continue
|
||||
|
||||
patches = []
|
||||
# First check if lldp data was already processed by lldp_basic
|
||||
# plugin which stores data in 'all_interfaces'
|
||||
proc_data = introspection_data['all_interfaces'][iface['name']]
|
||||
|
||||
for name, item in LLDP_PROC_DATA_MAPPING.items():
|
||||
patch = self._get_lldp_processed_patch(name, item,
|
||||
proc_data, port)
|
||||
if patch is not None:
|
||||
patches.append(patch)
|
||||
|
||||
# If no processed lldp data was available then parse raw lldp data
|
||||
if not patches:
|
||||
for tlv_type, tlv_value in lldp_data:
|
||||
patch = self._get_local_link_patch(tlv_type, tlv_value,
|
||||
port, node_info)
|
||||
if patch is not None:
|
||||
patches.append(patch)
|
||||
|
||||
node_info.patch_port(port, patches)
|
@ -1,86 +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.
|
||||
|
||||
"""Gather and distinguish PCI devices from inventory."""
|
||||
|
||||
import collections
|
||||
import json
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.plugins import base
|
||||
from ironic_inspector import utils
|
||||
|
||||
PCI_DEVICES_OPTS = [
|
||||
cfg.MultiStrOpt('alias',
|
||||
default=[],
|
||||
help=_('An alias for PCI device identified by '
|
||||
'\'vendor_id\' and \'product_id\' fields. Format: '
|
||||
'{"vendor_id": "1234", "product_id": "5678", '
|
||||
'"name": "pci_dev1"}')),
|
||||
]
|
||||
|
||||
|
||||
def list_opts():
|
||||
return [
|
||||
('pci_devices', PCI_DEVICES_OPTS)
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(PCI_DEVICES_OPTS, group='pci_devices')
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
|
||||
def _parse_pci_alias_entry():
|
||||
parsed_pci_devices = []
|
||||
for pci_alias_entry in CONF.pci_devices.alias:
|
||||
try:
|
||||
parsed_entry = json.loads(pci_alias_entry)
|
||||
if set(parsed_entry) != {'vendor_id', 'product_id', 'name'}:
|
||||
raise KeyError("The 'alias' entry should contain "
|
||||
"exactly 'vendor_id', 'product_id' and "
|
||||
"'name' keys")
|
||||
parsed_pci_devices.append(parsed_entry)
|
||||
except (ValueError, KeyError) as ex:
|
||||
LOG.error("Error parsing 'alias' option: %s", ex)
|
||||
return {(dev['vendor_id'], dev['product_id']): dev['name']
|
||||
for dev in parsed_pci_devices}
|
||||
|
||||
|
||||
class PciDevicesHook(base.ProcessingHook):
|
||||
"""Processing hook for counting and distinguishing various PCI devices.
|
||||
|
||||
That information can be later used by nova for node scheduling.
|
||||
"""
|
||||
aliases = _parse_pci_alias_entry()
|
||||
|
||||
def _found_pci_devices_count(self, found_pci_devices):
|
||||
return collections.Counter([(dev['vendor_id'], dev['product_id'])
|
||||
for dev in found_pci_devices
|
||||
if (dev['vendor_id'], dev['product_id'])
|
||||
in self.aliases])
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
if 'pci_devices' not in introspection_data:
|
||||
if CONF.pci_devices.alias:
|
||||
LOG.warning('No PCI devices information was received from '
|
||||
'the ramdisk.')
|
||||
return
|
||||
alias_count = {self.aliases[id_pair]: count for id_pair, count in
|
||||
self._found_pci_devices_count(
|
||||
introspection_data['pci_devices']).items()}
|
||||
if alias_count:
|
||||
node_info.update_capabilities(**alias_count)
|
||||
LOG.info('Found the following PCI devices: %s', alias_count)
|
@ -1,102 +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.
|
||||
|
||||
"""Gather root device hint from recognized block devices."""
|
||||
|
||||
from ironic_inspector.plugins import base
|
||||
from ironic_inspector import utils
|
||||
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
|
||||
class RaidDeviceDetection(base.ProcessingHook):
|
||||
"""Processing hook for learning the root device after RAID creation.
|
||||
|
||||
The plugin can figure out the root device in 2 runs. First, it saves the
|
||||
discovered block device serials in node.extra. The second run will check
|
||||
the difference between the recently discovered block devices and the
|
||||
previously saved ones. After saving the root device in node.properties, it
|
||||
will delete the temporarily saved block device serials in node.extra.
|
||||
|
||||
This way, it helps to figure out the root device hint in cases when
|
||||
otherwise Ironic doesn't have enough information to do so. Such a usecase
|
||||
is DRAC RAID configuration where the BMC doesn't provide any useful
|
||||
information about the created RAID disks. Using this plugin immediately
|
||||
before and after creating the root RAID device will solve the issue of root
|
||||
device hints.
|
||||
|
||||
In cases where there's no RAID volume on the node, the standard plugin will
|
||||
fail due to the missing local_gb value. This plugin fakes the missing
|
||||
value, until it's corrected during later runs. Note, that for this to work
|
||||
the plugin needs to take precedence over the standard plugin.
|
||||
"""
|
||||
|
||||
def _get_serials(self, data):
|
||||
if 'inventory' in data:
|
||||
return [x['serial'] for x in data['inventory'].get('disks', ())
|
||||
if x.get('serial')]
|
||||
elif 'block_devices' in data:
|
||||
return data['block_devices'].get('serials', ())
|
||||
|
||||
def before_processing(self, introspection_data, **kwargs):
|
||||
"""Adds fake local_gb value if it's missing from introspection_data."""
|
||||
if not introspection_data.get('local_gb'):
|
||||
LOG.info('No volume is found on the node. Adding a fake '
|
||||
'value for "local_gb"', data=introspection_data)
|
||||
introspection_data['local_gb'] = 1
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
current_devices = self._get_serials(introspection_data)
|
||||
if not current_devices:
|
||||
LOG.warning('No block device was received from ramdisk',
|
||||
node_info=node_info, data=introspection_data)
|
||||
return
|
||||
|
||||
node = node_info.node()
|
||||
|
||||
if 'root_device' in node.properties:
|
||||
LOG.info('Root device is already known for the node',
|
||||
node_info=node_info, data=introspection_data)
|
||||
return
|
||||
|
||||
if 'block_devices' in node.extra:
|
||||
# Compare previously discovered devices with the current ones
|
||||
previous_devices = node.extra['block_devices']['serials']
|
||||
new_devices = [device for device in current_devices
|
||||
if device not in previous_devices]
|
||||
|
||||
if len(new_devices) > 1:
|
||||
LOG.warning('Root device cannot be identified because '
|
||||
'multiple new devices were found',
|
||||
node_info=node_info, data=introspection_data)
|
||||
return
|
||||
elif len(new_devices) == 0:
|
||||
LOG.warning('No new devices were found',
|
||||
node_info=node_info, data=introspection_data)
|
||||
return
|
||||
|
||||
node_info.patch([
|
||||
{'op': 'remove',
|
||||
'path': '/extra/block_devices'},
|
||||
{'op': 'add',
|
||||
'path': '/properties/root_device',
|
||||
'value': {'serial': new_devices[0]}}
|
||||
])
|
||||
|
||||
else:
|
||||
# No previously discovered devices - save the inspector block
|
||||
# devices in node.extra
|
||||
node_info.patch([{'op': 'add',
|
||||
'path': '/extra/block_devices',
|
||||
'value': {'serials': current_devices}}])
|
@ -1,153 +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.
|
||||
|
||||
"""Standard plugins for rules API."""
|
||||
|
||||
import operator
|
||||
import re
|
||||
|
||||
import netaddr
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.plugins import base
|
||||
from ironic_inspector import utils
|
||||
|
||||
|
||||
def coerce(value, expected):
|
||||
if isinstance(expected, float):
|
||||
return float(value)
|
||||
elif isinstance(expected, int):
|
||||
return int(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
class SimpleCondition(base.RuleConditionPlugin):
|
||||
op = None
|
||||
|
||||
def check(self, node_info, field, params, **kwargs):
|
||||
value = params['value']
|
||||
return self.op(coerce(field, value), value)
|
||||
|
||||
|
||||
class EqCondition(SimpleCondition):
|
||||
op = operator.eq
|
||||
|
||||
|
||||
class LtCondition(SimpleCondition):
|
||||
op = operator.lt
|
||||
|
||||
|
||||
class GtCondition(SimpleCondition):
|
||||
op = operator.gt
|
||||
|
||||
|
||||
class LeCondition(SimpleCondition):
|
||||
op = operator.le
|
||||
|
||||
|
||||
class GeCondition(SimpleCondition):
|
||||
op = operator.ge
|
||||
|
||||
|
||||
class NeCondition(SimpleCondition):
|
||||
op = operator.ne
|
||||
|
||||
|
||||
class EmptyCondition(base.RuleConditionPlugin):
|
||||
REQUIRED_PARAMS = set()
|
||||
ALLOW_NONE = True
|
||||
|
||||
def check(self, node_info, field, params, **kwargs):
|
||||
return field in ('', None, [], {})
|
||||
|
||||
|
||||
class NetCondition(base.RuleConditionPlugin):
|
||||
def validate(self, params, **kwargs):
|
||||
super(NetCondition, self).validate(params, **kwargs)
|
||||
# Make sure it does not raise
|
||||
try:
|
||||
netaddr.IPNetwork(params['value'])
|
||||
except netaddr.AddrFormatError as exc:
|
||||
raise ValueError('invalid value: %s' % exc)
|
||||
|
||||
def check(self, node_info, field, params, **kwargs):
|
||||
network = netaddr.IPNetwork(params['value'])
|
||||
return netaddr.IPAddress(field) in network
|
||||
|
||||
|
||||
class ReCondition(base.RuleConditionPlugin):
|
||||
def validate(self, params, **kwargs):
|
||||
try:
|
||||
re.compile(params['value'])
|
||||
except re.error as exc:
|
||||
raise ValueError(_('invalid regular expression: %s') % exc)
|
||||
|
||||
|
||||
class MatchesCondition(ReCondition):
|
||||
def check(self, node_info, field, params, **kwargs):
|
||||
regexp = params['value']
|
||||
if regexp[-1] != '$':
|
||||
regexp += '$'
|
||||
return re.match(regexp, str(field)) is not None
|
||||
|
||||
|
||||
class ContainsCondition(ReCondition):
|
||||
def check(self, node_info, field, params, **kwargs):
|
||||
return re.search(params['value'], str(field)) is not None
|
||||
|
||||
|
||||
class FailAction(base.RuleActionPlugin):
|
||||
REQUIRED_PARAMS = {'message'}
|
||||
|
||||
def apply(self, node_info, params, **kwargs):
|
||||
raise utils.Error(params['message'], node_info=node_info)
|
||||
|
||||
|
||||
class SetAttributeAction(base.RuleActionPlugin):
|
||||
REQUIRED_PARAMS = {'path', 'value'}
|
||||
# TODO(dtantsur): proper validation of path
|
||||
|
||||
FORMATTED_PARAMS = ['value']
|
||||
|
||||
def apply(self, node_info, params, **kwargs):
|
||||
node_info.patch([{'op': 'add', 'path': params['path'],
|
||||
'value': params['value']}])
|
||||
|
||||
|
||||
class SetCapabilityAction(base.RuleActionPlugin):
|
||||
REQUIRED_PARAMS = {'name'}
|
||||
OPTIONAL_PARAMS = {'value'}
|
||||
|
||||
FORMATTED_PARAMS = ['value']
|
||||
|
||||
def apply(self, node_info, params, **kwargs):
|
||||
node_info.update_capabilities(
|
||||
**{params['name']: params.get('value')})
|
||||
|
||||
|
||||
class ExtendAttributeAction(base.RuleActionPlugin):
|
||||
REQUIRED_PARAMS = {'path', 'value'}
|
||||
OPTIONAL_PARAMS = {'unique'}
|
||||
# TODO(dtantsur): proper validation of path
|
||||
|
||||
FORMATTED_PARAMS = ['value']
|
||||
|
||||
def apply(self, node_info, params, **kwargs):
|
||||
def _replace(values):
|
||||
value = params['value']
|
||||
if not params.get('unique') or value not in values:
|
||||
values.append(value)
|
||||
return values
|
||||
|
||||
node_info.replace_field(params['path'], _replace, default=[])
|
@ -1,299 +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.
|
||||
|
||||
"""Standard set of plugins."""
|
||||
|
||||
|
||||
from ironic_lib import utils as il_utils
|
||||
import netaddr
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import netutils
|
||||
from oslo_utils import units
|
||||
import six
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.plugins import base
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
LOG = utils.getProcessingLogger('ironic_inspector.plugins.standard')
|
||||
|
||||
|
||||
class RootDiskSelectionHook(base.ProcessingHook):
|
||||
"""Smarter root disk selection using Ironic root device hints.
|
||||
|
||||
This hook must always go before SchedulerHook, otherwise root_disk field
|
||||
might not be updated.
|
||||
"""
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
"""Detect root disk from root device hints and IPA inventory."""
|
||||
hints = node_info.node().properties.get('root_device')
|
||||
if not hints:
|
||||
LOG.debug('Root device hints are not provided',
|
||||
node_info=node_info, data=introspection_data)
|
||||
return
|
||||
|
||||
inventory = utils.get_inventory(introspection_data,
|
||||
node_info=node_info)
|
||||
try:
|
||||
device = il_utils.match_root_device_hints(inventory['disks'],
|
||||
hints)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise utils.Error(
|
||||
_('No disks could be found using the root device hints '
|
||||
'%(hints)s because they failed to validate. '
|
||||
'Error: %(error)s') % {'hints': hints, 'error': e},
|
||||
node_info=node_info, data=introspection_data)
|
||||
|
||||
if not device:
|
||||
raise utils.Error(_('No disks satisfied root device hints'),
|
||||
node_info=node_info, data=introspection_data)
|
||||
|
||||
LOG.debug('Disk %(disk)s of size %(size)s satisfies '
|
||||
'root device hints',
|
||||
{'disk': device.get('name'), 'size': device['size']},
|
||||
node_info=node_info, data=introspection_data)
|
||||
introspection_data['root_disk'] = device
|
||||
|
||||
|
||||
class SchedulerHook(base.ProcessingHook):
|
||||
"""Nova scheduler required properties."""
|
||||
|
||||
KEYS = ('cpus', 'cpu_arch', 'memory_mb', 'local_gb')
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
"""Update node with scheduler properties."""
|
||||
inventory = utils.get_inventory(introspection_data,
|
||||
node_info=node_info)
|
||||
errors = []
|
||||
|
||||
root_disk = introspection_data.get('root_disk')
|
||||
if root_disk:
|
||||
introspection_data['local_gb'] = root_disk['size'] // units.Gi
|
||||
if CONF.processing.disk_partitioning_spacing:
|
||||
introspection_data['local_gb'] -= 1
|
||||
else:
|
||||
introspection_data['local_gb'] = 0
|
||||
|
||||
try:
|
||||
introspection_data['cpus'] = int(inventory['cpu']['count'])
|
||||
introspection_data['cpu_arch'] = six.text_type(
|
||||
inventory['cpu']['architecture'])
|
||||
except (KeyError, ValueError, TypeError):
|
||||
errors.append(_('malformed or missing CPU information: %s') %
|
||||
inventory.get('cpu'))
|
||||
|
||||
try:
|
||||
introspection_data['memory_mb'] = int(
|
||||
inventory['memory']['physical_mb'])
|
||||
except (KeyError, ValueError, TypeError):
|
||||
errors.append(_('malformed or missing memory information: %s; '
|
||||
'introspection requires physical memory size '
|
||||
'from dmidecode') % inventory.get('memory'))
|
||||
|
||||
if errors:
|
||||
raise utils.Error(_('The following problems encountered: %s') %
|
||||
'; '.join(errors),
|
||||
node_info=node_info, data=introspection_data)
|
||||
|
||||
LOG.info('Discovered data: CPUs: %(cpus)s %(cpu_arch)s, '
|
||||
'memory %(memory_mb)s MiB, disk %(local_gb)s GiB',
|
||||
{key: introspection_data.get(key) for key in self.KEYS},
|
||||
node_info=node_info, data=introspection_data)
|
||||
|
||||
overwrite = CONF.processing.overwrite_existing
|
||||
properties = {key: str(introspection_data[key])
|
||||
for key in self.KEYS if overwrite or
|
||||
not node_info.node().properties.get(key)}
|
||||
node_info.update_properties(**properties)
|
||||
|
||||
|
||||
class ValidateInterfacesHook(base.ProcessingHook):
|
||||
"""Hook to validate network interfaces."""
|
||||
|
||||
def __init__(self):
|
||||
# Some configuration checks
|
||||
if (CONF.processing.add_ports == 'disabled' and
|
||||
CONF.processing.keep_ports == 'added'):
|
||||
msg = _("Configuration error: add_ports set to disabled "
|
||||
"and keep_ports set to added. Please change keep_ports "
|
||||
"to all.")
|
||||
raise utils.Error(msg)
|
||||
|
||||
def _get_interfaces(self, data=None):
|
||||
"""Convert inventory to a dict with interfaces.
|
||||
|
||||
:return: dict interface name -> dict with keys 'mac' and 'ip'
|
||||
"""
|
||||
result = {}
|
||||
inventory = utils.get_inventory(data)
|
||||
|
||||
pxe_mac = utils.get_pxe_mac(data)
|
||||
|
||||
for iface in inventory['interfaces']:
|
||||
name = iface.get('name')
|
||||
mac = iface.get('mac_address')
|
||||
ip = iface.get('ipv4_address')
|
||||
client_id = iface.get('client_id')
|
||||
|
||||
if not name:
|
||||
LOG.error('Malformed interface record: %s',
|
||||
iface, data=data)
|
||||
continue
|
||||
|
||||
if not mac:
|
||||
LOG.debug('Skipping interface %s without link information',
|
||||
name, data=data)
|
||||
continue
|
||||
|
||||
if not netutils.is_valid_mac(mac):
|
||||
LOG.warning('MAC %(mac)s for interface %(name)s is '
|
||||
'not valid, skipping',
|
||||
{'mac': mac, 'name': name},
|
||||
data=data)
|
||||
continue
|
||||
|
||||
mac = mac.lower()
|
||||
|
||||
LOG.debug('Found interface %(name)s with MAC "%(mac)s", '
|
||||
'IP address "%(ip)s" and client_id "%(client_id)s"',
|
||||
{'name': name, 'mac': mac, 'ip': ip,
|
||||
'client_id': client_id}, data=data)
|
||||
result[name] = {'ip': ip, 'mac': mac, 'client_id': client_id,
|
||||
'pxe': (mac == pxe_mac)}
|
||||
|
||||
return result
|
||||
|
||||
def _validate_interfaces(self, interfaces, data=None):
|
||||
"""Validate interfaces on correctness and suitability.
|
||||
|
||||
:return: dict interface name -> dict with keys 'mac' and 'ip'
|
||||
"""
|
||||
if not interfaces:
|
||||
raise utils.Error(_('No interfaces supplied by the ramdisk'),
|
||||
data=data)
|
||||
|
||||
pxe_mac = utils.get_pxe_mac(data)
|
||||
if not pxe_mac and CONF.processing.add_ports == 'pxe':
|
||||
LOG.warning('No boot interface provided in the introspection '
|
||||
'data, will add all ports with IP addresses')
|
||||
|
||||
result = {}
|
||||
|
||||
for name, iface in interfaces.items():
|
||||
ip = iface.get('ip')
|
||||
pxe = iface.get('pxe', True)
|
||||
|
||||
if name == 'lo' or (ip and netaddr.IPAddress(ip).is_loopback()):
|
||||
LOG.debug('Skipping local interface %s', name, data=data)
|
||||
continue
|
||||
|
||||
if CONF.processing.add_ports == 'pxe' and pxe_mac and not pxe:
|
||||
LOG.debug('Skipping interface %s as it was not PXE booting',
|
||||
name, data=data)
|
||||
continue
|
||||
elif CONF.processing.add_ports != 'all' and not ip:
|
||||
LOG.debug('Skipping interface %s as it did not have '
|
||||
'an IP address assigned during the ramdisk run',
|
||||
name, data=data)
|
||||
continue
|
||||
|
||||
result[name] = iface
|
||||
|
||||
if not result:
|
||||
raise utils.Error(_('No suitable interfaces found in %s') %
|
||||
interfaces, data=data)
|
||||
return result
|
||||
|
||||
def before_processing(self, introspection_data, **kwargs):
|
||||
"""Validate information about network interfaces."""
|
||||
|
||||
bmc_address = utils.get_ipmi_address_from_data(introspection_data)
|
||||
if bmc_address:
|
||||
introspection_data['ipmi_address'] = bmc_address
|
||||
else:
|
||||
LOG.debug('No BMC address provided in introspection data, '
|
||||
'assuming virtual environment', data=introspection_data)
|
||||
|
||||
all_interfaces = self._get_interfaces(introspection_data)
|
||||
|
||||
interfaces = self._validate_interfaces(all_interfaces,
|
||||
introspection_data)
|
||||
|
||||
LOG.info('Using network interface(s): %s',
|
||||
', '.join('%s %s' % (name, items)
|
||||
for (name, items) in interfaces.items()),
|
||||
data=introspection_data)
|
||||
|
||||
introspection_data['all_interfaces'] = all_interfaces
|
||||
introspection_data['interfaces'] = interfaces
|
||||
valid_macs = [iface['mac'] for iface in interfaces.values()]
|
||||
introspection_data['macs'] = valid_macs
|
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs):
|
||||
"""Create new ports and drop ports that are not present in the data."""
|
||||
interfaces = introspection_data.get('interfaces')
|
||||
if CONF.processing.add_ports != 'disabled':
|
||||
node_info.create_ports(list(interfaces.values()))
|
||||
|
||||
if CONF.processing.keep_ports == 'present':
|
||||
expected_macs = {
|
||||
iface['mac']
|
||||
for iface in introspection_data['all_interfaces'].values()
|
||||
}
|
||||
elif CONF.processing.keep_ports == 'added':
|
||||
expected_macs = set(introspection_data['macs'])
|
||||
|
||||
if CONF.processing.keep_ports != 'all':
|
||||
# list is required as we modify underlying dict
|
||||
for port in list(node_info.ports().values()):
|
||||
if port.address not in expected_macs:
|
||||
LOG.info("Deleting port %(port)s as its MAC %(mac)s is "
|
||||
"not in expected MAC list %(expected)s",
|
||||
{'port': port.uuid,
|
||||
'mac': port.address,
|
||||
'expected': list(sorted(expected_macs))},
|
||||
node_info=node_info, data=introspection_data)
|
||||
node_info.delete_port(port)
|
||||
|
||||
if CONF.processing.overwrite_existing:
|
||||
# Make sure pxe_enabled is up-to-date
|
||||
ports = node_info.ports()
|
||||
for iface in introspection_data['interfaces'].values():
|
||||
try:
|
||||
port = ports[iface['mac']]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
real_pxe = iface.get('pxe', True)
|
||||
if port.pxe_enabled != real_pxe:
|
||||
LOG.info('Fixing pxe_enabled=%(val)s on port %(port)s '
|
||||
'to match introspected data',
|
||||
{'port': port.address, 'val': real_pxe},
|
||||
node_info=node_info, data=introspection_data)
|
||||
node_info.patch_port(port, [{'op': 'replace',
|
||||
'path': '/pxe_enabled',
|
||||
'value': real_pxe}])
|
||||
|
||||
|
||||
class RamdiskErrorHook(base.ProcessingHook):
|
||||
"""Hook to process error send from the ramdisk."""
|
||||
|
||||
def before_processing(self, introspection_data, **kwargs):
|
||||
error = introspection_data.get('error')
|
||||
if error:
|
||||
raise utils.Error(_('Ramdisk reported error: %s') % error,
|
||||
data=introspection_data)
|
@ -1,390 +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.
|
||||
|
||||
"""Handling introspection data from the ramdisk."""
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_serialization import base64
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import timeutils
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector.common import swift
|
||||
from ironic_inspector import firewall
|
||||
from ironic_inspector import introspection_state as istate
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector.plugins import base as plugins_base
|
||||
from ironic_inspector import rules
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
|
||||
_STORAGE_EXCLUDED_KEYS = {'logs'}
|
||||
_UNPROCESSED_DATA_STORE_SUFFIX = 'UNPROCESSED'
|
||||
|
||||
|
||||
def _store_logs(introspection_data, node_info):
|
||||
logs = introspection_data.get('logs')
|
||||
if not logs:
|
||||
LOG.warning('No logs were passed by the ramdisk',
|
||||
data=introspection_data, node_info=node_info)
|
||||
return
|
||||
|
||||
if not CONF.processing.ramdisk_logs_dir:
|
||||
LOG.warning('Failed to store logs received from the ramdisk '
|
||||
'because ramdisk_logs_dir configuration option '
|
||||
'is not set',
|
||||
data=introspection_data, node_info=node_info)
|
||||
return
|
||||
|
||||
fmt_args = {
|
||||
'uuid': node_info.uuid if node_info is not None else 'unknown',
|
||||
'mac': (utils.get_pxe_mac(introspection_data) or
|
||||
'unknown').replace(':', ''),
|
||||
'dt': datetime.datetime.utcnow(),
|
||||
'bmc': (utils.get_ipmi_address_from_data(introspection_data) or
|
||||
'unknown')
|
||||
}
|
||||
|
||||
file_name = CONF.processing.ramdisk_logs_filename_format.format(**fmt_args)
|
||||
|
||||
try:
|
||||
if not os.path.exists(CONF.processing.ramdisk_logs_dir):
|
||||
os.makedirs(CONF.processing.ramdisk_logs_dir)
|
||||
with open(os.path.join(CONF.processing.ramdisk_logs_dir, file_name),
|
||||
'wb') as fp:
|
||||
fp.write(base64.decode_as_bytes(logs))
|
||||
except EnvironmentError:
|
||||
LOG.exception('Could not store the ramdisk logs',
|
||||
data=introspection_data, node_info=node_info)
|
||||
else:
|
||||
LOG.info('Ramdisk logs were stored in file %s', file_name,
|
||||
data=introspection_data, node_info=node_info)
|
||||
|
||||
|
||||
def _find_node_info(introspection_data, failures):
|
||||
try:
|
||||
return node_cache.find_node(
|
||||
bmc_address=introspection_data.get('ipmi_address'),
|
||||
mac=utils.get_valid_macs(introspection_data))
|
||||
except utils.NotFoundInCacheError as exc:
|
||||
not_found_hook = plugins_base.node_not_found_hook_manager()
|
||||
if not_found_hook is None:
|
||||
failures.append(_('Look up error: %s') % exc)
|
||||
return
|
||||
|
||||
LOG.debug('Running node_not_found_hook %s',
|
||||
CONF.processing.node_not_found_hook,
|
||||
data=introspection_data)
|
||||
|
||||
# NOTE(sambetts): If not_found_hook is not none it means that we were
|
||||
# unable to find the node in the node cache and there is a node not
|
||||
# found hook defined so we should try to send the introspection data
|
||||
# to that hook to generate the node info before bubbling up the error.
|
||||
try:
|
||||
node_info = not_found_hook.driver(introspection_data)
|
||||
if node_info:
|
||||
return node_info
|
||||
failures.append(_("Node not found hook returned nothing"))
|
||||
except Exception as exc:
|
||||
failures.append(_("Node not found hook failed: %s") % exc)
|
||||
except utils.Error as exc:
|
||||
failures.append(_('Look up error: %s') % exc)
|
||||
|
||||
|
||||
def _run_pre_hooks(introspection_data, failures):
|
||||
hooks = plugins_base.processing_hooks_manager()
|
||||
for hook_ext in hooks:
|
||||
LOG.debug('Running pre-processing hook %s', hook_ext.name,
|
||||
data=introspection_data)
|
||||
# NOTE(dtantsur): catch exceptions, so that we have changes to update
|
||||
# node introspection status after look up
|
||||
try:
|
||||
hook_ext.obj.before_processing(introspection_data)
|
||||
except utils.Error as exc:
|
||||
LOG.error('Hook %(hook)s failed, delaying error report '
|
||||
'until node look up: %(error)s',
|
||||
{'hook': hook_ext.name, 'error': exc},
|
||||
data=introspection_data)
|
||||
failures.append('Preprocessing hook %(hook)s: %(error)s' %
|
||||
{'hook': hook_ext.name, 'error': exc})
|
||||
except Exception as exc:
|
||||
LOG.exception('Hook %(hook)s failed, delaying error report '
|
||||
'until node look up: %(error)s',
|
||||
{'hook': hook_ext.name, 'error': exc},
|
||||
data=introspection_data)
|
||||
failures.append(_('Unexpected exception %(exc_class)s during '
|
||||
'preprocessing in hook %(hook)s: %(error)s') %
|
||||
{'hook': hook_ext.name,
|
||||
'exc_class': exc.__class__.__name__,
|
||||
'error': exc})
|
||||
|
||||
|
||||
def _filter_data_excluded_keys(data):
|
||||
return {k: v for k, v in data.items()
|
||||
if k not in _STORAGE_EXCLUDED_KEYS}
|
||||
|
||||
|
||||
def _store_data(node_info, data, suffix=None):
|
||||
if CONF.processing.store_data != 'swift':
|
||||
LOG.debug("Swift support is disabled, introspection data "
|
||||
"won't be stored", node_info=node_info)
|
||||
return
|
||||
|
||||
swift_object_name = swift.store_introspection_data(
|
||||
_filter_data_excluded_keys(data),
|
||||
node_info.uuid,
|
||||
suffix=suffix
|
||||
)
|
||||
LOG.info('Introspection data was stored in Swift in object '
|
||||
'%s', swift_object_name, node_info=node_info)
|
||||
if CONF.processing.store_data_location:
|
||||
node_info.patch([{'op': 'add', 'path': '/extra/%s' %
|
||||
CONF.processing.store_data_location,
|
||||
'value': swift_object_name}])
|
||||
|
||||
|
||||
def _store_unprocessed_data(node_info, data):
|
||||
# runs in background
|
||||
try:
|
||||
_store_data(node_info, data,
|
||||
suffix=_UNPROCESSED_DATA_STORE_SUFFIX)
|
||||
except Exception:
|
||||
LOG.exception('Encountered exception saving unprocessed '
|
||||
'introspection data', node_info=node_info,
|
||||
data=data)
|
||||
|
||||
|
||||
def _get_unprocessed_data(uuid):
|
||||
if CONF.processing.store_data == 'swift':
|
||||
LOG.debug('Fetching unprocessed introspection data from '
|
||||
'Swift for %s', uuid)
|
||||
return json.loads(
|
||||
swift.get_introspection_data(
|
||||
uuid,
|
||||
suffix=_UNPROCESSED_DATA_STORE_SUFFIX
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise utils.Error(_('Swift support is disabled'), code=400)
|
||||
|
||||
|
||||
def process(introspection_data):
|
||||
"""Process data from the ramdisk.
|
||||
|
||||
This function heavily relies on the hooks to do the actual data processing.
|
||||
"""
|
||||
unprocessed_data = copy.deepcopy(introspection_data)
|
||||
failures = []
|
||||
_run_pre_hooks(introspection_data, failures)
|
||||
node_info = _find_node_info(introspection_data, failures)
|
||||
if node_info:
|
||||
# Locking is already done in find_node() but may be not done in a
|
||||
# node_not_found hook
|
||||
node_info.acquire_lock()
|
||||
|
||||
if failures or node_info is None:
|
||||
msg = _('The following failures happened during running '
|
||||
'pre-processing hooks:\n%s') % '\n'.join(failures)
|
||||
if node_info is not None:
|
||||
node_info.finished(error='\n'.join(failures))
|
||||
_store_logs(introspection_data, node_info)
|
||||
raise utils.Error(msg, node_info=node_info, data=introspection_data)
|
||||
|
||||
LOG.info('Matching node is %s', node_info.uuid,
|
||||
node_info=node_info, data=introspection_data)
|
||||
|
||||
if node_info.finished_at is not None:
|
||||
# race condition or introspection canceled
|
||||
raise utils.Error(_('Node processing already finished with '
|
||||
'error: %s') % node_info.error,
|
||||
node_info=node_info, code=400)
|
||||
|
||||
# Note(mkovacik): store data now when we're sure that a background
|
||||
# thread won't race with other process() or introspect.abort()
|
||||
# call
|
||||
utils.executor().submit(_store_unprocessed_data, node_info,
|
||||
unprocessed_data)
|
||||
|
||||
try:
|
||||
node = node_info.node()
|
||||
except ir_utils.NotFound as exc:
|
||||
with excutils.save_and_reraise_exception():
|
||||
node_info.finished(error=str(exc))
|
||||
_store_logs(introspection_data, node_info)
|
||||
|
||||
try:
|
||||
result = _process_node(node_info, node, introspection_data)
|
||||
except utils.Error as exc:
|
||||
node_info.finished(error=str(exc))
|
||||
with excutils.save_and_reraise_exception():
|
||||
_store_logs(introspection_data, node_info)
|
||||
except Exception as exc:
|
||||
LOG.exception('Unexpected exception during processing')
|
||||
msg = _('Unexpected exception %(exc_class)s during processing: '
|
||||
'%(error)s') % {'exc_class': exc.__class__.__name__,
|
||||
'error': exc}
|
||||
node_info.finished(error=msg)
|
||||
_store_logs(introspection_data, node_info)
|
||||
raise utils.Error(msg, node_info=node_info, data=introspection_data,
|
||||
code=500)
|
||||
|
||||
if CONF.processing.always_store_ramdisk_logs:
|
||||
_store_logs(introspection_data, node_info)
|
||||
return result
|
||||
|
||||
|
||||
def _run_post_hooks(node_info, introspection_data):
|
||||
hooks = plugins_base.processing_hooks_manager()
|
||||
|
||||
for hook_ext in hooks:
|
||||
LOG.debug('Running post-processing hook %s', hook_ext.name,
|
||||
node_info=node_info, data=introspection_data)
|
||||
hook_ext.obj.before_update(introspection_data, node_info)
|
||||
|
||||
|
||||
@node_cache.fsm_transition(istate.Events.process, reentrant=False)
|
||||
def _process_node(node_info, node, introspection_data):
|
||||
# NOTE(dtantsur): repeat the check in case something changed
|
||||
ir_utils.check_provision_state(node)
|
||||
_run_post_hooks(node_info, introspection_data)
|
||||
_store_data(node_info, introspection_data)
|
||||
|
||||
ironic = ir_utils.get_client()
|
||||
firewall.update_filters(ironic)
|
||||
|
||||
node_info.invalidate_cache()
|
||||
rules.apply(node_info, introspection_data)
|
||||
|
||||
resp = {'uuid': node.uuid}
|
||||
|
||||
utils.executor().submit(_finish, node_info, ironic, introspection_data,
|
||||
power_off=CONF.processing.power_off)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
@node_cache.fsm_transition(istate.Events.finish)
|
||||
def _finish(node_info, ironic, introspection_data, power_off=True):
|
||||
if power_off:
|
||||
LOG.debug('Forcing power off of node %s', node_info.uuid)
|
||||
try:
|
||||
ironic.node.set_power_state(node_info.uuid, 'off')
|
||||
except Exception as exc:
|
||||
if node_info.node().provision_state == 'enroll':
|
||||
LOG.info("Failed to power off the node in"
|
||||
"'enroll' state, ignoring; error was "
|
||||
"%s", exc, node_info=node_info,
|
||||
data=introspection_data)
|
||||
else:
|
||||
msg = (_('Failed to power off node %(node)s, check '
|
||||
'its power management configuration: '
|
||||
'%(exc)s') % {'node': node_info.uuid, 'exc':
|
||||
exc})
|
||||
node_info.finished(error=msg)
|
||||
raise utils.Error(msg, node_info=node_info,
|
||||
data=introspection_data)
|
||||
LOG.info('Node powered-off', node_info=node_info,
|
||||
data=introspection_data)
|
||||
|
||||
node_info.finished()
|
||||
LOG.info('Introspection finished successfully',
|
||||
node_info=node_info, data=introspection_data)
|
||||
|
||||
|
||||
def reapply(node_ident):
|
||||
"""Re-apply introspection steps.
|
||||
|
||||
Re-apply preprocessing, postprocessing and introspection rules on
|
||||
stored data.
|
||||
|
||||
:param node_ident: node UUID or name
|
||||
:raises: utils.Error
|
||||
|
||||
"""
|
||||
|
||||
LOG.debug('Processing re-apply introspection request for node '
|
||||
'UUID: %s', node_ident)
|
||||
node_info = node_cache.get_node(node_ident, locked=False)
|
||||
if not node_info.acquire_lock(blocking=False):
|
||||
# Note (mkovacik): it should be sufficient to check data
|
||||
# presence & locking. If either introspection didn't start
|
||||
# yet, was in waiting state or didn't finish yet, either data
|
||||
# won't be available or locking would fail
|
||||
raise utils.Error(_('Node locked, please, try again later'),
|
||||
node_info=node_info, code=409)
|
||||
|
||||
utils.executor().submit(_reapply, node_info)
|
||||
|
||||
|
||||
def _reapply(node_info):
|
||||
# runs in background
|
||||
try:
|
||||
node_info.started_at = timeutils.utcnow()
|
||||
node_info.commit()
|
||||
introspection_data = _get_unprocessed_data(node_info.uuid)
|
||||
except Exception as exc:
|
||||
LOG.exception('Encountered exception while fetching '
|
||||
'stored introspection data',
|
||||
node_info=node_info)
|
||||
msg = (_('Unexpected exception %(exc_class)s while fetching '
|
||||
'unprocessed introspection data from Swift: %(error)s') %
|
||||
{'exc_class': exc.__class__.__name__, 'error': exc})
|
||||
node_info.finished(error=msg)
|
||||
return
|
||||
|
||||
try:
|
||||
ironic = ir_utils.get_client()
|
||||
except Exception as exc:
|
||||
msg = _('Encountered an exception while getting the Ironic client: '
|
||||
'%s') % exc
|
||||
LOG.error(msg, node_info=node_info, data=introspection_data)
|
||||
node_info.fsm_event(istate.Events.error)
|
||||
node_info.finished(error=msg)
|
||||
return
|
||||
|
||||
try:
|
||||
_reapply_with_data(node_info, introspection_data)
|
||||
except Exception as exc:
|
||||
node_info.finished(error=str(exc))
|
||||
return
|
||||
|
||||
_finish(node_info, ironic, introspection_data,
|
||||
power_off=False)
|
||||
|
||||
LOG.info('Successfully reapplied introspection on stored '
|
||||
'data', node_info=node_info, data=introspection_data)
|
||||
|
||||
|
||||
@node_cache.fsm_event_before(istate.Events.reapply)
|
||||
@node_cache.triggers_fsm_error_transition()
|
||||
def _reapply_with_data(node_info, introspection_data):
|
||||
failures = []
|
||||
_run_pre_hooks(introspection_data, failures)
|
||||
if failures:
|
||||
raise utils.Error(_('Pre-processing failures detected reapplying '
|
||||
'introspection on stored data:\n%s') %
|
||||
'\n'.join(failures), node_info=node_info)
|
||||
|
||||
_run_post_hooks(node_info, introspection_data)
|
||||
_store_data(node_info, introspection_data)
|
||||
node_info.invalidate_cache()
|
||||
rules.apply(node_info, introspection_data)
|
@ -1,224 +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.
|
||||
|
||||
"""Base code for PXE boot filtering."""
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
|
||||
from automaton import exceptions as automaton_errors
|
||||
from automaton import machines
|
||||
from eventlet import semaphore
|
||||
from futurist import periodics
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
import stevedore
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector.pxe_filter import interface
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
_STEVEDORE_DRIVER_NAMESPACE = 'ironic_inspector.pxe_filter'
|
||||
|
||||
|
||||
class InvalidFilterDriverState(RuntimeError):
|
||||
"""The fsm of the filter driver raised an error."""
|
||||
|
||||
|
||||
class States(object):
|
||||
"""PXE filter driver states."""
|
||||
uninitialized = 'uninitialized'
|
||||
initialized = 'initialized'
|
||||
|
||||
|
||||
class Events(object):
|
||||
"""PXE filter driver transitions."""
|
||||
initialize = 'initialize'
|
||||
sync = 'sync'
|
||||
reset = 'reset'
|
||||
|
||||
|
||||
# a reset is always possible
|
||||
State_space = [
|
||||
{
|
||||
'name': States.uninitialized,
|
||||
'next_states': {
|
||||
Events.initialize: States.initialized,
|
||||
Events.reset: States.uninitialized,
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': States.initialized,
|
||||
'next_states': {
|
||||
Events.sync: States.initialized,
|
||||
Events.reset: States.uninitialized,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def locked_driver_event(event):
|
||||
"""Call driver method having processed the fsm event."""
|
||||
def outer(method):
|
||||
@functools.wraps(method)
|
||||
def inner(self, *args, **kwargs):
|
||||
with self.lock, self.fsm_reset_on_error() as fsm:
|
||||
fsm.process_event(event)
|
||||
return method(self, *args, **kwargs)
|
||||
return inner
|
||||
return outer
|
||||
|
||||
|
||||
class BaseFilter(interface.FilterDriver):
|
||||
"""The generic PXE boot filtering interface implementation.
|
||||
|
||||
This driver doesn't do anything but provides a basic synchronization and
|
||||
initialization logic for some drivers to reuse. Subclasses have to provide
|
||||
a custom sync() method.
|
||||
"""
|
||||
|
||||
fsm = machines.FiniteMachine.build(State_space)
|
||||
fsm.default_start_state = States.uninitialized
|
||||
|
||||
def __init__(self):
|
||||
super(BaseFilter, self).__init__()
|
||||
self.lock = semaphore.BoundedSemaphore()
|
||||
self.fsm.initialize(start_state=States.uninitialized)
|
||||
|
||||
def __str__(self):
|
||||
return '%(driver)s, state=%(state)s' % {
|
||||
'driver': type(self).__name__, 'state': self.state}
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Current driver state."""
|
||||
return self.fsm.current_state
|
||||
|
||||
def reset(self):
|
||||
"""Reset internal driver state.
|
||||
|
||||
This method is called by the fsm_context manager upon exception as well
|
||||
as by the tear_down_filter method. A subclass might wish to override as
|
||||
necessary, though must not lock the driver. The overriding subclass
|
||||
should up-call.
|
||||
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('Resetting the PXE filter driver %s', self)
|
||||
# a reset event is always possible
|
||||
self.fsm.process_event(Events.reset)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def fsm_reset_on_error(self):
|
||||
"""Reset the filter driver upon generic exception.
|
||||
|
||||
The context is self.fsm. The automaton.exceptions.NotFound error is
|
||||
cast to the InvalidFilterDriverState error. Other exceptions trigger
|
||||
self.reset()
|
||||
|
||||
:raises: InvalidFilterDriverState
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('The PXE filter driver %s enters the fsm_reset_on_error '
|
||||
'context', self)
|
||||
try:
|
||||
yield self.fsm
|
||||
except automaton_errors.NotFound as e:
|
||||
raise InvalidFilterDriverState(_('The PXE filter driver %(driver)s'
|
||||
': my fsm encountered an '
|
||||
'exception: %(error)s') % {
|
||||
'driver': self, 'error': e})
|
||||
except Exception as e:
|
||||
LOG.exception('The PXE filter %(filter)s encountered an '
|
||||
'exception: %(error)s; resetting the filter',
|
||||
{'filter': self, 'error': e})
|
||||
self.reset()
|
||||
raise
|
||||
finally:
|
||||
LOG.debug('The PXE filter driver %s left the fsm_reset_on_error '
|
||||
'context', self)
|
||||
|
||||
@locked_driver_event(Events.initialize)
|
||||
def init_filter(self):
|
||||
"""Base driver initialization logic. Locked.
|
||||
|
||||
:raises: InvalidFilterDriverState
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('Initializing the PXE filter driver %s', self)
|
||||
|
||||
def tear_down_filter(self):
|
||||
"""Base driver tear down logic. Locked.
|
||||
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('Tearing down the PXE filter driver %s', self)
|
||||
with self.lock:
|
||||
self.reset()
|
||||
|
||||
@locked_driver_event(Events.sync)
|
||||
def sync(self, ironic):
|
||||
"""Base driver sync logic. Locked.
|
||||
|
||||
:param ironic: obligatory ironic client instance
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('Syncing the PXE filter driver %s', self)
|
||||
|
||||
def get_periodic_sync_task(self):
|
||||
"""Get periodic sync task for the filter.
|
||||
|
||||
:returns: a periodic task to be run in the background.
|
||||
"""
|
||||
ironic = ir_utils.get_client()
|
||||
return periodics.periodic(
|
||||
# NOTE(milan): the periodic decorator doesn't support 0 as
|
||||
# a spacing value of (a switched off) periodic
|
||||
spacing=CONF.pxe_filter.sync_period or float('inf'),
|
||||
enabled=bool(CONF.pxe_filter.sync_period))(
|
||||
lambda: self.sync(ironic))
|
||||
|
||||
|
||||
class NoopFilter(BaseFilter):
|
||||
"""A trivial PXE boot filter."""
|
||||
|
||||
|
||||
_DRIVER_MANAGER = None
|
||||
|
||||
|
||||
@lockutils.synchronized(__name__)
|
||||
def _driver_manager():
|
||||
"""Create a Stevedore driver manager for filtering drivers. Locked."""
|
||||
global _DRIVER_MANAGER
|
||||
|
||||
name = CONF.pxe_filter.driver
|
||||
if _DRIVER_MANAGER is None:
|
||||
_DRIVER_MANAGER = stevedore.driver.DriverManager(
|
||||
_STEVEDORE_DRIVER_NAMESPACE,
|
||||
name=name,
|
||||
invoke_on_load=True
|
||||
)
|
||||
|
||||
return _DRIVER_MANAGER
|
||||
|
||||
|
||||
def driver():
|
||||
"""Get the driver for the PXE filter.
|
||||
|
||||
:returns: the singleton PXE filter driver object.
|
||||
"""
|
||||
return _driver_manager().driver
|
@ -1,64 +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.
|
||||
|
||||
"""The code of the PXE boot filtering interface."""
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class FilterDriver(object):
|
||||
"""The PXE boot filtering interface."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def init_filter(self):
|
||||
"""Initialize the internal driver state.
|
||||
|
||||
This method should be idempotent and may perform system-wide filter
|
||||
state changes. Can be synchronous.
|
||||
|
||||
:returns: nothing.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def sync(self, ironic):
|
||||
"""Synchronize the filter with ironic and inspector.
|
||||
|
||||
To be called both periodically and as needed by inspector. The filter
|
||||
should tear down its internal state if the sync method raises in order
|
||||
to "propagate" filtering exception between periodic and on-demand sync
|
||||
call. To this end, a driver should raise from the sync call if its
|
||||
internal state isn't properly initialized.
|
||||
|
||||
:param ironic: an ironic client instance.
|
||||
:returns: nothing.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def tear_down_filter(self):
|
||||
"""Reset the filter.
|
||||
|
||||
This method should be idempotent and may perform system-wide filter
|
||||
state changes. Can be synchronous.
|
||||
|
||||
:returns: nothing.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_periodic_sync_task(self):
|
||||
"""Get periodic sync task for the filter.
|
||||
|
||||
:returns: a periodic task to be run in the background.
|
||||
"""
|
@ -1,425 +0,0 @@
|
||||
# Copyright 2015 Red Hat, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Support for introspection rules."""
|
||||
|
||||
import jsonpath_rw as jsonpath
|
||||
import jsonschema
|
||||
from oslo_db import exception as db_exc
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
from sqlalchemy import orm
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector import db
|
||||
from ironic_inspector.plugins import base as plugins_base
|
||||
from ironic_inspector import utils
|
||||
|
||||
|
||||
LOG = utils.getProcessingLogger(__name__)
|
||||
_CONDITIONS_SCHEMA = None
|
||||
_ACTIONS_SCHEMA = None
|
||||
|
||||
|
||||
def conditions_schema():
|
||||
global _CONDITIONS_SCHEMA
|
||||
if _CONDITIONS_SCHEMA is None:
|
||||
condition_plugins = [x.name for x in
|
||||
plugins_base.rule_conditions_manager()]
|
||||
_CONDITIONS_SCHEMA = {
|
||||
"title": "Inspector rule conditions schema",
|
||||
"type": "array",
|
||||
# we can have rules that always apply
|
||||
"minItems": 0,
|
||||
"items": {
|
||||
"type": "object",
|
||||
# field might become optional in the future, but not right now
|
||||
"required": ["op", "field"],
|
||||
"properties": {
|
||||
"op": {
|
||||
"description": "condition operator",
|
||||
"enum": condition_plugins
|
||||
},
|
||||
"field": {
|
||||
"description": "JSON path to field for matching",
|
||||
"type": "string"
|
||||
},
|
||||
"multiple": {
|
||||
"description": "how to treat multiple values",
|
||||
"enum": ["all", "any", "first"]
|
||||
},
|
||||
"invert": {
|
||||
"description": "whether to invert the result",
|
||||
"type": "boolean"
|
||||
},
|
||||
},
|
||||
# other properties are validated by plugins
|
||||
"additionalProperties": True
|
||||
}
|
||||
}
|
||||
|
||||
return _CONDITIONS_SCHEMA
|
||||
|
||||
|
||||
def actions_schema():
|
||||
global _ACTIONS_SCHEMA
|
||||
if _ACTIONS_SCHEMA is None:
|
||||
action_plugins = [x.name for x in
|
||||
plugins_base.rule_actions_manager()]
|
||||
_ACTIONS_SCHEMA = {
|
||||
"title": "Inspector rule actions schema",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["action"],
|
||||
"properties": {
|
||||
"action": {
|
||||
"description": "action to take",
|
||||
"enum": action_plugins
|
||||
},
|
||||
},
|
||||
# other properties are validated by plugins
|
||||
"additionalProperties": True
|
||||
}
|
||||
}
|
||||
|
||||
return _ACTIONS_SCHEMA
|
||||
|
||||
|
||||
class IntrospectionRule(object):
|
||||
"""High-level class representing an introspection rule."""
|
||||
|
||||
def __init__(self, uuid, conditions, actions, description):
|
||||
"""Create rule object from database data."""
|
||||
self._uuid = uuid
|
||||
self._conditions = conditions
|
||||
self._actions = actions
|
||||
self._description = description
|
||||
|
||||
def as_dict(self, short=False):
|
||||
result = {
|
||||
'uuid': self._uuid,
|
||||
'description': self._description,
|
||||
}
|
||||
|
||||
if not short:
|
||||
result['conditions'] = [c.as_dict() for c in self._conditions]
|
||||
result['actions'] = [a.as_dict() for a in self._actions]
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self._description or self._uuid
|
||||
|
||||
def check_conditions(self, node_info, data):
|
||||
"""Check if conditions are true for a given node.
|
||||
|
||||
:param node_info: a NodeInfo object
|
||||
:param data: introspection data
|
||||
:returns: True if conditions match, otherwise False
|
||||
"""
|
||||
LOG.debug('Checking rule "%s"', self.description,
|
||||
node_info=node_info, data=data)
|
||||
ext_mgr = plugins_base.rule_conditions_manager()
|
||||
for cond in self._conditions:
|
||||
scheme, path = _parse_path(cond.field)
|
||||
|
||||
if scheme == 'node':
|
||||
source_data = node_info.node().to_dict()
|
||||
elif scheme == 'data':
|
||||
source_data = data
|
||||
|
||||
field_values = jsonpath.parse(path).find(source_data)
|
||||
field_values = [x.value for x in field_values]
|
||||
cond_ext = ext_mgr[cond.op].obj
|
||||
|
||||
if not field_values:
|
||||
if cond_ext.ALLOW_NONE:
|
||||
LOG.debug('Field with JSON path %s was not found in data',
|
||||
cond.field, node_info=node_info, data=data)
|
||||
field_values = [None]
|
||||
else:
|
||||
LOG.info('Field with JSON path %(path)s was not found '
|
||||
'in data, rule "%(rule)s" will not '
|
||||
'be applied',
|
||||
{'path': cond.field, 'rule': self.description},
|
||||
node_info=node_info, data=data)
|
||||
return False
|
||||
|
||||
for value in field_values:
|
||||
result = cond_ext.check(node_info, value, cond.params)
|
||||
if cond.invert:
|
||||
result = not result
|
||||
|
||||
if (cond.multiple == 'first'
|
||||
or (cond.multiple == 'all' and not result)
|
||||
or (cond.multiple == 'any' and result)):
|
||||
break
|
||||
|
||||
if not result:
|
||||
LOG.info('Rule "%(rule)s" will not be applied: condition '
|
||||
'%(field)s %(op)s %(params)s failed',
|
||||
{'rule': self.description, 'field': cond.field,
|
||||
'op': cond.op, 'params': cond.params},
|
||||
node_info=node_info, data=data)
|
||||
return False
|
||||
|
||||
LOG.info('Rule "%s" will be applied', self.description,
|
||||
node_info=node_info, data=data)
|
||||
return True
|
||||
|
||||
def apply_actions(self, node_info, data=None):
|
||||
"""Run actions on a node.
|
||||
|
||||
:param node_info: NodeInfo instance
|
||||
:param data: introspection data
|
||||
"""
|
||||
LOG.debug('Running actions for rule "%s"', self.description,
|
||||
node_info=node_info, data=data)
|
||||
|
||||
ext_mgr = plugins_base.rule_actions_manager()
|
||||
for act in self._actions:
|
||||
ext = ext_mgr[act.action].obj
|
||||
|
||||
for formatted_param in ext.FORMATTED_PARAMS:
|
||||
value = act.params.get(formatted_param)
|
||||
if not value or not isinstance(value, six.string_types):
|
||||
continue
|
||||
|
||||
# NOTE(aarefiev): verify provided value with introspection
|
||||
# data format specifications.
|
||||
# TODO(aarefiev): simple verify on import rule time.
|
||||
try:
|
||||
act.params[formatted_param] = value.format(data=data)
|
||||
except KeyError as e:
|
||||
raise utils.Error(_('Invalid formatting variable key '
|
||||
'provided: %s') % e,
|
||||
node_info=node_info, data=data)
|
||||
|
||||
LOG.debug('Running action `%(action)s %(params)s`',
|
||||
{'action': act.action, 'params': act.params},
|
||||
node_info=node_info, data=data)
|
||||
ext.apply(node_info, act.params)
|
||||
|
||||
LOG.debug('Successfully applied actions',
|
||||
node_info=node_info, data=data)
|
||||
|
||||
|
||||
def _parse_path(path):
|
||||
"""Parse path, extract scheme and path.
|
||||
|
||||
Parse path with 'node' and 'data' scheme, which links on
|
||||
introspection data and node info respectively. If scheme is
|
||||
missing in path, default is 'data'.
|
||||
|
||||
:param path: data or node path
|
||||
:return: tuple (scheme, path)
|
||||
"""
|
||||
try:
|
||||
index = path.index('://')
|
||||
except ValueError:
|
||||
scheme = 'data'
|
||||
path = path
|
||||
else:
|
||||
scheme = path[:index]
|
||||
path = path[index + 3:]
|
||||
return scheme, path
|
||||
|
||||
|
||||
def create(conditions_json, actions_json, uuid=None,
|
||||
description=None):
|
||||
"""Create a new rule in database.
|
||||
|
||||
:param conditions_json: list of dicts with the following keys:
|
||||
* op - operator
|
||||
* field - JSON path to field to compare
|
||||
Other keys are stored as is.
|
||||
:param actions_json: list of dicts with the following keys:
|
||||
* action - action type
|
||||
Other keys are stored as is.
|
||||
:param uuid: rule UUID, will be generated if empty
|
||||
:param description: human-readable rule description
|
||||
:returns: new IntrospectionRule object
|
||||
:raises: utils.Error on failure
|
||||
"""
|
||||
uuid = uuid or uuidutils.generate_uuid()
|
||||
LOG.debug('Creating rule %(uuid)s with description "%(descr)s", '
|
||||
'conditions %(conditions)s and actions %(actions)s',
|
||||
{'uuid': uuid, 'descr': description,
|
||||
'conditions': conditions_json, 'actions': actions_json})
|
||||
|
||||
try:
|
||||
jsonschema.validate(conditions_json, conditions_schema())
|
||||
except jsonschema.ValidationError as exc:
|
||||
raise utils.Error(_('Validation failed for conditions: %s') % exc)
|
||||
|
||||
try:
|
||||
jsonschema.validate(actions_json, actions_schema())
|
||||
except jsonschema.ValidationError as exc:
|
||||
raise utils.Error(_('Validation failed for actions: %s') % exc)
|
||||
|
||||
cond_mgr = plugins_base.rule_conditions_manager()
|
||||
act_mgr = plugins_base.rule_actions_manager()
|
||||
|
||||
conditions = []
|
||||
reserved_params = {'op', 'field', 'multiple', 'invert'}
|
||||
for cond_json in conditions_json:
|
||||
field = cond_json['field']
|
||||
|
||||
scheme, path = _parse_path(field)
|
||||
|
||||
if scheme not in ('node', 'data'):
|
||||
raise utils.Error(_('Unsupported scheme for field: %s, valid '
|
||||
'values are node:// or data://') % scheme)
|
||||
# verify field as JSON path
|
||||
try:
|
||||
jsonpath.parse(path)
|
||||
except Exception as exc:
|
||||
raise utils.Error(_('Unable to parse field JSON path %(field)s: '
|
||||
'%(error)s') % {'field': field, 'error': exc})
|
||||
|
||||
plugin = cond_mgr[cond_json['op']].obj
|
||||
params = {k: v for k, v in cond_json.items()
|
||||
if k not in reserved_params}
|
||||
try:
|
||||
plugin.validate(params)
|
||||
except ValueError as exc:
|
||||
raise utils.Error(_('Invalid parameters for operator %(op)s: '
|
||||
'%(error)s') %
|
||||
{'op': cond_json['op'], 'error': exc})
|
||||
|
||||
conditions.append((cond_json['field'],
|
||||
cond_json['op'],
|
||||
cond_json.get('multiple', 'any'),
|
||||
cond_json.get('invert', False),
|
||||
params))
|
||||
|
||||
actions = []
|
||||
for action_json in actions_json:
|
||||
plugin = act_mgr[action_json['action']].obj
|
||||
params = {k: v for k, v in action_json.items() if k != 'action'}
|
||||
try:
|
||||
plugin.validate(params)
|
||||
except ValueError as exc:
|
||||
raise utils.Error(_('Invalid parameters for action %(act)s: '
|
||||
'%(error)s') %
|
||||
{'act': action_json['action'], 'error': exc})
|
||||
|
||||
actions.append((action_json['action'], params))
|
||||
|
||||
try:
|
||||
with db.ensure_transaction() as session:
|
||||
rule = db.Rule(uuid=uuid, description=description,
|
||||
disabled=False, created_at=timeutils.utcnow())
|
||||
|
||||
for field, op, multiple, invert, params in conditions:
|
||||
rule.conditions.append(db.RuleCondition(op=op,
|
||||
field=field,
|
||||
multiple=multiple,
|
||||
invert=invert,
|
||||
params=params))
|
||||
|
||||
for action, params in actions:
|
||||
rule.actions.append(db.RuleAction(action=action,
|
||||
params=params))
|
||||
|
||||
rule.save(session)
|
||||
except db_exc.DBDuplicateEntry as exc:
|
||||
LOG.error('Database integrity error %s when '
|
||||
'creating a rule', exc)
|
||||
raise utils.Error(_('Rule with UUID %s already exists') % uuid,
|
||||
code=409)
|
||||
|
||||
LOG.info('Created rule %(uuid)s with description "%(descr)s"',
|
||||
{'uuid': uuid, 'descr': description})
|
||||
return IntrospectionRule(uuid=uuid,
|
||||
conditions=rule.conditions,
|
||||
actions=rule.actions,
|
||||
description=description)
|
||||
|
||||
|
||||
def get(uuid):
|
||||
"""Get a rule by its UUID."""
|
||||
try:
|
||||
rule = db.model_query(db.Rule).filter_by(uuid=uuid).one()
|
||||
except orm.exc.NoResultFound:
|
||||
raise utils.Error(_('Rule %s was not found') % uuid, code=404)
|
||||
|
||||
return IntrospectionRule(uuid=rule.uuid, actions=rule.actions,
|
||||
conditions=rule.conditions,
|
||||
description=rule.description)
|
||||
|
||||
|
||||
def get_all():
|
||||
"""List all rules."""
|
||||
query = db.model_query(db.Rule).order_by(db.Rule.created_at)
|
||||
return [IntrospectionRule(uuid=rule.uuid, actions=rule.actions,
|
||||
conditions=rule.conditions,
|
||||
description=rule.description)
|
||||
for rule in query]
|
||||
|
||||
|
||||
def delete(uuid):
|
||||
"""Delete a rule by its UUID."""
|
||||
with db.ensure_transaction() as session:
|
||||
db.model_query(db.RuleAction,
|
||||
session=session).filter_by(rule=uuid).delete()
|
||||
db.model_query(db.RuleCondition,
|
||||
session=session) .filter_by(rule=uuid).delete()
|
||||
count = (db.model_query(db.Rule, session=session)
|
||||
.filter_by(uuid=uuid).delete())
|
||||
if not count:
|
||||
raise utils.Error(_('Rule %s was not found') % uuid, code=404)
|
||||
|
||||
LOG.info('Introspection rule %s was deleted', uuid)
|
||||
|
||||
|
||||
def delete_all():
|
||||
"""Delete all rules."""
|
||||
with db.ensure_transaction() as session:
|
||||
db.model_query(db.RuleAction, session=session).delete()
|
||||
db.model_query(db.RuleCondition, session=session).delete()
|
||||
db.model_query(db.Rule, session=session).delete()
|
||||
|
||||
LOG.info('All introspection rules were deleted')
|
||||
|
||||
|
||||
def apply(node_info, data):
|
||||
"""Apply rules to a node."""
|
||||
rules = get_all()
|
||||
if not rules:
|
||||
LOG.debug('No custom introspection rules to apply',
|
||||
node_info=node_info, data=data)
|
||||
return
|
||||
|
||||
LOG.debug('Applying custom introspection rules',
|
||||
node_info=node_info, data=data)
|
||||
|
||||
to_apply = []
|
||||
for rule in rules:
|
||||
if rule.check_conditions(node_info, data):
|
||||
to_apply.append(rule)
|
||||
|
||||
if to_apply:
|
||||
LOG.debug('Running actions', node_info=node_info, data=data)
|
||||
for rule in to_apply:
|
||||
rule.apply_actions(node_info, data=data)
|
||||
else:
|
||||
LOG.debug('No actions to apply', node_info=node_info, data=data)
|
||||
|
||||
LOG.info('Successfully applied custom introspection rules',
|
||||
node_info=node_info, data=data)
|
@ -1,207 +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 datetime
|
||||
import time
|
||||
|
||||
import fixtures
|
||||
import futurist
|
||||
import mock
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_config import cfg
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslo_log import log
|
||||
from oslo_utils import units
|
||||
from oslo_utils import uuidutils
|
||||
from oslotest import base as test_base
|
||||
|
||||
from ironic_inspector.common import i18n
|
||||
# Import configuration options
|
||||
from ironic_inspector import conf # noqa
|
||||
from ironic_inspector import db
|
||||
from ironic_inspector import introspection_state as istate
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector.plugins import base as plugins_base
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class BaseTest(test_base.BaseTestCase):
|
||||
|
||||
IS_FUNCTIONAL = False
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTest, self).setUp()
|
||||
if not self.IS_FUNCTIONAL:
|
||||
self.init_test_conf()
|
||||
self.session = db.get_writer_session()
|
||||
engine = self.session.get_bind()
|
||||
db.Base.metadata.create_all(engine)
|
||||
engine.connect()
|
||||
self.addCleanup(engine.dispose)
|
||||
plugins_base._HOOKS_MGR = None
|
||||
node_cache._SEMAPHORES = lockutils.Semaphores()
|
||||
patch = mock.patch.object(i18n, '_', lambda s: s)
|
||||
patch.start()
|
||||
# 'p=patch' magic is due to how closures work
|
||||
self.addCleanup(lambda p=patch: p.stop())
|
||||
utils._EXECUTOR = futurist.SynchronousExecutor(green=True)
|
||||
|
||||
def init_test_conf(self):
|
||||
CONF.reset()
|
||||
log.register_options(CONF)
|
||||
self.cfg = self.useFixture(config_fixture.Config(CONF))
|
||||
self.cfg.set_default('connection', "sqlite:///", group='database')
|
||||
self.cfg.set_default('slave_connection', None, group='database')
|
||||
self.cfg.set_default('max_retries', 10, group='database')
|
||||
|
||||
def assertPatchEqual(self, expected, actual):
|
||||
expected = sorted(expected, key=lambda p: p['path'])
|
||||
actual = sorted(actual, key=lambda p: p['path'])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def assertCalledWithPatch(self, expected, mock_call):
|
||||
def _get_patch_param(call):
|
||||
try:
|
||||
if isinstance(call[0][1], list):
|
||||
return call[0][1]
|
||||
except IndexError:
|
||||
pass
|
||||
return call[0][0]
|
||||
|
||||
actual = sum(map(_get_patch_param, mock_call.call_args_list), [])
|
||||
self.assertPatchEqual(actual, expected)
|
||||
|
||||
|
||||
class InventoryTest(BaseTest):
|
||||
def setUp(self):
|
||||
super(InventoryTest, self).setUp()
|
||||
# Prepare some realistic inventory
|
||||
# https://github.com/openstack/ironic-inspector/blob/master/HTTP-API.rst # noqa
|
||||
self.bmc_address = '1.2.3.4'
|
||||
self.macs = (
|
||||
['11:22:33:44:55:66', '66:55:44:33:22:11', '7c:fe:90:29:26:52'])
|
||||
self.ips = ['1.2.1.2', '1.2.1.1', '1.2.1.3']
|
||||
self.inactive_mac = '12:12:21:12:21:12'
|
||||
self.pxe_mac = self.macs[0]
|
||||
self.all_macs = self.macs + [self.inactive_mac]
|
||||
self.pxe_iface_name = 'eth1'
|
||||
self.client_id = (
|
||||
'ff:00:00:00:00:00:02:00:00:02:c9:00:7c:fe:90:03:00:29:26:52')
|
||||
self.valid_interfaces = {
|
||||
self.pxe_iface_name: {'ip': self.ips[0], 'mac': self.macs[0],
|
||||
'client_id': None, 'pxe': True},
|
||||
'ib0': {'ip': self.ips[2], 'mac': self.macs[2],
|
||||
'client_id': self.client_id, 'pxe': False}
|
||||
}
|
||||
self.data = {
|
||||
'boot_interface': '01-' + self.pxe_mac.replace(':', '-'),
|
||||
'inventory': {
|
||||
'interfaces': [
|
||||
{'name': 'eth1', 'mac_address': self.macs[0],
|
||||
'ipv4_address': self.ips[0],
|
||||
'lldp': [
|
||||
[1, "04112233aabbcc"],
|
||||
[2, "07373334"],
|
||||
[3, "003c"]]},
|
||||
{'name': 'eth2', 'mac_address': self.inactive_mac},
|
||||
{'name': 'eth3', 'mac_address': self.macs[1],
|
||||
'ipv4_address': self.ips[1]},
|
||||
{'name': 'ib0', 'mac_address': self.macs[2],
|
||||
'ipv4_address': self.ips[2],
|
||||
'client_id': self.client_id}
|
||||
],
|
||||
'disks': [
|
||||
{'name': '/dev/sda', 'model': 'Big Data Disk',
|
||||
'size': 1000 * units.Gi},
|
||||
{'name': '/dev/sdb', 'model': 'Small OS Disk',
|
||||
'size': 20 * units.Gi},
|
||||
],
|
||||
'cpu': {
|
||||
'count': 4,
|
||||
'architecture': 'x86_64'
|
||||
},
|
||||
'memory': {
|
||||
'physical_mb': 12288
|
||||
},
|
||||
'bmc_address': self.bmc_address
|
||||
},
|
||||
'root_disk': {'name': '/dev/sda', 'model': 'Big Data Disk',
|
||||
'size': 1000 * units.Gi,
|
||||
'wwn': None},
|
||||
'interfaces': self.valid_interfaces,
|
||||
}
|
||||
self.inventory = self.data['inventory']
|
||||
self.all_interfaces = {
|
||||
'eth1': {'mac': self.macs[0], 'ip': self.ips[0],
|
||||
'client_id': None, 'pxe': True},
|
||||
'eth2': {'mac': self.inactive_mac, 'ip': None,
|
||||
'client_id': None, 'pxe': False},
|
||||
'eth3': {'mac': self.macs[1], 'ip': self.ips[1],
|
||||
'client_id': None, 'pxe': False},
|
||||
'ib0': {'mac': self.macs[2], 'ip': self.ips[2],
|
||||
'client_id': self.client_id, 'pxe': False}
|
||||
}
|
||||
self.active_interfaces = {
|
||||
name: data
|
||||
for (name, data) in self.all_interfaces.items()
|
||||
if data.get('ip')
|
||||
}
|
||||
self.pxe_interfaces = {
|
||||
self.pxe_iface_name: self.all_interfaces[self.pxe_iface_name]
|
||||
}
|
||||
|
||||
|
||||
class NodeTest(InventoryTest):
|
||||
def setUp(self):
|
||||
super(NodeTest, self).setUp()
|
||||
self.uuid = uuidutils.generate_uuid()
|
||||
fake_node = {
|
||||
'driver': 'pxe_ipmitool',
|
||||
'driver_info': {'ipmi_address': self.bmc_address},
|
||||
'properties': {'cpu_arch': 'i386', 'local_gb': 40},
|
||||
'uuid': self.uuid,
|
||||
'power_state': 'power on',
|
||||
'provision_state': 'inspecting',
|
||||
'extra': {},
|
||||
'instance_uuid': None,
|
||||
'maintenance': False
|
||||
}
|
||||
mock_to_dict = mock.Mock(return_value=fake_node)
|
||||
|
||||
self.node = mock.Mock(**fake_node)
|
||||
self.node.to_dict = mock_to_dict
|
||||
|
||||
self.ports = []
|
||||
self.node_info = node_cache.NodeInfo(
|
||||
uuid=self.uuid,
|
||||
started_at=datetime.datetime(1, 1, 1),
|
||||
node=self.node, ports=self.ports)
|
||||
self.node_info.node = mock.Mock(return_value=self.node)
|
||||
self.sleep_fixture = self.useFixture(
|
||||
fixtures.MockPatchObject(time, 'sleep', autospec=True))
|
||||
|
||||
|
||||
class NodeStateTest(NodeTest):
|
||||
def setUp(self):
|
||||
super(NodeStateTest, self).setUp()
|
||||
self.node_info._version_id = uuidutils.generate_uuid()
|
||||
self.node_info._state = istate.States.starting
|
||||
self.db_node = db.Node(uuid=self.node_info.uuid,
|
||||
version_id=self.node_info._version_id,
|
||||
state=self.node_info._state,
|
||||
started_at=self.node_info.started_at,
|
||||
finished_at=self.node_info.finished_at,
|
||||
error=self.node_info.error)
|
||||
self.db_node.save(self.session)
|
@ -1,767 +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 eventlet # noqa
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
import pytz
|
||||
import requests
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
from ironic_inspector.cmd import all as inspector_cmd
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector.common import swift
|
||||
from ironic_inspector import db
|
||||
from ironic_inspector import dbsync
|
||||
from ironic_inspector import introspection_state as istate
|
||||
from ironic_inspector import main
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector import rules
|
||||
from ironic_inspector.test import base
|
||||
from ironic_inspector.test.unit import test_rules
|
||||
|
||||
|
||||
CONF = """
|
||||
[ironic]
|
||||
os_auth_url = http://url
|
||||
os_username = user
|
||||
os_password = password
|
||||
os_tenant_name = tenant
|
||||
[firewall]
|
||||
manage_firewall = False
|
||||
[DEFAULT]
|
||||
debug = True
|
||||
auth_strategy = noauth
|
||||
introspection_delay = 0
|
||||
[database]
|
||||
connection = sqlite:///%(db_file)s
|
||||
[processing]
|
||||
processing_hooks=$default_processing_hooks,lldp_basic
|
||||
"""
|
||||
|
||||
|
||||
DEFAULT_SLEEP = 2
|
||||
TEST_CONF_FILE = None
|
||||
|
||||
|
||||
def get_test_conf_file():
|
||||
global TEST_CONF_FILE
|
||||
if not TEST_CONF_FILE:
|
||||
d = tempfile.mkdtemp()
|
||||
TEST_CONF_FILE = os.path.join(d, 'test.conf')
|
||||
db_file = os.path.join(d, 'test.db')
|
||||
with open(TEST_CONF_FILE, 'wb') as fp:
|
||||
content = CONF % {'db_file': db_file}
|
||||
fp.write(content.encode('utf-8'))
|
||||
return TEST_CONF_FILE
|
||||
|
||||
|
||||
def get_error(response):
|
||||
return response.json()['error']['message']
|
||||
|
||||
|
||||
def _query_string(*field_names):
|
||||
def outer(func):
|
||||
@six.wraps(func)
|
||||
def inner(*args, **kwargs):
|
||||
queries = []
|
||||
for field_name in field_names:
|
||||
field = kwargs.pop(field_name, None)
|
||||
if field is not None:
|
||||
queries.append('%s=%s' % (field_name, field))
|
||||
|
||||
query_string = '&'.join(queries)
|
||||
if query_string:
|
||||
query_string = '?' + query_string
|
||||
return func(*args, query_string=query_string, **kwargs)
|
||||
return inner
|
||||
return outer
|
||||
|
||||
|
||||
class Base(base.NodeTest):
|
||||
ROOT_URL = 'http://127.0.0.1:5050'
|
||||
IS_FUNCTIONAL = True
|
||||
|
||||
def setUp(self):
|
||||
super(Base, self).setUp()
|
||||
rules.delete_all()
|
||||
|
||||
self.cli_fixture = self.useFixture(
|
||||
fixtures.MockPatchObject(ir_utils, 'get_client'))
|
||||
self.cli = self.cli_fixture.mock.return_value
|
||||
self.cli.node.get.return_value = self.node
|
||||
self.cli.node.update.return_value = self.node
|
||||
self.cli.node.list.return_value = [self.node]
|
||||
|
||||
self.patch = [
|
||||
{'op': 'add', 'path': '/properties/cpus', 'value': '4'},
|
||||
{'path': '/properties/cpu_arch', 'value': 'x86_64', 'op': 'add'},
|
||||
{'op': 'add', 'path': '/properties/memory_mb', 'value': '12288'},
|
||||
{'path': '/properties/local_gb', 'value': '999', 'op': 'add'}
|
||||
]
|
||||
self.patch_root_hints = [
|
||||
{'op': 'add', 'path': '/properties/cpus', 'value': '4'},
|
||||
{'path': '/properties/cpu_arch', 'value': 'x86_64', 'op': 'add'},
|
||||
{'op': 'add', 'path': '/properties/memory_mb', 'value': '12288'},
|
||||
{'path': '/properties/local_gb', 'value': '19', 'op': 'add'}
|
||||
]
|
||||
|
||||
self.node.power_state = 'power off'
|
||||
|
||||
self.cfg = self.useFixture(config_fixture.Config())
|
||||
conf_file = get_test_conf_file()
|
||||
self.cfg.set_config_files([conf_file])
|
||||
|
||||
def tearDown(self):
|
||||
super(Base, self).tearDown()
|
||||
node_cache._delete_node(self.uuid)
|
||||
|
||||
def call(self, method, endpoint, data=None, expect_error=None,
|
||||
api_version=None):
|
||||
if data is not None:
|
||||
data = json.dumps(data)
|
||||
endpoint = self.ROOT_URL + endpoint
|
||||
headers = {'X-Auth-Token': 'token'}
|
||||
if api_version:
|
||||
headers[main._VERSION_HEADER] = '%d.%d' % api_version
|
||||
res = getattr(requests, method.lower())(endpoint, data=data,
|
||||
headers=headers)
|
||||
if expect_error:
|
||||
self.assertEqual(expect_error, res.status_code)
|
||||
else:
|
||||
if res.status_code >= 400:
|
||||
msg = ('%(meth)s %(url)s failed with code %(code)s: %(msg)s' %
|
||||
{'meth': method.upper(), 'url': endpoint,
|
||||
'code': res.status_code, 'msg': get_error(res)})
|
||||
raise AssertionError(msg)
|
||||
return res
|
||||
|
||||
def call_introspect(self, uuid, **kwargs):
|
||||
endpoint = '/v1/introspection/%s' % uuid
|
||||
return self.call('post', endpoint, **kwargs)
|
||||
|
||||
def call_get_status(self, uuid, **kwargs):
|
||||
return self.call('get', '/v1/introspection/%s' % uuid, **kwargs).json()
|
||||
|
||||
@_query_string('marker', 'limit')
|
||||
def call_get_statuses(self, query_string='', **kwargs):
|
||||
path = '/v1/introspection'
|
||||
return self.call('get', path + query_string, **kwargs).json()
|
||||
|
||||
def call_abort_introspect(self, uuid, **kwargs):
|
||||
return self.call('post', '/v1/introspection/%s/abort' % uuid, **kwargs)
|
||||
|
||||
def call_reapply(self, uuid, **kwargs):
|
||||
return self.call('post', '/v1/introspection/%s/data/unprocessed' %
|
||||
uuid, **kwargs)
|
||||
|
||||
def call_continue(self, data, **kwargs):
|
||||
return self.call('post', '/v1/continue', data=data, **kwargs).json()
|
||||
|
||||
def call_add_rule(self, data, **kwargs):
|
||||
return self.call('post', '/v1/rules', data=data, **kwargs).json()
|
||||
|
||||
def call_list_rules(self, **kwargs):
|
||||
return self.call('get', '/v1/rules', **kwargs).json()['rules']
|
||||
|
||||
def call_delete_rules(self, **kwargs):
|
||||
self.call('delete', '/v1/rules', **kwargs)
|
||||
|
||||
def call_delete_rule(self, uuid, **kwargs):
|
||||
self.call('delete', '/v1/rules/' + uuid, **kwargs)
|
||||
|
||||
def call_get_rule(self, uuid, **kwargs):
|
||||
return self.call('get', '/v1/rules/' + uuid, **kwargs).json()
|
||||
|
||||
def _fake_status(self, finished=mock.ANY, state=mock.ANY, error=mock.ANY,
|
||||
started_at=mock.ANY, finished_at=mock.ANY,
|
||||
links=mock.ANY):
|
||||
return {'uuid': self.uuid, 'finished': finished, 'error': error,
|
||||
'state': state, 'finished_at': finished_at,
|
||||
'started_at': started_at,
|
||||
'links': [{u'href': u'%s/v1/introspection/%s' % (self.ROOT_URL,
|
||||
self.uuid),
|
||||
u'rel': u'self'}]}
|
||||
|
||||
def check_status(self, status, finished, state, error=None):
|
||||
self.assertEqual(
|
||||
self._fake_status(finished=finished,
|
||||
state=state,
|
||||
finished_at=finished and mock.ANY or None,
|
||||
error=error),
|
||||
status
|
||||
)
|
||||
curr_time = datetime.datetime.fromtimestamp(
|
||||
time.time(), tz=pytz.timezone(time.tzname[0]))
|
||||
started_at = timeutils.parse_isotime(status['started_at'])
|
||||
self.assertLess(started_at, curr_time)
|
||||
if finished:
|
||||
finished_at = timeutils.parse_isotime(status['finished_at'])
|
||||
self.assertLess(started_at, finished_at)
|
||||
self.assertLess(finished_at, curr_time)
|
||||
else:
|
||||
self.assertIsNone(status['finished_at'])
|
||||
|
||||
def db_row(self):
|
||||
"""return database row matching self.uuid."""
|
||||
return db.model_query(db.Node).get(self.uuid)
|
||||
|
||||
|
||||
class Test(Base):
|
||||
def test_bmc(self):
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
|
||||
status = self.call_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(DEFAULT_SLEEP)
|
||||
|
||||
self.cli.node.update.assert_called_once_with(self.uuid, mock.ANY)
|
||||
self.assertCalledWithPatch(self.patch, self.cli.node.update)
|
||||
self.cli.port.create.assert_called_once_with(
|
||||
node_uuid=self.uuid, address='11:22:33:44:55:66', extra={},
|
||||
pxe_enabled=True)
|
||||
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.finished)
|
||||
|
||||
def test_port_creation_update_and_deletion(self):
|
||||
cfg.CONF.set_override('add_ports', 'active', 'processing')
|
||||
cfg.CONF.set_override('keep_ports', 'added', 'processing')
|
||||
|
||||
uuid_to_delete = uuidutils.generate_uuid()
|
||||
uuid_to_update = uuidutils.generate_uuid()
|
||||
# Two ports already exist: one with incorrect pxe_enabled, the other
|
||||
# should be deleted.
|
||||
self.cli.node.list_ports.return_value = [
|
||||
mock.Mock(address=self.macs[1], uuid=uuid_to_update,
|
||||
node_uuid=self.uuid, extra={}, pxe_enabled=True),
|
||||
mock.Mock(address='foobar', uuid=uuid_to_delete,
|
||||
node_uuid=self.uuid, extra={}, pxe_enabled=True),
|
||||
]
|
||||
# Two more ports are created, one with client_id. Make sure the
|
||||
# returned object has the same properties as requested in create().
|
||||
self.cli.port.create.side_effect = mock.Mock
|
||||
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
|
||||
status = self.call_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(DEFAULT_SLEEP)
|
||||
|
||||
self.cli.node.update.assert_called_once_with(self.uuid, mock.ANY)
|
||||
self.assertCalledWithPatch(self.patch, self.cli.node.update)
|
||||
calls = [
|
||||
mock.call(node_uuid=self.uuid, address=self.macs[0],
|
||||
extra={}, pxe_enabled=True),
|
||||
mock.call(node_uuid=self.uuid, address=self.macs[2],
|
||||
extra={'client-id': self.client_id}, pxe_enabled=False),
|
||||
]
|
||||
self.cli.port.create.assert_has_calls(calls, any_order=True)
|
||||
self.cli.port.delete.assert_called_once_with(uuid_to_delete)
|
||||
self.cli.port.update.assert_called_once_with(
|
||||
uuid_to_update,
|
||||
[{'op': 'replace', 'path': '/pxe_enabled', 'value': False}])
|
||||
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.finished)
|
||||
|
||||
def test_introspection_statuses(self):
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
|
||||
# NOTE(zhenguo): only test finished=False here, as we don't know
|
||||
# other nodes status in this thread.
|
||||
statuses = self.call_get_statuses().get('introspection')
|
||||
self.assertIn(self._fake_status(finished=False), statuses)
|
||||
|
||||
# check we've got 1 status with a limit of 1
|
||||
statuses = self.call_get_statuses(limit=1).get('introspection')
|
||||
self.assertEqual(1, len(statuses))
|
||||
|
||||
all_statuses = self.call_get_statuses().get('introspection')
|
||||
marker_statuses = self.call_get_statuses(
|
||||
marker=self.uuid, limit=1).get('introspection')
|
||||
marker_index = all_statuses.index(self.call_get_status(self.uuid))
|
||||
# marker is the last row on previous page
|
||||
self.assertEqual(all_statuses[marker_index+1:marker_index+2],
|
||||
marker_statuses)
|
||||
|
||||
self.call_continue(self.data)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.finished)
|
||||
|
||||
# fetch all statuses and db nodes to assert pagination
|
||||
statuses = self.call_get_statuses().get('introspection')
|
||||
nodes = db.model_query(db.Node).order_by(
|
||||
db.Node.started_at.desc()).all()
|
||||
|
||||
# assert ordering
|
||||
self.assertEqual([node.uuid for node in nodes],
|
||||
[status_.get('uuid') for status_ in statuses])
|
||||
|
||||
# assert pagination
|
||||
half = len(nodes) // 2
|
||||
marker = nodes[half].uuid
|
||||
statuses = self.call_get_statuses(marker=marker).get('introspection')
|
||||
self.assertEqual([node.uuid for node in nodes[half + 1:]],
|
||||
[status_.get('uuid') for status_ in statuses])
|
||||
|
||||
# assert status links work
|
||||
self.assertEqual([self.call_get_status(status_.get('uuid'))
|
||||
for status_ in statuses],
|
||||
[self.call('GET', urllib.parse.urlparse(
|
||||
status_.get('links')[0].get('href')).path).json()
|
||||
for status_ in statuses])
|
||||
|
||||
def test_rules_api(self):
|
||||
res = self.call_list_rules()
|
||||
self.assertEqual([], res)
|
||||
|
||||
rule = {
|
||||
'conditions': [
|
||||
{'op': 'eq', 'field': 'memory_mb', 'value': 1024},
|
||||
],
|
||||
'actions': [{'action': 'fail', 'message': 'boom'}],
|
||||
'description': 'Cool actions'
|
||||
}
|
||||
|
||||
res = self.call_add_rule(rule)
|
||||
self.assertTrue(res['uuid'])
|
||||
rule['uuid'] = res['uuid']
|
||||
rule['links'] = res['links']
|
||||
rule['conditions'] = [
|
||||
test_rules.BaseTest.condition_defaults(rule['conditions'][0]),
|
||||
]
|
||||
self.assertEqual(rule, res)
|
||||
|
||||
res = self.call('get', rule['links'][0]['href']).json()
|
||||
self.assertEqual(rule, res)
|
||||
|
||||
res = self.call_list_rules()
|
||||
self.assertEqual(rule['links'], res[0].pop('links'))
|
||||
self.assertEqual([{'uuid': rule['uuid'],
|
||||
'description': 'Cool actions'}],
|
||||
res)
|
||||
|
||||
res = self.call_get_rule(rule['uuid'])
|
||||
self.assertEqual(rule, res)
|
||||
|
||||
self.call_delete_rule(rule['uuid'])
|
||||
res = self.call_list_rules()
|
||||
self.assertEqual([], res)
|
||||
|
||||
links = rule.pop('links')
|
||||
del rule['uuid']
|
||||
for _ in range(3):
|
||||
self.call_add_rule(rule)
|
||||
|
||||
res = self.call_list_rules()
|
||||
self.assertEqual(3, len(res))
|
||||
|
||||
self.call_delete_rules()
|
||||
res = self.call_list_rules()
|
||||
self.assertEqual([], res)
|
||||
|
||||
self.call('get', links[0]['href'], expect_error=404)
|
||||
self.call('delete', links[0]['href'], expect_error=404)
|
||||
|
||||
def test_introspection_rules(self):
|
||||
self.node.extra['bar'] = 'foo'
|
||||
rules = [
|
||||
{
|
||||
'conditions': [
|
||||
{'field': 'memory_mb', 'op': 'eq', 'value': 12288},
|
||||
{'field': 'local_gb', 'op': 'gt', 'value': 998},
|
||||
{'field': 'local_gb', 'op': 'lt', 'value': 1000},
|
||||
{'field': 'local_gb', 'op': 'matches', 'value': '[0-9]+'},
|
||||
{'field': 'cpu_arch', 'op': 'contains', 'value': '[0-9]+'},
|
||||
{'field': 'root_disk.wwn', 'op': 'is-empty'},
|
||||
{'field': 'inventory.interfaces[*].ipv4_address',
|
||||
'op': 'contains', 'value': r'127\.0\.0\.1',
|
||||
'invert': True, 'multiple': 'all'},
|
||||
{'field': 'i.do.not.exist', 'op': 'is-empty'},
|
||||
],
|
||||
'actions': [
|
||||
{'action': 'set-attribute', 'path': '/extra/foo',
|
||||
'value': 'bar'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'conditions': [
|
||||
{'field': 'memory_mb', 'op': 'ge', 'value': 100500},
|
||||
],
|
||||
'actions': [
|
||||
{'action': 'set-attribute', 'path': '/extra/bar',
|
||||
'value': 'foo'},
|
||||
{'action': 'fail', 'message': 'boom'}
|
||||
]
|
||||
}
|
||||
]
|
||||
for rule in rules:
|
||||
self.call_add_rule(rule)
|
||||
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.call_continue(self.data)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
|
||||
self.cli.node.update.assert_any_call(
|
||||
self.uuid,
|
||||
[{'op': 'add', 'path': '/extra/foo', 'value': 'bar'}])
|
||||
|
||||
def test_conditions_scheme_actions_path(self):
|
||||
rules = [
|
||||
{
|
||||
'conditions': [
|
||||
{'field': 'node://properties.local_gb', 'op': 'eq',
|
||||
'value': 40},
|
||||
{'field': 'node://driver_info.ipmi_address', 'op': 'eq',
|
||||
'value': self.bmc_address},
|
||||
],
|
||||
'actions': [
|
||||
{'action': 'set-attribute', 'path': '/extra/foo',
|
||||
'value': 'bar'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'conditions': [
|
||||
{'field': 'data://inventory.cpu.count', 'op': 'eq',
|
||||
'value': self.data['inventory']['cpu']['count']},
|
||||
],
|
||||
'actions': [
|
||||
{'action': 'set-attribute',
|
||||
'path': '/driver_info/ipmi_address',
|
||||
'value': '{data[inventory][bmc_address]}'}
|
||||
]
|
||||
}
|
||||
]
|
||||
for rule in rules:
|
||||
self.call_add_rule(rule)
|
||||
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.call_continue(self.data)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
|
||||
self.cli.node.update.assert_any_call(
|
||||
self.uuid,
|
||||
[{'op': 'add', 'path': '/extra/foo', 'value': 'bar'}])
|
||||
|
||||
self.cli.node.update.assert_any_call(
|
||||
self.uuid,
|
||||
[{'op': 'add', 'path': '/driver_info/ipmi_address',
|
||||
'value': self.data['inventory']['bmc_address']}])
|
||||
|
||||
def test_root_device_hints(self):
|
||||
self.node.properties['root_device'] = {'size': 20}
|
||||
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
|
||||
status = self.call_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(DEFAULT_SLEEP)
|
||||
|
||||
self.assertCalledWithPatch(self.patch_root_hints, self.cli.node.update)
|
||||
self.cli.port.create.assert_called_once_with(
|
||||
node_uuid=self.uuid, address='11:22:33:44:55:66', extra={},
|
||||
pxe_enabled=True)
|
||||
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.finished)
|
||||
|
||||
def test_abort_introspection(self):
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=False, state=istate.States.waiting)
|
||||
|
||||
res = self.call_abort_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
|
||||
self.assertEqual(202, res.status_code)
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.assertTrue(status['finished'])
|
||||
self.assertEqual('Canceled by operator', status['error'])
|
||||
|
||||
# Note(mkovacik): we're checking just this doesn't pass OK as
|
||||
# there might be either a race condition (hard to test) that
|
||||
# yields a 'Node already finished.' or an attribute-based
|
||||
# look-up error from some pre-processing hooks because
|
||||
# node_info.finished() deletes the look-up attributes only
|
||||
# after releasing the node lock
|
||||
self.call('post', '/v1/continue', self.data, expect_error=400)
|
||||
|
||||
@mock.patch.object(swift, 'store_introspection_data', autospec=True)
|
||||
@mock.patch.object(swift, 'get_introspection_data', autospec=True)
|
||||
def test_stored_data_processing(self, get_mock, store_mock):
|
||||
cfg.CONF.set_override('store_data', 'swift', 'processing')
|
||||
|
||||
# ramdisk data copy
|
||||
# please mind the data is changed during processing
|
||||
ramdisk_data = json.dumps(copy.deepcopy(self.data))
|
||||
get_mock.return_value = ramdisk_data
|
||||
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
|
||||
res = self.call_continue(self.data)
|
||||
self.assertEqual({'uuid': self.uuid}, res)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
|
||||
status = self.call_get_status(self.uuid)
|
||||
inspect_started_at = timeutils.parse_isotime(status['started_at'])
|
||||
self.check_status(status, finished=True, state=istate.States.finished)
|
||||
|
||||
res = self.call_reapply(self.uuid)
|
||||
self.assertEqual(202, res.status_code)
|
||||
self.assertEqual('', res.text)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.finished)
|
||||
|
||||
# checks the started_at updated in DB is correct
|
||||
reapply_started_at = timeutils.parse_isotime(status['started_at'])
|
||||
self.assertLess(inspect_started_at, reapply_started_at)
|
||||
|
||||
# reapply request data
|
||||
get_mock.assert_called_once_with(self.uuid,
|
||||
suffix='UNPROCESSED')
|
||||
|
||||
# store ramdisk data, store processing result data, store
|
||||
# reapply processing result data; the ordering isn't
|
||||
# guaranteed as store ramdisk data runs in a background
|
||||
# thread; hower, last call has to always be reapply processing
|
||||
# result data
|
||||
store_ramdisk_call = mock.call(mock.ANY, self.uuid,
|
||||
suffix='UNPROCESSED')
|
||||
store_processing_call = mock.call(mock.ANY, self.uuid,
|
||||
suffix=None)
|
||||
self.assertEqual(3, len(store_mock.call_args_list))
|
||||
self.assertIn(store_ramdisk_call,
|
||||
store_mock.call_args_list[0:2])
|
||||
self.assertIn(store_processing_call,
|
||||
store_mock.call_args_list[0:2])
|
||||
self.assertEqual(store_processing_call,
|
||||
store_mock.call_args_list[2])
|
||||
|
||||
# second reapply call
|
||||
get_mock.return_value = ramdisk_data
|
||||
res = self.call_reapply(self.uuid)
|
||||
self.assertEqual(202, res.status_code)
|
||||
self.assertEqual('', res.text)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
|
||||
# reapply saves the result
|
||||
self.assertEqual(4, len(store_mock.call_args_list))
|
||||
self.assertEqual(store_processing_call,
|
||||
store_mock.call_args_list[-1])
|
||||
|
||||
@mock.patch.object(swift, 'store_introspection_data', autospec=True)
|
||||
@mock.patch.object(swift, 'get_introspection_data', autospec=True)
|
||||
def test_edge_state_transitions(self, get_mock, store_mock):
|
||||
"""Assert state transitions work as expected in edge conditions."""
|
||||
cfg.CONF.set_override('store_data', 'swift', 'processing')
|
||||
|
||||
# ramdisk data copy
|
||||
# please mind the data is changed during processing
|
||||
ramdisk_data = json.dumps(copy.deepcopy(self.data))
|
||||
get_mock.return_value = ramdisk_data
|
||||
|
||||
# multiple introspect calls
|
||||
self.call_introspect(self.uuid)
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=False, state=istate.States.waiting)
|
||||
|
||||
# an error -start-> starting state transition is possible
|
||||
self.call_abort_introspect(self.uuid)
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=False, state=istate.States.waiting)
|
||||
|
||||
# double abort works
|
||||
self.call_abort_introspect(self.uuid)
|
||||
status = self.call_get_status(self.uuid)
|
||||
error = status['error']
|
||||
self.check_status(status, finished=True, state=istate.States.error,
|
||||
error=error)
|
||||
self.call_abort_introspect(self.uuid)
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.error,
|
||||
error=error)
|
||||
|
||||
# preventing stale data race condition
|
||||
# waiting -> processing is a strict state transition
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
row = self.db_row()
|
||||
row.state = istate.States.processing
|
||||
with db.ensure_transaction() as session:
|
||||
row.save(session)
|
||||
self.call_continue(self.data, expect_error=400)
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.error,
|
||||
error=mock.ANY)
|
||||
self.assertIn('no defined transition', status['error'])
|
||||
# multiple reapply calls
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.call_continue(self.data)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.call_reapply(self.uuid)
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.finished,
|
||||
error=None)
|
||||
self.call_reapply(self.uuid)
|
||||
# assert an finished -reapply-> reapplying -> finished state transition
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.finished,
|
||||
error=None)
|
||||
|
||||
def test_without_root_disk(self):
|
||||
del self.data['root_disk']
|
||||
self.inventory['disks'] = []
|
||||
self.patch[-1] = {'path': '/properties/local_gb',
|
||||
'value': '0', 'op': 'add'}
|
||||
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
|
||||
status = self.call_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(DEFAULT_SLEEP)
|
||||
|
||||
self.cli.node.update.assert_called_once_with(self.uuid, mock.ANY)
|
||||
self.assertCalledWithPatch(self.patch, self.cli.node.update)
|
||||
self.cli.port.create.assert_called_once_with(
|
||||
node_uuid=self.uuid, extra={}, address='11:22:33:44:55:66',
|
||||
pxe_enabled=True)
|
||||
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.finished)
|
||||
|
||||
@mock.patch.object(swift, 'store_introspection_data', autospec=True)
|
||||
@mock.patch.object(swift, 'get_introspection_data', autospec=True)
|
||||
def test_lldp_plugin(self, get_mock, store_mock):
|
||||
cfg.CONF.set_override('store_data', 'swift', 'processing')
|
||||
|
||||
ramdisk_data = json.dumps(copy.deepcopy(self.data))
|
||||
get_mock.return_value = ramdisk_data
|
||||
|
||||
self.call_introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
||||
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
|
||||
status = self.call_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(DEFAULT_SLEEP)
|
||||
|
||||
status = self.call_get_status(self.uuid)
|
||||
self.check_status(status, finished=True, state=istate.States.finished)
|
||||
|
||||
# Verify that the lldp_processed data is written to swift
|
||||
# as expected by the lldp plugin
|
||||
updated_data = store_mock.call_args[0][0]
|
||||
lldp_out = updated_data['all_interfaces']['eth1']
|
||||
|
||||
expected_chassis_id = "11:22:33:aa:bb:cc"
|
||||
expected_port_id = "734"
|
||||
self.assertEqual(expected_chassis_id,
|
||||
lldp_out['lldp_processed']['switch_chassis_id'])
|
||||
self.assertEqual(expected_port_id,
|
||||
lldp_out['lldp_processed']['switch_port_id'])
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def mocked_server():
|
||||
conf_file = get_test_conf_file()
|
||||
dbsync.main(args=['--config-file', conf_file, 'upgrade'])
|
||||
|
||||
cfg.CONF.reset()
|
||||
cfg.CONF.unregister_opt(dbsync.command_opt)
|
||||
|
||||
eventlet.greenthread.spawn_n(inspector_cmd.main,
|
||||
args=['--config-file', conf_file])
|
||||
eventlet.greenthread.sleep(1)
|
||||
# Wait for service to start up to 30 seconds
|
||||
for i in range(10):
|
||||
try:
|
||||
requests.get('http://127.0.0.1:5050/v1')
|
||||
except requests.ConnectionError:
|
||||
if i == 9:
|
||||
raise
|
||||
print('Service did not start yet')
|
||||
eventlet.greenthread.sleep(3)
|
||||
else:
|
||||
break
|
||||
# start testing
|
||||
yield
|
||||
# Make sure all processes finished executing
|
||||
eventlet.greenthread.sleep(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with mocked_server():
|
||||
unittest.main(verbosity=2)
|
@ -1,18 +0,0 @@
|
||||
=======================================
|
||||
Tempest Integration of ironic-inspector
|
||||
=======================================
|
||||
|
||||
This directory contains Tempest tests to cover the ironic-inspector project.
|
||||
|
||||
It uses tempest plugin to automatically load these tests into tempest. More
|
||||
information about tempest plugin could be found here:
|
||||
`Plugin <http://docs.openstack.org/developer/tempest/plugin.html>`_
|
||||
|
||||
The legacy method of running Tempest is to just treat the Tempest source code
|
||||
as a python unittest:
|
||||
`Run tests <http://docs.openstack.org/developer/tempest/overview.html#legacy-run-method>`_
|
||||
|
||||
There is also tox configuration for tempest, use following regex for running
|
||||
introspection tests::
|
||||
|
||||
$ tox -e all-plugin -- inspector_tempest_plugin
|
@ -1,66 +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 oslo_config import cfg
|
||||
|
||||
service_option = cfg.BoolOpt("ironic-inspector",
|
||||
default=True,
|
||||
help="Whether or not ironic-inspector is expected"
|
||||
" to be available")
|
||||
|
||||
baremetal_introspection_group = cfg.OptGroup(
|
||||
name="baremetal_introspection",
|
||||
title="Baremetal introspection service options",
|
||||
help="When enabling baremetal introspection tests,"
|
||||
"Ironic must be configured.")
|
||||
|
||||
BaremetalIntrospectionGroup = [
|
||||
cfg.StrOpt('catalog_type',
|
||||
default='baremetal-introspection',
|
||||
help="Catalog type of the baremetal provisioning service"),
|
||||
cfg.StrOpt('endpoint_type',
|
||||
default='publicURL',
|
||||
choices=['public', 'admin', 'internal',
|
||||
'publicURL', 'adminURL', 'internalURL'],
|
||||
help="The endpoint type to use for the baremetal introspection"
|
||||
" service"),
|
||||
cfg.IntOpt('introspection_sleep',
|
||||
default=30,
|
||||
help="Introspection sleep before check status"),
|
||||
cfg.IntOpt('introspection_timeout',
|
||||
default=600,
|
||||
help="Introspection time out"),
|
||||
cfg.IntOpt('hypervisor_update_sleep',
|
||||
default=60,
|
||||
help="Time to wait until nova becomes aware of "
|
||||
"bare metal instances"),
|
||||
cfg.IntOpt('hypervisor_update_timeout',
|
||||
default=300,
|
||||
help="Time out for wait until nova becomes aware of "
|
||||
"bare metal instances"),
|
||||
# NOTE(aarefiev): status_check_period default is 60s, but checking
|
||||
# node state takes some time(API call), so races appear here,
|
||||
# 80s would be enough to make one more check.
|
||||
cfg.IntOpt('ironic_sync_timeout',
|
||||
default=80,
|
||||
help="Time it might take for Ironic--Inspector "
|
||||
"sync to happen"),
|
||||
cfg.IntOpt('discovery_timeout',
|
||||
default=300,
|
||||
help="Time to wait until new node would enrolled in "
|
||||
"ironic"),
|
||||
cfg.BoolOpt('auto_discovery_feature',
|
||||
default=False,
|
||||
help="Is the auto-discovery feature enabled. Enroll hook "
|
||||
"should be specified in node_not_found_hook - processing "
|
||||
"section of inspector.conf"),
|
||||
]
|
@ -1,25 +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 tempest.lib import exceptions
|
||||
|
||||
|
||||
class IntrospectionFailed(exceptions.TempestException):
|
||||
message = "Introspection failed"
|
||||
|
||||
|
||||
class IntrospectionTimeout(exceptions.TempestException):
|
||||
message = "Introspection time out"
|
||||
|
||||
|
||||
class HypervisorUpdateTimeout(exceptions.TempestException):
|
||||
message = "Hypervisor stats update time out"
|
@ -1,41 +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 os
|
||||
|
||||
from tempest.test_discover import plugins
|
||||
|
||||
from ironic_inspector.test.inspector_tempest_plugin import config
|
||||
|
||||
|
||||
class InspectorTempestPlugin(plugins.TempestPlugin):
|
||||
def load_tests(self):
|
||||
base_path = os.path.split(os.path.dirname(
|
||||
os.path.abspath(__file__)))[0]
|
||||
test_dir = "inspector_tempest_plugin/tests"
|
||||
full_test_dir = os.path.join(base_path, test_dir)
|
||||
return full_test_dir, base_path
|
||||
|
||||
def register_opts(self, conf):
|
||||
conf.register_opt(config.service_option,
|
||||
group='service_available')
|
||||
conf.register_group(config.baremetal_introspection_group)
|
||||
conf.register_opts(config.BaremetalIntrospectionGroup,
|
||||
group="baremetal_introspection")
|
||||
|
||||
def get_opt_lists(self):
|
||||
return [
|
||||
(config.baremetal_introspection_group.name,
|
||||
config.BaremetalIntrospectionGroup),
|
||||
('service_available', [config.service_option])
|
||||
]
|
@ -1,25 +0,0 @@
|
||||
[
|
||||
{
|
||||
"description": "Successful Rule",
|
||||
"conditions": [
|
||||
{"op": "ge", "field": "memory_mb", "value": 256},
|
||||
{"op": "ge", "field": "local_gb", "value": 1}
|
||||
],
|
||||
"actions": [
|
||||
{"action": "set-attribute", "path": "/extra/rule_success",
|
||||
"value": "yes"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Failing Rule",
|
||||
"conditions": [
|
||||
{"op": "lt", "field": "memory_mb", "value": 42},
|
||||
{"op": "eq", "field": "local_gb", "value": 0}
|
||||
],
|
||||
"actions": [
|
||||
{"action": "set-attribute", "path": "/extra/rule_success",
|
||||
"value": "no"},
|
||||
{"action": "fail", "message": "This rule should not have run"}
|
||||
]
|
||||
}
|
||||
]
|
@ -1,83 +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 ironic_tempest_plugin.services.baremetal import base
|
||||
from tempest import clients
|
||||
from tempest.common import credentials_factory as common_creds
|
||||
from tempest import config
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
ADMIN_CREDS = common_creds.get_configured_admin_credentials()
|
||||
|
||||
|
||||
class Manager(clients.Manager):
|
||||
def __init__(self,
|
||||
credentials=ADMIN_CREDS,
|
||||
api_microversions=None):
|
||||
super(Manager, self).__init__(credentials)
|
||||
self.introspection_client = BaremetalIntrospectionClient(
|
||||
self.auth_provider,
|
||||
CONF.baremetal_introspection.catalog_type,
|
||||
CONF.identity.region,
|
||||
endpoint_type=CONF.baremetal_introspection.endpoint_type)
|
||||
|
||||
|
||||
class BaremetalIntrospectionClient(base.BaremetalClient):
|
||||
"""Base Tempest REST client for Ironic Inspector API v1."""
|
||||
version = '1'
|
||||
uri_prefix = 'v1'
|
||||
|
||||
@base.handle_errors
|
||||
def purge_rules(self):
|
||||
"""Purge all existing rules."""
|
||||
return self._delete_request('rules', uuid=None)
|
||||
|
||||
@base.handle_errors
|
||||
def create_rules(self, rules):
|
||||
"""Create introspection rules."""
|
||||
if not isinstance(rules, list):
|
||||
rules = [rules]
|
||||
for rule in rules:
|
||||
self._create_request('rules', rule)
|
||||
|
||||
@base.handle_errors
|
||||
def get_status(self, uuid):
|
||||
"""Get introspection status for a node."""
|
||||
return self._show_request('introspection', uuid=uuid)
|
||||
|
||||
@base.handle_errors
|
||||
def get_data(self, uuid):
|
||||
"""Get introspection data for a node."""
|
||||
return self._show_request('introspection', uuid=uuid,
|
||||
uri='/%s/introspection/%s/data' %
|
||||
(self.uri_prefix, uuid))
|
||||
|
||||
@base.handle_errors
|
||||
def start_introspection(self, uuid):
|
||||
"""Start introspection for a node."""
|
||||
resp, _body = self.post(url=('/%s/introspection/%s' %
|
||||
(self.uri_prefix, uuid)),
|
||||
body=None)
|
||||
self.expected_success(202, resp.status)
|
||||
|
||||
return resp
|
||||
|
||||
@base.handle_errors
|
||||
def abort_introspection(self, uuid):
|
||||
"""Abort introspection for a node."""
|
||||
resp, _body = self.post(url=('/%s/introspection/%s/abort' %
|
||||
(self.uri_prefix, uuid)),
|
||||
body=None)
|
||||
self.expected_success(202, resp.status)
|
||||
|
||||
return resp
|
@ -1,244 +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 time
|
||||
|
||||
import six
|
||||
import tempest
|
||||
from tempest import config
|
||||
from tempest.lib.common.api_version_utils import LATEST_MICROVERSION
|
||||
from tempest.lib.common.utils import test_utils
|
||||
from tempest.lib import exceptions as lib_exc
|
||||
|
||||
from ironic_inspector.test.inspector_tempest_plugin import exceptions
|
||||
from ironic_inspector.test.inspector_tempest_plugin.services import \
|
||||
introspection_client
|
||||
from ironic_tempest_plugin.tests.api.admin.api_microversion_fixture import \
|
||||
APIMicroversionFixture as IronicMicroversionFixture
|
||||
from ironic_tempest_plugin.tests.scenario.baremetal_manager import \
|
||||
BaremetalProvisionStates
|
||||
from ironic_tempest_plugin.tests.scenario.baremetal_manager import \
|
||||
BaremetalScenarioTest
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class InspectorScenarioTest(BaremetalScenarioTest):
|
||||
"""Provide harness to do Inspector scenario tests."""
|
||||
|
||||
wait_provisioning_state_interval = 15
|
||||
|
||||
credentials = ['primary', 'admin']
|
||||
|
||||
ironic_api_version = LATEST_MICROVERSION
|
||||
|
||||
@classmethod
|
||||
def setup_clients(cls):
|
||||
super(InspectorScenarioTest, cls).setup_clients()
|
||||
inspector_manager = introspection_client.Manager()
|
||||
cls.introspection_client = inspector_manager.introspection_client
|
||||
|
||||
def setUp(self):
|
||||
super(InspectorScenarioTest, self).setUp()
|
||||
# we rely on the 'available' provision_state; using latest
|
||||
# microversion
|
||||
self.useFixture(IronicMicroversionFixture(self.ironic_api_version))
|
||||
self.flavor = self.baremetal_flavor()
|
||||
self.node_ids = {node['uuid'] for node in
|
||||
self.node_filter(filter=lambda node:
|
||||
node['provision_state'] ==
|
||||
BaremetalProvisionStates.AVAILABLE)}
|
||||
self.rule_purge()
|
||||
|
||||
def item_filter(self, list_method, show_method,
|
||||
filter=lambda item: True, items=None):
|
||||
if items is None:
|
||||
items = [show_method(item['uuid']) for item in
|
||||
list_method()]
|
||||
return [item for item in items if filter(item)]
|
||||
|
||||
def node_list(self):
|
||||
return self.baremetal_client.list_nodes()[1]['nodes']
|
||||
|
||||
def node_port_list(self, node_uuid):
|
||||
return self.baremetal_client.list_node_ports(node_uuid)[1]['ports']
|
||||
|
||||
def node_update(self, uuid, patch):
|
||||
return self.baremetal_client.update_node(uuid, **patch)
|
||||
|
||||
def node_show(self, uuid):
|
||||
return self.baremetal_client.show_node(uuid)[1]
|
||||
|
||||
def node_delete(self, uuid):
|
||||
return self.baremetal_client.delete_node(uuid)
|
||||
|
||||
def node_filter(self, filter=lambda node: True, nodes=None):
|
||||
return self.item_filter(self.node_list, self.node_show,
|
||||
filter=filter, items=nodes)
|
||||
|
||||
def node_set_power_state(self, uuid, state):
|
||||
self.baremetal_client.set_node_power_state(uuid, state)
|
||||
|
||||
def node_set_provision_state(self, uuid, state):
|
||||
self.baremetal_client.set_node_provision_state(self, uuid, state)
|
||||
|
||||
def hypervisor_stats(self):
|
||||
return (self.admin_manager.hypervisor_client.
|
||||
show_hypervisor_statistics())
|
||||
|
||||
def server_show(self, uuid):
|
||||
self.servers_client.show_server(uuid)
|
||||
|
||||
def rule_purge(self):
|
||||
self.introspection_client.purge_rules()
|
||||
|
||||
def rule_import(self, rule_path):
|
||||
with open(rule_path, 'r') as fp:
|
||||
rules = json.load(fp)
|
||||
self.introspection_client.create_rules(rules)
|
||||
|
||||
def rule_import_from_dict(self, rules):
|
||||
self.introspection_client.create_rules(rules)
|
||||
|
||||
def introspection_status(self, uuid):
|
||||
return self.introspection_client.get_status(uuid)[1]
|
||||
|
||||
def introspection_data(self, uuid):
|
||||
return self.introspection_client.get_data(uuid)[1]
|
||||
|
||||
def introspection_start(self, uuid):
|
||||
return self.introspection_client.start_introspection(uuid)
|
||||
|
||||
def introspection_abort(self, uuid):
|
||||
return self.introspection_client.abort_introspection(uuid)
|
||||
|
||||
def baremetal_flavor(self):
|
||||
flavor_id = CONF.compute.flavor_ref
|
||||
flavor = self.flavors_client.show_flavor(flavor_id)['flavor']
|
||||
flavor['properties'] = self.flavors_client.list_flavor_extra_specs(
|
||||
flavor_id)['extra_specs']
|
||||
return flavor
|
||||
|
||||
def get_rule_path(self, rule_file):
|
||||
base_path = os.path.split(
|
||||
os.path.dirname(os.path.abspath(__file__)))[0]
|
||||
base_path = os.path.split(base_path)[0]
|
||||
return os.path.join(base_path, "inspector_tempest_plugin",
|
||||
"rules", rule_file)
|
||||
|
||||
def boot_instance(self):
|
||||
return super(InspectorScenarioTest, self).boot_instance()
|
||||
|
||||
def terminate_instance(self, instance):
|
||||
return super(InspectorScenarioTest, self).terminate_instance(instance)
|
||||
|
||||
def wait_for_node(self, node_name):
|
||||
def check_node():
|
||||
try:
|
||||
self.node_show(node_name)
|
||||
except lib_exc.NotFound:
|
||||
return False
|
||||
return True
|
||||
|
||||
if not test_utils.call_until_true(
|
||||
check_node,
|
||||
duration=CONF.baremetal_introspection.discovery_timeout,
|
||||
sleep_for=20):
|
||||
msg = ("Timed out waiting for node %s " % node_name)
|
||||
raise lib_exc.TimeoutException(msg)
|
||||
|
||||
inspected_node = self.node_show(self.node_info['name'])
|
||||
self.wait_for_introspection_finished(inspected_node['uuid'])
|
||||
|
||||
# TODO(aarefiev): switch to call_until_true
|
||||
def wait_for_introspection_finished(self, node_ids):
|
||||
"""Waits for introspection of baremetal nodes to finish.
|
||||
|
||||
"""
|
||||
if isinstance(node_ids, six.text_type):
|
||||
node_ids = [node_ids]
|
||||
start = int(time.time())
|
||||
not_introspected = {node_id for node_id in node_ids}
|
||||
|
||||
while not_introspected:
|
||||
time.sleep(CONF.baremetal_introspection.introspection_sleep)
|
||||
for node_id in node_ids:
|
||||
status = self.introspection_status(node_id)
|
||||
if status['finished']:
|
||||
if status['error']:
|
||||
message = ('Node %(node_id)s introspection failed '
|
||||
'with %(error)s.' %
|
||||
{'node_id': node_id,
|
||||
'error': status['error']})
|
||||
raise exceptions.IntrospectionFailed(message)
|
||||
not_introspected = not_introspected - {node_id}
|
||||
|
||||
if (int(time.time()) - start >=
|
||||
CONF.baremetal_introspection.introspection_timeout):
|
||||
message = ('Introspection timed out for nodes: %s' %
|
||||
not_introspected)
|
||||
raise exceptions.IntrospectionTimeout(message)
|
||||
|
||||
def wait_for_nova_aware_of_bvms(self):
|
||||
start = int(time.time())
|
||||
while True:
|
||||
time.sleep(CONF.baremetal_introspection.hypervisor_update_sleep)
|
||||
stats = self.hypervisor_stats()
|
||||
expected_cpus = self.baremetal_flavor()['vcpus']
|
||||
if int(stats['hypervisor_statistics']['vcpus']) >= expected_cpus:
|
||||
break
|
||||
|
||||
timeout = CONF.baremetal_introspection.hypervisor_update_timeout
|
||||
if (int(time.time()) - start >= timeout):
|
||||
message = (
|
||||
'Timeout while waiting for nova hypervisor-stats: '
|
||||
'%(stats)s required time (%(timeout)s s).' %
|
||||
{'stats': stats,
|
||||
'timeout': timeout})
|
||||
raise exceptions.HypervisorUpdateTimeout(message)
|
||||
|
||||
def node_cleanup(self, node_id):
|
||||
if (self.node_show(node_id)['provision_state'] ==
|
||||
BaremetalProvisionStates.AVAILABLE):
|
||||
return
|
||||
# in case when introspection failed we need set provision state
|
||||
# to 'manage' to make it possible transit into 'provide' state
|
||||
if self.node_show(node_id)['provision_state'] == 'inspect failed':
|
||||
self.baremetal_client.set_node_provision_state(node_id, 'manage')
|
||||
|
||||
try:
|
||||
self.baremetal_client.set_node_provision_state(node_id, 'provide')
|
||||
except tempest.lib.exceptions.RestClientException:
|
||||
# maybe node already cleaning or available
|
||||
pass
|
||||
|
||||
self.wait_provisioning_state(
|
||||
node_id, [BaremetalProvisionStates.AVAILABLE,
|
||||
BaremetalProvisionStates.NOSTATE],
|
||||
timeout=CONF.baremetal.unprovision_timeout,
|
||||
interval=self.wait_provisioning_state_interval)
|
||||
|
||||
def introspect_node(self, node_id, remove_props=True):
|
||||
if remove_props:
|
||||
# in case there are properties remove those
|
||||
patch = {('properties/%s' % key): None for key in
|
||||
self.node_show(node_id)['properties']}
|
||||
# reset any previous rule result
|
||||
patch['extra/rule_success'] = None
|
||||
self.node_update(node_id, patch)
|
||||
|
||||
self.baremetal_client.set_node_provision_state(node_id, 'manage')
|
||||
self.baremetal_client.set_node_provision_state(node_id, 'inspect')
|
||||
self.addCleanup(self.node_cleanup, node_id)
|
@ -1,175 +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 tempest.config import CONF
|
||||
from tempest.lib import decorators
|
||||
from tempest import test # noqa
|
||||
|
||||
from ironic_inspector.test.inspector_tempest_plugin.tests import manager
|
||||
from ironic_tempest_plugin.tests.scenario import baremetal_manager
|
||||
|
||||
|
||||
class InspectorBasicTest(manager.InspectorScenarioTest):
|
||||
|
||||
def verify_node_introspection_data(self, node):
|
||||
self.assertEqual('yes', node['extra']['rule_success'])
|
||||
data = self.introspection_data(node['uuid'])
|
||||
self.assertEqual(data['cpu_arch'],
|
||||
self.flavor['properties']['cpu_arch'])
|
||||
self.assertEqual(int(data['memory_mb']),
|
||||
int(self.flavor['ram']))
|
||||
self.assertEqual(int(data['cpus']), int(self.flavor['vcpus']))
|
||||
|
||||
def verify_node_flavor(self, node):
|
||||
expected_cpus = self.flavor['vcpus']
|
||||
expected_memory_mb = self.flavor['ram']
|
||||
expected_cpu_arch = self.flavor['properties']['cpu_arch']
|
||||
disk_size = self.flavor['disk']
|
||||
ephemeral_size = self.flavor['OS-FLV-EXT-DATA:ephemeral']
|
||||
expected_local_gb = disk_size + ephemeral_size
|
||||
|
||||
self.assertEqual(expected_cpus,
|
||||
int(node['properties']['cpus']))
|
||||
self.assertEqual(expected_memory_mb,
|
||||
int(node['properties']['memory_mb']))
|
||||
self.assertEqual(expected_local_gb,
|
||||
int(node['properties']['local_gb']))
|
||||
self.assertEqual(expected_cpu_arch,
|
||||
node['properties']['cpu_arch'])
|
||||
|
||||
def verify_introspection_aborted(self, uuid):
|
||||
status = self.introspection_status(uuid)
|
||||
|
||||
self.assertEqual('Canceled by operator', status['error'])
|
||||
self.assertTrue(status['finished'])
|
||||
|
||||
self.wait_provisioning_state(
|
||||
uuid, 'inspect failed',
|
||||
timeout=CONF.baremetal.active_timeout,
|
||||
interval=self.wait_provisioning_state_interval)
|
||||
|
||||
@decorators.idempotent_id('03bf7990-bee0-4dd7-bf74-b97ad7b52a4b')
|
||||
@test.services('compute', 'image', 'network', 'object_storage')
|
||||
def test_baremetal_introspection(self):
|
||||
"""This smoke test case follows this set of operations:
|
||||
|
||||
* Fetches expected properties from baremetal flavor
|
||||
* Removes all properties from nodes
|
||||
* Sets nodes to manageable state
|
||||
* Imports introspection rule basic_ops_rule.json
|
||||
* Inspects nodes
|
||||
* Verifies all properties are inspected
|
||||
* Verifies introspection data
|
||||
* Sets node to available state
|
||||
* Creates a keypair
|
||||
* Boots an instance using the keypair
|
||||
* Deletes the instance
|
||||
|
||||
"""
|
||||
# prepare introspection rule
|
||||
rule_path = self.get_rule_path("basic_ops_rule.json")
|
||||
self.rule_import(rule_path)
|
||||
self.addCleanup(self.rule_purge)
|
||||
|
||||
for node_id in self.node_ids:
|
||||
self.introspect_node(node_id)
|
||||
|
||||
# settle down introspection
|
||||
self.wait_for_introspection_finished(self.node_ids)
|
||||
for node_id in self.node_ids:
|
||||
self.wait_provisioning_state(
|
||||
node_id, 'manageable',
|
||||
timeout=CONF.baremetal_introspection.ironic_sync_timeout,
|
||||
interval=self.wait_provisioning_state_interval)
|
||||
|
||||
for node_id in self.node_ids:
|
||||
node = self.node_show(node_id)
|
||||
self.verify_node_introspection_data(node)
|
||||
self.verify_node_flavor(node)
|
||||
|
||||
for node_id in self.node_ids:
|
||||
self.baremetal_client.set_node_provision_state(node_id, 'provide')
|
||||
|
||||
for node_id in self.node_ids:
|
||||
self.wait_provisioning_state(
|
||||
node_id, baremetal_manager.BaremetalProvisionStates.AVAILABLE,
|
||||
timeout=CONF.baremetal.active_timeout,
|
||||
interval=self.wait_provisioning_state_interval)
|
||||
|
||||
self.wait_for_nova_aware_of_bvms()
|
||||
self.add_keypair()
|
||||
ins, _node = self.boot_instance()
|
||||
self.terminate_instance(ins)
|
||||
|
||||
@decorators.idempotent_id('70ca3070-184b-4b7d-8892-e977d2bc2870')
|
||||
def test_introspection_abort(self):
|
||||
"""This smoke test case follows this very basic set of operations:
|
||||
|
||||
* Start nodes introspection
|
||||
* Wait until nodes power on
|
||||
* Abort introspection
|
||||
* Verifies nodes status and power state
|
||||
|
||||
"""
|
||||
# start nodes introspection
|
||||
for node_id in self.node_ids:
|
||||
self.introspect_node(node_id, remove_props=False)
|
||||
|
||||
# wait for nodes power on
|
||||
for node_id in self.node_ids:
|
||||
self.wait_power_state(
|
||||
node_id,
|
||||
baremetal_manager.BaremetalPowerStates.POWER_ON)
|
||||
|
||||
# abort introspection
|
||||
for node_id in self.node_ids:
|
||||
self.introspection_abort(node_id)
|
||||
|
||||
# wait for nodes power off
|
||||
for node_id in self.node_ids:
|
||||
self.wait_power_state(
|
||||
node_id,
|
||||
baremetal_manager.BaremetalPowerStates.POWER_OFF)
|
||||
|
||||
# verify nodes status and provision state
|
||||
for node_id in self.node_ids:
|
||||
self.verify_introspection_aborted(node_id)
|
||||
|
||||
|
||||
class InspectorSmokeTest(manager.InspectorScenarioTest):
|
||||
|
||||
@decorators.idempotent_id('a702d1f1-88e4-42ce-88ef-cba2d9e3312e')
|
||||
@decorators.attr(type='smoke')
|
||||
@test.services('object_storage')
|
||||
def test_baremetal_introspection(self):
|
||||
"""This smoke test case follows this very basic set of operations:
|
||||
|
||||
* Fetches expected properties from baremetal flavor
|
||||
* Removes all properties from one node
|
||||
* Sets the node to manageable state
|
||||
* Inspects the node
|
||||
* Sets the node to available state
|
||||
|
||||
"""
|
||||
# NOTE(dtantsur): we can't silently skip this test because it runs in
|
||||
# grenade with several other tests, and we won't have any indication
|
||||
# that it was not run.
|
||||
assert self.node_ids, "No available nodes"
|
||||
node_id = next(iter(self.node_ids))
|
||||
self.introspect_node(node_id)
|
||||
|
||||
# settle down introspection
|
||||
self.wait_for_introspection_finished([node_id])
|
||||
self.wait_provisioning_state(
|
||||
node_id, 'manageable',
|
||||
timeout=CONF.baremetal_introspection.ironic_sync_timeout,
|
||||
interval=self.wait_provisioning_state_interval)
|
@ -1,149 +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 six
|
||||
|
||||
from ironic_tempest_plugin.tests.scenario import baremetal_manager
|
||||
from tempest import config
|
||||
from tempest.lib import decorators
|
||||
from tempest import test # noqa
|
||||
|
||||
from ironic_inspector.test.inspector_tempest_plugin.tests import manager
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
ProvisionStates = baremetal_manager.BaremetalProvisionStates
|
||||
|
||||
|
||||
class InspectorDiscoveryTest(manager.InspectorScenarioTest):
|
||||
@classmethod
|
||||
def skip_checks(cls):
|
||||
super(InspectorDiscoveryTest, cls).skip_checks()
|
||||
if not CONF.baremetal_introspection.auto_discovery_feature:
|
||||
msg = ("Please, provide a value for node_not_found_hook in "
|
||||
"processing section of inspector.conf for enable "
|
||||
"auto-discovery feature.")
|
||||
raise cls.skipException(msg)
|
||||
|
||||
def setUp(self):
|
||||
super(InspectorDiscoveryTest, self).setUp()
|
||||
|
||||
discovered_node = self._get_discovery_node()
|
||||
self.node_info = self._get_node_info(discovered_node)
|
||||
|
||||
rule = self._generate_discovery_rule(self.node_info)
|
||||
|
||||
self.rule_import_from_dict(rule)
|
||||
self.addCleanup(self.rule_purge)
|
||||
|
||||
def _get_node_info(self, node_uuid):
|
||||
node = self.node_show(node_uuid)
|
||||
ports = self.node_port_list(node_uuid)
|
||||
node['port_macs'] = [port['address'] for port in ports]
|
||||
return node
|
||||
|
||||
def _get_discovery_node(self):
|
||||
nodes = self.node_list()
|
||||
|
||||
discovered_node = None
|
||||
for node in nodes:
|
||||
if (node['provision_state'] == ProvisionStates.AVAILABLE or
|
||||
node['provision_state'] == ProvisionStates.ENROLL or
|
||||
node['provision_state'] is ProvisionStates.NOSTATE):
|
||||
discovered_node = node['uuid']
|
||||
break
|
||||
|
||||
self.assertIsNotNone(discovered_node)
|
||||
return discovered_node
|
||||
|
||||
def _generate_discovery_rule(self, node):
|
||||
rule = dict()
|
||||
rule["description"] = "Node %s discovery rule" % node['name']
|
||||
rule["actions"] = [
|
||||
{"action": "set-attribute", "path": "/name",
|
||||
"value": "%s" % node['name']},
|
||||
{"action": "set-attribute", "path": "/driver",
|
||||
"value": "%s" % node['driver']},
|
||||
]
|
||||
|
||||
for key, value in node['driver_info'].items():
|
||||
rule["actions"].append(
|
||||
{"action": "set-attribute", "path": "/driver_info/%s" % key,
|
||||
"value": "%s" % value})
|
||||
rule["conditions"] = [
|
||||
{"op": "eq", "field": "data://auto_discovered", "value": True}
|
||||
]
|
||||
return rule
|
||||
|
||||
def verify_node_introspection_data(self, node):
|
||||
data = self.introspection_data(node['uuid'])
|
||||
self.assertEqual(data['cpu_arch'],
|
||||
self.flavor['properties']['cpu_arch'])
|
||||
self.assertEqual(int(data['memory_mb']),
|
||||
int(self.flavor['ram']))
|
||||
self.assertEqual(int(data['cpus']), int(self.flavor['vcpus']))
|
||||
|
||||
def verify_node_flavor(self, node):
|
||||
expected_cpus = self.flavor['vcpus']
|
||||
expected_memory_mb = self.flavor['ram']
|
||||
expected_cpu_arch = self.flavor['properties']['cpu_arch']
|
||||
disk_size = self.flavor['disk']
|
||||
ephemeral_size = self.flavor['OS-FLV-EXT-DATA:ephemeral']
|
||||
expected_local_gb = disk_size + ephemeral_size
|
||||
|
||||
self.assertEqual(expected_cpus,
|
||||
int(node['properties']['cpus']))
|
||||
self.assertEqual(expected_memory_mb,
|
||||
int(node['properties']['memory_mb']))
|
||||
self.assertEqual(expected_local_gb,
|
||||
int(node['properties']['local_gb']))
|
||||
self.assertEqual(expected_cpu_arch,
|
||||
node['properties']['cpu_arch'])
|
||||
|
||||
def verify_node_driver_info(self, node_info, inspected_node):
|
||||
for key in node_info['driver_info']:
|
||||
self.assertEqual(six.text_type(node_info['driver_info'][key]),
|
||||
inspected_node['driver_info'].get(key))
|
||||
|
||||
@decorators.idempotent_id('dd3abe5e-0d23-488d-bb4e-344cdeff7dcb')
|
||||
def test_bearmetal_auto_discovery(self):
|
||||
"""This test case follows this set of operations:
|
||||
|
||||
* Choose appropriate node, based on provision state;
|
||||
* Get node info;
|
||||
* Generate discovery rule;
|
||||
* Delete discovered node from ironic;
|
||||
* Start baremetal vm via virsh;
|
||||
* Wating for node introspection;
|
||||
* Verify introspected node.
|
||||
"""
|
||||
# NOTE(aarefiev): workaround for infra, 'tempest' user doesn't
|
||||
# have virsh privileges, so lets power on the node via ironic
|
||||
# and then delete it. Because of node is blacklisted in inspector
|
||||
# we can't just power on it, therefor start introspection is used
|
||||
# to whitelist discovered node first.
|
||||
self.baremetal_client.set_node_provision_state(
|
||||
self.node_info['uuid'], 'manage')
|
||||
self.introspection_start(self.node_info['uuid'])
|
||||
self.wait_power_state(
|
||||
self.node_info['uuid'],
|
||||
baremetal_manager.BaremetalPowerStates.POWER_ON)
|
||||
self.node_delete(self.node_info['uuid'])
|
||||
|
||||
self.wait_for_node(self.node_info['name'])
|
||||
|
||||
inspected_node = self.node_show(self.node_info['name'])
|
||||
self.verify_node_flavor(inspected_node)
|
||||
self.verify_node_introspection_data(inspected_node)
|
||||
self.verify_node_driver_info(self.node_info, inspected_node)
|
||||
self.assertEqual(ProvisionStates.ENROLL,
|
||||
inspected_node['provision_state'])
|
@ -1,136 +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 flask
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
|
||||
from ironic_inspector import api_tools
|
||||
import ironic_inspector.test.base as test_base
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
app = flask.Flask(__name__)
|
||||
app.testing = True
|
||||
|
||||
|
||||
def mock_test_field(return_value=None, side_effect=None):
|
||||
"""Mock flask.request.args.get"""
|
||||
def outer(func):
|
||||
@six.wraps(func)
|
||||
def inner(self, *args, **kwargs):
|
||||
with app.test_request_context('/'):
|
||||
get_mock = flask.request.args.get = mock.Mock()
|
||||
get_mock.return_value = return_value
|
||||
get_mock.side_effect = side_effect
|
||||
ret = func(self, get_mock, *args, **kwargs)
|
||||
return ret
|
||||
return inner
|
||||
return outer
|
||||
|
||||
|
||||
class RaisesCoercionExceptionTestCase(test_base.BaseTest):
|
||||
def test_ok(self):
|
||||
@api_tools.raises_coercion_exceptions
|
||||
def fn():
|
||||
return True
|
||||
self.assertIs(True, fn())
|
||||
|
||||
def test_assertion_error(self):
|
||||
@api_tools.raises_coercion_exceptions
|
||||
def fn():
|
||||
assert False, 'Oops!'
|
||||
|
||||
six.assertRaisesRegex(self, utils.Error, 'Bad request: Oops!', fn)
|
||||
|
||||
def test_value_error(self):
|
||||
@api_tools.raises_coercion_exceptions
|
||||
def fn():
|
||||
raise ValueError('Oops!')
|
||||
|
||||
six.assertRaisesRegex(self, utils.Error, 'Bad request: Oops!', fn)
|
||||
|
||||
|
||||
class RequestFieldTestCase(test_base.BaseTest):
|
||||
@mock_test_field(return_value='42')
|
||||
def test_request_field_ok(self, get_mock):
|
||||
@api_tools.request_field('foo')
|
||||
def fn(value):
|
||||
self.assertEqual(get_mock.return_value, value)
|
||||
|
||||
fn()
|
||||
get_mock.assert_called_once_with('foo', default=None)
|
||||
|
||||
@mock_test_field(return_value='42')
|
||||
def test_request_field_with_default(self, get_mock):
|
||||
@api_tools.request_field('foo')
|
||||
def fn(value):
|
||||
self.assertEqual(get_mock.return_value, value)
|
||||
|
||||
fn(default='bar')
|
||||
get_mock.assert_called_once_with('foo', default='bar')
|
||||
|
||||
@mock_test_field(return_value=42)
|
||||
def test_request_field_with_default_returns_default(self, get_mock):
|
||||
@api_tools.request_field('foo')
|
||||
def fn(value):
|
||||
self.assertEqual(get_mock.return_value, value)
|
||||
|
||||
fn(default=42)
|
||||
get_mock.assert_called_once_with('foo', default=42)
|
||||
|
||||
|
||||
class MarkerFieldTestCase(test_base.BaseTest):
|
||||
@mock_test_field(return_value=uuidutils.generate_uuid())
|
||||
def test_marker_ok(self, get_mock):
|
||||
value = api_tools.marker_field()
|
||||
self.assertEqual(get_mock.return_value, value)
|
||||
|
||||
@mock.patch.object(uuidutils, 'is_uuid_like', autospec=True)
|
||||
@mock_test_field(return_value='foo')
|
||||
def test_marker_check_fails(self, get_mock, like_mock):
|
||||
like_mock.return_value = False
|
||||
six.assertRaisesRegex(self, utils.Error, '.*(Marker not UUID-like)',
|
||||
api_tools.marker_field)
|
||||
like_mock.assert_called_once_with(get_mock.return_value)
|
||||
|
||||
|
||||
class LimitFieldTestCase(test_base.BaseTest):
|
||||
@mock_test_field(return_value=42)
|
||||
def test_limit_ok(self, get_mock):
|
||||
value = api_tools.limit_field()
|
||||
self.assertEqual(get_mock.return_value, value)
|
||||
|
||||
@mock_test_field(return_value=str(CONF.api_max_limit + 1))
|
||||
def test_limit_over(self, get_mock):
|
||||
six.assertRaisesRegex(self, utils.Error,
|
||||
'.*(Limit over %s)' % CONF.api_max_limit,
|
||||
api_tools.limit_field)
|
||||
|
||||
@mock_test_field(return_value='0')
|
||||
def test_limit_zero(self, get_mock):
|
||||
value = api_tools.limit_field()
|
||||
self.assertEqual(CONF.api_max_limit, value)
|
||||
|
||||
@mock_test_field(return_value='-1')
|
||||
def test_limit_negative(self, get_mock):
|
||||
six.assertRaisesRegex(self, utils.Error,
|
||||
'.*(Limit cannot be negative)',
|
||||
api_tools.limit_field)
|
||||
|
||||
@mock_test_field(return_value='foo')
|
||||
def test_limit_invalid_value(self, get_mock):
|
||||
six.assertRaisesRegex(self, utils.Error, 'Bad request',
|
||||
api_tools.limit_field)
|
@ -1,131 +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 socket
|
||||
import unittest
|
||||
|
||||
from ironicclient import client
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector.common import keystone
|
||||
from ironic_inspector.test import base
|
||||
from ironic_inspector import utils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@mock.patch.object(keystone, 'register_auth_opts')
|
||||
@mock.patch.object(keystone, 'get_session')
|
||||
@mock.patch.object(client, 'Client')
|
||||
class TestGetClient(base.BaseTest):
|
||||
def setUp(self):
|
||||
super(TestGetClient, self).setUp()
|
||||
ir_utils.reset_ironic_session()
|
||||
self.cfg.config(auth_strategy='keystone')
|
||||
self.cfg.config(os_region='somewhere', group='ironic')
|
||||
self.addCleanup(ir_utils.reset_ironic_session)
|
||||
|
||||
def test_get_client_with_auth_token(self, mock_client, mock_load,
|
||||
mock_opts):
|
||||
fake_token = 'token'
|
||||
fake_ironic_url = 'http://127.0.0.1:6385'
|
||||
mock_sess = mock.Mock()
|
||||
mock_sess.get_endpoint.return_value = fake_ironic_url
|
||||
mock_load.return_value = mock_sess
|
||||
ir_utils.get_client(fake_token)
|
||||
mock_sess.get_endpoint.assert_called_once_with(
|
||||
endpoint_type=CONF.ironic.os_endpoint_type,
|
||||
service_type=CONF.ironic.os_service_type,
|
||||
region_name=CONF.ironic.os_region)
|
||||
args = {'token': fake_token,
|
||||
'endpoint': fake_ironic_url,
|
||||
'os_ironic_api_version': ir_utils.DEFAULT_IRONIC_API_VERSION,
|
||||
'max_retries': CONF.ironic.max_retries,
|
||||
'retry_interval': CONF.ironic.retry_interval}
|
||||
mock_client.assert_called_once_with(1, **args)
|
||||
|
||||
def test_get_client_without_auth_token(self, mock_client, mock_load,
|
||||
mock_opts):
|
||||
mock_sess = mock.Mock()
|
||||
mock_load.return_value = mock_sess
|
||||
ir_utils.get_client(None)
|
||||
args = {'session': mock_sess,
|
||||
'region_name': 'somewhere',
|
||||
'os_ironic_api_version': ir_utils.DEFAULT_IRONIC_API_VERSION,
|
||||
'max_retries': CONF.ironic.max_retries,
|
||||
'retry_interval': CONF.ironic.retry_interval}
|
||||
mock_client.assert_called_once_with(1, **args)
|
||||
|
||||
|
||||
class TestGetIpmiAddress(base.BaseTest):
|
||||
def test_ipv4_in_resolves(self):
|
||||
node = mock.Mock(spec=['driver_info', 'uuid'],
|
||||
driver_info={'ipmi_address': '192.168.1.1'})
|
||||
ip = ir_utils.get_ipmi_address(node)
|
||||
self.assertEqual('192.168.1.1', ip)
|
||||
|
||||
@mock.patch('socket.gethostbyname')
|
||||
def test_good_hostname_resolves(self, mock_socket):
|
||||
node = mock.Mock(spec=['driver_info', 'uuid'],
|
||||
driver_info={'ipmi_address': 'www.example.com'})
|
||||
mock_socket.return_value = '192.168.1.1'
|
||||
ip = ir_utils.get_ipmi_address(node)
|
||||
mock_socket.assert_called_once_with('www.example.com')
|
||||
self.assertEqual('192.168.1.1', ip)
|
||||
|
||||
@mock.patch('socket.gethostbyname')
|
||||
def test_bad_hostname_errors(self, mock_socket):
|
||||
node = mock.Mock(spec=['driver_info', 'uuid'],
|
||||
driver_info={'ipmi_address': 'meow'},
|
||||
uuid='uuid1')
|
||||
mock_socket.side_effect = socket.gaierror('Boom')
|
||||
self.assertRaises(utils.Error, ir_utils.get_ipmi_address, node)
|
||||
|
||||
def test_additional_fields(self):
|
||||
node = mock.Mock(spec=['driver_info', 'uuid'],
|
||||
driver_info={'foo': '192.168.1.1'})
|
||||
self.assertIsNone(ir_utils.get_ipmi_address(node))
|
||||
|
||||
self.cfg.config(ipmi_address_fields=['foo', 'bar', 'baz'])
|
||||
ip = ir_utils.get_ipmi_address(node)
|
||||
self.assertEqual('192.168.1.1', ip)
|
||||
|
||||
def test_ipmi_bridging_enabled(self):
|
||||
node = mock.Mock(spec=['driver_info', 'uuid'],
|
||||
driver_info={'ipmi_address': 'www.example.com',
|
||||
'ipmi_bridging': 'single'})
|
||||
self.assertIsNone(ir_utils.get_ipmi_address(node))
|
||||
|
||||
def test_loopback_address(self):
|
||||
node = mock.Mock(spec=['driver_info', 'uuid'],
|
||||
driver_info={'ipmi_address': '127.0.0.2'})
|
||||
ip = ir_utils.get_ipmi_address(node)
|
||||
self.assertIsNone(ip)
|
||||
|
||||
|
||||
class TestCapabilities(unittest.TestCase):
|
||||
|
||||
def test_capabilities_to_dict(self):
|
||||
capabilities = 'cat:meow,dog:wuff'
|
||||
expected_output = {'cat': 'meow', 'dog': 'wuff'}
|
||||
output = ir_utils.capabilities_to_dict(capabilities)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def test_dict_to_capabilities(self):
|
||||
capabilities_dict = {'cat': 'meow', 'dog': 'wuff'}
|
||||
output = ir_utils.dict_to_capabilities(capabilities_dict)
|
||||
self.assertIn('cat:meow', output)
|
||||
self.assertIn('dog:wuff', output)
|
@ -1,77 +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 mock
|
||||
|
||||
from ironic_inspector import db
|
||||
from ironic_inspector.test import base as test_base
|
||||
|
||||
|
||||
class TestDB(test_base.NodeTest):
|
||||
@mock.patch.object(db, 'get_reader_session', autospec=True)
|
||||
def test_model_query(self, mock_reader):
|
||||
mock_session = mock_reader.return_value
|
||||
fake_query = mock_session.query.return_value
|
||||
|
||||
query = db.model_query('db.Node')
|
||||
|
||||
mock_reader.assert_called_once_with()
|
||||
mock_session.query.assert_called_once_with('db.Node')
|
||||
self.assertEqual(fake_query, query)
|
||||
|
||||
@mock.patch.object(db, 'get_writer_session', autospec=True)
|
||||
def test_ensure_transaction_new_session(self, mock_writer):
|
||||
mock_session = mock_writer.return_value
|
||||
|
||||
with db.ensure_transaction() as session:
|
||||
mock_writer.assert_called_once_with()
|
||||
mock_session.begin.assert_called_once_with(subtransactions=True)
|
||||
self.assertEqual(mock_session, session)
|
||||
|
||||
@mock.patch.object(db, 'get_writer_session', autospec=True)
|
||||
def test_ensure_transaction_session(self, mock_writer):
|
||||
mock_session = mock.MagicMock()
|
||||
|
||||
with db.ensure_transaction(session=mock_session) as session:
|
||||
self.assertFalse(mock_writer.called)
|
||||
mock_session.begin.assert_called_once_with(subtransactions=True)
|
||||
self.assertEqual(mock_session, session)
|
||||
|
||||
@mock.patch.object(db.enginefacade, 'transaction_context', autospec=True)
|
||||
def test__create_context_manager(self, mock_cnxt):
|
||||
mock_ctx_mgr = mock_cnxt.return_value
|
||||
|
||||
ctx_mgr = db._create_context_manager()
|
||||
|
||||
mock_ctx_mgr.configure.assert_called_once_with(sqlite_fk=False)
|
||||
self.assertEqual(mock_ctx_mgr, ctx_mgr)
|
||||
|
||||
@mock.patch.object(db, 'get_context_manager', autospec=True)
|
||||
def test_get_reader_session(self, mock_cnxt_mgr):
|
||||
mock_cnxt = mock_cnxt_mgr.return_value
|
||||
mock_sess_maker = mock_cnxt.reader.get_sessionmaker.return_value
|
||||
|
||||
session = db.get_reader_session()
|
||||
|
||||
mock_sess_maker.assert_called_once_with()
|
||||
self.assertEqual(mock_sess_maker.return_value, session)
|
||||
|
||||
@mock.patch.object(db, 'get_context_manager', autospec=True)
|
||||
def test_get_writer_session(self, mock_cnxt_mgr):
|
||||
mock_cnxt = mock_cnxt_mgr.return_value
|
||||
mock_sess_maker = mock_cnxt.writer.get_sessionmaker.return_value
|
||||
|
||||
session = db.get_writer_session()
|
||||
|
||||
mock_sess_maker.assert_called_once_with()
|
||||
self.assertEqual(mock_sess_maker.return_value, session)
|
@ -1,444 +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.
|
||||
|
||||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import firewall
|
||||
from ironic_inspector import introspection_state as istate
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector.test import base as test_base
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
IB_DATA = """
|
||||
EMAC=02:00:02:97:00:01 IMAC=97:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:26:52
|
||||
EMAC=02:00:00:61:00:02 IMAC=61:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:24:4f
|
||||
"""
|
||||
|
||||
|
||||
@mock.patch.object(firewall, '_iptables')
|
||||
@mock.patch.object(ir_utils, 'get_client')
|
||||
@mock.patch.object(firewall.subprocess, 'check_call')
|
||||
class TestFirewall(test_base.NodeTest):
|
||||
CLIENT_ID = 'ff:00:00:00:00:00:02:00:00:02:c9:00:7c:fe:90:03:00:29:24:4f'
|
||||
|
||||
def test_update_filters_without_manage_firewall(self, mock_call,
|
||||
mock_get_client,
|
||||
mock_iptables):
|
||||
CONF.set_override('manage_firewall', False, 'firewall')
|
||||
firewall.update_filters()
|
||||
self.assertEqual(0, mock_iptables.call_count)
|
||||
|
||||
def test_init_args(self, mock_call, mock_get_client, mock_iptables):
|
||||
rootwrap_path = '/some/fake/path'
|
||||
CONF.set_override('rootwrap_config', rootwrap_path)
|
||||
firewall.init()
|
||||
init_expected_args = [
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', '67',
|
||||
'-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-N', CONF.firewall.firewall_chain)]
|
||||
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (args, call) in zip(init_expected_args, call_args_list):
|
||||
self.assertEqual(args, call[0])
|
||||
|
||||
expected = ('sudo', 'ironic-inspector-rootwrap', rootwrap_path,
|
||||
'iptables', '-w')
|
||||
self.assertEqual(expected, firewall.BASE_COMMAND)
|
||||
|
||||
def test_init_args_old_iptables(self, mock_call, mock_get_client,
|
||||
mock_iptables):
|
||||
rootwrap_path = '/some/fake/path'
|
||||
CONF.set_override('rootwrap_config', rootwrap_path)
|
||||
mock_call.side_effect = firewall.subprocess.CalledProcessError(2, '')
|
||||
firewall.init()
|
||||
init_expected_args = [
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', '67',
|
||||
'-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-N', CONF.firewall.firewall_chain)]
|
||||
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (args, call) in zip(init_expected_args, call_args_list):
|
||||
self.assertEqual(args, call[0])
|
||||
|
||||
expected = ('sudo', 'ironic-inspector-rootwrap', rootwrap_path,
|
||||
'iptables',)
|
||||
self.assertEqual(expected, firewall.BASE_COMMAND)
|
||||
|
||||
def test_init_kwargs(self, mock_call, mock_get_client, mock_iptables):
|
||||
firewall.init()
|
||||
init_expected_kwargs = [
|
||||
{'ignore': True},
|
||||
{'ignore': True},
|
||||
{'ignore': True}]
|
||||
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (kwargs, call) in zip(init_expected_kwargs, call_args_list):
|
||||
self.assertEqual(kwargs, call[1])
|
||||
|
||||
def test_update_filters_args(self, mock_call, mock_get_client,
|
||||
mock_iptables):
|
||||
# Pretend that we have nodes on introspection
|
||||
node_cache.add_node(self.node.uuid, state=istate.States.waiting,
|
||||
bmc_address='1.2.3.4')
|
||||
|
||||
firewall.init()
|
||||
|
||||
update_filters_expected_args = [
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-N', CONF.firewall.firewall_chain),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-F', firewall.NEW_CHAIN),
|
||||
('-X', firewall.NEW_CHAIN),
|
||||
('-N', firewall.NEW_CHAIN),
|
||||
('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT'),
|
||||
('-I', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-E', firewall.NEW_CHAIN, CONF.firewall.firewall_chain)
|
||||
]
|
||||
|
||||
firewall.update_filters()
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (args, call) in zip(update_filters_expected_args,
|
||||
call_args_list):
|
||||
self.assertEqual(args, call[0])
|
||||
|
||||
def test_update_filters_kwargs(self, mock_call, mock_get_client,
|
||||
mock_iptables):
|
||||
firewall.init()
|
||||
|
||||
update_filters_expected_kwargs = [
|
||||
{'ignore': True},
|
||||
{'ignore': True},
|
||||
{'ignore': True},
|
||||
{},
|
||||
{'ignore': True},
|
||||
{'ignore': True},
|
||||
{'ignore': True},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{'ignore': True},
|
||||
{'ignore': True},
|
||||
{'ignore': True}
|
||||
]
|
||||
|
||||
firewall.update_filters()
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (kwargs, call) in zip(update_filters_expected_kwargs,
|
||||
call_args_list):
|
||||
self.assertEqual(kwargs, call[1])
|
||||
|
||||
def test_update_filters_with_blacklist(self, mock_call, mock_get_client,
|
||||
mock_iptables):
|
||||
active_macs = ['11:22:33:44:55:66', '66:55:44:33:22:11']
|
||||
inactive_mac = ['AA:BB:CC:DD:EE:FF']
|
||||
self.macs = active_macs + inactive_mac
|
||||
self.ports = [mock.Mock(address=m) for m in self.macs]
|
||||
mock_get_client.port.list.return_value = self.ports
|
||||
node_cache.add_node(self.node.uuid, mac=active_macs,
|
||||
state=istate.States.finished,
|
||||
bmc_address='1.2.3.4', foo=None)
|
||||
firewall.init()
|
||||
|
||||
update_filters_expected_args = [
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-N', CONF.firewall.firewall_chain),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-F', firewall.NEW_CHAIN),
|
||||
('-X', firewall.NEW_CHAIN),
|
||||
('-N', firewall.NEW_CHAIN),
|
||||
# Blacklist
|
||||
('-A', firewall.NEW_CHAIN, '-m', 'mac', '--mac-source',
|
||||
inactive_mac[0], '-j', 'DROP'),
|
||||
('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT'),
|
||||
('-I', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-E', firewall.NEW_CHAIN, CONF.firewall.firewall_chain)
|
||||
]
|
||||
|
||||
firewall.update_filters(mock_get_client)
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (args, call) in zip(update_filters_expected_args,
|
||||
call_args_list):
|
||||
self.assertEqual(args, call[0])
|
||||
|
||||
# check caching
|
||||
|
||||
mock_iptables.reset_mock()
|
||||
firewall.update_filters(mock_get_client)
|
||||
self.assertFalse(mock_iptables.called)
|
||||
|
||||
def test_update_filters_clean_cache_on_error(self, mock_call,
|
||||
mock_get_client,
|
||||
mock_iptables):
|
||||
active_macs = ['11:22:33:44:55:66', '66:55:44:33:22:11']
|
||||
inactive_mac = ['AA:BB:CC:DD:EE:FF']
|
||||
self.macs = active_macs + inactive_mac
|
||||
self.ports = [mock.Mock(address=m) for m in self.macs]
|
||||
mock_get_client.port.list.return_value = self.ports
|
||||
node_cache.add_node(self.node.uuid, mac=active_macs,
|
||||
state=istate.States.finished,
|
||||
bmc_address='1.2.3.4', foo=None)
|
||||
firewall.init()
|
||||
|
||||
update_filters_expected_args = [
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-F', firewall.NEW_CHAIN),
|
||||
('-X', firewall.NEW_CHAIN),
|
||||
('-N', firewall.NEW_CHAIN),
|
||||
# Blacklist
|
||||
('-A', firewall.NEW_CHAIN, '-m', 'mac', '--mac-source',
|
||||
inactive_mac[0], '-j', 'DROP'),
|
||||
('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT'),
|
||||
('-I', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-E', firewall.NEW_CHAIN, CONF.firewall.firewall_chain)
|
||||
]
|
||||
|
||||
mock_iptables.side_effect = [None, None, RuntimeError()]
|
||||
self.assertRaises(RuntimeError, firewall.update_filters,
|
||||
mock_get_client)
|
||||
|
||||
# check caching
|
||||
|
||||
mock_iptables.reset_mock()
|
||||
mock_iptables.side_effect = None
|
||||
firewall.update_filters(mock_get_client)
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (args, call) in zip(update_filters_expected_args,
|
||||
call_args_list):
|
||||
self.assertEqual(args, call[0])
|
||||
|
||||
def test_update_filters_args_node_not_found_hook(self, mock_call,
|
||||
mock_get_client,
|
||||
mock_iptables):
|
||||
# DHCP should be always opened if node_not_found hook is set
|
||||
CONF.set_override('node_not_found_hook', 'enroll', 'processing')
|
||||
|
||||
firewall.init()
|
||||
|
||||
update_filters_expected_args = [
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-N', CONF.firewall.firewall_chain),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-F', firewall.NEW_CHAIN),
|
||||
('-X', firewall.NEW_CHAIN),
|
||||
('-N', firewall.NEW_CHAIN),
|
||||
('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT'),
|
||||
('-I', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-E', firewall.NEW_CHAIN, CONF.firewall.firewall_chain)
|
||||
]
|
||||
|
||||
firewall.update_filters()
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (args, call) in zip(update_filters_expected_args,
|
||||
call_args_list):
|
||||
self.assertEqual(args, call[0])
|
||||
|
||||
def test_update_filters_args_no_introspection(self, mock_call,
|
||||
mock_get_client,
|
||||
mock_iptables):
|
||||
firewall.init()
|
||||
firewall.BLACKLIST_CACHE = ['foo']
|
||||
mock_get_client.return_value.port.list.return_value = [
|
||||
mock.Mock(address='foobar')]
|
||||
|
||||
update_filters_expected_args = [
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-N', CONF.firewall.firewall_chain),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-F', firewall.NEW_CHAIN),
|
||||
('-X', firewall.NEW_CHAIN),
|
||||
('-N', firewall.NEW_CHAIN),
|
||||
('-A', firewall.NEW_CHAIN, '-j', 'REJECT'),
|
||||
('-I', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-E', firewall.NEW_CHAIN, CONF.firewall.firewall_chain)
|
||||
]
|
||||
|
||||
firewall.update_filters()
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (args, call) in zip(update_filters_expected_args,
|
||||
call_args_list):
|
||||
self.assertEqual(args, call[0])
|
||||
|
||||
self.assertIsNone(firewall.BLACKLIST_CACHE)
|
||||
|
||||
# Check caching enabled flag
|
||||
|
||||
mock_iptables.reset_mock()
|
||||
firewall.update_filters()
|
||||
self.assertFalse(mock_iptables.called)
|
||||
|
||||
# Adding a node changes it back
|
||||
|
||||
node_cache.add_node(self.node.uuid, state=istate.States.starting,
|
||||
bmc_address='1.2.3.4')
|
||||
mock_iptables.reset_mock()
|
||||
firewall.update_filters()
|
||||
|
||||
mock_iptables.assert_any_call('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT')
|
||||
self.assertEqual({'foobar'}, firewall.BLACKLIST_CACHE)
|
||||
|
||||
def test_update_filters_infiniband(
|
||||
self, mock_call, mock_get_client, mock_iptables):
|
||||
|
||||
CONF.set_override('ethoib_interfaces', ['eth0'], 'firewall')
|
||||
active_macs = ['11:22:33:44:55:66', '66:55:44:33:22:11']
|
||||
expected_rmac = '02:00:00:61:00:02'
|
||||
ports = [mock.Mock(address=m) for m in active_macs]
|
||||
ports.append(mock.Mock(address='7c:fe:90:29:24:4f',
|
||||
extra={'client-id': self.CLIENT_ID},
|
||||
spec=['address', 'extra']))
|
||||
mock_get_client.port.list.return_value = ports
|
||||
node_cache.add_node(self.node.uuid, mac=active_macs,
|
||||
state=istate.States.finished,
|
||||
bmc_address='1.2.3.4', foo=None)
|
||||
firewall.init()
|
||||
|
||||
update_filters_expected_args = [
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-N', CONF.firewall.firewall_chain),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-F', firewall.NEW_CHAIN),
|
||||
('-X', firewall.NEW_CHAIN),
|
||||
('-N', firewall.NEW_CHAIN),
|
||||
# Blacklist
|
||||
('-A', firewall.NEW_CHAIN, '-m', 'mac', '--mac-source',
|
||||
expected_rmac, '-j', 'DROP'),
|
||||
('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT'),
|
||||
('-I', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-E', firewall.NEW_CHAIN, CONF.firewall.firewall_chain)
|
||||
]
|
||||
|
||||
fileobj = mock.mock_open(read_data=IB_DATA)
|
||||
with mock.patch('six.moves.builtins.open', fileobj, create=True):
|
||||
firewall.update_filters(mock_get_client)
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (args, call) in zip(update_filters_expected_args,
|
||||
call_args_list):
|
||||
self.assertEqual(args, call[0])
|
||||
|
||||
def test_update_filters_infiniband_no_such_file(
|
||||
self, mock_call, mock_get_client, mock_iptables):
|
||||
|
||||
CONF.set_override('ethoib_interfaces', ['eth0'], 'firewall')
|
||||
active_macs = ['11:22:33:44:55:66', '66:55:44:33:22:11']
|
||||
ports = [mock.Mock(address=m) for m in active_macs]
|
||||
ports.append(mock.Mock(address='7c:fe:90:29:24:4f',
|
||||
extra={'client-id': self.CLIENT_ID},
|
||||
spec=['address', 'extra']))
|
||||
mock_get_client.port.list.return_value = ports
|
||||
node_cache.add_node(self.node.uuid, mac=active_macs,
|
||||
state=istate.States.finished,
|
||||
bmc_address='1.2.3.4', foo=None)
|
||||
firewall.init()
|
||||
|
||||
update_filters_expected_args = [
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-N', CONF.firewall.firewall_chain),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-F', firewall.NEW_CHAIN),
|
||||
('-X', firewall.NEW_CHAIN),
|
||||
('-N', firewall.NEW_CHAIN),
|
||||
# Blacklist
|
||||
('-A', firewall.NEW_CHAIN, '-m', 'mac', '--mac-source',
|
||||
'7c:fe:90:29:24:4f', '-j', 'DROP'),
|
||||
('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT'),
|
||||
('-I', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', firewall.NEW_CHAIN),
|
||||
('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport',
|
||||
'67', '-j', CONF.firewall.firewall_chain),
|
||||
('-F', CONF.firewall.firewall_chain),
|
||||
('-X', CONF.firewall.firewall_chain),
|
||||
('-E', firewall.NEW_CHAIN, CONF.firewall.firewall_chain)
|
||||
]
|
||||
|
||||
with mock.patch('six.moves.builtins.open', side_effect=IOError()):
|
||||
firewall.update_filters(mock_get_client)
|
||||
call_args_list = mock_iptables.call_args_list
|
||||
|
||||
for (args, call) in zip(update_filters_expected_args,
|
||||
call_args_list):
|
||||
self.assertEqual(args, call[0])
|
@ -1,432 +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 time
|
||||
|
||||
from ironicclient import exceptions
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import firewall
|
||||
from ironic_inspector import introspect
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector.test import base as test_base
|
||||
from ironic_inspector import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class BaseTest(test_base.NodeTest):
|
||||
def setUp(self):
|
||||
super(BaseTest, self).setUp()
|
||||
introspect._LAST_INTROSPECTION_TIME = 0
|
||||
self.node.power_state = 'power off'
|
||||
self.ports = [mock.Mock(address=m) for m in self.macs]
|
||||
self.ports_dict = collections.OrderedDict((p.address, p)
|
||||
for p in self.ports)
|
||||
self.node_info = mock.Mock(uuid=self.uuid, options={})
|
||||
self.node_info.ports.return_value = self.ports_dict
|
||||
self.node_info.node.return_value = self.node
|
||||
|
||||
def _prepare(self, client_mock):
|
||||
cli = client_mock.return_value
|
||||
cli.node.get.return_value = self.node
|
||||
cli.node.validate.return_value = mock.Mock(power={'result': True})
|
||||
return cli
|
||||
|
||||
|
||||
@mock.patch.object(firewall, 'update_filters', autospec=True)
|
||||
@mock.patch.object(node_cache, 'start_introspection', autospec=True)
|
||||
@mock.patch.object(ir_utils, 'get_client', autospec=True)
|
||||
class TestIntrospect(BaseTest):
|
||||
def test_ok(self, client_mock, start_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
start_mock.return_value = self.node_info
|
||||
|
||||
introspect.introspect(self.node.uuid)
|
||||
|
||||
cli.node.get.assert_called_once_with(self.uuid)
|
||||
cli.node.validate.assert_called_once_with(self.uuid)
|
||||
|
||||
start_mock.assert_called_once_with(self.uuid,
|
||||
bmc_address=self.bmc_address,
|
||||
ironic=cli)
|
||||
self.node_info.ports.assert_called_once_with()
|
||||
self.node_info.add_attribute.assert_called_once_with('mac',
|
||||
self.macs)
|
||||
filters_mock.assert_called_with(cli)
|
||||
cli.node.set_boot_device.assert_called_once_with(self.uuid,
|
||||
'pxe',
|
||||
persistent=False)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
self.node_info.acquire_lock.assert_called_once_with()
|
||||
self.node_info.release_lock.assert_called_once_with()
|
||||
|
||||
def test_loopback_bmc_address(self, client_mock, start_mock, filters_mock):
|
||||
self.node.driver_info['ipmi_address'] = '127.0.0.1'
|
||||
cli = self._prepare(client_mock)
|
||||
start_mock.return_value = self.node_info
|
||||
|
||||
introspect.introspect(self.node.uuid)
|
||||
|
||||
cli.node.get.assert_called_once_with(self.uuid)
|
||||
cli.node.validate.assert_called_once_with(self.uuid)
|
||||
|
||||
start_mock.assert_called_once_with(self.uuid,
|
||||
bmc_address=None,
|
||||
ironic=cli)
|
||||
self.node_info.ports.assert_called_once_with()
|
||||
self.node_info.add_attribute.assert_called_once_with('mac',
|
||||
self.macs)
|
||||
filters_mock.assert_called_with(cli)
|
||||
cli.node.set_boot_device.assert_called_once_with(self.uuid,
|
||||
'pxe',
|
||||
persistent=False)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
self.node_info.acquire_lock.assert_called_once_with()
|
||||
self.node_info.release_lock.assert_called_once_with()
|
||||
|
||||
def test_ok_ilo_and_drac(self, client_mock, start_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
start_mock.return_value = self.node_info
|
||||
|
||||
for name in ('ilo_address', 'drac_host'):
|
||||
self.node.driver_info = {name: self.bmc_address}
|
||||
introspect.introspect(self.node.uuid)
|
||||
|
||||
start_mock.assert_called_with(self.uuid,
|
||||
bmc_address=self.bmc_address,
|
||||
ironic=cli)
|
||||
|
||||
def test_power_failure(self, client_mock, start_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
cli.node.set_boot_device.side_effect = exceptions.BadRequest()
|
||||
cli.node.set_power_state.side_effect = exceptions.BadRequest()
|
||||
start_mock.return_value = self.node_info
|
||||
|
||||
introspect.introspect(self.node.uuid)
|
||||
|
||||
cli.node.get.assert_called_once_with(self.uuid)
|
||||
|
||||
start_mock.assert_called_once_with(self.uuid,
|
||||
bmc_address=self.bmc_address,
|
||||
ironic=cli)
|
||||
cli.node.set_boot_device.assert_called_once_with(self.uuid,
|
||||
'pxe',
|
||||
persistent=False)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
start_mock.return_value.finished.assert_called_once_with(
|
||||
error=mock.ANY)
|
||||
self.node_info.acquire_lock.assert_called_once_with()
|
||||
self.node_info.release_lock.assert_called_once_with()
|
||||
|
||||
def test_unexpected_error(self, client_mock, start_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
start_mock.return_value = self.node_info
|
||||
filters_mock.side_effect = RuntimeError()
|
||||
|
||||
introspect.introspect(self.node.uuid)
|
||||
|
||||
cli.node.get.assert_called_once_with(self.uuid)
|
||||
|
||||
start_mock.assert_called_once_with(self.uuid,
|
||||
bmc_address=self.bmc_address,
|
||||
ironic=cli)
|
||||
self.assertFalse(cli.node.set_boot_device.called)
|
||||
start_mock.return_value.finished.assert_called_once_with(
|
||||
error=mock.ANY)
|
||||
self.node_info.acquire_lock.assert_called_once_with()
|
||||
self.node_info.release_lock.assert_called_once_with()
|
||||
|
||||
def test_no_macs(self, client_mock, start_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
self.node_info.ports.return_value = []
|
||||
start_mock.return_value = self.node_info
|
||||
|
||||
introspect.introspect(self.node.uuid)
|
||||
|
||||
self.node_info.ports.assert_called_once_with()
|
||||
|
||||
start_mock.assert_called_once_with(self.uuid,
|
||||
bmc_address=self.bmc_address,
|
||||
ironic=cli)
|
||||
self.assertFalse(self.node_info.add_attribute.called)
|
||||
self.assertFalse(filters_mock.called)
|
||||
cli.node.set_boot_device.assert_called_once_with(self.uuid,
|
||||
'pxe',
|
||||
persistent=False)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
|
||||
def test_no_lookup_attrs(self, client_mock, start_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
self.node_info.ports.return_value = []
|
||||
start_mock.return_value = self.node_info
|
||||
self.node_info.attributes = {}
|
||||
|
||||
introspect.introspect(self.uuid)
|
||||
|
||||
self.node_info.ports.assert_called_once_with()
|
||||
self.node_info.finished.assert_called_once_with(error=mock.ANY)
|
||||
self.assertEqual(0, filters_mock.call_count)
|
||||
self.assertEqual(0, cli.node.set_power_state.call_count)
|
||||
self.node_info.acquire_lock.assert_called_once_with()
|
||||
self.node_info.release_lock.assert_called_once_with()
|
||||
|
||||
def test_no_lookup_attrs_with_node_not_found_hook(self, client_mock,
|
||||
start_mock,
|
||||
filters_mock):
|
||||
CONF.set_override('node_not_found_hook', 'example', 'processing')
|
||||
cli = self._prepare(client_mock)
|
||||
self.node_info.ports.return_value = []
|
||||
start_mock.return_value = self.node_info
|
||||
self.node_info.attributes = {}
|
||||
|
||||
introspect.introspect(self.uuid)
|
||||
|
||||
self.node_info.ports.assert_called_once_with()
|
||||
self.assertFalse(self.node_info.finished.called)
|
||||
cli.node.set_boot_device.assert_called_once_with(self.uuid,
|
||||
'pxe',
|
||||
persistent=False)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
|
||||
def test_failed_to_get_node(self, client_mock, start_mock, filters_mock):
|
||||
cli = client_mock.return_value
|
||||
cli.node.get.side_effect = exceptions.NotFound()
|
||||
self.assertRaisesRegex(utils.Error,
|
||||
'Node %s was not found' % self.uuid,
|
||||
introspect.introspect, self.uuid)
|
||||
|
||||
cli.node.get.side_effect = exceptions.BadRequest()
|
||||
self.assertRaisesRegex(utils.Error,
|
||||
'%s: Bad Request' % self.uuid,
|
||||
introspect.introspect, self.uuid)
|
||||
|
||||
self.assertEqual(0, self.node_info.ports.call_count)
|
||||
self.assertEqual(0, filters_mock.call_count)
|
||||
self.assertEqual(0, cli.node.set_power_state.call_count)
|
||||
self.assertFalse(start_mock.called)
|
||||
self.assertFalse(self.node_info.acquire_lock.called)
|
||||
|
||||
def test_failed_to_validate_node(self, client_mock, start_mock,
|
||||
filters_mock):
|
||||
cli = client_mock.return_value
|
||||
cli.node.get.return_value = self.node
|
||||
cli.node.validate.return_value = mock.Mock(power={'result': False,
|
||||
'reason': 'oops'})
|
||||
|
||||
self.assertRaisesRegex(
|
||||
utils.Error,
|
||||
'Failed validation of power interface',
|
||||
introspect.introspect, self.uuid)
|
||||
|
||||
cli.node.validate.assert_called_once_with(self.uuid)
|
||||
self.assertEqual(0, self.node_info.ports.call_count)
|
||||
self.assertEqual(0, filters_mock.call_count)
|
||||
self.assertEqual(0, cli.node.set_power_state.call_count)
|
||||
self.assertFalse(start_mock.called)
|
||||
self.assertFalse(self.node_info.acquire_lock.called)
|
||||
|
||||
def test_wrong_provision_state(self, client_mock, start_mock,
|
||||
filters_mock):
|
||||
self.node.provision_state = 'active'
|
||||
cli = client_mock.return_value
|
||||
cli.node.get.return_value = self.node
|
||||
|
||||
self.assertRaisesRegex(
|
||||
utils.Error, 'Invalid provision state for introspection: "active"',
|
||||
introspect.introspect, self.uuid)
|
||||
|
||||
self.assertEqual(0, self.node_info.ports.call_count)
|
||||
self.assertEqual(0, filters_mock.call_count)
|
||||
self.assertEqual(0, cli.node.set_power_state.call_count)
|
||||
self.assertFalse(start_mock.called)
|
||||
self.assertFalse(self.node_info.acquire_lock.called)
|
||||
|
||||
@mock.patch.object(time, 'time')
|
||||
def test_introspection_delay(self, time_mock, client_mock,
|
||||
start_mock, filters_mock):
|
||||
time_mock.return_value = 42
|
||||
introspect._LAST_INTROSPECTION_TIME = 40
|
||||
CONF.set_override('introspection_delay', 10)
|
||||
|
||||
cli = self._prepare(client_mock)
|
||||
start_mock.return_value = self.node_info
|
||||
|
||||
introspect.introspect(self.uuid)
|
||||
|
||||
self.sleep_fixture.mock.assert_called_once_with(8)
|
||||
cli.node.set_boot_device.assert_called_once_with(self.uuid,
|
||||
'pxe',
|
||||
persistent=False)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
# updated to the current time.time()
|
||||
self.assertEqual(42, introspect._LAST_INTROSPECTION_TIME)
|
||||
|
||||
@mock.patch.object(time, 'time')
|
||||
def test_introspection_delay_not_needed(
|
||||
self, time_mock, client_mock,
|
||||
start_mock, filters_mock):
|
||||
|
||||
time_mock.return_value = 100
|
||||
introspect._LAST_INTROSPECTION_TIME = 40
|
||||
CONF.set_override('introspection_delay', 10)
|
||||
|
||||
cli = self._prepare(client_mock)
|
||||
start_mock.return_value = self.node_info
|
||||
|
||||
introspect.introspect(self.uuid)
|
||||
|
||||
self.sleep_fixture.mock().assert_not_called()
|
||||
cli.node.set_boot_device.assert_called_once_with(self.uuid,
|
||||
'pxe',
|
||||
persistent=False)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
# updated to the current time.time()
|
||||
self.assertEqual(100, introspect._LAST_INTROSPECTION_TIME)
|
||||
|
||||
@mock.patch.object(time, 'time')
|
||||
def test_introspection_delay_custom_drivers(
|
||||
self, time_mock, client_mock, start_mock, filters_mock):
|
||||
self.node.driver = 'foobar'
|
||||
time_mock.return_value = 42
|
||||
introspect._LAST_INTROSPECTION_TIME = 40
|
||||
CONF.set_override('introspection_delay', 10)
|
||||
CONF.set_override('introspection_delay_drivers', 'fo{1,2}b.r')
|
||||
|
||||
cli = self._prepare(client_mock)
|
||||
start_mock.return_value = self.node_info
|
||||
|
||||
introspect.introspect(self.uuid)
|
||||
|
||||
self.sleep_fixture.mock.assert_called_once_with(8)
|
||||
cli.node.set_boot_device.assert_called_once_with(self.uuid,
|
||||
'pxe',
|
||||
persistent=False)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
# updated to the current time.time()
|
||||
self.assertEqual(42, introspect._LAST_INTROSPECTION_TIME)
|
||||
|
||||
|
||||
@mock.patch.object(firewall, 'update_filters', autospec=True)
|
||||
@mock.patch.object(node_cache, 'get_node', autospec=True)
|
||||
@mock.patch.object(ir_utils, 'get_client', autospec=True)
|
||||
class TestAbort(BaseTest):
|
||||
def setUp(self):
|
||||
super(TestAbort, self).setUp()
|
||||
self.node_info.started_at = None
|
||||
self.node_info.finished_at = None
|
||||
|
||||
def test_ok(self, client_mock, get_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
get_mock.return_value = self.node_info
|
||||
self.node_info.acquire_lock.return_value = True
|
||||
self.node_info.started_at = time.time()
|
||||
self.node_info.finished_at = None
|
||||
|
||||
introspect.abort(self.node.uuid)
|
||||
|
||||
get_mock.assert_called_once_with(self.uuid, ironic=cli,
|
||||
locked=False)
|
||||
self.node_info.acquire_lock.assert_called_once_with(blocking=False)
|
||||
filters_mock.assert_called_once_with(cli)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid, 'off')
|
||||
self.node_info.finished.assert_called_once_with(error='Canceled '
|
||||
'by operator')
|
||||
|
||||
def test_node_not_found(self, client_mock, get_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
exc = utils.Error('Not found.', code=404)
|
||||
get_mock.side_effect = exc
|
||||
|
||||
self.assertRaisesRegex(utils.Error, str(exc),
|
||||
introspect.abort, self.uuid)
|
||||
|
||||
self.assertEqual(0, filters_mock.call_count)
|
||||
self.assertEqual(0, cli.node.set_power_state.call_count)
|
||||
self.assertEqual(0, self.node_info.finished.call_count)
|
||||
|
||||
def test_node_locked(self, client_mock, get_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
get_mock.return_value = self.node_info
|
||||
self.node_info.acquire_lock.return_value = False
|
||||
self.node_info.started_at = time.time()
|
||||
|
||||
self.assertRaisesRegex(utils.Error, 'Node is locked, please, '
|
||||
'retry later', introspect.abort, self.uuid)
|
||||
|
||||
self.assertEqual(0, filters_mock.call_count)
|
||||
self.assertEqual(0, cli.node.set_power_state.call_count)
|
||||
self.assertEqual(0, self.node_info.finshed.call_count)
|
||||
|
||||
def test_introspection_already_finished(self, client_mock,
|
||||
get_mock, filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
get_mock.return_value = self.node_info
|
||||
self.node_info.acquire_lock.return_value = True
|
||||
self.node_info.started_at = time.time()
|
||||
self.node_info.finished_at = time.time()
|
||||
|
||||
introspect.abort(self.uuid)
|
||||
|
||||
self.assertEqual(0, filters_mock.call_count)
|
||||
self.assertEqual(0, cli.node.set_power_state.call_count)
|
||||
self.assertEqual(0, self.node_info.finshed.call_count)
|
||||
|
||||
def test_firewall_update_exception(self, client_mock, get_mock,
|
||||
filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
get_mock.return_value = self.node_info
|
||||
self.node_info.acquire_lock.return_value = True
|
||||
self.node_info.started_at = time.time()
|
||||
self.node_info.finished_at = None
|
||||
filters_mock.side_effect = Exception('Boom')
|
||||
|
||||
introspect.abort(self.uuid)
|
||||
|
||||
get_mock.assert_called_once_with(self.uuid, ironic=cli,
|
||||
locked=False)
|
||||
self.node_info.acquire_lock.assert_called_once_with(blocking=False)
|
||||
filters_mock.assert_called_once_with(cli)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid, 'off')
|
||||
self.node_info.finished.assert_called_once_with(error='Canceled '
|
||||
'by operator')
|
||||
|
||||
def test_node_power_off_exception(self, client_mock, get_mock,
|
||||
filters_mock):
|
||||
cli = self._prepare(client_mock)
|
||||
get_mock.return_value = self.node_info
|
||||
self.node_info.acquire_lock.return_value = True
|
||||
self.node_info.started_at = time.time()
|
||||
self.node_info.finished_at = None
|
||||
cli.node.set_power_state.side_effect = Exception('BadaBoom')
|
||||
|
||||
introspect.abort(self.uuid)
|
||||
|
||||
get_mock.assert_called_once_with(self.uuid, ironic=cli,
|
||||
locked=False)
|
||||
self.node_info.acquire_lock.assert_called_once_with(blocking=False)
|
||||
filters_mock.assert_called_once_with(cli)
|
||||
cli.node.set_power_state.assert_called_once_with(self.uuid, 'off')
|
||||
self.node_info.finished.assert_called_once_with(error='Canceled '
|
||||
'by operator')
|
@ -1,62 +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 mock
|
||||
|
||||
from keystoneauth1 import loading as kaloading
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common import keystone
|
||||
from ironic_inspector.test import base
|
||||
|
||||
|
||||
TESTGROUP = 'keystone_test'
|
||||
|
||||
|
||||
class KeystoneTest(base.BaseTest):
|
||||
|
||||
def setUp(self):
|
||||
super(KeystoneTest, self).setUp()
|
||||
self.cfg.conf.register_group(cfg.OptGroup(TESTGROUP))
|
||||
|
||||
def test_register_auth_opts(self):
|
||||
keystone.register_auth_opts(TESTGROUP)
|
||||
auth_opts = ['auth_type', 'auth_section']
|
||||
sess_opts = ['certfile', 'keyfile', 'insecure', 'timeout', 'cafile']
|
||||
for o in auth_opts + sess_opts:
|
||||
self.assertIn(o, self.cfg.conf[TESTGROUP])
|
||||
self.assertEqual('password', self.cfg.conf[TESTGROUP]['auth_type'])
|
||||
|
||||
@mock.patch.object(kaloading, 'load_auth_from_conf_options', autospec=True)
|
||||
def test_get_session(self, auth_mock):
|
||||
keystone.register_auth_opts(TESTGROUP)
|
||||
self.cfg.config(group=TESTGROUP,
|
||||
cafile='/path/to/ca/file')
|
||||
auth1 = mock.Mock()
|
||||
auth_mock.return_value = auth1
|
||||
sess = keystone.get_session(TESTGROUP)
|
||||
self.assertEqual('/path/to/ca/file', sess.verify)
|
||||
self.assertEqual(auth1, sess.auth)
|
||||
|
||||
def test_add_auth_options(self):
|
||||
group, opts = keystone.add_auth_options([], TESTGROUP)[0]
|
||||
self.assertEqual(TESTGROUP, group)
|
||||
# check that there is no duplicates
|
||||
names = {o.dest for o in opts}
|
||||
self.assertEqual(len(names), len(opts))
|
||||
# NOTE(pas-ha) checking for most standard auth and session ones only
|
||||
expected = {'timeout', 'insecure', 'cafile', 'certfile', 'keyfile',
|
||||
'auth_type', 'auth_url', 'username', 'password',
|
||||
'tenant_name', 'project_name', 'trust_id',
|
||||
'domain_id', 'user_domain_id', 'project_domain_id'}
|
||||
self.assertTrue(expected.issubset(names))
|
@ -1,615 +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 datetime
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import conf
|
||||
from ironic_inspector import introspect
|
||||
from ironic_inspector import introspection_state as istate
|
||||
from ironic_inspector import main
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector.plugins import base as plugins_base
|
||||
from ironic_inspector.plugins import example as example_plugin
|
||||
from ironic_inspector import process
|
||||
from ironic_inspector import rules
|
||||
from ironic_inspector.test import base as test_base
|
||||
from ironic_inspector import utils
|
||||
from oslo_config import cfg
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def _get_error(res):
|
||||
return json.loads(res.data.decode('utf-8'))['error']['message']
|
||||
|
||||
|
||||
class BaseAPITest(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(BaseAPITest, self).setUp()
|
||||
main.app.config['TESTING'] = True
|
||||
self.app = main.app.test_client()
|
||||
CONF.set_override('auth_strategy', 'noauth')
|
||||
self.uuid = uuidutils.generate_uuid()
|
||||
|
||||
|
||||
class TestApiIntrospect(BaseAPITest):
|
||||
@mock.patch.object(introspect, 'introspect', autospec=True)
|
||||
def test_introspect_no_authentication(self, introspect_mock):
|
||||
CONF.set_override('auth_strategy', 'noauth')
|
||||
res = self.app.post('/v1/introspection/%s' % self.uuid)
|
||||
self.assertEqual(202, res.status_code)
|
||||
introspect_mock.assert_called_once_with(self.uuid,
|
||||
token=None)
|
||||
|
||||
@mock.patch.object(introspect, 'introspect', autospec=True)
|
||||
def test_intospect_failed(self, introspect_mock):
|
||||
introspect_mock.side_effect = utils.Error("boom")
|
||||
res = self.app.post('/v1/introspection/%s' % self.uuid)
|
||||
self.assertEqual(400, res.status_code)
|
||||
self.assertEqual(
|
||||
'boom',
|
||||
json.loads(res.data.decode('utf-8'))['error']['message'])
|
||||
introspect_mock.assert_called_once_with(
|
||||
self.uuid,
|
||||
token=None)
|
||||
|
||||
@mock.patch.object(utils, 'check_auth', autospec=True)
|
||||
@mock.patch.object(introspect, 'introspect', autospec=True)
|
||||
def test_introspect_failed_authentication(self, introspect_mock,
|
||||
auth_mock):
|
||||
CONF.set_override('auth_strategy', 'keystone')
|
||||
auth_mock.side_effect = utils.Error('Boom', code=403)
|
||||
res = self.app.post('/v1/introspection/%s' % self.uuid,
|
||||
headers={'X-Auth-Token': 'token'})
|
||||
self.assertEqual(403, res.status_code)
|
||||
self.assertFalse(introspect_mock.called)
|
||||
|
||||
|
||||
@mock.patch.object(process, 'process', autospec=True)
|
||||
class TestApiContinue(BaseAPITest):
|
||||
def test_continue(self, process_mock):
|
||||
# should be ignored
|
||||
CONF.set_override('auth_strategy', 'keystone')
|
||||
process_mock.return_value = {'result': 42}
|
||||
res = self.app.post('/v1/continue', data='{"foo": "bar"}')
|
||||
self.assertEqual(200, res.status_code)
|
||||
process_mock.assert_called_once_with({"foo": "bar"})
|
||||
self.assertEqual({"result": 42}, json.loads(res.data.decode()))
|
||||
|
||||
def test_continue_failed(self, process_mock):
|
||||
process_mock.side_effect = utils.Error("boom")
|
||||
res = self.app.post('/v1/continue', data='{"foo": "bar"}')
|
||||
self.assertEqual(400, res.status_code)
|
||||
process_mock.assert_called_once_with({"foo": "bar"})
|
||||
self.assertEqual('boom', _get_error(res))
|
||||
|
||||
def test_continue_wrong_type(self, process_mock):
|
||||
res = self.app.post('/v1/continue', data='42')
|
||||
self.assertEqual(400, res.status_code)
|
||||
self.assertEqual('Invalid data: expected a JSON object, got int',
|
||||
_get_error(res))
|
||||
self.assertFalse(process_mock.called)
|
||||
|
||||
|
||||
@mock.patch.object(introspect, 'abort', autospec=True)
|
||||
class TestApiAbort(BaseAPITest):
|
||||
def test_ok(self, abort_mock):
|
||||
abort_mock.return_value = '', 202
|
||||
|
||||
res = self.app.post('/v1/introspection/%s/abort' % self.uuid,
|
||||
headers={'X-Auth-Token': 'token'})
|
||||
|
||||
abort_mock.assert_called_once_with(self.uuid, token='token')
|
||||
self.assertEqual(202, res.status_code)
|
||||
self.assertEqual(b'', res.data)
|
||||
|
||||
def test_no_authentication(self, abort_mock):
|
||||
abort_mock.return_value = b'', 202
|
||||
|
||||
res = self.app.post('/v1/introspection/%s/abort' % self.uuid)
|
||||
|
||||
abort_mock.assert_called_once_with(self.uuid, token=None)
|
||||
self.assertEqual(202, res.status_code)
|
||||
self.assertEqual(b'', res.data)
|
||||
|
||||
def test_node_not_found(self, abort_mock):
|
||||
exc = utils.Error("Not Found.", code=404)
|
||||
abort_mock.side_effect = exc
|
||||
|
||||
res = self.app.post('/v1/introspection/%s/abort' % self.uuid)
|
||||
|
||||
abort_mock.assert_called_once_with(self.uuid, token=None)
|
||||
self.assertEqual(404, res.status_code)
|
||||
data = json.loads(str(res.data.decode()))
|
||||
self.assertEqual(str(exc), data['error']['message'])
|
||||
|
||||
def test_abort_failed(self, abort_mock):
|
||||
exc = utils.Error("Locked.", code=409)
|
||||
abort_mock.side_effect = exc
|
||||
|
||||
res = self.app.post('/v1/introspection/%s/abort' % self.uuid)
|
||||
|
||||
abort_mock.assert_called_once_with(self.uuid, token=None)
|
||||
self.assertEqual(409, res.status_code)
|
||||
data = json.loads(res.data.decode())
|
||||
self.assertEqual(str(exc), data['error']['message'])
|
||||
|
||||
|
||||
class GetStatusAPIBaseTest(BaseAPITest):
|
||||
def setUp(self):
|
||||
super(GetStatusAPIBaseTest, self).setUp()
|
||||
self.uuid2 = uuidutils.generate_uuid()
|
||||
self.finished_node = node_cache.NodeInfo(
|
||||
uuid=self.uuid,
|
||||
started_at=datetime.datetime(1, 1, 1),
|
||||
finished_at=datetime.datetime(1, 1, 2),
|
||||
error='boom',
|
||||
state=istate.States.error)
|
||||
self.finished_node.links = [
|
||||
{u'href': u'http://localhost/v1/introspection/%s' %
|
||||
self.finished_node.uuid,
|
||||
u'rel': u'self'},
|
||||
]
|
||||
self.finished_node.status = {
|
||||
'finished': True,
|
||||
'state': self.finished_node._state,
|
||||
'started_at': self.finished_node.started_at.isoformat(),
|
||||
'finished_at': self.finished_node.finished_at.isoformat(),
|
||||
'error': self.finished_node.error,
|
||||
'uuid': self.finished_node.uuid,
|
||||
'links': self.finished_node.links
|
||||
}
|
||||
|
||||
self.unfinished_node = node_cache.NodeInfo(
|
||||
uuid=self.uuid2,
|
||||
started_at=datetime.datetime(1, 1, 1),
|
||||
state=istate.States.processing)
|
||||
self.unfinished_node.links = [
|
||||
{u'href': u'http://localhost/v1/introspection/%s' %
|
||||
self.unfinished_node.uuid,
|
||||
u'rel': u'self'}
|
||||
]
|
||||
finished_at = (self.unfinished_node.finished_at.isoformat()
|
||||
if self.unfinished_node.finished_at else None)
|
||||
self.unfinished_node.status = {
|
||||
'finished': False,
|
||||
'state': self.unfinished_node._state,
|
||||
'started_at': self.unfinished_node.started_at.isoformat(),
|
||||
'finished_at': finished_at,
|
||||
'error': None,
|
||||
'uuid': self.unfinished_node.uuid,
|
||||
'links': self.unfinished_node.links
|
||||
}
|
||||
|
||||
|
||||
@mock.patch.object(node_cache, 'get_node', autospec=True)
|
||||
class TestApiGetStatus(GetStatusAPIBaseTest):
|
||||
def test_get_introspection_in_progress(self, get_mock):
|
||||
get_mock.return_value = self.unfinished_node
|
||||
res = self.app.get('/v1/introspection/%s' % self.uuid)
|
||||
self.assertEqual(200, res.status_code)
|
||||
self.assertEqual(self.unfinished_node.status,
|
||||
json.loads(res.data.decode('utf-8')))
|
||||
|
||||
def test_get_introspection_finished(self, get_mock):
|
||||
get_mock.return_value = self.finished_node
|
||||
res = self.app.get('/v1/introspection/%s' % self.uuid)
|
||||
self.assertEqual(200, res.status_code)
|
||||
self.assertEqual(self.finished_node.status,
|
||||
json.loads(res.data.decode('utf-8')))
|
||||
|
||||
|
||||
@mock.patch.object(node_cache, 'get_node_list', autospec=True)
|
||||
class TestApiListStatus(GetStatusAPIBaseTest):
|
||||
|
||||
def test_list_introspection(self, list_mock):
|
||||
list_mock.return_value = [self.finished_node, self.unfinished_node]
|
||||
res = self.app.get('/v1/introspection')
|
||||
self.assertEqual(200, res.status_code)
|
||||
statuses = json.loads(res.data.decode('utf-8')).get('introspection')
|
||||
|
||||
self.assertEqual([self.finished_node.status,
|
||||
self.unfinished_node.status], statuses)
|
||||
list_mock.assert_called_once_with(marker=None,
|
||||
limit=CONF.api_max_limit)
|
||||
|
||||
def test_list_introspection_limit(self, list_mock):
|
||||
res = self.app.get('/v1/introspection?limit=1000')
|
||||
self.assertEqual(200, res.status_code)
|
||||
list_mock.assert_called_once_with(marker=None, limit=1000)
|
||||
|
||||
def test_list_introspection_makrer(self, list_mock):
|
||||
res = self.app.get('/v1/introspection?marker=%s' %
|
||||
self.finished_node.uuid)
|
||||
self.assertEqual(200, res.status_code)
|
||||
list_mock.assert_called_once_with(marker=self.finished_node.uuid,
|
||||
limit=CONF.api_max_limit)
|
||||
|
||||
|
||||
class TestApiGetData(BaseAPITest):
|
||||
@mock.patch.object(main.swift, 'SwiftAPI', autospec=True)
|
||||
def test_get_introspection_data(self, swift_mock):
|
||||
CONF.set_override('store_data', 'swift', 'processing')
|
||||
data = {
|
||||
'ipmi_address': '1.2.3.4',
|
||||
'cpus': 2,
|
||||
'cpu_arch': 'x86_64',
|
||||
'memory_mb': 1024,
|
||||
'local_gb': 20,
|
||||
'interfaces': {
|
||||
'em1': {'mac': '11:22:33:44:55:66', 'ip': '1.2.0.1'},
|
||||
}
|
||||
}
|
||||
swift_conn = swift_mock.return_value
|
||||
swift_conn.get_object.return_value = json.dumps(data)
|
||||
res = self.app.get('/v1/introspection/%s/data' % self.uuid)
|
||||
name = 'inspector_data-%s' % self.uuid
|
||||
swift_conn.get_object.assert_called_once_with(name)
|
||||
self.assertEqual(200, res.status_code)
|
||||
self.assertEqual(data, json.loads(res.data.decode('utf-8')))
|
||||
|
||||
@mock.patch.object(main.swift, 'SwiftAPI', autospec=True)
|
||||
def test_introspection_data_not_stored(self, swift_mock):
|
||||
CONF.set_override('store_data', 'none', 'processing')
|
||||
swift_conn = swift_mock.return_value
|
||||
res = self.app.get('/v1/introspection/%s/data' % self.uuid)
|
||||
self.assertFalse(swift_conn.get_object.called)
|
||||
self.assertEqual(404, res.status_code)
|
||||
|
||||
@mock.patch.object(ir_utils, 'get_node', autospec=True)
|
||||
@mock.patch.object(main.swift, 'SwiftAPI', autospec=True)
|
||||
def test_with_name(self, swift_mock, get_mock):
|
||||
get_mock.return_value = mock.Mock(uuid=self.uuid)
|
||||
CONF.set_override('store_data', 'swift', 'processing')
|
||||
data = {
|
||||
'ipmi_address': '1.2.3.4',
|
||||
'cpus': 2,
|
||||
'cpu_arch': 'x86_64',
|
||||
'memory_mb': 1024,
|
||||
'local_gb': 20,
|
||||
'interfaces': {
|
||||
'em1': {'mac': '11:22:33:44:55:66', 'ip': '1.2.0.1'},
|
||||
}
|
||||
}
|
||||
swift_conn = swift_mock.return_value
|
||||
swift_conn.get_object.return_value = json.dumps(data)
|
||||
res = self.app.get('/v1/introspection/name1/data')
|
||||
name = 'inspector_data-%s' % self.uuid
|
||||
swift_conn.get_object.assert_called_once_with(name)
|
||||
self.assertEqual(200, res.status_code)
|
||||
self.assertEqual(data, json.loads(res.data.decode('utf-8')))
|
||||
get_mock.assert_called_once_with('name1', fields=['uuid'])
|
||||
|
||||
|
||||
@mock.patch.object(process, 'reapply', autospec=True)
|
||||
class TestApiReapply(BaseAPITest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestApiReapply, self).setUp()
|
||||
CONF.set_override('store_data', 'swift', 'processing')
|
||||
|
||||
def test_ok(self, reapply_mock):
|
||||
|
||||
self.app.post('/v1/introspection/%s/data/unprocessed' %
|
||||
self.uuid)
|
||||
reapply_mock.assert_called_once_with(self.uuid)
|
||||
|
||||
def test_user_data(self, reapply_mock):
|
||||
res = self.app.post('/v1/introspection/%s/data/unprocessed' %
|
||||
self.uuid, data='some data')
|
||||
self.assertEqual(400, res.status_code)
|
||||
message = json.loads(res.data.decode())['error']['message']
|
||||
self.assertEqual('User data processing is not supported yet',
|
||||
message)
|
||||
self.assertFalse(reapply_mock.called)
|
||||
|
||||
def test_swift_disabled(self, reapply_mock):
|
||||
CONF.set_override('store_data', 'none', 'processing')
|
||||
|
||||
res = self.app.post('/v1/introspection/%s/data/unprocessed' %
|
||||
self.uuid)
|
||||
self.assertEqual(400, res.status_code)
|
||||
message = json.loads(res.data.decode())['error']['message']
|
||||
self.assertEqual('Inspector is not configured to store '
|
||||
'data. Set the [processing] store_data '
|
||||
'configuration option to change this.',
|
||||
message)
|
||||
self.assertFalse(reapply_mock.called)
|
||||
|
||||
def test_node_locked(self, reapply_mock):
|
||||
exc = utils.Error('Locked.', code=409)
|
||||
reapply_mock.side_effect = exc
|
||||
|
||||
res = self.app.post('/v1/introspection/%s/data/unprocessed' %
|
||||
self.uuid)
|
||||
|
||||
self.assertEqual(409, res.status_code)
|
||||
message = json.loads(res.data.decode())['error']['message']
|
||||
self.assertEqual(str(exc), message)
|
||||
reapply_mock.assert_called_once_with(self.uuid)
|
||||
|
||||
def test_node_not_found(self, reapply_mock):
|
||||
exc = utils.Error('Not found.', code=404)
|
||||
reapply_mock.side_effect = exc
|
||||
|
||||
res = self.app.post('/v1/introspection/%s/data/unprocessed' %
|
||||
self.uuid)
|
||||
|
||||
self.assertEqual(404, res.status_code)
|
||||
message = json.loads(res.data.decode())['error']['message']
|
||||
self.assertEqual(str(exc), message)
|
||||
reapply_mock.assert_called_once_with(self.uuid)
|
||||
|
||||
def test_generic_error(self, reapply_mock):
|
||||
exc = utils.Error('Oops', code=400)
|
||||
reapply_mock.side_effect = exc
|
||||
|
||||
res = self.app.post('/v1/introspection/%s/data/unprocessed' %
|
||||
self.uuid)
|
||||
|
||||
self.assertEqual(400, res.status_code)
|
||||
message = json.loads(res.data.decode())['error']['message']
|
||||
self.assertEqual(str(exc), message)
|
||||
reapply_mock.assert_called_once_with(self.uuid)
|
||||
|
||||
|
||||
class TestApiRules(BaseAPITest):
|
||||
@mock.patch.object(rules, 'get_all')
|
||||
def test_get_all(self, get_all_mock):
|
||||
get_all_mock.return_value = [
|
||||
mock.Mock(spec=rules.IntrospectionRule,
|
||||
**{'as_dict.return_value': {'uuid': 'foo'}}),
|
||||
mock.Mock(spec=rules.IntrospectionRule,
|
||||
**{'as_dict.return_value': {'uuid': 'bar'}}),
|
||||
]
|
||||
|
||||
res = self.app.get('/v1/rules')
|
||||
self.assertEqual(200, res.status_code)
|
||||
self.assertEqual(
|
||||
{
|
||||
'rules': [{'uuid': 'foo',
|
||||
'links': [
|
||||
{'href': '/v1/rules/foo', 'rel': 'self'}
|
||||
]},
|
||||
{'uuid': 'bar',
|
||||
'links': [
|
||||
{'href': '/v1/rules/bar', 'rel': 'self'}
|
||||
]}]
|
||||
},
|
||||
json.loads(res.data.decode('utf-8')))
|
||||
get_all_mock.assert_called_once_with()
|
||||
for m in get_all_mock.return_value:
|
||||
m.as_dict.assert_called_with(short=True)
|
||||
|
||||
@mock.patch.object(rules, 'delete_all')
|
||||
def test_delete_all(self, delete_all_mock):
|
||||
res = self.app.delete('/v1/rules')
|
||||
self.assertEqual(204, res.status_code)
|
||||
delete_all_mock.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(rules, 'create', autospec=True)
|
||||
def test_create(self, create_mock):
|
||||
data = {'uuid': self.uuid,
|
||||
'conditions': 'cond',
|
||||
'actions': 'act'}
|
||||
exp = data.copy()
|
||||
exp['description'] = None
|
||||
create_mock.return_value = mock.Mock(spec=rules.IntrospectionRule,
|
||||
**{'as_dict.return_value': exp})
|
||||
|
||||
res = self.app.post('/v1/rules', data=json.dumps(data))
|
||||
self.assertEqual(201, res.status_code)
|
||||
create_mock.assert_called_once_with(conditions_json='cond',
|
||||
actions_json='act',
|
||||
uuid=self.uuid,
|
||||
description=None)
|
||||
self.assertEqual(exp, json.loads(res.data.decode('utf-8')))
|
||||
|
||||
@mock.patch.object(rules, 'create', autospec=True)
|
||||
def test_create_api_less_1_6(self, create_mock):
|
||||
data = {'uuid': self.uuid,
|
||||
'conditions': 'cond',
|
||||
'actions': 'act'}
|
||||
exp = data.copy()
|
||||
exp['description'] = None
|
||||
create_mock.return_value = mock.Mock(spec=rules.IntrospectionRule,
|
||||
**{'as_dict.return_value': exp})
|
||||
|
||||
headers = {conf.VERSION_HEADER:
|
||||
main._format_version((1, 5))}
|
||||
|
||||
res = self.app.post('/v1/rules', data=json.dumps(data),
|
||||
headers=headers)
|
||||
self.assertEqual(200, res.status_code)
|
||||
create_mock.assert_called_once_with(conditions_json='cond',
|
||||
actions_json='act',
|
||||
uuid=self.uuid,
|
||||
description=None)
|
||||
self.assertEqual(exp, json.loads(res.data.decode('utf-8')))
|
||||
|
||||
@mock.patch.object(rules, 'create', autospec=True)
|
||||
def test_create_bad_uuid(self, create_mock):
|
||||
data = {'uuid': 'foo',
|
||||
'conditions': 'cond',
|
||||
'actions': 'act'}
|
||||
|
||||
res = self.app.post('/v1/rules', data=json.dumps(data))
|
||||
self.assertEqual(400, res.status_code)
|
||||
|
||||
@mock.patch.object(rules, 'get')
|
||||
def test_get_one(self, get_mock):
|
||||
get_mock.return_value = mock.Mock(spec=rules.IntrospectionRule,
|
||||
**{'as_dict.return_value':
|
||||
{'uuid': 'foo'}})
|
||||
|
||||
res = self.app.get('/v1/rules/' + self.uuid)
|
||||
self.assertEqual(200, res.status_code)
|
||||
self.assertEqual({'uuid': 'foo',
|
||||
'links': [
|
||||
{'href': '/v1/rules/foo', 'rel': 'self'}
|
||||
]},
|
||||
json.loads(res.data.decode('utf-8')))
|
||||
get_mock.assert_called_once_with(self.uuid)
|
||||
get_mock.return_value.as_dict.assert_called_once_with(short=False)
|
||||
|
||||
@mock.patch.object(rules, 'delete')
|
||||
def test_delete_one(self, delete_mock):
|
||||
res = self.app.delete('/v1/rules/' + self.uuid)
|
||||
self.assertEqual(204, res.status_code)
|
||||
delete_mock.assert_called_once_with(self.uuid)
|
||||
|
||||
|
||||
class TestApiMisc(BaseAPITest):
|
||||
@mock.patch.object(node_cache, 'get_node', autospec=True)
|
||||
def test_404_expected(self, get_mock):
|
||||
get_mock.side_effect = utils.Error('boom', code=404)
|
||||
res = self.app.get('/v1/introspection/%s' % self.uuid)
|
||||
self.assertEqual(404, res.status_code)
|
||||
self.assertEqual('boom', _get_error(res))
|
||||
|
||||
def test_404_unexpected(self):
|
||||
res = self.app.get('/v42')
|
||||
self.assertEqual(404, res.status_code)
|
||||
self.assertIn('not found', _get_error(res).lower())
|
||||
|
||||
@mock.patch.object(node_cache, 'get_node', autospec=True)
|
||||
def test_500_with_debug(self, get_mock):
|
||||
CONF.set_override('debug', True)
|
||||
get_mock.side_effect = RuntimeError('boom')
|
||||
res = self.app.get('/v1/introspection/%s' % self.uuid)
|
||||
self.assertEqual(500, res.status_code)
|
||||
self.assertEqual('Internal server error (RuntimeError): boom',
|
||||
_get_error(res))
|
||||
|
||||
@mock.patch.object(node_cache, 'get_node', autospec=True)
|
||||
def test_500_without_debug(self, get_mock):
|
||||
CONF.set_override('debug', False)
|
||||
get_mock.side_effect = RuntimeError('boom')
|
||||
res = self.app.get('/v1/introspection/%s' % self.uuid)
|
||||
self.assertEqual(500, res.status_code)
|
||||
self.assertEqual('Internal server error',
|
||||
_get_error(res))
|
||||
|
||||
|
||||
class TestApiVersions(BaseAPITest):
|
||||
def _check_version_present(self, res):
|
||||
self.assertEqual('%d.%d' % main.MINIMUM_API_VERSION,
|
||||
res.headers.get(conf.MIN_VERSION_HEADER))
|
||||
self.assertEqual('%d.%d' % main.CURRENT_API_VERSION,
|
||||
res.headers.get(conf.MAX_VERSION_HEADER))
|
||||
|
||||
def test_root_endpoint(self):
|
||||
res = self.app.get("/")
|
||||
self.assertEqual(200, res.status_code)
|
||||
self._check_version_present(res)
|
||||
data = res.data.decode('utf-8')
|
||||
json_data = json.loads(data)
|
||||
expected = {"versions": [{
|
||||
"status": "CURRENT", "id": '%s.%s' % main.CURRENT_API_VERSION,
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": ("http://localhost/v%s" %
|
||||
main.CURRENT_API_VERSION[0])
|
||||
}]
|
||||
}]}
|
||||
self.assertEqual(expected, json_data)
|
||||
|
||||
@mock.patch.object(main.app.url_map, "iter_rules", autospec=True)
|
||||
def test_version_endpoint(self, mock_rules):
|
||||
mock_rules.return_value = ["/v1/endpoint1", "/v1/endpoint2/<uuid>",
|
||||
"/v1/endpoint1/<name>",
|
||||
"/v2/endpoint1", "/v1/endpoint3",
|
||||
"/v1/endpoint2/<uuid>/subpoint"]
|
||||
endpoint = "/v1"
|
||||
res = self.app.get(endpoint)
|
||||
self.assertEqual(200, res.status_code)
|
||||
self._check_version_present(res)
|
||||
json_data = json.loads(res.data.decode('utf-8'))
|
||||
expected = {u'resources': [
|
||||
{
|
||||
u'name': u'endpoint1',
|
||||
u'links': [{
|
||||
u'rel': u'self',
|
||||
u'href': u'http://localhost/v1/endpoint1'}]
|
||||
},
|
||||
{
|
||||
u'name': u'endpoint3',
|
||||
u'links': [{
|
||||
u'rel': u'self',
|
||||
u'href': u'http://localhost/v1/endpoint3'}]
|
||||
},
|
||||
]}
|
||||
self.assertEqual(expected, json_data)
|
||||
|
||||
def test_version_endpoint_invalid(self):
|
||||
endpoint = "/v-1"
|
||||
res = self.app.get(endpoint)
|
||||
self.assertEqual(404, res.status_code)
|
||||
|
||||
def test_404_unexpected(self):
|
||||
# API version on unknown pages
|
||||
self._check_version_present(self.app.get('/v1/foobar'))
|
||||
|
||||
@mock.patch.object(node_cache, 'get_node', autospec=True)
|
||||
def test_usual_requests(self, get_mock):
|
||||
get_mock.return_value = node_cache.NodeInfo(uuid=self.uuid,
|
||||
started_at=42.0)
|
||||
# Successfull
|
||||
self._check_version_present(
|
||||
self.app.post('/v1/introspection/%s' % self.uuid))
|
||||
# With error
|
||||
self._check_version_present(
|
||||
self.app.post('/v1/introspection/foobar'))
|
||||
|
||||
def test_request_correct_version(self):
|
||||
headers = {conf.VERSION_HEADER:
|
||||
main._format_version(main.CURRENT_API_VERSION)}
|
||||
self._check_version_present(self.app.get('/', headers=headers))
|
||||
|
||||
def test_request_unsupported_version(self):
|
||||
bad_version = (main.CURRENT_API_VERSION[0],
|
||||
main.CURRENT_API_VERSION[1] + 1)
|
||||
headers = {conf.VERSION_HEADER:
|
||||
main._format_version(bad_version)}
|
||||
res = self.app.get('/', headers=headers)
|
||||
self._check_version_present(res)
|
||||
self.assertEqual(406, res.status_code)
|
||||
error = _get_error(res)
|
||||
self.assertIn('%d.%d' % bad_version, error)
|
||||
self.assertIn('%d.%d' % main.MINIMUM_API_VERSION, error)
|
||||
self.assertIn('%d.%d' % main.CURRENT_API_VERSION, error)
|
||||
|
||||
|
||||
class TestPlugins(unittest.TestCase):
|
||||
@mock.patch.object(example_plugin.ExampleProcessingHook,
|
||||
'before_processing', autospec=True)
|
||||
@mock.patch.object(example_plugin.ExampleProcessingHook,
|
||||
'before_update', autospec=True)
|
||||
def test_hook(self, mock_post, mock_pre):
|
||||
plugins_base._HOOKS_MGR = None
|
||||
CONF.set_override('processing_hooks', 'example', 'processing')
|
||||
mgr = plugins_base.processing_hooks_manager()
|
||||
mgr.map_method('before_processing', 'introspection_data')
|
||||
mock_pre.assert_called_once_with(mock.ANY, 'introspection_data')
|
||||
mgr.map_method('before_update', 'node_info', {})
|
||||
mock_post.assert_called_once_with(mock.ANY, 'node_info', {})
|
||||
|
||||
def test_manager_is_cached(self):
|
||||
self.assertIs(plugins_base.processing_hooks_manager(),
|
||||
plugins_base.processing_hooks_manager())
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user