ironic-inspector/ironic_inspector/utils.py
Anton Arefiev 3fe42b53fd SQLAlchemy 2.0 Support
Primarily remove the workaround added in
Ia6d512ff2ae417bab938cb095fbb0884d195010a which added
continued use of autocommit, which is incompatible with
SQLAlchemy 2.0.

Also set the environment for unit tests to report compatability
warnings, although it appears none are being reported at this time.

Also cuts out the db upgrade cruft to only use the online database
migration code through oslo_db's enginefacade, which has the smarts
to handle online or offline migrations.

And then, retools unit/functional test data storage to utlize sqlite,
and in that re-tooled the queries to prevent locking conditions
which could exist with queries, and some additional refactoring/cleanup.

Also, don't mock and test time.sleep().

Additionally, it looks like we have discovered the root cause of the
memory/connection leakage issue which has been observed, due to the
way lists of nodes are processed/returned.

This change was based upon the work in
I506da42a9891a245831f325e34bec92e0a3f33f0 which is included in
this commit as the entire database structure and interaction
has been modified for ironic-inspector.

Co-Authored-By: aarefiev <aarefiev@mirantis.com>
Story: 2009727
Task: 44132
Change-Id: Ic88eb9dec5fddc924a72d9a23c17a304954ebf46
2022-12-15 09:28:55 -08:00

