Merge "Migrate ironic_lib to ironic"
This commit is contained in:
commit
75b84f6638
@ -35,7 +35,6 @@ set +o pipefail
|
||||
|
||||
# Set up default directories
|
||||
GITDIR["python-ironicclient"]=$DEST/python-ironicclient
|
||||
GITDIR["ironic-lib"]=$DEST/ironic-lib
|
||||
|
||||
GITREPO["pyghmi"]=${PYGHMI_REPO:-${GIT_BASE}/x/pyghmi}
|
||||
GITBRANCH["pyghmi"]=${PYGHMI_BRANCH:-master}
|
||||
@ -1200,11 +1199,6 @@ function install_ironic {
|
||||
done
|
||||
fi
|
||||
|
||||
if use_library_from_git "ironic-lib"; then
|
||||
git_clone_by_name "ironic-lib"
|
||||
setup_dev_lib "ironic-lib"
|
||||
fi
|
||||
|
||||
setup_develop $IRONIC_DIR
|
||||
|
||||
install_apache_wsgi
|
||||
@ -2956,9 +2950,6 @@ function build_tinyipa_ramdisk {
|
||||
export AUTHORIZE_SSH=true
|
||||
export SSH_PUBLIC_KEY=$IRONIC_ANSIBLE_SSH_KEY.pub
|
||||
fi
|
||||
if [ -e $DEST/ironic-lib ]; then
|
||||
export IRONIC_LIB_SOURCE="$DEST/ironic-lib"
|
||||
fi
|
||||
make
|
||||
cp tinyipa.gz $ramdisk_path
|
||||
cp tinyipa.vmlinuz $kernel_path
|
||||
@ -3009,12 +3000,6 @@ function build_ipa_dib_ramdisk {
|
||||
install_diskimage_builder
|
||||
fi
|
||||
|
||||
if [ -e $DEST/ironic-lib ]; then
|
||||
export IRONIC_LIB_FROM_SOURCE=true
|
||||
export DIB_REPOLOCATION_ironic_lib=$DEST/ironic-lib
|
||||
export DIB_REPOREF_ironic_lib=$TARGET_BRANCH
|
||||
fi
|
||||
|
||||
echo "Building IPA ramdisk with DIB options: $IRONIC_DIB_RAMDISK_OPTIONS"
|
||||
if is_deploy_iso_required; then
|
||||
IRONIC_DIB_RAMDISK_OPTIONS+=" iso"
|
||||
|
@ -87,8 +87,6 @@ or via notifier plugin (such as is done with ironic-prometheus-exporter).
|
||||
Ironic service model. A separate webserver process presently does not have
|
||||
the capability of triggering the call to retrieve and transmit the data.
|
||||
|
||||
.. NOTE::
|
||||
This functionality requires ironic-lib version 5.4.0 to be installed.
|
||||
|
||||
Types of Metrics Emitted
|
||||
========================
|
||||
|
@ -15,7 +15,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from ironic_lib import auth_basic
|
||||
import keystonemiddleware.audit as audit_middleware
|
||||
from keystonemiddleware import auth_token
|
||||
from oslo_config import cfg
|
||||
@ -31,6 +30,7 @@ from ironic.api import hooks
|
||||
from ironic.api import middleware
|
||||
from ironic.api.middleware import auth_public_routes
|
||||
from ironic.api.middleware import json_ext
|
||||
from ironic.common import auth_basic
|
||||
from ironic.common import exception
|
||||
from ironic.conf import CONF
|
||||
|
||||
|
@ -12,7 +12,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
import pecan
|
||||
@ -27,6 +26,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
from ironic import objects
|
||||
|
||||
|
||||
|
@ -13,7 +13,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from pecan import rest
|
||||
|
||||
from ironic import api
|
||||
@ -21,6 +20,7 @@ from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common import metrics_utils
|
||||
from ironic import objects
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
@ -15,7 +15,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_utils import uuidutils
|
||||
from pecan import rest
|
||||
|
||||
@ -29,6 +28,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
from ironic import objects
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
@ -10,7 +10,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_log import log
|
||||
from oslo_utils import timeutils
|
||||
from pecan import rest
|
||||
@ -22,6 +21,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
import ironic.conf
|
||||
from ironic import objects
|
||||
|
||||
|
@ -12,7 +12,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_log import log
|
||||
from oslo_utils import strutils
|
||||
from oslo_utils import uuidutils
|
||||
@ -29,6 +28,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
import ironic.conf
|
||||
from ironic import objects
|
||||
|
||||
|
@ -15,7 +15,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from pecan import rest
|
||||
|
||||
from ironic import api
|
||||
@ -25,6 +24,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
from ironic.drivers import base as driver_base
|
||||
|
||||
|
||||
|
@ -12,7 +12,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_log import log
|
||||
import pecan
|
||||
|
||||
@ -20,6 +19,7 @@ from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common import metrics_utils
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
||||
|
@ -13,13 +13,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from pecan import rest
|
||||
|
||||
from ironic import api
|
||||
from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import metrics_utils
|
||||
from ironic import objects
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
@ -19,7 +19,6 @@ from http import client as http_client
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
import jsonschema
|
||||
from jsonschema import exceptions as json_schema_exc
|
||||
from oslo_log import log
|
||||
@ -46,6 +45,7 @@ from ironic.common import boot_devices
|
||||
from ironic.common import boot_modes
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
from ironic.common import policy
|
||||
from ironic.common import states as ir_states
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
|
@ -15,7 +15,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_log import log
|
||||
from oslo_utils import uuidutils
|
||||
from pecan import rest
|
||||
@ -29,6 +28,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
from ironic.common import states as ir_states
|
||||
from ironic import objects
|
||||
|
||||
|
@ -12,7 +12,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_utils import uuidutils
|
||||
import pecan
|
||||
|
||||
@ -26,6 +25,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
from ironic.common import states as ir_states
|
||||
from ironic import objects
|
||||
|
||||
|
@ -12,7 +12,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_log import log
|
||||
from oslo_utils import strutils
|
||||
from oslo_utils import uuidutils
|
||||
@ -29,6 +28,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
import ironic.conf
|
||||
from ironic import objects
|
||||
|
||||
|
@ -10,7 +10,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_config import cfg
|
||||
import pecan
|
||||
|
||||
@ -20,6 +19,7 @@ from ironic.api.controllers.v1 import versions
|
||||
from ironic.api import method
|
||||
from ironic.api import validation
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
@ -14,7 +14,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_utils import uuidutils
|
||||
from pecan import rest
|
||||
|
||||
@ -27,6 +26,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
from ironic import objects
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
@ -14,7 +14,6 @@
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from ironic_lib import metrics_utils
|
||||
from oslo_utils import uuidutils
|
||||
from pecan import rest
|
||||
|
||||
@ -27,6 +26,7 @@ from ironic.api import method
|
||||
from ironic.common import args
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
from ironic.common import policy
|
||||
from ironic import objects
|
||||
|
||||
|
203
ironic/common/auth_basic.py
Normal file
203
ironic/common/auth_basic.py
Normal file
@ -0,0 +1,203 @@
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import logging
|
||||
|
||||
import bcrypt
|
||||
import webob
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BasicAuthMiddleware(object):
|
||||
"""Middleware which performs HTTP basic authentication on requests
|
||||
|
||||
"""
|
||||
def __init__(self, app, auth_file):
|
||||
self.app = app
|
||||
self.auth_file = auth_file
|
||||
validate_auth_file(auth_file)
|
||||
|
||||
def format_exception(self, e):
|
||||
result = {'error': {'message': str(e), 'code': e.code}}
|
||||
headers = list(e.headers.items()) + [
|
||||
('Content-Type', 'application/json')
|
||||
]
|
||||
return webob.Response(content_type='application/json',
|
||||
status_code=e.code,
|
||||
json_body=result,
|
||||
headerlist=headers)
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
|
||||
try:
|
||||
token = parse_header(env)
|
||||
username, password = parse_token(token)
|
||||
env.update(authenticate(self.auth_file, username, password))
|
||||
|
||||
return self.app(env, start_response)
|
||||
|
||||
except exception.IronicException as e:
|
||||
response = self.format_exception(e)
|
||||
return response(env, start_response)
|
||||
|
||||
|
||||
def authenticate(auth_file, username, password):
|
||||
"""Finds username and password match in Apache style user auth file
|
||||
|
||||
The user auth file format is expected to comply with Apache
|
||||
documentation[1] however the bcrypt password digest is the *only*
|
||||
digest format supported.
|
||||
|
||||
[1] https://httpd.apache.org/docs/current/misc/password_encryptions.html
|
||||
|
||||
:param: auth_file: Path to user auth file
|
||||
:param: username: Username to authenticate
|
||||
:param: password: Password encoded as bytes
|
||||
:returns: A dictionary of WSGI environment values to append to the request
|
||||
:raises: Unauthorized, if no file entries match supplied username/password
|
||||
"""
|
||||
line_prefix = username + ':'
|
||||
try:
|
||||
with open(auth_file, 'r') as f:
|
||||
for line in f:
|
||||
entry = line.strip()
|
||||
if entry and entry.startswith(line_prefix):
|
||||
return auth_entry(entry, password)
|
||||
except OSError as exc:
|
||||
LOG.error('Problem reading auth user file: %s', exc)
|
||||
raise exception.ConfigInvalid(
|
||||
error_msg=_('Problem reading auth user file'))
|
||||
|
||||
# reached end of file with no matches
|
||||
LOG.info('User %s not found', username)
|
||||
unauthorized()
|
||||
|
||||
|
||||
def auth_entry(entry, password):
|
||||
"""Compare a password with a single user auth file entry
|
||||
|
||||
:param: entry: Line from auth user file to use for authentication
|
||||
:param: password: Password encoded as bytes
|
||||
:returns: A dictionary of WSGI environment values to append to the request
|
||||
:raises: Unauthorized, if the entry doesn't match supplied password or
|
||||
if the entry is encrypted with a method other than bcrypt
|
||||
"""
|
||||
username, encrypted = parse_entry(entry)
|
||||
|
||||
if not bcrypt.checkpw(password, encrypted):
|
||||
LOG.info('Password for %s does not match', username)
|
||||
unauthorized()
|
||||
|
||||
return {
|
||||
'HTTP_X_USER': username,
|
||||
'HTTP_X_USER_NAME': username
|
||||
}
|
||||
|
||||
|
||||
def validate_auth_file(auth_file):
|
||||
"""Read the auth user file and validate its correctness
|
||||
|
||||
:param: auth_file: Path to user auth file
|
||||
:raises: ConfigInvalid on validation error
|
||||
"""
|
||||
try:
|
||||
with open(auth_file, 'r') as f:
|
||||
for line in f:
|
||||
entry = line.strip()
|
||||
if entry and ':' in entry:
|
||||
parse_entry(entry)
|
||||
except OSError:
|
||||
raise exception.ConfigInvalid(
|
||||
error_msg=_('Problem reading auth user file: %s') % auth_file)
|
||||
|
||||
|
||||
def parse_entry(entry):
|
||||
"""Extrace the username and encrypted password from a user auth file entry
|
||||
|
||||
:param: entry: Line from auth user file to use for authentication
|
||||
:returns: a tuple of username and encrypted password
|
||||
:raises: ConfigInvalid if the password is not in the supported bcrypt
|
||||
format
|
||||
"""
|
||||
username, encrypted_str = entry.split(':', maxsplit=1)
|
||||
encrypted = encrypted_str.encode('utf-8')
|
||||
|
||||
if encrypted[:4] not in (b'$2y$', b'$2a$', b'$2b$'):
|
||||
error_msg = _('Only bcrypt digested passwords are supported for '
|
||||
'%(username)s') % {'username': username}
|
||||
raise exception.ConfigInvalid(error_msg=error_msg)
|
||||
return username, encrypted
|
||||
|
||||
|
||||
def parse_token(token):
|
||||
"""Parse the token portion of the Authentication header value
|
||||
|
||||
:param: token: Token value from basic authorization header
|
||||
:returns: tuple of username, password
|
||||
:raises: Unauthorized, if username and password could not be parsed for any
|
||||
reason
|
||||
"""
|
||||
try:
|
||||
if isinstance(token, str):
|
||||
token = token.encode('utf-8')
|
||||
auth_pair = base64.b64decode(token, validate=True)
|
||||
(username, password) = auth_pair.split(b':', maxsplit=1)
|
||||
|
||||
return (username.decode('utf-8'), password)
|
||||
except (TypeError, binascii.Error, ValueError) as exc:
|
||||
LOG.info('Could not decode authorization token: %s', exc)
|
||||
raise exception.BadRequest(_('Could not decode authorization token'))
|
||||
|
||||
|
||||
def parse_header(env):
|
||||
"""Parse WSGI environment for Authorization header of type Basic
|
||||
|
||||
:param: env: WSGI environment to get header from
|
||||
:returns: Token portion of the header value
|
||||
:raises: Unauthorized, if header is missing or if the type is not Basic
|
||||
"""
|
||||
try:
|
||||
auth_header = env.pop('HTTP_AUTHORIZATION')
|
||||
except KeyError:
|
||||
LOG.info('No authorization token received')
|
||||
unauthorized(_('Authorization required'))
|
||||
try:
|
||||
auth_type, token = auth_header.strip().split(maxsplit=1)
|
||||
except (ValueError, AttributeError) as exc:
|
||||
LOG.info('Could not parse Authorization header: %s', exc)
|
||||
raise exception.BadRequest(_('Could not parse Authorization header'))
|
||||
|
||||
if auth_type.lower() != 'basic':
|
||||
msg = _('Unsupported authorization type "%s"') % auth_type
|
||||
LOG.info(msg)
|
||||
raise exception.BadRequest(msg)
|
||||
return token
|
||||
|
||||
|
||||
def unauthorized(message=None):
|
||||
"""Raise an Unauthorized exception to prompt for basic authentication
|
||||
|
||||
:param: message: Optional message for esception
|
||||
:raises: Unauthorized with WWW-Authenticate header set
|
||||
"""
|
||||
if not message:
|
||||
message = _('Incorrect username or password')
|
||||
raise exception.Unauthorized(message)
|
@ -15,17 +15,117 @@
|
||||
# under the License.
|
||||
|
||||
"""Ironic specific exceptions list."""
|
||||
|
||||
import collections
|
||||
from http import client as http_client
|
||||
import json
|
||||
|
||||
from ironic_lib.exception import IronicException
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
|
||||
from ironic.common.i18n import _
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def _ensure_exception_kwargs_serializable(exc_class_name, kwargs):
|
||||
"""Ensure that kwargs are serializable
|
||||
|
||||
Ensure that all kwargs passed to exception constructor can be passed over
|
||||
RPC, by trying to convert them to JSON, or, as a last resort, to string.
|
||||
If it is not possible, unserializable kwargs will be removed, letting the
|
||||
receiver handle the exception string as it is configured to.
|
||||
|
||||
:param exc_class_name: a IronicException class name.
|
||||
:param kwargs: a dictionary of keyword arguments passed to the exception
|
||||
constructor.
|
||||
:returns: a dictionary of serializable keyword arguments.
|
||||
"""
|
||||
serializers = [(json.dumps, _('when converting to JSON')),
|
||||
(str, _('when converting to string'))]
|
||||
exceptions = collections.defaultdict(list)
|
||||
serializable_kwargs = {}
|
||||
for k, v in kwargs.items():
|
||||
for serializer, msg in serializers:
|
||||
try:
|
||||
serializable_kwargs[k] = serializer(v)
|
||||
exceptions.pop(k, None)
|
||||
break
|
||||
except Exception as e:
|
||||
exceptions[k].append(
|
||||
'(%(serializer_type)s) %(e_type)s: %(e_contents)s' %
|
||||
{'serializer_type': msg, 'e_contents': e,
|
||||
'e_type': e.__class__.__name__})
|
||||
if exceptions:
|
||||
LOG.error("One or more arguments passed to the %(exc_class)s "
|
||||
"constructor as kwargs can not be serialized. The "
|
||||
"serialized arguments: %(serialized)s. These "
|
||||
"unserialized kwargs were dropped because of the "
|
||||
"exceptions encountered during their "
|
||||
"serialization:\n%(errors)s",
|
||||
dict(errors=';\n'.join("%s: %s" % (k, '; '.join(v))
|
||||
for k, v in exceptions.items()),
|
||||
exc_class=exc_class_name,
|
||||
serialized=serializable_kwargs))
|
||||
# We might be able to actually put the following keys' values into
|
||||
# format string, but there is no guarantee, drop it just in case.
|
||||
for k in exceptions:
|
||||
del kwargs[k]
|
||||
return serializable_kwargs
|
||||
|
||||
|
||||
class IronicException(Exception):
|
||||
"""Base Ironic Exception
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a '_msg_fmt' property. That _msg_fmt will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
|
||||
If you need to access the message from an exception you should use
|
||||
str(exc)
|
||||
|
||||
"""
|
||||
|
||||
_msg_fmt = _("An unknown exception occurred.")
|
||||
code = 500
|
||||
headers = {}
|
||||
safe = False
|
||||
|
||||
def __init__(self, message=None, **kwargs):
|
||||
self.kwargs = _ensure_exception_kwargs_serializable(
|
||||
self.__class__.__name__, kwargs)
|
||||
|
||||
if 'code' not in self.kwargs:
|
||||
try:
|
||||
self.kwargs['code'] = self.code
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.code = int(kwargs['code'])
|
||||
|
||||
if not message:
|
||||
try:
|
||||
message = self._msg_fmt % kwargs
|
||||
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception() as ctxt:
|
||||
# kwargs doesn't match a variable in the message
|
||||
# log the issue and the kwargs
|
||||
prs = ', '.join('%s=%s' % pair for pair in kwargs.items())
|
||||
LOG.exception('Exception in string format operation '
|
||||
'(arguments %s)', prs)
|
||||
if not CONF.errors.fatal_exception_format_errors:
|
||||
# at least get the core message out if something
|
||||
# happened
|
||||
message = self._msg_fmt
|
||||
ctxt.reraise = False
|
||||
|
||||
super(IronicException, self).__init__(message)
|
||||
|
||||
|
||||
class NotAuthorized(IronicException):
|
||||
_msg_fmt = _("Not authorized.")
|
||||
code = http_client.FORBIDDEN
|
||||
@ -861,8 +961,7 @@ class ImageRefIsARedirect(IronicException):
|
||||
|
||||
def __init__(self, image_ref=None, redirect_url=None, msg=None):
|
||||
self.redirect_url = redirect_url
|
||||
# Kwargs are expected by ironic_lib's IronicException to convert
|
||||
# the message.
|
||||
# Kwargs are expected by IronicException to convert the message.
|
||||
super(ImageRefIsARedirect, self).__init__(
|
||||
message=msg,
|
||||
image_ref=image_ref,
|
||||
@ -945,3 +1044,21 @@ class ChildNodeLocked(Conflict):
|
||||
"and we are unable to perform any action on it at this "
|
||||
"time. Please retry after the current operation is "
|
||||
"completed.")
|
||||
|
||||
|
||||
class MetricsNotSupported(IronicException):
|
||||
_msg_fmt = _("Metrics action is not supported. You may need to "
|
||||
"adjust the [metrics] section in ironic.conf.")
|
||||
|
||||
|
||||
class ServiceLookupFailure(IronicException):
|
||||
_msg_fmt = _("Cannot find %(service)s service through multicast.")
|
||||
|
||||
|
||||
class ServiceRegistrationFailure(IronicException):
|
||||
_msg_fmt = _("Cannot register %(service)s service: %(error)s")
|
||||
|
||||
|
||||
class Unauthorized(IronicException):
|
||||
code = http_client.UNAUTHORIZED
|
||||
headers = {'WWW-Authenticate': 'Basic realm="Baremetal API"'}
|
||||
|
@ -15,7 +15,6 @@ import os.path
|
||||
import shutil
|
||||
from urllib import parse as urlparse
|
||||
|
||||
from ironic_lib import utils as ironic_utils
|
||||
from oslo_log import log
|
||||
|
||||
from ironic.common import exception
|
||||
@ -114,7 +113,7 @@ class LocalPublisher(AbstractPublisher):
|
||||
def unpublish(self, file_name):
|
||||
published_file = os.path.join(
|
||||
CONF.deploy.http_root, self.image_subdir, file_name)
|
||||
ironic_utils.unlink_without_raise(published_file)
|
||||
utils.unlink_without_raise(published_file)
|
||||
|
||||
|
||||
class SwiftPublisher(AbstractPublisher):
|
||||
|
@ -101,7 +101,7 @@ def create_vfat_image(output_file, files_info=None, parameters=None,
|
||||
mounting, creating filesystem, copying files, etc.
|
||||
"""
|
||||
try:
|
||||
# TODO(sbaker): use ironic_lib.utils.dd when rootwrap has been removed
|
||||
# TODO(sbaker): use utils.dd when rootwrap has been removed
|
||||
utils.execute('dd', 'if=/dev/zero', 'of=%s' % output_file, 'count=1',
|
||||
'bs=%dKiB' % fs_size_kib)
|
||||
except processutils.ProcessExecutionError as e:
|
||||
@ -113,8 +113,7 @@ def create_vfat_image(output_file, files_info=None, parameters=None,
|
||||
# The label helps ramdisks to find the partition containing
|
||||
# the parameters (by using /dev/disk/by-label/ir-vfd-dev).
|
||||
# NOTE: FAT filesystem label can be up to 11 characters long.
|
||||
# TODO(sbaker): use ironic_lib.utils.mkfs when rootwrap has been
|
||||
# removed
|
||||
# TODO(sbaker): use utils.mkfs when rootwrap has been removed
|
||||
utils.execute('mkfs', '-t', 'vfat', '-n',
|
||||
'ir-vfd-dev', output_file)
|
||||
except processutils.ProcessExecutionError as e:
|
||||
|
0
ironic/common/json_rpc/__init__.py
Normal file
0
ironic/common/json_rpc/__init__.py
Normal file
241
ironic/common/json_rpc/client.py
Normal file
241
ironic/common/json_rpc/client.py
Normal file
@ -0,0 +1,241 @@
|
||||
# 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.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from oslo_config import cfg
|
||||
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 keystone
|
||||
from ironic.conf import json_rpc
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.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."""
|
||||
|
||||
allowed_exception_namespaces = [
|
||||
"ironic.common.exception.",
|
||||
"ironic_inspector.utils.",
|
||||
]
|
||||
|
||||
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):
|
||||
"""Prepare the client to transmit a request.
|
||||
|
||||
:param topic: Topic which is being addressed. Typically this
|
||||
is the hostname of the remote json-rpc service.
|
||||
:param version: The RPC API version to utilize.
|
||||
"""
|
||||
|
||||
host = topic.split('.', 1)[1]
|
||||
host, port = netutils.parse_host_port(host)
|
||||
return _CallContext(
|
||||
host, self.serializer, version=version,
|
||||
version_cap=self.version_cap,
|
||||
allowed_exception_namespaces=self.allowed_exception_namespaces,
|
||||
port=port)
|
||||
|
||||
|
||||
class _CallContext(object):
|
||||
"""Wrapper object for compatibility with oslo.messaging API."""
|
||||
|
||||
def __init__(self, host, serializer, version=None, version_cap=None,
|
||||
allowed_exception_namespaces=(), port=None):
|
||||
if not port:
|
||||
self.port = CONF.json_rpc.port
|
||||
else:
|
||||
self.port = int(port)
|
||||
self.host = host
|
||||
self.serializer = serializer
|
||||
self.version = version
|
||||
self.version_cap = version_cap
|
||||
self.allowed_exception_namespaces = allowed_exception_namespaces
|
||||
|
||||
def _is_known_exception(self, class_name):
|
||||
for ns in self.allowed_exception_namespaces:
|
||||
if class_name.startswith(ns):
|
||||
return True
|
||||
return False
|
||||
|
||||
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 self._is_known_exception(cls):
|
||||
# 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'] = (getattr(context, 'request_id', None)
|
||||
or uuidutils.generate_uuid())
|
||||
|
||||
scheme = 'http'
|
||||
if CONF.json_rpc.use_ssl:
|
||||
scheme = 'https'
|
||||
url = '%s://%s:%d' % (scheme,
|
||||
netutils.escape_ipv6(self.host),
|
||||
self.port)
|
||||
LOG.debug("RPC %s to %s with %s", method, url,
|
||||
strutils.mask_dict_password(body))
|
||||
try:
|
||||
result = _get_session().post(url, json=body)
|
||||
except Exception as exc:
|
||||
LOG.debug('RPC %s to %s failed with %s', method, url, exc)
|
||||
raise
|
||||
LOG.debug('RPC %s to %s returned %s', method, url,
|
||||
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})
|
281
ironic/common/json_rpc/server.py
Normal file
281
ironic/common/json_rpc/server.py
Normal file
@ -0,0 +1,281 @@
|
||||
# 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
|
||||
import logging
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
from oslo_config import cfg
|
||||
import oslo_messaging
|
||||
from oslo_utils import strutils
|
||||
import webob
|
||||
|
||||
from ironic.common import auth_basic
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common.json_rpc import wsgi
|
||||
from ironic.conf import json_rpc
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.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 EmptyContext:
|
||||
|
||||
request_id = None
|
||||
|
||||
def __init__(self, src):
|
||||
self.__dict__.update(src)
|
||||
|
||||
def to_dict(self):
|
||||
return self.__dict__.copy()
|
||||
|
||||
|
||||
class WSGIService(wsgi.WSGIService):
|
||||
"""Provides ability to launch JSON RPC as a WSGI application."""
|
||||
|
||||
def __init__(self, manager, serializer, context_class=EmptyContext):
|
||||
"""Create a JSON RPC service.
|
||||
|
||||
:param manager: Object from which to expose methods.
|
||||
:param serializer: A serializer that supports calls serialize_entity
|
||||
and deserialize_entity.
|
||||
:param context_class: A context class - a callable accepting a dict
|
||||
received from network.
|
||||
"""
|
||||
self.manager = manager
|
||||
self.serializer = serializer
|
||||
self.context_class = context_class
|
||||
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
|
||||
super().__init__('ironic-json-rpc', app, CONF.json_rpc)
|
||||
|
||||
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(',')
|
||||
allowed_roles = CONF.json_rpc.allowed_roles
|
||||
if set(roles).isdisjoint(allowed_roles):
|
||||
LOG.debug('Roles %s do not contain any of %s, rejecting '
|
||||
'request', roles, allowed_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 = self.context_class(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
|
77
ironic/common/json_rpc/wsgi.py
Normal file
77
ironic/common/json_rpc/wsgi.py
Normal file
@ -0,0 +1,77 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import socket
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_service import service
|
||||
from oslo_service import wsgi
|
||||
|
||||
from ironic.common import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class WSGIService(service.ServiceBase):
|
||||
|
||||
def __init__(self, name, app, conf):
|
||||
"""Initialize, but do not start the WSGI server.
|
||||
|
||||
:param name: The name of the WSGI server given |