From f2e8c031cc2c20fecc96fa9c93a4c1452e49c11e Mon Sep 17 00:00:00 2001
From: Dmitry Tantsur <dtantsur@protonmail.com>
Date: Tue, 23 Feb 2021 16:15:24 +0100
Subject: [PATCH] Switch to JSON RPC from ironic-lib

Change-Id: I8b438861780c85faae7ff18646960723a1fd9876
---
 ironic/common/json_rpc/__init__.py        |  20 -
 ironic/common/json_rpc/client.py          | 207 -------
 ironic/common/json_rpc/server.py          | 293 ---------
 ironic/common/rpc_service.py              |   6 +-
 ironic/conductor/rpcapi.py                |   2 +-
 ironic/conf/__init__.py                   |   2 -
 ironic/conf/json_rpc.py                   |  61 --
 ironic/conf/opts.py                       |   1 -
 ironic/tests/unit/common/test_json_rpc.py | 714 ----------------------
 requirements.txt                          |   2 +-
 tools/config/ironic-config-generator.conf |   1 +
 11 files changed, 6 insertions(+), 1303 deletions(-)
 delete mode 100644 ironic/common/json_rpc/__init__.py
 delete mode 100644 ironic/common/json_rpc/client.py
 delete mode 100644 ironic/common/json_rpc/server.py
 delete mode 100644 ironic/conf/json_rpc.py
 delete mode 100644 ironic/tests/unit/common/test_json_rpc.py

