Coverage for nova/api/openstack/common.py: 99%
215 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright 2010 OpenStack Foundation
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16import collections
17import itertools
18import re
19from urllib import parse as urlparse
21from oslo_log import log as logging
22from oslo_utils import strutils
23import webob
24from webob import exc
26from nova.api.openstack import api_version_request
27from nova.compute import task_states
28from nova.compute import vm_states
29import nova.conf
30from nova import exception
31from nova.i18n import _
32from nova import objects
33from nova import quota
34from nova import utils
36CONF = nova.conf.CONF
38LOG = logging.getLogger(__name__)
39QUOTAS = quota.QUOTAS
42POWER_ON = 'POWER_ON'
43POWER_OFF = 'POWER_OFF'
45_STATE_MAP = {
46 vm_states.ACTIVE: {
47 'default': 'ACTIVE',
48 task_states.REBOOTING: 'REBOOT',
49 task_states.REBOOT_PENDING: 'REBOOT',
50 task_states.REBOOT_STARTED: 'REBOOT',
51 task_states.REBOOTING_HARD: 'HARD_REBOOT',
52 task_states.REBOOT_PENDING_HARD: 'HARD_REBOOT',
53 task_states.REBOOT_STARTED_HARD: 'HARD_REBOOT',
54 task_states.UPDATING_PASSWORD: 'PASSWORD',
55 task_states.REBUILDING: 'REBUILD',
56 task_states.REBUILD_BLOCK_DEVICE_MAPPING: 'REBUILD',
57 task_states.REBUILD_SPAWNING: 'REBUILD',
58 task_states.MIGRATING: 'MIGRATING',
59 task_states.RESIZE_PREP: 'RESIZE',
60 task_states.RESIZE_MIGRATING: 'RESIZE',
61 task_states.RESIZE_MIGRATED: 'RESIZE',
62 task_states.RESIZE_FINISH: 'RESIZE',
63 },
64 vm_states.BUILDING: {
65 'default': 'BUILD',
66 },
67 vm_states.STOPPED: {
68 'default': 'SHUTOFF',
69 task_states.RESIZE_PREP: 'RESIZE',
70 task_states.RESIZE_MIGRATING: 'RESIZE',
71 task_states.RESIZE_MIGRATED: 'RESIZE',
72 task_states.RESIZE_FINISH: 'RESIZE',
73 task_states.REBUILDING: 'REBUILD',
74 task_states.REBUILD_BLOCK_DEVICE_MAPPING: 'REBUILD',
75 task_states.REBUILD_SPAWNING: 'REBUILD',
76 },
77 vm_states.RESIZED: {
78 'default': 'VERIFY_RESIZE',
79 # Note(maoy): the OS API spec 1.1 doesn't have CONFIRMING_RESIZE
80 # state so we comment that out for future reference only.
81 # task_states.RESIZE_CONFIRMING: 'CONFIRMING_RESIZE',
82 task_states.RESIZE_REVERTING: 'REVERT_RESIZE',
83 },
84 vm_states.PAUSED: {
85 'default': 'PAUSED',
86 task_states.MIGRATING: 'MIGRATING',
87 },
88 vm_states.SUSPENDED: {
89 'default': 'SUSPENDED',
90 },
91 vm_states.RESCUED: {
92 'default': 'RESCUE',
93 },
94 vm_states.ERROR: {
95 'default': 'ERROR',
96 task_states.REBUILDING: 'REBUILD',
97 task_states.REBUILD_BLOCK_DEVICE_MAPPING: 'REBUILD',
98 task_states.REBUILD_SPAWNING: 'REBUILD',
99 },
100 vm_states.DELETED: {
101 'default': 'DELETED',
102 },
103 vm_states.SOFT_DELETED: {
104 'default': 'SOFT_DELETED',
105 },
106 vm_states.SHELVED: {
107 'default': 'SHELVED',
108 },
109 vm_states.SHELVED_OFFLOADED: {
110 'default': 'SHELVED_OFFLOADED',
111 },
112}
115def status_from_state(vm_state, task_state='default'):
116 """Given vm_state and task_state, return a status string."""
117 task_map = _STATE_MAP.get(vm_state, dict(default='UNKNOWN'))
118 status = task_map.get(task_state, task_map['default'])
119 if status == "UNKNOWN":
120 LOG.error("status is UNKNOWN from vm_state=%(vm_state)s "
121 "task_state=%(task_state)s. Bad upgrade or db "
122 "corrupted?",
123 {'vm_state': vm_state, 'task_state': task_state})
124 return status
127def task_and_vm_state_from_status(statuses):
128 """Map the server's multiple status strings to list of vm states and
129 list of task states.
130 """
131 vm_states = set()
132 task_states = set()
133 lower_statuses = [status.lower() for status in statuses]
134 for state, task_map in _STATE_MAP.items():
135 for task_state, mapped_state in task_map.items():
136 status_string = mapped_state
137 if status_string.lower() in lower_statuses:
138 vm_states.add(state)
139 task_states.add(task_state)
140 # Add sort to avoid different order on set in Python 3
141 return sorted(vm_states), sorted(task_states)
144def get_sort_params(input_params, default_key='created_at',
145 default_dir='desc'):
146 """Retrieves sort keys/directions parameters.
148 Processes the parameters to create a list of sort keys and sort directions
149 that correspond to the 'sort_key' and 'sort_dir' parameter values. These
150 sorting parameters can be specified multiple times in order to generate
151 the list of sort keys and directions.
153 The input parameters are not modified.
155 :param input_params: webob.multidict of request parameters (from
156 nova.api.wsgi.Request.params)
157 :param default_key: default sort key value, added to the list if no
158 'sort_key' parameters are supplied
159 :param default_dir: default sort dir value, added to the list if no
160 'sort_dir' parameters are supplied
161 :returns: list of sort keys, list of sort dirs
162 """
163 params = input_params.copy()
164 sort_keys = []
165 sort_dirs = []
166 while 'sort_key' in params:
167 sort_keys.append(params.pop('sort_key').strip())
168 while 'sort_dir' in params:
169 sort_dirs.append(params.pop('sort_dir').strip())
170 if len(sort_keys) == 0 and default_key:
171 sort_keys.append(default_key)
172 if len(sort_dirs) == 0 and default_dir:
173 sort_dirs.append(default_dir)
174 return sort_keys, sort_dirs
177def get_pagination_params(request):
178 """Return marker, limit tuple from request.
180 :param request: `wsgi.Request` possibly containing 'marker' and 'limit'
181 GET variables. 'marker' is the id of the last element
182 the client has seen, and 'limit' is the maximum number
183 of items to return. If 'limit' is not specified, 0, or
184 > max_limit, we default to max_limit. Negative values
185 for either marker or limit will cause
186 exc.HTTPBadRequest() exceptions to be raised.
188 """
189 params = {}
190 if 'limit' in request.GET:
191 params['limit'] = _get_int_param(request, 'limit')
192 if 'page_size' in request.GET:
193 params['page_size'] = _get_int_param(request, 'page_size')
194 if 'marker' in request.GET:
195 params['marker'] = _get_marker_param(request)
196 if 'offset' in request.GET:
197 params['offset'] = _get_int_param(request, 'offset')
198 return params
201def _get_int_param(request, param):
202 """Extract integer param from request or fail."""
203 try:
204 int_param = utils.validate_integer(request.GET[param], param,
205 min_value=0)
206 except exception.InvalidInput as e:
207 raise webob.exc.HTTPBadRequest(explanation=e.format_message())
208 return int_param
211def _get_marker_param(request):
212 """Extract marker id from request or fail."""
213 return request.GET['marker']
216def limited(items, request):
217 """Return a slice of items according to requested offset and limit.
219 :param items: A sliceable entity
220 :param request: ``wsgi.Request`` possibly containing 'offset' and 'limit'
221 GET variables. 'offset' is where to start in the list,
222 and 'limit' is the maximum number of items to return. If
223 'limit' is not specified, 0, or > max_limit, we default
224 to max_limit. Negative values for either offset or limit
225 will cause exc.HTTPBadRequest() exceptions to be raised.
226 """
227 params = get_pagination_params(request)
228 offset = params.get('offset', 0)
229 limit = CONF.api.max_limit
230 limit = min(limit, params.get('limit') or limit)
232 return items[offset:(offset + limit)]
235def get_limit_and_marker(request):
236 """Get limited parameter from request."""
237 params = get_pagination_params(request)
238 limit = CONF.api.max_limit
239 limit = min(limit, params.get('limit', limit))
240 marker = params.get('marker', None)
242 return limit, marker
245def get_id_from_href(href):
246 """Return the id or uuid portion of a url.
248 Given: 'http://www.foo.com/bar/123?q=4'
249 Returns: '123'
251 Given: 'http://www.foo.com/bar/abc123?q=4'
252 Returns: 'abc123'
254 """
255 return urlparse.urlsplit("%s" % href).path.split('/')[-1]
258def remove_trailing_version_from_href(href):
259 """Removes the api version from the href.
261 Given: 'http://www.nova.com/compute/v1.1'
262 Returns: 'http://www.nova.com/compute'
264 Given: 'http://www.nova.com/v1.1'
265 Returns: 'http://www.nova.com'
267 """
268 parsed_url = urlparse.urlsplit(href)
269 url_parts = parsed_url.path.rsplit('/', 1)
271 # NOTE: this should match vX.X or vX
272 expression = re.compile(r'^v([0-9]+|[0-9]+\.[0-9]+)(/.*|$)')
273 if not expression.match(url_parts.pop()):
274 LOG.debug('href %s does not contain version', href)
275 raise ValueError(_('href %s does not contain version') % href)
277 new_path = url_join(*url_parts)
278 parsed_url = list(parsed_url)
279 parsed_url[2] = new_path
280 return urlparse.urlunsplit(parsed_url)
283def check_img_metadata_properties_quota(context, metadata):
284 if not metadata:
285 return
286 try:
287 QUOTAS.limit_check(context, metadata_items=len(metadata))
288 except exception.OverQuota:
289 expl = _("Image metadata limit exceeded")
290 raise webob.exc.HTTPForbidden(explanation=expl)
293def get_networks_for_instance_from_nw_info(nw_info):
294 networks = collections.OrderedDict()
295 for vif in nw_info:
296 ips = vif.fixed_ips()
297 floaters = vif.floating_ips()
298 label = vif['network']['label']
299 if label not in networks:
300 networks[label] = {'ips': [], 'floating_ips': []}
301 for ip in itertools.chain(ips, floaters):
302 ip['mac_address'] = vif['address']
303 networks[label]['ips'].extend(ips)
304 networks[label]['floating_ips'].extend(floaters)
305 return networks
308def get_networks_for_instance(context, instance):
309 """Returns a prepared nw_info list for passing into the view builders
311 We end up with a data structure like::
313 {'public': {'ips': [{'address': '10.0.0.1',
314 'version': 4,
315 'mac_address': 'aa:aa:aa:aa:aa:aa'},
316 {'address': '2001::1',
317 'version': 6,
318 'mac_address': 'aa:aa:aa:aa:aa:aa'}],
319 'floating_ips': [{'address': '172.16.0.1',
320 'version': 4,
321 'mac_address': 'aa:aa:aa:aa:aa:aa'},
322 {'address': '172.16.2.1',
323 'version': 4,
324 'mac_address': 'aa:aa:aa:aa:aa:aa'}]},
325 ...}
326 """
327 nw_info = instance.get_network_info()
328 return get_networks_for_instance_from_nw_info(nw_info)
331def raise_http_conflict_for_instance_invalid_state(exc, action, server_id):
332 """Raises a webob.exc.HTTPConflict instance containing a message
333 appropriate to return via the API based on the original
334 InstanceInvalidState exception.
335 """
336 attr = exc.kwargs.get('attr')
337 state = exc.kwargs.get('state')
338 if attr is not None and state is not None:
339 msg = _("Cannot '%(action)s' instance %(server_id)s while it is in "
340 "%(attr)s %(state)s") % {'action': action, 'attr': attr,
341 'state': state,
342 'server_id': server_id}
343 else:
344 # At least give some meaningful message
345 msg = _("Instance %(server_id)s is in an invalid state for "
346 "'%(action)s'") % {'action': action, 'server_id': server_id}
347 raise webob.exc.HTTPConflict(explanation=msg)
350def url_join(*parts):
351 """Convenience method for joining parts of a URL
353 Any leading and trailing '/' characters are removed, and the parts joined
354 together with '/' as a separator. If last element of 'parts' is an empty
355 string, the returned URL will have a trailing slash.
356 """
357 parts = parts or [""]
358 clean_parts = [part.strip("/") for part in parts if part]
359 if not parts[-1]:
360 # Empty last element should add a trailing slash
361 clean_parts.append("")
362 return "/".join(clean_parts)
365class ViewBuilder(object):
366 """Model API responses as dictionaries."""
368 def _get_project_id(self, request):
369 """Get project id from request url if present or empty string
370 otherwise
371 """
372 project_id = request.environ["nova.context"].project_id
373 if project_id and project_id in request.url:
374 return project_id
375 return ''
377 def _get_links(self, request, identifier, collection_name):
378 return [{
379 "rel": "self",
380 "href": self._get_href_link(request, identifier, collection_name),
381 },
382 {
383 "rel": "bookmark",
384 "href": self._get_bookmark_link(request,
385 identifier,
386 collection_name),
387 }]
389 def _get_next_link(self, request, identifier, collection_name):
390 """Return href string with proper limit and marker params."""
391 params = collections.OrderedDict(sorted(request.params.items()))
392 params["marker"] = identifier
393 prefix = self._update_compute_link_prefix(request.application_url)
394 url = url_join(prefix,
395 self._get_project_id(request),
396 collection_name)
397 return "%s?%s" % (url, urlparse.urlencode(params))
399 def _get_href_link(self, request, identifier, collection_name):
400 """Return an href string pointing to this object."""
401 prefix = self._update_compute_link_prefix(request.application_url)
402 return url_join(prefix,
403 self._get_project_id(request),
404 collection_name,
405 str(identifier))
407 def _get_bookmark_link(self, request, identifier, collection_name):
408 """Create a URL that refers to a specific resource."""
409 base_url = remove_trailing_version_from_href(request.application_url)
410 base_url = self._update_compute_link_prefix(base_url)
411 return url_join(base_url,
412 self._get_project_id(request),
413 collection_name,
414 str(identifier))
416 def _get_collection_links(self,
417 request,
418 items,
419 collection_name,
420 id_key="uuid"):
421 """Retrieve 'next' link, if applicable. This is included if:
422 1) 'limit' param is specified and equals the number of items.
423 2) 'limit' param is specified but it exceeds CONF.api.max_limit,
424 in this case the number of items is CONF.api.max_limit.
425 3) 'limit' param is NOT specified but the number of items is
426 CONF.api.max_limit.
427 """
428 links = []
429 max_items = min(
430 int(request.params.get("limit", CONF.api.max_limit)),
431 CONF.api.max_limit)
432 if max_items and max_items == len(items):
433 last_item = items[-1]
434 if id_key in last_item:
435 last_item_id = last_item[id_key]
436 elif 'id' in last_item: 436 ↛ 439line 436 didn't jump to line 439 because the condition on line 436 was always true
437 last_item_id = last_item["id"]
438 else:
439 last_item_id = last_item["flavorid"]
440 links.append({
441 "rel": "next",
442 "href": self._get_next_link(request,
443 last_item_id,
444 collection_name),
445 })
446 return links
448 def _update_link_prefix(self, orig_url, prefix):
449 if not prefix:
450 return orig_url
451 url_parts = list(urlparse.urlsplit(orig_url))
452 prefix_parts = list(urlparse.urlsplit(prefix))
453 url_parts[0:2] = prefix_parts[0:2]
454 url_parts[2] = prefix_parts[2] + url_parts[2]
455 return urlparse.urlunsplit(url_parts).rstrip('/')
457 def _update_glance_link_prefix(self, orig_url):
458 return self._update_link_prefix(orig_url, CONF.api.glance_link_prefix)
460 def _update_compute_link_prefix(self, orig_url):
461 return self._update_link_prefix(orig_url, CONF.api.compute_link_prefix)
464def get_instance(compute_api, context, instance_id, expected_attrs=None,
465 cell_down_support=False):
466 """Fetch an instance from the compute API, handling error checking."""
467 try:
468 return compute_api.get(context, instance_id,
469 expected_attrs=expected_attrs,
470 cell_down_support=cell_down_support)
471 except exception.InstanceNotFound as e:
472 raise exc.HTTPNotFound(explanation=e.format_message())
475def normalize_name(name):
476 # NOTE(alex_xu): This method is used by v2.1 legacy v2 compat mode.
477 # In the legacy v2 API, some of APIs strip the spaces and some of APIs not.
478 # The v2.1 disallow leading/trailing, for compatible v2 API and consistent,
479 # we enable leading/trailing spaces and strip spaces in legacy v2 compat
480 # mode. Althrough in legacy v2 API there are some APIs didn't strip spaces,
481 # but actually leading/trailing spaces(that means user depend on leading/
482 # trailing spaces distinguish different instance) is pointless usecase.
483 return name.strip()
486def raise_feature_not_supported(msg=None):
487 if msg is None:
488 msg = _("The requested functionality is not supported.")
489 raise webob.exc.HTTPNotImplemented(explanation=msg)
492def get_flavor(context, flavor_id):
493 try:
494 return objects.Flavor.get_by_flavor_id(context, flavor_id)
495 except exception.FlavorNotFound as error:
496 raise exc.HTTPNotFound(explanation=error.format_message())
499def is_all_tenants(search_opts):
500 """Checks to see if the all_tenants flag is in search_opts
502 :param dict search_opts: The search options for a request
503 :returns: boolean indicating if all_tenants are being requested or not
504 """
505 all_tenants = search_opts.get('all_tenants')
506 if all_tenants:
507 try:
508 all_tenants = strutils.bool_from_string(all_tenants, True)
509 except ValueError as err:
510 raise exception.InvalidInput(str(err))
511 else:
512 # The empty string is considered enabling all_tenants
513 all_tenants = 'all_tenants' in search_opts
514 return all_tenants
517def is_locked(search_opts):
518 """Converts the value of the locked parameter to a boolean. Note that
519 this function will be called only if locked exists in search_opts.
521 :param dict search_opts: The search options for a request
522 :returns: boolean indicating if locked is being requested or not
523 """
524 locked = search_opts.get('locked')
525 try:
526 locked = strutils.bool_from_string(locked, strict=True)
527 except ValueError as err:
528 raise exception.InvalidInput(str(err))
529 return locked
532def supports_multiattach_volume(req):
533 """Check to see if the requested API version is high enough for multiattach
535 Microversion 2.60 adds support for booting from a multiattach volume.
536 The actual validation for a multiattach volume is done in the compute
537 API code, this is just checking the version so we can tell the API
538 code if the request version is high enough to even support it.
540 :param req: The incoming API request
541 :returns: True if the requested API microversion is high enough for
542 volume multiattach support, False otherwise.
543 """
544 return api_version_request.is_supported(req, '2.60')
547def supports_port_resource_request(req):
548 """Check to see if the requested API version is high enough for resource
549 request
551 :param req: The incoming API request
552 :returns: True if the requested API microversion is high enough for
553 port resource request support, False otherwise.
554 """
555 return api_version_request.is_supported(req, '2.72')