diff --git a/.coveragerc b/.coveragerc
index f45d51a87..ba45bdd9a 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,3 +1,7 @@
+[run]
+branch = True
+source = ironicclient
+omit = ironicclient/tests/*
+
[report]
-include = ironicclient/*
-omit = ironicclient/tests/functional/*
+ignore_errors = True
diff --git a/.gitignore b/.gitignore
index 6a6b9a94f..ffb6c75e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@
# Sphinx
_build
-doc/source/api/
+doc/source/reference/api/
# Release notes
releasenotes/build
@@ -16,6 +16,7 @@ releasenotes/build
*.egg-info
dist
build
+.eggs
eggs
parts
var
@@ -25,7 +26,7 @@ develop-eggs
# Other
*.DS_Store
-.testrepository
+.stestr
.tox
.idea
.venv
diff --git a/.gitreview b/.gitreview
index 1ed269672..130a080b0 100644
--- a/.gitreview
+++ b/.gitreview
@@ -1,4 +1,4 @@
[gerrit]
-host=review.openstack.org
+host=review.opendev.org
port=29418
project=openstack/python-ironicclient.git
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..bdfcdcf02
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,6 @@
+repos:
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.14.1
+ hooks:
+ - id: mypy
+ args: [--config-file=pyproject.toml]
diff --git a/.stestr.conf b/.stestr.conf
new file mode 100644
index 000000000..c50970703
--- /dev/null
+++ b/.stestr.conf
@@ -0,0 +1,3 @@
+[DEFAULT]
+test_path=${TESTS_DIR:-./ironicclient/tests/unit}
+top_dir=./
diff --git a/.testr.conf b/.testr.conf
deleted file mode 100644
index 984a59737..000000000
--- a/.testr.conf
+++ /dev/null
@@ -1,5 +0,0 @@
-[DEFAULT]
-test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ${TESTS_DIR:-./ironicclient/tests/unit} $LISTOPT $IDOPTION
-
-test_id_option=--load-list $IDFILE
-test_list_option=--list
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index fa92b9dbe..b98a01425 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,9 +1,9 @@
If you would like to contribute to the development of OpenStack,
you must follow the steps documented at:
- http://docs.openstack.org/infra/manual/developers.html
+ https://docs.openstack.org/infra/manual/developers.html
More information on contributing can be found within the project
documentation:
- http://docs.openstack.org/developer/python-ironicclient/contributing.html
+ https://docs.openstack.org/python-ironicclient/latest/contributor/contributing.html
diff --git a/README.rst b/README.rst
index ab790fe6b..ef767496a 100644
--- a/README.rst
+++ b/README.rst
@@ -1,35 +1,47 @@
-========================
+==================================
+Python bindings for the Ironic API
+==================================
+
Team and repository tags
-========================
+------------------------
-.. image:: http://governance.openstack.org/badges/python-ironicclient.svg
- :target: http://governance.openstack.org/reference/tags/index.html
+.. image:: https://governance.openstack.org/tc/badges/python-ironicclient.svg
+ :target: https://governance.openstack.org/tc/reference/tags/index.html
-.. Change things from this point on
+Overview
+--------
-Python bindings for the Ironic API
-==================================
+This is a client for the OpenStack `Bare Metal API
+`_. It provides:
-This is a client for the OpenStack `Ironic
-`_ API. It provides a Python API (the
-``ironicclient`` module) and a command-line interface (``ironic``).
+* a Python API: the ``ironicclient`` module, and
+* a command-line interfaces: ``openstack baremetal``
Development takes place via the usual OpenStack processes as outlined in the
-`developer guide `_. The master
-repository is on `git.openstack.org
-`_.
-
-``python-ironicclient`` is licensed under the Apache License like the rest
-of OpenStack.
+`developer guide `_.
+The master repository is on `opendev.org
+`_.
+``python-ironicclient`` is licensed under the Apache License, Version 2.0,
+like the rest of OpenStack.
.. contents:: Contents:
:local:
+Project resources
+-----------------
+
+* Documentation: https://docs.openstack.org/python-ironicclient/latest/
+* Source: https://opendev.org/openstack/python-ironicclient
+* PyPi: https://pypi.org/project/python-ironicclient
+* Bugs: https://storyboard.openstack.org/#!/project/959
+* Release notes: https://docs.openstack.org/releasenotes/python-ironicclient/
+
Python API
----------
Quick-start Example::
+
>>> from ironicclient import client
>>>
>>> kwargs = {'os_auth_token': '3bcc3d3a03f44e3d8377f9247b0ad155',
@@ -37,57 +49,27 @@ Quick-start Example::
>>> ironic = client.get_client(1, **kwargs)
-Command-line API
-----------------
-
-This package will install the ``ironic`` command line interface that you
-can use to interact with the ``ironic`` API.
-
-In order to use the ``ironic`` CLI you'll need to provide your OpenStack
-tenant, username, password and authentication endpoint. You can do this with
-the ``--os-tenant-name``, ``--os-username``, ``--os-password`` and
-``--os-auth-url`` parameters, though it may be easier to set them
-as environment variables::
-
- $ export OS_PROJECT_NAME=project
- $ export OS_USERNAME=user
- $ export OS_PASSWORD=pass
- $ export OS_AUTH_URL=http://auth.example.com:5000/v2.0
+``openstack baremetal`` CLI
+---------------------------
-To use a specific Ironic API endpoint::
+The ``openstack baremetal`` command line interface is available when the bare
+metal plugin (included in this package) is used with the `OpenStackClient
+`_.
- $ export IRONIC_URL=http://ironic.example.com:6385
+There are two ways to install the OpenStackClient (python-openstackclient)
+package:
-An example of creating a basic node with the pxe_ipmitool driver::
+* along with this python-ironicclient package::
- $ ironic node-create -d pxe_ipmitool
+ # pip install python-ironicclient[cli]
-An example of creating a port on a node::
-
- $ ironic port-create -a AA:BB:CC:DD:EE:FF -n nodeUUID
-
-An example of updating driver properties for a node::
-
- $ ironic node-update nodeUUID add driver_info/ipmi_address=
- $ ironic node-update nodeUUID add driver_info/ipmi_username=
- $ ironic node-update nodeUUID add driver_info/ipmi_password=
-
-
-For more information about the ``ironic`` command and the subcommands
-available, run::
+* directly::
- $ ironic help
+ # pip install python-openstackclient
-OpenStackClient Baremetal Plugin
---------------------------------
+An example of creating a basic node with the ``ipmi`` driver::
-In order to use Baremetal Plugin the OpenStackClient should be installed::
-
- # pip install python-openstackclient
-
-An example of creating a basic node with the agent_ipmitool driver::
-
- $ openstack baremetal node create --driver agent_ipmitool
+ $ openstack baremetal node create --driver ipmi
An example of creating a port on a node::
@@ -101,13 +83,3 @@ For more information about the ``openstack baremetal`` command and
the subcommands available, run::
$ openstack help baremetal
-
-* License: Apache License, Version 2.0
-* Documentation: http://docs.openstack.org/developer/python-ironicclient
-* Source: http://git.openstack.org/cgit/openstack/python-ironicclient
-* Bugs: http://bugs.launchpad.net/python-ironicclient
-
-Change logs with information about specific versions (or tags) are
-available at:
-
- ``_.
diff --git a/bindep.txt b/bindep.txt
new file mode 100644
index 000000000..0b2a2b7fb
--- /dev/null
+++ b/bindep.txt
@@ -0,0 +1,4 @@
+# install fonts missing for PDF job
+# see https://bugs.launchpad.net/openstack-i18n/+bug/1935742
+tex-gyre [platform:dpkg doc]
+
diff --git a/doc/requirements.txt b/doc/requirements.txt
new file mode 100644
index 000000000..7993d4722
--- /dev/null
+++ b/doc/requirements.txt
@@ -0,0 +1,4 @@
+openstackdocstheme>=2.2.0 # Apache-2.0
+reno>=3.1.0 # Apache-2.0
+sphinx>=2.0.0 # BSD
+sphinxcontrib-apidoc>=0.2.0 # BSD
diff --git a/doc/source/api_v1.rst b/doc/source/api_v1.rst
index cd5c866cf..3072a560a 100644
--- a/doc/source/api_v1.rst
+++ b/doc/source/api_v1.rst
@@ -8,24 +8,26 @@ The ironicclient python API lets you access ironic, the OpenStack
Bare Metal Provisioning Service.
For example, to manipulate nodes, you interact with an
-`ironicclient.v1.node`_ object.
+:py:class:`ironicclient.v1.node.Node` object.
You obtain access to nodes via attributes of the
-`ironicclient.v1.client.Client`_ object.
+:py:class:`ironicclient.v1.client.Client` object.
Usage
=====
Get a Client object
-------------------
-First, create an `ironicclient.v1.client.Client`_ instance by passing your
-credentials to `ironicclient.client.get_client()`_. By default, the
-Bare Metal Provisioning system is configured so that only administrators
+First, create an :py:class:`ironicclient.v1.client.Client` instance by passing
+your credentials to :py:meth:`ironicclient.client.get_client()`. By default,
+the Bare Metal Provisioning system is configured so that only administrators
(users with 'admin' role) have access.
.. note::
- Explicit instantiation of `ironicclient.v1.client.Client`_ may cause
- errors since it doesn't verify provided arguments, using
- `ironicclient.client.get_client()` is preferred way to get client object.
+
+ Explicit instantiation of :py:class:`ironicclient.v1.client.Client` may
+ cause errors since it doesn't verify provided arguments, using
+ :py:meth:`ironicclient.client.get_client()` is preferred way to get client
+ object.
There are two different sets of credentials that can be used::
@@ -61,6 +63,13 @@ These Identity Service credentials can be used to authenticate::
service. default: False (optional)
* os_tenant_{name|id}: name or ID of tenant
+Also the following parameters are required when using the Identity API v3::
+
+ * os_user_domain_name: name of a domain the user belongs to,
+ usually 'default'
+ * os_project_domain_name: name of a domain the project belongs to,
+ usually 'default'
+
To create a client, you can use the API like so::
>>> from ironicclient import client
@@ -74,30 +83,20 @@ To create a client, you can use the API like so::
Perform ironic operations
-------------------------
-Once you have an ironic `Client`_, you can perform various tasks::
+Once you have an :py:class:`ironicclient.v1.client.Client`, you can perform
+various tasks::
>>> ironic.driver.list() # list of drivers
>>> ironic.node.list() # list of nodes
>>> ironic.node.get(node_uuid) # information about a particular node
-When the `Client`_ needs to propagate an exception, it will usually
-raise an instance subclassed from
-`ironicclient.exc.BaseException`_ or `ironicclient.exc.ClientException`_.
+When the Client needs to propagate an exception, it will usually raise an
+instance subclassed from
+:py:class:`ironicclient.common.apiclient.exceptions.ClientException`.
Refer to the modules themselves, for more details.
ironicclient Modules
====================
-.. toctree::
- :maxdepth: 1
-
- modules
-
-
-.. _ironicclient.v1.node: api/ironicclient.v1.node.html#ironicclient.v1.node.Node
-.. _ironicclient.v1.client.Client: api/ironicclient.v1.client.html#ironicclient.v1.client.Client
-.. _Client: api/ironicclient.v1.client.html#ironicclient.v1.client.Client
-.. _ironicclient.client.get_client(): api/ironicclient.client.html#ironicclient.client.get_client
-.. _ironicclient.exc.BaseException: api/ironicclient.exc.html#ironicclient.exc.BaseException
-.. _ironicclient.exc.ClientException: api/ironicclient.exc.html#ironicclient.exc.ClientException
+* :ref:`modindex`
diff --git a/doc/source/cli.rst b/doc/source/cli.rst
deleted file mode 100644
index 78f1ee3e0..000000000
--- a/doc/source/cli.rst
+++ /dev/null
@@ -1,94 +0,0 @@
-==============================================
-:program:`ironic` Command-Line Interface (CLI)
-==============================================
-
-.. program:: ironic
-.. highlight:: bash
-
-SYNOPSIS
-========
-
-:program:`ironic` [options] [command-options]
-
-:program:`ironic help`
-
-:program:`ironic help`
-
-
-DESCRIPTION
-===========
-
-The :program:`ironic` command-line interface (CLI) interacts with the
-OpenStack Bare Metal Service (Ironic).
-
-In order to use the CLI, you must provide your OpenStack username, password,
-project (historically called tenant), and auth endpoint. You can use
-configuration options :option:`--os-username`, :option:`--os-password`,
-:option:`--os-tenant-id` (or :option:`--os-tenant-name`),
-and :option:`--os-auth-url`, or set the corresponding
-environment variables::
-
- $ export OS_USERNAME=user
- $ export OS_PASSWORD=password
- $ export OS_PROJECT_ID=b363706f891f48019483f8bd6503c54b # or OS_PROJECT_NAME
- $ export OS_PROJECT_NAME=project # or OS_PROJECT_ID
- $ export OS_AUTH_URL=http://auth.example.com:5000/v2.0
-
-The command-line tool will attempt to reauthenticate using the provided
-credentials for every request. You can override this behavior by manually
-supplying an auth token using :option:`--ironic-url` and
-:option:`--os-auth-token`, or by setting the corresponding environment
-variables::
-
- $ export IRONIC_URL=http://ironic.example.org:6385/
- $ export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
-
-Since Keystone can return multiple regions in the Service Catalog, you can
-specify the one you want with :option:`--os-region-name` or set the following
-environment variable. (It defaults to the first in the list returned.)
-::
-
- export OS_REGION_NAME=region
-
-Ironic CLI supports bash completion. The command-line tool can automatically
-fill partially typed commands. To use this feature, source the below file
-(available at
-https://git.openstack.org/cgit/openstack/python-ironicclient/tree/tools/ironic.bash_completion)
-to your terminal and then bash completion should work::
-
- $ source ironic.bash_completion
-
-To avoid doing this every time, add this to your ``.bashrc`` or copy the
-ironic.bash_completion file to the default bash completion scripts directory
-on your linux distribution.
-
-OPTIONS
-=======
-
-To get a list of available (sub)commands and options, run::
-
- $ ironic help
-
-To get usage and options of a command, run::
-
- $ ironic help
-
-
-EXAMPLES
-========
-
-Get information about the node-create command::
-
- $ ironic help node-create
-
-Get a list of available drivers::
-
- $ ironic driver-list
-
-Enroll a node with "fake" deploy driver and "ipmitool" power driver::
-
- $ ironic node-create -d fake_ipmitool -i ipmi_address=1.2.3.4
-
-Get a list of nodes::
-
- $ ironic node-list
diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst
new file mode 100644
index 000000000..51704cec1
--- /dev/null
+++ b/doc/source/cli/index.rst
@@ -0,0 +1,8 @@
+======================================
+python-ironicclient User Documentation
+======================================
+
+.. toctree::
+
+ standalone
+ osc_plugin_cli
diff --git a/doc/source/cli/osc/v1/index.rst b/doc/source/cli/osc/v1/index.rst
new file mode 100644
index 000000000..d5b98509c
--- /dev/null
+++ b/doc/source/cli/osc/v1/index.rst
@@ -0,0 +1,68 @@
+Command Reference
+=================
+
+List of released CLI commands available in openstack client. These commands
+can be referenced by doing ``openstack help baremetal``.
+
+====================
+baremetal allocation
+====================
+
+.. autoprogram-cliff:: openstack.baremetal.v1
+ :command: baremetal allocation *
+
+=================
+baremetal chassis
+=================
+
+.. autoprogram-cliff:: openstack.baremetal.v1
+ :command: baremetal chassis *
+
+===================
+baremetal conductor
+===================
+
+.. autoprogram-cliff:: openstack.baremetal.v1
+ :command: baremetal conductor *
+
+================
+baremetal create
+================
+
+.. autoprogram-cliff:: openstack.baremetal.v1
+ :command: baremetal create
+
+=========================
+baremetal deploy template
+=========================
+
+.. autoprogram-cliff:: openstack.baremetal.v1
+ :command: baremetal deploy template *
+
+================
+baremetal driver
+================
+
+.. autoprogram-cliff:: openstack.baremetal.v1
+ :command: baremetal driver *
+
+==============
+baremetal node
+==============
+
+.. autoprogram-cliff:: openstack.baremetal.v1
+ :command: baremetal node *
+
+==============
+baremetal port
+==============
+
+.. autoprogram-cliff:: openstack.baremetal.v1
+ :command: baremetal port *
+
+================
+baremetal volume
+================
+
+.. autoprogram-cliff:: openstack.baremetal.v1
+ :command: baremetal volume *
diff --git a/doc/source/osc_plugin_cli.rst b/doc/source/cli/osc_plugin_cli.rst
similarity index 65%
rename from doc/source/osc_plugin_cli.rst
rename to doc/source/cli/osc_plugin_cli.rst
index 119bc44f0..76815fdd9 100644
--- a/doc/source/osc_plugin_cli.rst
+++ b/doc/source/cli/osc_plugin_cli.rst
@@ -1,6 +1,6 @@
-============================================================================
-:program:`openstack baremetal` OpenStack Client Command-Line Interface (CLI)
-============================================================================
+====================================================
+``openstack baremetal`` Command-Line Interface (CLI)
+====================================================
.. program:: openstack baremetal
.. highlight:: bash
@@ -19,14 +19,31 @@ Description
The OpenStack Client plugin interacts with the Bare Metal service
through the ``openstack baremetal`` command line interface (CLI).
-To use ``openstack`` CLI, the OpenStackClient should be installed::
+To use the ``openstack`` CLI, the OpenStackClient (python-openstackclient)
+package must be installed. There are two ways to do this:
- # pip install python-openstackclient
+* along with this python-ironicclient package::
+
+ $ pip install python-ironicclient[cli]
+
+* directly::
+
+ $ pip install python-openstackclient
+
+This CLI is provided by python-openstackclient and osc-lib projects:
+
+* https://opendev.org/openstack/python-openstackclient
+* https://opendev.org/openstack/osc-lib
+
+.. _osc-auth:
+
+Authentication
+--------------
To use the CLI, you must provide your OpenStack username, password,
project, and auth endpoint. You can use configuration options
-:option:`--os-username`, :option:`--os-password`, :option:`--os-project-id`
-(or :option:`--os-project-name`), and :option:`--os-auth-url`,
+``--os-username``, ``--os-password``, ``--os-project-id``
+(or ``--os-project-name``), and ``--os-auth-url``,
or set the corresponding environment variables::
$ export OS_USERNAME=user
@@ -37,11 +54,6 @@ or set the corresponding environment variables::
$ export OS_IDENTITY_API_VERSION=3
$ export OS_AUTH_URL=http://auth.example.com:5000/identity
-This CLI is provided by python-openstackclient and osc-lib projects:
-
-* https://git.openstack.org/openstack/python-openstackclient
-* https://git.openstack.org/openstack/osc-lib
-
Getting help
============
@@ -66,9 +78,9 @@ Get a list of available drivers::
$ openstack baremetal driver list
-Enroll a node with "agent_ipmitool" driver::
+Enroll a node with the ``ipmi`` driver::
- $ openstack baremetal node create --driver agent_ipmitool --driver-info ipmi_address=1.2.3.4
+ $ openstack baremetal node create --driver ipmi --driver-info ipmi_address=1.2.3.4
Get a list of nodes::
@@ -83,3 +95,12 @@ The baremetal API version can be specified via:
* or optional command line argument --os-baremetal-api-version::
$ openstack baremetal port group list --os-baremetal-api-version 1.25
+
+
+Command Reference
+=================
+.. toctree::
+ :glob:
+ :maxdepth: 3
+
+ osc/v1/*
diff --git a/doc/source/cli/standalone.rst b/doc/source/cli/standalone.rst
new file mode 100644
index 000000000..ef00a3b44
--- /dev/null
+++ b/doc/source/cli/standalone.rst
@@ -0,0 +1,81 @@
+=====================================================
+``baremetal`` Standalone Command-Line Interface (CLI)
+=====================================================
+
+.. program:: baremetal
+.. highlight:: bash
+
+Synopsis
+========
+
+:program:`baremetal [options]` [command-options]
+
+:program:`baremetal help`
+
+
+Description
+===========
+
+The standalone ``baremetal`` tool allows interacting with the Bare Metal
+service without installing the OpenStack Client tool as in
+:doc:`osc_plugin_cli`.
+
+The standalone tool is mostly identical to its OSC counterpart, with two
+exceptions:
+
+#. No need to prefix commands with ``openstack``.
+#. No authentication is assumed by default.
+
+Check the :doc:`OSC CLI reference ` for a list of available
+commands.
+
+Inspector support
+-----------------
+
+The standalone ``baremetal`` tool optionally supports the low-level bare metal
+introspection API provided by ironic-inspector_. If ironic-inspector-client_ is
+installed, its commands_ are automatically available (also without the
+``openstack`` prefix).
+
+.. _ironic-inspector: https://docs.openstack.org/ironic-inspector/
+.. _ironic-inspector-client: https://docs.openstack.org/python-ironic-inspector-client/
+.. _commands: https://docs.openstack.org/python-ironic-inspector-client/latest/cli/index.html
+
+Standalone usage
+----------------
+
+To use the CLI with a standalone bare metal service, you need to provide an
+endpoint to connect to. It can be done in three ways:
+
+#. Provide an explicit ``--os-endpoint`` argument, e.g.:
+
+ .. code-block:: bash
+
+ $ baremetal --os-endpoint https://ironic.host:6385 node list
+
+#. Set the corresponding environment variable, e.g.:
+
+ .. code-block:: bash
+
+ $ export OS_ENDPOINT=https://ironic.host:6385
+ $ baremetal node list
+
+#. Populate a clouds.yaml_ file, setting ``baremetal_endpoint_override``, e.g.:
+
+ .. code-block:: bash
+
+ $ cat ~/.config/openstack/clouds.yaml
+ clouds:
+ ironic:
+ auth_type: none
+ baremetal_endpoint_override: http://127.0.0.1:6385
+ $ export OS_CLOUD=ironic
+ $ baremetal node list
+
+.. _clouds.yaml: https://docs.openstack.org/openstacksdk/latest/user/guides/connect_from_config.html
+
+Usage with OpenStack
+--------------------
+
+The standalone CLI can also be used with the Bare Metal service installed as
+part of OpenStack. See :ref:`osc-auth` for information on the required input.
diff --git a/doc/source/conf.py b/doc/source/conf.py
index cda54be4d..0d54eeb62 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -2,11 +2,25 @@
# 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',
+extensions = ['sphinxcontrib.apidoc',
'sphinx.ext.viewcode',
- 'oslosphinx',
+ 'openstackdocstheme',
+ 'cliff.sphinxext',
]
+# sphinxcontrib.apidoc options
+apidoc_module_dir = '../../ironicclient'
+apidoc_output_dir = 'reference/api'
+apidoc_excluded_paths = [
+ 'tests']
+apidoc_separate_modules = True
+
+
+# openstackdocstheme options
+openstackdocs_repo_name = 'openstack/python-ironicclient'
+openstackdocs_pdf_link = True
+openstackdocs_use_storyboard = True
+
# 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
@@ -21,8 +35,7 @@
master_doc = 'index'
# General information about the project.
-project = u'python-ironicclient'
-copyright = u'OpenStack Foundation'
+copyright = 'OpenStack Foundation'
# A list of ignored prefixes for module index sorting.
modindex_common_prefix = ['ironicclient.']
@@ -35,12 +48,12 @@
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = 'native'
# A list of glob-style patterns that should be excluded when looking for
# source files. They are matched against the source file names relative to the
# source directory, using slashes as directory separators on all platforms.
-exclude_patterns = ['api/ironicclient.tests.functional.*']
+exclude_patterns = ['']
# -- Options for HTML output --------------------------------------------------
@@ -49,10 +62,12 @@
#html_theme_path = ["."]
#html_theme = '_theme'
#html_static_path = ['_static']
+html_theme = 'openstackdocs'
# Output file base name for HTML help builder.
-htmlhelp_basename = '%sdoc' % project
+htmlhelp_basename = 'python-ironicclientdoc'
+latex_use_xindy = False
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
@@ -60,9 +75,11 @@
latex_documents = [
(
'index',
- '%s.tex' % project,
- u'%s Documentation' % project,
- u'OpenStack LLC',
+ 'doc-python-ironicclient.tex',
+ 'Python Ironic Client Documentation',
+ 'OpenStack LLC',
'manual'
),
]
+
+autoprogram_cliff_application = 'openstack'
diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst
deleted file mode 100644
index 18e8cab77..000000000
--- a/doc/source/contributing.rst
+++ /dev/null
@@ -1,62 +0,0 @@
-.. _contributing:
-
-===================================
-Contributing to python-ironicclient
-===================================
-
-If you're interested in contributing to the python-ironicclient project,
-the following will help get you started.
-
-#openstack-ironic on Freenode IRC Network
------------------------------------------
-There is a very active chat channel at irc://freenode.net/#openstack-ironic.
-This is usually the best place to ask questions and find your way around.
-IRC stands for Internet Relay Chat and it is a way to chat online in real
-time. You can ask a question and come back later to read the answer in the
-log files. Logs for the #openstack-ironic IRC channel are stored at
-http://eavesdrop.openstack.org/irclogs/%23openstack-ironic/.
-
-Contributor License Agreement
------------------------------
-
-.. index::
- single: license; agreement
-
-In order to contribute to the python-ironicclient project, you need to have
-signed OpenStack's contributor's agreement.
-
-.. seealso::
-
- * http://docs.openstack.org/infra/manual/developers.html
- * http://wiki.openstack.org/CLA
-
-LaunchPad Project
------------------
-
-Most of the tools used for OpenStack depend on a launchpad.net ID for
-authentication. After signing up for a launchpad account, join the
-"openstack" team to have access to the mailing list and receive
-notifications of important events.
-
-.. seealso::
-
- * http://launchpad.net
- * http://launchpad.net/python-ironicclient
- * http://launchpad.net/~openstack
-
-
-Project Hosting Details
------------------------
-
-Bug tracker
- http://launchpad.net/python-ironicclient
-
-Mailing list (prefix subjects with ``[ironic]`` for faster responses)
- http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev
-
-Code Hosting
- https://git.openstack.org/cgit/openstack/python-ironicclient
-
-Code Review
- https://review.openstack.org/#/q/status:open+project:openstack/python-ironicclient,n,z
-
diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst
new file mode 100644
index 000000000..5d62ef7d2
--- /dev/null
+++ b/doc/source/contributor/contributing.rst
@@ -0,0 +1,52 @@
+.. _contributing:
+
+===================================
+Contributing to python-ironicclient
+===================================
+
+If you're interested in contributing to the python-ironicclient project,
+the following will help get you started.
+
+#openstack-ironic on OFTC IRC Network
+-------------------------------------
+There is a very active chat channel at irc://irc.oftc.net/#openstack-ironic.
+This is usually the best place to ask questions and find your way around.
+IRC stands for Internet Relay Chat and it is a way to chat online in real
+time. You can ask a question and come back later to read the answer in the
+log files. Logs for the #openstack-ironic IRC channel are stored at
+http://eavesdrop.openstack.org/irclogs/%23openstack-ironic/.
+
+Developer Certificate of Origin
+-------------------------------
+
+.. index::
+ single: license; agreement
+
+In order to contribute to the python-ironicclient project, you need to adhere
+to the `Developer Certificate of Origin`_. OpenStack utilizes the Developer
+Certificate of Origin (DCO) as a lightweight means to confirm that you are
+entitled to contribute the code you submit. This ensures that you are
+providing your contributions under the project's license and that you have
+the right to do so.
+
+.. _Developer Certificate of Origin: https://developercertificate.org/
+
+.. seealso::
+
+ * https://docs.openstack.org/contributors/common/dco.html
+
+Project Hosting Details
+-----------------------
+
+Bug tracker
+ https://storyboard.openstack.org/#!/project/959
+
+Mailing list (prefix subjects with ``[ironic]`` for faster responses)
+ http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss
+
+Code Hosting
+ https://opendev.org/openstack/python-ironicclient
+
+Code Review
+ https://review.opendev.org/#/q/status:open+project:openstack/python-ironicclient,n,z
+
diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst
new file mode 100644
index 000000000..e0c982322
--- /dev/null
+++ b/doc/source/contributor/index.rst
@@ -0,0 +1,8 @@
+=============================================
+python-ironicclient Contributor Documentation
+=============================================
+
+.. toctree::
+
+ contributing
+ testing
diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst
new file mode 100644
index 000000000..99090ea27
--- /dev/null
+++ b/doc/source/contributor/testing.rst
@@ -0,0 +1,37 @@
+.. _testing:
+
+=======
+Testing
+=======
+
+Python Guideline Enforcement
+............................
+
+All code has to pass the pep8 style guideline to merge into OpenStack, to
+validate the code against these guidelines you can run::
+
+ $ tox -e pep8
+
+Unit Testing
+............
+
+It is strongly encouraged to run the unit tests locally under one or more
+test environments prior to submitting a patch. To run all the recommended
+environments sequentially and pep8 style guideline run::
+
+ $ tox
+
+You can also selectively pick specific test environments by listing your
+chosen environments after a -e flag::
+
+ $ tox -e py3,pep8
+
+.. note::
+ Tox sets up virtual environment and installs all necessary dependencies.
+ Sharing the environment with devstack testing is not recommended due to
+ conflicting configuration with system dependencies.
+
+Functional Testing
+..................
+
+Functional tests have been removed as of November 2024.
diff --git a/doc/source/index.rst b/doc/source/index.rst
index a7e7156d5..67dc6ee8a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -2,23 +2,23 @@
Python Bindings to the OpenStack Ironic API
===========================================
-This is a client for OpenStack `Ironic`_ API. There's a Python API
-(the `ironicclient` modules), and a command-line interface (installed as
-`ironic`).
+This is a client for the OpenStack `Ironic`_ API. It provides:
+
+* a Python API: the ``ironicclient`` module, and
+* command-line interface: ``openstack baremetal``
Contents
========
.. toctree::
- :maxdepth: 1
+ :maxdepth: 2
api_v1
- cli
- osc_plugin_cli
- create_command
- contributing
- testing
- Release Notes
+ cli/index
+ user/create_command
+ contributor/index
+ reference/index
+ Release Notes
Indices and tables
==================
diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst
new file mode 100644
index 000000000..87dea8a7c
--- /dev/null
+++ b/doc/source/reference/index.rst
@@ -0,0 +1,8 @@
+=======================================
+Full Ironic Client Python API Reference
+=======================================
+
+.. toctree::
+ :maxdepth: 1
+
+ api/modules
diff --git a/doc/source/testing.rst b/doc/source/testing.rst
deleted file mode 100644
index 97a6491c1..000000000
--- a/doc/source/testing.rst
+++ /dev/null
@@ -1,67 +0,0 @@
-.. _testing:
-
-=======
-Testing
-=======
-
-Python Guideline Enforcement
-............................
-
-All code has to pass the pep8 style guideline to merge into OpenStack, to
-validate the code against these guidelines you can run::
-
- $ tox -e pep8
-
-Unit Testing
-............
-
-It is strongly encouraged to run the unit tests locally under one or more
-test environments prior to submitting a patch. To run all the recommended
-environments sequentially and pep8 style guideline run::
-
- $ tox
-
-You can also selectively pick specific test environments by listing your
-chosen environments after a -e flag::
-
- $ tox -e py35,py27,pep8,pypy
-
-.. note::
- Tox sets up virtual environment and installs all necessary dependencies.
- Sharing the environment with devstack testing is not recommended due to
- conflicting configuration with system dependencies.
-
-Functional Testing
-..................
-
-Functional testing assumes the existence of the script run_functional.sh in the
-python-ironicclient/tools directory. The script run_functional.sh generates
-test.conf file. To run functional tests just run ./run_functional.sh.
-
-Also, the test.conf file could be created manually or generated from
-environment variables. It assumes the existence of an openstack
-cloud installation along with admin credentials. The test.conf file lives in
-ironicclient/tests/functional/ directory. To run functional tests in that way
-create test.conf manually and run::
-
- $ tox -e functional
-
-An example test.conf file::
-
- [functional]
- api_version = 1
- os_auth_url=http://192.168.0.2:5000/v2.0/
- os_username=admin
- os_password=admin
- os_project_name=admin
-
-If you are testing ironic in standalone mode, only the parameters
-'auth_strategy', 'os_auth_token' and 'ironic_url' are required;
-all others will be ignored.
-
-An example test.conf file for standalone host::
-
- [functional]
- auth_strategy = noauth
- os_auth_token = fake
- ironic_url = http://10.0.0.2:6385
diff --git a/doc/source/create_command.rst b/doc/source/user/create_command.rst
similarity index 65%
rename from doc/source/create_command.rst
rename to doc/source/user/create_command.rst
index ec966c9f1..ce49edca4 100644
--- a/doc/source/create_command.rst
+++ b/doc/source/user/create_command.rst
@@ -3,58 +3,24 @@ Creating the Bare Metal service resources from file
===================================================
It is possible to create a set of resources using their descriptions in JSON
-or YAML format. It can be done in one of three ways:
+or YAML format. It can be done in one of two ways:
-1. Using ironic CLI's ``ironic create`` command::
-
- $ ironic help create
- usage: ironic create [ ...]
-
- Create baremetal resources (chassis, nodes, port groups and ports). The
- resources may be described in one or more JSON or YAML files. If any file
- cannot be validated, no resources are created. An attempt is made to
- create all the resources; those that could not be created are skipped
- (with a corresponding error message).
-
- Positional arguments:
- File (.yaml or .json) containing descriptions of the resources
- to create. Can be specified multiple times.
-
-2. Using openstackclient plugin command ``openstack baremetal create``::
+1. Using OpenStackClient bare metal plugin CLI's command ``openstack baremetal
+ create``::
$ openstack -h baremetal create
- usage: openstack [-h] [-f {json,shell,table,value,yaml}] [-c COLUMN]
- [--max-width ] [--noindent] [--prefix PREFIX]
- [--chassis-uuid ] [--driver-info ]
- [--property ] [--extra ]
- [--uuid ] [--name ]
- [--network-interface ]
- [--resource-class ] [--driver ]
- [ [ ...]]
-
- Create resources from files or Register a new node (DEPRECATED). Create
- resources from files (by only specifying the files) or register a new
- node by specifying one or more optional arguments (DEPRECATED, use
- 'openstack baremetal node create' instead).
+ usage: openstack baremetal create [-h] [ ...]
- positional arguments:
- File (.yaml or .json) containing descriptions of
- the resources to create. Can be specified
- multiple times. If you want to create resources,
- only specify the files. Do not specify any of
- the optional arguments.
+ Create resources from files
- .. note::
- If the ``--driver`` argument is passed in, the behaviour of the command
- is the same as ``openstack baremetal node create``, and positional
- arguments are ignored. If it is not provided, the command does resource
- creation from file(s), and only positional arguments will be taken into
- account.
+ positional arguments:
+ File (.yaml or .json) containing descriptions of the
+ resources to create. Can be specified multiple times.
-3. Programmatically using the Python API:
+2. Programmatically using the Python API:
- .. autofunction:: ironicclient.v1.create_resources.create_resources
- :noindex:
+ .. autofunction:: ironicclient.v1.create_resources.create_resources
+ :noindex:
File containing Resource Descriptions
=====================================
@@ -109,7 +75,7 @@ command::
"nodes": [
{
"name": "node-3",
- "driver": "agent_ipmitool",
+ "driver": "ipmi",
"portgroups": [
{
"name": "switch.cz7882.ports.1-2",
@@ -130,11 +96,16 @@ command::
{
"address": "00:00:00:00:00:03"
}
- ]
+ ],
+ "driver_info": {
+ "ipmi_address": "192.168.1.23",
+ "ipmi_username": "BmcUsername",
+ "ipmi_password": "BmcPassword",
+ }
},
{
"name": "node-4",
- "driver": "agent_ipmitool",
+ "driver": "ipmi",
"ports": [
{
"address": "00:00:00:00:00:04"
@@ -150,7 +121,7 @@ command::
"nodes": [
{
"name": "node-5",
- "driver": "pxe_ipmitool",
+ "driver": "ipmi",
"chassis_uuid": "74d93e6e-7384-4994-a614-fd7b399b0785",
"ports": [
{
@@ -160,7 +131,7 @@ command::
},
{
"name": "node-6",
- "driver": "pxe_ipmitool"
+ "driver": "ipmi"
}
]
}
diff --git a/ironicclient/client.py b/ironicclient/client.py
index 74c5ae832..dbab71f1c 100644
--- a/ironicclient/client.py
+++ b/ironicclient/client.py
@@ -10,144 +10,126 @@
# License for the specific language governing permissions and limitations
# under the License.
-from keystoneauth1 import loading as kaloading
+import logging
+
+from openstack import config
from oslo_utils import importutils
from ironicclient.common.i18n import _
from ironicclient import exc
+LOG = logging.getLogger(__name__)
+
-def get_client(api_version, os_auth_token=None, ironic_url=None,
- os_username=None, os_password=None, os_auth_url=None,
- os_project_id=None, os_project_name=None, os_tenant_id=None,
- os_tenant_name=None, os_region_name=None,
- os_user_domain_id=None, os_user_domain_name=None,
- os_project_domain_id=None, os_project_domain_name=None,
- os_service_type=None, os_endpoint_type=None,
- insecure=None, timeout=None, os_cacert=None, ca_file=None,
- os_cert=None, cert_file=None, os_key=None, key_file=None,
- os_ironic_api_version=None, max_retries=None,
- retry_interval=None, session=None, **ignored_kwargs):
+def get_client(api_version, auth_type=None, os_ironic_api_version=None,
+ max_retries=None, retry_interval=None, session=None,
+ valid_interfaces=None, interface=None, service_type=None,
+ region_name=None, additional_headers=None,
+ global_request_id=None, **kwargs):
"""Get an authenticated client, based on the credentials.
:param api_version: the API version to use. Valid value: '1'.
- :param os_auth_token: pre-existing token to re-use
- :param ironic_url: ironic API endpoint
- :param os_username: name of a user
- :param os_password: user's password
- :param os_auth_url: endpoint to authenticate against
- :param os_tenant_name: name of a tenant (deprecated in favour of
- os_project_name)
- :param os_tenant_id: ID of a tenant (deprecated in favour of
- os_project_id)
- :param os_project_name: name of a project
- :param os_project_id: ID of a project
- :param os_region_name: name of a keystone region
- :param os_user_domain_name: name of a domain the user belongs to
- :param os_user_domain_id: ID of a domain the user belongs to
- :param os_project_domain_name: name of a domain the project belongs to
- :param os_project_domain_id: ID of a domain the project belongs to
- :param os_service_type: the type of service to lookup the endpoint for
- :param os_endpoint_type: the type (exposure) of the endpoint
- :param insecure: allow insecure SSL (no cert verification)
- :param timeout: allows customization of the timeout for client HTTP
- requests
- :param os_cacert: path to cacert file
- :param ca_file: path to cacert file, deprecated in favour of os_cacert
- :param os_cert: path to cert file
- :param cert_file: path to cert file, deprecated in favour of os_cert
- :param os_key: path to key file
- :param key_file: path to key file, deprecated in favour of os_key
- :param os_ironic_api_version: ironic API version to use
+ :param auth_type: type of keystoneauth auth plugin loader to use.
+ :param os_ironic_api_version: ironic API version to use.
:param max_retries: Maximum number of retries in case of conflict error
:param retry_interval: Amount of time (in seconds) between retries in case
- of conflict error
- :param session: Keystone session to use
- :param ignored_kwargs: all the other params that are passed. Left for
- backwards compatibility. They are ignored.
+ of conflict error.
+ :param session: An existing keystoneauth session. Will be created from
+ kwargs if not provided.
+ :param valid_interfaces: List of valid endpoint interfaces to use if
+ the bare metal endpoint is not provided.
+ :param interface: An alias for valid_interfaces.
+ :param service_type: Bare metal endpoint service type.
+ :param region_name: Name of the region to use when searching the bare metal
+ endpoint.
+ :param additional_headers: Additional headers that should be attached
+ to every request passing through the client. Headers of the same name
+ specified per request will take priority.
+ :param global_request_id: A header (in the form of ``req-$uuid``) that will
+ be passed on all requests. Enables cross project request id tracking.
+ :param kwargs: all the other params that are passed to keystoneauth for
+ session construction.
"""
- os_service_type = os_service_type or 'baremetal'
- os_endpoint_type = os_endpoint_type or 'publicURL'
- project_id = (os_project_id or os_tenant_id)
- project_name = (os_project_name or os_tenant_name)
- kwargs = {
+ # TODO(TheJulia): At some point, we should consider possibly noting
+ # the "latest" flag for os_ironic_api_version to cause the client to
+ # auto-negotiate to the greatest available version, however we do not
+ # have the ability yet for a caller to cap the version, and will hold
+ # off doing so until then.
+ if auth_type is None:
+ if 'endpoint' in kwargs:
+ if 'token' in kwargs:
+ auth_type = 'admin_token'
+ else:
+ auth_type = 'none'
+ elif 'token' in kwargs and 'auth_url' in kwargs:
+ auth_type = 'token'
+ else:
+ auth_type = 'password'
+
+ if not session:
+ # TODO(dtantsur): consider flipping load_yaml_config to True to support
+ # the clouds.yaml format.
+ region = config.get_cloud_region(load_yaml_config=False,
+ load_envvars=False,
+ auth_type=auth_type,
+ **kwargs)
+ session = region.get_session()
+
+ # Make sure we also pass the endpoint interface to the HTTP client.
+ # NOTE(gyee/efried): 'interface' in ksa config is deprecated in favor of
+ # 'valid_interfaces'. So, since the caller may be deriving kwargs from
+ # conf, accept 'valid_interfaces' first. But keep support for 'interface',
+ # in case the caller is deriving kwargs from, say, an existing Adapter.
+ interface = valid_interfaces or interface
+
+ endpoint = kwargs.get('endpoint')
+ if not endpoint:
+ try:
+ # endpoint will be used to get hostname
+ # and port that will be used for API version caching.
+ # NOTE(gyee): KSA defaults interface to 'public' if it is
+ # empty or None so there's no need to set it to publicURL
+ # explicitly.
+ endpoint = session.get_endpoint(
+ service_type=service_type or 'baremetal',
+ interface=interface,
+ region_name=region_name,
+ )
+ except Exception as e:
+ raise exc.AmbiguousAuthSystem(
+ _('Must provide Keystone credentials or user-defined '
+ 'endpoint, error was: %s') % e)
+
+ ironicclient_kwargs = {
'os_ironic_api_version': os_ironic_api_version,
+ 'additional_headers': additional_headers,
+ 'global_request_id': global_request_id,
'max_retries': max_retries,
'retry_interval': retry_interval,
+ 'session': session,
+ 'endpoint_override': endpoint,
+ 'interface': interface
}
- endpoint = ironic_url
- cacert = os_cacert or ca_file
- cert = os_cert or cert_file
- key = os_key or key_file
- if os_auth_token and endpoint:
- kwargs.update({
- 'token': os_auth_token,
- 'insecure': insecure,
- 'ca_file': cacert,
- 'cert_file': cert,
- 'key_file': key,
- 'timeout': timeout,
- })
- elif os_auth_url:
- auth_type = 'password'
- auth_kwargs = {
- 'auth_url': os_auth_url,
- 'project_id': project_id,
- 'project_name': project_name,
- 'user_domain_id': os_user_domain_id,
- 'user_domain_name': os_user_domain_name,
- 'project_domain_id': os_project_domain_id,
- 'project_domain_name': os_project_domain_name,
- }
- if os_username and os_password:
- auth_kwargs.update({
- 'username': os_username,
- 'password': os_password,
- })
- elif os_auth_token:
- auth_type = 'token'
- auth_kwargs.update({
- 'token': os_auth_token,
- })
- # Create new session only if it was not passed in
- if not session:
- loader = kaloading.get_plugin_loader(auth_type)
- auth_plugin = loader.load_from_options(**auth_kwargs)
- # Let keystoneauth do the necessary parameter conversions
- session = kaloading.session.Session().load_from_options(
- auth=auth_plugin, insecure=insecure, cacert=cacert,
- cert=cert, key=key, timeout=timeout,
- )
- exception_msg = _('Must provide Keystone credentials or user-defined '
- 'endpoint and token')
- if not endpoint:
- if session:
- try:
- # Pass the endpoint, it will be used to get hostname
- # and port that will be used for API version caching. It will
- # be also set as endpoint_override.
- endpoint = session.get_endpoint(
- service_type=os_service_type,
- interface=os_endpoint_type,
- region_name=os_region_name
- )
- except Exception as e:
- raise exc.AmbiguousAuthSystem(
- _('%(message)s, error was: %(error)s') %
- {'message': exception_msg, 'error': e})
- else:
- # Neither session, nor valid auth parameters provided
- raise exc.AmbiguousAuthSystem(exception_msg)
+ return Client(api_version, **ironicclient_kwargs)
- # Always pass the session
- kwargs['session'] = session
- return Client(api_version, endpoint, **kwargs)
+def Client(version, endpoint_override=None, session=None, *args, **kwargs):
+ """Create a client of an appropriate version.
+ This call requires a session. If you want it to be created, use
+ ``get_client`` instead.
-def Client(version, *args, **kwargs):
+ :param endpoint_override: A bare metal endpoint to use.
+ :param session: A keystoneauth session to use. This argument is actually
+ required and is marked optional only for backward compatibility.
+ :param args: Other arguments to pass to the HTTP client. Not recommended,
+ use kwargs instead.
+ :param kwargs: Other keyword arguments to pass to the HTTP client (e.g.
+ ``insecure``).
+ """
module = importutils.import_versioned_module('ironicclient',
version, 'client')
client_class = getattr(module, 'Client')
- return client_class(*args, **kwargs)
+ return client_class(endpoint_override=endpoint_override, session=session,
+ *args, **kwargs)
diff --git a/ironicclient/common/apiclient/base.py b/ironicclient/common/apiclient/base.py
index fcf536d0c..b153de305 100644
--- a/ironicclient/common/apiclient/base.py
+++ b/ironicclient/common/apiclient/base.py
@@ -20,23 +20,28 @@
Base utilities to build API operation managers and objects on top of.
"""
+from __future__ import annotations
# E1102: %s is not callable
# pylint: disable=E1102
import abc
+from collections.abc import Sequence
import copy
+from http import client as http_client
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import ClassVar
+from urllib import parse as urlparse
from oslo_utils import strutils
-import six
-from six.moves import http_client
-from six.moves.urllib import parse
from ironicclient.common.apiclient import exceptions
from ironicclient.common.i18n import _
-def getid(obj):
+def getid(obj: Any) -> Any:
"""Return id if argument is a Resource.
Abstracts the common pattern of allowing both an object or an object's ID
@@ -56,10 +61,11 @@ def getid(obj):
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
- _hooks_map = {}
+
+ _hooks_map: ClassVar[dict[str, list[Callable[..., Any]]]] = {}
@classmethod
- def add_hook(cls, hook_type, hook_func):
+ def add_hook(cls, hook_type: str, hook_func: Callable[..., Any]) -> None:
"""Add a new hook of specified type.
:param cls: class that registers hooks
@@ -72,7 +78,7 @@ def add_hook(cls, hook_type, hook_func):
cls._hooks_map[hook_type].append(hook_func)
@classmethod
- def run_hooks(cls, hook_type, *args, **kwargs):
+ def run_hooks(cls, hook_type: str, *args: Any, **kwargs: Any) -> None:
"""Run all hooks of specified type.
:param cls: class that registers hooks
@@ -91,9 +97,10 @@ class BaseManager(HookableMixin):
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
- resource_class = None
- def __init__(self, client):
+ resource_class: type[Resource] | None = None
+
+ def __init__(self, client: Any) -> None:
"""Initializes BaseManager with `client`.
:param client: instance of BaseClient descendant for HTTP requests
@@ -101,7 +108,13 @@ def __init__(self, client):
super(BaseManager, self).__init__()
self.client = client
- def _list(self, url, response_key=None, obj_class=None, json=None):
+ def _list(
+ self,
+ url: str,
+ response_key: str | None = None,
+ obj_class: type[Resource] | None = None,
+ json: dict[str, Any] | None = None,
+ ) -> list[Resource]:
"""List the collection.
:param url: a partial URL, e.g., '/servers'
@@ -121,17 +134,20 @@ def _list(self, url, response_key=None, obj_class=None, json=None):
if obj_class is None:
obj_class = self.resource_class
+ if obj_class is None:
+ raise ValueError("resource_class must be set")
+
data = body[response_key] if response_key is not None else body
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
- data = data['values']
+ data = data["values"]
except (KeyError, TypeError):
pass
return [obj_class(self, res, loaded=True) for res in data if res]
- def _get(self, url, response_key=None):
+ def _get(self, url: str, response_key: str | None = None) -> Resource:
"""Get an object from collection.
:param url: a partial URL, e.g., '/servers'
@@ -139,19 +155,27 @@ def _get(self, url, response_key=None):
e.g., 'server'. If response_key is None - all response body
will be used.
"""
+ if self.resource_class is None:
+ raise ValueError("resource_class must be set")
body = self.client.get(url).json()
data = body[response_key] if response_key is not None else body
return self.resource_class(self, data, loaded=True)
- def _head(self, url):
+ def _head(self, url: str) -> bool:
"""Retrieve request headers for an object.
:param url: a partial URL, e.g., '/servers'
"""
resp = self.client.head(url)
- return resp.status_code == http_client.NO_CONTENT
-
- def _post(self, url, json, response_key=None, return_raw=False):
+ return bool(resp.status_code == http_client.NO_CONTENT)
+
+ def _post(
+ self,
+ url: str,
+ json: dict[str, Any],
+ response_key: str | None = None,
+ return_raw: bool = False,
+ ) -> dict[str, Any] | Resource:
"""Create an object.
:param url: a partial URL, e.g., '/servers'
@@ -163,13 +187,20 @@ def _post(self, url, json, response_key=None, return_raw=False):
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
+ if self.resource_class is None:
+ raise ValueError("resource_class must be set")
body = self.client.post(url, json=json).json()
data = body[response_key] if response_key is not None else body
if return_raw:
- return data
+ return cast(dict[str, Any], data)
return self.resource_class(self, data)
- def _put(self, url, json=None, response_key=None):
+ def _put(
+ self,
+ url: str,
+ json: dict[str, Any] | None = None,
+ response_key: str | None = None,
+ ) -> Resource | None:
"""Update an object with PUT method.
:param url: a partial URL, e.g., '/servers'
@@ -179,6 +210,8 @@ def _put(self, url, json=None, response_key=None):
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
+ if self.resource_class is None:
+ raise ValueError("resource_class must be set")
resp = self.client.put(url, json=json)
# PUT requests may not return a body
if resp.content:
@@ -187,8 +220,14 @@ def _put(self, url, json=None, response_key=None):
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
+ return None
- def _patch(self, url, json=None, response_key=None):
+ def _patch(
+ self,
+ url: str,
+ json: dict[str, Any] | None = None,
+ response_key: str | None = None,
+ ) -> Resource:
"""Update an object with PATCH method.
:param url: a partial URL, e.g., '/servers'
@@ -198,13 +237,15 @@ def _patch(self, url, json=None, response_key=None):
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
+ if self.resource_class is None:
+ raise ValueError("resource_class must be set")
body = self.client.patch(url, json=json).json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
- def _delete(self, url):
+ def _delete(self, url: str) -> Any:
"""Delete an object.
:param url: a partial URL, e.g., '/servers/my-server'
@@ -212,26 +253,27 @@ def _delete(self, url):
return self.client.delete(url)
-@six.add_metaclass(abc.ABCMeta)
-class ManagerWithFind(BaseManager):
+class ManagerWithFind(BaseManager, metaclass=abc.ABCMeta):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
- def list(self):
+ def list(self) -> list[Resource]:
pass
- def find(self, **kwargs):
+ def find(self, **kwargs: Any) -> Resource:
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
+ if self.resource_class is None:
+ raise ValueError("resource_class must be set")
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = _("No %(name)s matching %(args)s.") % {
- 'name': self.resource_class.__name__,
- 'args': kwargs
+ "name": self.resource_class.__name__,
+ "args": kwargs,
}
raise exceptions.NotFound(msg)
elif num_matches > 1:
@@ -239,19 +281,22 @@ def find(self, **kwargs):
else:
return matches[0]
- def findall(self, **kwargs):
+ def findall(self, **kwargs: Any) -> Sequence[Resource]:
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
- found = []
+ found: list[Resource] = []
searches = kwargs.items()
- for obj in self.list():
+ items = self.list()
+ for obj in items:
try:
- if all(getattr(obj, attr) == value
- for (attr, value) in searches):
+ if all(
+ getattr(obj, attr) == value
+ for (attr, value) in searches
+ ):
found.append(obj)
except AttributeError:
continue
@@ -272,10 +317,11 @@ class CrudManager(BaseManager):
refer to an individual member of the collection.
"""
- collection_key = None
- key = None
- def build_url(self, base_url=None, **kwargs):
+ collection_key: str | None = None
+ key: str | None = None
+
+ def build_url(self, base_url: str | None = None, **kwargs: Any) -> str:
"""Builds a resource URL for the given kwargs.
Given an example collection where `collection_key = 'entities'` and
@@ -292,18 +338,18 @@ def build_url(self, base_url=None, **kwargs):
:param base_url: if provided, the generated URL will be appended to it
"""
- url = base_url if base_url is not None else ''
+ url = base_url if base_url is not None else ""
- url += '/%s' % self.collection_key
+ url += "/%s" % self.collection_key
# do we have a specific entity?
- entity_id = kwargs.get('%s_id' % self.key)
+ entity_id = kwargs.get("%s_id" % self.key)
if entity_id is not None:
- url += '/%s' % entity_id
+ url += "/%s" % entity_id
return url
- def _filter_kwargs(self, kwargs):
+ def _filter_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
"""Drop null values and handle ids."""
for key, ref in kwargs.copy().items():
if ref is None:
@@ -311,27 +357,33 @@ def _filter_kwargs(self, kwargs):
else:
if isinstance(ref, Resource):
kwargs.pop(key)
- kwargs['%s_id' % key] = getid(ref)
+ kwargs["%s_id" % key] = getid(ref)
return kwargs
- def create(self, **kwargs):
+ def create(self, **kwargs: Any) -> Resource:
+ if self.key is None:
+ raise ValueError("key must be set")
kwargs = self._filter_kwargs(kwargs)
- return self._post(
+ result = self._post(
self.build_url(**kwargs),
{self.key: kwargs},
- self.key)
+ self.key,
+ )
+ if isinstance(result, dict):
+ raise ValueError("Expected Resource, got dict")
+ return result
- def get(self, **kwargs):
+ def get(self, **kwargs: Any) -> Resource:
kwargs = self._filter_kwargs(kwargs)
- return self._get(
- self.build_url(**kwargs),
- self.key)
+ return self._get(self.build_url(**kwargs), self.key)
- def head(self, **kwargs):
+ def head(self, **kwargs: Any) -> bool:
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))
- def list(self, base_url=None, **kwargs):
+ def list(
+ self, base_url: str | None = None, **kwargs: Any
+ ) -> list[Resource]:
"""List the collection.
:param base_url: if provided, the generated URL will be appended to it
@@ -339,13 +391,20 @@ def list(self, base_url=None, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._list(
- '%(base_url)s%(query)s' % {
- 'base_url': self.build_url(base_url=base_url, **kwargs),
- 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
+ "%(base_url)s%(query)s"
+ % {
+ "base_url": self.build_url(base_url=base_url, **kwargs),
+ "query": (
+ "?%s" % urlparse.urlencode(kwargs)
+ if kwargs else ""
+ ),
},
- self.collection_key)
+ self.collection_key,
+ )
- def put(self, base_url=None, **kwargs):
+ def put(
+ self, base_url: str | None = None, **kwargs: Any
+ ) -> Resource | None:
"""Update an element.
:param base_url: if provided, the generated URL will be appended to it
@@ -354,41 +413,47 @@ def put(self, base_url=None, **kwargs):
return self._put(self.build_url(base_url=base_url, **kwargs))
- def update(self, **kwargs):
+ def update(self, **kwargs: Any) -> Resource:
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
- params.pop('%s_id' % self.key)
+ if self.key is None:
+ raise ValueError("key must be set")
+ params.pop("%s_id" % self.key)
return self._patch(
self.build_url(**kwargs),
{self.key: params},
- self.key)
+ self.key,
+ )
- def delete(self, **kwargs):
+ def delete(self, **kwargs: Any) -> Any:
kwargs = self._filter_kwargs(kwargs)
- return self._delete(
- self.build_url(**kwargs))
+ return self._delete(self.build_url(**kwargs))
- def find(self, base_url=None, **kwargs):
+ def find(self, base_url: str | None = None, **kwargs: Any) -> Resource:
"""Find a single item with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
"""
+ if self.resource_class is None:
+ raise ValueError("resource_class must be set")
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
- '%(base_url)s%(query)s' % {
- 'base_url': self.build_url(base_url=base_url, **kwargs),
- 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
+ "%(base_url)s%(query)s"
+ % {
+ "base_url": self.build_url(base_url=base_url, **kwargs),
+ "query": "?%s" % urlparse.urlencode(kwargs) if kwargs else "",
},
- self.collection_key)
+ self.collection_key,
+ )
num = len(rl)
if num == 0:
msg = _("No %(name)s matching %(args)s.") % {
- 'name': self.resource_class.__name__,
- 'args': kwargs
+ "name": self.resource_class.__name__,
+ "args": kwargs,
}
raise exceptions.NotFound(msg)
elif num > 1:
@@ -400,16 +465,18 @@ def find(self, base_url=None, **kwargs):
class Extension(HookableMixin):
"""Extension descriptor."""
- SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
- manager_class = None
+ SUPPORTED_HOOKS: tuple[str, ...] = (
+ "__pre_parse_args__", "__post_parse_args__"
+ )
+ manager_class: type[BaseManager] | None = None
- def __init__(self, name, module):
+ def __init__(self, name: str, module: Any) -> None:
super(Extension, self).__init__()
self.name = name
self.module = module
self._parse_extension_module()
- def _parse_extension_module(self):
+ def _parse_extension_module(self) -> None:
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
@@ -421,7 +488,7 @@ def _parse_extension_module(self):
except TypeError:
pass
- def __repr__(self):
+ def __repr__(self) -> str:
return "" % self.name
@@ -431,10 +498,12 @@ class Resource(object):
This is pretty much just a bag for attributes.
"""
- HUMAN_ID = False
- NAME_ATTR = 'name'
+ HUMAN_ID: bool = False
+ NAME_ATTR: str = "name"
- def __init__(self, manager, info, loaded=False):
+ def __init__(
+ self, manager: BaseManager, info: dict[str, Any], loaded: bool = False
+ ) -> None:
"""Populate and bind to a manager.
:param manager: BaseManager object
@@ -446,24 +515,24 @@ def __init__(self, manager, info, loaded=False):
self._add_details(info)
self._loaded = loaded
- def __repr__(self):
- reprkeys = sorted(k
- for k in self.__dict__.keys()
- if k[0] != '_' and k != 'manager')
+ def __repr__(self) -> str:
+ reprkeys = sorted(
+ k for k in self.__dict__.keys() if k[0] != "_" and k != "manager"
+ )
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
@property
- def human_id(self):
+ def human_id(self) -> str | None:
"""Human-readable ID which can be used for bash completion."""
if self.HUMAN_ID:
name = getattr(self, self.NAME_ATTR, None)
if name is not None:
- return strutils.to_slug(name)
+ return str(strutils.to_slug(name))
return None
- def _add_details(self, info):
- for (k, v) in info.items():
+ def _add_details(self, info: dict[str, Any]) -> None:
+ for k, v in info.items():
try:
setattr(self, k, v)
self._info[k] = v
@@ -471,7 +540,7 @@ def _add_details(self, info):
# In this case we already defined the attribute on the class
pass
- def __getattr__(self, k):
+ def __getattr__(self, k: str) -> Any:
if k not in self.__dict__:
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
@@ -482,7 +551,7 @@ def __getattr__(self, k):
else:
return self.__dict__[k]
- def get(self):
+ def get(self) -> None:
"""Support for lazy loading details.
Some clients, such as novaclient have the option to lazy load the
@@ -490,16 +559,17 @@ def get(self):
"""
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
- if not hasattr(self.manager, 'get'):
+ if not hasattr(self.manager, "get"):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
self._add_details(
- {'x_request_id': self.manager.client.last_request_id})
+ {"x_request_id": self.manager.client.last_request_id}
+ )
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
@@ -507,11 +577,11 @@ def __eq__(self, other):
return False
return self._info == other._info
- def is_loaded(self):
+ def is_loaded(self) -> bool:
return self._loaded
- def set_loaded(self, val):
+ def set_loaded(self, val: bool) -> None:
self._loaded = val
- def to_dict(self):
+ def to_dict(self) -> dict[str, Any]:
return copy.deepcopy(self._info)
diff --git a/ironicclient/common/apiclient/exceptions.py b/ironicclient/common/apiclient/exceptions.py
index 81a3340a2..a0a863ed9 100644
--- a/ironicclient/common/apiclient/exceptions.py
+++ b/ironicclient/common/apiclient/exceptions.py
@@ -20,12 +20,15 @@
Exception definitions.
"""
+from __future__ import annotations
+from http import client as http_client
import inspect
import sys
+from typing import Any
+from typing import cast
-import six
-from six.moves import http_client
+import requests # type: ignore[import-untyped]
from ironicclient.common.i18n import _
@@ -67,7 +70,7 @@ class ConnectionRefused(ConnectionError):
class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
- def __init__(self, opt_names):
+ def __init__(self, opt_names: list[str]) -> None:
super(AuthPluginOptionsMissing, self).__init__(
_("Authentication failed. Missing options: %s") %
", ".join(opt_names))
@@ -76,7 +79,7 @@ def __init__(self, opt_names):
class AuthSystemNotFound(AuthorizationFailure):
"""User has specified an AuthSystem that is not installed."""
- def __init__(self, auth_system):
+ def __init__(self, auth_system: str) -> None:
super(AuthSystemNotFound, self).__init__(
_("AuthSystemNotFound: %r") % auth_system)
self.auth_system = auth_system
@@ -99,7 +102,7 @@ class EndpointNotFound(EndpointException):
class AmbiguousEndpoints(EndpointException):
"""Found more than one matching endpoint in Service Catalog."""
- def __init__(self, endpoints=None):
+ def __init__(self, endpoints: object | None = None) -> None:
super(AmbiguousEndpoints, self).__init__(
_("AmbiguousEndpoints: %r") % endpoints)
self.endpoints = endpoints
@@ -107,12 +110,19 @@ def __init__(self, endpoints=None):
class HttpError(ClientException):
"""The base exception class for all HTTP exceptions."""
- http_status = 0
- message = _("HTTP Error")
-
- def __init__(self, message=None, details=None,
- response=None, request_id=None,
- url=None, method=None, http_status=None):
+ http_status: int = 0
+ message: str = _("HTTP Error")
+
+ def __init__(
+ self,
+ message: str | None = None,
+ details: str | None = None,
+ response: requests.Response | None = None,
+ request_id: str | None = None,
+ url: str | None = None,
+ method: str | None = None,
+ http_status: int | None = None,
+ ) -> None:
self.http_status = http_status or self.http_status
self.message = message or self.message
self.details = details
@@ -128,7 +138,7 @@ def __init__(self, message=None, details=None,
class HTTPRedirection(HttpError):
"""HTTP Redirection."""
- message = _("HTTP Redirection")
+ message: str = _("HTTP Redirection")
class HTTPClientError(HttpError):
@@ -136,7 +146,7 @@ class HTTPClientError(HttpError):
Exception for cases in which the client seems to have erred.
"""
- message = _("HTTP Client Error")
+ message: str = _("HTTP Client Error")
class HttpServerError(HttpError):
@@ -145,7 +155,7 @@ class HttpServerError(HttpError):
Exception for cases in which the server is aware that it has
erred or is incapable of performing the request.
"""
- message = _("HTTP Server Error")
+ message: str = _("HTTP Server Error")
class MultipleChoices(HTTPRedirection):
@@ -154,8 +164,8 @@ class MultipleChoices(HTTPRedirection):
Indicates multiple options for the resource that the client may follow.
"""
- http_status = http_client.MULTIPLE_CHOICES
- message = _("Multiple Choices")
+ http_status: int = http_client.MULTIPLE_CHOICES
+ message: str = _("Multiple Choices")
class BadRequest(HTTPClientError):
@@ -163,8 +173,8 @@ class BadRequest(HTTPClientError):
The request cannot be fulfilled due to bad syntax.
"""
- http_status = http_client.BAD_REQUEST
- message = _("Bad Request")
+ http_status: int = http_client.BAD_REQUEST
+ message: str = _("Bad Request")
class Unauthorized(HTTPClientError):
@@ -173,8 +183,8 @@ class Unauthorized(HTTPClientError):
Similar to 403 Forbidden, but specifically for use when authentication
is required and has failed or has not yet been provided.
"""
- http_status = http_client.UNAUTHORIZED
- message = _("Unauthorized")
+ http_status: int = http_client.UNAUTHORIZED
+ message: str = _("Unauthorized")
class PaymentRequired(HTTPClientError):
@@ -182,8 +192,8 @@ class PaymentRequired(HTTPClientError):
Reserved for future use.
"""
- http_status = http_client.PAYMENT_REQUIRED
- message = _("Payment Required")
+ http_status: int = http_client.PAYMENT_REQUIRED
+ message: str = _("Payment Required")
class Forbidden(HTTPClientError):
@@ -192,8 +202,8 @@ class Forbidden(HTTPClientError):
The request was a valid request, but the server is refusing to respond
to it.
"""
- http_status = http_client.FORBIDDEN
- message = _("Forbidden")
+ http_status: int = http_client.FORBIDDEN
+ message: str = _("Forbidden")
class NotFound(HTTPClientError):
@@ -202,8 +212,8 @@ class NotFound(HTTPClientError):
The requested resource could not be found but may be available again
in the future.
"""
- http_status = http_client.NOT_FOUND
- message = _("Not Found")
+ http_status: int = http_client.NOT_FOUND
+ message: str = _("Not Found")
class MethodNotAllowed(HTTPClientError):
@@ -212,8 +222,8 @@ class MethodNotAllowed(HTTPClientError):
A request was made of a resource using a request method not supported
by that resource.
"""
- http_status = http_client.METHOD_NOT_ALLOWED
- message = _("Method Not Allowed")
+ http_status: int = http_client.METHOD_NOT_ALLOWED
+ message: str = _("Method Not Allowed")
class NotAcceptable(HTTPClientError):
@@ -222,8 +232,8 @@ class NotAcceptable(HTTPClientError):
The requested resource is only capable of generating content not
acceptable according to the Accept headers sent in the request.
"""
- http_status = http_client.NOT_ACCEPTABLE
- message = _("Not Acceptable")
+ http_status: int = http_client.NOT_ACCEPTABLE
+ message: str = _("Not Acceptable")
class ProxyAuthenticationRequired(HTTPClientError):
@@ -231,8 +241,8 @@ class ProxyAuthenticationRequired(HTTPClientError):
The client must first authenticate itself with the proxy.
"""
- http_status = http_client.PROXY_AUTHENTICATION_REQUIRED
- message = _("Proxy Authentication Required")
+ http_status: int = http_client.PROXY_AUTHENTICATION_REQUIRED
+ message: str = _("Proxy Authentication Required")
class RequestTimeout(HTTPClientError):
@@ -240,8 +250,8 @@ class RequestTimeout(HTTPClientError):
The server timed out waiting for the request.
"""
- http_status = http_client.REQUEST_TIMEOUT
- message = _("Request Timeout")
+ http_status: int = http_client.REQUEST_TIMEOUT
+ message: str = _("Request Timeout")
class Conflict(HTTPClientError):
@@ -250,8 +260,8 @@ class Conflict(HTTPClientError):
Indicates that the request could not be processed because of conflict
in the request, such as an edit conflict.
"""
- http_status = http_client.CONFLICT
- message = _("Conflict")
+ http_status: int = http_client.CONFLICT
+ message: str = _("Conflict")
class Gone(HTTPClientError):
@@ -260,8 +270,8 @@ class Gone(HTTPClientError):
Indicates that the resource requested is no longer available and will
not be available again.
"""
- http_status = http_client.GONE
- message = _("Gone")
+ http_status: int = http_client.GONE
+ message: str = _("Gone")
class LengthRequired(HTTPClientError):
@@ -270,8 +280,8 @@ class LengthRequired(HTTPClientError):
The request did not specify the length of its content, which is
required by the requested resource.
"""
- http_status = http_client.LENGTH_REQUIRED
- message = _("Length Required")
+ http_status: int = http_client.LENGTH_REQUIRED
+ message: str = _("Length Required")
class PreconditionFailed(HTTPClientError):
@@ -280,8 +290,8 @@ class PreconditionFailed(HTTPClientError):
The server does not meet one of the preconditions that the requester
put on the request.
"""
- http_status = http_client.PRECONDITION_FAILED
- message = _("Precondition Failed")
+ http_status: int = http_client.PRECONDITION_FAILED
+ message: str = _("Precondition Failed")
class RequestEntityTooLarge(HTTPClientError):
@@ -289,10 +299,10 @@ class RequestEntityTooLarge(HTTPClientError):
The request is larger than the server is willing or able to process.
"""
- http_status = http_client.REQUEST_ENTITY_TOO_LARGE
- message = _("Request Entity Too Large")
+ http_status: int = http_client.REQUEST_ENTITY_TOO_LARGE
+ message: str = _("Request Entity Too Large")
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
try:
self.retry_after = int(kwargs.pop('retry_after'))
except (KeyError, ValueError):
@@ -306,8 +316,8 @@ class RequestUriTooLong(HTTPClientError):
The URI provided was too long for the server to process.
"""
- http_status = http_client.REQUEST_URI_TOO_LONG
- message = _("Request-URI Too Long")
+ http_status: int = http_client.REQUEST_URI_TOO_LONG
+ message: str = _("Request-URI Too Long")
class UnsupportedMediaType(HTTPClientError):
@@ -316,8 +326,8 @@ class UnsupportedMediaType(HTTPClientError):
The request entity has a media type which the server or resource does
not support.
"""
- http_status = http_client.UNSUPPORTED_MEDIA_TYPE
- message = _("Unsupported Media Type")
+ http_status: int = http_client.UNSUPPORTED_MEDIA_TYPE
+ message: str = _("Unsupported Media Type")
class RequestedRangeNotSatisfiable(HTTPClientError):
@@ -326,8 +336,8 @@ class RequestedRangeNotSatisfiable(HTTPClientError):
The client has asked for a portion of the file, but the server cannot
supply that portion.
"""
- http_status = http_client.REQUESTED_RANGE_NOT_SATISFIABLE
- message = _("Requested Range Not Satisfiable")
+ http_status: int = http_client.REQUESTED_RANGE_NOT_SATISFIABLE
+ message: str = _("Requested Range Not Satisfiable")
class ExpectationFailed(HTTPClientError):
@@ -335,8 +345,8 @@ class ExpectationFailed(HTTPClientError):
The server cannot meet the requirements of the Expect request-header field.
"""
- http_status = http_client.EXPECTATION_FAILED
- message = _("Expectation Failed")
+ http_status: int = http_client.EXPECTATION_FAILED
+ message: str = _("Expectation Failed")
class UnprocessableEntity(HTTPClientError):
@@ -345,8 +355,8 @@ class UnprocessableEntity(HTTPClientError):
The request was well-formed but was unable to be followed due to semantic
errors.
"""
- http_status = http_client.UNPROCESSABLE_ENTITY
- message = _("Unprocessable Entity")
+ http_status: int = http_client.UNPROCESSABLE_ENTITY
+ message: str = _("Unprocessable Entity")
class InternalServerError(HttpServerError):
@@ -354,8 +364,8 @@ class InternalServerError(HttpServerError):
A generic error message, given when no more specific message is suitable.
"""
- http_status = http_client.INTERNAL_SERVER_ERROR
- message = _("Internal Server Error")
+ http_status: int = http_client.INTERNAL_SERVER_ERROR
+ message: str = _("Internal Server Error")
# NotImplemented is a python keyword.
@@ -365,8 +375,8 @@ class HttpNotImplemented(HttpServerError):
The server either does not recognize the request method, or it lacks
the ability to fulfill the request.
"""
- http_status = http_client.NOT_IMPLEMENTED
- message = _("Not Implemented")
+ http_status: int = http_client.NOT_IMPLEMENTED
+ message: str = _("Not Implemented")
class BadGateway(HttpServerError):
@@ -375,8 +385,8 @@ class BadGateway(HttpServerError):
The server was acting as a gateway or proxy and received an invalid
response from the upstream server.
"""
- http_status = http_client.BAD_GATEWAY
- message = _("Bad Gateway")
+ http_status: int = http_client.BAD_GATEWAY
+ message: str = _("Bad Gateway")
class ServiceUnavailable(HttpServerError):
@@ -384,8 +394,8 @@ class ServiceUnavailable(HttpServerError):
The server is currently unavailable.
"""
- http_status = http_client.SERVICE_UNAVAILABLE
- message = _("Service Unavailable")
+ http_status: int = http_client.SERVICE_UNAVAILABLE
+ message: str = _("Service Unavailable")
class GatewayTimeout(HttpServerError):
@@ -394,8 +404,8 @@ class GatewayTimeout(HttpServerError):
The server was acting as a gateway or proxy and did not receive a timely
response from the upstream server.
"""
- http_status = http_client.GATEWAY_TIMEOUT
- message = _("Gateway Timeout")
+ http_status: int = http_client.GATEWAY_TIMEOUT
+ message: str = _("Gateway Timeout")
class HttpVersionNotSupported(HttpServerError):
@@ -403,19 +413,21 @@ class HttpVersionNotSupported(HttpServerError):
The server does not support the HTTP protocol version used in the request.
"""
- http_status = http_client.HTTP_VERSION_NOT_SUPPORTED
- message = _("HTTP Version Not Supported")
+ http_status: int = http_client.HTTP_VERSION_NOT_SUPPORTED
+ message: str = _("HTTP Version Not Supported")
# _code_map contains all the classes that have http_status attribute.
-_code_map = dict(
- (getattr(obj, 'http_status', None), obj)
+_code_map: dict[int, type[HttpError]] = {
+ cast(int, getattr(obj, 'http_status')): cast(type[HttpError], obj)
for name, obj in vars(sys.modules[__name__]).items()
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
-)
+}
-def from_response(response, method, url):
+def from_response(
+ response: requests.Response, method: str, url: str
+) -> HttpError:
"""Returns an instance of :class:`HttpError` or subclass based on response.
:param response: instance of `requests.Response` class
@@ -427,7 +439,7 @@ def from_response(response, method, url):
# NOTE(hdd) true for older versions of nova and cinder
if not req_id:
req_id = response.headers.get("x-compute-request-id")
- kwargs = {
+ kwargs: dict[str, Any] = {
"http_status": response.status_code,
"response": response,
"method": method,
@@ -447,13 +459,14 @@ def from_response(response, method, url):
if isinstance(body, dict):
error = body.get(list(body)[0])
if isinstance(error, dict):
- kwargs["message"] = (error.get("message") or
- error.get("faultstring"))
- kwargs["details"] = (error.get("details") or
- six.text_type(body))
+ kwargs["message"] = (error.get("message")
+ or error.get("faultstring"))
+ kwargs["details"] = (error.get("details")
+ or str(body))
elif content_type.startswith("text/"):
kwargs["details"] = getattr(response, 'text', '')
+ cls: type[HttpError]
try:
cls = _code_map[response.status_code]
except KeyError:
@@ -461,8 +474,8 @@ def from_response(response, method, url):
if response.status_code >= http_client.INTERNAL_SERVER_ERROR:
cls = HttpServerError
# 4XX status codes are client request errors
- elif (http_client.BAD_REQUEST <= response.status_code <
- http_client.INTERNAL_SERVER_ERROR):
+ elif (http_client.BAD_REQUEST <= response.status_code
+ < http_client.INTERNAL_SERVER_ERROR):
cls = HTTPClientError
else:
cls = HttpError
diff --git a/ironicclient/common/base.py b/ironicclient/common/base.py
index 294f06ac7..cc3cb7030 100644
--- a/ironicclient/common/base.py
+++ b/ironicclient/common/base.py
@@ -19,9 +19,7 @@
import abc
import copy
-import six
-
-import six.moves.urllib.parse as urlparse
+from urllib import parse as urlparse
from ironicclient.common.apiclient import base
from ironicclient import exc
@@ -39,8 +37,7 @@ def getid(obj):
return obj
-@six.add_metaclass(abc.ABCMeta)
-class Manager(object):
+class Manager(object, metaclass=abc.ABCMeta):
"""Provides CRUD operations with a particular API."""
def __init__(self, api):
@@ -55,23 +52,30 @@ def _path(self, resource_id=None):
return ('/v1/%s/%s' % (self._resource_name, resource_id)
if resource_id else '/v1/%s' % self._resource_name)
- @abc.abstractproperty
+ @property
+ @abc.abstractmethod
def resource_class(self):
"""The resource class
"""
- @abc.abstractproperty
+ @property
+ @abc.abstractmethod
def _resource_name(self):
"""The resource name.
"""
- def _get(self, resource_id, fields=None):
+ def _get(self, resource_id, fields=None, os_ironic_api_version=None,
+ global_request_id=None):
"""Retrieve a resource.
:param resource_id: Identifier of the resource.
:param fields: List of specific fields to be returned.
+ :param os_ironic_api_version: String version (e.g. "1.35") to use for
+ the request. If not specified, the client's default is used.
+ :param global_request_id: String containing global request ID header
+ value (in form "req-") to use for the request.
:raises exc.ValidationError: For invalid resource_id arg value.
"""
@@ -85,19 +89,29 @@ def _get(self, resource_id, fields=None):
resource_id += ','.join(fields)
try:
- return self._list(self._path(resource_id))[0]
+ return self._list(
+ self._path(resource_id),
+ os_ironic_api_version=os_ironic_api_version,
+ global_request_id=global_request_id)[0]
except IndexError:
return None
- def _get_as_dict(self, resource_id, fields=None):
+ def _get_as_dict(self, resource_id, fields=None,
+ os_ironic_api_version=None, global_request_id=None):
"""Retrieve a resource as a dictionary
:param resource_id: Identifier of the resource.
:param fields: List of specific fields to be returned.
+ :param os_ironic_api_version: String version (e.g. "1.35") to use for
+ the request. If not specified, the client's default is used.
+ :param global_request_id: String containing global request ID header
+ value (in form "req-") to use for the request.
:returns: a dictionary representing the resource; may be empty
"""
- resource = self._get(resource_id, fields=fields)
+ resource = self._get(resource_id, fields=fields,
+ os_ironic_api_version=os_ironic_api_version,
+ global_request_id=global_request_id)
if resource:
return resource.to_dict()
else:
@@ -118,7 +132,8 @@ def _format_body_data(self, body, response_key):
return data
def _list_pagination(self, url, response_key=None, obj_class=None,
- limit=None):
+ limit=None, os_ironic_api_version=None,
+ global_request_id=None):
"""Retrieve a list of items.
The Ironic API is configured to return a maximum number of
@@ -134,19 +149,42 @@ def _list_pagination(self, url, response_key=None, obj_class=None,
:param obj_class: class for constructing the returned objects.
:param limit: maximum number of items to return. If None returns
everything.
-
+ :param os_ironic_api_version: String version (e.g. "1.35") to use for
+ the request. If not specified, the client's default is used.
+ :param global_request_id: String containing global request ID header
+ value (in form "req-") to use for the request.
"""
if obj_class is None:
obj_class = self.resource_class
if limit is not None:
limit = int(limit)
+ kwargs = {"headers": {}}
+ if os_ironic_api_version is not None:
+ kwargs['headers'][
+ 'X-OpenStack-Ironic-API-Version'] = os_ironic_api_version
+ if global_request_id is not None:
+ kwargs["headers"]["X-Openstack-Request-Id"] = global_request_id
+
+ # NOTE(jroll)
+ # endpoint_trimmed is what is prepended if we only pass a path
+ # to json_request. This might be something like
+ # https://example.com:4443/baremetal
+ # The code below which parses the 'next' URL keeps any path prefix
+ # we're using, so it ends up calling json_request() with e.g.
+ # /baremetal/v1/nodes. When this is appended to endpoint_trimmed,
+ # we end up with a full URL of e.g.
+ # https://example.com:4443/baremetal/baremetal/v1/nodes.
+ # Parse out the prefix from the base endpoint here, so we can strip
+ # it below and make sure we're passing a correct path.
+ endpoint_parts = urlparse.urlparse(self.api.endpoint_trimmed)
+ url_path_prefix = endpoint_parts[2]
object_list = []
object_count = 0
limit_reached = False
while url:
- resp, body = self.api.json_request('GET', url)
+ resp, body = self.api.json_request('GET', url, **kwargs)
data = self._format_body_data(body, response_key)
for obj in data:
object_list.append(obj_class(self, obj, loaded=True))
@@ -166,56 +204,114 @@ def _list_pagination(self, url, response_key=None, obj_class=None,
# the scheme and netloc
url_parts = list(urlparse.urlparse(url))
url_parts[0] = url_parts[1] = ''
+ if url_path_prefix:
+ url_parts[2] = url_parts[2].replace(
+ url_path_prefix, '', 1)
url = urlparse.urlunparse(url_parts)
return object_list
- def _list(self, url, response_key=None, obj_class=None, body=None):
- resp, body = self.api.json_request('GET', url)
+ def __list(self, url, response_key=None, body=None,
+ os_ironic_api_version=None, global_request_id=None):
+ kwargs = {"headers": {}}
+
+ if os_ironic_api_version is not None:
+ kwargs['headers'][
+ 'X-OpenStack-Ironic-API-Version'] = os_ironic_api_version
+ if global_request_id is not None:
+ kwargs["headers"]["X-Openstack-Request-Id"] = global_request_id
+ resp, body = self.api.json_request('GET', url, **kwargs)
+
+ data = self._format_body_data(body, response_key)
+ return data
+
+ def _list(self, url, response_key=None, obj_class=None, body=None,
+ os_ironic_api_version=None, global_request_id=None):
if obj_class is None:
obj_class = self.resource_class
- data = self._format_body_data(body, response_key)
+ data = self.__list(url, response_key=response_key, body=body,
+ os_ironic_api_version=os_ironic_api_version,
+ global_request_id=global_request_id)
return [obj_class(self, res, loaded=True) for res in data if res]
- def _update(self, resource_id, patch, method='PATCH'):
+ def _list_primitives(self, url, response_key=None,
+ os_ironic_api_version=None, global_request_id=None):
+ return self.__list(url, response_key=response_key,
+ os_ironic_api_version=os_ironic_api_version,
+ global_request_id=global_request_id)
+
+ def _update(self, resource_id, patch, method='PATCH',
+ os_ironic_api_version=None, global_request_id=None,
+ params=None):
"""Update a resource.
:param resource_id: Resource identifier.
- :param patch: New version of a given resource.
+ :param patch: New version of a given resource, a dictionary or None.
:param method: Name of the method for the request.
+ :param os_ironic_api_version: String version (e.g. "1.35") to use for
+ the request. If not specified, the client's default is used.
+ :param global_request_id: String containing global request ID header
+ value (in form "req-") to use for the request.
+ :param params: query parameters to pass.
"""
url = self._path(resource_id)
- resp, body = self.api.json_request(method, url, body=patch)
+ kwargs = {"headers": {}}
+ if patch is not None:
+ kwargs['body'] = patch
+ if os_ironic_api_version is not None:
+ kwargs['headers'][
+ 'X-OpenStack-Ironic-API-Version'] = os_ironic_api_version
+ if global_request_id is not None:
+ kwargs["headers"]["X-Openstack-Request-Id"] = global_request_id
+ if params:
+ kwargs['params'] = params
+ resp, body = self.api.json_request(method, url, **kwargs)
# PATCH/PUT requests may not return a body
if body:
return self.resource_class(self, body)
- def _delete(self, resource_id):
+ def _delete(self, resource_id,
+ os_ironic_api_version=None, global_request_id=None):
"""Delete a resource.
:param resource_id: Resource identifier.
+ :param os_ironic_api_version: String version (e.g. "1.35") to use for
+ the request. If not specified, the client's default is used.
+ :param global_request_id: String containing global request ID header
+ value (in form "req-") to use for the request.
"""
- self.api.raw_request('DELETE', self._path(resource_id))
+ headers = {}
+ if os_ironic_api_version is not None:
+ headers["X-OpenStack-Ironic-API-Version"] = os_ironic_api_version
+ if global_request_id is not None:
+ headers["X-Openstack-Request-Id"] = global_request_id
+ self.api.raw_request('DELETE', self._path(resource_id),
+ headers=headers)
-@six.add_metaclass(abc.ABCMeta)
-class CreateManager(Manager):
+class CreateManager(Manager, metaclass=abc.ABCMeta):
"""Provides creation operations with a particular API."""
- @abc.abstractproperty
+ @property
+ @abc.abstractmethod
def _creation_attributes(self):
"""A list of required creation attributes for a resource type.
"""
- def create(self, **kwargs):
+ def create(self, os_ironic_api_version=None, global_request_id=None,
+ **kwargs):
"""Create a resource based on a kwargs dictionary of attributes.
:param kwargs: A dictionary containing the attributes of the resource
that will be created.
+ :param os_ironic_api_version: String version (e.g. "1.35") to use for
+ the request. If not specified, the client's default is used.
+ :param global_request_id: String containing global request ID header
+ value (in form "req-") to use for the request.
:raises exc.InvalidAttribute: For invalid attributes that are not
needed to create the resource.
"""
@@ -233,8 +329,14 @@ def create(self, **kwargs):
'needed to create %(resource)s.' %
{'resource': self._resource_name,
'attrs': '","'.join(invalid)})
+ headers = {}
+ if os_ironic_api_version is not None:
+ headers['X-OpenStack-Ironic-API-Version'] = os_ironic_api_version
+ if global_request_id is not None:
+ headers["X-Openstack-Request-Id"] = global_request_id
url = self._path()
- resp, body = self.api.json_request('POST', url, body=new)
+ resp, body = self.api.json_request('POST', url, body=new,
+ headers=headers)
if body:
return self.resource_class(self, body)
diff --git a/ironicclient/common/cliutils.py b/ironicclient/common/cliutils.py
deleted file mode 100644
index ce396a08f..000000000
--- a/ironicclient/common/cliutils.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# Copyright 2012 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.
-
-# W0603: Using the global statement
-# W0621: Redefining name %s from outer scope
-# pylint: disable=W0603,W0621
-
-from __future__ import print_function
-
-import getpass
-import inspect
-import json
-import os
-import sys
-import textwrap
-
-from oslo_utils import encodeutils
-from oslo_utils import strutils
-import prettytable
-import six
-from six import moves
-
-from ironicclient.common.i18n import _
-
-
-class MissingArgs(Exception):
- """Supplied arguments are not sufficient for calling a function."""
- def __init__(self, missing):
- self.missing = missing
- msg = _("Missing arguments: %s") % ", ".join(missing)
- super(MissingArgs, self).__init__(msg)
-
-
-def validate_args(fn, *args, **kwargs):
- """Check that the supplied args are sufficient for calling a function.
-
- >>> validate_args(lambda a: None)
- Traceback (most recent call last):
- ...
- MissingArgs: Missing argument(s): a
- >>> validate_args(lambda a, b, c, d: None, 0, c=1)
- Traceback (most recent call last):
- ...
- MissingArgs: Missing argument(s): b, d
-
- :param fn: the function to check
- :param args: the positional arguments supplied
- :param kwargs: the keyword arguments supplied
- """
- argspec = inspect.getargspec(fn)
-
- num_defaults = len(argspec.defaults or [])
- required_args = argspec.args[:len(argspec.args) - num_defaults]
-
- def isbound(method):
- return getattr(method, '__self__', None) is not None
-
- if isbound(fn):
- required_args.pop(0)
-
- missing = [arg for arg in required_args if arg not in kwargs]
- missing = missing[len(args):]
- if missing:
- raise MissingArgs(missing)
-
-
-def arg(*args, **kwargs):
- """Decorator for CLI args.
-
- Example:
-
- >>> @arg("name", help="Name of the new entity")
- ... def entity_create(args):
- ... pass
- """
- def _decorator(func):
- add_arg(func, *args, **kwargs)
- return func
- return _decorator
-
-
-def env(*args, **kwargs):
- """Returns the first environment variable set.
-
- If all are empty, defaults to '' or keyword arg `default`.
- """
- for arg in args:
- value = os.environ.get(arg)
- if value:
- return value
- return kwargs.get('default', '')
-
-
-def add_arg(func, *args, **kwargs):
- """Bind CLI arguments to a shell.py `do_foo` function."""
-
- if not hasattr(func, 'arguments'):
- func.arguments = []
-
- # NOTE(sirp): avoid dups that can occur when the module is shared across
- # tests.
- if (args, kwargs) not in func.arguments:
- # Because of the semantics of decorator composition if we just append
- # to the options list positional options will appear to be backwards.
- func.arguments.insert(0, (args, kwargs))
-
-
-def unauthenticated(func):
- """Adds 'unauthenticated' attribute to decorated function.
-
- Usage:
-
- >>> @unauthenticated
- ... def mymethod(f):
- ... pass
- """
- func.unauthenticated = True
- return func
-
-
-def isunauthenticated(func):
- """Checks if the function does not require authentication.
-
- Mark such functions with the `@unauthenticated` decorator.
-
- :returns: bool
- """
- return getattr(func, 'unauthenticated', False)
-
-
-def print_list(objs, fields, formatters=None, sortby_index=0,
- mixed_case_fields=None, field_labels=None, json_flag=False):
- """Print a list of objects or dict as a table, one row per object or dict.
-
- :param objs: iterable of :class:`Resource`
- :param fields: attributes that correspond to columns, in order
- :param formatters: `dict` of callables for field formatting
- :param sortby_index: index of the field for sorting table rows
- :param mixed_case_fields: fields corresponding to object attributes that
- have mixed case names (e.g., 'serverId')
- :param field_labels: Labels to use in the heading of the table, default to
- fields.
- :param json_flag: print the list as JSON instead of table
- """
- def _get_name_and_data(field):
- if field in formatters:
- # The value of the field has to be modified.
- # For example, it can be used to add extra fields.
- return (field, formatters[field](o))
-
- field_name = field.replace(' ', '_')
- if field not in mixed_case_fields:
- field_name = field.lower()
- if isinstance(o, dict):
- data = o.get(field_name, '')
- else:
- data = getattr(o, field_name, '')
- return (field_name, data)
-
- formatters = formatters or {}
- mixed_case_fields = mixed_case_fields or []
- field_labels = field_labels or fields
- if len(field_labels) != len(fields):
- raise ValueError(_("Field labels list %(labels)s has different number "
- "of elements than fields list %(fields)s"),
- {'labels': field_labels, 'fields': fields})
-
- if sortby_index is None:
- kwargs = {}
- else:
- kwargs = {'sortby': field_labels[sortby_index]}
- pt = prettytable.PrettyTable(field_labels)
- pt.align = 'l'
-
- json_array = []
-
- for o in objs:
- row = []
- for field in fields:
- row.append(_get_name_and_data(field))
- if json_flag:
- json_array.append(dict(row))
- else:
- pt.add_row([r[1] for r in row])
-
- if json_flag:
- print(json.dumps(json_array, indent=4, separators=(',', ': ')))
- elif six.PY3:
- print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
- else:
- print(encodeutils.safe_encode(pt.get_string(**kwargs)))
-
-
-def print_dict(dct, dict_property="Property", wrap=0, dict_value='Value',
- json_flag=False):
- """Print a `dict` as a table of two columns.
-
- :param dct: `dict` to print
- :param dict_property: name of the first column
- :param wrap: wrapping for the second column
- :param dict_value: header label for the value (second) column
- :param json_flag: print `dict` as JSON instead of table
- """
- if json_flag:
- print(json.dumps(dct, indent=4, separators=(',', ': ')))
- return
- pt = prettytable.PrettyTable([dict_property, dict_value])
- pt.align = 'l'
- for k, v in sorted(dct.items()):
- # convert dict to str to check length
- if isinstance(v, dict):
- v = six.text_type(v)
- if wrap > 0:
- v = textwrap.fill(six.text_type(v), wrap)
- # if value has a newline, add in multiple rows
- # e.g. fault with stacktrace
- if v and isinstance(v, six.string_types) and r'\n' in v:
- lines = v.strip().split(r'\n')
- col1 = k
- for line in lines:
- pt.add_row([col1, line])
- col1 = ''
- else:
- pt.add_row([k, v])
-
- if six.PY3:
- print(encodeutils.safe_encode(pt.get_string()).decode())
- else:
- print(encodeutils.safe_encode(pt.get_string()))
-
-
-def get_password(max_password_prompts=3):
- """Read password from TTY."""
- verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
- pw = None
- if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
- # Check for Ctrl-D
- try:
- for __ in moves.range(max_password_prompts):
- pw1 = getpass.getpass("OS Password: ")
- if verify:
- pw2 = getpass.getpass("Please verify: ")
- else:
- pw2 = pw1
- if pw1 == pw2 and pw1:
- pw = pw1
- break
- except EOFError:
- pass
- return pw
-
-
-def service_type(stype):
- """Adds 'service_type' attribute to decorated function.
-
- Usage:
-
- .. code-block:: python
-
- @service_type('volume')
- def mymethod(f):
- ...
- """
- def inner(f):
- f.service_type = stype
- return f
- return inner
-
-
-def get_service_type(f):
- """Retrieves service type from function."""
- return getattr(f, 'service_type', None)
-
-
-def pretty_choice_list(l):
- return ', '.join("'%s'" % i for i in l)
-
-
-def exit(msg=''):
- if msg:
- print(msg, file=sys.stderr)
- sys.exit(1)
diff --git a/ironicclient/common/filecache.py b/ironicclient/common/filecache.py
index d4fdf348d..0efd01921 100644
--- a/ironicclient/common/filecache.py
+++ b/ironicclient/common/filecache.py
@@ -14,26 +14,29 @@
# License for the specific language governing permissions and limitations
# under the License.
+from __future__ import annotations
+
import logging
import os
+from typing import cast
-import appdirs
import dogpile.cache
+import platformdirs
LOG = logging.getLogger(__name__)
-AUTHOR = 'openstack'
-PROGNAME = 'python-ironicclient'
+AUTHOR: str = 'openstack'
+PROGNAME: str = 'python-ironicclient'
-CACHE = None
-CACHE_DIR = appdirs.user_cache_dir(PROGNAME, AUTHOR)
-CACHE_EXPIRY_ENV_VAR = 'IRONICCLIENT_CACHE_EXPIRY' # environment variable
-CACHE_FILENAME = os.path.join(CACHE_DIR, 'ironic-api-version.dbm')
-DEFAULT_EXPIRY = 300 # seconds
+CACHE: dogpile.cache.CacheRegion | None = None
+CACHE_DIR: str = platformdirs.user_cache_dir(PROGNAME, AUTHOR)
+CACHE_EXPIRY_ENV_VAR: str = 'IRONICCLIENT_CACHE_EXPIRY' # environment variable
+CACHE_FILENAME: str = os.path.join(CACHE_DIR, 'ironic-api-version.dbm')
+DEFAULT_EXPIRY: int = 300 # seconds
-def _get_cache():
+def _get_cache() -> dogpile.cache.CacheRegion:
"""Configure file caching."""
global CACHE
if CACHE is None:
@@ -65,12 +68,12 @@ def _get_cache():
return CACHE
-def _build_key(host, port):
+def _build_key(host: str, port: str | int) -> str:
"""Build a key based upon the hostname or address supplied."""
return "%s:%s" % (host, port)
-def save_data(host, port, data):
+def save_data(host: str, port: str | int, data: str) -> None:
"""Save 'data' for a particular 'host' in the appropriate cache dir.
param host: The host that we need to save data for
@@ -81,7 +84,9 @@ def save_data(host, port, data):
_get_cache().set(key, data)
-def retrieve_data(host, port, expiry=None):
+def retrieve_data(
+ host: str, port: str | int, expiry: int | None = None
+) -> str | None:
"""Retrieve the version stored for an ironic 'host', if it's not stale.
Check to see if there is valid cached data for the host/port
@@ -100,4 +105,4 @@ def retrieve_data(host, port, expiry=None):
if data == dogpile.cache.api.NO_VALUE:
return None
- return data
+ return cast(str, data)
diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py
index 0c6de897f..feba1405e 100644
--- a/ironicclient/common/http.py
+++ b/ironicclient/common/http.py
@@ -13,25 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
-import copy
-from distutils.version import StrictVersion
import functools
-import hashlib
+from http import client as http_client
+import json
import logging
-import os
-import socket
-import ssl
+import re
import textwrap
+import threading
import time
+from urllib import parse as urlparse
from keystoneauth1 import adapter
from keystoneauth1 import exceptions as kexc
-from oslo_serialization import jsonutils
-from oslo_utils import strutils
-import requests
-import six
-from six.moves import http_client
-import six.moves.urllib.parse as urlparse
from ironicclient.common import filecache
from ironicclient.common.i18n import _
@@ -44,48 +37,79 @@
# http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa
# for full details.
DEFAULT_VER = '1.9'
-
+LAST_KNOWN_API_VERSION = 109
+LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION)
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-ironicclient'
CHUNKSIZE = 1024 * 64 # 64kB
-API_VERSION = '/v1'
+_MAJOR_VERSION = 1
+API_VERSION = '/v%d' % _MAJOR_VERSION
API_VERSION_SELECTED_STATES = ('user', 'negotiated', 'cached', 'default')
-
DEFAULT_MAX_RETRIES = 5
DEFAULT_RETRY_INTERVAL = 2
SENSITIVE_HEADERS = ('X-Auth-Token',)
-
SUPPORTED_ENDPOINT_SCHEME = ('http', 'https')
+_API_VERSION_RE = re.compile(r'/+(v%d)?/*$' % _MAJOR_VERSION)
+
+
+@functools.total_ordering
+class _Version:
+ _version_re = re.compile(r'^(\d) \. (\d+)$', re.VERBOSE | re.ASCII)
+
+ def __init__(self, version):
+ match = self._version_re.match(version)
+ if not match:
+ raise ValueError('invalid version number %s' % version)
+ major, minor = match.group(1, 2)
+ self.version = (int(major), int(minor))
+
+ def __str__(self):
+ return '.'.join(str(v) for v in self.version)
+
+ def __eq__(self, other):
+ return self.version == other.version
+
+ def __lt__(self, other):
+ return self.version < other.version
+
def _trim_endpoint_api_version(url):
"""Trim API version and trailing slash from endpoint."""
- return url.rstrip('/').rstrip(API_VERSION)
+ return re.sub(_API_VERSION_RE, '', url)
def _extract_error_json(body):
"""Return error_message from the HTTP response body."""
- error_json = {}
try:
- body_json = jsonutils.loads(body)
- if 'error_message' in body_json:
- raw_msg = body_json['error_message']
- error_json = jsonutils.loads(raw_msg)
+ body_json = json.loads(body)
except ValueError:
- pass
+ return {}
+
+ if 'error_message' not in body_json:
+ return {}
+
+ try:
+ error_json = json.loads(body_json['error_message'])
+ except ValueError:
+ return body_json
+
+ err_msg = (error_json.get('faultstring') or error_json.get('description'))
+ if err_msg:
+ body_json['error_message'] = err_msg
- return error_json
+ return body_json
-def get_server(endpoint):
- """Extract and return the server & port that we're connecting to."""
- if endpoint is None:
+def get_server(url):
+ """Extract and return the server & port."""
+ if url is None:
return None, None
- parts = urlparse.urlparse(endpoint)
+ parts = urlparse.urlparse(url)
return parts.hostname, str(parts.port)
@@ -95,9 +119,50 @@ def negotiate_version(self, conn, resp):
Assumption: Called after receiving a 406 error when doing a request.
- param conn: A connection object
- param resp: The response object from http request
+ :param conn: A connection object
+ :param resp: The response object from http request
"""
+ def _query_server(conn):
+ if (self.os_ironic_api_version
+ and not isinstance(self.os_ironic_api_version, list)
+ and self.os_ironic_api_version != 'latest'):
+ base_version = ("/v%s" %
+ str(self.os_ironic_api_version).split('.')[0])
+ else:
+ base_version = API_VERSION
+ # Raise exception on client or server error.
+ resp = self._make_simple_request(conn, 'GET', base_version)
+ if not resp.ok:
+ raise exc.from_response(resp, method='GET', url=base_version)
+ return resp
+
+ version_overridden = False
+
+ if (resp and hasattr(resp, 'request')
+ and hasattr(resp.request, 'headers')):
+ orig_hdr = resp.request.headers
+ # Get the version of the client's last request and fallback
+ # to the default for things like unit tests to not cause
+ # migraines.
+ req_api_ver = orig_hdr.get('X-OpenStack-Ironic-API-Version',
+ self.os_ironic_api_version)
+ else:
+ req_api_ver = self.os_ironic_api_version
+ if (resp and req_api_ver != self.os_ironic_api_version
+ and self.api_version_select_state == 'negotiated'):
+ # If we have a non-standard api version on the request,
+ # but we think we've negotiated, then the call was overridden.
+ # We should report the error with the called version
+ requested_version = req_api_ver
+ # And then we shouldn't save the newly negotiated
+ # version of this negotiation because we have been
+ # overridden a request.
+ version_overridden = True
+ else:
+ requested_version = self.os_ironic_api_version
+
+ if not resp:
+ resp = _query_server(conn)
if self.api_version_select_state not in API_VERSION_SELECTED_STATES:
raise RuntimeError(
_('Error: self.api_version_select_state should be one of the '
@@ -110,37 +175,86 @@ def negotiate_version(self, conn, resp):
# the supported version range
if not max_ver:
LOG.debug('No version header in response, requesting from server')
- if self.os_ironic_api_version:
- base_version = ("/v%s" %
- str(self.os_ironic_api_version).split('.')[0])
- else:
- base_version = API_VERSION
- resp = self._make_simple_request(conn, 'GET', base_version)
+ resp = _query_server(conn)
min_ver, max_ver = self._parse_version_headers(resp)
+ # Reset the maximum version that we permit
+
+ if _Version(max_ver) > _Version(LATEST_VERSION):
+ LOG.debug("Remote API version %(max_ver)s is greater than the "
+ "version supported by ironicclient. Maximum available "
+ "version is %(client_ver)s",
+ {'max_ver': max_ver,
+ 'client_ver': LATEST_VERSION})
+ max_ver = LATEST_VERSION
+
# If the user requested an explicit version or we have negotiated a
# version and still failing then error now. The server could
# support the version requested but the requested operation may not
# be supported by the requested version.
- if self.api_version_select_state == 'user':
+ # TODO(TheJulia): We should break this method into several parts,
+ # such as a sanity check/error method.
+ if ((self.api_version_select_state == 'user'
+ and not self._must_negotiate_version())
+ or (self.api_version_select_state == 'negotiated'
+ and version_overridden)):
raise exc.UnsupportedVersion(textwrap.fill(
_("Requested API version %(req)s is not supported by the "
- "server or the requested operation is not supported by the "
- "requested version. Supported version range is %(min)s to "
+ "server, client, or the requested operation is not "
+ "supported by the requested version. "
+ "Supported version range is %(min)s to "
"%(max)s")
- % {'req': self.os_ironic_api_version,
+ % {'req': requested_version,
'min': min_ver, 'max': max_ver}))
- if self.api_version_select_state == 'negotiated':
+ if (self.api_version_select_state == 'negotiated'):
raise exc.UnsupportedVersion(textwrap.fill(
- _("No API version was specified and the requested operation "
+ _("No API version was specified or the requested operation "
"was not supported by the client's negotiated API version "
"%(req)s. Supported version range is: %(min)s to %(max)s")
- % {'req': self.os_ironic_api_version,
+ % {'req': requested_version,
'min': min_ver, 'max': max_ver}))
- negotiated_ver = str(min(StrictVersion(self.os_ironic_api_version),
- StrictVersion(max_ver)))
- if negotiated_ver < min_ver:
+ if isinstance(requested_version, str):
+ if requested_version == 'latest':
+ negotiated_ver = max_ver
+ else:
+ negotiated_ver = str(
+ min(_Version(requested_version), _Version(max_ver))
+ )
+
+ elif isinstance(requested_version, list):
+ if 'latest' in requested_version:
+ raise ValueError(textwrap.fill(
+ _("The 'latest' API version can not be requested "
+ "in a list of versions. Please explicitly request "
+ "'latest' or request only versions between "
+ "%(min)s to %(max)s")
+ % {'min': min_ver, 'max': max_ver}))
+
+ versions = []
+ for version in requested_version:
+ if _Version(min_ver) <= _Version(version) <= _Version(max_ver):
+ versions.append(_Version(version))
+ if versions:
+ negotiated_ver = str(max(versions))
+ else:
+ raise exc.UnsupportedVersion(textwrap.fill(
+ _("Requested API version specified and the requested "
+ "operation was not supported by the client's "
+ "requested API version %(req)s. Supported "
+ "version range is: %(min)s to %(max)s")
+ % {'req': requested_version,
+ 'min': min_ver, 'max': max_ver}))
+
+ else:
+ raise ValueError(textwrap.fill(
+ _("Requested API version %(req)s type is unsupported. "
+ "Valid types are Strings such as '1.1', 'latest' "
+ "or a list of string values representing API versions.")
+ % {'req': requested_version}))
+
+ if _Version(negotiated_ver) < _Version(min_ver):
negotiated_ver = min_ver
+
# server handles microversions, but doesn't support
# the requested version, so try a negotiated version
self.api_version_select_state = 'negotiated'
@@ -148,16 +262,15 @@ def negotiate_version(self, conn, resp):
LOG.debug('Negotiated API version is %s', negotiated_ver)
# Cache the negotiated version for this server
- host, port = get_server(self.endpoint)
+ endpoint_override = getattr(self, 'endpoint_override', None)
+ host, port = get_server(endpoint_override)
filecache.save_data(host=host, port=port, data=negotiated_ver)
return negotiated_ver
def _generic_parse_version_headers(self, accessor_func):
- min_ver = accessor_func('X-OpenStack-Ironic-API-Minimum-Version',
- None)
- max_ver = accessor_func('X-OpenStack-Ironic-API-Maximum-Version',
- None)
+ min_ver = accessor_func('X-OpenStack-Ironic-API-Minimum-Version', None)
+ max_ver = accessor_func('X-OpenStack-Ironic-API-Maximum-Version', None)
return min_ver, max_ver
def _parse_version_headers(self, accessor_func):
@@ -168,6 +281,11 @@ def _make_simple_request(self, conn, method, url):
# NOTE(jlvillal): Declared for unit testing purposes
raise NotImplementedError()
+ def _must_negotiate_version(self):
+ return (self.api_version_select_state == 'user'
+ and (self.os_ironic_api_version == 'latest'
+ or isinstance(self.os_ironic_api_version, list)))
+
_RETRY_EXCEPTIONS = (exc.Conflict, exc.ServiceUnavailable,
exc.ConnectionRefused, kexc.RetriableConnectionFailure)
@@ -202,283 +320,6 @@ def wrapper(self, url, method, **kwargs):
return wrapper
-class HTTPClient(VersionNegotiationMixin):
-
- def __init__(self, endpoint, **kwargs):
- self.endpoint = endpoint
- self.endpoint_trimmed = _trim_endpoint_api_version(endpoint)
- self.auth_token = kwargs.get('token')
- self.auth_ref = kwargs.get('auth_ref')
- self.os_ironic_api_version = kwargs.get('os_ironic_api_version',
- DEFAULT_VER)
- self.api_version_select_state = kwargs.get(
- 'api_version_select_state', 'default')
- self.conflict_max_retries = kwargs.pop('max_retries',
- DEFAULT_MAX_RETRIES)
- self.conflict_retry_interval = kwargs.pop('retry_interval',
- DEFAULT_RETRY_INTERVAL)
- self.session = requests.Session()
-
- parts = urlparse.urlparse(endpoint)
- if parts.scheme not in SUPPORTED_ENDPOINT_SCHEME:
- msg = _('Unsupported scheme: %s') % parts.scheme
- raise exc.EndpointException(msg)
-
- if parts.scheme == 'https':
- if kwargs.get('insecure') is True:
- self.session.verify = False
- elif kwargs.get('ca_file'):
- self.session.verify = kwargs['ca_file']
- self.session.cert = (kwargs.get('cert_file'),
- kwargs.get('key_file'))
-
- def _process_header(self, name, value):
- """Redacts any sensitive header
-
- Redact a header that contains sensitive information, by returning an
- updated header with the sha1 hash of that value. The redacted value is
- prefixed by '{SHA1}' because that's the convention used within
- OpenStack.
-
- :returns: A tuple of (name, value)
- name: the safe encoding format of name
- value: the redacted value if name is x-auth-token,
- or the safe encoding format of name
-
- """
- if name in SENSITIVE_HEADERS:
- v = value.encode('utf-8')
- h = hashlib.sha1(v)
- d = h.hexdigest()
- return (name, "{SHA1}%s" % d)
- else:
- return (name, value)
-
- def log_curl_request(self, method, url, kwargs):
- curl = ['curl -i -X %s' % method]
-
- for (key, value) in kwargs['headers'].items():
- header = '-H \'%s: %s\'' % self._process_header(key, value)
- curl.append(header)
-
- if not self.session.verify:
- curl.append('-k')
- elif isinstance(self.session.verify, six.string_types):
- curl.append('--cacert %s' % self.session.verify)
-
- if self.session.cert:
- curl.append('--cert %s' % self.session.cert[0])
- curl.append('--key %s' % self.session.cert[1])
-
- if 'body' in kwargs:
- body = strutils.mask_password(kwargs['body'])
- curl.append('-d \'%s\'' % body)
-
- curl.append(urlparse.urljoin(self.endpoint_trimmed, url))
- LOG.debug(' '.join(curl))
-
- @staticmethod
- def log_http_response(resp, body=None):
- # NOTE(aarefiev): resp.raw is urllib3 response object, it's used
- # only to get 'version', response from request with 'stream = True'
- # should be used for raw reading.
- status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
- dump = ['\nHTTP/%.1f %s %s' % status]
- dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()])
- dump.append('')
- if body:
- body = strutils.mask_password(body)
- dump.extend([body, ''])
- LOG.debug('\n'.join(dump))
-
- def _make_connection_url(self, url):
- return urlparse.urljoin(self.endpoint_trimmed, url)
-
- def _parse_version_headers(self, resp):
- return self._generic_parse_version_headers(resp.headers.get)
-
- def _make_simple_request(self, conn, method, url):
- return conn.request(method, self._make_connection_url(url))
-
- @with_retries
- def _http_request(self, url, method, **kwargs):
- """Send an http request with the specified characteristics.
-
- Wrapper around request.Session.request to handle tasks such
- as setting headers and error handling.
- """
- # Copy the kwargs so we can reuse the original in case of redirects
- kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
- kwargs['headers'].setdefault('User-Agent', USER_AGENT)
- if self.os_ironic_api_version:
- kwargs['headers'].setdefault('X-OpenStack-Ironic-API-Version',
- self.os_ironic_api_version)
- if self.auth_token:
- kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
-
- self.log_curl_request(method, url, kwargs)
-
- # NOTE(aarefiev): This is for backwards compatibility, request
- # expected body in 'data' field, previously we used httplib,
- # which expected 'body' field.
- body = kwargs.pop('body', None)
- if body:
- kwargs['data'] = body
-
- conn_url = self._make_connection_url(url)
- try:
- resp = self.session.request(method,
- conn_url,
- **kwargs)
-
- # TODO(deva): implement graceful client downgrade when connecting
- # to servers that did not support microversions. Details here:
- # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html#use-case-3b-new-client-communicating-with-a-old-ironic-user-specified # noqa
-
- if resp.status_code == http_client.NOT_ACCEPTABLE:
- negotiated_ver = self.negotiate_version(self.session, resp)
- kwargs['headers']['X-OpenStack-Ironic-API-Version'] = (
- negotiated_ver)
- return self._http_request(url, method, **kwargs)
-
- except requests.exceptions.RequestException as e:
- message = (_("Error has occurred while handling "
- "request for %(url)s: %(e)s") %
- dict(url=conn_url, e=e))
- # NOTE(aarefiev): not valid request(invalid url, missing schema,
- # and so on), retrying is not needed.
- if isinstance(e, ValueError):
- raise exc.ValidationError(message)
-
- raise exc.ConnectionRefused(message)
-
- body_str = None
- if resp.headers.get('Content-Type') == 'application/octet-stream':
- body_iter = resp.iter_content(chunk_size=CHUNKSIZE)
- self.log_http_response(resp)
- else:
- # Read body into string if it isn't obviously image data
- body_str = resp.text
- self.log_http_response(resp, body_str)
- body_iter = six.StringIO(body_str)
-
- if resp.status_code >= http_client.BAD_REQUEST:
- error_json = _extract_error_json(body_str)
- # NOTE(vdrok): exceptions from ironic controllers' _lookup methods
- # are constructed directly by pecan instead of wsme, and contain
- # only description field
- raise exc.from_response(
- resp, (error_json.get('faultstring') or
- error_json.get('description')),
- error_json.get('debuginfo'), method, url)
- elif resp.status_code in (http_client.MOVED_PERMANENTLY,
- http_client.FOUND,
- http_client.USE_PROXY):
- # Redirected. Reissue the request to the new location.
- return self._http_request(resp['location'], method, **kwargs)
- elif resp.status_code == http_client.MULTIPLE_CHOICES:
- raise exc.from_response(resp, method=method, url=url)
-
- return resp, body_iter
-
- def json_request(self, method, url, **kwargs):
- kwargs.setdefault('headers', {})
- kwargs['headers'].setdefault('Content-Type', 'application/json')
- kwargs['headers'].setdefault('Accept', 'application/json')
-
- if 'body' in kwargs:
- kwargs['body'] = jsonutils.dump_as_bytes(kwargs['body'])
-
- resp, body_iter = self._http_request(url, method, **kwargs)
- content_type = resp.headers.get('Content-Type')
-
- if (resp.status_code in (http_client.NO_CONTENT,
- http_client.RESET_CONTENT)
- or content_type is None):
- return resp, list()
-
- if 'application/json' in content_type:
- body = ''.join([chunk for chunk in body_iter])
- try:
- body = jsonutils.loads(body)
- except ValueError:
- LOG.error('Could not decode response body as JSON')
- else:
- body = None
-
- return resp, body
-
- def raw_request(self, method, url, **kwargs):
- kwargs.setdefault('headers', {})
- kwargs['headers'].setdefault('Content-Type',
- 'application/octet-stream')
- return self._http_request(url, method, **kwargs)
-
-
-class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
- """httplib-compatible connection using client-side SSL authentication
-
- :see http://code.activestate.com/recipes/
- 577548-https-httplib-client-connection-with-certificate-v/
- """
-
- def __init__(self, host, port, key_file=None, cert_file=None,
- ca_file=None, timeout=None, insecure=False):
- six.moves.http_client.HTTPSConnection.__init__(self, host, port,
- key_file=key_file,
- cert_file=cert_file)
- self.key_file = key_file
- self.cert_file = cert_file
- if ca_file is not None:
- self.ca_file = ca_file
- else:
- self.ca_file = self.get_system_ca_file()
- self.timeout = timeout
- self.insecure = insecure
-
- def connect(self):
- """Connect to a host on a given (SSL) port.
-
- If ca_file is pointing somewhere, use it to check Server Certificate.
-
- Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
- This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
- ssl.wrap_socket(), which forces SSL to check server certificate against
- our client certificate.
- """
- sock = socket.create_connection((self.host, self.port), self.timeout)
-
- if self._tunnel_host:
- self.sock = sock
- self._tunnel()
-
- if self.insecure is True:
- kwargs = {'cert_reqs': ssl.CERT_NONE}
- else:
- kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
-
- if self.cert_file:
- kwargs['certfile'] = self.cert_file
- if self.key_file:
- kwargs['keyfile'] = self.key_file
-
- self.sock = ssl.wrap_socket(sock, **kwargs)
-
- @staticmethod
- def get_system_ca_file():
- """Return path to system default CA file."""
- # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
- # Suse, FreeBSD/OpenBSD
- ca_path = ['/etc/ssl/certs/ca-certificates.crt',
- '/etc/pki/tls/certs/ca-bundle.crt',
- '/etc/ssl/ca-bundle.pem',
- '/etc/ssl/cert.pem']
- for ca in ca_path:
- if os.path.exists(ca):
- return ca
- return None
-
-
class SessionClient(VersionNegotiationMixin, adapter.LegacyJsonAdapter):
"""HTTP client based on Keystone client session."""
@@ -487,37 +328,69 @@ def __init__(self,
api_version_select_state,
max_retries,
retry_interval,
- endpoint,
**kwargs):
self.os_ironic_api_version = os_ironic_api_version
self.api_version_select_state = api_version_select_state
self.conflict_max_retries = max_retries
self.conflict_retry_interval = retry_interval
- self.endpoint = endpoint
+ if isinstance(kwargs.get('endpoint_override'), str):
+ kwargs['endpoint_override'] = _trim_endpoint_api_version(
+ kwargs['endpoint_override'])
super(SessionClient, self).__init__(**kwargs)
+ endpoint_filter = self._get_endpoint_filter()
+ endpoint = self.get_endpoint(**endpoint_filter)
+ if endpoint is None:
+ raise exc.EndpointNotFound(
+ _('The Bare Metal API endpoint cannot be detected and was '
+ 'not provided explicitly'))
+ self.endpoint_trimmed = _trim_endpoint_api_version(endpoint)
+ self._first_negotiation_lock = threading.Lock()
+
def _parse_version_headers(self, resp):
return self._generic_parse_version_headers(resp.headers.get)
+ def _get_endpoint_filter(self):
+ return {
+ 'interface': self.interface,
+ 'service_type': self.service_type,
+ 'region_name': self.region_name
+ }
+
def _make_simple_request(self, conn, method, url):
# NOTE: conn is self.session for this class
- return conn.request(url, method, raise_exc=False)
+ return conn.request(url, method, raise_exc=False,
+ user_agent=USER_AGENT,
+ endpoint_filter=self._get_endpoint_filter(),
+ endpoint_override=self.endpoint_override)
@with_retries
def _http_request(self, url, method, **kwargs):
+
+ # NOTE(TheJulia): self.os_ironic_api_version is reset in
+ # the self.negotiate_version() call if negotiation occurs.
+ if self.os_ironic_api_version and self._must_negotiate_version():
+ with self._first_negotiation_lock:
+ if self._must_negotiate_version():
+ self.negotiate_version(self.session, None)
+
kwargs.setdefault('user_agent', USER_AGENT)
kwargs.setdefault('auth', self.auth)
- if isinstance(self.endpoint_override, six.string_types):
- kwargs.setdefault(
- 'endpoint_override',
- _trim_endpoint_api_version(self.endpoint_override)
- )
+ if isinstance(self.endpoint_override, str):
+ kwargs.setdefault('endpoint_override', self.endpoint_override)
if getattr(self, 'os_ironic_api_version', None):
kwargs['headers'].setdefault('X-OpenStack-Ironic-API-Version',
self.os_ironic_api_version)
+ for k, v in self.additional_headers.items():
+ kwargs['headers'].setdefault(k, v)
+
+ if self.global_request_id is not None:
+ kwargs['headers'].setdefault(
+ "X-OpenStack-Request-ID", self.global_request_id)
+
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
endpoint_filter.setdefault('interface', self.interface)
endpoint_filter.setdefault('service_type', self.service_type)
@@ -532,11 +405,7 @@ def _http_request(self, url, method, **kwargs):
return self._http_request(url, method, **kwargs)
if resp.status_code >= http_client.BAD_REQUEST:
error_json = _extract_error_json(resp.content)
- # NOTE(vdrok): exceptions from ironic controllers' _lookup methods
- # are constructed directly by pecan instead of wsme, and contain
- # only description field
- raise exc.from_response(resp, (error_json.get('faultstring') or
- error_json.get('description')),
+ raise exc.from_response(resp, error_json.get('error_message'),
error_json.get('debuginfo'), method, url)
elif resp.status_code in (http_client.MOVED_PERMANENTLY,
http_client.FOUND, http_client.USE_PROXY):
@@ -553,14 +422,14 @@ def json_request(self, method, url, **kwargs):
kwargs['headers'].setdefault('Accept', 'application/json')
if 'body' in kwargs:
- kwargs['data'] = jsonutils.dump_as_bytes(kwargs.pop('body'))
+ kwargs['json'] = kwargs.pop('body')
resp = self._http_request(url, method, **kwargs)
body = resp.content
content_type = resp.headers.get('content-type', None)
status = resp.status_code
- if (status in (http_client.NO_CONTENT, http_client.RESET_CONTENT) or
- content_type is None):
+ if (status in (http_client.NO_CONTENT, http_client.RESET_CONTENT)
+ or content_type is None):
return resp, list()
if 'application/json' in content_type:
try:
@@ -579,8 +448,7 @@ def raw_request(self, method, url, **kwargs):
return self._http_request(url, method, **kwargs)
-def _construct_http_client(endpoint=None,
- session=None,
+def _construct_http_client(session,
token=None,
auth_ref=None,
os_ironic_api_version=DEFAULT_VER,
@@ -593,48 +461,30 @@ def _construct_http_client(endpoint=None,
key_file=None,
insecure=None,
**kwargs):
- if session:
- kwargs.setdefault('service_type', 'baremetal')
- kwargs.setdefault('user_agent', 'python-ironicclient')
- kwargs.setdefault('interface', kwargs.pop('endpoint_type', None))
- kwargs.setdefault('endpoint_override', endpoint)
-
- ignored = {'token': token,
- 'auth_ref': auth_ref,
- 'timeout': timeout != 600,
- 'ca_file': ca_file,
- 'cert_file': cert_file,
- 'key_file': key_file,
- 'insecure': insecure}
-
- dvars = [k for k, v in ignored.items() if v]
-
- if dvars:
- LOG.warning('The following arguments are ignored when using '
- 'the session to construct a client: %s',
- ', '.join(dvars))
-
- return SessionClient(session=session,
- os_ironic_api_version=os_ironic_api_version,
- api_version_select_state=api_version_select_state,
- max_retries=max_retries,
- retry_interval=retry_interval,
- endpoint=endpoint,
- **kwargs)
- else:
- if kwargs:
- LOG.warning('The following arguments are being ignored when '
- 'constructing the client: %s'), ', '.join(kwargs)
-
- return HTTPClient(endpoint=endpoint,
- token=token,
- auth_ref=auth_ref,
- os_ironic_api_version=os_ironic_api_version,
- api_version_select_state=api_version_select_state,
- max_retries=max_retries,
- retry_interval=retry_interval,
- timeout=timeout,
- ca_file=ca_file,
- cert_file=cert_file,
- key_file=key_file,
- insecure=insecure)
+
+ kwargs.setdefault('service_type', 'baremetal')
+ kwargs.setdefault('user_agent', 'python-ironicclient')
+ kwargs.setdefault('interface', kwargs.pop('endpoint_type',
+ 'publicURL'))
+
+ ignored = {'token': token,
+ 'auth_ref': auth_ref,
+ 'timeout': timeout != 600,
+ 'ca_file': ca_file,
+ 'cert_file': cert_file,
+ 'key_file': key_file,
+ 'insecure': insecure}
+
+ dvars = [k for k, v in ignored.items() if v]
+
+ if dvars:
+ LOG.warning('The following arguments are ignored when using '
+ 'the session to construct a client: %s',
+ ', '.join(dvars))
+
+ return SessionClient(session=session,
+ os_ironic_api_version=os_ironic_api_version,
+ api_version_select_state=api_version_select_state,
+ max_retries=max_retries,
+ retry_interval=retry_interval,
+ **kwargs)
diff --git a/ironicclient/common/i18n.py b/ironicclient/common/i18n.py
index 90f289ae7..48b8d13ac 100644
--- a/ironicclient/common/i18n.py
+++ b/ironicclient/common/i18n.py
@@ -13,9 +13,19 @@
# License for the specific language governing permissions and limitations
# under the License.
-import oslo_i18n
+from __future__ import annotations
-_translators = oslo_i18n.TranslatorFactory(domain='ironicclient')
+from typing import Callable
-# The primary translation function using the well-known name "_"
-_ = _translators.primary
+try:
+ import oslo_i18n
+except ImportError:
+ def _(msg: str) -> str:
+ return msg
+else:
+ _translators: oslo_i18n.TranslatorFactory = oslo_i18n.TranslatorFactory(
+ domain="ironicclient"
+ )
+
+ # The primary translation function using the well-known name "_"
+ _: Callable[[str], str] = _translators.primary # type: ignore[no-redef]
diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py
index dc5ac6236..33227771b 100644
--- a/ironicclient/common/utils.py
+++ b/ironicclient/common/utils.py
@@ -13,9 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
-from __future__ import print_function
-
import argparse
+import base64
import contextlib
import gzip
import json
@@ -24,10 +23,11 @@
import subprocess
import sys
import tempfile
+import time
-from oslo_serialization import base64
+from cliff import columns
from oslo_utils import strutils
-import six
+import yaml
from ironicclient.common.i18n import _
from ironicclient import exc
@@ -162,7 +162,7 @@ def convert_list_props_to_comma_separated(data, props=None):
for prop in props:
val = data.get(prop, None)
if isinstance(val, list):
- result[prop] = ', '.join(map(six.text_type, val))
+ result[prop] = ', '.join(map(str, val))
return result
@@ -215,7 +215,7 @@ def common_params_for_list(args, fields, field_labels):
def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None,
- fields=None):
+ fields=None, detail=False, project=None, public=None):
"""Generate common filters for any list request.
:param marker: entity ID from which to start returning entities.
@@ -224,6 +224,9 @@ def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None,
:param sort_dir: direction of sorting: 'asc' or 'desc'.
:param fields: a list with a specified set of fields of the resource
to be returned.
+ :param detail: Boolean, True to return detailed information. This parameter
+ can be used for resources which accept 'detail' as a URL
+ parameter.
:returns: list of string filters.
"""
filters = []
@@ -235,8 +238,14 @@ def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None,
filters.append('sort_key=%s' % sort_key)
if sort_dir is not None:
filters.append('sort_dir=%s' % sort_dir)
+ if project is not None:
+ filters.append('project=%s' % project)
+ if public is not None:
+ filters.append('public=True')
if fields is not None:
filters.append('fields=%s' % ','.join(fields))
+ if detail:
+ filters.append('detail=True')
return filters
@@ -263,20 +272,30 @@ def make_configdrive(path):
with tempfile.NamedTemporaryFile() as tmpfile:
with tempfile.NamedTemporaryFile() as tmpzipfile:
publisher = 'ironicclient-configdrive 0.1'
- try:
- p = subprocess.Popen(['genisoimage', '-o', tmpfile.name,
- '-ldots', '-allow-lowercase',
- '-allow-multidot', '-l',
- '-publisher', publisher,
- '-quiet', '-J',
- '-r', '-V', 'config-2',
- path],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- except OSError as e:
+ # NOTE(toabctl): Luckily, genisoimage, mkisofs and xorrisofs
+ # understand the same parameters which are currently used.
+ cmds = ['genisoimage', 'mkisofs', 'xorrisofs']
+ for c in cmds:
+ try:
+ p = subprocess.Popen([c, '-o', tmpfile.name,
+ '-ldots', '-allow-lowercase',
+ '-allow-multidot', '-l',
+ '-publisher', publisher,
+ '-quiet', '-J',
+ '-r', '-V', 'config-2',
+ path],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ except OSError as e:
+ error = e
+ else:
+ error = None
+ break
+ if error:
raise exc.CommandError(
_('Error generating the config drive. Make sure the '
- '"genisoimage" tool is installed. Error: %s') % e)
+ '"genisoimage", "mkisofs", or "xorriso" tool is '
+ 'installed. Error: %s') % error)
stdout, stderr = p.communicate()
if p.returncode != 0:
@@ -287,12 +306,11 @@ def make_configdrive(path):
# Compress file
tmpfile.seek(0)
- g = gzip.GzipFile(fileobj=tmpzipfile, mode='wb')
- shutil.copyfileobj(tmpfile, g)
- g.close()
+ with gzip.GzipFile(fileobj=tmpzipfile, mode='wb') as gz_file:
+ shutil.copyfileobj(tmpfile, gz_file)
tmpzipfile.seek(0)
- return base64.encode_as_bytes(tmpzipfile.read())
+ return base64.b64encode(tmpzipfile.read())
def check_empty_arg(arg, arg_descriptor):
@@ -364,7 +382,7 @@ def get_from_stdin(info_desc):
def handle_json_or_file_arg(json_arg):
"""Attempts to read JSON argument from file or string.
- :param json_arg: May be a file name containing the JSON, or
+ :param json_arg: May be a file name containing the YAML or JSON, or
a JSON string.
:returns: A list or dictionary parsed from JSON.
:raises: InvalidAttribute if the argument cannot be parsed.
@@ -373,16 +391,98 @@ def handle_json_or_file_arg(json_arg):
if os.path.isfile(json_arg):
try:
with open(json_arg, 'r') as f:
- json_arg = f.read().strip()
+ return yaml.safe_load(f)
except Exception as e:
- err = _("Cannot get JSON from file '%(file)s'. "
+ err = _("Cannot get JSON/YAML from file '%(file)s'. "
"Error: %(err)s") % {'err': e, 'file': json_arg}
raise exc.InvalidAttribute(err)
try:
json_arg = json.loads(json_arg)
except ValueError as e:
- err = (_("For JSON: '%(string)s', error: '%(err)s'") %
- {'err': e, 'string': json_arg})
+ err = (_("Value '%(string)s' is not a file and cannot be parsed "
+ "as JSON: '%(err)s'") % {'err': e, 'string': json_arg})
raise exc.InvalidAttribute(err)
return json_arg
+
+
+def poll(timeout, poll_interval, poll_delay_function, timeout_message):
+ if not isinstance(timeout, (int, float)) or timeout < 0:
+ raise ValueError(_('Timeout must be a non-negative number'))
+
+ threshold = time.time() + timeout
+ poll_delay_function = (time.sleep if poll_delay_function is None
+ else poll_delay_function)
+ if not callable(poll_delay_function):
+ raise TypeError(_('poll_delay_function must be callable'))
+
+ count = 0
+ while not timeout or time.time() < threshold:
+ yield count
+
+ poll_delay_function(poll_interval)
+ count += 1
+
+ if callable(timeout_message):
+ timeout_message = timeout_message()
+ raise exc.StateTransitionTimeout(timeout_message)
+
+
+def handle_json_arg(json_arg, info_desc):
+ """Read a JSON argument from stdin, file or string.
+
+ :param json_arg: May be a file name containing the JSON, a JSON string, or
+ '-' indicating that the argument should be read from standard input.
+ :param info_desc: A string description of the desired information
+ :returns: A list or dictionary parsed from JSON.
+ :raises: InvalidAttribute if the argument cannot be parsed.
+ """
+ if json_arg == '-':
+ json_arg = get_from_stdin(info_desc)
+ if json_arg:
+ json_arg = handle_json_or_file_arg(json_arg)
+ return json_arg
+
+
+def get_json_data(data):
+ """Check if the binary data is JSON and parse it if so.
+
+ Only supports dictionaries.
+ """
+ # We don't want to simply loads() a potentially large binary. Doing so,
+ # in my testing, is orders of magnitude (!!) slower than this process.
+ for idx in range(len(data)):
+ char = data[idx:idx + 1]
+ if char.isspace():
+ continue
+ if char != b'{' and char != 'b[':
+ return None # not JSON, at least not JSON we care about
+ break # maybe JSON
+
+ try:
+ return json.loads(data)
+ except ValueError:
+ return None
+
+
+def format_hash(data):
+ if data is None:
+ return None
+
+ output = ""
+ for s in sorted(data):
+ key_str = s
+ if isinstance(data[s], dict):
+ # NOTE(dtroyer): Only append the separator chars here, quoting
+ # is completely handled in the terminal case.
+ output = output + format_hash(data[s], prefix=key_str) + ", "
+ elif data[s] is not None:
+ output = output + key_str + "='" + str(data[s]) + "', "
+ else:
+ output = output + key_str + "=, "
+ return output[:-2]
+
+
+class HashColumn(columns.FormattableColumn):
+ def human_readable(self):
+ return format_hash(self._value)
diff --git a/ironicclient/exc.py b/ironicclient/exc.py
index 85caf0138..aa81914c3 100644
--- a/ironicclient/exc.py
+++ b/ironicclient/exc.py
@@ -16,36 +16,38 @@
# NOTE(akurilin): This alias is left here since v.0.1.3 to support backwards
# compatibility.
-InvalidEndpoint = EndpointException
-CommunicationError = ConnectionRefused
-HTTPBadRequest = BadRequest
-HTTPInternalServerError = InternalServerError
-HTTPNotFound = NotFound
-HTTPServiceUnavailable = ServiceUnavailable
+InvalidEndpoint = exceptions.EndpointException
+CommunicationError = exceptions.ConnectionRefused
+HTTPBadRequest = exceptions.BadRequest
+HTTPInternalServerError = exceptions.InternalServerError
+HTTPNotFound = exceptions.NotFound
+HTTPServiceUnavailable = exceptions.ServiceUnavailable
-class AmbiguousAuthSystem(ClientException):
+class AmbiguousAuthSystem(exceptions.ClientException):
"""Could not obtain token and endpoint using provided credentials."""
pass
+
# Alias for backwards compatibility
AmbigiousAuthSystem = AmbiguousAuthSystem
-class InvalidAttribute(ClientException):
+class InvalidAttribute(exceptions.ClientException):
pass
-class StateTransitionFailed(ClientException):
+class StateTransitionFailed(exceptions.ClientException):
"""Failed to reach a requested provision state."""
-class StateTransitionTimeout(ClientException):
+class StateTransitionTimeout(exceptions.ClientException):
"""Timed out while waiting for a requested provision state."""
-def from_response(response, message=None, traceback=None, method=None,
- url=None):
+def from_response( # type: ignore[no-redef]
+ response, message=None, traceback=None, method=None, url=None
+):
"""Return an HttpError instance based on response from httplib/requests."""
error_body = {}
diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py
index f0b84e06c..09f3d78c6 100644
--- a/ironicclient/osc/plugin.py
+++ b/ironicclient/osc/plugin.py
@@ -19,37 +19,64 @@
import argparse
import logging
-from ironicclient.common import http
from osc_lib import utils
+from ironicclient.common import http
+
LOG = logging.getLogger(__name__)
+CLIENT_CLASS = 'ironicclient.v1.client.Client'
API_VERSION_OPTION = 'os_baremetal_api_version'
API_NAME = 'baremetal'
-LAST_KNOWN_API_VERSION = 31
+# NOTE(TheJulia) Latest known version tracking has been moved
+# to the ironicclient/common/http.py file as the OSC commitment
+# is latest known, and we should only store it in one location.
+LAST_KNOWN_API_VERSION = http.LAST_KNOWN_API_VERSION
+LATEST_VERSION = http.LATEST_VERSION
+
+
API_VERSIONS = {
- '1.%d' % i: 'ironicclient.v1.client.Client'
+ '1.%d' % i: CLIENT_CLASS
for i in range(1, LAST_KNOWN_API_VERSION + 1)
}
-API_VERSIONS['1'] = API_VERSIONS[http.DEFAULT_VER]
+API_VERSIONS['1'] = CLIENT_CLASS
+# NOTE(dtantsur): flag to indicate that the requested version was "latest".
+# Due to how OSC works we cannot just add "latest" to the list of supported
+# versions - it breaks the major version detection.
+OS_BAREMETAL_API_LATEST = True
def make_client(instance):
"""Returns a baremetal service client."""
+ requested_api_version = instance._api_version[API_NAME]
+
baremetal_client_class = utils.get_client_class(
API_NAME,
- instance._api_version[API_NAME],
+ requested_api_version,
API_VERSIONS)
LOG.debug('Instantiating baremetal client: %s', baremetal_client_class)
- LOG.debug('Baremetal API version: %s', http.DEFAULT_VER)
+ LOG.debug('Baremetal API version: %s',
+ requested_api_version if not OS_BAREMETAL_API_LATEST
+ else "latest")
+
+ if requested_api_version == '1':
+ # NOTE(dtantsur): '1' means 'the latest v1 API version'. Since we don't
+ # have other major versions, it's identical to 'latest'.
+ requested_api_version = LATEST_VERSION
+ allow_api_version_downgrade = True
+ else:
+ allow_api_version_downgrade = OS_BAREMETAL_API_LATEST
client = baremetal_client_class(
- os_ironic_api_version=instance._api_version[API_NAME],
+ os_ironic_api_version=requested_api_version,
+ # NOTE(dtantsur): enable re-negotiation of the latest version, if CLI
+ # latest is too high for the server we're talking to.
+ allow_api_version_downgrade=allow_api_version_downgrade,
session=instance.session,
region_name=instance._region_name,
# NOTE(vdrok): This will be set as endpoint_override, and the Client
# class will be able to do the version stripping if needed
- endpoint=instance.get_endpoint_for_service_type(
+ endpoint_override=instance.get_endpoint_for_service_type(
API_NAME, interface=instance.interface,
region_name=instance._region_name
)
@@ -62,27 +89,47 @@ def build_option_parser(parser):
parser.add_argument(
'--os-baremetal-api-version',
metavar='',
- default=utils.env(
- 'OS_BAREMETAL_API_VERSION',
- default=http.DEFAULT_VER),
+ default=_get_environment_version("latest"),
choices=sorted(
API_VERSIONS,
key=lambda k: [int(x) for x in k.split('.')]) + ['latest'],
action=ReplaceLatestVersion,
- help='Baremetal API version, default=' +
- http.DEFAULT_VER +
- ' (Env: OS_BAREMETAL_API_VERSION). '
- '"latest" is the latest known API version',
+ help='Bare metal API version, default="latest" (the maximum version '
+ 'supported by both the client and the server). '
+ '(Env: OS_BAREMETAL_API_VERSION)',
)
return parser
+def _get_environment_version(default):
+ global OS_BAREMETAL_API_LATEST
+ env_value = utils.env('OS_BAREMETAL_API_VERSION')
+ if not env_value:
+ env_value = default
+ if env_value == 'latest':
+ env_value = LATEST_VERSION
+ else:
+ OS_BAREMETAL_API_LATEST = False
+ return env_value
+
+
class ReplaceLatestVersion(argparse.Action):
- """Replaces `latest` keyword by last known version."""
+ """Replaces `latest` keyword by last known version.
+
+ OSC cannot accept the literal "latest" as a supported API version as it
+ breaks the major version detection (OSC tries to load configuration options
+ from setuptools entrypoint openstack.baremetal.vlatest). This action
+ replaces "latest" with the latest known version, and sets the global
+ OS_BAREMETAL_API_LATEST flag appropriately.
+ """
def __call__(self, parser, namespace, values, option_string=None):
- latest = values == 'latest'
- if latest:
- values = '1.%d' % LAST_KNOWN_API_VERSION
- LOG.debug("Replacing 'latest' API version with the "
- "latest known version '%s'", values)
+ global OS_BAREMETAL_API_LATEST
+ if values == 'latest':
+ values = LATEST_VERSION
+ # The default value of "True" may have been overridden due to
+ # non-empty OS_BAREMETAL_API_VERSION env variable. If a user
+ # explicitly requests "latest", we need to correct it.
+ OS_BAREMETAL_API_LATEST = True
+ else:
+ OS_BAREMETAL_API_LATEST = False
setattr(namespace, self.dest, values)
diff --git a/ironicclient/osc/v1/baremetal_allocation.py b/ironicclient/osc/v1/baremetal_allocation.py
new file mode 100644
index 000000000..bb67dcece
--- /dev/null
+++ b/ironicclient/osc/v1/baremetal_allocation.py
@@ -0,0 +1,378 @@
+# 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 itertools
+import logging
+
+from osc_lib.command import command
+from osc_lib import utils as oscutils
+
+from ironicclient.common.i18n import _
+from ironicclient.common import utils
+from ironicclient import exc
+from ironicclient.v1 import resource_fields as res_fields
+
+
+class CreateBaremetalAllocation(command.ShowOne):
+ """Create a new baremetal allocation."""
+
+ log = logging.getLogger(__name__ + ".CreateBaremetalAllocation")
+
+ def get_parser(self, prog_name):
+ parser = super(CreateBaremetalAllocation, self).get_parser(prog_name)
+
+ parser.add_argument(
+ '--resource-class',
+ dest='resource_class',
+ help=_('Resource class to request.'))
+ parser.add_argument(
+ '--trait',
+ action='append',
+ dest='traits',
+ help=_('A trait to request. Can be specified multiple times.'))
+ parser.add_argument(
+ '--candidate-node',
+ action='append',
+ dest='candidate_nodes',
+ help=_('A candidate node for this allocation. Can be specified '
+ 'multiple times. If at least one is specified, only the '
+ 'provided candidate nodes are considered for the '
+ 'allocation.'))
+ parser.add_argument(
+ '--name',
+ dest='name',
+ help=_('Unique name of the allocation.'))
+ parser.add_argument(
+ '--uuid',
+ dest='uuid',
+ help=_('UUID of the allocation.'))
+ parser.add_argument(
+ '--owner',
+ dest='owner',
+ help=_('Owner of the allocation.'))
+ parser.add_argument(
+ '--extra',
+ metavar="",
+ action='append',
+ help=_("Record arbitrary key/value metadata. "
+ "Can be specified multiple times."))
+ parser.add_argument(
+ '--wait',
+ type=int,
+ dest='wait_timeout',
+ default=None,
+ metavar='',
+ const=0,
+ nargs='?',
+ help=_("Wait for the new allocation to become active. An error "
+ "is returned if allocation fails and --wait is used. "
+ "Optionally takes a timeout value (in seconds). The "
+ "default value is 0, meaning it will wait indefinitely."))
+ parser.add_argument(
+ '--node',
+ help=_("Backfill this allocation from the provided node that has "
+ "already been deployed. Bypasses the normal allocation "
+ "process."))
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+ baremetal_client = self.app.client_manager.baremetal
+
+ if not parsed_args.node and not parsed_args.resource_class:
+ raise exc.ClientException(
+ _('--resource-class is required except when --node is used'))
+
+ field_list = ['name', 'uuid', 'extra', 'resource_class', 'traits',
+ 'candidate_nodes', 'node', 'owner']
+ fields = dict((k, v) for (k, v) in vars(parsed_args).items()
+ if k in field_list and v is not None)
+
+ fields = utils.args_array_to_dict(fields, 'extra')
+ allocation = baremetal_client.allocation.create(**fields)
+ if parsed_args.wait_timeout is not None:
+ allocation = baremetal_client.allocation.wait(
+ allocation.uuid, timeout=parsed_args.wait_timeout)
+
+ data = dict([(f, getattr(allocation, f, '')) for f in
+ res_fields.ALLOCATION_DETAILED_RESOURCE.fields])
+
+ return self.dict2columns(data)
+
+
+class ShowBaremetalAllocation(command.ShowOne):
+ """Show baremetal allocation details."""
+
+ log = logging.getLogger(__name__ + ".ShowBaremetalAllocation")
+
+ def get_parser(self, prog_name):
+ parser = super(ShowBaremetalAllocation, self).get_parser(prog_name)
+ parser.add_argument(
+ "allocation",
+ metavar="",
+ help=_("UUID or name of the allocation"))
+ parser.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ choices=res_fields.ALLOCATION_DETAILED_RESOURCE.fields,
+ default=[],
+ help=_("One or more allocation fields. Only these fields will be "
+ "fetched from the server."))
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ fields = list(itertools.chain.from_iterable(parsed_args.fields))
+ fields = fields if fields else None
+
+ allocation = baremetal_client.allocation.get(
+ parsed_args.allocation, fields=fields)._info
+
+ allocation.pop("links", None)
+ return zip(*sorted(allocation.items()))
+
+
+class ListBaremetalAllocation(command.Lister):
+ """List baremetal allocations."""
+
+ log = logging.getLogger(__name__ + ".ListBaremetalAllocation")
+
+ def get_parser(self, prog_name):
+ parser = super(ListBaremetalAllocation, self).get_parser(prog_name)
+ parser.add_argument(
+ '--limit',
+ metavar='',
+ type=int,
+ help=_('Maximum number of allocations to return per request, '
+ '0 for no limit. Default is the maximum number used '
+ 'by the Baremetal API Service.'))
+ parser.add_argument(
+ '--marker',
+ metavar='',
+ help=_('Allocation UUID (for example, of the last allocation in '
+ 'the list from a previous request). Returns the list of '
+ 'allocations after this UUID.'))
+ parser.add_argument(
+ '--sort',
+ metavar="[:]",
+ help=_('Sort output by specified allocation fields and directions '
+ '(asc or desc) (default: asc). Multiple fields and '
+ 'directions can be specified, separated by comma.'))
+ parser.add_argument(
+ '--node',
+ metavar='',
+ help=_("Only list allocations of this node (name or UUID)."))
+ parser.add_argument(
+ '--resource-class',
+ metavar='',
+ help=_("Only list allocations with this resource class."))
+ parser.add_argument(
+ '--state',
+ metavar='',
+ help=_("Only list allocations in this state."))
+ parser.add_argument(
+ '--owner',
+ metavar='',
+ help=_("Only list allocations with this owner."))
+
+ # NOTE(dtantsur): the allocation API does not expose the 'detail' flag,
+ # but some fields are inconvenient to display in a table, so we emulate
+ # it on the client side.
+ display_group = parser.add_mutually_exclusive_group(required=False)
+ display_group.add_argument(
+ '--long',
+ default=False,
+ help=_("Show detailed information about the allocations."),
+ action='store_true')
+ display_group.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ default=[],
+ choices=res_fields.ALLOCATION_DETAILED_RESOURCE.fields,
+ help=_("One or more allocation fields. Only these fields will be "
+ "fetched from the server. Can not be used when '--long' "
+ "is specified."))
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+ client = self.app.client_manager.baremetal
+
+ params = {}
+ if parsed_args.limit is not None and parsed_args.limit < 0:
+ raise exc.CommandError(
+ _('Expected non-negative --limit, got %s') %
+ parsed_args.limit)
+ params['limit'] = parsed_args.limit
+ params['marker'] = parsed_args.marker
+ for field in ('node', 'resource_class', 'state', 'owner'):
+ value = getattr(parsed_args, field)
+ if value is not None:
+ params[field] = value
+
+ if parsed_args.long:
+ columns = res_fields.ALLOCATION_DETAILED_RESOURCE.fields
+ elif parsed_args.fields:
+ fields = itertools.chain.from_iterable(parsed_args.fields)
+ resource = res_fields.Resource(list(fields))
+ columns = resource.fields
+ params['fields'] = columns
+ else:
+ columns = res_fields.ALLOCATION_RESOURCE.fields
+
+ self.log.debug("params(%s)", params)
+ data = client.allocation.list(**params)
+
+ data = oscutils.sort_items(data, parsed_args.sort)
+
+ return (columns,
+ (oscutils.get_item_properties(s, columns) for s in data))
+
+
+class DeleteBaremetalAllocation(command.Command):
+ """Unregister baremetal allocation(s)."""
+
+ log = logging.getLogger(__name__ + ".DeleteBaremetalAllocation")
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteBaremetalAllocation, self).get_parser(prog_name)
+ parser.add_argument(
+ "allocations",
+ metavar="",
+ nargs="+",
+ help=_("Allocations(s) to delete (name or UUID)."))
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ failures = []
+ for allocation in parsed_args.allocations:
+ try:
+ baremetal_client.allocation.delete(allocation)
+ print(_('Deleted allocation %s') % allocation)
+ except exc.ClientException as e:
+ failures.append(_("Failed to delete allocation "
+ "%(allocation)s: %(error)s")
+ % {'allocation': allocation, 'error': e})
+
+ if failures:
+ raise exc.ClientException("\n".join(failures))
+
+
+class SetBaremetalAllocation(command.Command):
+ """Set baremetal allocation properties."""
+
+ log = logging.getLogger(__name__ + ".SetBaremetalAllocation")
+
+ def get_parser(self, prog_name):
+ parser = super(SetBaremetalAllocation, self).get_parser(prog_name)
+
+ parser.add_argument(
+ "allocation",
+ metavar="",
+ help=_("Name or UUID of the allocation")
+ )
+ parser.add_argument(
+ "--name",
+ metavar="",
+ help=_("Set the name of the allocation")
+ )
+ parser.add_argument(
+ "--extra",
+ metavar="",
+ action="append",
+ help=_("Extra property to set on this allocation "
+ "(repeat option to set multiple extra properties)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ properties = []
+ if parsed_args.name:
+ properties.extend(utils.args_array_to_patch(
+ 'add', ["name=%s" % parsed_args.name]))
+ if parsed_args.extra:
+ properties.extend(utils.args_array_to_patch(
+ 'add', ["extra/" + x for x in parsed_args.extra]))
+
+ if properties:
+ baremetal_client.allocation.update(
+ parsed_args.allocation, properties)
+ else:
+ self.log.warning("Please specify what to set.")
+
+
+class UnsetBaremetalAllocation(command.Command):
+ """Unset baremetal allocation properties."""
+ log = logging.getLogger(__name__ + ".UnsetBaremetalAllocation")
+
+ def get_parser(self, prog_name):
+ parser = super(UnsetBaremetalAllocation, self).get_parser(
+ prog_name)
+
+ parser.add_argument(
+ "allocation",
+ metavar="",
+ help=_("Name or UUID of the allocation")
+ )
+ parser.add_argument(
+ "--name",
+ action="store_true",
+ default=False,
+ help=_("Unset the name of the allocation")
+ )
+ parser.add_argument(
+ "--extra",
+ metavar="",
+ action='append',
+ help=_('Extra property to unset on this baremetal allocation '
+ '(repeat option to unset multiple extra property).'),
+ )
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ properties = []
+ if parsed_args.name:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['name']))
+ if parsed_args.extra:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['extra/' + x for x in parsed_args.extra]))
+
+ if properties:
+ baremetal_client.allocation.update(parsed_args.allocation,
+ properties)
+ else:
+ self.log.warning("Please specify what to unset.")
diff --git a/ironicclient/osc/v1/baremetal_chassis.py b/ironicclient/osc/v1/baremetal_chassis.py
index a84fc52f4..26e7bbfe2 100644
--- a/ironicclient/osc/v1/baremetal_chassis.py
+++ b/ironicclient/osc/v1/baremetal_chassis.py
@@ -162,7 +162,6 @@ def take_action(self, parsed_args):
client = self.app.client_manager.baremetal
columns = res_fields.CHASSIS_RESOURCE.fields
- labels = res_fields.CHASSIS_RESOURCE.labels
params = {}
if parsed_args.limit is not None and parsed_args.limit < 0:
@@ -174,13 +173,11 @@ def take_action(self, parsed_args):
if parsed_args.long:
params['detail'] = parsed_args.long
columns = res_fields.CHASSIS_DETAILED_RESOURCE.fields
- labels = res_fields.CHASSIS_DETAILED_RESOURCE.labels
elif parsed_args.fields:
params['detail'] = False
fields = itertools.chain.from_iterable(parsed_args.fields)
resource = res_fields.Resource(list(fields))
columns = resource.fields
- labels = resource.labels
params['fields'] = columns
self.log.debug("params(%s)", params)
@@ -188,9 +185,9 @@ def take_action(self, parsed_args):
data = oscutils.sort_items(data, parsed_args.sort)
- return (labels,
+ return (columns,
(oscutils.get_item_properties(s, columns, formatters={
- 'Properties': oscutils.format_dict},) for s in data))
+ 'Properties': utils.HashColumn},) for s in data))
class SetBaremetalChassis(command.Command):
diff --git a/ironicclient/osc/v1/baremetal_conductor.py b/ironicclient/osc/v1/baremetal_conductor.py
new file mode 100644
index 000000000..e66fb1dbc
--- /dev/null
+++ b/ironicclient/osc/v1/baremetal_conductor.py
@@ -0,0 +1,143 @@
+#
+# 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.
+#
+
+import itertools
+import logging
+
+from osc_lib.command import command
+from osc_lib import utils as oscutils
+
+from ironicclient.common.i18n import _
+from ironicclient.common import utils
+from ironicclient import exc
+from ironicclient.v1 import resource_fields as res_fields
+
+
+class ListBaremetalConductor(command.Lister):
+ """List baremetal conductors"""
+
+ log = logging.getLogger(__name__ + ".ListBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(ListBaremetalConductor, self).get_parser(prog_name)
+ parser.add_argument(
+ '--limit',
+ metavar='',
+ type=int,
+ help=_('Maximum number of conductors to return per request, '
+ '0 for no limit. Default is the maximum number used '
+ 'by the Baremetal API Service.')
+ )
+ parser.add_argument(
+ '--marker',
+ metavar='',
+ help=_('Hostname of the conductor (for example, of the last '
+ 'conductor in the list from a previous request). Returns '
+ 'the list of conductors after this conductor.')
+ )
+ parser.add_argument(
+ '--sort',
+ metavar="[:]",
+ help=_('Sort output by specified conductor fields and directions '
+ '(asc or desc) (default: asc). Multiple fields and '
+ 'directions can be specified, separated by comma.'),
+ )
+ display_group = parser.add_mutually_exclusive_group(required=False)
+ display_group.add_argument(
+ '--long',
+ default=False,
+ help=_("Show detailed information about the conductors."),
+ action='store_true')
+ display_group.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ default=[],
+ choices=res_fields.CONDUCTOR_DETAILED_RESOURCE.fields,
+ help=_("One or more conductor fields. Only these fields will be "
+ "fetched from the server. Can not be used when '--long' "
+ "is specified."))
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+ client = self.app.client_manager.baremetal
+
+ columns = res_fields.CONDUCTOR_RESOURCE.fields
+
+ params = {}
+ if parsed_args.limit is not None and parsed_args.limit < 0:
+ raise exc.CommandError(
+ _('Expected non-negative --limit, got %s') %
+ parsed_args.limit)
+ params['limit'] = parsed_args.limit
+ params['marker'] = parsed_args.marker
+ if parsed_args.long:
+ params['detail'] = parsed_args.long
+ columns = res_fields.CONDUCTOR_DETAILED_RESOURCE.fields
+ elif parsed_args.fields:
+ params['detail'] = False
+ fields = itertools.chain.from_iterable(parsed_args.fields)
+ resource = res_fields.Resource(list(fields))
+ columns = resource.fields
+ params['fields'] = columns
+
+ self.log.debug("params(%s)", params)
+ data = client.conductor.list(**params)
+
+ data = oscutils.sort_items(data, parsed_args.sort)
+
+ return (columns,
+ (oscutils.get_item_properties(s, columns, formatters={
+ 'Properties': utils.HashColumn},) for s in data))
+
+
+class ShowBaremetalConductor(command.ShowOne):
+ """Show baremetal conductor details"""
+
+ log = logging.getLogger(__name__ + ".ShowBaremetalConductor")
+
+ def get_parser(self, prog_name):
+ parser = super(ShowBaremetalConductor, self).get_parser(prog_name)
+ parser.add_argument(
+ "conductor",
+ metavar="",
+ help=_("Hostname of the conductor"))
+ parser.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ choices=res_fields.CONDUCTOR_DETAILED_RESOURCE.fields,
+ default=[],
+ help=_("One or more conductor fields. Only these fields will be "
+ "fetched from the server."))
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ fields = list(itertools.chain.from_iterable(parsed_args.fields))
+ fields = fields if fields else None
+ conductor = baremetal_client.conductor.get(
+ parsed_args.conductor, fields=fields)._info
+ conductor.pop("links", None)
+
+ return self.dict2columns(conductor)
diff --git a/ironicclient/osc/v1/baremetal_create.py b/ironicclient/osc/v1/baremetal_create.py
index c16db10b9..5d23ddbf2 100644
--- a/ironicclient/osc/v1/baremetal_create.py
+++ b/ironicclient/osc/v1/baremetal_create.py
@@ -10,69 +10,28 @@
# License for the specific language governing permissions and limitations
# under the License.
-import argparse
import logging
+from osc_lib.command import command
+
from ironicclient.common.i18n import _
-from ironicclient import exc
-from ironicclient.osc.v1 import baremetal_node
from ironicclient.v1 import create_resources
-class CreateBaremetal(baremetal_node.CreateBaremetalNode):
- """Create resources from files or Register a new node (DEPRECATED).
-
- Create resources from files (by only specifying the files) or register
- a new node by specifying one or more optional arguments (DEPRECATED,
- use 'openstack baremetal node create' instead).
- """
+class CreateBaremetal(command.Command):
+ """Create resources from files"""
log = logging.getLogger(__name__ + ".CreateBaremetal")
- def get_description(self):
- return _("Create resources from files (by only specifying the files) "
- "or register a new node by specifying one or more optional "
- "arguments (DEPRECATED, use 'openstack baremetal node "
- "create' instead)")
-
- # TODO(vdrok): Remove support for new node creation after 11-July-2017
- # during the 'Queens' cycle.
def get_parser(self, prog_name):
parser = super(CreateBaremetal, self).get_parser(prog_name)
- # NOTE(vdrok): It is a workaround to allow --driver to be optional for
- # openstack create command while creation of nodes via this command is
- # not removed completely
- parser = argparse.ArgumentParser(parents=[parser],
- conflict_handler='resolve',
- description=self.__doc__)
- parser.add_argument(
- '--driver',
- metavar='',
- help=_('Specify this and any other optional arguments if you want '
- 'to create a node only. Note that this is deprecated; '
- 'please use "openstack baremetal node create" instead.'))
+
parser.add_argument(
- "resource_files", metavar="", default=[], nargs="*",
+ "resource_files", metavar="", nargs="+",
help=_("File (.yaml or .json) containing descriptions of the "
- "resources to create. Can be specified multiple times. If "
- "you want to create resources, only specify the files. Do "
- "not specify any of the optional arguments."))
+ "resources to create. Can be specified multiple times."))
return parser
def take_action(self, parsed_args):
- if parsed_args.driver:
- self.log.warning("This command is deprecated. Instead, use "
- "'openstack baremetal node create'.")
- return super(CreateBaremetal, self).take_action(parsed_args)
- if not parsed_args.resource_files:
- raise exc.ValidationError(_(
- "If --driver is not supplied to openstack create command, "
- "it is considered that it will create ironic resources from "
- "one or more .json or .yaml files, but no files provided."))
create_resources.create_resources(self.app.client_manager.baremetal,
parsed_args.resource_files)
- # NOTE(vdrok): CreateBaremetal is still inherited from ShowOne class,
- # which requires the return value of the function to be of certain
- # type, leave this workaround until creation of nodes is removed and
- # then change it so that this inherits from command.Command
- return tuple(), tuple()
diff --git a/ironicclient/osc/v1/baremetal_deploy_template.py b/ironicclient/osc/v1/baremetal_deploy_template.py
new file mode 100644
index 000000000..aaec26685
--- /dev/null
+++ b/ironicclient/osc/v1/baremetal_deploy_template.py
@@ -0,0 +1,342 @@
+# 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 itertools
+import json
+import logging
+
+from osc_lib.command import command
+from osc_lib import utils as oscutils
+
+from ironicclient.common.i18n import _
+from ironicclient.common import utils
+from ironicclient import exc
+from ironicclient.v1 import resource_fields as res_fields
+
+
+_DEPLOY_STEPS_HELP = _(
+ "The deploy steps. May be the path to a YAML file containing the deploy "
+ "steps; OR '-', with the deploy steps being read from standard "
+ "input; OR a JSON string. The value should be a list of deploy-step "
+ "dictionaries; each dictionary should have keys 'interface', 'step', "
+ "'args' and 'priority'.")
+
+
+class CreateBaremetalDeployTemplate(command.ShowOne):
+ """Create a new deploy template"""
+
+ log = logging.getLogger(__name__ + ".CreateBaremetalDeployTemplate")
+
+ def get_parser(self, prog_name):
+ parser = super(CreateBaremetalDeployTemplate, self).get_parser(
+ prog_name)
+
+ parser.add_argument(
+ 'name',
+ metavar='',
+ help=_('Unique name for this deploy template. Must be a valid '
+ 'trait name')
+ )
+ parser.add_argument(
+ '--uuid',
+ dest='uuid',
+ metavar='',
+ help=_('UUID of the deploy template.'))
+ parser.add_argument(
+ '--extra',
+ metavar="",
+ action='append',
+ help=_("Record arbitrary key/value metadata. "
+ "Can be specified multiple times."))
+ parser.add_argument(
+ '--steps',
+ metavar="",
+ required=True,
+ help=_DEPLOY_STEPS_HELP
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+ baremetal_client = self.app.client_manager.baremetal
+
+ steps = utils.handle_json_arg(parsed_args.steps, 'deploy steps')
+
+ field_list = ['name', 'uuid', 'extra']
+ fields = dict((k, v) for (k, v) in vars(parsed_args).items()
+ if k in field_list and v is not None)
+ fields = utils.args_array_to_dict(fields, 'extra')
+ template = baremetal_client.deploy_template.create(steps=steps,
+ **fields)
+
+ data = dict([(f, getattr(template, f, '')) for f in
+ res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.fields])
+
+ return self.dict2columns(data)
+
+
+class ShowBaremetalDeployTemplate(command.ShowOne):
+ """Show baremetal deploy template details."""
+
+ log = logging.getLogger(__name__ + ".ShowBaremetalDeployTemplate")
+
+ def get_parser(self, prog_name):
+ parser = super(ShowBaremetalDeployTemplate, self).get_parser(prog_name)
+ parser.add_argument(
+ "template",
+ metavar="",
+ help=_("Name or UUID of the deploy template.")
+ )
+ parser.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ choices=res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.fields,
+ default=[],
+ help=_("One or more deploy template fields. Only these fields "
+ "will be fetched from the server.")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ fields = list(itertools.chain.from_iterable(parsed_args.fields))
+ fields = fields if fields else None
+
+ template = baremetal_client.deploy_template.get(
+ parsed_args.template, fields=fields)._info
+
+ template.pop("links", None)
+ return zip(*sorted(template.items()))
+
+
+class SetBaremetalDeployTemplate(command.Command):
+ """Set baremetal deploy template properties."""
+
+ log = logging.getLogger(__name__ + ".SetBaremetalDeployTemplate")
+
+ def get_parser(self, prog_name):
+ parser = super(SetBaremetalDeployTemplate, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'template',
+ metavar='',
+ help=_("Name or UUID of the deploy template")
+ )
+ parser.add_argument(
+ '--name',
+ metavar='',
+ help=_('Set unique name of the deploy template. Must be a valid '
+ 'trait name.')
+ )
+ parser.add_argument(
+ '--steps',
+ metavar="",
+ help=_DEPLOY_STEPS_HELP
+ )
+ parser.add_argument(
+ "--extra",
+ metavar="",
+ action='append',
+ help=_('Extra to set on this baremetal deploy template '
+ '(repeat option to set multiple extras).'),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ properties = []
+ if parsed_args.name:
+ name = ["name=%s" % parsed_args.name]
+ properties.extend(utils.args_array_to_patch('add', name))
+ if parsed_args.steps:
+ steps = utils.handle_json_arg(parsed_args.steps, 'deploy steps')
+ steps = ["steps=%s" % json.dumps(steps)]
+ properties.extend(utils.args_array_to_patch('add', steps))
+ if parsed_args.extra:
+ properties.extend(utils.args_array_to_patch(
+ 'add', ['extra/' + x for x in parsed_args.extra]))
+
+ if properties:
+ baremetal_client.deploy_template.update(parsed_args.template,
+ properties)
+ else:
+ self.log.warning("Please specify what to set.")
+
+
+class UnsetBaremetalDeployTemplate(command.Command):
+ """Unset baremetal deploy template properties."""
+ log = logging.getLogger(__name__ + ".UnsetBaremetalDeployTemplate")
+
+ def get_parser(self, prog_name):
+ parser = super(UnsetBaremetalDeployTemplate, self).get_parser(
+ prog_name)
+
+ parser.add_argument(
+ 'template',
+ metavar='',
+ help=_("Name or UUID of the deploy template")
+ )
+ parser.add_argument(
+ "--extra",
+ metavar="",
+ action='append',
+ help=_('Extra to unset on this baremetal deploy template '
+ '(repeat option to unset multiple extras).'),
+ )
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ properties = []
+ if parsed_args.extra:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['extra/' + x for x in parsed_args.extra]))
+
+ if properties:
+ baremetal_client.deploy_template.update(parsed_args.template,
+ properties)
+ else:
+ self.log.warning("Please specify what to unset.")
+
+
+class DeleteBaremetalDeployTemplate(command.Command):
+ """Delete deploy template(s)."""
+
+ log = logging.getLogger(__name__ + ".DeleteBaremetalDeployTemplate")
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteBaremetalDeployTemplate, self).get_parser(
+ prog_name)
+ parser.add_argument(
+ "templates",
+ metavar="",
+ nargs="+",
+ help=_("Name(s) or UUID(s) of the deploy template(s) to delete.")
+ )
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ failures = []
+ for template in parsed_args.templates:
+ try:
+ baremetal_client.deploy_template.delete(template)
+ print(_('Deleted deploy template %s') % template)
+ except exc.ClientException as e:
+ failures.append(_("Failed to delete deploy template "
+ "%(template)s: %(error)s")
+ % {'template': template, 'error': e})
+
+ if failures:
+ raise exc.ClientException("\n".join(failures))
+
+
+class ListBaremetalDeployTemplate(command.Lister):
+ """List baremetal deploy templates."""
+
+ log = logging.getLogger(__name__ + ".ListBaremetalDeployTemplate")
+
+ def get_parser(self, prog_name):
+ parser = super(ListBaremetalDeployTemplate, self).get_parser(prog_name)
+ parser.add_argument(
+ '--limit',
+ metavar='',
+ type=int,
+ help=_('Maximum number of deploy templates to return per request, '
+ '0 for no limit. Default is the maximum number used '
+ 'by the Baremetal API Service.')
+ )
+ parser.add_argument(
+ '--marker',
+ metavar='',
+ help=_('DeployTemplate UUID (for example, of the last deploy '
+ 'template in the list from a previous request). Returns '
+ 'the list of deploy templates after this UUID.')
+ )
+ parser.add_argument(
+ '--sort',
+ metavar="[:]",
+ help=_('Sort output by specified deploy template fields and '
+ 'directions (asc or desc) (default: asc). Multiple fields '
+ 'and directions can be specified, separated by comma.')
+ )
+ display_group = parser.add_mutually_exclusive_group()
+ display_group.add_argument(
+ '--long',
+ dest='detail',
+ action='store_true',
+ default=False,
+ help=_("Show detailed information about deploy templates.")
+ )
+ display_group.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ default=[],
+ choices=res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.fields,
+ help=_("One or more deploy template fields. Only these fields "
+ "will be fetched from the server. Can not be used when "
+ "'--long' is specified.")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+ client = self.app.client_manager.baremetal
+
+ columns = res_fields.DEPLOY_TEMPLATE_RESOURCE.fields
+
+ params = {}
+ if parsed_args.limit is not None and parsed_args.limit < 0:
+ raise exc.CommandError(
+ _('Expected non-negative --limit, got %s') %
+ parsed_args.limit)
+ params['limit'] = parsed_args.limit
+ params['marker'] = parsed_args.marker
+
+ if parsed_args.detail:
+ params['detail'] = parsed_args.detail
+ columns = res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.fields
+
+ elif parsed_args.fields:
+ params['detail'] = False
+ fields = itertools.chain.from_iterable(parsed_args.fields)
+ resource = res_fields.Resource(list(fields))
+ columns = resource.fields
+ params['fields'] = columns
+
+ self.log.debug("params(%s)", params)
+ data = client.deploy_template.list(**params)
+
+ data = oscutils.sort_items(data, parsed_args.sort)
+
+ return (columns,
+ (oscutils.get_item_properties(s, columns) for s in data))
diff --git a/ironicclient/osc/v1/baremetal_driver.py b/ironicclient/osc/v1/baremetal_driver.py
index 8bd3e8a89..39fb2f4d2 100644
--- a/ironicclient/osc/v1/baremetal_driver.py
+++ b/ironicclient/osc/v1/baremetal_driver.py
@@ -12,8 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
-
-
+import itertools
import logging
from osc_lib.command import command
@@ -39,11 +38,23 @@ def get_parser(self, prog_name):
help='Type of driver ("classic" or "dynamic"). '
'The default is to list all of them.'
)
- parser.add_argument(
+ display_group = parser.add_mutually_exclusive_group()
+ display_group.add_argument(
'--long',
action='store_true',
default=None,
help="Show detailed information about the drivers.")
+ display_group.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ default=[],
+ choices=res_fields.DRIVER_DETAILED_RESOURCE.fields,
+ help=_("One or more node fields. Only these fields will be "
+ "fetched from the server. Can not be used when '--long' "
+ "is specified."))
return parser
def take_action(self, parsed_args):
@@ -53,10 +64,13 @@ def take_action(self, parsed_args):
params = {'driver_type': parsed_args.type,
'detail': parsed_args.long}
if parsed_args.long:
- labels = res_fields.DRIVER_DETAILED_RESOURCE.labels
columns = res_fields.DRIVER_DETAILED_RESOURCE.fields
+ elif parsed_args.fields:
+ fields = itertools.chain.from_iterable(parsed_args.fields)
+ resource = res_fields.Resource(list(fields))
+ columns = resource.fields
+ params['fields'] = columns
else:
- labels = res_fields.DRIVER_RESOURCE.labels
columns = res_fields.DRIVER_RESOURCE.fields
drivers = client.driver.list(**params)
@@ -67,10 +81,57 @@ def take_action(self, parsed_args):
data = [utils.convert_list_props_to_comma_separated(d._info)
for d in drivers]
- return (labels,
+ return (columns,
(oscutils.get_dict_properties(s, columns) for s in data))
+class ListBaremetalDriverProperty(command.Lister):
+ """List the driver properties."""
+
+ log = logging.getLogger(__name__ + ".ListBaremetalDriverProperty")
+
+ def get_parser(self, prog_name):
+ parser = super(ListBaremetalDriverProperty, self).get_parser(prog_name)
+ parser.add_argument(
+ 'driver',
+ metavar='',
+ help='Name of the driver.')
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+ baremetal_client = self.app.client_manager.baremetal
+
+ driver_properties = baremetal_client.driver.properties(
+ parsed_args.driver)
+ columns = ['property', 'description']
+ return columns, sorted(driver_properties.items())
+
+
+class ListBaremetalDriverRaidProperty(command.Lister):
+ """List a driver's RAID logical disk properties."""
+
+ log = logging.getLogger(__name__ + ".ListBaremetalDriverRaidProperty")
+
+ def get_parser(self, prog_name):
+ parser = super(ListBaremetalDriverRaidProperty, self).get_parser(
+ prog_name)
+ parser.add_argument(
+ 'driver',
+ metavar='',
+ help='Name of the driver.')
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+ baremetal_client = self.app.client_manager.baremetal
+
+ raid_props = baremetal_client.driver.raid_logical_disk_properties(
+ parsed_args.driver)
+ columns = ['property', 'description']
+ return columns, sorted(raid_props.items())
+
+
class PassthruCallBaremetalDriver(command.ShowOne):
"""Call a vendor passthru method for a driver."""
@@ -139,7 +200,6 @@ def take_action(self, parsed_args):
baremetal_client = self.app.client_manager.baremetal
columns = res_fields.VENDOR_PASSTHRU_METHOD_RESOURCE.fields
- labels = res_fields.VENDOR_PASSTHRU_METHOD_RESOURCE.labels
methods = baremetal_client.driver.get_vendor_passthru_methods(
parsed_args.driver)
@@ -151,7 +211,7 @@ def take_action(self, parsed_args):
response['http_methods'] = http_methods
params.append(response)
- return (labels,
+ return (columns,
(oscutils.get_dict_properties(s, columns) for s in params))
@@ -166,13 +226,26 @@ def get_parser(self, prog_name):
'driver',
metavar='',
help=_('Name of the driver.'))
+ parser.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ default=[],
+ choices=res_fields.DRIVER_DETAILED_RESOURCE.fields,
+ help=_("One or more node fields. Only these fields will be "
+ "fetched from the server."))
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
baremetal_client = self.app.client_manager.baremetal
- driver = baremetal_client.driver.get(parsed_args.driver)._info
+ fields = list(itertools.chain.from_iterable(parsed_args.fields))
+ fields = fields if fields else None
+ driver = baremetal_client.driver.get(parsed_args.driver,
+ fields=fields)._info
driver.pop("links", None)
driver.pop("properties", None)
# For list-type properties, show the values as comma separated
diff --git a/ironicclient/osc/v1/baremetal_inspection_rule.py b/ironicclient/osc/v1/baremetal_inspection_rule.py
new file mode 100644
index 000000000..1a173a1cc
--- /dev/null
+++ b/ironicclient/osc/v1/baremetal_inspection_rule.py
@@ -0,0 +1,409 @@
+# 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 itertools
+import json
+import logging
+
+from osc_lib.command import command
+from osc_lib import utils as oscutils
+
+from ironicclient.common.i18n import _
+from ironicclient.common import utils
+from ironicclient import exc
+from ironicclient.v1 import resource_fields as res_fields
+
+
+class CreateBaremetalInspectionRule(command.ShowOne):
+ """Create a new rule"""
+
+ log = logging.getLogger(__name__ + ".CreateBaremetalInspectionRule")
+
+ def get_parser(self, prog_name):
+ parser = super(CreateBaremetalInspectionRule, self).get_parser(
+ prog_name)
+
+ parser.add_argument(
+ '--uuid',
+ dest='uuid',
+ metavar='',
+ help=_('UUID of the rule.'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('A brief explanation about the rule.')
+ )
+ parser.add_argument(
+ '--priority',
+ metavar='',
+ help=_("Specifies the rule's precedence level during execution.")
+ )
+ parser.add_argument(
+ '--sensitive',
+ metavar='',
+ help=_('Indicates whether the rule contains sensitive '
+ 'information.')
+ )
+ parser.add_argument(
+ '--phase',
+ metavar='',
+ help=_('Specifies the processing phase when the rule should run.')
+ )
+ parser.add_argument(
+ '--conditions',
+ metavar="",
+ help=_('Conditions under which the rule should be triggered.')
+ )
+ parser.add_argument(
+ '--actions',
+ metavar="",
+ required=True,
+ help=_('Actions to be executed when the rule conditions are met.')
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+ baremetal_client = self.app.client_manager.baremetal
+
+ actions = utils.handle_json_arg(parsed_args.actions, 'rule actions')
+ conditions = utils.handle_json_arg(parsed_args.conditions,
+ 'rule conditions')
+
+ field_list = ['uuid', 'description', 'priority', 'sensitive', 'phase']
+ fields = dict((k, v) for (k, v) in vars(parsed_args).items()
+ if k in field_list and v is not None)
+ rule = baremetal_client.inspection_rule.create(actions=actions,
+ conditions=conditions,
+ **fields)
+
+ data = dict([(f, getattr(rule, f, '')) for f in
+ res_fields.INSPECTION_RULE_DETAILED_RESOURCE.fields])
+
+ return self.dict2columns(data)
+
+
+class ShowBaremetalInspectionRule(command.ShowOne):
+ """Show baremetal rule details."""
+
+ log = logging.getLogger(__name__ + ".ShowBaremetalInspectionRule")
+
+ def get_parser(self, prog_name):
+ parser = super(ShowBaremetalInspectionRule, self).get_parser(
+ prog_name)
+ parser.add_argument(
+ "rule",
+ metavar="",
+ help=_("UUID of the inspection rule.")
+ )
+ parser.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ choices=res_fields.INSPECTION_RULE_DETAILED_RESOURCE.fields,
+ default=[],
+ help=_("One or more rule fields. Only these fields "
+ "will be fetched from the server.")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ fields = list(itertools.chain.from_iterable(parsed_args.fields))
+ fields = fields if fields else None
+
+ rule = baremetal_client.inspection_rule.get(
+ parsed_args.rule, fields=fields)._info
+
+ rule.pop("links", None)
+ return zip(*sorted(rule.items()))
+
+
+class SetBaremetalInspectionRule(command.Command):
+ """Set baremetal rule properties."""
+
+ log = logging.getLogger(__name__ + ".SetBaremetalInspectionRule")
+
+ def get_parser(self, prog_name):
+ parser = super(SetBaremetalInspectionRule, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'rule',
+ metavar='',
+ help=_("UUID of the inspection rule")
+ )
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Set a brief explanation about the rule.')
+ )
+ parser.add_argument(
+ '--priority',
+ metavar='',
+ help=_("Specifies the rule's precedence level during execution.")
+ )
+ parser.add_argument(
+ '--phase',
+ metavar='',
+ help=_('Specifies the processing phase when the rule should run.')
+ )
+ parser.add_argument(
+ '--conditions',
+ metavar="",
+ help=_('Conditions under which the rule should be triggered.')
+ )
+ parser.add_argument(
+ '--actions',
+ metavar="",
+ help=_('Actions to be executed when the rule conditions are met.')
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ properties = []
+ if parsed_args.description:
+ description = ["description=%s" % parsed_args.description]
+ properties.extend(utils.args_array_to_patch('add', description))
+ if parsed_args.priority:
+ priority = ["priority=%s" % parsed_args.priority]
+ properties.extend(utils.args_array_to_patch('add', priority))
+ if parsed_args.phase:
+ phase = ["phase=%s" % parsed_args.phase]
+ properties.extend(utils.args_array_to_patch('add', phase))
+ if parsed_args.actions:
+ actions = utils.handle_json_arg(parsed_args.actions,
+ 'rule actions')
+ actions = ["actions=%s" % json.dumps(actions)]
+ properties.extend(utils.args_array_to_patch('add', actions))
+ if parsed_args.conditions:
+ conditions = utils.handle_json_arg(parsed_args.conditions,
+ 'rule conditions')
+ conditions = ["conditions=%s" % json.dumps(conditions)]
+ properties.extend(utils.args_array_to_patch('add', conditions))
+
+ if properties:
+ baremetal_client.inspection_rule.update(parsed_args.rule,
+ properties)
+ else:
+ self.log.warning("Please specify what to set.")
+
+
+class UnsetBaremetalInspectionRule(command.Command):
+ """Unset baremetal rule properties."""
+ log = logging.getLogger(__name__ + ".UnsetBaremetalInspectionRule")
+
+ def get_parser(self, prog_name):
+ parser = super(UnsetBaremetalInspectionRule, self).get_parser(
+ prog_name)
+
+ parser.add_argument(
+ 'rule',
+ metavar='',
+ help=_("UUID of the inspection rule")
+ )
+ parser.add_argument(
+ '--description',
+ dest='description',
+ action='store_true',
+ help=_('Unset a brief explanation about the rule.')
+ )
+ parser.add_argument(
+ '--priority',
+ dest='priority',
+ action='store_true',
+ help=_("Specifies the rule's precedence level during execution.")
+ )
+ parser.add_argument(
+ '--phase',
+ dest='phase',
+ action='store_true',
+ help=_('Specifies the processing phase when the rule should run.')
+ )
+ parser.add_argument(
+ '--condition',
+ metavar="",
+ action='append',
+ help=_('Condition to unset on this baremetal inspection rule '
+ '(repeat option to unset multiple conditions).')
+ )
+ parser.add_argument(
+ '--action',
+ metavar="",
+ action='append',
+ help=_('Action to unset on this baremetal inspection rule '
+ '(repeat option to unset multiple actions).')
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ properties = []
+ field_list = ['description', 'priority', 'phase']
+ for field in field_list:
+ if getattr(parsed_args, field):
+ properties.extend(utils.args_array_to_patch('remove', [field]))
+
+ if parsed_args.action:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['action/' + x for x in parsed_args.action]))
+ if parsed_args.condition:
+ properties.extend(
+ utils.args_array_to_patch(
+ 'remove',
+ ['condition/' + x for x in parsed_args.condition]))
+
+ if properties:
+ baremetal_client.inspection_rule.update(parsed_args.rule,
+ properties)
+ else:
+ self.log.warning("Please specify what to unset.")
+
+
+class DeleteBaremetalInspectionRule(command.Command):
+ """Delete rule(s)."""
+
+ log = logging.getLogger(__name__ + ".DeleteBaremetalInspectionRule")
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteBaremetalInspectionRule, self).get_parser(
+ prog_name)
+ parser.add_argument(
+ "rules",
+ metavar="",
+ nargs="+",
+ help=_("UUID(s) of the rule(s) to delete."),
+ )
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ failures = []
+ if parsed_args.rules == 'all':
+ try:
+ baremetal_client.inspection_rule.delete()
+ print(_('Deleted all rules.'))
+ except exc.ClientException as e:
+ raise exc.ClientException(
+ _("Failed to delete all rules: %s") % e)
+ else:
+ for rule in parsed_args.rules:
+ try:
+ baremetal_client.inspection_rule.delete(rule)
+ print(_('Deleted rule %s') % rule)
+ except exc.ClientException as e:
+ failures.append(_("Failed to delete rule "
+ "%(rule)s: %(error)s")
+ % {'rule': rule, 'error': e})
+
+ if failures:
+ raise exc.ClientException("\n".join(failures))
+
+
+class ListBaremetalInspectionRule(command.Lister):
+ """List baremetal rules."""
+
+ log = logging.getLogger(__name__ + ".ListBaremetalInspectionRule")
+
+ def get_parser(self, prog_name):
+ parser = super(ListBaremetalInspectionRule, self).get_parser(prog_name)
+ parser.add_argument(
+ '--limit',
+ metavar='',
+ type=int,
+ help=_('Maximum number of rules to return per request, '
+ '0 for no limit. Default is the maximum number used '
+ 'by the Baremetal API Service.')
+ )
+ parser.add_argument(
+ '--marker',
+ metavar='',
+ help=_('InspectionRule UUID (for example, of the last rule '
+ 'in the list from a previous request). Returns '
+ 'the list of rules after this UUID.')
+ )
+ parser.add_argument(
+ '--sort',
+ metavar="[:]",
+ help=_('Sort output by specified rule fields and '
+ 'directions (asc or desc) (default: asc). Multiple fields '
+ 'and directions can be specified, separated by comma.')
+ )
+ display_group = parser.add_mutually_exclusive_group()
+ display_group.add_argument(
+ '--long',
+ dest='detail',
+ action='store_true',
+ default=False,
+ help=_("Show detailed information about rules.")
+ )
+ display_group.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ default=[],
+ choices=res_fields.INSPECTION_RULE_DETAILED_RESOURCE.fields,
+ help=_("One or more rule fields. Only these fields "
+ "will be fetched from the server. Can not be used when "
+ "'--long' is specified.")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+ client = self.app.client_manager.baremetal
+
+ columns = res_fields.INSPECTION_RULE_RESOURCE.fields
+
+ params = {}
+ if parsed_args.limit is not None and parsed_args.limit < 0:
+ raise exc.CommandError(
+ _('Expected non-negative --limit, got %s') %
+ parsed_args.limit)
+ params['limit'] = parsed_args.limit
+ params['marker'] = parsed_args.marker
+
+ if parsed_args.detail:
+ params['detail'] = parsed_args.detail
+ columns = res_fields.INSPECTION_RULE_DETAILED_RESOURCE.fields
+
+ elif parsed_args.fields:
+ params['detail'] = False
+ fields = itertools.chain.from_iterable(parsed_args.fields)
+ resource = res_fields.Resource(list(fields))
+ columns = resource.fields
+ params['fields'] = columns
+
+ self.log.debug("params(%s)", params)
+ data = client.inspection_rule.list(**params)
+
+ data = oscutils.sort_items(data, parsed_args.sort)
+
+ return (columns,
+ (oscutils.get_item_properties(s, columns) for s in data))
diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py
old mode 100755
new mode 100644
index 0dadba634..c00a4b73b
--- a/ironicclient/osc/v1/baremetal_node.py
+++ b/ironicclient/osc/v1/baremetal_node.py
@@ -16,7 +16,9 @@
import argparse
import itertools
+import json
import logging
+import sys
from osc_lib.command import command
from osc_lib import utils as oscutils
@@ -27,6 +29,29 @@
from ironicclient.v1 import resource_fields as res_fields
from ironicclient.v1 import utils as v1_utils
+CONFIG_DRIVE_ARG_HELP = _(
+ "A gzipped, base64-encoded configuration drive string OR "
+ "the path to the configuration drive file OR the path to a "
+ "directory containing the config drive files OR a JSON object to build "
+ "config drive from OR the path to the JSON file. In case it's a "
+ "directory, a config drive will be generated from it. In case it's a JSON "
+ "object with optional keys `meta_data`, `user_data` and `network_data` "
+ "or a JSON file, a config drive will be generated on the server side "
+ "(see the bare metal API reference for more details).")
+
+
+NETWORK_DATA_ARG_HELP = _(
+ "JSON string or a YAML file or '-' for stdin to read static network "
+ "configuration for the baremetal node associated with this ironic node. "
+ "Format of this file should comply with Nova network data metadata "
+ "(network_data.json). Depending on ironic boot interface capabilities "
+ "being used, network configuration may or may not been served to the "
+ "node for offline network configuration.")
+
+SUPPORTED_INTERFACES = ['bios', 'boot', 'console', 'deploy', 'firmware',
+ 'inspect', 'management', 'network', 'power', 'raid',
+ 'rescue', 'storage', 'vendor']
+
class ProvisionStateBaremetalNode(command.Command):
"""Base provision state class"""
@@ -37,9 +62,10 @@ def get_parser(self, prog_name):
parser = super(ProvisionStateBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node.")
+ nargs='+',
+ help=_("Names or UUID's of the nodes.")
)
parser.add_argument(
'--provision-state',
@@ -54,19 +80,47 @@ def take_action(self, parsed_args):
baremetal_client = self.app.client_manager.baremetal
+ runbook = getattr(parsed_args, 'runbook', None)
+
clean_steps = getattr(parsed_args, 'clean_steps', None)
- if clean_steps == '-':
- clean_steps = utils.get_from_stdin('clean steps')
- if clean_steps:
- clean_steps = utils.handle_json_or_file_arg(clean_steps)
+ clean_steps = utils.handle_json_arg(clean_steps, 'clean steps')
+
+ deploy_steps = getattr(parsed_args, 'deploy_steps', None)
+ deploy_steps = utils.handle_json_arg(deploy_steps, 'deploy steps')
+
+ service_steps = getattr(parsed_args, 'service_steps', None)
+ service_steps = utils.handle_json_arg(service_steps, 'service steps')
config_drive = getattr(parsed_args, 'config_drive', None)
+ if config_drive:
+ try:
+ config_drive_dict = json.loads(config_drive)
+ except (ValueError, TypeError):
+ pass
+ else:
+ if isinstance(config_drive_dict, dict):
+ config_drive = config_drive_dict
- baremetal_client.node.set_provision_state(
- parsed_args.node,
- parsed_args.provision_state,
- configdrive=config_drive,
- cleansteps=clean_steps)
+ rescue_password = getattr(parsed_args, 'rescue_password', None)
+
+ disable_ramdisk = getattr(parsed_args, 'disable_ramdisk', None)
+
+ if runbook and disable_ramdisk:
+ raise exc.CommandError(
+ _("You cannot supply --runbook and --disable-ramdisk together")
+ )
+
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_provision_state(
+ node,
+ parsed_args.provision_state,
+ configdrive=config_drive,
+ cleansteps=clean_steps,
+ deploysteps=deploy_steps,
+ rescue_password=rescue_password,
+ servicesteps=service_steps,
+ runbook=runbook,
+ disable_ramdisk=disable_ramdisk)
class ProvisionStateWithWait(ProvisionStateBaremetalNode):
@@ -109,11 +163,12 @@ def take_action(self, parsed_args):
_("'--wait is not supported for provision state '%s'")
% parsed_args.provision_state)
- print(_('Waiting for provision state %(state)s on node %(node)s') %
- {'state': wait_args['expected_state'], 'node': parsed_args.node})
+ print(_('Waiting for provision state %(state)s on node(s) %(node)s') %
+ {'state': wait_args['expected_state'],
+ 'node': ', '.join(parsed_args.nodes)})
baremetal_client.node.wait_for_provision_state(
- parsed_args.node,
+ parsed_args.nodes,
timeout=parsed_args.wait_timeout,
**wait_args)
@@ -141,9 +196,10 @@ def get_parser(self, prog_name):
parser = super(BootdeviceSetBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node")
+ nargs='+',
+ help=_("Names or UUID's of the nodes")
)
parser.add_argument(
'device',
@@ -164,10 +220,9 @@ def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
baremetal_client = self.app.client_manager.baremetal
- baremetal_client.node.set_boot_device(
- parsed_args.node,
- parsed_args.device,
- parsed_args.persistent)
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_boot_device(
+ node, parsed_args.device, parsed_args.persistent)
class BootdeviceShowBaremetalNode(command.ShowOne):
@@ -206,6 +261,37 @@ def take_action(self, parsed_args):
return zip(*sorted(info.items()))
+class BootmodeSetBaremetalNode(command.Command):
+ """Set the boot mode for the next baremetal node deployment"""
+
+ log = logging.getLogger(__name__ + ".BootmodeSetBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(BootmodeSetBaremetalNode, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'nodes',
+ metavar='',
+ nargs='+',
+ help=_("Names or UUID's of the nodes.")
+ )
+ parser.add_argument(
+ 'boot_mode',
+ choices=['uefi', 'bios'],
+ metavar='',
+ help=_('The boot mode to set for node (uefi/bios)')
+ )
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_boot_mode(node, parsed_args.boot_mode)
+
+
class CleanBaremetalNode(ProvisionStateWithWait):
"""Set provision state of baremetal node to 'clean'"""
@@ -214,18 +300,63 @@ class CleanBaremetalNode(ProvisionStateWithWait):
def get_parser(self, prog_name):
parser = super(CleanBaremetalNode, self).get_parser(prog_name)
+ clean_group = parser.add_mutually_exclusive_group(required=True)
- parser.add_argument(
+ clean_group.add_argument(
'--clean-steps',
metavar='',
- required=True,
- default=None,
- help=_("The clean steps in JSON format. May be the path to a file "
+ help=_("The clean steps. May be the path to a YAML file "
"containing the clean steps; OR '-', with the clean steps "
- "being read from standard input; OR a string. The value "
- "should be a list of clean-step dictionaries; each "
+ "being read from standard input; OR a JSON string. The "
+ "value should be a list of clean-step dictionaries; each "
"dictionary should have keys 'interface' and 'step', and "
"optional key 'args'."))
+ clean_group.add_argument(
+ '--runbook',
+ metavar='',
+ help=_("The identifier of a predefined runbook to use for "
+ "cleaning."))
+ parser.add_argument(
+ '--disable-ramdisk',
+ action='store_true',
+ default=None,
+ help=_("ironic-python-agent will not be booted for cleaning. "
+ "Only steps explicitly marked as not requiring "
+ "ironic-python-agent can be executed with this set."))
+ return parser
+
+
+class ServiceBaremetalNode(ProvisionStateWithWait):
+ """Set provision state of baremetal node to 'service'"""
+
+ log = logging.getLogger(__name__ + ".ServiceBaremetalNode")
+ PROVISION_STATE = 'service'
+
+ def get_parser(self, prog_name):
+ parser = super(ServiceBaremetalNode, self).get_parser(prog_name)
+ service_group = parser.add_mutually_exclusive_group(required=True)
+
+ service_group.add_argument(
+ '--service-steps',
+ metavar='',
+ help=_("The service steps. May be the path to a YAML file "
+ "containing the service steps; OR '-', with the service "
+ " steps being read from standard input; OR a JSON string. "
+ "The value should be a list of service-step dictionaries; "
+ "each dictionary should have keys 'interface' and 'step', "
+ "and optional key 'args'."))
+ service_group.add_argument(
+ '--runbook',
+ metavar='',
+ help=_("The identifier of a predefined runbook to use for "
+ "servicing."))
+ parser.add_argument(
+ '--disable-ramdisk',
+ action='store_true',
+ default=None,
+ help=_("ironic-python-agent will not be booted for cleaning. "
+ "Only steps explicitly marked as not requiring "
+ "ironic-python-agent can be executed with this set."))
return parser
@@ -238,9 +369,10 @@ def get_parser(self, prog_name):
parser = super(ConsoleDisableBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node")
+ nargs='+',
+ help=_("Names or UUID's of the nodes")
)
return parser
@@ -248,7 +380,8 @@ def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
baremetal_client = self.app.client_manager.baremetal
- baremetal_client.node.set_console_mode(parsed_args.node, False)
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_console_mode(node, False)
class ConsoleEnableBaremetalNode(command.Command):
@@ -260,9 +393,10 @@ def get_parser(self, prog_name):
parser = super(ConsoleEnableBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node")
+ nargs='+',
+ help=_("Names or UUID's of the nodes")
)
return parser
@@ -270,7 +404,8 @@ def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
baremetal_client = self.app.client_manager.baremetal
- baremetal_client.node.set_console_mode(parsed_args.node, True)
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_console_mode(node, True)
class ConsoleShowBaremetalNode(command.ShowOne):
@@ -342,6 +477,16 @@ def get_parser(self, prog_name):
'--name',
metavar='',
help=_("Unique name for the node."))
+ parser.add_argument(
+ '--instance-name',
+ metavar='',
+ help=_('Name of the instance deployed on this node.'))
+ parser.add_argument(
+ '--bios-interface',
+ metavar='',
+ help=_('BIOS interface used by the node\'s driver. This is '
+ 'only applicable when the specified --driver is a '
+ 'hardware type.'))
parser.add_argument(
'--boot-interface',
metavar='',
@@ -372,6 +517,11 @@ def get_parser(self, prog_name):
help=_('Management interface used by the node\'s driver. This is '
'only applicable when the specified --driver is a '
'hardware type.'))
+ parser.add_argument(
+ '--network-data',
+ metavar="",
+ dest='network_data',
+ help=NETWORK_DATA_ARG_HELP)
parser.add_argument(
'--network-interface',
metavar='',
@@ -389,6 +539,16 @@ def get_parser(self, prog_name):
help=_('RAID interface used by the node\'s driver. This is '
'only applicable when the specified --driver is a '
'hardware type.'))
+ parser.add_argument(
+ '--rescue-interface',
+ metavar='',
+ help=_('Rescue interface used by the node\'s driver. This is '
+ 'only applicable when the specified --driver is a '
+ 'hardware type.'))
+ parser.add_argument(
+ '--storage-interface',
+ metavar='',
+ help=_('Storage interface used by the node\'s driver.'))
parser.add_argument(
'--vendor-interface',
metavar='',
@@ -399,25 +559,74 @@ def get_parser(self, prog_name):
'--resource-class',
metavar='',
help=_('Resource class for mapping nodes to Nova flavors'))
-
+ parser.add_argument(
+ '--conductor-group',
+ metavar='',
+ help=_('Conductor group the node will belong to'))
+ clean = parser.add_mutually_exclusive_group()
+ clean.add_argument(
+ '--automated-clean',
+ action='store_true',
+ default=None,
+ help=_('Enable automated cleaning for the node'))
+ clean.add_argument(
+ '--no-automated-clean',
+ action='store_false',
+ dest='automated_clean',
+ default=None,
+ help=_('Explicitly disable automated cleaning for the node'))
+ parser.add_argument(
+ '--owner',
+ metavar='',
+ help=_('Owner of the node.'))
+ parser.add_argument(
+ '--lessee',
+ metavar='',
+ help=_('Lessee of the node.'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_("Description for the node."))
+ parser.add_argument(
+ '--shard',
+ metavar='',
+ help=_("Shard for the node."))
+ parser.add_argument(
+ '--parent-node',
+ metavar='',
+ help=_('Parent node for the node being created.'))
+ parser.add_argument(
+ '--firmware-interface',
+ metavar='',
+ help=_('Firmware interface used by the node\'s driver. This is '
+ 'only applicable when the specified --driver is a '
+ 'hardware type.'))
+ parser.add_argument(
+ '--disable-power-off',
+ action='store_true',
+ dest='disable_power_off',
+ default=None,
+ help=_('Explicitly disable power off actions on the node'))
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
-
baremetal_client = self.app.client_manager.baremetal
- field_list = ['chassis_uuid', 'driver', 'driver_info',
- 'properties', 'extra', 'uuid', 'name',
- 'boot_interface', 'console_interface',
- 'deploy_interface', 'inspect_interface',
- 'management_interface', 'network_interface',
- 'power_interface', 'raid_interface',
- 'vendor_interface', 'resource_class']
+ field_list = ['automated_clean', 'chassis_uuid', 'disable_power_off',
+ 'driver', 'driver_info', 'properties', 'extra', 'uuid',
+ 'name', 'instance_name', 'conductor_group', 'owner',
+ 'description', 'lessee', 'shard', 'resource_class',
+ 'parent_node',
+ ] + ['%s_interface' % iface
+ for iface in SUPPORTED_INTERFACES]
fields = dict((k, v) for (k, v) in vars(parsed_args).items()
if k in field_list and not (v is None))
fields = utils.args_array_to_dict(fields, 'driver_info')
fields = utils.args_array_to_dict(fields, 'extra')
+ if parsed_args.network_data:
+ fields['network_data'] = utils.handle_json_arg(
+ parsed_args.network_data, 'static network configuration')
fields = utils.args_array_to_dict(fields, 'properties')
node = baremetal_client.node.create(**fields)._info
@@ -425,6 +634,7 @@ def take_action(self, parsed_args):
node.pop('ports', None)
node.pop('portgroups', None)
node.pop('states', None)
+ node.pop('volume', None)
node.setdefault('chassis_uuid', '')
@@ -464,18 +674,6 @@ def take_action(self, parsed_args):
raise exc.ClientException("\n".join(failures))
-class DeleteBaremetal(DeleteBaremetalNode):
- """Unregister a baremetal node. DEPRECATED"""
-
- # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle.
- log = logging.getLogger(__name__ + ".DeleteBaremetal")
-
- def take_action(self, parsed_args):
- self.log.warning("This command is deprecated. Instead, use "
- "'openstack baremetal node delete'.")
- super(DeleteBaremetal, self).take_action(parsed_args)
-
-
class DeployBaremetalNode(ProvisionStateWithWait):
"""Set provision state of baremetal node to 'deploy'"""
@@ -489,10 +687,19 @@ def get_parser(self, prog_name):
'--config-drive',
metavar='',
default=None,
- help=_("A gzipped, base64-encoded configuration drive string OR "
- "the path to the configuration drive file OR the path to a "
- "directory containing the config drive files. In case it's "
- "a directory, a config drive will be generated from it. "))
+ help=CONFIG_DRIVE_ARG_HELP)
+
+ parser.add_argument(
+ '--deploy-steps',
+ metavar='',
+ required=False,
+ default=None,
+ help=_("The deploy steps. May be the path to a YAML file "
+ "containing the deploy steps; OR '-', with the deploy "
+ "steps being read from standard input; OR a JSON string. "
+ "The value should be a list of deploy-step dictionaries; "
+ "each dictionary should have keys 'interface' and 'step', "
+ "and optional key 'args'."))
return parser
@@ -550,6 +757,26 @@ def get_parser(self, prog_name):
default=None,
help=_("Limit list to nodes not in maintenance mode"),
)
+ retired_group = parser.add_mutually_exclusive_group(required=False)
+ retired_group.add_argument(
+ '--retired',
+ dest='retired',
+ action='store_true',
+ default=None,
+ help=_("Limit list to retired nodes.")
+ )
+ retired_group.add_argument(
+ '--no-retired',
+ dest='retired',
+ action='store_false',
+ default=None,
+ help=_("Limit list to not retired nodes.")
+ )
+ parser.add_argument(
+ '--fault',
+ dest='fault',
+ metavar='',
+ help=_("List nodes in specified fault."))
associated_group = parser.add_mutually_exclusive_group()
associated_group.add_argument(
'--associated',
@@ -566,16 +793,67 @@ def get_parser(self, prog_name):
dest='provision_state',
metavar='',
help=_("List nodes in specified provision state."))
+ parser.add_argument(
+ '--driver',
+ dest='driver',
+ metavar='',
+ help=_("Limit list to nodes with driver "))
parser.add_argument(
'--resource-class',
dest='resource_class',
metavar='',
help=_("Limit list to nodes with resource class "))
+ parser.add_argument(
+ '--conductor-group',
+ metavar='',
+ help=_("Limit list to nodes with conductor group "))
+ parser.add_argument(
+ '--conductor',
+ metavar='',
+ help=_("Limit list to nodes with conductor "))
parser.add_argument(
'--chassis',
dest='chassis',
metavar='',
help=_("Limit list to nodes of this chassis"))
+ parser.add_argument(
+ '--owner',
+ metavar='',
+ help=_("Limit list to nodes with owner "
+ ""))
+ parser.add_argument(
+ '--lessee',
+ metavar='',
+ help=_("Limit list to nodes with lessee "
+ ""))
+ parser.add_argument(
+ '--description-contains',
+ metavar='',
+ help=_("Limit list to nodes with description contains "
+ ""))
+ parser.add_argument(
+ '--instance-name',
+ metavar='',
+ help=_("Filter the list of returned nodes by an instance name."))
+ sharded_group = parser.add_mutually_exclusive_group(required=False)
+ sharded_group.add_argument(
+ '--sharded',
+ dest='sharded',
+ help=_("List only nodes that are sharded."),
+ default=None,
+ action='store_true')
+ sharded_group.add_argument(
+ '--unsharded',
+ dest='sharded',
+ help=_("List only nodes that are not sharded."),
+ default=None,
+ action='store_false')
+ parser.add_argument(
+ '--shards',
+ nargs='+',
+ metavar='',
+ help=_("List only nodes that are in shards ."))
display_group = parser.add_mutually_exclusive_group(required=False)
display_group.add_argument(
'--long',
@@ -593,6 +871,18 @@ def get_parser(self, prog_name):
help=_("One or more node fields. Only these fields will be "
"fetched from the server. Can not be used when '--long' "
"is specified."))
+ children_group = parser.add_mutually_exclusive_group(required=False)
+ children_group.add_argument(
+ '--include-children',
+ action='store_true',
+ help=_("Include children in the node list."),
+ )
+ children_group.add_argument(
+ '--parent-node',
+ dest='parent_node',
+ metavar="",
+ help=_('List only nodes associated with a parent node.'),
+ )
return parser
def take_action(self, parsed_args):
@@ -600,7 +890,6 @@ def take_action(self, parsed_args):
client = self.app.client_manager.baremetal
columns = res_fields.NODE_RESOURCE.fields
- labels = res_fields.NODE_RESOURCE.labels
params = {}
if parsed_args.limit is not None and parsed_args.limit < 0:
@@ -613,24 +902,27 @@ def take_action(self, parsed_args):
params['associated'] = True
if parsed_args.unassociated:
params['associated'] = False
- if parsed_args.maintenance is not None:
- params['maintenance'] = parsed_args.maintenance
- if parsed_args.provision_state:
- params['provision_state'] = parsed_args.provision_state
- if parsed_args.resource_class:
- params['resource_class'] = parsed_args.resource_class
- if parsed_args.chassis:
- params['chassis'] = parsed_args.chassis
+
+ for field in ['maintenance', 'fault', 'conductor_group', 'retired',
+ 'sharded']:
+ if getattr(parsed_args, field) is not None:
+ params[field] = getattr(parsed_args, field)
+ for field in ['provision_state', 'driver', 'resource_class',
+ 'chassis', 'conductor', 'owner', 'lessee',
+ 'description_contains', 'shards', 'parent_node',
+ 'instance_name']:
+ if getattr(parsed_args, field):
+ params[field] = getattr(parsed_args, field)
+ if parsed_args.include_children:
+ params['include_children'] = True
if parsed_args.long:
params['detail'] = parsed_args.long
columns = res_fields.NODE_DETAILED_RESOURCE.fields
- labels = res_fields.NODE_DETAILED_RESOURCE.labels
elif parsed_args.fields:
params['detail'] = False
fields = itertools.chain.from_iterable(parsed_args.fields)
resource = res_fields.Resource(list(fields))
columns = resource.fields
- labels = resource.labels
params['fields'] = columns
self.log.debug("params(%s)", params)
@@ -638,21 +930,9 @@ def take_action(self, parsed_args):
data = oscutils.sort_items(data, parsed_args.sort)
- return (labels,
+ return (columns,
(oscutils.get_item_properties(s, columns, formatters={
- 'Properties': oscutils.format_dict},) for s in data))
-
-
-class ListBaremetal(ListBaremetalNode):
- """List baremetal nodes. DEPRECATED"""
-
- # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle.
- log = logging.getLogger(__name__ + ".ListBaremetal")
-
- def take_action(self, parsed_args):
- self.log.warning("This command is deprecated. Instead, use "
- "'openstack baremetal node list'.")
- return super(ListBaremetal, self).take_action(parsed_args)
+ 'Properties': utils.HashColumn},) for s in data))
class MaintenanceSetBaremetalNode(command.Command):
@@ -664,9 +944,10 @@ def get_parser(self, prog_name):
parser = super(MaintenanceSetBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node.")
+ nargs='+',
+ help=_("Names or UUID's of the nodes.")
)
parser.add_argument(
'--reason',
@@ -681,10 +962,9 @@ def take_action(self, parsed_args):
baremetal_client = self.app.client_manager.baremetal
- baremetal_client.node.set_maintenance(
- parsed_args.node,
- True,
- maint_reason=parsed_args.reason)
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_maintenance(
+ node, True, maint_reason=parsed_args.reason)
class MaintenanceUnsetBaremetalNode(command.Command):
@@ -697,9 +977,10 @@ def get_parser(self, prog_name):
self).get_parser(prog_name)
parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node.")
+ nargs='+',
+ help=_("Names or UUID's of the nodes.")
)
return parser
@@ -708,9 +989,8 @@ def take_action(self, parsed_args):
baremetal_client = self.app.client_manager.baremetal
- baremetal_client.node.set_maintenance(
- parsed_args.node,
- False)
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_maintenance(node, False)
class ManageBaremetalNode(ProvisionStateWithWait):
@@ -720,10 +1000,17 @@ class ManageBaremetalNode(ProvisionStateWithWait):
PROVISION_STATE = 'manage'
+class UnholdBaremetalNode(ProvisionStateBaremetalNode):
+ """Set provision state of baremetal node to 'unhold'"""
+
+ log = logging.getLogger(__name__ + ".UnholdBaremetalNode")
+ PROVISION_STATE = 'unhold'
+
+
class PassthruCallBaremetalNode(command.Command):
- """Call a vendor passthu method for a node"""
+ """Call a vendor passthru method for a node"""
- log = logging.getLogger(__name__ + ".PassthuCallBaremetalNode")
+ log = logging.getLogger(__name__ + ".PassthruCallBaremetalNode")
def get_parser(self, prog_name):
parser = super(PassthruCallBaremetalNode, self).get_parser(
@@ -810,7 +1097,7 @@ def take_action(self, parsed_args):
class PowerBaremetalNode(command.Command):
- """Set power state of baremetal node"""
+ """Base power state class, for setting the power of a node"""
log = logging.getLogger(__name__ + ".PowerBaremetalNode")
@@ -818,22 +1105,10 @@ def get_parser(self, prog_name):
parser = super(PowerBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
- 'power_state',
- metavar='',
- choices=['on', 'off'],
- help=_("Power node on or off")
- )
- parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node.")
- )
- parser.add_argument(
- '--soft',
- dest='soft',
- action='store_true',
- default=False,
- help=_("Request graceful power-off.")
+ nargs='+',
+ help=_("Names or UUID's of the nodes.")
)
parser.add_argument(
'--power-timeout',
@@ -850,9 +1125,37 @@ def take_action(self, parsed_args):
baremetal_client = self.app.client_manager.baremetal
- baremetal_client.node.set_power_state(
- parsed_args.node, parsed_args.power_state, parsed_args.soft,
- timeout=parsed_args.power_timeout)
+ soft = getattr(parsed_args, 'soft', False)
+
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_power_state(
+ node, self.POWER_STATE, soft,
+ timeout=parsed_args.power_timeout)
+
+
+class PowerOffBaremetalNode(PowerBaremetalNode):
+ """Power off a node"""
+
+ log = logging.getLogger(__name__ + ".PowerOffBaremetalNode")
+ POWER_STATE = 'off'
+
+ def get_parser(self, prog_name):
+ parser = super(PowerOffBaremetalNode, self).get_parser(prog_name)
+ parser.add_argument(
+ '--soft',
+ dest='soft',
+ action='store_true',
+ default=False,
+ help=_("Request graceful power-off.")
+ )
+ return parser
+
+
+class PowerOnBaremetalNode(PowerBaremetalNode):
+ """Power on a node"""
+
+ log = logging.getLogger(__name__ + ".PowerOnBaremetalNode")
+ POWER_STATE = 'on'
class ProvideBaremetalNode(ProvisionStateWithWait):
@@ -871,9 +1174,10 @@ def get_parser(self, prog_name):
parser = super(RebootBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node.")
+ nargs='+',
+ help=_("Names or UUID's of the nodes.")
)
parser.add_argument(
'--soft',
@@ -898,9 +1202,10 @@ def take_action(self, parsed_args):
baremetal_client = self.app.client_manager.baremetal
- baremetal_client.node.set_power_state(
- parsed_args.node, 'reboot', parsed_args.soft,
- timeout=parsed_args.power_timeout)
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_power_state(
+ node, 'reboot', parsed_args.soft,
+ timeout=parsed_args.power_timeout)
class RebuildBaremetalNode(ProvisionStateWithWait):
@@ -909,25 +1214,133 @@ class RebuildBaremetalNode(ProvisionStateWithWait):
log = logging.getLogger(__name__ + ".RebuildBaremetalNode")
PROVISION_STATE = 'rebuild'
+ def get_parser(self, prog_name):
+ parser = super(RebuildBaremetalNode, self).get_parser(prog_name)
+
+ parser.add_argument(
+ '--config-drive',
+ metavar='',
+ default=None,
+ help=CONFIG_DRIVE_ARG_HELP)
+
+ parser.add_argument(
+ '--deploy-steps',
+ metavar='',
+ required=False,
+ default=None,
+ help=_("The deploy steps in JSON format. May be the path to a "
+ "file containing the deploy steps; OR '-', with the deploy "
+ "steps being read from standard input; OR a string. The "
+ "value should be a list of deploy-step dictionaries; each "
+ "dictionary should have keys 'interface', 'step', "
+ "'priority' and optional key 'args'."))
+ return parser
+
+
+class RescueBaremetalNode(ProvisionStateWithWait):
+ """Set provision state of baremetal node to 'rescue'"""
+
+ log = logging.getLogger(__name__ + ".RescueBaremetalNode")
+ PROVISION_STATE = 'rescue'
+
+ def get_parser(self, prog_name):
+ parser = super(RescueBaremetalNode, self).get_parser(prog_name)
+
+ parser.add_argument(
+ '--rescue-password',
+ metavar='',
+ required=True,
+ default=None,
+ help=("The password that will be used to login to the rescue "
+ "ramdisk. The value should be a non-empty string."))
+ return parser
+
+
+class SecurebootOnBaremetalNode(command.Command):
+ """Turn secure boot on"""
+
+ log = logging.getLogger(__name__ + ".SecurebootOnBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(SecurebootOnBaremetalNode, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'nodes',
+ metavar='',
+ nargs='+',
+ help=_("Name or UUID of the node")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_secure_boot(node, 'on')
+
+
+class SecurebootOffBaremetalNode(command.Command):
+ """Turn secure boot off"""
+
+ log = logging.getLogger(__name__ + ".SecurebootOffBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(SecurebootOffBaremetalNode, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'nodes',
+ metavar='',
+ nargs='+',
+ help=_("Name or UUID of the node")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_secure_boot(node, 'off')
+
class SetBaremetalNode(command.Command):
"""Set baremetal properties"""
log = logging.getLogger(__name__ + ".SetBaremetalNode")
+ def _add_interface_args(self, parser, iface, set_help, reset_help):
+ grp = parser.add_mutually_exclusive_group()
+ grp.add_argument(
+ '--%s-interface' % iface,
+ metavar='<%s_interface>' % iface,
+ help=set_help
+ )
+ grp.add_argument(
+ '--reset-%s-interface' % iface,
+ action='store_true',
+ help=reset_help
+ )
+
def get_parser(self, prog_name):
parser = super(SetBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node."),
+ nargs='+',
+ help=_("Names or UUID's of the nodes."),
)
parser.add_argument(
"--instance-uuid",
metavar="",
help=_("Set instance UUID of node to "),
)
+ parser.add_argument(
+ "--instance-name",
+ metavar="",
+ help=_('Set the name of the instance deployed on this node.'),
+ )
parser.add_argument(
"--name",
metavar="",
@@ -943,61 +1356,143 @@ def get_parser(self, prog_name):
metavar="",
help=_("Set the driver for the node"),
)
- parser.add_argument(
- '--boot-interface',
- metavar='',
- help=_('Set the boot interface for the node'),
+ self._add_interface_args(
+ parser, 'bios',
+ set_help=_('Set the BIOS interface for the node'),
+ reset_help=_('Reset the BIOS interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'boot',
+ set_help=_('Set the boot interface for the node'),
+ reset_help=_('Reset the boot interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'console',
+ set_help=_('Set the console interface for the node'),
+ reset_help=_('Reset the console interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'deploy',
+ set_help=_('Set the deploy interface for the node'),
+ reset_help=_('Reset the deploy interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'firmware',
+ set_help=_('Set the firmware interface for the node'),
+ reset_help=_('Reset the firmware interface for its hardware '
+ 'type default'),
+ )
+ self._add_interface_args(
+ parser, 'inspect',
+ set_help=_('Set the inspect interface for the node'),
+ reset_help=_('Reset the inspect interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'management',
+ set_help=_('Set the management interface for the node'),
+ reset_help=_('Reset the management interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'network',
+ set_help=_('Set the network interface for the node'),
+ reset_help=_('Reset the network interface to its hardware type '
+ 'default'),
+ )
+ parser.add_argument(
+ '--network-data',
+ metavar="",
+ dest='network_data',
+ help=NETWORK_DATA_ARG_HELP
+ )
+ self._add_interface_args(
+ parser, 'power',
+ set_help=_('Set the power interface for the node'),
+ reset_help=_('Reset the power interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'raid',
+ set_help=_('Set the RAID interface for the node'),
+ reset_help=_('Reset the RAID interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'rescue',
+ set_help=_('Set the rescue interface for the node'),
+ reset_help=_('Reset the rescue interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'storage',
+ set_help=_('Set the storage interface for the node'),
+ reset_help=_('Reset the storage interface to its hardware type '
+ 'default'),
+ )
+ self._add_interface_args(
+ parser, 'vendor',
+ set_help=_('Set the vendor interface for the node'),
+ reset_help=_('Reset the vendor interface to its hardware type '
+ 'default'),
+ )
+ parser.add_argument(
+ '--reset-interfaces',
+ action='store_true', default=None,
+ help=_('Reset all interfaces not specified explicitly to their '
+ 'default implementations. Only valid with --driver.'),
)
parser.add_argument(
- '--console-interface',
- metavar='',
- help=_('Set the console interface for the node'),
+ '--resource-class',
+ metavar='',
+ help=_('Set the resource class for the node'),
)
parser.add_argument(
- '--deploy-interface',
- metavar='',
- help=_('Set the deploy interface for the node'),
+ '--conductor-group',
+ metavar='',
+ help=_('Set the conductor group for the node'),
)
+ clean = parser.add_mutually_exclusive_group()
+ clean.add_argument(
+ '--automated-clean',
+ action='store_true',
+ default=None,
+ help=_('Enable automated cleaning for the node'))
+ clean.add_argument(
+ '--no-automated-clean',
+ action='store_false',
+ dest='automated_clean',
+ default=None,
+ help=_('Explicitly disable automated cleaning for the node'))
parser.add_argument(
- '--inspect-interface',
- metavar='',
- help=_('Set the inspect interface for the node'),
- )
- parser.add_argument(
- '--management-interface',
- metavar='',
- help=_('Set the management interface for the node'),
- )
- parser.add_argument(
- '--network-interface',
- metavar='',
- help=_('Set the network interface for the node'),
- )
- parser.add_argument(
- '--power-interface',
- metavar='',
- help=_('Set the power interface for the node'),
+ '--protected',
+ action='store_true',
+ help=_('Mark the node as protected'),
)
parser.add_argument(
- '--raid-interface',
- metavar='',
- help=_('Set the RAID interface for the node'),
+ '--protected-reason',
+ metavar='',
+ help=_('Set the reason of marking the node as protected'),
)
parser.add_argument(
- '--vendor-interface',
- metavar='',
- help=_('Set the vendor interface for the node'),
+ '--retired',
+ action='store_true',
+ help=_('Mark the node as retired'),
)
parser.add_argument(
- '--resource-class',
- metavar='',
- help=_('Set the resource class for the node'),
+ '--retired-reason',
+ metavar='',
+ help=_('Set the reason of marking the node as retired'),
)
parser.add_argument(
'--target-raid-config',
metavar='',
help=_('Set the target RAID configuration (JSON) for the node. '
- 'This can be one of: 1. a file containing JSON data of the '
+ 'This can be one of: 1. a file containing YAML data of the '
'RAID configuration; 2. "-" to read the contents from '
'standard input; or 3. a valid JSON string.'),
)
@@ -1029,12 +1524,55 @@ def get_parser(self, prog_name):
help=_('Instance information to set on this baremetal node '
'(repeat option to set multiple instance infos)'),
)
+ parser.add_argument(
+ "--owner",
+ metavar='',
+ help=_('Set the owner for the node')),
+ parser.add_argument(
+ "--lessee",
+ metavar='',
+ help=_('Set the lessee for the node')),
+ parser.add_argument(
+ "--description",
+ metavar='',
+ help=_('Set the description for the node'),
+ )
+ parser.add_argument(
+ "--shard",
+ metavar='',
+ help=_('Set the shard for the node'),
+ )
+ parser.add_argument(
+ "--parent-node",
+ metavar='',
+ help=_('Set the parent node for the node'),
+ )
+ power_off = parser.add_mutually_exclusive_group()
+ power_off.add_argument(
+ '--enable-power-off',
+ action='store_false',
+ dest='disable_power_off',
+ default=None,
+ help=_('Explicitly enable power off actions on nodes'))
+ power_off.add_argument(
+ '--disable-power-off',
+ action='store_true',
+ dest='disable_power_off',
+ default=None,
+ help=_('Explicitly disable power off actions on nodes'))
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
+ if parsed_args.name and len(parsed_args.nodes) > 1:
+ raise exc.CommandError(
+ _("--name cannot be used with more than one node"))
+ if parsed_args.instance_uuid and len(parsed_args.nodes) > 1:
+ raise exc.CommandError(
+ _("--instance-uuid cannot be used with more than one node"))
+
baremetal_client = self.app.client_manager.baremetal
# NOTE(rloo): Do this before updating the rest. Otherwise, it won't
@@ -1042,79 +1580,46 @@ def take_action(self, parsed_args):
# also being modified.
if parsed_args.target_raid_config:
raid_config = parsed_args.target_raid_config
- if raid_config == '-':
- raid_config = utils.get_from_stdin('target_raid_config')
- raid_config = utils.handle_json_or_file_arg(raid_config)
- baremetal_client.node.set_target_raid_config(parsed_args.node,
- raid_config)
+ raid_config = utils.handle_json_arg(raid_config,
+ 'target_raid_config')
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_target_raid_config(node, raid_config)
properties = []
- if parsed_args.instance_uuid:
- instance_uuid = ["instance_uuid=%s" % parsed_args.instance_uuid]
- properties.extend(utils.args_array_to_patch(
- 'add', instance_uuid))
- if parsed_args.name:
- name = ["name=%s" % parsed_args.name]
- properties.extend(utils.args_array_to_patch(
- 'add', name))
- if parsed_args.chassis_uuid:
- chassis_uuid = ["chassis_uuid=%s" % parsed_args.chassis_uuid]
- properties.extend(utils.args_array_to_patch(
- 'add', chassis_uuid))
- if parsed_args.driver:
- driver = ["driver=%s" % parsed_args.driver]
- properties.extend(utils.args_array_to_patch(
- 'add', driver))
- if parsed_args.boot_interface:
- boot_interface = [
- "boot_interface=%s" % parsed_args.boot_interface]
- properties.extend(utils.args_array_to_patch(
- 'add', boot_interface))
- if parsed_args.console_interface:
- console_interface = [
- "console_interface=%s" % parsed_args.console_interface]
- properties.extend(utils.args_array_to_patch(
- 'add', console_interface))
- if parsed_args.deploy_interface:
- deploy_interface = [
- "deploy_interface=%s" % parsed_args.deploy_interface]
- properties.extend(utils.args_array_to_patch(
- 'add', deploy_interface))
- if parsed_args.inspect_interface:
- inspect_interface = [
- "inspect_interface=%s" % parsed_args.inspect_interface]
- properties.extend(utils.args_array_to_patch(
- 'add', inspect_interface))
- if parsed_args.management_interface:
- management_interface = [
- "management_interface=%s" % parsed_args.management_interface]
- properties.extend(utils.args_array_to_patch(
- 'add', management_interface))
- if parsed_args.network_interface:
- network_interface = [
- "network_interface=%s" % parsed_args.network_interface]
+ for field in ['instance_uuid', 'instance_name', 'name',
+ 'chassis_uuid', 'driver', 'resource_class',
+ 'conductor_group', 'protected', 'protected_reason',
+ 'retired', 'retired_reason', 'owner', 'lessee',
+ 'description', 'shard', 'parent_node']:
+ value = getattr(parsed_args, field)
+ if value:
+ properties.extend(utils.args_array_to_patch(
+ 'add', ["%s=%s" % (field, value)]))
+
+ if parsed_args.automated_clean is not None:
properties.extend(utils.args_array_to_patch(
- 'add', network_interface))
- if parsed_args.power_interface:
- power_interface = [
- "power_interface=%s" % parsed_args.power_interface]
- properties.extend(utils.args_array_to_patch(
- 'add', power_interface))
- if parsed_args.raid_interface:
- raid_interface = [
- "raid_interface=%s" % parsed_args.raid_interface]
- properties.extend(utils.args_array_to_patch(
- 'add', raid_interface))
- if parsed_args.vendor_interface:
- vendor_interface = [
- "vendor_interface=%s" % parsed_args.vendor_interface]
- properties.extend(utils.args_array_to_patch(
- 'add', vendor_interface))
- if parsed_args.resource_class:
- resource_class = [
- "resource_class=%s" % parsed_args.resource_class]
+ 'add', ["automated_clean=%s" % parsed_args.automated_clean]))
+
+ if parsed_args.disable_power_off is not None:
properties.extend(utils.args_array_to_patch(
- 'add', resource_class))
+ 'add', ["disable_power_off=%s" % parsed_args.disable_power_off]
+ ))
+
+ if parsed_args.reset_interfaces and not parsed_args.driver:
+ raise exc.CommandError(
+ _("--reset-interfaces can only be specified with --driver"))
+
+ for iface in SUPPORTED_INTERFACES:
+ field = '%s_interface' % iface
+ if getattr(parsed_args, field):
+ properties.extend(utils.args_array_to_patch(
+ 'add',
+ ["%s_interface=%s" % (iface,
+ getattr(parsed_args, field))]))
+ elif getattr(parsed_args, 'reset_%s_interface' % iface):
+ properties.extend(utils.args_array_to_patch(
+ 'remove', ['%s_interface' % iface]))
+
if parsed_args.property:
properties.extend(utils.args_array_to_patch(
'add', ['properties/' + x for x in parsed_args.property]))
@@ -1128,24 +1633,21 @@ def take_action(self, parsed_args):
properties.extend(utils.args_array_to_patch(
'add', ['instance_info/' + x for x
in parsed_args.instance_info]))
+ if parsed_args.network_data:
+ network_data = utils.handle_json_arg(
+ parsed_args.network_data, 'static network configuration')
+ network_data = ["network_data=%s" % json.dumps(network_data)]
+ properties.extend(utils.args_array_to_patch('add', network_data))
+
if properties:
- baremetal_client.node.update(parsed_args.node, properties)
- else:
+ for node in parsed_args.nodes:
+ baremetal_client.node.update(
+ node, properties,
+ reset_interfaces=parsed_args.reset_interfaces)
+ elif not parsed_args.target_raid_config:
self.log.warning("Please specify what to set.")
-class SetBaremetal(SetBaremetalNode):
- """Set baremetal properties. DEPRECATED"""
-
- # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle.
- log = logging.getLogger(__name__ + ".SetBaremetal")
-
- def take_action(self, parsed_args):
- self.log.warning("This command is deprecated. Instead, use "
- "'openstack baremetal node set'.")
- return super(SetBaremetal, self).take_action(parsed_args)
-
-
class ShowBaremetalNode(command.ShowOne):
"""Show baremetal node details"""
@@ -1192,6 +1694,7 @@ def take_action(self, parsed_args):
node.pop("ports", None)
node.pop('portgroups', None)
node.pop('states', None)
+ node.pop('volume', None)
if not fields or 'chassis_uuid' in fields:
node.setdefault('chassis_uuid', '')
@@ -1199,18 +1702,6 @@ def take_action(self, parsed_args):
return self.dict2columns(node)
-class ShowBaremetal(ShowBaremetalNode):
- """Show baremetal node details. DEPRECATED"""
-
- # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle.
- log = logging.getLogger(__name__ + ".ShowBaremetal")
-
- def take_action(self, parsed_args):
- self.log.warning("This command is deprecated. Instead, use "
- "'openstack baremetal node show'.")
- return super(ShowBaremetal, self).take_action(parsed_args)
-
-
class UndeployBaremetalNode(ProvisionStateWithWait):
"""Set provision state of baremetal node to 'deleted'"""
@@ -1218,6 +1709,13 @@ class UndeployBaremetalNode(ProvisionStateWithWait):
PROVISION_STATE = 'deleted'
+class UnrescueBaremetalNode(ProvisionStateWithWait):
+ """Set provision state of baremetal node to 'unrescue'"""
+
+ log = logging.getLogger(__name__ + ".UnrescueBaremetalNode")
+ PROVISION_STATE = 'unrescue'
+
+
class UnsetBaremetalNode(command.Command):
"""Unset baremetal properties"""
log = logging.getLogger(__name__ + ".UnsetBaremetalNode")
@@ -1226,9 +1724,10 @@ def get_parser(self, prog_name):
parser = super(UnsetBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
- 'node',
+ 'nodes',
metavar='',
- help=_("Name or UUID of the node.")
+ nargs='+',
+ help=_("Names or UUID's of the nodes.")
)
parser.add_argument(
'--instance-uuid',
@@ -1236,6 +1735,12 @@ def get_parser(self, prog_name):
default=False,
help=_('Unset instance UUID on this baremetal node')
)
+ parser.add_argument(
+ '--instance-name',
+ action='store_true',
+ default=False,
+ help=_('Unset instance name on this baremetal node')
+ )
parser.add_argument(
"--name",
action='store_true',
@@ -1271,14 +1776,15 @@ def get_parser(self, prog_name):
metavar="",
action='append',
help=_('Driver information to unset on this baremetal node '
- '(repeat option to unset multiple driver informations)'),
+ '(repeat option to unset multiple items '
+ 'in driver information)'),
)
parser.add_argument(
"--instance-info",
metavar="",
action='append',
help=_('Instance information to unset on this baremetal node '
- '(repeat option to unset multiple instance informations)'),
+ '(repeat option to unset multiple instance information)'),
)
parser.add_argument(
"--chassis-uuid",
@@ -1286,6 +1792,12 @@ def get_parser(self, prog_name):
action='store_true',
help=_('Unset chassis UUID on this baremetal node'),
)
+ parser.add_argument(
+ "--bios-interface",
+ dest='bios_interface',
+ action='store_true',
+ help=_('Unset BIOS interface on this baremetal node'),
+ )
parser.add_argument(
"--boot-interface",
dest='boot_interface',
@@ -1304,12 +1816,23 @@ def get_parser(self, prog_name):
action='store_true',
help=_('Unset deploy interface on this baremetal node'),
)
+ parser.add_argument(
+ "--firmware-interface",
+ dest='firmware_interface',
+ action='store_true',
+ help=_('Unset firmware interface on this baremetal node'),
+ )
parser.add_argument(
"--inspect-interface",
dest='inspect_interface',
action='store_true',
help=_('Unset inspect interface on this baremetal node'),
)
+ parser.add_argument(
+ '--network-data',
+ action='store_true',
+ help=_("Unset network data on this baremetal port.")
+ )
parser.add_argument(
"--management-interface",
dest='management_interface',
@@ -1334,12 +1857,83 @@ def get_parser(self, prog_name):
action='store_true',
help=_('Unset RAID interface on this baremetal node'),
)
+ parser.add_argument(
+ "--rescue-interface",
+ dest='rescue_interface',
+ action='store_true',
+ help=_('Unset rescue interface on this baremetal node'),
+ )
+ parser.add_argument(
+ "--storage-interface",
+ dest='storage_interface',
+ action='store_true',
+ help=_('Unset storage interface on this baremetal node'),
+ )
parser.add_argument(
"--vendor-interface",
dest='vendor_interface',
action='store_true',
help=_('Unset vendor interface on this baremetal node'),
)
+ parser.add_argument(
+ "--conductor-group",
+ action="store_true",
+ help=_('Unset conductor group for this baremetal node (the '
+ 'default group will be used)'),
+ )
+ parser.add_argument(
+ "--automated-clean",
+ action="store_true",
+ help=_('Unset automated clean option on this baremetal node '
+ '(the value from configuration will be used)'),
+ )
+ parser.add_argument(
+ "--protected",
+ action="store_true",
+ help=_('Unset the protected flag on the node'),
+ )
+ parser.add_argument(
+ "--protected-reason",
+ action="store_true",
+ help=_('Unset the protected reason (gets unset automatically when '
+ 'protected is unset)'),
+ )
+ parser.add_argument(
+ "--retired",
+ action="store_true",
+ help=_('Unset the retired flag on the node'),
+ )
+ parser.add_argument(
+ "--retired-reason",
+ action="store_true",
+ help=_('Unset the retired reason (gets unset automatically when '
+ 'retired is unset)'),
+ )
+ parser.add_argument(
+ "--owner",
+ action="store_true",
+ help=_('Unset the owner field of the node'),
+ )
+ parser.add_argument(
+ "--lessee",
+ action="store_true",
+ help=_('Unset the lessee field of the node'),
+ )
+ parser.add_argument(
+ "--description",
+ action="store_true",
+ help=_('Unset the description field of the node'),
+ )
+ parser.add_argument(
+ "--shard",
+ action="store_true",
+ help=_('Unset the shard field of the node'),
+ )
+ parser.add_argument(
+ "--parent-node",
+ action="store_true",
+ help=_('Unset the parent node field of the node'),
+ )
return parser
@@ -1352,18 +1946,24 @@ def take_action(self, parsed_args):
# work if parsed_args.node is the name and the name is
# also being removed.
if parsed_args.target_raid_config:
- baremetal_client.node.set_target_raid_config(parsed_args.node, {})
+ for node in parsed_args.nodes:
+ baremetal_client.node.set_target_raid_config(node, {})
properties = []
- if parsed_args.instance_uuid:
- properties.extend(utils.args_array_to_patch('remove',
- ['instance_uuid']))
- if parsed_args.name:
- properties.extend(utils.args_array_to_patch('remove',
- ['name']))
- if parsed_args.resource_class:
- properties.extend(utils.args_array_to_patch('remove',
- ['resource_class']))
+ for field in ['instance_uuid', 'instance_name', 'name', 'chassis_uuid',
+ 'resource_class', 'conductor_group', 'automated_clean',
+ 'bios_interface', 'boot_interface', 'console_interface',
+ 'deploy_interface', 'firmware_interface',
+ 'inspect_interface',
+ 'management_interface', 'network_interface',
+ 'power_interface', 'raid_interface', 'rescue_interface',
+ 'storage_interface', 'vendor_interface',
+ 'protected', 'protected_reason', 'retired',
+ 'retired_reason', 'owner', 'lessee', 'description',
+ 'shard', 'parent_node']:
+ if getattr(parsed_args, field):
+ properties.extend(utils.args_array_to_patch('remove', [field]))
+
if parsed_args.property:
properties.extend(utils.args_array_to_patch('remove',
['properties/' + x
@@ -1379,54 +1979,17 @@ def take_action(self, parsed_args):
properties.extend(utils.args_array_to_patch('remove',
['instance_info/' + x for x
in parsed_args.instance_info]))
- if parsed_args.chassis_uuid:
- properties.extend(utils.args_array_to_patch('remove',
- ['chassis_uuid']))
- if parsed_args.boot_interface:
- properties.extend(utils.args_array_to_patch('remove',
- ['boot_interface']))
- if parsed_args.console_interface:
- properties.extend(utils.args_array_to_patch('remove',
- ['console_interface']))
- if parsed_args.deploy_interface:
- properties.extend(utils.args_array_to_patch('remove',
- ['deploy_interface']))
- if parsed_args.inspect_interface:
- properties.extend(utils.args_array_to_patch('remove',
- ['inspect_interface']))
- if parsed_args.management_interface:
- properties.extend(utils.args_array_to_patch('remove',
- ['management_interface']))
- if parsed_args.network_interface:
- properties.extend(utils.args_array_to_patch('remove',
- ['network_interface']))
- if parsed_args.power_interface:
- properties.extend(utils.args_array_to_patch('remove',
- ['power_interface']))
- if parsed_args.raid_interface:
- properties.extend(utils.args_array_to_patch('remove',
- ['raid_interface']))
- if parsed_args.vendor_interface:
- properties.extend(utils.args_array_to_patch('remove',
- ['vendor_interface']))
+ if parsed_args.network_data:
+ properties.extend(utils.args_array_to_patch(
+ 'remove', ["network_data"]))
+
if properties:
- baremetal_client.node.update(parsed_args.node, properties)
- else:
+ for node in parsed_args.nodes:
+ baremetal_client.node.update(node, properties)
+ elif not parsed_args.target_raid_config:
self.log.warning("Please specify what to unset.")
-class UnsetBaremetal(UnsetBaremetalNode):
- """Unset baremetal properties. DEPRECATED"""
-
- # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle.
- log = logging.getLogger(__name__ + ".UnsetBaremetal")
-
- def take_action(self, parsed_args):
- self.log.warning("This command is deprecated. Instead, use "
- "'openstack baremetal node unset'.")
- super(UnsetBaremetal, self).take_action(parsed_args)
-
-
class ValidateBaremetalNode(command.Lister):
"""Validate a node's driver interfaces"""
@@ -1479,12 +2042,11 @@ def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
columns = res_fields.VIF_RESOURCE.fields
- labels = res_fields.VIF_RESOURCE.labels
baremetal_client = self.app.client_manager.baremetal
data = baremetal_client.node.vif_list(parsed_args.node)
- return (labels,
+ return (columns,
(oscutils.get_item_properties(s, columns) for s in data))
@@ -1506,6 +2068,11 @@ def get_parser(self, prog_name):
metavar='',
help=_("Name or UUID of the VIF to attach to a node.")
)
+ parser.add_argument(
+ '--port-uuid',
+ metavar='',
+ help=_("UUID of the baremetal port to attach the VIF to.")
+ )
parser.add_argument(
'--vif-info',
metavar='',
@@ -1521,6 +2088,8 @@ def take_action(self, parsed_args):
baremetal_client = self.app.client_manager.baremetal
fields = utils.key_value_pairs_to_dict(parsed_args.vif_info or [])
+ if parsed_args.port_uuid:
+ fields['port_uuid'] = parsed_args.port_uuid
baremetal_client.node.vif_attach(parsed_args.node, parsed_args.vif_id,
**fields)
@@ -1560,6 +2129,336 @@ class InjectNmiBaremetalNode(command.Command):
def get_parser(self, prog_name):
parser = super(InjectNmiBaremetalNode, self).get_parser(prog_name)
+ parser.add_argument(
+ 'nodes',
+ metavar='',
+ nargs='+',
+ help=_("Names or UUID's of the nodes.")
+ )
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ for node in parsed_args.nodes:
+ baremetal_client.node.inject_nmi(node)
+
+
+class ListTraitsBaremetalNode(command.Lister):
+ """List a node's traits."""
+
+ log = logging.getLogger(__name__ + ".ListTraitsBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(ListTraitsBaremetalNode, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'node',
+ metavar='',
+ help=_("Name or UUID of the node"))
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ columns = res_fields.TRAIT_RESOURCE.fields
+
+ baremetal_client = self.app.client_manager.baremetal
+ traits = baremetal_client.node.get_traits(parsed_args.node)
+
+ return (columns, [[trait] for trait in traits])
+
+
+class AddTraitBaremetalNode(command.Command):
+ """Add traits to a node."""
+
+ log = logging.getLogger(__name__ + ".AddTraitBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(AddTraitBaremetalNode, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'node',
+ metavar='',
+ help=_("Name or UUID of the node"))
+ parser.add_argument(
+ 'traits',
+ nargs='+',
+ metavar='',
+ help=_("Trait(s) to add"))
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ failures = []
+ for trait in parsed_args.traits:
+ try:
+ baremetal_client.node.add_trait(parsed_args.node, trait)
+ print(_('Added trait %s') % trait)
+ except exc.ClientException as e:
+ failures.append(_("Failed to add trait %(trait)s: %(error)s")
+ % {'trait': trait, 'error': e})
+
+ if failures:
+ raise exc.ClientException("\n".join(failures))
+
+
+class RemoveTraitBaremetalNode(command.Command):
+ """Remove trait(s) from a node."""
+
+ log = logging.getLogger(__name__ + ".RemoveTraitBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(RemoveTraitBaremetalNode, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'node',
+ metavar='',
+ help=_("Name or UUID of the node"))
+ all_or_trait = parser.add_mutually_exclusive_group(required=True)
+ all_or_trait.add_argument(
+ '--all',
+ dest='remove_all',
+ action='store_true',
+ help=_("Remove all traits"))
+ all_or_trait.add_argument(
+ 'traits',
+ metavar='',
+ nargs='*',
+ default=[],
+ help=_("Trait(s) to remove"))
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ failures = []
+ if parsed_args.remove_all:
+ baremetal_client.node.remove_all_traits(parsed_args.node)
+ else:
+ for trait in parsed_args.traits:
+ try:
+ baremetal_client.node.remove_trait(parsed_args.node, trait)
+ print(_('Removed trait %s') % trait)
+ except exc.ClientException as e:
+ failures.append(_("Failed to remove trait %(trait)s: "
+ "%(error)s")
+ % {'trait': trait, 'error': e})
+
+ if failures:
+ raise exc.ClientException("\n".join(failures))
+
+
+class ListBIOSSettingBaremetalNode(command.Lister):
+ """List a node's BIOS settings."""
+
+ log = logging.getLogger(__name__ + ".ListBIOSSettingBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(ListBIOSSettingBaremetalNode, self).get_parser(
+ prog_name)
+
+ parser.add_argument(
+ 'node',
+ metavar='',
+ help=_("Name or UUID of the node")
+ )
+ display_group = parser.add_mutually_exclusive_group(required=False)
+ display_group.add_argument(
+ '--long',
+ default=False,
+ help=_("Show detailed information about the BIOS settings."),
+ action='store_true')
+ display_group.add_argument(
+ '--fields',
+ nargs='+',
+ dest='fields',
+ metavar='',
+ action='append',
+ default=[],
+ choices=res_fields.BIOS_DETAILED_RESOURCE.fields,
+ help=_("One or more node fields. Only these fields will be "
+ "fetched from the server. Can not be used when '--long' "
+ "is specified."))
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ fields = res_fields.BIOS_RESOURCE.fields
+
+ params = {}
+ if parsed_args.long:
+ params['detail'] = parsed_args.long
+ fields = res_fields.BIOS_DETAILED_RESOURCE.fields
+ elif parsed_args.fields:
+ params['detail'] = False
+ fields = itertools.chain.from_iterable(parsed_args.fields)
+ resource = res_fields.Resource(list(fields))
+ fields = resource.fields
+ params['fields'] = fields
+
+ self.log.debug("params(%s)", params)
+
+ baremetal_client = self.app.client_manager.baremetal
+ settings = baremetal_client.node.list_bios_settings(parsed_args.node,
+ **params)
+
+ return (fields,
+ (oscutils.get_dict_properties(s, fields) for s in settings))
+
+
+class BIOSSettingShowBaremetalNode(command.ShowOne):
+ """Show a specific BIOS setting for a node."""
+
+ log = logging.getLogger(__name__ + ".BIOSSettingShowBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(BIOSSettingShowBaremetalNode, self).get_parser(
+ prog_name)
+
+ parser.add_argument(
+ 'node',
+ metavar='',
+ help=_("Name or UUID of the node")
+ )
+ parser.add_argument(
+ 'setting_name',
+ metavar='',
+ help=_("Setting name to show")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ setting = baremetal_client.node.get_bios_setting(
+ parsed_args.node, parsed_args.setting_name)
+ setting.pop("links", None)
+ return self.dict2columns(setting)
+
+
+class NodeHistoryList(command.Lister):
+ """Get history events for a baremetal node."""
+
+ log = logging.getLogger(__name__ + ".NodeHistoryList")
+
+ def get_parser(self, prog_name):
+ parser = super(NodeHistoryList, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'node',
+ metavar='',
+ help=_("Name or UUID of the node.")
+ )
+ parser.add_argument(
+ '--long',
+ default=False,
+ help=_("Show detailed information about the node history events."),
+ action='store_true')
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ if parsed_args.long:
+ fields = res_fields.NODE_HISTORY_DETAILED_RESOURCE.fields
+ else:
+ fields = res_fields.NODE_HISTORY_RESOURCE.fields
+
+ data = baremetal_client.node.get_history_list(
+ parsed_args.node,
+ parsed_args.long)
+
+ return (fields,
+ (oscutils.get_dict_properties(s, fields) for s in data))
+
+
+class NodeHistoryEventGet(command.ShowOne):
+ """Get history event for a baremetal node."""
+
+ log = logging.getLogger(__name__ + ".NodeHistoryEventGet")
+
+ def get_parser(self, prog_name):
+ parser = super(NodeHistoryEventGet, self).get_parser(prog_name)
+
+ parser.add_argument(
+ 'node',
+ metavar='',
+ help=_("Name or UUID of the node.")
+ )
+
+ parser.add_argument(
+ 'event',
+ metavar='',
+ help=_("UUID of the event.")
+ )
+
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+
+ data = baremetal_client.node.get_history_event(
+ parsed_args.node,
+ parsed_args.event)
+ data.pop('links')
+
+ return self.dict2columns(data)
+
+
+class NodeInventorySave(command.Command):
+ """Get hardware inventory of a node (in JSON format) or save it to file."""
+
+ log = logging.getLogger(__name__ + ".NodeInventorySave")
+
+ def get_parser(self, prog_name):
+ parser = super(NodeInventorySave, self).get_parser(prog_name)
+ parser.add_argument(
+ "node",
+ metavar="",
+ help=_("Name or UUID of the node"))
+ parser.add_argument("--file",
+ metavar="",
+ help="Save inspection data to file with name "
+ "(default: stdout).")
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ baremetal_client = self.app.client_manager.baremetal
+ inventory = baremetal_client.node.get_inventory(parsed_args.node)
+
+ if parsed_args.file:
+ with open(parsed_args.file, 'w') as fp:
+ json.dump(inventory, fp)
+ else:
+ json.dump(inventory, sys.stdout)
+
+
+class NodeChildrenList(command.ShowOne):
+ """Get a list of nodes associated as children."""
+
+ log = logging.getLogger(__name__ + ".NodeChildrenList")
+
+ def get_parser(self, prog_name):
+ parser = super(NodeChildrenList, self).get_parser(prog_name)
+
parser.add_argument(
'node',
metavar='',
@@ -1573,4 +2472,37 @@ def take_action(self, parsed_args):
baremetal_client = self.app.client_manager.baremetal
- baremetal_client.node.inject_nmi(parsed_args.node)
+ columns = res_fields.CHILDREN_RESOURCE.fields
+
+ data = baremetal_client.node.list_children_of_node(
+ parsed_args.node)
+ return (columns, [[node] for node in data])
+
+
+class ListFirmwareComponentBaremetalNode(command.Lister):
+ """List all Firmware Components of a node"""
+
+ log = logging.getLogger(__name__ + ".ListFirmwareComponentBaremetalNode")
+
+ def get_parser(self, prog_name):
+ parser = super(ListFirmwareComponentBaremetalNode, self).get_parser(
+ prog_name)
+
+ parser.add_argument(
+ 'node',
+ metavar='',
+ help=_("Name or UUID of the node")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ fields = res_fields.FIRMWARE_RESOURCE.fields
+
+ baremetal_client = self.app.client_manager.baremetal
+ components = baremetal_client.node.list_firmware_components(
+ parsed_args.node)
+
+ return (fields,
+ (oscutils.get_dict_properties(s, fields) for s in components))
diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py
index f0a3dc64e..8e73c490c 100644
--- a/ironicclient/osc/v1/baremetal_port.py
+++ b/ironicclient/osc/v1/baremetal_port.py
@@ -46,32 +46,28 @@ def get_parser(self, prog_name):
required=True,
help=_('UUID of the node that this port belongs to.')
)
+ parser.add_argument(
+ '--uuid',
+ dest='uuid',
+ metavar='',
+ help=_('UUID of the port.'))
parser.add_argument(
'--extra',
metavar="",
action='append',
help=_("Record arbitrary key/value metadata. "
- "Can be specified multiple times.")
+ "Argument can be specified multiple times.")
)
parser.add_argument(
'--local-link-connection',
metavar="",
action='append',
help=_("Key/value metadata describing Local link connection "
- "information. Valid keys are switch_info, switch_id, "
- "port_id; switch_id and port_id are obligatory. Can be "
- "specified multiple times.")
- )
- parser.add_argument(
- '-l',
- dest='local_link_connection_deprecated',
- metavar="",
- action='append',
- help=_("DEPRECATED. Please use --local-link-connection instead. "
- "Key/value metadata describing Local link connection "
- "information. Valid keys are switch_info, switch_id, "
- "port_id; switch_id and port_id are obligatory. Can be "
- "specified multiple times.")
+ "information. Valid keys are 'switch_info', 'switch_id', "
+ "'port_id' and 'hostname'. The keys 'switch_id' and "
+ "'port_id' are required. In case of a Smart NIC port, "
+ "the required keys are 'port_id' and 'hostname'. "
+ "Argument can be specified multiple times.")
)
parser.add_argument(
'--pxe-enabled',
@@ -86,30 +82,59 @@ def get_parser(self, prog_name):
metavar='',
help=_("UUID of the port group that this port belongs to."))
+ parser.add_argument(
+ '--physical-network',
+ dest='physical_network',
+ metavar='',
+ help=_("Name of the physical network to which this port is "
+ "connected."))
+
+ parser.add_argument(
+ '--is-smartnic',
+ dest='is_smartnic',
+ action='store_true',
+ help=_("Indicates whether this Port is a Smart NIC port"))
+
+ parser.add_argument(
+ '--name',
+ dest='name',
+ metavar='',
+ help=_("Name of the port."))
+
+ parser.add_argument(
+ '--description',
+ dest='description',
+ metavar='',
+ help=_("An optional description for the port."))
+
+ parser.add_argument(
+ '--vendor',
+ dest='vendor',
+ metavar='',
+ help=_("An optional vendor for the port."))
+
+ parser.add_argument(
+ '--category',
+ dest='category',
+ metavar='',
+ help=_("An optional category for the port."))
+
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
baremetal_client = self.app.client_manager.baremetal
- if parsed_args.local_link_connection_deprecated:
- self.log.warning("Please use --local-link-connection instead "
- "of -l, as it is deprecated and will be "
- "removed in future releases.")
- # It is parsed to either None, or to an array
- if parsed_args.local_link_connection:
- parsed_args.local_link_connection.extend(
- parsed_args.local_link_connection_deprecated)
- else:
- parsed_args.local_link_connection = (
- parsed_args.local_link_connection_deprecated)
-
- field_list = ['address', 'extra', 'node_uuid', 'pxe_enabled',
- 'local_link_connection', 'portgroup_uuid']
+ field_list = ['address', 'uuid', 'extra', 'node_uuid', 'pxe_enabled',
+ 'local_link_connection', 'portgroup_uuid',
+ 'physical_network', 'name', 'description', 'vendor',
+ 'category']
fields = dict((k, v) for (k, v) in vars(parsed_args).items()
if k in field_list and v is not None)
fields = utils.args_array_to_dict(fields, 'extra')
fields = utils.args_array_to_dict(fields, 'local_link_connection')
+ if parsed_args.is_smartnic:
+ fields['is_smartnic'] = parsed_args.is_smartnic
port = baremetal_client.port.create(**fields)
data = dict([(f, getattr(port, f, '')) for f in
@@ -196,6 +221,42 @@ def get_parser(self, prog_name):
dest='portgroup',
help=_("Remove port from the port group"))
+ parser.add_argument(
+ '--physical-network',
+ action='store_true',
+ dest='physical_network',
+ help=_("Unset the physical network on this baremetal port."))
+
+ parser.add_argument(
+ '--is-smartnic',
+ dest='is_smartnic',
+ action='store_true',
+ help=_("Set Port as not Smart NIC port"))
+
+ parser.add_argument(
+ '--name',
+ dest='name',
+ action='store_true',
+ help=_("Unset the name for this port"))
+
+ parser.add_argument(
+ '--description',
+ dest='description',
+ action='store_true',
+ help=_("Unset the description for this port."))
+
+ parser.add_argument(
+ '--vendor',
+ dest='vendor',
+ action='store_true',
+ help=_("Unset the vendor for this port."))
+
+ parser.add_argument(
+ '--category',
+ dest='category',
+ action='store_true',
+ help=_("Unset the category for this port."))
+
return parser
def take_action(self, parsed_args):
@@ -210,6 +271,24 @@ def take_action(self, parsed_args):
if parsed_args.portgroup:
properties.extend(utils.args_array_to_patch('remove',
['portgroup_uuid']))
+ if parsed_args.physical_network:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['physical_network']))
+ if parsed_args.is_smartnic:
+ properties.extend(utils.args_array_to_patch(
+ 'add', ["is_smartnic=False"]))
+ if parsed_args.name:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['name']))
+ if parsed_args.description:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['description']))
+ if parsed_args.vendor:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['vendor']))
+ if parsed_args.category:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['category']))
if properties:
baremetal_client.port.update(parsed_args.port, properties)
@@ -249,12 +328,75 @@ def get_parser(self, prog_name):
help=_('Extra to set on this baremetal port '
'(repeat option to set multiple extras)')
)
-
parser.add_argument(
"--port-group",
metavar="",
dest='portgroup_uuid',
help=_('Set UUID of the port group that this port belongs to.'))
+ parser.add_argument(
+ "--local-link-connection",
+ metavar="",
+ action='append',
+ help=_("Key/value metadata describing local link connection "
+ "information. Valid keys are 'switch_info', 'switch_id', "
+ "'port_id' and 'hostname'. The keys 'switch_id' and "
+ "'port_id' are required. In case of a Smart NIC port, "
+ "the required keys are 'port_id' and 'hostname'. "
+ "Argument can be specified multiple times.")
+ )
+ pxe_enabled_group = parser.add_mutually_exclusive_group(required=False)
+ pxe_enabled_group.add_argument(
+ "--pxe-enabled",
+ dest='pxe_enabled',
+ default=None,
+ action='store_true',
+ help=_("Indicates that this port should be used when "
+ "PXE booting this node (default)")
+ )
+ pxe_enabled_group.add_argument(
+ "--pxe-disabled",
+ dest='pxe_enabled',
+ default=None,
+ action='store_false',
+ help=_("Indicates that this port should not be used when "
+ "PXE booting this node")
+ )
+ parser.add_argument(
+ '--physical-network',
+ metavar='',
+ dest='physical_network',
+ help=_("Set the name of the physical network to which this port "
+ "is connected."))
+
+ parser.add_argument(
+ '--is-smartnic',
+ dest='is_smartnic',
+ action='store_true',
+ help=_("Set port to be Smart NIC port"))
+
+ parser.add_argument(
+ '--name',
+ metavar='',
+ dest='name',
+ help=_("Set name for this port"))
+
+ parser.add_argument(
+ '--description',
+ metavar='',
+ dest='description',
+ help=_("Set a description for this port"))
+
+ parser.add_argument(
+ '--vendor',
+ metavar='',
+ dest='vendor',
+ help=_("Set a vendor for this port"))
+
+ parser.add_argument(
+ '--category',
+ metavar='',
+ dest='category',
+ help=_("Set a category for this port"))
return parser
@@ -277,6 +419,36 @@ def take_action(self, parsed_args):
if parsed_args.portgroup_uuid:
portgroup_uuid = ["portgroup_uuid=%s" % parsed_args.portgroup_uuid]
properties.extend(utils.args_array_to_patch('add', portgroup_uuid))
+ if parsed_args.local_link_connection:
+ properties.extend(utils.args_array_to_patch(
+ 'add', ['local_link_connection/' + x for x in
+ parsed_args.local_link_connection]))
+ if parsed_args.pxe_enabled is not None:
+ properties.extend(utils.args_array_to_patch(
+ 'add', ['pxe_enabled=%s' % parsed_args.pxe_enabled]))
+ if parsed_args.physical_network:
+ physical_network = ["physical_network=%s" %
+ parsed_args.physical_network]
+ properties.extend(utils.args_array_to_patch('add',
+ physical_network))
+ if parsed_args.is_smartnic:
+ is_smartnic = ["is_smartnic=%s" % parsed_args.is_smartnic]
+ properties.extend(utils.args_array_to_patch('add', is_smartnic))
+ if parsed_args.name:
+ port_name = ["name=%s" % parsed_args.name]
+ properties.extend(utils.args_array_to_patch('add', port_name))
+ if parsed_args.description:
+ port_description = ["description=%s" % parsed_args.description]
+ properties.extend(utils.args_array_to_patch('add',
+ port_description))
+ if parsed_args.vendor:
+ port_vendor = ["vendor=%s" % parsed_args.vendor]
+ properties.extend(utils.args_array_to_patch('add',
+ port_vendor))
+ if parsed_args.category:
+ port_category = ["category=%s" % parsed_args.category]
+ properties.extend(utils.args_array_to_patch('add',
+ port_category))
if properties:
baremetal_client.port.update(parsed_args.port, properties)
@@ -348,7 +520,7 @@ def get_parser(self, prog_name):
type=int,
help=_('Maximum number of ports to return per request, '
'0 for no limit. Default is the maximum number used '
- 'by the Ironic API Service.')
+ 'by the Baremetal API Service.')
)
parser.add_argument(
'--marker',
@@ -391,7 +563,6 @@ def take_action(self, parsed_args):
client = self.app.client_manager.baremetal
columns = res_fields.PORT_RESOURCE.fields
- labels = res_fields.PORT_RESOURCE.labels
params = {}
if parsed_args.limit is not None and parsed_args.limit < 0:
@@ -411,14 +582,12 @@ def take_action(self, parsed_args):
if parsed_args.detail:
params['detail'] = parsed_args.detail
columns = res_fields.PORT_DETAILED_RESOURCE.fields
- labels = res_fields.PORT_DETAILED_RESOURCE.labels
elif parsed_args.fields:
params['detail'] = False
fields = itertools.chain.from_iterable(parsed_args.fields)
resource = res_fields.Resource(list(fields))
columns = resource.fields
- labels = resource.labels
params['fields'] = columns
self.log.debug("params(%s)", params)
@@ -426,6 +595,6 @@ def take_action(self, parsed_args):
data = oscutils.sort_items(data, parsed_args.sort)
- return (labels,
+ return (columns,
(oscutils.get_item_properties(s, columns, formatters={
- 'extra': oscutils.format_dict},) for s in data))
+ 'extra': utils.HashColumn},) for s in data))
diff --git a/ironicclient/osc/v1/baremetal_portgroup.py b/ironicclient/osc/v1/baremetal_portgroup.py
index 20c7625a5..246557bf0 100644
--- a/ironicclient/osc/v1/baremetal_portgroup.py
+++ b/ironicclient/osc/v1/baremetal_portgroup.py
@@ -81,6 +81,17 @@ def get_parser(self, prog_name):
action='store_true',
help=_("Ports that are members of this port group "
"cannot be used as stand-alone ports."))
+ parser.add_argument(
+ '--physical-network',
+ dest='physical_network',
+ metavar='',
+ help=_("Name of the physical network to which the ports in "
+ "this port group are connected."))
+ parser.add_argument(
+ '--category',
+ dest='category',
+ metavar='',
+ help=_("An optional category for the port group."))
return parser
@@ -89,7 +100,7 @@ def take_action(self, parsed_args):
baremetal_client = self.app.client_manager.baremetal
field_list = ['node_uuid', 'address', 'name', 'uuid', 'extra', 'mode',
- 'properties']
+ 'properties', 'physical_network', 'category']
fields = dict((k, v) for (k, v) in vars(parsed_args).items()
if k in field_list and v is not None)
if parsed_args.support_standalone_ports:
@@ -193,6 +204,11 @@ def get_parser(self, prog_name):
dest='node',
metavar='',
help=_("Only list port groups of this node (name or UUID)."))
+ parser.add_argument(
+ '--shards',
+ nargs='+',
+ metavar='',
+ help=_("Only list port groups of nodes in these shards."))
display_group = parser.add_mutually_exclusive_group(required=False)
display_group.add_argument(
@@ -219,7 +235,6 @@ def take_action(self, parsed_args):
client = self.app.client_manager.baremetal
columns = res_fields.PORTGROUP_RESOURCE.fields
- labels = res_fields.PORTGROUP_RESOURCE.labels
params = {}
if parsed_args.limit is not None and parsed_args.limit < 0:
@@ -232,17 +247,17 @@ def take_action(self, parsed_args):
params['address'] = parsed_args.address
if parsed_args.node is not None:
params['node'] = parsed_args.node
+ if parsed_args.shards:
+ params['shards'] = parsed_args.shards
if parsed_args.detail:
params['detail'] = parsed_args.detail
columns = res_fields.PORTGROUP_DETAILED_RESOURCE.fields
- labels = res_fields.PORTGROUP_DETAILED_RESOURCE.labels
elif parsed_args.fields:
params['detail'] = False
fields = itertools.chain.from_iterable(parsed_args.fields)
resource = res_fields.Resource(list(fields))
columns = resource.fields
- labels = resource.labels
params['fields'] = columns
self.log.debug("params(%s)", params)
@@ -250,9 +265,9 @@ def take_action(self, parsed_args):
data = oscutils.sort_items(data, parsed_args.sort)
- return (labels,
+ return (columns,
(oscutils.get_item_properties(s, columns, formatters={
- 'Properties': oscutils.format_dict},) for s in data))
+ 'Properties': utils.HashColumn},) for s in data))
class DeleteBaremetalPortGroup(command.Command):
@@ -352,6 +367,17 @@ def get_parser(self, prog_name):
help=_("Ports that are members of this port group "
"cannot be used as stand-alone ports.")
)
+ parser.add_argument(
+ '--physical-network',
+ metavar='',
+ dest='physical_network',
+ help=_("Set a physical network for the ports in this port group"))
+
+ parser.add_argument(
+ '--category',
+ metavar='',
+ dest='category',
+ help=_("Set a category for this port group"))
return parser
@@ -371,6 +397,16 @@ def take_action(self, parsed_args):
name = ["name=%s" % parsed_args.name]
properties.extend(utils.args_array_to_patch(
'add', name))
+ if parsed_args.physical_network:
+ physical_network = [
+ f"physical_network={parsed_args.physical_network}"
+ ]
+ properties.extend(utils.args_array_to_patch(
+ 'add', physical_network))
+ if parsed_args.category:
+ category = [f"category={parsed_args.category}"]
+ properties.extend(utils.args_array_to_patch(
+ 'add', category))
if parsed_args.support_standalone_ports:
properties.extend(utils.args_array_to_patch(
'add', ["standalone_ports_supported=True"]))
@@ -379,7 +415,7 @@ def take_action(self, parsed_args):
'add', ["standalone_ports_supported=False"]))
if parsed_args.mode:
properties.extend(utils.args_array_to_patch(
- 'add', ["mode=%s" % parsed_args.mode]))
+ 'add', ["mode=\"%s\"" % parsed_args.mode]))
if parsed_args.extra:
properties.extend(utils.args_array_to_patch(
@@ -432,6 +468,17 @@ def get_parser(self, prog_name):
help=_('Property to unset on this baremetal port group '
'(repeat option to unset multiple properties).'),
)
+ parser.add_argument(
+ '--physical-network',
+ action='store_true',
+ dest='physical_network',
+ help=_("Unset the physical network on this baremetal port group."))
+
+ parser.add_argument(
+ '--category',
+ dest='category',
+ action='store_true',
+ help=_("Unset the category for this port group."))
return parser
@@ -447,6 +494,12 @@ def take_action(self, parsed_args):
if parsed_args.address:
properties.extend(utils.args_array_to_patch('remove',
['address']))
+ if parsed_args.physical_network:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['physical_network']))
+ if parsed_args.category:
+ properties.extend(utils.args_array_to_patch('remove',
+ ['category']))
if parsed_args.extra:
properties.extend(utils.args_array_to_patch('remove',
['extra/' + x for x in parsed_args.extra]))
diff --git a/ironicclient/osc/v1/baremetal_runbook.py b/ironicclient/osc/v1/baremetal_runbook.py
new file mode 100644
index 000000000..9591c9138
--- /dev/null
+++ b/ironicclient/osc/v1/baremetal_runbook.py
@@ -0,0 +1,411 @@
+# 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 itertools
+import json
+import logging
+
+from osc_lib.command import command
+from osc_lib import utils as oscutils
+
+from ironicclient.common.i18n import _
+from ironicclient.common import utils
+from ironicclient import exc
+from ironicclient.v1 import resource_fields as res_fields
+
+
+_RUNBOOK_STEPS_HELP = _(
+ "The runbook steps. May be the path to a YAML file containing the "
+ "runbook steps; OR '-', with the runbook steps being read from standard "
+ "input; OR a JSON string. The value should be a list of runbook step "
+ "dictionaries; each dictionary should have keys 'interface', 'step', "
+ "'args' and 'order'.")
+
+
+class CreateBaremetalRunbook(command.ShowOne):
+ """Create a new runbook"""
+
+ log = logging.getLogger(__name__ + ".CreateBaremetalRunbook")
+
+ def get_parser(self, prog_name):
+ parser = super(CreateBaremetalRunbook, self).get_parser(
+ prog_name)
+
+ parser.add_argument(
+ '--name',
+ metavar='',
+ required=True,
+ help=_('Unique name for this runbook. Must be a valid '
+ 'trait name')
+ )
+ parser.add_argument(
+ '--uuid',
+ dest='uuid',
+ metavar='',
+ help=_('UUID of the runbook.'))
+ parser.add_argument(
+ '--public',
+ dest='public',
+ nargs='?',
+ const='true',
+ metavar='',
+ help=_('Whether the runbook will be private or public.')
+ )
+ parser.add_argument(
+ '--owner',
+ metavar='',
+ help=_('Owner of the runbook.')
+ )
+ parser.add_argument(
+ '--extra',
+ metavar="",
+ action='append',
+ help=_("Record arbitrary key/value metadata. "
+ "Can be specified multiple times."))
+ parser.add_argument(
+ '--steps',
+ metavar="