1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
|
# 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'
class Request(werkzeug.Request):
"""Custom request class with JSON support."""
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')
def make_link(url, rel_name, resource='', resource_args='',
bookmark=False, type_=None):
if rel_name == 'describedby':
url = _DOCS_URL
type_ = 'text/html'
elif rel_name == 'bookmark':
bookmark = True
template = ('%(root)s/%(resource)s' if bookmark
else '%(root)s/v1/%(resource)s')
template += ('%(args)s'
if resource_args.startswith('?') or not resource_args
else '/%(args)s')
result = {'href': template % {'root': url,
'resource': resource,
'args': resource_args},
'rel': rel_name}
if type_:
result['type'] = type_
return result
def version(url):
return {
'id': 'v1',
'links': [
make_link(url, 'self', 'v1', bookmark=True),
make_link(url, 'describedby', bookmark=True),
],
}
# Emulate WSME format
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),
}
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']),
])
self.security_get_token_support = False
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)
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)
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)
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))
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),
})
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)))
def api_status(self, request):
with metrics_utils.get_metrics_logger(__name__).timer('get_status'):
status = self.agent.get_status()
return jsonify(status)
def require_agent_token_for_command(func):
def wrapper(self, request, *args, **kwargs):
token = request.args.get('agent_token', None)
if token:
# TODO(TheJulia): At some point down the road, remove the
# self.security_get_token_support flag and use the same
# decorator for the api_run_command endpoint.
self.security_get_token_support = True
if (self.security_get_token_support
and not self.agent.validate_agent_token(token)):
raise http_exc.Unauthorized('Token invalid.')
return func(self, request, *args, **kwargs)
return wrapper
@require_agent_token_for_command
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})
@require_agent_token_for_command
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)
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)
|