diff --git a/ironic/common/json_rpc/__init__.py b/ironic/common/json_rpc/__init__.py
deleted file mode 100644
index ad58e3bc6b..0000000000
--- a/ironic/common/json_rpc/__init__.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from oslo_config import cfg
-
-
-CONF = cfg.CONF
-
-
-def auth_strategy():
-    return CONF.json_rpc.auth_strategy or CONF.auth_strategy
diff --git a/ironic/common/json_rpc/client.py b/ironic/common/json_rpc/client.py
deleted file mode 100644
index 3fcc06d99a..0000000000
--- a/ironic/common/json_rpc/client.py
+++ /dev/null
@@ -1,207 +0,0 @@
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""A simple JSON RPC client.
-
-This client is compatible with any JSON RPC 2.0 implementation, including ours.
-"""
-
-from oslo_config import cfg
-from oslo_log import log
-from oslo_utils import importutils
-from oslo_utils import netutils
-from oslo_utils import strutils
-from oslo_utils import uuidutils
-
-from ironic.common import exception
-from ironic.common.i18n import _
-from ironic.common import json_rpc
-from ironic.common import keystone
-
-
-CONF = cfg.CONF
-LOG = log.getLogger(__name__)
-_SESSION = None
-
-
-def _get_session():
-    global _SESSION
-
-    if _SESSION is None:
-        kwargs = {}
-        auth_strategy = json_rpc.auth_strategy()
-        if auth_strategy != 'keystone':
-            auth_type = 'none' if auth_strategy == 'noauth' else auth_strategy
-            CONF.set_default('auth_type', auth_type, group='json_rpc')
-
-            # Deprecated, remove in W
-            if auth_strategy == 'http_basic':
-                if CONF.json_rpc.http_basic_username:
-                    kwargs['username'] = CONF.json_rpc.http_basic_username
-                if CONF.json_rpc.http_basic_password:
-                    kwargs['password'] = CONF.json_rpc.http_basic_password
-
-        auth = keystone.get_auth('json_rpc', **kwargs)
-
-        session = keystone.get_session('json_rpc', auth=auth)
-        headers = {
-            'Content-Type': 'application/json'
-        }
-
-        # Adds options like connect_retries
-        _SESSION = keystone.get_adapter('json_rpc', session=session,
-                                        additional_headers=headers)
-
-    return _SESSION
-
-
-class Client(object):
-    """JSON RPC client with ironic exception handling."""
-
-    def __init__(self, serializer, version_cap=None):
-        self.serializer = serializer
-        self.version_cap = version_cap
-
-    def can_send_version(self, version):
-        return _can_send_version(version, self.version_cap)
-
-    def prepare(self, topic, version=None):
-        host = topic.split('.', 1)[1]
-        return _CallContext(host, self.serializer, version=version,
-                            version_cap=self.version_cap)
-
-
-class _CallContext(object):
-    """Wrapper object for compatibility with oslo.messaging API."""
-
-    def __init__(self, host, serializer, version=None, version_cap=None):
-        self.host = host
-        self.serializer = serializer
-        self.version = version
-        self.version_cap = version_cap
-
-    def _handle_error(self, error):
-        if not error:
-            return
-
-        message = error['message']
-        try:
-            cls = error['data']['class']
-        except KeyError:
-            LOG.error("Unexpected error from RPC: %s", error)
-            raise exception.IronicException(
-                _("Unexpected error raised by RPC"))
-        else:
-            if not cls.startswith('ironic.common.exception.'):
-                # NOTE(dtantsur): protect against arbitrary code execution
-                LOG.error("Unexpected error from RPC: %s", error)
-                raise exception.IronicException(
-                    _("Unexpected error raised by RPC"))
-            raise importutils.import_object(cls, message,
-                                            code=error.get('code', 500))
-
-    def call(self, context, method, version=None, **kwargs):
-        """Call conductor RPC.
-
-        Versioned objects are automatically serialized and deserialized.
-
-        :param context: Security context.
-        :param method: Method name.
-        :param version: RPC API version to use.
-        :param kwargs: Keyword arguments to pass.
-        :return: RPC result (if any).
-        """
-        return self._request(context, method, cast=False, version=version,
-                             **kwargs)
-
-    def cast(self, context, method, version=None, **kwargs):
-        """Call conductor RPC asynchronously.
-
-        Versioned objects are automatically serialized and deserialized.
-
-        :param context: Security context.
-        :param method: Method name.
-        :param version: RPC API version to use.
-        :param kwargs: Keyword arguments to pass.
-        :return: None
-        """
-        return self._request(context, method, cast=True, version=version,
-                             **kwargs)
-
-    def _request(self, context, method, cast=False, version=None, **kwargs):
-        """Call conductor RPC.
-
-        Versioned objects are automatically serialized and deserialized.
-
-        :param context: Security context.
-        :param method: Method name.
-        :param cast: If true, use a JSON RPC notification.
-        :param version: RPC API version to use.
-        :param kwargs: Keyword arguments to pass.
-        :return: RPC result (if any).
-        """
-        params = {key: self.serializer.serialize_entity(context, value)
-                  for key, value in kwargs.items()}
-        params['context'] = context.to_dict()
-
-        if version is None:
-            version = self.version
-        if version is not None:
-            _check_version(version, self.version_cap)
-            params['rpc.version'] = version
-
-        body = {
-            "jsonrpc": "2.0",
-            "method": method,
-            "params": params,
-        }
-        if not cast:
-            body['id'] = context.request_id or uuidutils.generate_uuid()
-
-        LOG.debug("RPC %s with %s", method, strutils.mask_dict_password(body))
-        scheme = 'http'
-        if CONF.json_rpc.use_ssl:
-            scheme = 'https'
-        url = '%s://%s:%d' % (scheme,
-                              netutils.escape_ipv6(self.host),
-                              CONF.json_rpc.port)
-        result = _get_session().post(url, json=body)
-        LOG.debug('RPC %s returned %s', method,
-                  strutils.mask_password(result.text or '<None>'))
-
-        if not cast:
-            result = result.json()
-            self._handle_error(result.get('error'))
-            result = self.serializer.deserialize_entity(context,
-                                                        result['result'])
-            return result
-
-
-def _can_send_version(requested, version_cap):
-    if requested is None or version_cap is None:
-        return True
-
-    requested_parts = [int(item) for item in requested.split('.', 1)]
-    version_cap_parts = [int(item) for item in version_cap.split('.', 1)]
-
-    if requested_parts[0] != version_cap_parts[0]:
-        return False  # major version mismatch
-    else:
-        return requested_parts[1] <= version_cap_parts[1]
-
-
-def _check_version(requested, version_cap):
-    if not _can_send_version(requested, version_cap):
-        raise RuntimeError(_("Cannot send RPC request: requested version "
-                             "%(requested)s, maximum allowed version is "
-                             "%(version_cap)s") % {'requested': requested,
-                                                   'version_cap': version_cap})
diff --git a/ironic/common/json_rpc/server.py b/ironic/common/json_rpc/server.py
deleted file mode 100644
index 2fdab0c4fc..0000000000
--- a/ironic/common/json_rpc/server.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""Implementation of JSON RPC for communication between API and conductors.
-
-This module implementa a subset of JSON RPC 2.0 as defined in
-https://www.jsonrpc.org/specification. Main differences:
-* No support for batched requests.
-* No support for positional arguments passing.
-* No JSON RPC 1.0 fallback.
-"""
-
-import json
-
-from ironic_lib import auth_basic
-from keystonemiddleware import auth_token
-from oslo_config import cfg
-from oslo_log import log
-import oslo_messaging
-from oslo_service import service
-from oslo_service import wsgi
-from oslo_utils import strutils
-import webob
-
-from ironic.common import context as ir_context
-from ironic.common import exception
-from ironic.common.i18n import _
-from ironic.common import json_rpc
-
-
-CONF = cfg.CONF
-LOG = log.getLogger(__name__)
-_DENY_LIST = {'init_host', 'del_host', 'target', 'iter_nodes'}
-
-
-def _build_method_map(manager):
-    """Build mapping from method names to their bodies.
-
-    :param manager: A conductor manager.
-    :return: dict with mapping
-    """
-    result = {}
-    for method in dir(manager):
-        if method.startswith('_') or method in _DENY_LIST:
-            continue
-        func = getattr(manager, method)
-        if not callable(func):
-            continue
-        LOG.debug('Adding RPC method %s', method)
-        result[method] = func
-    return result
-
-
-class JsonRpcError(exception.IronicException):
-    pass
-
-
-class ParseError(JsonRpcError):
-    code = -32700
-    _msg_fmt = _("Invalid JSON received by RPC server")
-
-
-class InvalidRequest(JsonRpcError):
-    code = -32600
-    _msg_fmt = _("Invalid request object received by RPC server")
-
-
-class MethodNotFound(JsonRpcError):
-    code = -32601
-    _msg_fmt = _("Method %(name)s was not found")
-
-
-class InvalidParams(JsonRpcError):
-    code = -32602
-    _msg_fmt = _("Params %(params)s are invalid for %(method)s: %(error)s")
-
-
-class WSGIService(service.Service):
-    """Provides ability to launch JSON RPC as a WSGI application."""
-
-    def __init__(self, manager, serializer):
-        self.manager = manager
-        self.serializer = serializer
-        self._method_map = _build_method_map(manager)
-        auth_strategy = json_rpc.auth_strategy()
-        if auth_strategy == 'keystone':
-            conf = dict(CONF.keystone_authtoken)
-            app = auth_token.AuthProtocol(self._application, conf)
-        elif auth_strategy == 'http_basic':
-            app = auth_basic.BasicAuthMiddleware(
-                self._application,
-                cfg.CONF.json_rpc.http_basic_auth_user_file)
-        else:
-            app = self._application
-        self.server = wsgi.Server(CONF, 'ironic-json-rpc', app,
-                                  host=CONF.json_rpc.host_ip,
-                                  port=CONF.json_rpc.port,
-                                  use_ssl=CONF.json_rpc.use_ssl)
-
-    def _application(self, environment, start_response):
-        """WSGI application for conductor JSON RPC."""
-        request = webob.Request(environment)
-        if request.method != 'POST':
-            body = {'error': {'code': 405,
-                              'message': _('Only POST method can be used')}}
-            return webob.Response(status_code=405, json_body=body)(
-                environment, start_response)
-
-        if json_rpc.auth_strategy() == 'keystone':
-            roles = (request.headers.get('X-Roles') or '').split(',')
-            if 'admin' not in roles:
-                LOG.debug('Roles %s do not contain "admin", rejecting '
-                          'request', roles)
-                body = {'error': {'code': 403, 'message': _('Forbidden')}}
-                return webob.Response(status_code=403, json_body=body)(
-                    environment, start_response)
-
-        result = self._call(request)
-        if result is not None:
-            response = webob.Response(content_type='application/json',
-                                      charset='UTF-8',
-                                      json_body=result)
-        else:
-            response = webob.Response(status_code=204)
-        return response(environment, start_response)
-
-    def _handle_error(self, exc, request_id=None):
-        """Generate a JSON RPC 2.0 error body.
-
-        :param exc: Exception object.
-        :param request_id: ID of the request (if any).
-        :return: dict with response body
-        """
-        if isinstance(exc, oslo_messaging.ExpectedException):
-            exc = exc.exc_info[1]
-
-        expected = isinstance(exc, exception.IronicException)
-        cls = exc.__class__
-        if expected:
-            LOG.debug('RPC error %s: %s', cls.__name__, exc)
-        else:
-            LOG.exception('Unexpected RPC exception %s', cls.__name__)
-
-        response = {
-            "jsonrpc": "2.0",
-            "id": request_id,
-            "error": {
-                "code": getattr(exc, 'code', 500),
-                "message": str(exc),
-            }
-        }
-        if expected and not isinstance(exc, JsonRpcError):
-            # Allow de-serializing the correct class for expected errors.
-            response['error']['data'] = {
-                'class': '%s.%s' % (cls.__module__, cls.__name__)
-            }
-        return response
-
-    def _call(self, request):
-        """Process a JSON RPC request.
-
-        :param request: ``webob.Request`` object.
-        :return: dict with response body.
-        """
-        request_id = None
-        try:
-            try:
-                body = json.loads(request.text)
-            except ValueError:
-                LOG.error('Cannot parse JSON RPC request as JSON')
-                raise ParseError()
-
-            if not isinstance(body, dict):
-                LOG.error('JSON RPC request %s is not an object (batched '
-                          'requests are not supported)', body)
-                raise InvalidRequest()
-
-            request_id = body.get('id')
-            params = body.get('params', {})
-
-            if (body.get('jsonrpc') != '2.0'
-                    or not body.get('method')
-                    or not isinstance(params, dict)):
-                LOG.error('JSON RPC request %s is invalid', body)
-                raise InvalidRequest()
-        except Exception as exc:
-            # We do not treat malformed requests as notifications and return
-            # a response even when request_id is None. This seems in agreement
-            # with the examples in the specification.
-            return self._handle_error(exc, request_id)
-
-        try:
-            method = body['method']
-            try:
-                func = self._method_map[method]
-            except KeyError:
-                raise MethodNotFound(name=method)
-
-            result = self._handle_requests(func, method, params)
-            if request_id is not None:
-                return {
-                    "jsonrpc": "2.0",
-                    "result": result,
-                    "id": request_id
-                }
-        except Exception as exc:
-            result = self._handle_error(exc, request_id)
-            # We treat correctly formed requests without "id" as notifications
-            # and do not return any errors.
-            if request_id is not None:
-                return result
-
-    def _handle_requests(self, func, name, params):
-        """Convert arguments and call a method.
-
-        :param func: Callable object.
-        :param name: RPC call name for logging.
-        :param params: Keyword arguments.
-        :return: call result as JSON.
-        """
-        # TODO(dtantsur): server-side version check?
-        params.pop('rpc.version', None)
-        logged_params = strutils.mask_dict_password(params)
-
-        try:
-            context = params.pop('context')
-        except KeyError:
-            context = None
-        else:
-            # A valid context is required for deserialization
-            if not isinstance(context, dict):
-                raise InvalidParams(
-                    _("Context must be a dictionary, if provided"))
-
-            context = ir_context.RequestContext.from_dict(context)
-            params = {key: self.serializer.deserialize_entity(context, value)
-                      for key, value in params.items()}
-            params['context'] = context
-
-        LOG.debug('RPC %s with %s', name, logged_params)
-        try:
-            result = func(**params)
-        # FIXME(dtantsur): we could use the inspect module, but
-        # oslo_messaging.expected_exceptions messes up signatures.
-        except TypeError as exc:
-            raise InvalidParams(params=', '.join(params),
-                                method=name, error=exc)
-
-        if context is not None:
-            # Currently it seems that we can serialize even with invalid
-            # context, but I'm not sure it's guaranteed to be the case.
-            result = self.serializer.serialize_entity(context, result)
-        LOG.debug('RPC %s returned %s', name,
-                  strutils.mask_dict_password(result)
-                  if isinstance(result, dict) else result)
-        return result
-
-    def start(self):
-        """Start serving this service using loaded configuration.
-
-        :returns: None
-        """
-        self.server.start()
-
-    def stop(self):
-        """Stop serving this API.
-
-        :returns: None
-        """
-        self.server.stop()
-
-    def wait(self):
-        """Wait for the service to stop serving this API.
-
-        :returns: None
-        """
-        self.server.wait()
-
-    def reset(self):
-        """Reset server greenpool size to default.
-
-        :returns: None
-        """
-        self.server.reset()
diff --git a/ironic/common/rpc_service.py b/ironic/common/rpc_service.py
index a385822506..edf14e9be7 100644
--- a/ironic/common/rpc_service.py
+++ b/ironic/common/rpc_service.py
@@ -16,6 +16,7 @@
 
 import signal
 
