ironic-inspector/ironic_inspector/main.py
Kaifeng Wang 9c6c7c0a57 Follow up to introspection data driver api revision
In https://review.openstack.org/#/c/637673, the get introspection
interface is narrowed down to only accept node uuid, which previously
accepts both uuid and name. But the name to uuid conversion is
missing in the reapply api, which causes feature regression, and
this is the fix :(

Story: 1726713
Task: 11373

Change-Id: I6912853deab77f1365f665ca1e52c13063d2cdf1
2019-02-26 16:26:45 +08:00

372 lines
12 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 os
import re
import flask
from oslo_utils import strutils
from oslo_utils import uuidutils
import six
import werkzeug
from ironic_inspector import api_tools
from ironic_inspector.common import context
from ironic_inspector.common.i18n import _
from ironic_inspector.common import ironic as ir_utils
from ironic_inspector.common import rpc
import ironic_inspector.conf
from ironic_inspector.conf import opts as conf_opts
from ironic_inspector import node_cache
from ironic_inspector import process
from ironic_inspector import rules
from ironic_inspector import utils
CONF = ironic_inspector.conf.CONF
app = flask.Flask(__name__)
LOG = utils.getProcessingLogger(__name__)
MINIMUM_API_VERSION = (1, 0)
CURRENT_API_VERSION = (1, 14)
DEFAULT_API_VERSION = CURRENT_API_VERSION
_LOGGING_EXCLUDED_KEYS = ('logs',)
def _get_version():
ver = flask.request.headers.get(conf_opts.VERSION_HEADER,
_DEFAULT_API_VERSION)
try:
if ver.lower() == 'latest':
requested = CURRENT_API_VERSION
else:
requested = tuple(int(x) for x in ver.split('.'))
except (ValueError, TypeError):
return error_response(_('Malformed API version: expected string '
'in form of X.Y or latest'), code=400)
return requested
def _format_version(ver):
return '%d.%d' % ver
_DEFAULT_API_VERSION = _format_version(DEFAULT_API_VERSION)
def error_response(exc, code=500):
res = flask.jsonify(error={'message': str(exc)})
res.status_code = code
LOG.debug('Returning error to client: %s', exc)
return res
def convert_exceptions(func):
@six.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except utils.Error as exc:
return error_response(exc, exc.http_code)
except werkzeug.exceptions.HTTPException as exc:
return error_response(exc, exc.code or 400)
except Exception as exc:
LOG.exception('Internal server error')
msg = _('Internal server error')
if CONF.debug:
msg += ' (%s): %s' % (exc.__class__.__name__, exc)
return error_response(msg)
return wrapper
@app.before_request
def check_api_version():
requested = _get_version()
if requested < MINIMUM_API_VERSION or requested > CURRENT_API_VERSION:
return error_response(_('Unsupported API version %(requested)s, '
'supported range is %(min)s to %(max)s') %
{'requested': _format_version(requested),
'min': _format_version(MINIMUM_API_VERSION),
'max': _format_version(CURRENT_API_VERSION)},
code=406)
@app.after_request
def add_version_headers(res):
res.headers[conf_opts.MIN_VERSION_HEADER] = '%s.%s' % MINIMUM_API_VERSION
res.headers[conf_opts.MAX_VERSION_HEADER] = '%s.%s' % CURRENT_API_VERSION
return res
def create_link_object(urls):
links = []
for url in urls:
links.append({"rel": "self",
"href": os.path.join(flask.request.url_root, url)})
return links
def generate_resource_data(resources):
data = []
for resource in resources:
item = {}
item['name'] = str(resource).split('/')[-1]
item['links'] = create_link_object([str(resource)[1:]])
data.append(item)
return data
def generate_introspection_status(node):
"""Return a dict representing current node status.
:param node: a NodeInfo instance
:return: dictionary
"""
started_at = node.started_at.isoformat()
finished_at = node.finished_at.isoformat() if node.finished_at else None
status = {}
status['uuid'] = node.uuid
status['finished'] = bool(node.finished_at)
status['state'] = node.state
status['started_at'] = started_at
status['finished_at'] = finished_at
status['error'] = node.error
status['links'] = create_link_object(
["v%s/introspection/%s" % (CURRENT_API_VERSION[0], node.uuid)])
return status
def api(path, is_public_api=False, rule=None, verb_to_rule_map=None,
**flask_kwargs):
"""Decorator to wrap api methods.
Performs flask routing, exception conversion,
generation of oslo context for request and API access policy enforcement.
:param path: flask app route path
:param is_public_api: whether this API path should be treated
as public, with minimal access enforcement
:param rule: API access policy rule to enforce.
If rule is None, the 'default' policy rule will be enforced,
which is "deny all" if not overridden in policy confif file.
:param verb_to_rule_map: if both rule and this are given,
defines mapping between http verbs (uppercase)
and strings to format the 'rule' string with
:param kwargs: all the rest kwargs are passed to flask app.route
"""
def outer(func):
@app.route(path, **flask_kwargs)
@convert_exceptions
@six.wraps(func)
def wrapper(*args, **kwargs):
flask.request.context = context.RequestContext.from_environ(
flask.request.environ,
is_public_api=is_public_api)
if verb_to_rule_map and rule:
policy_rule = rule.format(
verb_to_rule_map[flask.request.method.upper()])
else:
policy_rule = rule
utils.check_auth(flask.request, rule=policy_rule)
return func(*args, **kwargs)
return wrapper
return outer
@api('/', rule='introspection', is_public_api=True, methods=['GET'])
def api_root():
versions = [
{
"status": "CURRENT",
"id": '%s.%s' % CURRENT_API_VERSION,
},
]
for version in versions:
version['links'] = create_link_object(
["v%s" % version['id'].split('.')[0]])
return flask.jsonify(versions=versions)
@api('/<version>', rule='introspection:version', is_public_api=True,
methods=['GET'])
def version_root(version):
pat = re.compile(r'^\/%s\/[^\/]*?$' % version)
resources = []
for url in app.url_map.iter_rules():
if pat.match(str(url)):
resources.append(url)
if not resources:
raise utils.Error(_('Version not found.'), code=404)
return flask.jsonify(resources=generate_resource_data(resources))
@api('/v1/continue', rule="introspection:continue", is_public_api=True,
methods=['POST'])
def api_continue():
data = flask.request.get_json(force=True)
if not isinstance(data, dict):
raise utils.Error(_('Invalid data: expected a JSON object, got %s') %
data.__class__.__name__)
logged_data = {k: (v if k not in _LOGGING_EXCLUDED_KEYS else '<hidden>')
for k, v in data.items()}
LOG.debug("Received data from the ramdisk: %s", logged_data,
data=data)
return flask.jsonify(process.process(data))
# TODO(sambetts) Add API discovery for this endpoint
@api('/v1/introspection/<node_id>',
rule="introspection:{}",
verb_to_rule_map={'GET': 'status', 'POST': 'start'},
methods=['GET', 'POST'])
def api_introspection(node_id):
if flask.request.method == 'POST':
args = flask.request.args
manage_boot = args.get('manage_boot', 'True')
try:
manage_boot = strutils.bool_from_string(manage_boot, strict=True)
except ValueError:
raise utils.Error(_('Invalid boolean value for manage_boot: %s') %
manage_boot, code=400)
if manage_boot and not CONF.can_manage_boot:
raise utils.Error(_('Managed boot is requested, but this '
'installation cannot manage boot ('
'(can_manage_boot set to False)'),
code=400)
client = rpc.get_client()
client.call({}, 'do_introspection', node_id=node_id,
manage_boot=manage_boot,
token=flask.request.headers.get('X-Auth-Token'))
return '', 202
else:
node_info = node_cache.get_node(node_id)
return flask.json.jsonify(generate_introspection_status(node_info))
@api('/v1/introspection', rule='introspection:status', methods=['GET'])
def api_introspection_statuses():
nodes = node_cache.get_node_list(
marker=api_tools.marker_field(),
limit=api_tools.limit_field(default=CONF.api_max_limit)
)
data = {
'introspection': [generate_introspection_status(node)
for node in nodes]
}
return flask.json.jsonify(data)
@api('/v1/introspection/<node_id>/abort', rule="introspection:abort",
methods=['POST'])
def api_introspection_abort(node_id):
client = rpc.get_client()
client.call({}, 'do_abort', node_id=node_id,
token=flask.request.headers.get('X-Auth-Token'))
return '', 202
@api('/v1/introspection/<node_id>/data', rule="introspection:data",
methods=['GET'])
def api_introspection_data(node_id):
try:
if not uuidutils.is_uuid_like(node_id):
node = ir_utils.get_node(node_id, fields=['uuid'])
node_id = node.uuid
res = process.get_introspection_data(node_id)
return res, 200, {'Content-Type': 'application/json'}
except utils.IntrospectionDataStoreDisabled:
return error_response(_('Inspector is not configured to store data. '
'Set the [processing]store_data '
'configuration option to change this.'),
code=404)
@api('/v1/introspection/<node_id>/data/unprocessed',
rule="introspection:reapply", methods=['POST'])
def api_introspection_reapply(node_id):
if flask.request.content_length:
return error_response(_('User data processing is not '
'supported yet'), code=400)
if not uuidutils.is_uuid_like(node_id):
node = ir_utils.get_node(node_id, fields=['uuid'])
node_id = node.uuid
client = rpc.get_client()
client.call({}, 'do_reapply', node_uuid=node_id)
return '', 202
def rule_repr(rule, short):
result = rule.as_dict(short=short)
result['links'] = [{
'href': flask.url_for('api_rule', uuid=result['uuid']),
'rel': 'self'
}]
return result
@api('/v1/rules',
rule="introspection:rule:{}",
verb_to_rule_map={'GET': 'get', 'POST': 'create', 'DELETE': 'delete'},
methods=['GET', 'POST', 'DELETE'])
def api_rules():
if flask.request.method == 'GET':
res = [rule_repr(rule, short=True) for rule in rules.get_all()]
return flask.jsonify(rules=res)
elif flask.request.method == 'DELETE':
rules.delete_all()
return '', 204
else:
body = flask.request.get_json(force=True)
if body.get('uuid') and not uuidutils.is_uuid_like(body['uuid']):
raise utils.Error(_('Invalid UUID value'), code=400)
rule = rules.create(conditions_json=body.get('conditions', []),
actions_json=body.get('actions', []),
uuid=body.get('uuid'),
description=body.get('description'))
response_code = (200 if _get_version() < (1, 6) else 201)
return flask.make_response(
flask.jsonify(rule_repr(rule, short=False)), response_code)
@api('/v1/rules/<uuid>',
rule="introspection:rule:{}",
verb_to_rule_map={'GET': 'get', 'DELETE': 'delete'},
methods=['GET', 'DELETE'])
def api_rule(uuid):
if flask.request.method == 'GET':
rule = rules.get(uuid)
return flask.jsonify(rule_repr(rule, short=False))
else:
rules.delete(uuid)
return '', 204
@app.errorhandler(404)
def handle_404(error):
return error_response(error, code=404)