1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2010 OpenStack LLC.
6
# Licensed under the Apache License, Version 2.0 (the "License"); you may
7
# not use this file except in compliance with the License. You may obtain
8
# a copy of the License at
10
# http://www.apache.org/licenses/LICENSE-2.0
12
# Unless required by applicable law or agreed to in writing, software
13
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
# License for the specific language governing permissions and limitations
24
from xml.dom import minidom
26
from nova.api.openstack import wsgi
27
from nova.api.openstack import xmlutil
28
from nova.compute import task_states
29
from nova.compute import utils as compute_utils
30
from nova.compute import vm_states
31
from nova import exception
32
from nova import flags
33
from nova.openstack.common import log as logging
34
from nova import quota
37
LOG = logging.getLogger(__name__)
42
XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
48
task_states.REBOOTING: 'REBOOT',
49
task_states.REBOOTING_HARD: 'HARD_REBOOT',
50
task_states.UPDATING_PASSWORD: 'PASSWORD',
51
task_states.REBUILDING: 'REBUILD',
52
task_states.REBUILD_BLOCK_DEVICE_MAPPING: 'REBUILD',
53
task_states.REBUILD_SPAWNING: 'REBUILD',
54
task_states.MIGRATING: 'MIGRATING',
55
task_states.RESIZE_PREP: 'RESIZE',
56
task_states.RESIZE_MIGRATING: 'RESIZE',
57
task_states.RESIZE_MIGRATED: 'RESIZE',
58
task_states.RESIZE_FINISH: 'RESIZE',
67
'default': 'VERIFY_RESIZE',
68
# Note(maoy): the OS API spec 1.1 doesn't have CONFIRMING_RESIZE
69
# state so we comment that out for future reference only.
70
#task_states.RESIZE_CONFIRMING: 'CONFIRMING_RESIZE',
71
task_states.RESIZE_REVERTING: 'REVERT_RESIZE',
76
vm_states.SUSPENDED: {
77
'default': 'SUSPENDED',
88
vm_states.SOFT_DELETED: {
94
def status_from_state(vm_state, task_state='default'):
95
"""Given vm_state and task_state, return a status string."""
96
task_map = _STATE_MAP.get(vm_state, dict(default='UNKNOWN'))
97
status = task_map.get(task_state, task_map['default'])
98
if status == "UNKNOWN":
99
LOG.error(_("status is UNKNOWN from vm_state=%(vm_state)s "
100
"task_state=%(task_state)s. Bad upgrade or db "
101
"corrupted?") % locals())
105
def vm_state_from_status(status):
106
"""Map the server status string to a vm state."""
107
for state, task_map in _STATE_MAP.iteritems():
108
status_string = task_map.get("default")
109
if status.lower() == status_string.lower():
113
def get_pagination_params(request):
114
"""Return marker, limit tuple from request.
116
:param request: `wsgi.Request` possibly containing 'marker' and 'limit'
117
GET variables. 'marker' is the id of the last element
118
the client has seen, and 'limit' is the maximum number
119
of items to return. If 'limit' is not specified, 0, or
120
> max_limit, we default to max_limit. Negative values
121
for either marker or limit will cause
122
exc.HTTPBadRequest() exceptions to be raised.
126
if 'limit' in request.GET:
127
params['limit'] = _get_limit_param(request)
128
if 'marker' in request.GET:
129
params['marker'] = _get_marker_param(request)
133
def _get_limit_param(request):
134
"""Extract integer limit from request or fail"""
136
limit = int(request.GET['limit'])
138
msg = _('limit param must be an integer')
139
raise webob.exc.HTTPBadRequest(explanation=msg)
141
msg = _('limit param must be positive')
142
raise webob.exc.HTTPBadRequest(explanation=msg)
146
def _get_marker_param(request):
147
"""Extract marker id from request or fail"""
148
return request.GET['marker']
151
def limited(items, request, max_limit=FLAGS.osapi_max_limit):
152
"""Return a slice of items according to requested offset and limit.
154
:param items: A sliceable entity
155
:param request: ``wsgi.Request`` possibly containing 'offset' and 'limit'
156
GET variables. 'offset' is where to start in the list,
157
and 'limit' is the maximum number of items to return. If
158
'limit' is not specified, 0, or > max_limit, we default
159
to max_limit. Negative values for either offset or limit
160
will cause exc.HTTPBadRequest() exceptions to be raised.
161
:kwarg max_limit: The maximum number of items to return from 'items'
164
offset = int(request.GET.get('offset', 0))
166
msg = _('offset param must be an integer')
167
raise webob.exc.HTTPBadRequest(explanation=msg)
170
limit = int(request.GET.get('limit', max_limit))
172
msg = _('limit param must be an integer')
173
raise webob.exc.HTTPBadRequest(explanation=msg)
176
msg = _('limit param must be positive')
177
raise webob.exc.HTTPBadRequest(explanation=msg)
180
msg = _('offset param must be positive')
181
raise webob.exc.HTTPBadRequest(explanation=msg)
183
limit = min(max_limit, limit or max_limit)
184
range_end = offset + limit
185
return items[offset:range_end]
188
def get_limit_and_marker(request, max_limit=FLAGS.osapi_max_limit):
189
"""get limited parameter from request"""
190
params = get_pagination_params(request)
191
limit = params.get('limit', max_limit)
192
limit = min(max_limit, limit)
193
marker = params.get('marker')
198
def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit):
199
"""Return a slice of items according to the requested marker and limit."""
200
limit, marker = get_limit_and_marker(request, max_limit)
202
limit = min(max_limit, limit)
206
for i, item in enumerate(items):
207
if 'flavorid' in item:
208
if item['flavorid'] == marker:
211
elif item['id'] == marker or item.get('uuid') == marker:
215
msg = _('marker [%s] not found') % marker
216
raise webob.exc.HTTPBadRequest(explanation=msg)
217
range_end = start_index + limit
218
return items[start_index:range_end]
221
def get_id_from_href(href):
222
"""Return the id or uuid portion of a url.
224
Given: 'http://www.foo.com/bar/123?q=4'
227
Given: 'http://www.foo.com/bar/abc123?q=4'
231
return urlparse.urlsplit("%s" % href).path.split('/')[-1]
234
def remove_version_from_href(href):
235
"""Removes the first api version from the href.
237
Given: 'http://www.nova.com/v1.1/123'
238
Returns: 'http://www.nova.com/123'
240
Given: 'http://www.nova.com/v1.1'
241
Returns: 'http://www.nova.com'
244
parsed_url = urlparse.urlsplit(href)
245
url_parts = parsed_url.path.split('/', 2)
247
# NOTE: this should match vX.X or vX
248
expression = re.compile(r'^v([0-9]+|[0-9]+\.[0-9]+)(/.*|$)')
249
if expression.match(url_parts[1]):
252
new_path = '/'.join(url_parts)
254
if new_path == parsed_url.path:
255
msg = _('href %s does not contain version') % href
257
raise ValueError(msg)
259
parsed_url = list(parsed_url)
260
parsed_url[2] = new_path
261
return urlparse.urlunsplit(parsed_url)
264
def check_img_metadata_properties_quota(context, metadata):
268
QUOTAS.limit_check(context, metadata_items=len(metadata))
269
except exception.OverQuota:
270
expl = _("Image metadata limit exceeded")
271
raise webob.exc.HTTPRequestEntityTooLarge(explanation=expl,
272
headers={'Retry-After': 0})
274
# check the key length.
275
if isinstance(metadata, dict):
276
for key, value in metadata.iteritems():
278
expl = _("Image metadata key cannot be blank")
279
raise webob.exc.HTTPBadRequest(explanation=expl)
281
expl = _("Image metadata key too long")
282
raise webob.exc.HTTPBadRequest(explanation=expl)
284
expl = _("Invalid image metadata")
285
raise webob.exc.HTTPBadRequest(explanation=expl)
288
def dict_to_query_str(params):
289
# TODO(throughnothing): we should just use urllib.urlencode instead of this
290
# But currently we don't work with urlencoded url's
292
for key, val in params.iteritems():
293
param_str = param_str + '='.join([str(key), str(val)]) + '&'
295
return param_str.rstrip('&')
298
def get_networks_for_instance_from_nw_info(nw_info):
301
ips = vif.fixed_ips()
302
floaters = vif.floating_ips()
303
label = vif['network']['label']
304
if label not in networks:
305
networks[label] = {'ips': [], 'floating_ips': []}
307
networks[label]['ips'].extend(ips)
308
networks[label]['floating_ips'].extend(floaters)
312
def get_networks_for_instance(context, instance):
313
"""Returns a prepared nw_info list for passing into the view builders
315
We end up with a data structure like::
317
{'public': {'ips': [{'addr': '10.0.0.1', 'version': 4},
318
{'addr': '2001::1', 'version': 6}],
319
'floating_ips': [{'addr': '172.16.0.1', 'version': 4},
320
{'addr': '172.16.2.1', 'version': 4}]},
323
nw_info = compute_utils.get_nw_info_for_instance(instance)
324
return get_networks_for_instance_from_nw_info(nw_info)
327
def raise_http_conflict_for_instance_invalid_state(exc, action):
328
"""Return a webob.exc.HTTPConflict instance containing a message
329
appropriate to return via the API based on the original
330
InstanceInvalidState exception.
332
attr = exc.kwargs.get('attr')
333
state = exc.kwargs.get('state')
335
msg = _("Cannot '%(action)s' while instance is in %(attr)s %(state)s")
337
# At least give some meaningful message
338
msg = _("Instance is in an invalid state for '%(action)s'")
339
raise webob.exc.HTTPConflict(explanation=msg % locals())
342
class MetadataDeserializer(wsgi.MetadataXMLDeserializer):
343
def deserialize(self, text):
344
dom = minidom.parseString(text)
345
metadata_node = self.find_first_child_named(dom, "metadata")
346
metadata = self.extract_metadata(metadata_node)
347
return {'body': {'metadata': metadata}}
350
class MetaItemDeserializer(wsgi.MetadataXMLDeserializer):
351
def deserialize(self, text):
352
dom = minidom.parseString(text)
353
metadata_item = self.extract_metadata(dom)
354
return {'body': {'meta': metadata_item}}
357
class MetadataXMLDeserializer(wsgi.XMLDeserializer):
359
def extract_metadata(self, metadata_node):
360
"""Marshal the metadata attribute of a parsed request"""
361
if metadata_node is None:
364
for meta_node in self.find_children_named(metadata_node, "meta"):
365
key = meta_node.getAttribute("key")
366
metadata[key] = self.extract_text(meta_node)
369
def _extract_metadata_container(self, datastring):
370
dom = minidom.parseString(datastring)
371
metadata_node = self.find_first_child_named(dom, "metadata")
372
metadata = self.extract_metadata(metadata_node)
373
return {'body': {'metadata': metadata}}
375
def create(self, datastring):
376
return self._extract_metadata_container(datastring)
378
def update_all(self, datastring):
379
return self._extract_metadata_container(datastring)
381
def update(self, datastring):
382
dom = minidom.parseString(datastring)
383
metadata_item = self.extract_metadata(dom)
384
return {'body': {'meta': metadata_item}}
387
metadata_nsmap = {None: xmlutil.XMLNS_V11}
390
class MetaItemTemplate(xmlutil.TemplateBuilder):
392
sel = xmlutil.Selector('meta', xmlutil.get_items, 0)
393
root = xmlutil.TemplateElement('meta', selector=sel)
396
return xmlutil.MasterTemplate(root, 1, nsmap=metadata_nsmap)
399
class MetadataTemplateElement(xmlutil.TemplateElement):
400
def will_render(self, datum):
404
class MetadataTemplate(xmlutil.TemplateBuilder):
406
root = MetadataTemplateElement('metadata', selector='metadata')
407
elem = xmlutil.SubTemplateElement(root, 'meta',
408
selector=xmlutil.get_items)
411
return xmlutil.MasterTemplate(root, 1, nsmap=metadata_nsmap)
414
def check_snapshots_enabled(f):
416
def inner(*args, **kwargs):
417
if not FLAGS.allow_instance_snapshots:
418
LOG.warn(_('Rejecting snapshot request, snapshots currently'
420
msg = _("Instance snapshots are not permitted at this time.")
421
raise webob.exc.HTTPBadRequest(explanation=msg)
422
return f(*args, **kwargs)
426
class ViewBuilder(object):
427
"""Model API responses as dictionaries."""
429
def _get_links(self, request, identifier, collection_name):
432
"href": self._get_href_link(request, identifier, collection_name),
436
"href": self._get_bookmark_link(request,
441
def _get_next_link(self, request, identifier, collection_name):
442
"""Return href string with proper limit and marker params."""
443
params = request.params.copy()
444
params["marker"] = identifier
445
prefix = self._update_link_prefix(request.application_url,
446
FLAGS.osapi_compute_link_prefix)
447
url = os.path.join(prefix,
448
request.environ["nova.context"].project_id,
450
return "%s?%s" % (url, dict_to_query_str(params))
452
def _get_href_link(self, request, identifier, collection_name):
453
"""Return an href string pointing to this object."""
454
prefix = self._update_link_prefix(request.application_url,
455
FLAGS.osapi_compute_link_prefix)
456
return os.path.join(prefix,
457
request.environ["nova.context"].project_id,
461
def _get_bookmark_link(self, request, identifier, collection_name):
462
"""Create a URL that refers to a specific resource."""
463
base_url = remove_version_from_href(request.application_url)
464
base_url = self._update_link_prefix(base_url,
465
FLAGS.osapi_compute_link_prefix)
466
return os.path.join(base_url,
467
request.environ["nova.context"].project_id,
471
def _get_collection_links(self,
476
"""Retrieve 'next' link, if applicable."""
478
limit = int(request.params.get("limit", 0))
479
if limit and limit == len(items):
480
last_item = items[-1]
481
if id_key in last_item:
482
last_item_id = last_item[id_key]
483
elif 'id' in last_item:
484
last_item_id = last_item["id"]
486
last_item_id = last_item["flavorid"]
489
"href": self._get_next_link(request,
495
def _update_link_prefix(self, orig_url, prefix):
498
url_parts = list(urlparse.urlsplit(orig_url))
499
prefix_parts = list(urlparse.urlsplit(prefix))
500
url_parts[0:2] = prefix_parts[0:2]
501
return urlparse.urlunsplit(url_parts)