+from ironic_lib.json_rpc import server as json_rpc
 from oslo_config import cfg
 from oslo_log import log
 import oslo_messaging as messaging
@@ -23,7 +24,6 @@ from oslo_service import service
 from oslo_utils import importutils
 
 from ironic.common import context
-from ironic.common.json_rpc import server as json_rpc
 from ironic.common import rpc
 from ironic.objects import base as objects_base
 
@@ -51,8 +51,8 @@ class RPCService(service.Service):
         # Perform preparatory actions before starting the RPC listener
         self.manager.prepare_host()
         if CONF.rpc_transport == 'json-rpc':
-            self.rpcserver = json_rpc.WSGIService(self.manager,
-                                                  serializer)
+            self.rpcserver = json_rpc.WSGIService(
+                self.manager, serializer, context.RequestContext.from_dict)
         else:
             target = messaging.Target(topic=self.topic, server=self.host)
             endpoints = [self.manager]
diff --git a/ironic/conductor/rpcapi.py b/ironic/conductor/rpcapi.py
index e41cb77919..e8fa084ac9 100644
--- a/ironic/conductor/rpcapi.py
+++ b/ironic/conductor/rpcapi.py
@@ -20,12 +20,12 @@ Client side of the conductor RPC API.
 
 import random
 
+from ironic_lib.json_rpc import client as json_rpc
 import oslo_messaging as messaging
 
 from ironic.common import exception
 from ironic.common import hash_ring
 from ironic.common.i18n import _
