Add a proper client object
Introduce BaseClient and ClientV1 objects, make existing functions shortcuts for ClientV1 methods. Reorganize README to be ClientV1-centric. Add new functional tests. Switch CLI to the ClientV1 object. Change-Id: I64604657bf552aab942f2aad34a7362b931f6fcf
This commit is contained in:
128
README.rst
128
README.rst
@@ -11,46 +11,25 @@ This is a client library and tool for `Ironic Inspector`_.
|
||||
Please follow usual OpenStack `Gerrit Workflow`_ to submit a patch, see
|
||||
`Inspector contributing guide`_ for more detail.
|
||||
|
||||
Usage
|
||||
-----
|
||||
Python API
|
||||
----------
|
||||
|
||||
CLI tool is based on OpenStackClient_ with prefix
|
||||
``openstack baremetal introspection``. Accepts optional argument
|
||||
``--inspector-url`` with the **Ironic Inspector** API endpoint.
|
||||
To use Python API first create a ``ClientV1`` object::
|
||||
|
||||
* **Start introspection on a node**:
|
||||
import ironic_inspector_client
|
||||
|
||||
``ironic_inspector_client.introspect(uuid, new_ipmi_username=None,
|
||||
new_ipmi_password=None)``
|
||||
url = 'http://HOST:5050'
|
||||
client = ironic_inspector_client.ClientV1(auth_token=token, inspector_url=url)
|
||||
|
||||
::
|
||||
This code creates a client with API version *1.0* and an authentication token.
|
||||
If ``inspector_url`` is missing, local host is assumed for now. Service
|
||||
catalog will be used in the future.
|
||||
|
||||
$ openstack baremetal introspection start UUID [--new-ipmi-password=PWD [--new-ipmi-username=USER]]
|
||||
Optional ``api_version`` argument is a minimum API version that a server must
|
||||
support. It can be a tuple (MAJ, MIN), string "MAJ.MIN" or integer
|
||||
(only major, minimum supported minor version is assumed).
|
||||
|
||||
* ``uuid`` - Ironic node UUID;
|
||||
* ``new_ipmi_username`` and ``new_ipmi_password`` - if these are set,
|
||||
**Ironic Inspector** will switch to manual power on and assigning IPMI
|
||||
credentials on introspection. See `Setting IPMI Credentials`_ for details.
|
||||
|
||||
* **Query introspection status**:
|
||||
|
||||
``ironic_inspector_client.get_status(uuid)``
|
||||
|
||||
::
|
||||
|
||||
$ openstack baremetal introspection status UUID
|
||||
|
||||
* ``uuid`` - Ironic node UUID.
|
||||
|
||||
Every call accepts additional optional arguments:
|
||||
|
||||
* ``base_url`` **Ironic Inspector** API endpoint, defaults to
|
||||
``127.0.0.1:5050``,
|
||||
* ``auth_token`` Keystone authentication token.
|
||||
* ``api_version`` requested API version; can be a tuple (MAJ, MIN), string
|
||||
"MAJ.MIN" or integer (only major). Defaults to ``DEFAULT_API_VERSION``.
|
||||
|
||||
Refer to HTTP-API.rst_ for information on the **Ironic Inspector** HTTP API.
|
||||
See `Usage`_ for the list of available calls.
|
||||
|
||||
API Versioning
|
||||
~~~~~~~~~~~~~~
|
||||
@@ -61,18 +40,7 @@ versioning. Version is a tuple (X, Y), where X is always 1 for now.
|
||||
The server has maximum and minimum supported versions. If no version is
|
||||
requested, the server assumes (1, 0).
|
||||
|
||||
* There is a helper function to figure out the current server API versions
|
||||
range:
|
||||
|
||||
``ironic_inspector_client.server_api_versions()``
|
||||
|
||||
Returns a tuple (minimum version, maximum version).
|
||||
Supports optional argument:
|
||||
|
||||
* ``base_url`` **Ironic Inspector** API endpoint, defaults to
|
||||
``127.0.0.1:5050``,
|
||||
|
||||
Two constants are exposed by the client:
|
||||
Two constants are exposed for convenience:
|
||||
|
||||
* ``DEFAULT_API_VERSION`` server API version used by default, always (1, 0)
|
||||
for now.
|
||||
@@ -81,6 +49,74 @@ Two constants are exposed by the client:
|
||||
with. This does not mean that other versions won't work at all - the server
|
||||
might still support them.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
CLI tool is based on OpenStackClient_ with prefix
|
||||
``openstack baremetal introspection``. Accepts optional argument
|
||||
``--inspector-url`` with the **Ironic Inspector** API endpoint.
|
||||
|
||||
Refer to HTTP-API.rst_ for information on the **Ironic Inspector** HTTP API.
|
||||
|
||||
Detect server API versions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``client.server_api_versions()``
|
||||
|
||||
Returns a tuple (minimum version, maximum version). See `API Versioning`_ for
|
||||
details.
|
||||
|
||||
Start introspection on a node
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``client.introspect(uuid, new_ipmi_username=None, new_ipmi_password=None)``
|
||||
|
||||
* ``uuid`` - Ironic node UUID;
|
||||
* ``new_ipmi_username`` and ``new_ipmi_password`` - if these are set,
|
||||
**Ironic Inspector** will switch to manual power on and assigning IPMI
|
||||
credentials on introspection. See `Setting IPMI Credentials`_ for details.
|
||||
|
||||
CLI::
|
||||
|
||||
$ openstack baremetal introspection start UUID [--new-ipmi-password=PWD [--new-ipmi-username=USER]]
|
||||
|
||||
Query introspection status
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``client.get_status(uuid)``
|
||||
|
||||
* ``uuid`` - Ironic node UUID.
|
||||
|
||||
Returns a dict with keys:
|
||||
|
||||
* ``finished`` - whether introspection has finished for this node;
|
||||
* ``error`` - last error, ``None`` if introspection ended without an error.
|
||||
|
||||
CLI::
|
||||
|
||||
$ openstack baremetal introspection status UUID
|
||||
|
||||
Shortcut Functions
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The following functions are available for simplified access to the most common
|
||||
functionality:
|
||||
|
||||
* Starting introspection::
|
||||
|
||||
ironic_inspector_client.introspect(uuid[, new_ipmi_password[, new_ipmi_username]][, auth_token][, base_url][, api_version])
|
||||
|
||||
* Getting introspection status::
|
||||
|
||||
ironic_inspector_client.get_status(uuid[, auth_token][, base_url][, api_version])
|
||||
|
||||
* Getting API versions supported by a server::
|
||||
|
||||
ironic_inspector_client.server_api_versions([base_url])
|
||||
|
||||
Here ``base_url`` argument is the same as ``inspector_url`` argument to
|
||||
``ClientV1`` constructor.
|
||||
|
||||
|
||||
.. _Gerrit Workflow: http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
.. _Ironic Inspector: https://pypi.python.org/pypi/ironic-inspector
|
||||
|
||||
@@ -12,3 +12,4 @@
|
||||
# limitations under the License.
|
||||
|
||||
from .client import * # noqa
|
||||
from .v1 import ClientV1 # noqa
|
||||
|
||||
@@ -11,98 +11,19 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
"""Shorthand client functions using V1 API."""
|
||||
|
||||
from oslo_utils import netutils
|
||||
import requests
|
||||
import six
|
||||
|
||||
from ironic_inspector_client.common.i18n import _
|
||||
|
||||
|
||||
_DEFAULT_URL = 'http://' + netutils.get_my_ipv4() + ':5050/v1'
|
||||
_ERROR_ENCODING = 'utf-8'
|
||||
LOG = logging.getLogger('ironic_inspector_client')
|
||||
from ironic_inspector_client.common import http
|
||||
from ironic_inspector_client import v1
|
||||
|
||||
|
||||
DEFAULT_API_VERSION = (1, 0)
|
||||
MAX_API_VERSION = (1, 0)
|
||||
_MIN_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Minimum-Version'
|
||||
_MAX_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Maximum-Version'
|
||||
_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Version'
|
||||
|
||||
|
||||
def _prepare(base_url, auth_token, api_version=None):
|
||||
base_url = (base_url or _DEFAULT_URL).rstrip('/')
|
||||
if not base_url.endswith('v1'):
|
||||
base_url += '/v1'
|
||||
headers = {'X-Auth-Token': auth_token} if auth_token else {}
|
||||
if api_version:
|
||||
api_version = _check_api_version(api_version, base_url)
|
||||
headers[_VERSION_HEADER] = '%d.%d' % api_version
|
||||
return base_url, headers
|
||||
|
||||
|
||||
def _parse_version(api_version):
|
||||
try:
|
||||
return tuple(int(x) for x in api_version.split('.'))
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(_("Malformed API version: expect tuple, string "
|
||||
"in form of X.Y or integer"))
|
||||
|
||||
|
||||
def _check_api_version(api_version, base_url=None):
|
||||
if isinstance(api_version, int):
|
||||
api_version = (api_version, 0)
|
||||
if isinstance(api_version, six.string_types):
|
||||
api_version = _parse_version(api_version)
|
||||
api_version = tuple(api_version)
|
||||
if not all(isinstance(x, int) for x in api_version):
|
||||
raise TypeError(_("All API version components should be integers"))
|
||||
if len(api_version) == 1:
|
||||
api_version += (0,)
|
||||
elif len(api_version) > 2:
|
||||
raise ValueError(_("API version should be of length 1 or 2"))
|
||||
|
||||
minv, maxv = server_api_versions(base_url=base_url)
|
||||
if api_version < minv or api_version > maxv:
|
||||
raise VersionNotSupported(api_version, (minv, maxv))
|
||||
|
||||
return api_version
|
||||
|
||||
|
||||
class ClientError(requests.HTTPError):
|
||||
"""Error returned from a server."""
|
||||
def __init__(self, response):
|
||||
# inspector returns error message in body
|
||||
msg = response.content.decode(_ERROR_ENCODING)
|
||||
try:
|
||||
msg = json.loads(msg)['error']['message']
|
||||
except ValueError:
|
||||
LOG.debug('Old style error response returned, assuming '
|
||||
'ironic-discoverd')
|
||||
except (KeyError, TypeError):
|
||||
LOG.exception('Bad error response from Ironic Inspector')
|
||||
super(ClientError, self).__init__(msg, response=response)
|
||||
|
||||
@classmethod
|
||||
def raise_if_needed(cls, response):
|
||||
"""Raise exception if response contains error."""
|
||||
if response.status_code >= 400:
|
||||
raise cls(response)
|
||||
|
||||
|
||||
class VersionNotSupported(Exception):
|
||||
"""Denotes that requested API versions is not supported by the server."""
|
||||
def __init__(self, expected, supported):
|
||||
msg = (_('Version %(expected)s is not supported by the server, '
|
||||
'supported range is %(supported)s') %
|
||||
{'expected': expected,
|
||||
'supported': ' to '.join(str(x) for x in supported)})
|
||||
self.expected_version = expected
|
||||
self.supported_versions = supported
|
||||
super(Exception, self).__init__(msg)
|
||||
# Reimport for backward compatibility
|
||||
ClientError = http.ClientError
|
||||
VersionNotSupported = http.VersionNotSupported
|
||||
|
||||
|
||||
def introspect(uuid, base_url=None, auth_token=None,
|
||||
@@ -125,18 +46,10 @@ def introspect(uuid, base_url=None, auth_token=None,
|
||||
:raises: VersionNotSupported if requested api_version is not supported
|
||||
:raises: *requests* library exception on connection problems.
|
||||
"""
|
||||
if not isinstance(uuid, six.string_types):
|
||||
raise TypeError(_("Expected string for uuid argument, got %r") % uuid)
|
||||
if new_ipmi_username and not new_ipmi_password:
|
||||
raise ValueError(_("Setting IPMI user name requires a new password"))
|
||||
|
||||
base_url, headers = _prepare(base_url, auth_token, api_version=api_version)
|
||||
|
||||
params = {'new_ipmi_username': new_ipmi_username,
|
||||
'new_ipmi_password': new_ipmi_password}
|
||||
res = requests.post("%s/introspection/%s" % (base_url, uuid),
|
||||
headers=headers, params=params)
|
||||
ClientError.raise_if_needed(res)
|
||||
c = v1.ClientV1(api_version=api_version, auth_token=auth_token,
|
||||
inspector_url=base_url)
|
||||
return c.introspect(uuid, new_ipmi_username=new_ipmi_username,
|
||||
new_ipmi_password=new_ipmi_password)
|
||||
|
||||
|
||||
def get_status(uuid, base_url=None, auth_token=None,
|
||||
@@ -154,15 +67,9 @@ def get_status(uuid, base_url=None, auth_token=None,
|
||||
:raises: VersionNotSupported if requested api_version is not supported
|
||||
:raises: *requests* library exception on connection problems.
|
||||
"""
|
||||
if not isinstance(uuid, six.string_types):
|
||||
raise TypeError(_("Expected string for uuid argument, got %r") % uuid)
|
||||
|
||||
base_url, headers = _prepare(base_url, auth_token, api_version=api_version)
|
||||
|
||||
res = requests.get("%s/introspection/%s" % (base_url, uuid),
|
||||
headers=headers)
|
||||
ClientError.raise_if_needed(res)
|
||||
return res.json()
|
||||
c = v1.ClientV1(api_version=api_version, auth_token=auth_token,
|
||||
inspector_url=base_url)
|
||||
return c.get_status(uuid)
|
||||
|
||||
|
||||
def server_api_versions(base_url=None):
|
||||
@@ -175,13 +82,8 @@ def server_api_versions(base_url=None):
|
||||
:raises: *requests* library exception on connection problems.
|
||||
:raises: ValueError if returned version cannot be parsed
|
||||
"""
|
||||
base_url, _headers = _prepare(base_url, auth_token=None)
|
||||
res = requests.get(base_url)
|
||||
# HTTP Not Found is a valid response for older (2.0.0) servers
|
||||
if res.status_code >= 400 and res.status_code != 404:
|
||||
ClientError.raise_if_needed(res)
|
||||
return (_parse_version(res.headers.get(_MIN_VERSION_HEADER, '1.0')),
|
||||
_parse_version(res.headers.get(_MAX_VERSION_HEADER, '1.0')))
|
||||
c = http.BaseClient(1, inspector_url=base_url)
|
||||
return c.server_api_versions()
|
||||
|
||||
|
||||
__all__ = ['DEFAULT_API_VERSION', 'MAX_API_VERSION', 'ClientError',
|
||||
|
||||
168
ironic_inspector_client/common/http.py
Normal file
168
ironic_inspector_client/common/http.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Generic code for inspector client."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from oslo_utils import netutils
|
||||
import requests
|
||||
import six
|
||||
|
||||
from ironic_inspector_client.common.i18n import _
|
||||
|
||||
|
||||
_DEFAULT_URL = 'http://' + netutils.get_my_ipv4() + ':5050'
|
||||
_ERROR_ENCODING = 'utf-8'
|
||||
LOG = logging.getLogger('ironic_inspector_client')
|
||||
|
||||
|
||||
_MIN_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Minimum-Version'
|
||||
_MAX_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Maximum-Version'
|
||||
_VERSION_HEADER = 'X-OpenStack-Ironic-Inspector-API-Version'
|
||||
_AUTH_TOKEN_HEADER = 'X-Auth-Token'
|
||||
|
||||
|
||||
def _parse_version(api_version):
|
||||
try:
|
||||
return tuple(int(x) for x in api_version.split('.'))
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(_("Malformed API version: expect tuple, string "
|
||||
"in form of X.Y or integer"))
|
||||
|
||||
|
||||
class ClientError(requests.HTTPError):
|
||||
"""Error returned from a server."""
|
||||
def __init__(self, response):
|
||||
# inspector returns error message in body
|
||||
msg = response.content.decode(_ERROR_ENCODING)
|
||||
try:
|
||||
msg = json.loads(msg)['error']['message']
|
||||
except ValueError:
|
||||
LOG.debug('Old style error response returned, assuming '
|
||||
'ironic-discoverd')
|
||||
except (KeyError, TypeError):
|
||||
LOG.exception('Bad error response from Ironic Inspector')
|
||||
|
||||
LOG.debug('Inspector returned error "%(msg)s" (HTTP %(code)s)',
|
||||
{'msg': msg, 'code': response.status_code})
|
||||
super(ClientError, self).__init__(msg, response=response)
|
||||
|
||||
@classmethod
|
||||
def raise_if_needed(cls, response):
|
||||
"""Raise exception if response contains error."""
|
||||
if response.status_code >= 400:
|
||||
raise cls(response)
|
||||
|
||||
|
||||
class VersionNotSupported(Exception):
|
||||
"""Denotes that requested API versions is not supported by the server."""
|
||||
def __init__(self, expected, supported):
|
||||
msg = (_('Version %(expected)s is not supported by the server, '
|
||||
'supported range is %(supported)s') %
|
||||
{'expected': expected,
|
||||
'supported': ' to '.join(str(x) for x in supported)})
|
||||
self.expected_version = expected
|
||||
self.supported_versions = supported
|
||||
super(Exception, self).__init__(msg)
|
||||
|
||||
|
||||
class BaseClient(object):
|
||||
"""Base class for clients, provides common HTTP code."""
|
||||
|
||||
def __init__(self, api_version, inspector_url=None, auth_token=None):
|
||||
"""Create a client.
|
||||
|
||||
:param api_version: minimum API version that must be supported by
|
||||
the server
|
||||
:param inspector_url: *Ironic Inspector* URL in form:
|
||||
http://host:port[/ver],
|
||||
defaults to ``http://<current host>:5050/v<MAJOR>``.
|
||||
:param auth_token: authentication token
|
||||
"""
|
||||
self._base_url = (inspector_url or _DEFAULT_URL).rstrip('/')
|
||||
|
||||
self._auth_token = auth_token
|
||||
self._api_version = self._check_api_version(api_version)
|
||||
self._version_str = '%d.%d' % self._api_version
|
||||
ver_postfix = '/v%d' % self._api_version[0]
|
||||
|
||||
if not self._base_url.endswith(ver_postfix):
|
||||
self._base_url += ver_postfix
|
||||
|
||||
def _make_headers(self, **kwargs):
|
||||
kwargs[_VERSION_HEADER] = self._version_str
|
||||
if self._auth_token:
|
||||
kwargs[_AUTH_TOKEN_HEADER] = self._auth_token
|
||||
return kwargs
|
||||
|
||||
def _check_api_version(self, api_version):
|
||||
if isinstance(api_version, int):
|
||||
api_version = (api_version, 0)
|
||||
if isinstance(api_version, six.string_types):
|
||||
api_version = _parse_version(api_version)
|
||||
api_version = tuple(api_version)
|
||||
if not all(isinstance(x, int) for x in api_version):
|
||||
raise TypeError(_("All API version components should be integers"))
|
||||
if len(api_version) == 1:
|
||||
api_version += (0,)
|
||||
elif len(api_version) > 2:
|
||||
raise ValueError(_("API version should be of length 1 or 2"))
|
||||
|
||||
minv, maxv = self.server_api_versions()
|
||||
if api_version < minv or api_version > maxv:
|
||||
raise VersionNotSupported(api_version, (minv, maxv))
|
||||
|
||||
return api_version
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"""Make an HTTP request.
|
||||
|
||||
:param method: HTTP method
|
||||
:param endpoint: relative endpoint
|
||||
:param kwargs: arguments to pass to 'requests' library
|
||||
"""
|
||||
headers = self._make_headers()
|
||||
url = self._base_url + '/' + url.lstrip('/')
|
||||
LOG.debug('Requesting %(method)s %(url)s (API version %(ver)s) '
|
||||
'with %(args)s',
|
||||
{'method': method.upper(), 'url': url,
|
||||
'ver': self._version_str, 'args': kwargs})
|
||||
res = getattr(requests, method)(url, headers=headers, **kwargs)
|
||||
LOG.debug('Got response for %(method)s %(url)s with status code '
|
||||
'%(code)s', {'url': url, 'method': method.upper(),
|
||||
'code': res.status_code})
|
||||
ClientError.raise_if_needed(res)
|
||||
return res
|
||||
|
||||
def server_api_versions(self):
|
||||
"""Get minimum and maximum supported API versions from a server.
|
||||
|
||||
:return: tuple (minimum version, maximum version) each version
|
||||
is returned as a tuple (X, Y)
|
||||
:raises: *requests* library exception on connection problems.
|
||||
:raises: ValueError if returned version cannot be parsed
|
||||
"""
|
||||
res = requests.get(self._base_url)
|
||||
# HTTP Not Found is a valid response for older (2.0.0) servers
|
||||
if res.status_code >= 400 and res.status_code != 404:
|
||||
ClientError.raise_if_needed(res)
|
||||
|
||||
min_ver = res.headers.get(_MIN_VERSION_HEADER, '1.0')
|
||||
max_ver = res.headers.get(_MAX_VERSION_HEADER, '1.0')
|
||||
res = (_parse_version(min_ver), _parse_version(max_ver))
|
||||
LOG.debug('Supported API version range for %(url)s is '
|
||||
'[%(min)s, %(max)s]',
|
||||
{'url': self._base_url, 'min': min_ver, 'max': max_ver})
|
||||
return res
|
||||
@@ -21,27 +21,38 @@ from cliff import command
|
||||
from cliff import show
|
||||
from openstackclient.common import utils
|
||||
|
||||
from ironic_inspector_client import client
|
||||
import ironic_inspector_client
|
||||
|
||||
|
||||
LOG = logging.getLogger('ironic_inspector.shell')
|
||||
API_NAME = 'baremetal-introspection'
|
||||
API_NAME = 'baremetal_introspection'
|
||||
API_VERSION_OPTION = 'inspector_api_version'
|
||||
DEFAULT_API_VERSION = '1'
|
||||
API_VERSIONS = {
|
||||
"1": "ironic_inspector.shell",
|
||||
}
|
||||
|
||||
for mversion in range(client.MAX_API_VERSION[1] + 1):
|
||||
for mversion in range(ironic_inspector_client.MAX_API_VERSION[1] + 1):
|
||||
API_VERSIONS["1.%d" % mversion] = API_VERSIONS["1"]
|
||||
|
||||
|
||||
def make_client(instance):
|
||||
return ironic_inspector_client.ClientV1(
|
||||
inspector_url=instance.get_configuration().get('inspector_url'),
|
||||
auth_token=instance.auth_ref.auth_token,
|
||||
api_version=instance._api_version[API_NAME])
|
||||
|
||||
|
||||
def build_option_parser(parser):
|
||||
parser.add_argument('--inspector-api-version',
|
||||
default=utils.env('INSPECTOR_VERSION',
|
||||
default=DEFAULT_API_VERSION),
|
||||
help='inspector API version, only 1 is supported now '
|
||||
'(env: INSPECTOR_VERSION).')
|
||||
parser.add_argument('--inspector-url',
|
||||
default=utils.env('INSPECTOR_URL', default=None),
|
||||
help='inspector URL, defaults to localhost '
|
||||
'(env: INSPECTOR_URL).')
|
||||
return parser
|
||||
|
||||
|
||||
@@ -50,7 +61,7 @@ class StartCommand(command.Command):
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(StartCommand, self).get_parser(prog_name)
|
||||
_add_common_arguments(parser)
|
||||
parser.add_argument('uuid', help='baremetal node UUID')
|
||||
parser.add_argument('--new-ipmi-username',
|
||||
default=None,
|
||||
help='if set, *Ironic Inspector* will update IPMI '
|
||||
@@ -62,13 +73,10 @@ class StartCommand(command.Command):
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
auth_token = self.app.client_manager.auth_ref.auth_token
|
||||
api_version = self.app.client_manager._api_version[API_NAME]
|
||||
client.introspect(parsed_args.uuid, base_url=parsed_args.inspector_url,
|
||||
auth_token=auth_token,
|
||||
client = self.app.client_manager.baremetal_introspection
|
||||
client.introspect(parsed_args.uuid,
|
||||
new_ipmi_username=parsed_args.new_ipmi_username,
|
||||
new_ipmi_password=parsed_args.new_ipmi_password,
|
||||
api_version=api_version)
|
||||
new_ipmi_password=parsed_args.new_ipmi_password)
|
||||
if parsed_args.new_ipmi_password:
|
||||
print('Setting IPMI credentials requested, please power on '
|
||||
'the machine manually')
|
||||
@@ -79,26 +87,10 @@ class StatusCommand(show.ShowOne):
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(StatusCommand, self).get_parser(prog_name)
|
||||
_add_common_arguments(parser)
|
||||
parser.add_argument('uuid', help='baremetal node UUID')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
auth_token = self.app.client_manager.auth_ref.auth_token
|
||||
api_version = self.app.client_manager._api_version[API_NAME]
|
||||
status = client.get_status(
|
||||
parsed_args.uuid,
|
||||
base_url=parsed_args.inspector_url,
|
||||
auth_token=auth_token,
|
||||
api_version=api_version)
|
||||
client = self.app.client_manager.baremetal_introspection
|
||||
status = client.get_status(parsed_args.uuid)
|
||||
return zip(*sorted(status.items()))
|
||||
|
||||
|
||||
def _add_common_arguments(parser):
|
||||
"""Add commonly used arguments to a parser."""
|
||||
parser.add_argument('uuid', help='baremetal node UUID')
|
||||
# FIXME(dtantsur): this should be in build_option_parser, but then it won't
|
||||
# be available in commands
|
||||
parser.add_argument('--inspector-url',
|
||||
default=utils.env('INSPECTOR_URL', default=None),
|
||||
help='inspector URL, defaults to localhost '
|
||||
'(env: INSPECTOR_URL).')
|
||||
|
||||
@@ -18,10 +18,76 @@ import unittest
|
||||
|
||||
from ironic_inspector.test import functional
|
||||
|
||||
from ironic_inspector_client import client
|
||||
import ironic_inspector_client as client
|
||||
|
||||
|
||||
class TestPythonAPI(functional.Base):
|
||||
class TestV1PythonAPI(functional.Base):
|
||||
def setUp(self):
|
||||
super(TestV1PythonAPI, self).setUp()
|
||||
self.client = client.ClientV1()
|
||||
|
||||
def test_introspect_get_status(self):
|
||||
self.client.introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(functional.DEFAULT_SLEEP)
|
||||
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
||||
'reboot')
|
||||
|
||||
status = self.client.get_status(self.uuid)
|
||||
self.assertEqual({'finished': False, 'error': None}, status)
|
||||
|
||||
res = self.call_continue(self.data)
|
||||
self.assertEqual({'uuid': self.uuid}, res)
|
||||
eventlet.greenthread.sleep(functional.DEFAULT_SLEEP)
|
||||
|
||||
self.cli.node.update.assert_any_call(self.uuid, self.patch)
|
||||
self.cli.port.create.assert_called_once_with(
|
||||
node_uuid=self.uuid, address='11:22:33:44:55:66')
|
||||
|
||||
status = self.client.get_status(self.uuid)
|
||||
self.assertEqual({'finished': True, 'error': None}, status)
|
||||
|
||||
def test_setup_ipmi(self):
|
||||
self.node.maintenance = True
|
||||
self.client.introspect(self.uuid, new_ipmi_username='admin',
|
||||
new_ipmi_password='pwd')
|
||||
eventlet.greenthread.sleep(functional.DEFAULT_SLEEP)
|
||||
self.assertFalse(self.cli.node.set_power_state.called)
|
||||
|
||||
res = self.call_continue(self.data)
|
||||
self.assertEqual('admin', res['ipmi_username'])
|
||||
self.assertEqual('pwd', res['ipmi_password'])
|
||||
self.assertTrue(res['ipmi_setup_credentials'])
|
||||
|
||||
def test_api_versions(self):
|
||||
minv, maxv = self.client.server_api_versions()
|
||||
self.assertEqual((1, 0), minv)
|
||||
self.assertGreaterEqual(maxv, (1, 0))
|
||||
self.assertLess(maxv, (2, 0))
|
||||
|
||||
def test_client_init(self):
|
||||
self.assertRaises(client.VersionNotSupported,
|
||||
client.ClientV1, api_version=(1, 999))
|
||||
self.assertRaises(client.VersionNotSupported,
|
||||
client.ClientV1, api_version=2)
|
||||
|
||||
self.assertTrue(client.ClientV1(api_version=1).server_api_versions())
|
||||
self.assertTrue(client.ClientV1(api_version='1.0')
|
||||
.server_api_versions())
|
||||
self.assertTrue(client.ClientV1(api_version=(1, 0))
|
||||
.server_api_versions())
|
||||
|
||||
self.assertTrue(
|
||||
client.ClientV1(inspector_url='http://127.0.0.1:5050')
|
||||
.server_api_versions())
|
||||
self.assertTrue(
|
||||
client.ClientV1(inspector_url='http://127.0.0.1:5050/v1')
|
||||
.server_api_versions())
|
||||
|
||||
self.assertTrue(client.ClientV1(auth_token='some token')
|
||||
.server_api_versions())
|
||||
|
||||
|
||||
class TestSimplePythonAPI(functional.Base):
|
||||
def test_introspect_get_status(self):
|
||||
client.introspect(self.uuid)
|
||||
eventlet.greenthread.sleep(functional.DEFAULT_SLEEP)
|
||||
@@ -42,18 +108,6 @@ class TestPythonAPI(functional.Base):
|
||||
status = client.get_status(self.uuid)
|
||||
self.assertEqual({'finished': True, 'error': None}, status)
|
||||
|
||||
def test_setup_ipmi(self):
|
||||
self.node.maintenance = True
|
||||
client.introspect(self.uuid, new_ipmi_username='admin',
|
||||
new_ipmi_password='pwd')
|
||||
eventlet.greenthread.sleep(functional.DEFAULT_SLEEP)
|
||||
self.assertFalse(self.cli.node.set_power_state.called)
|
||||
|
||||
res = self.call_continue(self.data)
|
||||
self.assertEqual('admin', res['ipmi_username'])
|
||||
self.assertEqual('pwd', res['ipmi_password'])
|
||||
self.assertTrue(res['ipmi_setup_credentials'])
|
||||
|
||||
def test_api_versions(self):
|
||||
minv, maxv = client.server_api_versions()
|
||||
self.assertEqual((1, 0), minv)
|
||||
|
||||
@@ -15,207 +15,73 @@ import types
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from oslo_utils import netutils
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
import ironic_inspector_client
|
||||
from ironic_inspector_client import client
|
||||
from ironic_inspector_client.common import http
|
||||
from ironic_inspector_client import v1
|
||||
|
||||
|
||||
class BaseTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(BaseTest, self).setUp()
|
||||
self.uuid = uuidutils.generate_uuid()
|
||||
self.my_ip = 'http://' + netutils.get_my_ipv4() + ':5050/v1'
|
||||
self.token = "token"
|
||||
self.headers = {'X-OpenStack-Ironic-Inspector-API-Version': '1.0',
|
||||
'X-Auth-Token': self.token}
|
||||
uuid = uuidutils.generate_uuid()
|
||||
token = "token"
|
||||
|
||||
|
||||
@mock.patch.object(client, 'server_api_versions',
|
||||
lambda *args, **kwargs: ((1, 0), (1, 99)))
|
||||
@mock.patch.object(client.requests, 'post', autospec=True,
|
||||
**{'return_value.status_code': 200})
|
||||
@mock.patch.object(v1, 'ClientV1', autospec=True)
|
||||
class TestIntrospect(BaseTest):
|
||||
def test(self, mock_post):
|
||||
def test(self, mock_v1):
|
||||
client.introspect(self.uuid, base_url="http://host:port",
|
||||
auth_token=self.token)
|
||||
mock_post.assert_called_once_with(
|
||||
"http://host:port/v1/introspection/%s" % self.uuid,
|
||||
headers=self.headers,
|
||||
params={'new_ipmi_username': None, 'new_ipmi_password': None}
|
||||
)
|
||||
mock_v1.assert_called_once_with(auth_token=self.token,
|
||||
inspector_url="http://host:port",
|
||||
api_version=client.DEFAULT_API_VERSION)
|
||||
mock_v1.return_value.introspect.assert_called_once_with(
|
||||
self.uuid, new_ipmi_username=None, new_ipmi_password=None)
|
||||
|
||||
def test_invalid_input(self, _):
|
||||
self.assertRaises(TypeError, client.introspect, 42)
|
||||
self.assertRaises(ValueError, client.introspect, 'uuid',
|
||||
new_ipmi_username='user')
|
||||
|
||||
def test_full_url(self, mock_post):
|
||||
def test_full_url(self, mock_v1):
|
||||
client.introspect(self.uuid, base_url="http://host:port/v1/",
|
||||
auth_token=self.token)
|
||||
mock_post.assert_called_once_with(
|
||||
"http://host:port/v1/introspection/%s" % self.uuid,
|
||||
headers=self.headers,
|
||||
params={'new_ipmi_username': None, 'new_ipmi_password': None}
|
||||
)
|
||||
mock_v1.assert_called_once_with(auth_token=self.token,
|
||||
inspector_url="http://host:port/v1/",
|
||||
api_version=client.DEFAULT_API_VERSION)
|
||||
mock_v1.return_value.introspect.assert_called_once_with(
|
||||
self.uuid, new_ipmi_username=None, new_ipmi_password=None)
|
||||
|
||||
def test_default_url(self, mock_post):
|
||||
client.introspect(self.uuid, auth_token=self.token)
|
||||
mock_post.assert_called_once_with(
|
||||
"%(my_ip)s/introspection/%(uuid)s" %
|
||||
{'my_ip': self.my_ip, 'uuid': self.uuid},
|
||||
headers=self.headers,
|
||||
params={'new_ipmi_username': None, 'new_ipmi_password': None}
|
||||
)
|
||||
|
||||
def test_set_ipmi_credentials(self, mock_post):
|
||||
def test_set_ipmi_credentials(self, mock_v1):
|
||||
client.introspect(self.uuid, base_url="http://host:port",
|
||||
auth_token=self.token, new_ipmi_password='p',
|
||||
new_ipmi_username='u')
|
||||
mock_post.assert_called_once_with(
|
||||
"http://host:port/v1/introspection/%s" % self.uuid,
|
||||
headers=self.headers,
|
||||
params={'new_ipmi_username': 'u', 'new_ipmi_password': 'p'}
|
||||
)
|
||||
|
||||
def test_none_ok(self, mock_post):
|
||||
client.introspect(self.uuid)
|
||||
del self.headers['X-Auth-Token']
|
||||
mock_post.assert_called_once_with(
|
||||
"%(my_ip)s/introspection/%(uuid)s" %
|
||||
{'my_ip': self.my_ip, 'uuid': self.uuid},
|
||||
headers=self.headers,
|
||||
params={'new_ipmi_username': None, 'new_ipmi_password': None}
|
||||
)
|
||||
|
||||
def test_failed(self, mock_post):
|
||||
mock_post.return_value.status_code = 404
|
||||
mock_post.return_value.content = b'{"error":{"message":"boom"}}'
|
||||
self.assertRaisesRegexp(client.ClientError, "boom",
|
||||
client.introspect, self.uuid)
|
||||
|
||||
def test_failed_discoverd_style(self, mock_post):
|
||||
mock_post.return_value.status_code = 404
|
||||
mock_post.return_value.content = b"boom"
|
||||
self.assertRaisesRegexp(client.ClientError, "boom",
|
||||
client.introspect, self.uuid)
|
||||
|
||||
def test_failed_bad_json(self, mock_post):
|
||||
mock_post.return_value.status_code = 404
|
||||
mock_post.return_value.content = b'42'
|
||||
self.assertRaisesRegexp(client.ClientError, "42",
|
||||
client.introspect, self.uuid)
|
||||
mock_v1.assert_called_once_with(auth_token=self.token,
|
||||
inspector_url="http://host:port",
|
||||
api_version=client.DEFAULT_API_VERSION)
|
||||
mock_v1.return_value.introspect.assert_called_once_with(
|
||||
self.uuid, new_ipmi_username='u', new_ipmi_password='p')
|
||||
|
||||
|
||||
@mock.patch.object(client, 'server_api_versions',
|
||||
lambda *args, **kwargs: ((1, 0), (1, 99)))
|
||||
@mock.patch.object(client.requests, 'get', autospec=True,
|
||||
**{'return_value.status_code': 200})
|
||||
@mock.patch.object(v1, 'ClientV1', autospec=True)
|
||||
class TestGetStatus(BaseTest):
|
||||
def test(self, mock_get):
|
||||
mock_get.return_value.json.return_value = 'json'
|
||||
def test(self, mock_v1):
|
||||
mock_v1.return_value.get_status.return_value = 'json'
|
||||
|
||||
client.get_status(self.uuid, auth_token=self.token)
|
||||
self.assertEqual('json',
|
||||
client.get_status(self.uuid, auth_token=self.token))
|
||||
|
||||
mock_get.assert_called_once_with(
|
||||
"%(my_ip)s/introspection/%(uuid)s" %
|
||||
{'my_ip': self.my_ip, 'uuid': self.uuid},
|
||||
headers=self.headers
|
||||
)
|
||||
|
||||
def test_invalid_input(self, _):
|
||||
self.assertRaises(TypeError, client.get_status, 42)
|
||||
|
||||
def test_failed(self, mock_post):
|
||||
mock_post.return_value.status_code = 404
|
||||
mock_post.return_value.content = b'{"error":{"message":"boom"}}'
|
||||
self.assertRaisesRegexp(client.ClientError, "boom",
|
||||
client.get_status, self.uuid)
|
||||
|
||||
def test_failed_discoverd_style(self, mock_post):
|
||||
mock_post.return_value.status_code = 404
|
||||
mock_post.return_value.content = b"boom"
|
||||
self.assertRaisesRegexp(client.ClientError, "boom",
|
||||
client.get_status, self.uuid)
|
||||
|
||||
def test_failed_bad_json(self, mock_post):
|
||||
mock_post.return_value.status_code = 404
|
||||
mock_post.return_value.content = b'42'
|
||||
self.assertRaisesRegexp(client.ClientError, "42",
|
||||
client.get_status, self.uuid)
|
||||
mock_v1.assert_called_once_with(auth_token=self.token,
|
||||
inspector_url=None,
|
||||
api_version=client.DEFAULT_API_VERSION)
|
||||
mock_v1.return_value.get_status.assert_called_once_with(self.uuid)
|
||||
|
||||
|
||||
@mock.patch.object(client, 'server_api_versions',
|
||||
lambda *args, **kwargs: ((1, 0), (1, 99)))
|
||||
class TestCheckVesion(unittest.TestCase):
|
||||
def test_tuple(self):
|
||||
self.assertEqual((1, 0), client._check_api_version((1, 0)))
|
||||
|
||||
def test_small_tuple(self):
|
||||
self.assertEqual((1, 0), client._check_api_version((1,)))
|
||||
|
||||
def test_int(self):
|
||||
self.assertEqual((1, 0), client._check_api_version(1))
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual((1, 0), client._check_api_version("1.0"))
|
||||
|
||||
def test_invalid_tuple(self):
|
||||
self.assertRaises(TypeError, client._check_api_version, (1, "x"))
|
||||
self.assertRaises(ValueError, client._check_api_version, (1, 2, 3))
|
||||
|
||||
def test_invalid_str(self):
|
||||
self.assertRaises(ValueError, client._check_api_version, "a.b")
|
||||
self.assertRaises(ValueError, client._check_api_version, "1.2.3")
|
||||
self.assertRaises(ValueError, client._check_api_version, "foo")
|
||||
|
||||
def test_unsupported(self):
|
||||
self.assertRaises(client.VersionNotSupported,
|
||||
client._check_api_version, (99, 42))
|
||||
|
||||
|
||||
@mock.patch.object(client.requests, 'get', autospec=True,
|
||||
**{'return_value.status_code': 200})
|
||||
@mock.patch.object(http, 'BaseClient', autospec=True)
|
||||
class TestServerApiVersions(BaseTest):
|
||||
def test_no_headers(self, mock_get):
|
||||
mock_get.return_value.headers = {}
|
||||
def test(self, mock_cli):
|
||||
mock_cli.return_value.server_api_versions.return_value = (1, 0), (1, 1)
|
||||
|
||||
minv, maxv = client.server_api_versions()
|
||||
|
||||
self.assertEqual((1, 0), minv)
|
||||
self.assertEqual((1, 0), maxv)
|
||||
mock_get.assert_called_once_with(self.my_ip)
|
||||
|
||||
def test_with_headers(self, mock_get):
|
||||
mock_get.return_value.headers = {
|
||||
'X-OpenStack-Ironic-Inspector-API-Minimum-Version': '1.1',
|
||||
'X-OpenStack-Ironic-Inspector-API-Maximum-Version': '1.42',
|
||||
}
|
||||
|
||||
minv, maxv = client.server_api_versions()
|
||||
|
||||
self.assertEqual((1, 1), minv)
|
||||
self.assertEqual((1, 42), maxv)
|
||||
mock_get.assert_called_once_with(self.my_ip)
|
||||
|
||||
def test_with_404(self, mock_get):
|
||||
mock_get.return_value.status_code = 404
|
||||
mock_get.return_value.headers = {}
|
||||
|
||||
minv, maxv = client.server_api_versions()
|
||||
|
||||
self.assertEqual((1, 0), minv)
|
||||
self.assertEqual((1, 0), maxv)
|
||||
mock_get.assert_called_once_with(self.my_ip)
|
||||
|
||||
def test_with_other_error(self, mock_get):
|
||||
mock_get.return_value.status_code = 500
|
||||
mock_get.return_value.headers = {}
|
||||
|
||||
self.assertRaises(client.ClientError, client.server_api_versions)
|
||||
|
||||
mock_get.assert_called_once_with(self.my_ip)
|
||||
self.assertEqual((1, 1), maxv)
|
||||
|
||||
|
||||
class TestExposedAPI(unittest.TestCase):
|
||||
@@ -224,7 +90,7 @@ class TestExposedAPI(unittest.TestCase):
|
||||
if not x.startswith('__') and
|
||||
not isinstance(getattr(ironic_inspector_client, x),
|
||||
types.ModuleType)}
|
||||
self.assertEqual(set(client.__all__), exposed)
|
||||
self.assertEqual(set(client.__all__) | {'ClientV1'}, exposed)
|
||||
|
||||
def test_client_exposes_everything(self):
|
||||
actual = {x for x in dir(client)
|
||||
|
||||
171
ironic_inspector_client/test/test_common_http.py
Normal file
171
ironic_inspector_client/test/test_common_http.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from ironic_inspector_client.common import http
|
||||
|
||||
|
||||
class TestCheckVersion(unittest.TestCase):
|
||||
@mock.patch.object(http.BaseClient, 'server_api_versions',
|
||||
lambda *args, **kwargs: ((1, 0), (1, 99)))
|
||||
def _check(self, version):
|
||||
cli = http.BaseClient(1)
|
||||
return cli._check_api_version(version)
|
||||
|
||||
def test_tuple(self):
|
||||
self.assertEqual((1, 0), self._check((1, 0)))
|
||||
|
||||
def test_small_tuple(self):
|
||||
self.assertEqual((1, 0), self._check((1,)))
|
||||
|
||||
def test_int(self):
|
||||
self.assertEqual((1, 0), self._check(1))
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual((1, 0), self._check("1.0"))
|
||||
|
||||
def test_invalid_tuple(self):
|
||||
self.assertRaises(TypeError, self._check, (1, "x"))
|
||||
self.assertRaises(ValueError, self._check, (1, 2, 3))
|
||||
|
||||
def test_invalid_str(self):
|
||||
self.assertRaises(ValueError, self._check, "a.b")
|
||||
self.assertRaises(ValueError, self._check, "1.2.3")
|
||||
self.assertRaises(ValueError, self._check, "foo")
|
||||
|
||||
def test_unsupported(self):
|
||||
self.assertRaises(http.VersionNotSupported, self._check, (99, 42))
|
||||
|
||||
|
||||
FAKE_HEADERS = {
|
||||
http._MIN_VERSION_HEADER: '1.0',
|
||||
http._MAX_VERSION_HEADER: '1.9'
|
||||
}
|
||||
|
||||
|
||||
@mock.patch.object(http.requests, 'get', autospec=True,
|
||||
**{'return_value.status_code': 200,
|
||||
'return_value.headers': FAKE_HEADERS})
|
||||
class TestServerApiVersions(unittest.TestCase):
|
||||
def _check(self, current=1):
|
||||
return http.BaseClient(api_version=current).server_api_versions()
|
||||
|
||||
def test_no_headers(self, mock_get):
|
||||
mock_get.return_value.headers = {}
|
||||
|
||||
minv, maxv = self._check()
|
||||
|
||||
self.assertEqual((1, 0), minv)
|
||||
self.assertEqual((1, 0), maxv)
|
||||
|
||||
def test_with_headers(self, mock_get):
|
||||
mock_get.return_value.headers = {
|
||||
'X-OpenStack-Ironic-Inspector-API-Minimum-Version': '1.1',
|
||||
'X-OpenStack-Ironic-Inspector-API-Maximum-Version': '1.42',
|
||||
}
|
||||
|
||||
minv, maxv = self._check(current=(1, 2))
|
||||
|
||||
self.assertEqual((1, 1), minv)
|
||||
self.assertEqual((1, 42), maxv)
|
||||
|
||||
def test_with_404(self, mock_get):
|
||||
mock_get.return_value.status_code = 404
|
||||
mock_get.return_value.headers = {}
|
||||
|
||||
minv, maxv = self._check()
|
||||
|
||||
self.assertEqual((1, 0), minv)
|
||||
self.assertEqual((1, 0), maxv)
|
||||
|
||||
def test_with_other_error(self, mock_get):
|
||||
mock_get.return_value.status_code = 500
|
||||
mock_get.return_value.headers = {}
|
||||
|
||||
self.assertRaises(http.ClientError, self._check)
|
||||
|
||||
|
||||
@mock.patch.object(http.requests, 'get', autospec=True,
|
||||
**{'return_value.status_code': 200})
|
||||
class TestRequest(unittest.TestCase):
|
||||
base_url = http._DEFAULT_URL + '/v1'
|
||||
token = 'token'
|
||||
|
||||
def setUp(self):
|
||||
super(TestRequest, self).setUp()
|
||||
self.headers = {http._VERSION_HEADER: '1.0',
|
||||
'X-Auth-Token': self.token}
|
||||
|
||||
@mock.patch.object(http.BaseClient, 'server_api_versions',
|
||||
lambda self: ((1, 0), (1, 42)))
|
||||
def get_client(self, version=1, auth_token='token', inspector_url=None):
|
||||
return http.BaseClient(version, auth_token=auth_token,
|
||||
inspector_url=inspector_url)
|
||||
|
||||
def test_ok(self, mock_req):
|
||||
res = self.get_client().request('get', '/foo/bar')
|
||||
|
||||
self.assertIs(mock_req.return_value, res)
|
||||
mock_req.assert_called_once_with(self.base_url + '/foo/bar',
|
||||
headers=self.headers)
|
||||
|
||||
def test_no_auth(self, mock_req):
|
||||
res = self.get_client(auth_token=None).request('get', '/foo/bar')
|
||||
|
||||
self.assertIs(mock_req.return_value, res)
|
||||
del self.headers['X-Auth-Token']
|
||||
mock_req.assert_called_once_with(self.base_url + '/foo/bar',
|
||||
headers=self.headers)
|
||||
|
||||
def test_explicit_version(self, mock_req):
|
||||
res = self.get_client(version='1.2').request('get', '/foo/bar')
|
||||
|
||||
self.assertIs(mock_req.return_value, res)
|
||||
self.headers[http._VERSION_HEADER] = '1.2'
|
||||
mock_req.assert_called_once_with(self.base_url + '/foo/bar',
|
||||
headers=self.headers)
|
||||
|
||||
def test_explicit_url(self, mock_req):
|
||||
res = self.get_client(inspector_url='http://host').request(
|
||||
'get', '/foo/bar')
|
||||
|
||||
self.assertIs(mock_req.return_value, res)
|
||||
mock_req.assert_called_once_with('http://host/v1/foo/bar',
|
||||
headers=self.headers)
|
||||
|
||||
def test_explicit_url_with_version(self, mock_req):
|
||||
res = self.get_client(inspector_url='http://host/v1').request(
|
||||
'get', '/foo/bar')
|
||||
|
||||
self.assertIs(mock_req.return_value, res)
|
||||
mock_req.assert_called_once_with('http://host/v1/foo/bar',
|
||||
headers=self.headers)
|
||||
|
||||
def test_error(self, mock_req):
|
||||
mock_req.return_value.status_code = 400
|
||||
mock_req.return_value.content = json.dumps(
|
||||
{'error': {'message': 'boom'}}).encode('utf-8')
|
||||
|
||||
self.assertRaisesRegexp(http.ClientError, 'boom',
|
||||
self.get_client().request, 'get', 'url')
|
||||
|
||||
def test_error_discoverd_style(self, mock_req):
|
||||
mock_req.return_value.status_code = 400
|
||||
mock_req.return_value.content = b'boom'
|
||||
|
||||
self.assertRaisesRegexp(http.ClientError, 'boom',
|
||||
self.get_client().request, 'get', 'url')
|
||||
110
ironic_inspector_client/test/test_v1.py
Normal file
110
ironic_inspector_client/test/test_v1.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# 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 unittest
|
||||
|
||||
import mock
|
||||
from oslo_utils import netutils
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
import ironic_inspector_client
|
||||
from ironic_inspector_client.common import http
|
||||
|
||||
|
||||
FAKE_HEADERS = {
|
||||
http._MIN_VERSION_HEADER: '1.0',
|
||||
http._MAX_VERSION_HEADER: '1.9'
|
||||
}
|
||||
|
||||
|
||||
@mock.patch.object(http.requests, 'get',
|
||||
return_value=mock.Mock(headers=FAKE_HEADERS,
|
||||
status_code=200))
|
||||
class TestInit(unittest.TestCase):
|
||||
my_ip = 'http://' + netutils.get_my_ipv4() + ':5050'
|
||||
token = "token"
|
||||
|
||||
def get_client(self, **kwargs):
|
||||
return ironic_inspector_client.ClientV1(auth_token=self.token,
|
||||
**kwargs)
|
||||
|
||||
def test_ok(self, mock_get):
|
||||
self.get_client()
|
||||
mock_get.assert_called_once_with(self.my_ip)
|
||||
|
||||
def test_explicit_version(self, mock_get):
|
||||
self.get_client(api_version=(1, 2))
|
||||
self.get_client(api_version=1)
|
||||
self.get_client(api_version='1.3')
|
||||
|
||||
def test_unsupported_version(self, mock_get):
|
||||
self.assertRaises(ironic_inspector_client.VersionNotSupported,
|
||||
self.get_client, api_version=(1, 99))
|
||||
self.assertRaises(ironic_inspector_client.VersionNotSupported,
|
||||
self.get_client, api_version=2)
|
||||
self.assertRaises(ironic_inspector_client.VersionNotSupported,
|
||||
self.get_client, api_version='1.42')
|
||||
|
||||
def test_explicit_url(self, mock_get):
|
||||
self.get_client(inspector_url='http://host:port')
|
||||
mock_get.assert_called_once_with('http://host:port')
|
||||
|
||||
|
||||
class BaseTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(BaseTest, self).setUp()
|
||||
self.uuid = uuidutils.generate_uuid()
|
||||
self.my_ip = 'http://' + netutils.get_my_ipv4() + ':5050'
|
||||
self.token = "token"
|
||||
|
||||
@mock.patch.object(http.BaseClient, 'server_api_versions',
|
||||
lambda self: ((1, 0), (1, 99)))
|
||||
def get_client(self, **kwargs):
|
||||
return ironic_inspector_client.ClientV1(auth_token=self.token,
|
||||
**kwargs)
|
||||
|
||||
|
||||
@mock.patch.object(http.BaseClient, 'request')
|
||||
class TestIntrospect(BaseTest):
|
||||
def test(self, mock_req):
|
||||
self.get_client().introspect(self.uuid)
|
||||
mock_req.assert_called_once_with(
|
||||
'post', '/introspection/%s' % self.uuid,
|
||||
params={'new_ipmi_username': None, 'new_ipmi_password': None})
|
||||
|
||||
def test_invalid_input(self, mock_req):
|
||||
self.assertRaises(TypeError, self.get_client().introspect, 42)
|
||||
self.assertRaises(ValueError, self.get_client().introspect, 'uuid',
|
||||
new_ipmi_username='user')
|
||||
|
||||
def test_set_ipmi_credentials(self, mock_req):
|
||||
self.get_client().introspect(self.uuid,
|
||||
new_ipmi_password='p',
|
||||
new_ipmi_username='u')
|
||||
mock_req.assert_called_once_with(
|
||||
'post', '/introspection/%s' % self.uuid,
|
||||
params={'new_ipmi_username': 'u', 'new_ipmi_password': 'p'})
|
||||
|
||||
|
||||
@mock.patch.object(http.BaseClient, 'request')
|
||||
class TestGetStatus(BaseTest):
|
||||
def test(self, mock_req):
|
||||
mock_req.return_value.json.return_value = 'json'
|
||||
|
||||
self.get_client().get_status(self.uuid)
|
||||
|
||||
mock_req.assert_called_once_with(
|
||||
'get', '/introspection/%s' % self.uuid)
|
||||
|
||||
def test_invalid_input(self, _):
|
||||
self.assertRaises(TypeError, self.get_client().get_status, 42)
|
||||
74
ironic_inspector_client/v1.py
Normal file
74
ironic_inspector_client/v1.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Client for V1 API."""
|
||||
|
||||
import six
|
||||
|
||||
from ironic_inspector_client.common import http
|
||||
from ironic_inspector_client.common.i18n import _
|
||||
|
||||
|
||||
DEFAULT_API_VERSION = (1, 0)
|
||||
MAX_API_VERSION = (1, 0)
|
||||
|
||||
|
||||
class ClientV1(http.BaseClient):
|
||||
"""Client for API v1."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Create a client.
|
||||
|
||||
:param kwargs: arguments to pass to the BaseClient constructor.
|
||||
api_version is set to DEFAULT_API_VERSION by default.
|
||||
"""
|
||||
kwargs.setdefault('api_version', DEFAULT_API_VERSION)
|
||||
super(ClientV1, self).__init__(**kwargs)
|
||||
|
||||
def introspect(self, uuid, new_ipmi_password=None, new_ipmi_username=None):
|
||||
"""Start introspection for a node.
|
||||
|
||||
:param uuid: node uuid
|
||||
:param new_ipmi_password: if set, *Ironic Inspector* will update IPMI
|
||||
password to this value.
|
||||
:param new_ipmi_username: if new_ipmi_password is set, this values sets
|
||||
new IPMI user name. Defaults to one in
|
||||
driver_info.
|
||||
:raises: ClientError on error reported from a server
|
||||
:raises: VersionNotSupported if requested api_version is not supported
|
||||
:raises: *requests* library exception on connection problems.
|
||||
"""
|
||||
if not isinstance(uuid, six.string_types):
|
||||
raise TypeError(
|
||||
_("Expected string for uuid argument, got %r") % uuid)
|
||||
if new_ipmi_username and not new_ipmi_password:
|
||||
raise ValueError(
|
||||
_("Setting IPMI user name requires a new password"))
|
||||
|
||||
params = {'new_ipmi_username': new_ipmi_username,
|
||||
'new_ipmi_password': new_ipmi_password}
|
||||
self.request('post', '/introspection/%s' % uuid, params=params)
|
||||
|
||||
def get_status(self, uuid):
|
||||
"""Get introspection status for a node.
|
||||
|
||||
:param uuid: node uuid.
|
||||
:raises: ClientError on error reported from a server
|
||||
:raises: VersionNotSupported if requested api_version is not supported
|
||||
:raises: *requests* library exception on connection problems.
|
||||
"""
|
||||
if not isinstance(uuid, six.string_types):
|
||||
raise TypeError(
|
||||
_("Expected string for uuid argument, got %r") % uuid)
|
||||
|
||||
return self.request('get', '/introspection/%s' % uuid).json()
|
||||
Reference in New Issue
Block a user