Source code for ironic_python_agent.api.app

# Copyright 2013 Rackspace, Inc.
#
# 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 json

from oslo_log import log
from oslo_service import wsgi
import werkzeug
from werkzeug import exceptions as http_exc
from werkzeug import routing

from ironic_python_agent import encoding
from ironic_python_agent.metrics_lib import metrics_utils


LOG = log.getLogger(__name__)
_CUSTOM_MEDIA_TYPE = 'application/vnd.openstack.ironic-python-agent.v1+json'
_DOCS_URL = 'https://docs.openstack.org/ironic-python-agent'


[docs] class Request(werkzeug.Request): """Custom request class with JSON support."""
[docs] def jsonify(value, status=200): """Convert value to a JSON response using the custom encoder.""" encoder = encoding.RESTJSONEncoder() data = encoder.encode(value) return werkzeug.Response(data, status=status, mimetype='application/json')
[docs] def version(url): return { 'id': 'v1', 'links': [ make_link(url, 'self', 'v1', bookmark=True), make_link(url, 'describedby', bookmark=True), ], }
# Emulate WSME format
[docs] def format_exception(value): code = getattr(value, 'status_code', None) or getattr(value, 'code', 500) return { 'faultcode': 'Server' if code >= 500 else 'Client', 'faultstring': str(value), }
[docs] class Application(object): def __init__(self, agent, conf): """Set up the API app. :param agent: an :class:`ironic_python_agent.agent.IronicPythonAgent` instance. :param conf: configuration object. """ self.agent = agent self.service = None self._conf = conf self.url_map = routing.Map([ routing.Rule('/', endpoint='root', methods=['GET']), routing.Rule('/v1/', endpoint='v1', methods=['GET']), routing.Rule('/v1/status', endpoint='status', methods=['GET']), routing.Rule('/v1/commands/', endpoint='list_commands', methods=['GET']), routing.Rule('/v1/commands/<cmd>', endpoint='get_command', methods=['GET']), routing.Rule('/v1/commands/', endpoint='run_command', methods=['POST']), # Use the default version (i.e. v1) when the version is missing routing.Rule('/status', endpoint='status', methods=['GET']), routing.Rule('/commands/', endpoint='list_commands', methods=['GET']), routing.Rule('/commands/<cmd>', endpoint='get_command', methods=['GET']), routing.Rule('/commands/', endpoint='run_command', methods=['POST']), ]) def __call__(self, environ, start_response): """WSGI entry point.""" try: request = Request(environ) adapter = self.url_map.bind_to_environ(request.environ) endpoint, values = adapter.match() response = getattr(self, "api_" + endpoint)(request, **values) except Exception as exc: response = self.handle_exception(environ, exc) return response(environ, start_response)
[docs] def start(self, tls_cert_file=None, tls_key_file=None): """Start the API service in the background.""" if tls_cert_file and tls_key_file: self._conf.set_override('cert_file', tls_cert_file, group='ssl') self._conf.set_override('key_file', tls_key_file, group='ssl') use_tls = True else: use_tls = self._conf.listen_tls self.service = wsgi.Server(self._conf, 'ironic-python-agent', app=self, host=self.agent.listen_address.hostname, port=self.agent.listen_address.port, use_ssl=use_tls) self.service.start() LOG.info('Started API service on port %s', self.agent.listen_address.port)
[docs] def stop(self): """Stop the API service.""" LOG.debug("Stopping the API service.") if self.service is None: return self.service.stop() self.service = None LOG.info('Stopped API service on port %s', self.agent.listen_address.port)
[docs] def handle_exception(self, environ, exc): """Handle an exception during request processing.""" if isinstance(exc, http_exc.HTTPException): if exc.code and exc.code < 400: return exc # redirect resp = exc.get_response(environ) resp.data = json.dumps(format_exception(exc)) resp.content_type = 'application/json' return resp else: formatted = format_exception(exc) if formatted['faultcode'] == 'Server': LOG.exception('Internal server error: %s', exc) return jsonify(formatted, status=getattr(exc, 'status_code', 500))
[docs] def api_root(self, request): url = request.url_root.rstrip('/') return jsonify({ 'name': 'OpenStack Ironic Python Agent API', 'description': ('Ironic Python Agent is a provisioning agent for ' 'OpenStack Ironic'), 'versions': [version(url)], 'default_version': version(url), })
[docs] def api_v1(self, request): url = request.url_root.rstrip('/') return jsonify(dict({ 'commands': [ make_link(url, 'self', 'commands'), make_link(url, 'bookmark', 'commands'), ], 'status': [ make_link(url, 'self', 'status'), make_link(url, 'bookmark', 'status'), ], 'media_types': [ {'base': 'application/json', 'type': _CUSTOM_MEDIA_TYPE}, ], }, **version(url)))
[docs] def api_status(self, request): with metrics_utils.get_metrics_logger(__name__).timer('get_status'): status = self.agent.get_status() return jsonify(status)
[docs] def api_list_commands(self, request): with metrics_utils.get_metrics_logger(__name__).timer('list_commands'): results = self.agent.list_command_results() return jsonify({'commands': results})
[docs] def api_get_command(self, request, cmd): with metrics_utils.get_metrics_logger(__name__).timer('get_command'): result = self.agent.get_command_result(cmd) wait = request.args.get('wait') if wait and wait.lower() == 'true': result.join() return jsonify(result)
[docs] def api_run_command(self, request): body = request.get_json(force=True) if ('name' not in body or 'params' not in body or not isinstance(body['params'], dict)): raise http_exc.BadRequest('Missing or invalid name or params') token = request.args.get('agent_token', None) if not self.agent.validate_agent_token(token): raise http_exc.Unauthorized( 'Token invalid.') with metrics_utils.get_metrics_logger(__name__).timer('run_command'): result = self.agent.execute_command(body['name'], **body['params']) wait = request.args.get('wait') if wait and wait.lower() == 'true': result.join() return jsonify(result)