-from ironic.common.json_rpc import client as json_rpc
 from ironic.common import release_mappings as versions
 from ironic.common import rpc
 from ironic.conductor import manager
diff --git a/ironic/conf/__init__.py b/ironic/conf/__init__.py
index 9243cbbe3c..1503fdd210 100644
--- a/ironic/conf/__init__.py
+++ b/ironic/conf/__init__.py
@@ -35,7 +35,6 @@ from ironic.conf import inspector
 from ironic.conf import ipmi
 from ironic.conf import irmc
 from ironic.conf import iscsi
-from ironic.conf import json_rpc
 from ironic.conf import metrics
 from ironic.conf import metrics_statsd
 from ironic.conf import neutron
@@ -69,7 +68,6 @@ inspector.register_opts(CONF)
 ipmi.register_opts(CONF)
 irmc.register_opts(CONF)
 iscsi.register_opts(CONF)
-json_rpc.register_opts(CONF)
 metrics.register_opts(CONF)
 metrics_statsd.register_opts(CONF)
 neutron.register_opts(CONF)
diff --git a/ironic/conf/json_rpc.py b/ironic/conf/json_rpc.py
deleted file mode 100644
index 3fdff21f45..0000000000
--- a/ironic/conf/json_rpc.py
+++ /dev/null
@@ -1,61 +0,0 @@
-#    Licensed under the Apache License, Version 2.0 (the "License"); you may
-#    not use this file except in compliance with the License. You may obtain
-#    a copy of the License at
-#
-#         http://www.apache.org/licenses/LICENSE-2.0
-#
-#    Unless required by applicable law or agreed to in writing, software
-#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-#    License for the specific language governing permissions and limitations
-#    under the License.
-
-from oslo_config import cfg
-
-from ironic.common.i18n import _
-from ironic.conf import auth
-
-opts = [
-    cfg.StrOpt('auth_strategy',
-               choices=[('noauth', _('no authentication')),
-                        ('keystone', _('use the Identity service for '
-                                       'authentication')),
-                        ('http_basic', _('HTTP basic authentication'))],
-               help=_('Authentication strategy used by JSON RPC. Defaults to '
-                      'the global auth_strategy setting.')),
-    cfg.StrOpt('http_basic_auth_user_file',
-               default='/etc/ironic/htpasswd-json-rpc',
-               help=_('Path to Apache format user authentication file used '
-                      'when auth_strategy=http_basic')),
-    cfg.HostAddressOpt('host_ip',
-                       default='::',
-                       help=_('The IP address or hostname on which JSON RPC '
-                              'will listen.')),
-    cfg.PortOpt('port',
-                default=8089,
-                help=_('The port to use for JSON RPC')),
-    cfg.BoolOpt('use_ssl',
-                default=False,
-                help=_('Whether to use TLS for JSON RPC')),
-    cfg.StrOpt('http_basic_username',
-               deprecated_for_removal=True,
-               deprecated_reason=_("Use username instead"),
-               help=_("Name of the user to use for HTTP Basic authentication "
-                      "client requests.")),
-    cfg.StrOpt('http_basic_password',
-               deprecated_for_removal=True,
-               deprecated_reason=_("Use password instead"),
-               secret=True,
-               help=_("Password to use for HTTP Basic authentication "
-                      "client requests.")),
-]
-
-
-def register_opts(conf):
-    conf.register_opts(opts, group='json_rpc')
-    auth.register_auth_opts(conf, 'json_rpc')
-    conf.set_default('timeout', 120, group='json_rpc')
-
-
-def list_opts():
-    return opts + auth.add_auth_opts([])
diff --git a/ironic/conf/opts.py b/ironic/conf/opts.py
index 464469d193..1cde057cb2 100644
--- a/ironic/conf/opts.py
+++ b/ironic/conf/opts.py
@@ -51,7 +51,6 @@ _opts = [
     ('ipmi', ironic.conf.ipmi.opts),
     ('irmc', ironic.conf.irmc.opts),
     ('iscsi', ironic.conf.iscsi.opts),
-    ('json_rpc', ironic.conf.json_rpc.list_opts()),
     ('metrics', ironic.conf.metrics.opts),
     ('metrics_statsd', ironic.conf.metrics_statsd.opts),
     ('neutron', ironic.conf.neutron.list_opts()),
diff --git a/ironic/tests/unit/common/test_json_rpc.py b/ironic/tests/unit/common/test_json_rpc.py
deleted file mode 100644
index fb7e7eca04..0000000000
--- a/ironic/tests/unit/common/test_json_rpc.py
+++ /dev/null
@@ -1,714 +0,0 @@
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import os
-import tempfile
-from unittest import mock
-
-import fixtures
-import oslo_messaging
-import webob
-
-from ironic.common import context as ir_ctx
-from ironic.common import exception
-from ironic.common.json_rpc import client
-from ironic.common.json_rpc import server
-from ironic import objects
-from ironic.objects import base as objects_base
-from ironic.tests import base as test_base
-from ironic.tests.unit.db import utils as db_utils
-from ironic.tests.unit.objects import utils as obj_utils
-
-
-class FakeManager(object):
-
-    def success(self, context, x, y=0):
-        assert isinstance(context, ir_ctx.RequestContext)
-        assert context.user_name == 'admin'
-        return x - y
-
-    def with_node(self, context, node):
-        assert isinstance(context, ir_ctx.RequestContext)
-        assert isinstance(node, objects.Node)
-        node.extra['answer'] = 42
-        return node
-
-    def no_result(self, context):
-        assert isinstance(context, ir_ctx.RequestContext)
-        return None
-
-    def no_context(self):
-        return 42
-
-    def fail(self, context, message):
-        assert isinstance(context, ir_ctx.RequestContext)
-        raise exception.IronicException(message)
-
-    @oslo_messaging.expected_exceptions(exception.Invalid)
-    def expected(self, context, message):
-        assert isinstance(context, ir_ctx.RequestContext)
-        raise exception.Invalid(message)
-
-    def crash(self, context):
-        raise RuntimeError('boom')
-
-    def init_host(self, context):
-        assert False, "This should not be exposed"
-
-    def _private(self, context):
-        assert False, "This should not be exposed"
-
-    # This should not be exposed either
-    value = 42
-
-
-class TestService(test_base.TestCase):
-
-    def setUp(self):
-        super(TestService, self).setUp()
-        self.config(auth_strategy='noauth', group='json_rpc')
-        self.server_mock = self.useFixture(fixtures.MockPatch(
-            'oslo_service.wsgi.Server', autospec=True)).mock
-
-        self.serializer = objects_base.IronicObjectSerializer(is_server=True)
-        self.service = server.WSGIService(FakeManager(), self.serializer)
-        self.app = self.service._application
-        self.ctx = {'user_name': 'admin'}
-
-    def _request(self, name=None, params=None, expected_error=None,
-                 request_id='abcd', **kwargs):
-        body = {
-            'jsonrpc': '2.0',
-        }
-        if request_id is not None:
-            body['id'] = request_id
-        if name is not None:
-            body['method'] = name
-        if params is not None:
-            body['params'] = params
-        if 'json_body' not in kwargs:
-            kwargs['json_body'] = body
-        kwargs.setdefault('method', 'POST')
-        kwargs.setdefault('headers', {'Content-Type': 'application/json'})
-
-        request = webob.Request.blank("/", **kwargs)
-        response = request.get_response(self.app)
-        self.assertEqual(response.status_code,
-                         expected_error or (200 if request_id else 204))
-        if request_id is not None:
-            if expected_error:
-                self.assertEqual(expected_error,
-                                 response.json_body['error']['code'])
-            else:
-                return response.json_body
-        else:
-            return response.text
-
-    def _check(self, body, result=None, error=None, request_id='abcd'):
-        self.assertEqual('2.0', body.pop('jsonrpc'))
-        self.assertEqual(request_id, body.pop('id'))
-        if error is not None:
-            self.assertEqual({'error': error}, body)
-        else:
-            self.assertEqual({'result': result}, body)
-
-    def _setup_http_basic(self):
-        with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
-            f.write('myName:$2y$05$lE3eGtyj41jZwrzS87KTqe6.'
-                    'JETVCWBkc32C63UP2aYrGoYOEpbJm\n\n\n')
-        self.addCleanup(os.remove, f.name)
-        self.config(http_basic_auth_user_file=f.name, group='json_rpc')
-        self.config(auth_strategy='http_basic', group='json_rpc')
-        # self.config(http_basic_username='myUser', group='json_rpc')
-        # self.config(http_basic_password='myPassword', group='json_rpc')
-        self.service = server.WSGIService(FakeManager(), self.serializer)
-        self.app = self.server_mock.call_args[0][2]
-
-    def test_http_basic_not_authenticated(self):
-        self._setup_http_basic()
-        self._request('success', {'context': self.ctx, 'x': 42},
-                      request_id=None, expected_error=401)
-
-    def test_http_basic(self):
-        self._setup_http_basic()
-        headers = {
-            'Content-Type': 'application/json',
-            'Authorization': 'Basic bXlOYW1lOm15UGFzc3dvcmQ='
-        }
-        body = self._request('success', {'context': self.ctx, 'x': 42},
-                             headers=headers)
-        self._check(body, result=42)
-
-    def test_success(self):
-        body = self._request('success', {'context': self.ctx, 'x': 42})
-        self._check(body, result=42)
-
-    def test_success_no_result(self):
-        body = self._request('no_result', {'context': self.ctx})
-        self._check(body, result=None)
-
-    def test_notification(self):
-        body = self._request('no_result', {'context': self.ctx},
-                             request_id=None)
-        self.assertEqual('', body)
-
-    def test_no_context(self):
-        body = self._request('no_context')
-        self._check(body, result=42)
-
-    def test_serialize_objects(self):
-        node = obj_utils.get_test_node(self.context)
-        node = self.serializer.serialize_entity(self.context, node)
-        body = self._request('with_node', {'context': self.ctx, 'node': node})
-        self.assertNotIn('error', body)
-        self.assertIsInstance(body['result'], dict)
-        node = self.serializer.deserialize_entity(self.context, body['result'])
-        self.assertEqual({'answer': 42}, node.extra)
-
-    def test_non_json_body(self):
-        for body in (b'', b'???', b"\xc3\x28"):
-            request = webob.Request.blank("/", method='POST', body=body)
-            response = request.get_response(self.app)
-            self._check(
-                response.json_body,
-                error={
-                    'message': server.ParseError._msg_fmt,
-                    'code': -32700,
-                },
-                request_id=None)
-
-    def test_invalid_requests(self):
-        bodies = [
-            # Invalid requests with request ID.
-            {'method': 'no_result', 'id': 'abcd',
-             'params': {'context': self.ctx}},
-            {'jsonrpc': '2.0', 'id': 'abcd', 'params': {'context': self.ctx}},
-            # These do not count as notifications, since they're malformed.
-            {'method': 'no_result', 'params': {'context': self.ctx}},
-            {'jsonrpc': '2.0', 'params': {'context': self.ctx}},
-            42,
-            # We do not implement batched requests.
-            [],
-            [{'jsonrpc': '2.0', 'method': 'no_result',
-              'params': {'context': self.ctx}}],
-        ]
-        for body in bodies:
-            body = self._request(json_body=body)
-            self._check(
-                body,
-                error={
-                    'message': server.InvalidRequest._msg_fmt,
-                    'code': -32600,
-                },
-                request_id=body.get('id'))
-
-    def test_malformed_context(self):
-        body = self._request(json_body={'jsonrpc': '2.0', 'id': 'abcd',
-                                        'method': 'no_result',
-                                        'params': {'context': 42}})
-        self._check(
-            body,
-            error={
-                'message': 'Context must be a dictionary, if provided',
-                'code': -32602,
-            })
-
-    def test_expected_failure(self):
-        body = self._request('fail', {'context': self.ctx,
-                                      'message': 'some error'})
-        self._check(body,
-                    error={
-                        'message': 'some error',
-                        'code': 500,
-                        'data': {
-                            'class': 'ironic_lib.exception.IronicException'
-                        }
-                    })
-
-    def test_expected_failure_oslo(self):
-        # Check that exceptions wrapped by oslo's expected_exceptions get
-        # unwrapped correctly.
-        body = self._request('expected', {'context': self.ctx,
-                                          'message': 'some error'})
-        self._check(body,
-                    error={
-                        'message': 'some error',
-                        'code': 400,
-                        'data': {
-                            'class': 'ironic.common.exception.Invalid'
-                        }
-                    })
-
-    @mock.patch.object(server.LOG, 'exception', autospec=True)
-    def test_unexpected_failure(self, mock_log):
-        body = self._request('crash', {'context': self.ctx})
-        self._check(body,
-                    error={
-                        'message': 'boom',
-                        'code': 500,
-                    })
-        self.assertTrue(mock_log.called)
-
-    def test_method_not_found(self):
-        body = self._request('banana', {'context': self.ctx})
-        self._check(body,
-                    error={
-                        'message': 'Method banana was not found',
-                        'code': -32601,
-                    })
-
-    def test_no_deny_methods(self):
-        for name in ('__init__', '_private', 'init_host', 'value'):
-            body = self._request(name, {'context': self.ctx})
-            self._check(body,
-                        error={
-                            'message': 'Method %s was not found' % name,
-                            'code': -32601,
-                        })
-
-    def test_missing_argument(self):
-        body = self._request('success', {'context': self.ctx})
-        # The exact error message depends on the Python version
-        self.assertEqual(-32602, body['error']['code'])
-        self.assertNotIn('result', body)
-
-    def test_method_not_post(self):
-        self._request('success', {'context': self.ctx, 'x': 42},
-                      method='GET', expected_error=405)
-
-    def test_authenticated(self):
-        self.config(auth_strategy='keystone', group='json_rpc')
-        self.service = server.WSGIService(FakeManager(), self.serializer)
-        self.app = self.server_mock.call_args[0][2]
-        self._request('success', {'context': self.ctx, 'x': 42},
-                      expected_error=401)
-
-    def test_authenticated_no_admin_role(self):
-        self.config(auth_strategy='keystone', group='json_rpc')
-        self._request('success', {'context': self.ctx, 'x': 42},
-                      expected_error=403)
-
-    @mock.patch.object(server.LOG, 'debug', autospec=True)
-    def test_mask_secrets(self, mock_log):
-        node = obj_utils.get_test_node(
-            self.context, driver_info=db_utils.get_test_ipmi_info())
-        node = self.serializer.serialize_entity(self.context, node)
-        body = self._request('with_node', {'context': self.ctx, 'node': node})
-        node = self.serializer.deserialize_entity(self.context, body['result'])
-        logged_params = mock_log.call_args_list[0][0][2]
-        logged_node = logged_params['node']['ironic_object.data']
-        self.assertEqual('***', logged_node['driver_info']['ipmi_password'])
-        logged_resp = mock_log.call_args_list[1][0][2]
-        logged_node = logged_resp['ironic_object.data']
-        self.assertEqual('***', logged_node['driver_info']['ipmi_password'])
-        # The result is not affected, only logging
-        self.assertEqual(db_utils.get_test_ipmi_info(), node.driver_info)
-
-
-@mock.patch.object(client, '_get_session', autospec=True)
-class TestClient(test_base.TestCase):
-
-    def setUp(self):
-        super(TestClient, self).setUp()
-        self.serializer = objects_base.IronicObjectSerializer(is_server=True)
-        self.client = client.Client(self.serializer)
-        self.ctx_json = self.context.to_dict()
-
-    def test_can_send_version(self, mock_session):
-        self.assertTrue(self.client.can_send_version('1.42'))
-        self.client = client.Client(self.serializer, version_cap='1.42')
-        self.assertTrue(self.client.can_send_version('1.42'))
-        self.assertTrue(self.client.can_send_version('1.0'))
-        self.assertFalse(self.client.can_send_version('1.99'))
-        self.assertFalse(self.client.can_send_version('2.0'))
-
-    def test_call_success(self, mock_session):
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'result': 42
-        }
-        cctx = self.client.prepare('foo.example.com')
-        self.assertEqual('example.com', cctx.host)
-        result = cctx.call(self.context, 'do_something', answer=42)
-        self.assertEqual(42, result)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json},
-                  'id': self.context.request_id})
-
-    def test_call_ipv4_success(self, mock_session):
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'result': 42
-        }
-        cctx = self.client.prepare('foo.192.0.2.1')
-        self.assertEqual('192.0.2.1', cctx.host)
-        result = cctx.call(self.context, 'do_something', answer=42)
-        self.assertEqual(42, result)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://192.0.2.1:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json},
-                  'id': self.context.request_id})
-
-    def test_call_ipv6_success(self, mock_session):
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'result': 42
-        }
-        cctx = self.client.prepare('foo.2001:db8::1')
-        self.assertEqual('2001:db8::1', cctx.host)
-        result = cctx.call(self.context, 'do_something', answer=42)
-        self.assertEqual(42, result)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://[2001:db8::1]:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json},
-                  'id': self.context.request_id})
-
-    def test_call_success_with_version(self, mock_session):
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'result': 42
-        }
-        cctx = self.client.prepare('foo.example.com', version='1.42')
-        self.assertEqual('example.com', cctx.host)
-        result = cctx.call(self.context, 'do_something', answer=42)
-        self.assertEqual(42, result)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json,
-                             'rpc.version': '1.42'},
-                  'id': self.context.request_id})
-
-    def test_call_success_with_version_and_cap(self, mock_session):
-        self.client = client.Client(self.serializer, version_cap='1.99')
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'result': 42
-        }
-        cctx = self.client.prepare('foo.example.com', version='1.42')
-        self.assertEqual('example.com', cctx.host)
-        result = cctx.call(self.context, 'do_something', answer=42)
-        self.assertEqual(42, result)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json,
-                             'rpc.version': '1.42'},
-                  'id': self.context.request_id})
-
-    def test_call_with_ssl(self, mock_session):
-        self.config(use_ssl=True, group='json_rpc')
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'result': 42
-        }
-        cctx = self.client.prepare('foo.example.com')
-        self.assertEqual('example.com', cctx.host)
-        result = cctx.call(self.context, 'do_something', answer=42)
-        self.assertEqual(42, result)
-        mock_session.return_value.post.assert_called_once_with(
-            'https://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json},
-                  'id': self.context.request_id})
-
-    def test_cast_success(self, mock_session):
-        cctx = self.client.prepare('foo.example.com')
-        self.assertEqual('example.com', cctx.host)
-        result = cctx.cast(self.context, 'do_something', answer=42)
-        self.assertIsNone(result)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json}})
-
-    def test_cast_success_with_version(self, mock_session):
-        cctx = self.client.prepare('foo.example.com', version='1.42')
-        self.assertEqual('example.com', cctx.host)
-        result = cctx.cast(self.context, 'do_something', answer=42)
-        self.assertIsNone(result)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json,
-                             'rpc.version': '1.42'}})
-
-    def test_call_serialization(self, mock_session):
-        node = obj_utils.get_test_node(self.context)
-        node_json = self.serializer.serialize_entity(self.context, node)
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'result': node_json
-        }
-        cctx = self.client.prepare('foo.example.com')
-        self.assertEqual('example.com', cctx.host)
-        result = cctx.call(self.context, 'do_something', node=node)
-        self.assertIsInstance(result, objects.Node)
-        self.assertEqual(result.uuid, node.uuid)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'node': node_json, 'context': self.ctx_json},
-                  'id': self.context.request_id})
-
-    def test_call_failure(self, mock_session):
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'error': {
-                'code': 418,
-                'message': 'I am a teapot',
-                'data': {
-                    'class': 'ironic.common.exception.Invalid'
-                }
-            }
-        }
-        cctx = self.client.prepare('foo.example.com')
-        self.assertEqual('example.com', cctx.host)
-        # Make sure that the class is restored correctly for expected errors.
-        exc = self.assertRaises(exception.Invalid,
-                                cctx.call,
-                                self.context, 'do_something', answer=42)
-        # Code from the body has priority over one in the class.
-        self.assertEqual(418, exc.code)
-        self.assertIn('I am a teapot', str(exc))
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json},
-                  'id': self.context.request_id})
-
-    def test_call_unexpected_failure(self, mock_session):
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'error': {
-                'code': 500,
-                'message': 'AttributeError',
-            }
-        }
-        cctx = self.client.prepare('foo.example.com')
-        self.assertEqual('example.com', cctx.host)
-        exc = self.assertRaises(exception.IronicException,
-                                cctx.call,
-                                self.context, 'do_something', answer=42)
-        self.assertEqual(500, exc.code)
-        self.assertIn('Unexpected error', str(exc))
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json},
-                  'id': self.context.request_id})
-
-    def test_call_failure_with_foreign_class(self, mock_session):
-        # This should not happen, but provide an additional safeguard
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'error': {
-                'code': 500,
-                'message': 'AttributeError',
-                'data': {
-                    'class': 'AttributeError'
-                }
-            }
-        }
-        cctx = self.client.prepare('foo.example.com')
-        self.assertEqual('example.com', cctx.host)
-        exc = self.assertRaises(exception.IronicException,
-                                cctx.call,
-                                self.context, 'do_something', answer=42)
-        self.assertEqual(500, exc.code)
-        self.assertIn('Unexpected error', str(exc))
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json},
-                  'id': self.context.request_id})
-
-    def test_cast_failure(self, mock_session):
-        # Cast cannot return normal failures, but make sure we ignore them even
-        # if server sends something in violation of the protocol (or because
-        # it's a low-level error like HTTP Forbidden).
-        response = mock_session.return_value.post.return_value
-        response.json.return_value = {
-            'jsonrpc': '2.0',
-            'error': {
-                'code': 418,
-                'message': 'I am a teapot',
-                'data': {
-                    'class': 'ironic.common.exception.IronicException'
-                }
-            }
-        }
-        cctx = self.client.prepare('foo.example.com')
-        self.assertEqual('example.com', cctx.host)
-        result = cctx.cast(self.context, 'do_something', answer=42)
-        self.assertIsNone(result)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'answer': 42, 'context': self.ctx_json}})
-
-    def test_call_failure_with_version_and_cap(self, mock_session):
-        self.client = client.Client(self.serializer, version_cap='1.42')
-        cctx = self.client.prepare('foo.example.com', version='1.99')
-        self.assertRaisesRegex(RuntimeError,
-                               "requested version 1.99, maximum allowed "
-                               "version is 1.42",
-                               cctx.call, self.context, 'do_something',
-                               answer=42)
-        self.assertFalse(mock_session.return_value.post.called)
-
-    @mock.patch.object(client.LOG, 'debug', autospec=True)
-    def test_mask_secrets(self, mock_log, mock_session):
-        request = {
-            'redfish_username': 'admin',
-            'redfish_password': 'passw0rd'
-        }
-        body = """{
-            "jsonrpc": "2.0",
-            "result": {
-                "driver_info": {
-                    "ipmi_username": "admin",
-                    "ipmi_password": "passw0rd"
-                }
-            }
-        }"""
-        response = mock_session.return_value.post.return_value
-        response.text = body
-        cctx = self.client.prepare('foo.example.com')
-        cctx.cast(self.context, 'do_something', node=request)
-        mock_session.return_value.post.assert_called_once_with(
-            'http://example.com:8089',
-            json={'jsonrpc': '2.0',
-                  'method': 'do_something',
-                  'params': {'node': request, 'context': self.ctx_json}})
-        self.assertEqual(2, mock_log.call_count)
-        node = mock_log.call_args_list[0][0][2]['params']['node']
-        self.assertEqual(node, {'redfish_username': 'admin',
-                                'redfish_password': '***'})
-        resp_text = mock_log.call_args_list[1][0][2]
-        self.assertEqual(body.replace('passw0rd', '***'), resp_text)
-
-
-@mock.patch('ironic.common.json_rpc.client.keystone', autospec=True)
-class TestSession(test_base.TestCase):
-
-    def setUp(self):
-        super(TestSession, self).setUp()
-        client._SESSION = None
-
-    def test_noauth(self, mock_keystone):
-        self.config(auth_strategy='noauth', group='json_rpc')
-        session = client._get_session()
-
-        mock_keystone.get_auth.assert_called_once_with('json_rpc')
-        auth = mock_keystone.get_auth.return_value
-
-        mock_keystone.get_session.assert_called_once_with(
-            'json_rpc', auth=auth)
-
-        internal_session = mock_keystone.get_session.return_value
-
-        mock_keystone.get_adapter.assert_called_once_with(
-            'json_rpc',
-            session=internal_session,
-            additional_headers={
-                'Content-Type': 'application/json'
-            })
-        self.assertEqual(mock_keystone.get_adapter.return_value, session)
-
-    def test_keystone(self, mock_keystone):
-        self.config(auth_strategy='keystone', group='json_rpc')
-        session = client._get_session()
-
-        mock_keystone.get_auth.assert_called_once_with('json_rpc')
-        auth = mock_keystone.get_auth.return_value
-
-        mock_keystone.get_session.assert_called_once_with(
-            'json_rpc', auth=auth)
-
-        internal_session = mock_keystone.get_session.return_value
-
-        mock_keystone.get_adapter.assert_called_once_with(
-            'json_rpc',
-            session=internal_session,
-            additional_headers={
-                'Content-Type': 'application/json'
-            })
-        self.assertEqual(mock_keystone.get_adapter.return_value, session)
-
-    def test_http_basic(self, mock_keystone):
-        self.config(auth_strategy='http_basic', group='json_rpc')
-        session = client._get_session()
-
-        mock_keystone.get_auth.assert_called_once_with('json_rpc')
-        auth = mock_keystone.get_auth.return_value
-        mock_keystone.get_session.assert_called_once_with(
-            'json_rpc', auth=auth)
-
-        internal_session = mock_keystone.get_session.return_value
-
-        mock_keystone.get_adapter.assert_called_once_with(
-            'json_rpc',
-            session=internal_session,
-            additional_headers={
-                'Content-Type': 'application/json'
-            })
-        self.assertEqual(mock_keystone.get_adapter.return_value, session)
-
-    def test_http_basic_deprecated(self, mock_keystone):
-        self.config(auth_strategy='http_basic', group='json_rpc')
-        self.config(http_basic_username='myName', group='json_rpc')
-        self.config(http_basic_password='myPassword', group='json_rpc')
-        session = client._get_session()
-
-        mock_keystone.get_auth.assert_called_once_with(
-            'json_rpc', username='myName', password='myPassword')
-        auth = mock_keystone.get_auth.return_value
-        mock_keystone.get_session.assert_called_once_with(
-            'json_rpc', auth=auth)
-
-        internal_session = mock_keystone.get_session.return_value
-
-        mock_keystone.get_adapter.assert_called_once_with(
-            'json_rpc',
-            session=internal_session,
-            additional_headers={
-                'Content-Type': 'application/json'
-            })
-        self.assertEqual(mock_keystone.get_adapter.return_value, session)
diff --git a/requirements.txt b/requirements.txt
index 192913b64e..ca59703ccb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@ WebOb>=1.7.1 # MIT
 python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0
 python-glanceclient>=2.8.0 # Apache-2.0
 keystoneauth1>=4.2.0 # Apache-2.0
-ironic-lib>=4.3.0 # Apache-2.0
+ironic-lib>=4.6.1 # Apache-2.0
 python-swiftclient>=3.2.0 # Apache-2.0
 pytz>=2013.6 # MIT
 stevedore>=1.20.0 # Apache-2.0
diff --git a/tools/config/ironic-config-generator.conf b/tools/config/ironic-config-generator.conf
index a14a0ec325..5c01f82dda 100644
--- a/tools/config/ironic-config-generator.conf
+++ b/tools/config/ironic-config-generator.conf
@@ -5,6 +5,7 @@ namespace = ironic
 namespace = ironic_lib.disk_utils
 namespace = ironic_lib.disk_partitioner
 namespace = ironic_lib.exception
+namespace = ironic_lib.json_rpc
 namespace = ironic_lib.mdns
 namespace = ironic_lib.metrics
 namespace = ironic_lib.metrics_statsd