343 lines
10 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import logging as pylog
import futurist
from ironic_lib import auth_basic
from ironic_lib import exception
from keystonemiddleware import auth_token
from openstack.baremetal.v1 import node
from oslo_config import cfg
from oslo_log import log
from oslo_middleware import cors as cors_middleware
from oslo_middleware import healthcheck as healthcheck_middleware
import pytz
import webob
from ironic_inspector.common.i18n import _
from ironic_inspector import policy
CONF = cfg.CONF
_EXECUTOR = None
def get_ipmi_address_from_data(introspection_data):
try:
result = introspection_data['inventory']['bmc_address']
except KeyError:
result = introspection_data.get('ipmi_address')
if result in ('', '0.0.0.0'):
# ipmitool can return these values, if it does not know the address
return None
else:
return result
def get_ipmi_v6address_from_data(introspection_data):
try:
result = introspection_data['inventory']['bmc_v6address']
except KeyError:
result = introspection_data.get('ipmi_v6address')
if result in ('', '::/0'):
# ipmitool can return these values, if it does not know the address
return None
else:
return result
def get_pxe_mac(introspection_data):
pxe_mac = introspection_data.get('boot_interface')
if pxe_mac and '-' in pxe_mac:
# pxelinux format: 01-aa-bb-cc-dd-ee-ff
pxe_mac = pxe_mac.split('-', 1)[1]
pxe_mac = pxe_mac.replace('-', ':').lower()
return pxe_mac
def processing_logger_prefix(data=None, node_info=None):
"""Calculate prefix for logging.
Tries to use:
* node UUID, node._state
* node PXE MAC,
* node BMC address
:param data: introspection data
:param node_info: NodeInfo or ironic node object
:return: logging prefix as a string
"""
# TODO(dtantsur): try to get MAC and BMC address for node_info as well
parts = []
data = data or {}
if node_info is not None:
if isinstance(node_info, node.Node):
parts.append(str(node_info.id))
else:
parts.append(str(node_info))
pxe_mac = get_pxe_mac(data)
if pxe_mac:
parts.append('MAC %s' % pxe_mac)
bmc_address = get_ipmi_address_from_data(data) if data else None
if bmc_address:
parts.append('BMC %s' % bmc_address)
if parts:
return _('[node: %s]') % ' '.join(parts)
else:
return _('[unidentified node]')
class ProcessingLoggerAdapter(log.KeywordArgumentAdapter):
def process(self, msg, kwargs):
if 'data' not in kwargs and 'node_info' not in kwargs:
return super(ProcessingLoggerAdapter, self).process(msg, kwargs)
data = kwargs.get('data', {})
node_info = kwargs.get('node_info')
prefix = processing_logger_prefix(data, node_info)
msg, kwargs = super(ProcessingLoggerAdapter, self).process(msg, kwargs)
return ('%s %s' % (prefix, msg)), kwargs
def getProcessingLogger(name):
# We can't use getLogger from oslo_log, as it's an adapter itself
logger = pylog.getLogger(name)
return ProcessingLoggerAdapter(logger, {})
LOG = getProcessingLogger(__name__)
class Error(Exception):
"""Inspector exception."""
def __init__(self, msg, code=400, log_level='error', **kwargs):
super(Error, self).__init__(msg)
getattr(LOG, log_level)(msg, **kwargs)
self.http_code = code
class NotFoundInCacheError(Error):
"""Exception when node was not found in cache during processing."""
def __init__(self, msg, code=404, **kwargs):
super(NotFoundInCacheError, self).__init__(msg, code,
log_level='info', **kwargs)
class NodeStateRaceCondition(Error):
"""State mismatch between the DB and a node_info."""
def __init__(self, *args, **kwargs):
message = _('Node state mismatch detected between the DB and the '
'cached node_info object')
kwargs.setdefault('code', 500)
super(NodeStateRaceCondition, self).__init__(message, *args, **kwargs)
class NodeStateInvalidEvent(Error):
"""Invalid event attempted."""
class IntrospectionDataStoreDisabled(Error):
"""Introspection data store is disabled."""
class IntrospectionDataNotFound(NotFoundInCacheError):
"""Introspection data not found."""
class NoAvailableConductor(Error):
"""No available conductor in the service group."""
def __init__(self, msg, **kwargs):
super(NoAvailableConductor, self).__init__(msg, code=503, **kwargs)
class DeferredBasicAuthMiddleware(object):
"""Middleware which sets X-Identity-Status header based on authentication
"""
def __init__(self, app, auth_file):
self.app = app
self.auth_file = auth_file
auth_basic.validate_auth_file(auth_file)
@webob.dec.wsgify()
def __call__(self, req):
headers = req.headers
try:
if 'Authorization' not in headers:
auth_basic.unauthorized()
token = auth_basic.parse_header({
'HTTP_AUTHORIZATION': headers.get('Authorization')
})
username, password = auth_basic.parse_token(token)
headers.update(
auth_basic.authenticate(self.auth_file, username, password))
headers['X-Identity-Status'] = 'Confirmed'
except exception.Unauthorized:
headers['X-Identity-Status'] = 'Invalid'
except exception.IronicException as e:
status = '%s %s' % (int(e.code), str(e))
resp = webob.Response(status=status)
resp.headers.update(e.headers)
return resp
return req.get_response(self.app)
class NodeNotFoundInDBError(Error):
"""The node was not found in the database."""
# NOTE(TheJulia): This exception exists largely to help facilitate
# Internal error handling.
def __init__(self, **kwargs):
msg = 'The requested node was not found.'
super(NodeNotFoundInDBError, self).__init__(
msg, code=404, log_level='error', **kwargs)
class RuleUUIDExistError(Error):
"""Rule requested already exists in the database."""
def __init__(self, uuid, *args, **kwargs):
message = _('Rule with UUID %s already exists') % uuid
kwargs.setdefault('code', 409)
super(RuleUUIDExistError, self).__init__(message, *args, **kwargs)
class RuleNotFoundError(Error):
"""The requested rule was not found."""
def __init__(self, uuid, *args, **kwargs):
message = _('Rule %s was not found') % uuid
kwargs.setdefault('code', 404)
super(RuleNotFoundError, self).__init__(message, *args, **kwargs)
def executor():
"""Return the current futures executor."""
global _EXECUTOR
if _EXECUTOR is None:
_EXECUTOR = futurist.GreenThreadPoolExecutor(
max_workers=CONF.max_concurrency)
return _EXECUTOR
def add_auth_middleware(app):
"""Add authentication middleware to Flask application.
:param app: application.
"""
auth_conf = dict(CONF.keystone_authtoken)
auth_conf['delay_auth_decision'] = True
app.wsgi_app = auth_token.AuthProtocol(app.wsgi_app, auth_conf)
def add_basic_auth_middleware(app):
"""Add HTTP Basic authentication middleware to Flask application.
:param app: application.
"""
app.wsgi_app = DeferredBasicAuthMiddleware(
app.wsgi_app, CONF.http_basic_auth_user_file)
def add_cors_middleware(app):
"""Create a CORS wrapper
Attach ironic-inspector-specific defaults that must be included
in all CORS responses.
:param app: application
"""
app.wsgi_app = cors_middleware.CORS(app.wsgi_app, CONF)
def add_healthcheck_middleware(app):
"""Add healthcheck middleware
:param app: application
"""
app.wsgi_app = healthcheck_middleware.Healthcheck(app.wsgi_app, CONF)
def check_auth(request, rule=None, target=None):
"""Check authentication on request.
:param request: Flask request
:param rule: policy rule to check the request against
:param target: dict-like structure to check rule against
:raises: utils.Error if access is denied
"""
if CONF.auth_strategy not in ('keystone', 'http_basic'):
return
if not request.context.is_public_api:
if request.headers.get('X-Identity-Status', '').lower() == 'invalid':
raise Error(_('Authentication required'), code=401)
if CONF.auth_strategy != 'keystone':
return
target = {} if target is None else target
if not policy.authorize(rule, target, request.context.to_policy_values()):
raise Error(_("Access denied by policy"), code=403)
def get_valid_macs(data):
"""Get a list of valid MAC's from the introspection data."""
return [m['mac']
for m in data.get('all_interfaces', {}).values()
if m.get('mac')]
def get_inventory(data, node_info=None):
"""Get and validate the hardware inventory from introspection data."""
inventory = data.get('inventory')
# TODO(dtantsur): validate inventory using JSON schema
if not inventory:
raise Error(_('Hardware inventory is empty or missing'),
data=data, node_info=node_info)
if not inventory.get('interfaces'):
raise Error(_('No network interfaces provided in the inventory'),
data=data, node_info=node_info)
if not inventory.get('disks'):
LOG.info('No disks were detected in the inventory, assuming this '
'is a disk-less node', data=data, node_info=node_info)
# Make sure the code iterating over it does not fail with a TypeError
inventory['disks'] = []
return inventory
def iso_timestamp(timestamp=None, tz=pytz.timezone('utc')):
"""Return an ISO8601-formatted timestamp (tz: UTC) or None.
:param timestamp: such as time.time() or None
:param tz: timezone
:returns: an ISO8601-formatted timestamp, or None
"""
if timestamp is None:
return None
date = datetime.datetime.fromtimestamp(timestamp, tz=tz)
return date.isoformat()