c06a9a7f24
The i18n team has decided not to translate the logs because it seems like it not very useful. This are the files as of now do not have merge conflicts. Change-Id: I082f6302f120a8c0c81482b73be301e83fe3a3a8 Partial-Bug: #1674374
227 lines
6.9 KiB
Python
227 lines
6.9 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 ironicclient.v1 import node
|
|
from keystonemiddleware import auth_token
|
|
from oslo_config import cfg
|
|
from oslo_log import log
|
|
from oslo_middleware import cors as cors_middleware
|
|
import pytz
|
|
|
|
from ironic_inspector.common.i18n import _
|
|
from ironic_inspector import conf # noqa
|
|
|
|
CONF = cfg.CONF
|
|
|
|
_EXECUTOR = None
|
|
|
|
|
|
def get_ipmi_address_from_data(introspection_data):
|
|
try:
|
|
return introspection_data['inventory']['bmc_address']
|
|
except KeyError:
|
|
return introspection_data.get('ipmi_address')
|
|
|
|
|
|
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.uuid))
|
|
else:
|
|
parts.append(str(node_info))
|
|
|
|
pxe_mac = get_pxe_mac(data)
|
|
if pxe_mac:
|
|
parts.append('MAC %s' % pxe_mac)
|
|
|
|
if CONF.processing.log_bmc_address:
|
|
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."""
|
|
|
|
|
|
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_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 check_auth(request):
|
|
"""Check authentication on request.
|
|
|
|
:param request: Flask request
|
|
:raises: utils.Error if access is denied
|
|
"""
|
|
if CONF.auth_strategy == 'noauth':
|
|
return
|
|
if request.headers.get('X-Identity-Status').lower() == 'invalid':
|
|
raise Error(_('Authentication required'), code=401)
|
|
roles = (request.headers.get('X-Roles') or '').split(',')
|
|
if 'admin' not in roles:
|
|
LOG.error('Role "admin" not in user role list %s', roles)
|
|
raise Error(_('Access denied'), 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')]
|
|
|
|
|
|
_INVENTORY_MANDATORY_KEYS = ('memory', 'cpu', 'interfaces')
|
|
|
|
|
|
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)
|
|
|
|
for key in _INVENTORY_MANDATORY_KEYS:
|
|
if not inventory.get(key):
|
|
raise Error(_('Invalid hardware inventory: %s key is missing '
|
|
'or empty') % key, 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()
|