Coverage for nova/api/openstack/compute/servers.py: 97%
795 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
1# Copyright 2010 OpenStack Foundation
2# Copyright 2011 Piston Cloud Computing, Inc
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
17import copy
19from oslo_log import log as logging
20import oslo_messaging as messaging
21from oslo_utils import strutils
22from oslo_utils import timeutils
23from oslo_utils import uuidutils
24import webob
25from webob import exc
27from nova.api.openstack import api_version_request
28from nova.api.openstack import common
29from nova.api.openstack.compute import helpers
30from nova.api.openstack.compute.schemas import servers as schema
31from nova.api.openstack.compute.views import servers as views_servers
32from nova.api.openstack import wsgi
33from nova.api import validation
34from nova import block_device
35from nova.compute import api as compute
36from nova.compute import flavors
37from nova.compute import utils as compute_utils
38import nova.conf
39from nova import context as nova_context
40from nova import exception
41from nova.i18n import _
42from nova.image import glance
43from nova import objects
44from nova.policies import servers as server_policies
45from nova import utils
47TAG_SEARCH_FILTERS = ('tags', 'tags-any', 'not-tags', 'not-tags-any')
48PAGING_SORTING_PARAMS = ('sort_key', 'sort_dir', 'limit', 'marker')
50CONF = nova.conf.CONF
52LOG = logging.getLogger(__name__)
54INVALID_FLAVOR_IMAGE_EXCEPTIONS = (
55 exception.BadRequirementEmulatorThreadsPolicy,
56 exception.CPUThreadPolicyConfigurationInvalid,
57 exception.FlavorImageConflict,
58 exception.FlavorDiskTooSmall,
59 exception.FlavorMemoryTooSmall,
60 exception.ImageCPUPinningForbidden,
61 exception.ImageCPUThreadPolicyForbidden,
62 exception.ImageNUMATopologyAsymmetric,
63 exception.ImageNUMATopologyCPUDuplicates,
64 exception.ImageNUMATopologyCPUOutOfRange,
65 exception.ImageNUMATopologyCPUsUnassigned,
66 exception.ImageNUMATopologyForbidden,
67 exception.ImageNUMATopologyIncomplete,
68 exception.ImageNUMATopologyMemoryOutOfRange,
69 exception.ImageNUMATopologyRebuildConflict,
70 exception.ImageSerialPortNumberExceedFlavorValue,
71 exception.ImageSerialPortNumberInvalid,
72 exception.ImageVCPULimitsRangeExceeded,
73 exception.ImageVCPUTopologyRangeExceeded,
74 exception.InvalidCPUAllocationPolicy,
75 exception.InvalidCPUThreadAllocationPolicy,
76 exception.InvalidEmulatorThreadsPolicy,
77 exception.InvalidMachineType,
78 exception.InvalidNUMANodesNumber,
79 exception.InvalidRequest,
80 exception.MemoryPageSizeForbidden,
81 exception.MemoryPageSizeInvalid,
82 exception.PciInvalidAlias,
83 exception.PciRequestAliasNotDefined,
84 exception.RealtimeConfigurationInvalid,
85 exception.RealtimeMaskNotFoundOrInvalid,
86 exception.RequiredMixedInstancePolicy,
87 exception.RequiredMixedOrRealtimeCPUMask,
88 exception.InvalidMixedInstanceDedicatedMask,
89)
92class ServersController(wsgi.Controller):
93 """The Server API base controller class for the OpenStack API."""
95 _view_builder_class = views_servers.ViewBuilder
97 @staticmethod
98 def _add_location(robj):
99 # Just in case...
100 if 'server' not in robj.obj: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 return robj
103 link = [link for link in robj.obj['server'][
104 'links'] if link['rel'] == 'self']
105 if link: 105 ↛ 109line 105 didn't jump to line 109 because the condition on line 105 was always true
106 robj['Location'] = link[0]['href']
108 # Convenience return
109 return robj
111 def __init__(self):
112 super(ServersController, self).__init__()
113 self.compute_api = compute.API()
115 @wsgi.expected_errors((400, 403))
116 @validation.query_schema(schema.query_params_v275, '2.75')
117 @validation.query_schema(schema.query_params_v273, '2.73', '2.74')
118 @validation.query_schema(schema.query_params_v266, '2.66', '2.72')
119 @validation.query_schema(schema.query_params_v226, '2.26', '2.65')
120 @validation.query_schema(schema.query_params_v21, '2.1', '2.25')
121 def index(self, req):
122 """Returns a list of server names and ids for a given user."""
123 context = req.environ['nova.context']
124 context.can(server_policies.SERVERS % 'index')
125 try:
126 servers = self._get_servers(req, is_detail=False)
127 except exception.Invalid as err:
128 raise exc.HTTPBadRequest(explanation=err.format_message())
129 return servers
131 @wsgi.expected_errors((400, 403))
132 @validation.query_schema(schema.query_params_v275, '2.75')
133 @validation.query_schema(schema.query_params_v273, '2.73', '2.74')
134 @validation.query_schema(schema.query_params_v266, '2.66', '2.72')
135 @validation.query_schema(schema.query_params_v226, '2.26', '2.65')
136 @validation.query_schema(schema.query_params_v21, '2.1', '2.25')
137 def detail(self, req):
138 """Returns a list of server details for a given user."""
139 context = req.environ['nova.context']
140 context.can(server_policies.SERVERS % 'detail')
141 try:
142 servers = self._get_servers(req, is_detail=True)
143 except exception.Invalid as err:
144 raise exc.HTTPBadRequest(explanation=err.format_message())
145 return servers
147 @staticmethod
148 def _is_cell_down_supported(req, search_opts):
149 cell_down_support = api_version_request.is_supported(
150 req, min_version='2.69')
152 if cell_down_support:
153 # NOTE(tssurya): Minimal constructs would be returned from the down
154 # cells if cell_down_support is True, however if filtering, sorting
155 # or paging is requested by the user, then cell_down_support should
156 # be made False and the down cells should be skipped (depending on
157 # CONF.api.list_records_by_skipping_down_cells) as there is no
158 # way to return correct results for the down cells in those
159 # situations due to missing keys/information.
160 # NOTE(tssurya): Since there is a chance that
161 # remove_invalid_options function could have removed the paging and
162 # sorting parameters, we add the additional check for that from the
163 # request.
164 pag_sort = any(
165 ps in req.GET.keys() for ps in PAGING_SORTING_PARAMS)
166 # NOTE(tssurya): ``nova list --all_tenants`` is the only
167 # allowed filter exception when handling down cells.
168 filters = list(search_opts.keys()) not in ([u'all_tenants'], [])
169 if pag_sort or filters:
170 cell_down_support = False
171 return cell_down_support
173 def _get_servers(self, req, is_detail):
174 """Returns a list of servers, based on any search options specified."""
176 search_opts = {}
177 search_opts.update(req.GET)
179 context = req.environ['nova.context']
180 remove_invalid_options(context, search_opts,
181 self._get_server_search_options(req))
183 cell_down_support = self._is_cell_down_supported(req, search_opts)
185 for search_opt in search_opts:
186 if (search_opt in
187 schema.JOINED_TABLE_QUERY_PARAMS_SERVERS.keys() or
188 search_opt.startswith('_')):
189 msg = _("Invalid filter field: %s.") % search_opt
190 raise exc.HTTPBadRequest(explanation=msg)
192 # Verify search by 'status' contains a valid status.
193 # Convert it to filter by vm_state or task_state for compute_api.
194 # For non-admin user, vm_state and task_state are filtered through
195 # remove_invalid_options function, based on value of status field.
196 # Set value to vm_state and task_state to make search simple.
197 search_opts.pop('status', None)
198 if 'status' in req.GET.keys():
199 statuses = req.GET.getall('status')
200 states = common.task_and_vm_state_from_status(statuses)
201 vm_state, task_state = states
202 if not vm_state and not task_state:
203 if api_version_request.is_supported(req, min_version='2.38'):
204 msg = _('Invalid status value')
205 raise exc.HTTPBadRequest(explanation=msg)
207 return {'servers': []}
208 search_opts['vm_state'] = vm_state
209 # When we search by vm state, task state will return 'default'.
210 # So we don't need task_state search_opt.
211 if 'default' not in task_state:
212 search_opts['task_state'] = task_state
214 if 'changes-since' in search_opts:
215 try:
216 search_opts['changes-since'] = timeutils.parse_isotime(
217 search_opts['changes-since'])
218 except ValueError:
219 # NOTE: This error handling is for V2.0 API to pass the
220 # experimental jobs at the gate. V2.1 API covers this case
221 # with JSON-Schema and it is a hard burden to apply it to
222 # v2.0 API at this time.
223 msg = _("Invalid filter field: changes-since.")
224 raise exc.HTTPBadRequest(explanation=msg)
226 if 'changes-before' in search_opts:
227 try:
228 search_opts['changes-before'] = timeutils.parse_isotime(
229 search_opts['changes-before'])
230 changes_since = search_opts.get('changes-since')
231 if changes_since and search_opts['changes-before'] < \
232 search_opts['changes-since']:
233 msg = _('The value of changes-since must be'
234 ' less than or equal to changes-before.')
235 raise exc.HTTPBadRequest(explanation=msg)
236 except ValueError:
237 msg = _("Invalid filter field: changes-before.")
238 raise exc.HTTPBadRequest(explanation=msg)
240 # By default, compute's get_all() will return deleted instances.
241 # If an admin hasn't specified a 'deleted' search option, we need
242 # to filter out deleted instances by setting the filter ourselves.
243 # ... Unless 'changes-since' or 'changes-before' is specified,
244 # because those will return recently deleted instances according to
245 # the API spec.
247 if 'deleted' not in search_opts:
248 if 'changes-since' not in search_opts and \
249 'changes-before' not in search_opts:
250 # No 'changes-since' or 'changes-before', so we only
251 # want non-deleted servers
252 search_opts['deleted'] = False
253 else:
254 # Convert deleted filter value to a valid boolean.
255 # Return non-deleted servers if an invalid value
256 # is passed with deleted filter.
257 search_opts['deleted'] = strutils.bool_from_string(
258 search_opts['deleted'], default=False)
260 if search_opts.get("vm_state") == ['deleted']:
261 if context.is_admin:
262 search_opts['deleted'] = True
263 else:
264 msg = _("Only administrators may list deleted instances")
265 raise exc.HTTPForbidden(explanation=msg)
267 if api_version_request.is_supported(req, min_version='2.26'):
268 for tag_filter in TAG_SEARCH_FILTERS:
269 if tag_filter in search_opts:
270 search_opts[tag_filter] = search_opts[
271 tag_filter].split(',')
273 all_tenants = common.is_all_tenants(search_opts)
274 # use the boolean from here on out so remove the entry from search_opts
275 # if it's present.
276 # NOTE(tssurya): In case we support handling down cells
277 # we need to know further down the stack whether the 'all_tenants'
278 # filter was passed with the true value or not, so we pass the flag
279 # further down the stack.
280 search_opts.pop('all_tenants', None)
282 if 'locked' in search_opts:
283 search_opts['locked'] = common.is_locked(search_opts)
285 elevated = None
286 if all_tenants:
287 if is_detail:
288 context.can(server_policies.SERVERS % 'detail:get_all_tenants')
289 else:
290 context.can(server_policies.SERVERS % 'index:get_all_tenants')
291 elevated = context.elevated()
292 else:
293 # As explained in lp:#1185290, if `all_tenants` is not passed
294 # we must ignore the `tenant_id` search option.
295 search_opts.pop('tenant_id', None)
296 if context.project_id:
297 search_opts['project_id'] = context.project_id
298 else:
299 search_opts['user_id'] = context.user_id
301 limit, marker = common.get_limit_and_marker(req)
302 sort_keys, sort_dirs = common.get_sort_params(req.params)
303 blacklist = schema.SERVER_LIST_IGNORE_SORT_KEY
304 if api_version_request.is_supported(req, min_version='2.73'):
305 blacklist = schema.SERVER_LIST_IGNORE_SORT_KEY_V273
306 sort_keys, sort_dirs = remove_invalid_sort_keys(
307 context, sort_keys, sort_dirs, blacklist, ('host', 'node'))
309 expected_attrs = []
310 if is_detail:
311 if api_version_request.is_supported(req, '2.16'):
312 expected_attrs.append('services')
313 if api_version_request.is_supported(req, '2.26'):
314 expected_attrs.append("tags")
315 if api_version_request.is_supported(req, '2.63'): 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true
316 expected_attrs.append("trusted_certs")
317 if api_version_request.is_supported(req, '2.73'): 317 ↛ 318line 317 didn't jump to line 318 because the condition on line 317 was never true
318 expected_attrs.append("system_metadata")
320 # merge our expected attrs with what the view builder needs for
321 # showing details
322 expected_attrs = self._view_builder.get_show_expected_attrs(
323 expected_attrs)
325 try:
326 instance_list = self.compute_api.get_all(elevated or context,
327 search_opts=search_opts, limit=limit, marker=marker,
328 expected_attrs=expected_attrs, sort_keys=sort_keys,
329 sort_dirs=sort_dirs, cell_down_support=cell_down_support,
330 all_tenants=all_tenants)
331 except exception.MarkerNotFound:
332 msg = _('marker [%s] not found') % marker
333 raise exc.HTTPBadRequest(explanation=msg)
334 except exception.FlavorNotFound:
335 LOG.debug("Flavor '%s' could not be found ",
336 search_opts['flavor'])
337 instance_list = objects.InstanceList()
339 if is_detail:
340 instance_list._context = context
341 instance_list.fill_faults()
342 response = self._view_builder.detail(
343 req, instance_list, cell_down_support=cell_down_support)
344 else:
345 response = self._view_builder.index(
346 req, instance_list, cell_down_support=cell_down_support)
347 return response
349 def _get_server(self, context, req, instance_uuid, is_detail=False,
350 cell_down_support=False, columns_to_join=None):
351 """Utility function for looking up an instance by uuid.
353 :param context: request context for auth
354 :param req: HTTP request.
355 :param instance_uuid: UUID of the server instance to get
356 :param is_detail: True if you plan on showing the details of the
357 instance in the response, False otherwise.
358 :param cell_down_support: True if the API (and caller) support
359 returning a minimal instance
360 construct if the relevant cell is
361 down.
362 :param columns_to_join: optional list of extra fields to join on the
363 Instance object
364 """
365 expected_attrs = ['flavor', 'numa_topology']
366 if is_detail:
367 if api_version_request.is_supported(req, '2.26'):
368 expected_attrs.append("tags")
369 if api_version_request.is_supported(req, '2.63'):
370 expected_attrs.append("trusted_certs")
371 expected_attrs = self._view_builder.get_show_expected_attrs(
372 expected_attrs)
373 if columns_to_join:
374 expected_attrs.extend(columns_to_join)
375 instance = common.get_instance(self.compute_api, context,
376 instance_uuid,
377 expected_attrs=expected_attrs,
378 cell_down_support=cell_down_support)
379 return instance
381 @staticmethod
382 def _validate_network_id(net_id, network_uuids):
383 """Validates that a requested network id.
385 This method checks that the network id is in the proper UUID format.
387 :param net_id: The network id to validate.
388 :param network_uuids: A running list of requested network IDs that have
389 passed validation already.
390 :raises: webob.exc.HTTPBadRequest if validation fails
391 """
392 if not uuidutils.is_uuid_like(net_id):
393 msg = _("Bad networks format: network uuid is "
394 "not in proper format (%s)") % net_id
395 raise exc.HTTPBadRequest(explanation=msg)
397 def _get_requested_networks(self, requested_networks):
398 """Create a list of requested networks from the networks attribute."""
400 # Starting in the 2.37 microversion, requested_networks is either a
401 # list or a string enum with value 'auto' or 'none'. The auto/none
402 # values are verified via jsonschema so we don't check them again here.
403 if isinstance(requested_networks, str):
404 return objects.NetworkRequestList(
405 objects=[objects.NetworkRequest(
406 network_id=requested_networks)])
408 networks = []
409 network_uuids = []
410 port_uuids = []
411 for network in requested_networks:
412 request = objects.NetworkRequest()
413 try:
414 # fixed IP address is optional
415 # if the fixed IP address is not provided then
416 # it will use one of the available IP address from the network
417 request.address = network.get('fixed_ip', None)
418 request.port_id = network.get('port', None)
419 request.tag = network.get('tag', None)
421 if request.port_id:
422 if request.port_id in port_uuids:
423 msg = _(
424 "Port ID '%(port)s' was specified twice: you "
425 "cannot attach a port multiple times."
426 ) % {
427 "port": request.port_id,
428 }
429 raise exc.HTTPBadRequest(explanation=msg)
431 if request.address is not None:
432 msg = _(
433 "Specified Fixed IP '%(addr)s' cannot be used "
434 "with port '%(port)s': the two cannot be "
435 "specified together."
436 ) % {
437 "addr": request.address,
438 "port": request.port_id,
439 }
440 raise exc.HTTPBadRequest(explanation=msg)
442 request.network_id = None
443 port_uuids.append(request.port_id)
444 else:
445 request.network_id = network['uuid']
446 self._validate_network_id(
447 request.network_id, network_uuids)
448 network_uuids.append(request.network_id)
450 networks.append(request)
451 except KeyError as key:
452 expl = _('Bad network format: missing %s') % key
453 raise exc.HTTPBadRequest(explanation=expl)
454 except TypeError:
455 expl = _('Bad networks format')
456 raise exc.HTTPBadRequest(explanation=expl)
458 return objects.NetworkRequestList(objects=networks)
460 @wsgi.expected_errors(404)
461 @validation.query_schema(schema.show_query)
462 def show(self, req, id):
463 """Returns server details by server id."""
464 context = req.environ['nova.context']
465 cell_down_support = api_version_request.is_supported(
466 req, min_version='2.69')
467 show_server_groups = api_version_request.is_supported(
468 req, min_version='2.71')
470 instance = self._get_server(
471 context, req, id, is_detail=True,
472 columns_to_join=['services'],
473 cell_down_support=cell_down_support)
474 context.can(server_policies.SERVERS % 'show',
475 target={'project_id': instance.project_id})
477 return self._view_builder.show(
478 req, instance, cell_down_support=cell_down_support,
479 show_server_groups=show_server_groups)
481 @staticmethod
482 def _process_bdms_for_create(
483 context, target, server_dict, create_kwargs):
484 """Processes block_device_mapping(_v2) req parameters for server create
486 :param context: The nova auth request context
487 :param target: The target dict for ``context.can`` policy checks
488 :param server_dict: The POST /servers request body "server" entry
489 :param create_kwargs: dict that gets populated by this method and
490 passed to nova.compute.api.API.create()
491 :raises: webob.exc.HTTPBadRequest if the request parameters are invalid
492 :raises: nova.exception.Forbidden if a policy check fails
493 """
494 block_device_mapping_legacy = server_dict.get('block_device_mapping',
495 [])
496 block_device_mapping_v2 = server_dict.get('block_device_mapping_v2',
497 [])
499 if block_device_mapping_legacy and block_device_mapping_v2:
500 expl = _('Using different block_device_mapping syntaxes '
501 'is not allowed in the same request.')
502 raise exc.HTTPBadRequest(explanation=expl)
504 if block_device_mapping_legacy:
505 for bdm in block_device_mapping_legacy:
506 if 'delete_on_termination' in bdm:
507 bdm['delete_on_termination'] = strutils.bool_from_string(
508 bdm['delete_on_termination'])
509 create_kwargs[
510 'block_device_mapping'] = block_device_mapping_legacy
511 # Sets the legacy_bdm flag if we got a legacy block device mapping.
512 create_kwargs['legacy_bdm'] = True
513 elif block_device_mapping_v2:
514 # Have to check whether --image is given, see bug 1433609
515 image_href = server_dict.get('imageRef')
516 image_uuid_specified = image_href is not None
517 try:
518 block_device_mapping = [
519 block_device.BlockDeviceDict.from_api(bdm_dict,
520 image_uuid_specified)
521 for bdm_dict in block_device_mapping_v2]
522 except exception.InvalidBDMFormat as e:
523 raise exc.HTTPBadRequest(explanation=e.format_message())
524 create_kwargs['block_device_mapping'] = block_device_mapping
525 # Unset the legacy_bdm flag if we got a block device mapping.
526 create_kwargs['legacy_bdm'] = False
528 block_device_mapping = create_kwargs.get("block_device_mapping")
529 if block_device_mapping:
530 context.can(server_policies.SERVERS % 'create:attach_volume',
531 target)
533 def _process_networks_for_create(
534 self, context, target, server_dict, create_kwargs):
535 """Processes networks request parameter for server create
537 :param context: The nova auth request context
538 :param target: The target dict for ``context.can`` policy checks
539 :param server_dict: The POST /servers request body "server" entry
540 :param create_kwargs: dict that gets populated by this method and
541 passed to nova.compute.api.API.create()
542 :raises: webob.exc.HTTPBadRequest if the request parameters are invalid
543 :raises: nova.exception.Forbidden if a policy check fails
544 """
545 requested_networks = server_dict.get('networks', None)
547 if requested_networks is not None:
548 requested_networks = self._get_requested_networks(
549 requested_networks)
551 # Skip policy check for 'create:attach_network' if there is no
552 # network allocation request.
553 if requested_networks and len(requested_networks) and \
554 not requested_networks.no_allocate:
555 context.can(server_policies.SERVERS % 'create:attach_network',
556 target)
558 create_kwargs['requested_networks'] = requested_networks
560 @staticmethod
561 def _validate_host_availability_zone(context, availability_zone, host):
562 """Ensure the host belongs in the availability zone.
564 This is slightly tricky and it's probably worth recapping how host
565 aggregates and availability zones are related before reading. Hosts can
566 belong to zero or more host aggregates, but they will always belong to
567 exactly one availability zone. If the user has set the availability
568 zone key on one of the host aggregates that the host is a member of
569 then the host will belong to this availability zone. If the user has
570 not set the availability zone key on any of the host aggregates that
571 the host is a member of or the host is not a member of any host
572 aggregates, then the host will belong to the default availability zone.
573 Setting the availability zone key on more than one of host aggregates
574 that the host is a member of is an error and will be rejected by the
575 API.
577 Given the above, our host-availability zone check needs to vary
578 behavior based on whether we're requesting the default availability
579 zone or not. If we are not, then we simply ask "does this host belong
580 to a host aggregate and, if so, do any of the host aggregates have the
581 requested availability zone metadata set". By comparison, if we *are*
582 requesting the default availability zone then we want to ask the
583 inverse, or "does this host not belong to a host aggregate or, if it
584 does, is the availability zone information unset (or, naughty naughty,
585 set to the default) for each of the host aggregates". If both cases, if
586 the answer is no then we warn about the mismatch and then use the
587 actual availability zone of the host to avoid mismatches.
589 :param context: The nova auth request context
590 :param availability_zone: The name of the requested availability zone
591 :param host: The name of the requested host
592 :returns: The availability zone that should actually be used for the
593 request
594 """
595 aggregates = objects.AggregateList.get_by_host(context, host=host)
596 if not aggregates:
597 # a host is assigned to the default availability zone if it is not
598 # a member of any host aggregates
599 if availability_zone == CONF.default_availability_zone:
600 return availability_zone
602 LOG.warning(
603 "Requested availability zone '%s' but forced host '%s' "
604 "does not belong to any availability zones; ignoring "
605 "requested availability zone to avoid bug #1934770",
606 availability_zone, host,
607 )
608 return None
610 # only one host aggregate will have the availability_zone field set so
611 # use the first non-null value
612 host_availability_zone = next(
613 (a.availability_zone for a in aggregates if a.availability_zone),
614 None,
615 )
617 if availability_zone == host_availability_zone:
618 # if there's an exact match, use what the user requested
619 return availability_zone
621 if (
622 availability_zone == CONF.default_availability_zone and
623 host_availability_zone is None
624 ):
625 # special case the default availability zone since this won't (or
626 # rather shouldn't) be explicitly stored on any host aggregate
627 return availability_zone
629 # no match, so use the host's availability zone information, if any
630 LOG.warning(
631 "Requested availability zone '%s' but forced host '%s' "
632 "does not belong to this availability zone; overwriting "
633 "requested availability zone to avoid bug #1934770",
634 availability_zone, host,
635 )
636 return None
638 @staticmethod
639 def _process_hosts_for_create(
640 context, target, server_dict, create_kwargs, host, node):
641 """Processes hosts request parameter for server create
643 :param context: The nova auth request context
644 :param target: The target dict for ``context.can`` policy checks
645 :param server_dict: The POST /servers request body "server" entry
646 :param create_kwargs: dict that gets populated by this method and
647 passed to nova.compute.api.API.create()
648 :param host: Forced host of availability_zone
649 :param node: Forced node of availability_zone
650 :raise: webob.exc.HTTPBadRequest if the request parameters are invalid
651 :raise: nova.exception.Forbidden if a policy check fails
652 """
653 requested_host = server_dict.get('host')
654 requested_hypervisor_hostname = server_dict.get('hypervisor_hostname')
655 if requested_host or requested_hypervisor_hostname:
656 # If the policy check fails, this will raise Forbidden exception.
657 context.can(server_policies.REQUESTED_DESTINATION, target=target)
658 if host or node:
659 msg = _("One mechanism with host and/or "
660 "hypervisor_hostname and another mechanism "
661 "with zone:host:node are mutually exclusive.")
662 raise exc.HTTPBadRequest(explanation=msg)
663 create_kwargs['requested_host'] = requested_host
664 create_kwargs['requested_hypervisor_hostname'] = (
665 requested_hypervisor_hostname)
667 @wsgi.response(202)
668 @wsgi.expected_errors((400, 403, 409))
669 @validation.schema(schema.create_v20, '2.0', '2.0')
670 @validation.schema(schema.create, '2.1', '2.18')
671 @validation.schema(schema.create_v219, '2.19', '2.31')
672 @validation.schema(schema.create_v232, '2.32', '2.32')
673 @validation.schema(schema.create_v233, '2.33', '2.36')
674 @validation.schema(schema.create_v237, '2.37', '2.41')
675 @validation.schema(schema.create_v242, '2.42', '2.51')
676 @validation.schema(schema.create_v252, '2.52', '2.56')
677 @validation.schema(schema.create_v257, '2.57', '2.62')
678 @validation.schema(schema.create_v263, '2.63', '2.66')
679 @validation.schema(schema.create_v267, '2.67', '2.73')
680 @validation.schema(schema.create_v274, '2.74', '2.89')
681 @validation.schema(schema.create_v290, '2.90', '2.93')
682 @validation.schema(schema.create_v294, '2.94')
683 def create(self, req, body):
684 """Creates a new server for a given user."""
685 context = req.environ['nova.context']
686 server_dict = body['server']
687 password = self._get_server_admin_password(server_dict)
688 name = common.normalize_name(server_dict['name'])
689 description = name
690 if api_version_request.is_supported(req, min_version='2.19'):
691 description = server_dict.get('description')
692 hostname = None
693 if api_version_request.is_supported(req, min_version='2.90'):
694 hostname = server_dict.get('hostname')
696 # Arguments to be passed to instance create function
697 create_kwargs = {}
699 create_kwargs['user_data'] = server_dict.get('user_data')
700 # NOTE(alex_xu): The v2.1 API compat mode, we strip the spaces for
701 # keypair create. But we didn't strip spaces at here for
702 # backward-compatible some users already created keypair and name with
703 # leading/trailing spaces by legacy v2 API.
704 create_kwargs['key_name'] = server_dict.get('key_name')
705 create_kwargs['config_drive'] = server_dict.get('config_drive')
706 security_groups = server_dict.get('security_groups')
707 if security_groups is not None:
708 create_kwargs['security_groups'] = [
709 sg['name'] for sg in security_groups if sg.get('name')]
710 create_kwargs['security_groups'] = list(
711 set(create_kwargs['security_groups']))
713 scheduler_hints = {}
714 if 'os:scheduler_hints' in body:
715 scheduler_hints = body['os:scheduler_hints']
716 elif 'OS-SCH-HNT:scheduler_hints' in body:
717 scheduler_hints = body['OS-SCH-HNT:scheduler_hints']
718 create_kwargs['scheduler_hints'] = scheduler_hints
720 # min_count and max_count are optional. If they exist, they may come
721 # in as strings. Verify that they are valid integers and > 0.
722 # Also, we want to default 'min_count' to 1, and default
723 # 'max_count' to be 'min_count'.
724 min_count = int(server_dict.get('min_count', 1))
725 max_count = int(server_dict.get('max_count', min_count))
726 if min_count > max_count:
727 msg = _('min_count must be <= max_count')
728 raise exc.HTTPBadRequest(explanation=msg)
729 create_kwargs['min_count'] = min_count
730 create_kwargs['max_count'] = max_count
732 availability_zone = server_dict.pop("availability_zone", None)
734 if api_version_request.is_supported(req, min_version='2.52'):
735 create_kwargs['tags'] = server_dict.get('tags')
737 helpers.translate_attributes(helpers.CREATE,
738 server_dict, create_kwargs)
740 target = {
741 'project_id': context.project_id,
742 'user_id': context.user_id,
743 'availability_zone': availability_zone}
744 context.can(server_policies.SERVERS % 'create', target)
746 # Skip policy check for 'create:trusted_certs' if no trusted
747 # certificate IDs were provided.
748 trusted_certs = server_dict.get('trusted_image_certificates', None)
749 if trusted_certs:
750 create_kwargs['trusted_certs'] = trusted_certs
751 context.can(server_policies.SERVERS % 'create:trusted_certs',
752 target=target)
754 parse_az = self.compute_api.parse_availability_zone
755 try:
756 availability_zone, host, node = parse_az(context,
757 availability_zone)
758 except exception.InvalidInput as err:
759 raise exc.HTTPBadRequest(explanation=str(err))
760 if host or node:
761 context.can(server_policies.SERVERS % 'create:forced_host',
762 target=target)
763 availability_zone = self._validate_host_availability_zone(
764 context, availability_zone, host)
766 if api_version_request.is_supported(req, min_version='2.74'):
767 self._process_hosts_for_create(context, target, server_dict,
768 create_kwargs, host, node)
770 self._process_bdms_for_create(
771 context, target, server_dict, create_kwargs)
773 image_uuid = self._image_from_req_data(server_dict, create_kwargs)
775 self._process_networks_for_create(
776 context, target, server_dict, create_kwargs)
778 flavor_id = self._flavor_id_from_req_data(body)
779 try:
780 flavor = flavors.get_flavor_by_flavor_id(
781 flavor_id, ctxt=context, read_deleted="no")
783 supports_multiattach = common.supports_multiattach_volume(req)
784 supports_port_resource_request = \
785 common.supports_port_resource_request(req)
786 instances, resv_id = self.compute_api.create(
787 context,
788 flavor,
789 image_uuid,
790 display_name=name,
791 display_description=description,
792 hostname=hostname,
793 availability_zone=availability_zone,
794 forced_host=host, forced_node=node,
795 metadata=server_dict.get('metadata', {}),
796 admin_password=password,
797 check_server_group_quota=True,
798 supports_multiattach=supports_multiattach,
799 supports_port_resource_request=supports_port_resource_request,
800 **create_kwargs)
801 except exception.OverQuota as error:
802 raise exc.HTTPForbidden(
803 explanation=error.format_message())
804 except exception.ImageNotFound:
805 msg = _("Can not find requested image")
806 raise exc.HTTPBadRequest(explanation=msg)
807 except exception.KeypairNotFound:
808 msg = _("Invalid key_name provided.")
809 raise exc.HTTPBadRequest(explanation=msg)
810 except exception.ConfigDriveInvalidValue:
811 msg = _("Invalid config_drive provided.")
812 raise exc.HTTPBadRequest(explanation=msg)
813 except (exception.BootFromVolumeRequiredForZeroDiskFlavor,
814 exception.ExternalNetworkAttachForbidden) as error:
815 raise exc.HTTPForbidden(explanation=error.format_message())
816 except messaging.RemoteError as err:
817 msg = "%(err_type)s: %(err_msg)s" % {'err_type': err.exc_type,
818 'err_msg': err.value}
819 raise exc.HTTPBadRequest(explanation=msg)
820 except UnicodeDecodeError as error:
821 msg = "UnicodeError: %s" % error
822 raise exc.HTTPBadRequest(explanation=msg)
823 except (exception.ImageNotActive,
824 exception.ImageBadRequest,
825 exception.ImageNotAuthorized,
826 exception.ImageUnacceptable,
827 exception.FixedIpNotFoundForAddress,
828 exception.FlavorNotFound,
829 exception.InvalidMetadata,
830 exception.InvalidVolume,
831 exception.VolumeNotFound,
832 exception.MismatchVolumeAZException,
833 exception.MultiplePortsNotApplicable,
834 exception.InvalidFixedIpAndMaxCountRequest,
835 exception.AmbiguousHostnameForMultipleInstances,
836 exception.InstanceUserDataMalformed,
837 exception.PortNotFound,
838 exception.FixedIpAlreadyInUse,
839 exception.SecurityGroupNotFound,
840 exception.PortRequiresFixedIP,
841 exception.NetworkRequiresSubnet,
842 exception.NetworkNotFound,
843 exception.InvalidBDM,
844 exception.InvalidBDMSnapshot,
845 exception.InvalidBDMVolume,
846 exception.InvalidBDMImage,
847 exception.InvalidBDMBootSequence,
848 exception.InvalidBDMLocalsLimit,
849 exception.InvalidBDMVolumeNotBootable,
850 exception.InvalidBDMEphemeralSize,
851 exception.InvalidBDMFormat,
852 exception.InvalidBDMSwapSize,
853 exception.InvalidBDMDiskBus,
854 exception.VolumeTypeNotFound,
855 exception.AutoDiskConfigDisabledByImage,
856 exception.InstanceGroupNotFound,
857 exception.SnapshotNotFound,
858 exception.UnableToAutoAllocateNetwork,
859 exception.MultiattachNotSupportedOldMicroversion,
860 exception.CertificateValidationFailed,
861 exception.CreateWithPortResourceRequestOldVersion,
862 exception.DeviceProfileError,
863 exception.ComputeHostNotFound,
864 exception.ForbiddenPortsWithAccelerator,
865 exception.ForbiddenWithRemoteManagedPorts,
866 exception.ExtendedResourceRequestOldCompute,
867 ) as error:
868 raise exc.HTTPBadRequest(explanation=error.format_message())
869 except INVALID_FLAVOR_IMAGE_EXCEPTIONS as error:
870 raise exc.HTTPBadRequest(explanation=error.format_message())
871 except (exception.PortInUse,
872 exception.InstanceExists,
873 exception.NetworkAmbiguous,
874 exception.NoUniqueMatch,
875 exception.MixedInstanceNotSupportByComputeService) as error:
876 raise exc.HTTPConflict(explanation=error.format_message())
878 # If the caller wanted a reservation_id, return it
879 if server_dict.get('return_reservation_id', False):
880 return wsgi.ResponseObject({'reservation_id': resv_id})
882 server = self._view_builder.create(req, instances[0])
884 if CONF.api.enable_instance_password:
885 server['server']['adminPass'] = password
887 robj = wsgi.ResponseObject(server)
889 return self._add_location(robj)
891 def _delete(self, context, req, instance_uuid):
892 instance = self._get_server(context, req, instance_uuid)
893 context.can(server_policies.SERVERS % 'delete',
894 target={'user_id': instance.user_id,
895 'project_id': instance.project_id})
896 if CONF.reclaim_instance_interval:
897 try:
898 self.compute_api.soft_delete(context, instance)
899 except exception.InstanceInvalidState:
900 # Note(yufang521247): instance which has never been active
901 # is not allowed to be soft_deleted. Thus we have to call
902 # delete() to clean up the instance.
903 self.compute_api.delete(context, instance)
904 else:
905 self.compute_api.delete(context, instance)
907 @wsgi.expected_errors(404)
908 @validation.schema(schema.update_v20, '2.0', '2.0')
909 @validation.schema(schema.update, '2.1', '2.18')
910 @validation.schema(schema.update_v219, '2.19', '2.89')
911 @validation.schema(schema.update_v290, '2.90', '2.93')
912 @validation.schema(schema.update_v294, '2.94')
913 def update(self, req, id, body):
914 """Update server then pass on to version-specific controller."""
916 ctxt = req.environ['nova.context']
917 update_dict = {}
918 instance = self._get_server(ctxt, req, id, is_detail=True)
919 ctxt.can(server_policies.SERVERS % 'update',
920 target={'user_id': instance.user_id,
921 'project_id': instance.project_id})
922 show_server_groups = api_version_request.is_supported(
923 req, min_version='2.71')
925 server = body['server']
927 if 'name' in server:
928 update_dict['display_name'] = common.normalize_name(
929 server['name'])
931 if 'description' in server:
932 # This is allowed to be None (remove description)
933 update_dict['display_description'] = server['description']
935 if 'hostname' in server:
936 update_dict['hostname'] = server['hostname']
938 helpers.translate_attributes(helpers.UPDATE, server, update_dict)
940 try:
941 instance = self.compute_api.update_instance(
942 ctxt, instance, update_dict)
944 # NOTE(gmann): Starting from microversion 2.75, PUT and Rebuild
945 # API response will show all attributes like GET /servers API.
946 show_all_attributes = api_version_request.is_supported(
947 req, min_version='2.75')
948 extend_address = show_all_attributes
949 show_AZ = show_all_attributes
950 show_config_drive = show_all_attributes
951 show_keypair = show_all_attributes
952 show_srv_usg = show_all_attributes
953 show_sec_grp = show_all_attributes
954 show_extended_status = show_all_attributes
955 show_extended_volumes = show_all_attributes
956 # NOTE(gmann): Below attributes need to be added in response
957 # if respective policy allows.So setting these as None
958 # to perform the policy check in view builder.
959 show_extended_attr = None if show_all_attributes else False
960 show_host_status = None if show_all_attributes else False
962 return self._view_builder.show(
963 req, instance,
964 extend_address=extend_address,
965 show_AZ=show_AZ,
966 show_config_drive=show_config_drive,
967 show_extended_attr=show_extended_attr,
968 show_host_status=show_host_status,
969 show_keypair=show_keypair,
970 show_srv_usg=show_srv_usg,
971 show_sec_grp=show_sec_grp,
972 show_extended_status=show_extended_status,
973 show_extended_volumes=show_extended_volumes,
974 show_server_groups=show_server_groups)
975 except exception.InstanceNotFound:
976 msg = _("Instance could not be found")
977 raise exc.HTTPNotFound(explanation=msg)
979 # NOTE(gmann): Returns 204 for backwards compatibility but should be 202
980 # for representing async API as this API just accepts the request and
981 # request hypervisor driver to complete the same in async mode.
982 @wsgi.response(204)
983 @wsgi.expected_errors((400, 404, 409))
984 @wsgi.action('confirmResize')
985 @validation.schema(schema.confirm_resize)
986 @validation.response_body_schema(schema.confirm_resize_response)
987 def _action_confirm_resize(self, req, id, body):
988 context = req.environ['nova.context']
989 instance = self._get_server(context, req, id)
990 context.can(server_policies.SERVERS % 'confirm_resize',
991 target={'project_id': instance.project_id})
992 try:
993 self.compute_api.confirm_resize(context, instance)
994 except exception.MigrationNotFound:
995 msg = _("Instance has not been resized.")
996 raise exc.HTTPBadRequest(explanation=msg)
997 except (
998 exception.InstanceIsLocked,
999 exception.ServiceUnavailable,
1000 ) as e:
1001 raise exc.HTTPConflict(explanation=e.format_message())
1002 except exception.InstanceInvalidState as state_error:
1003 common.raise_http_conflict_for_instance_invalid_state(state_error,
1004 'confirmResize', id)
1006 @wsgi.response(202)
1007 @wsgi.expected_errors((400, 404, 409))
1008 @wsgi.action('revertResize')
1009 @validation.schema(schema.revert_resize)
1010 @validation.response_body_schema(schema.revert_resize_response)
1011 def _action_revert_resize(self, req, id, body):
1012 context = req.environ['nova.context']
1013 instance = self._get_server(context, req, id)
1014 context.can(server_policies.SERVERS % 'revert_resize',
1015 target={'project_id': instance.project_id})
1016 try:
1017 self.compute_api.revert_resize(context, instance)
1018 except exception.MigrationNotFound:
1019 msg = _("Instance has not been resized.")
1020 raise exc.HTTPBadRequest(explanation=msg)
1021 except exception.FlavorNotFound:
1022 msg = _("Flavor used by the instance could not be found.")
1023 raise exc.HTTPBadRequest(explanation=msg)
1024 except exception.InstanceIsLocked as e:
1025 raise exc.HTTPConflict(explanation=e.format_message())
1026 except exception.InstanceInvalidState as state_error:
1027 common.raise_http_conflict_for_instance_invalid_state(state_error,
1028 'revertResize', id)
1030 @wsgi.response(202)
1031 @wsgi.expected_errors((404, 409))
1032 @wsgi.action('reboot')
1033 @validation.schema(schema.reboot)
1034 @validation.response_body_schema(schema.reboot_response)
1035 def _action_reboot(self, req, id, body):
1037 reboot_type = body['reboot']['type'].upper()
1038 context = req.environ['nova.context']
1039 instance = self._get_server(context, req, id)
1040 context.can(server_policies.SERVERS % 'reboot',
1041 target={'project_id': instance.project_id})
1043 try:
1044 self.compute_api.reboot(context, instance, reboot_type)
1045 except exception.InstanceIsLocked as e:
1046 raise exc.HTTPConflict(explanation=e.format_message())
1047 except exception.InstanceInvalidState as state_error:
1048 common.raise_http_conflict_for_instance_invalid_state(state_error,
1049 'reboot', id)
1051 def _resize(self, req, instance_id, flavor_id, auto_disk_config=None):
1052 """Begin the resize process with given instance/flavor."""
1053 context = req.environ["nova.context"]
1054 instance = self._get_server(context, req, instance_id,
1055 columns_to_join=['services', 'resources',
1056 'pci_requests',
1057 'pci_devices',
1058 'trusted_certs',
1059 'vcpu_model'])
1060 context.can(server_policies.SERVERS % 'resize',
1061 target={'user_id': instance.user_id,
1062 'project_id': instance.project_id})
1064 try:
1065 self.compute_api.resize(context, instance, flavor_id,
1066 auto_disk_config=auto_disk_config)
1067 except exception.OverQuota as error:
1068 raise exc.HTTPForbidden(
1069 explanation=error.format_message())
1070 except (
1071 exception.InstanceIsLocked,
1072 exception.InstanceNotReady,
1073 exception.MixedInstanceNotSupportByComputeService,
1074 exception.ServiceUnavailable,
1075 ) as e:
1076 raise exc.HTTPConflict(explanation=e.format_message())
1077 except exception.InstanceInvalidState as state_error:
1078 common.raise_http_conflict_for_instance_invalid_state(state_error,
1079 'resize', instance_id)
1080 except exception.ImageNotAuthorized:
1081 msg = _("You are not authorized to access the image "
1082 "the instance was started with.")
1083 raise exc.HTTPUnauthorized(explanation=msg)
1084 except exception.ImageNotFound:
1085 msg = _("Image that the instance was started "
1086 "with could not be found.")
1087 raise exc.HTTPBadRequest(explanation=msg)
1088 except (
1089 exception.AutoDiskConfigDisabledByImage,
1090 exception.CannotResizeDisk,
1091 exception.CannotResizeToSameFlavor,
1092 exception.FlavorNotFound,
1093 exception.ExtendedResourceRequestOldCompute,
1094 ) as e:
1095 raise exc.HTTPBadRequest(explanation=e.format_message())
1096 except INVALID_FLAVOR_IMAGE_EXCEPTIONS as e:
1097 raise exc.HTTPBadRequest(explanation=e.format_message())
1098 except exception.Invalid:
1099 msg = _("Invalid instance image.")
1100 raise exc.HTTPBadRequest(explanation=msg)
1101 except (
1102 exception.ForbiddenSharesNotSupported,
1103 exception.ForbiddenWithShare) as e:
1104 raise exc.HTTPConflict(explanation=e.format_message())
1106 @wsgi.response(204)
1107 @wsgi.expected_errors((404, 409))
1108 def delete(self, req, id):
1109 """Destroys a server."""
1110 try:
1111 self._delete(req.environ['nova.context'], req, id)
1112 except exception.InstanceNotFound:
1113 msg = _("Instance could not be found")
1114 raise exc.HTTPNotFound(explanation=msg)
1115 except (exception.InstanceIsLocked,
1116 exception.AllocationDeleteFailed) as e:
1117 raise exc.HTTPConflict(explanation=e.format_message())
1118 except exception.InstanceInvalidState as state_error:
1119 common.raise_http_conflict_for_instance_invalid_state(state_error,
1120 'delete', id)
1122 def _image_from_req_data(self, server_dict, create_kwargs):
1123 """Get image data from the request or raise appropriate
1124 exceptions.
1126 The field imageRef is mandatory when no block devices have been
1127 defined and must be a proper uuid when present.
1128 """
1129 image_href = server_dict.get('imageRef')
1131 if not image_href and create_kwargs.get('block_device_mapping'):
1132 return ''
1133 elif image_href:
1134 return image_href
1135 else:
1136 msg = _("Missing imageRef attribute")
1137 raise exc.HTTPBadRequest(explanation=msg)
1139 def _flavor_id_from_req_data(self, data):
1140 flavor_ref = data['server']['flavorRef']
1141 return common.get_id_from_href(flavor_ref)
1143 @wsgi.response(202)
1144 @wsgi.expected_errors((400, 401, 403, 404, 409))
1145 @wsgi.action('resize')
1146 @validation.schema(schema.resize)
1147 @validation.response_body_schema(schema.resize_response)
1148 def _action_resize(self, req, id, body):
1149 """Resizes a given instance to the flavor size requested."""
1150 resize_dict = body['resize']
1151 flavor_ref = str(resize_dict["flavorRef"])
1153 kwargs = {}
1154 helpers.translate_attributes(helpers.RESIZE, resize_dict, kwargs)
1156 self._resize(req, id, flavor_ref, **kwargs)
1158 @wsgi.response(202)
1159 @wsgi.expected_errors((400, 403, 404, 409))
1160 @wsgi.action('rebuild')
1161 @validation.schema(schema.rebuild_v20, '2.0', '2.0')
1162 @validation.schema(schema.rebuild, '2.1', '2.18')
1163 @validation.schema(schema.rebuild_v219, '2.19', '2.53')
1164 @validation.schema(schema.rebuild_v254, '2.54', '2.56')
1165 @validation.schema(schema.rebuild_v257, '2.57', '2.62')
1166 @validation.schema(schema.rebuild_v263, '2.63', '2.89')
1167 @validation.schema(schema.rebuild_v290, '2.90', '2.93')
1168 @validation.schema(schema.rebuild_v294, '2.94')
1169 @validation.response_body_schema(schema.rebuild_response, '2.0', '2.8')
1170 @validation.response_body_schema(
1171 schema.rebuild_response_v29, '2.9', '2.18')
1172 @validation.response_body_schema(
1173 schema.rebuild_response_v219, '2.19', '2.25')
1174 @validation.response_body_schema(
1175 schema.rebuild_response_v226, '2.26', '2.45')
1176 @validation.response_body_schema(
1177 schema.rebuild_response_v246, '2.46', '2.53')
1178 @validation.response_body_schema(
1179 schema.rebuild_response_v254, '2.54', '2.56')
1180 @validation.response_body_schema(
1181 schema.rebuild_response_v257, '2.57', '2.62')
1182 @validation.response_body_schema(
1183 schema.rebuild_response_v263, '2.63', '2.70')
1184 @validation.response_body_schema(
1185 schema.rebuild_response_v271, '2.71', '2.72')
1186 @validation.response_body_schema(
1187 schema.rebuild_response_v273, '2.73', '2.74')
1188 @validation.response_body_schema(
1189 schema.rebuild_response_v275, '2.75', '2.95')
1190 @validation.response_body_schema(
1191 schema.rebuild_response_v296, '2.96', '2.97')
1192 @validation.response_body_schema(
1193 schema.rebuild_response_v298, '2.98', '2.99')
1194 @validation.response_body_schema(
1195 schema.rebuild_response_v2100, '2.100')
1196 def _action_rebuild(self, req, id, body):
1197 """Rebuild an instance with the given attributes."""
1198 rebuild_dict = body['rebuild']
1200 image_href = rebuild_dict["imageRef"]
1202 password = self._get_server_admin_password(rebuild_dict)
1204 context = req.environ['nova.context']
1205 instance = self._get_server(context, req, id,
1206 columns_to_join=['trusted_certs',
1207 'pci_requests',
1208 'pci_devices',
1209 'resources',
1210 'migration_context'])
1211 target = {'user_id': instance.user_id,
1212 'project_id': instance.project_id}
1213 context.can(server_policies.SERVERS % 'rebuild', target=target)
1214 attr_map = {
1215 'name': 'display_name',
1216 'description': 'display_description',
1217 'metadata': 'metadata',
1218 }
1220 kwargs = {}
1222 helpers.translate_attributes(helpers.REBUILD, rebuild_dict, kwargs)
1224 if (
1225 api_version_request.is_supported(req, min_version='2.54') and
1226 'key_name' in rebuild_dict
1227 ):
1228 kwargs['key_name'] = rebuild_dict.get('key_name')
1230 # If user_data is not specified, we don't include it in kwargs because
1231 # we don't want to overwrite the existing user_data.
1232 include_user_data = api_version_request.is_supported(
1233 req, min_version='2.57')
1234 if include_user_data and 'user_data' in rebuild_dict:
1235 kwargs['user_data'] = rebuild_dict['user_data']
1237 # Skip policy check for 'rebuild:trusted_certs' if no trusted
1238 # certificate IDs were provided.
1239 if (
1240 api_version_request.is_supported(req, min_version='2.63') and
1241 # Note that this is different from server create since with
1242 # rebuild a user can unset/reset the trusted certs by
1243 # specifying trusted_image_certificates=None, similar to
1244 # key_name.
1245 'trusted_image_certificates' in rebuild_dict
1246 ):
1247 kwargs['trusted_certs'] = rebuild_dict.get(
1248 'trusted_image_certificates')
1249 context.can(server_policies.SERVERS % 'rebuild:trusted_certs',
1250 target=target)
1252 if (
1253 api_version_request.is_supported(req, min_version='2.90') and
1254 'hostname' in rebuild_dict
1255 ):
1256 kwargs['hostname'] = rebuild_dict['hostname']
1258 if api_version_request.is_supported(req, min_version='2.93'): 1258 ↛ 1259line 1258 didn't jump to line 1259 because the condition on line 1258 was never true
1259 kwargs['reimage_boot_volume'] = True
1261 for request_attribute, instance_attribute in attr_map.items():
1262 try:
1263 if request_attribute == 'name':
1264 kwargs[instance_attribute] = common.normalize_name(
1265 rebuild_dict[request_attribute])
1266 else:
1267 kwargs[instance_attribute] = rebuild_dict[
1268 request_attribute]
1269 except (KeyError, TypeError):
1270 pass
1272 try:
1273 self.compute_api.rebuild(context,
1274 instance,
1275 image_href,
1276 password,
1277 **kwargs)
1278 except exception.InstanceIsLocked as e:
1279 raise exc.HTTPConflict(explanation=e.format_message())
1280 except exception.InstanceInvalidState as state_error:
1281 common.raise_http_conflict_for_instance_invalid_state(state_error,
1282 'rebuild', id)
1283 except exception.InstanceNotFound:
1284 msg = _("Instance could not be found")
1285 raise exc.HTTPNotFound(explanation=msg)
1286 except exception.ImageNotFound:
1287 msg = _("Cannot find image for rebuild")
1288 raise exc.HTTPBadRequest(explanation=msg)
1289 except exception.KeypairNotFound:
1290 msg = _("Invalid key_name provided.")
1291 raise exc.HTTPBadRequest(explanation=msg)
1292 except exception.OverQuota as error:
1293 raise exc.HTTPForbidden(explanation=error.format_message())
1294 except (exception.AutoDiskConfigDisabledByImage,
1295 exception.CertificateValidationFailed,
1296 exception.ImageNotActive,
1297 exception.ImageUnacceptable,
1298 exception.InvalidMetadata,
1299 exception.InvalidArchitectureName,
1300 exception.InvalidVolume,
1301 ) as error:
1302 raise exc.HTTPBadRequest(explanation=error.format_message())
1303 except INVALID_FLAVOR_IMAGE_EXCEPTIONS as error:
1304 raise exc.HTTPBadRequest(explanation=error.format_message())
1306 instance = self._get_server(context, req, id, is_detail=True)
1308 # NOTE(liuyulong): set the new key_name for the API response.
1309 # from microversion 2.54 onwards.
1310 show_keypair = api_version_request.is_supported(
1311 req, min_version='2.54')
1312 show_server_groups = api_version_request.is_supported(
1313 req, min_version='2.71')
1315 # NOTE(gmann): Starting from microversion 2.75, PUT and Rebuild
1316 # API response will show all attributes like GET /servers API.
1317 show_all_attributes = api_version_request.is_supported(
1318 req, min_version='2.75')
1319 extend_address = show_all_attributes
1320 show_AZ = show_all_attributes
1321 show_config_drive = show_all_attributes
1322 show_srv_usg = show_all_attributes
1323 show_sec_grp = show_all_attributes
1324 show_extended_status = show_all_attributes
1325 show_extended_volumes = show_all_attributes
1326 # NOTE(gmann): Below attributes need to be added in response
1327 # if respective policy allows.So setting these as None
1328 # to perform the policy check in view builder.
1329 show_extended_attr = None if show_all_attributes else False
1330 show_host_status = None if show_all_attributes else False
1332 view = self._view_builder.show(
1333 req, instance,
1334 extend_address=extend_address,
1335 show_AZ=show_AZ,
1336 show_config_drive=show_config_drive,
1337 show_extended_attr=show_extended_attr,
1338 show_host_status=show_host_status,
1339 show_keypair=show_keypair,
1340 show_srv_usg=show_srv_usg,
1341 show_sec_grp=show_sec_grp,
1342 show_extended_status=show_extended_status,
1343 show_extended_volumes=show_extended_volumes,
1344 show_server_groups=show_server_groups,
1345 # NOTE(gmann): user_data has been added in response (by code at
1346 # the end of this API method) since microversion 2.57 so tell
1347 # view builder not to include it.
1348 show_user_data=False)
1350 # Add on the admin_password attribute since the view doesn't do it
1351 # unless instance passwords are disabled
1352 if CONF.api.enable_instance_password:
1353 view['server']['adminPass'] = password
1355 if include_user_data:
1356 view['server']['user_data'] = instance.user_data
1358 robj = wsgi.ResponseObject(view)
1359 return self._add_location(robj)
1361 @wsgi.response(202)
1362 @wsgi.expected_errors((400, 403, 404, 409))
1363 @wsgi.action('createImage')
1364 @validation.schema(schema.create_image, '2.0', '2.0')
1365 @validation.schema(schema.create_image, '2.1')
1366 @validation.response_body_schema(
1367 schema.create_image_response, '2.0', '2.44')
1368 @validation.response_body_schema(schema.create_image_response_v245, '2.45')
1369 def _action_create_image(self, req, id, body):
1370 """Snapshot a server instance."""
1371 context = req.environ['nova.context']
1372 instance = self._get_server(context, req, id)
1373 target = {'project_id': instance.project_id}
1374 context.can(server_policies.SERVERS % 'create_image',
1375 target=target)
1377 entity = body["createImage"]
1378 image_name = common.normalize_name(entity["name"])
1379 metadata = entity.get('metadata', {})
1381 # Starting from microversion 2.39 we don't check quotas on createImage
1382 if api_version_request.is_supported(
1383 req, max_version=
1384 api_version_request.MAX_IMAGE_META_PROXY_API_VERSION):
1385 common.check_img_metadata_properties_quota(context, metadata)
1387 bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
1388 context, instance.uuid)
1390 try:
1391 if compute_utils.is_volume_backed_instance(context, instance,
1392 bdms):
1393 context.can(server_policies.SERVERS %
1394 'create_image:allow_volume_backed', target=target)
1395 image = self.compute_api.snapshot_volume_backed(
1396 context,
1397 instance,
1398 image_name,
1399 extra_properties=
1400 metadata)
1401 else:
1402 image = self.compute_api.snapshot(context,
1403 instance,
1404 image_name,
1405 extra_properties=metadata)
1406 except exception.InstanceInvalidState as state_error:
1407 common.raise_http_conflict_for_instance_invalid_state(state_error,
1408 'createImage', id)
1409 except exception.InstanceQuiesceFailed as err:
1410 raise exc.HTTPConflict(explanation=err.format_message())
1411 except exception.Invalid as err:
1412 raise exc.HTTPBadRequest(explanation=err.format_message())
1413 except exception.OverQuota as e:
1414 raise exc.HTTPForbidden(explanation=e.format_message())
1416 # Starting with microversion 2.45 we return a response body containing
1417 # the snapshot image id without the Location header.
1418 if api_version_request.is_supported(req, '2.45'):
1419 return {'image_id': image['id']}
1421 # build location of newly-created image entity
1422 image_id = str(image['id'])
1423 image_ref = glance.API().generate_image_url(image_id, context)
1425 resp = webob.Response(status_int=202)
1426 resp.headers['Location'] = image_ref
1427 return resp
1429 def _get_server_admin_password(self, server):
1430 """Determine the admin password for a server on creation."""
1431 if 'adminPass' in server:
1432 password = server['adminPass']
1433 else:
1434 password = utils.generate_password()
1435 return password
1437 def _get_server_search_options(self, req):
1438 """Return server search options allowed by non-admin."""
1439 # NOTE(mriedem): all_tenants is admin-only by default but because of
1440 # tight-coupling between this method, the remove_invalid_options method
1441 # and how _get_servers uses them, we include all_tenants here but it
1442 # will be removed later for non-admins. Fixing this would be nice but
1443 # probably not trivial.
1444 opt_list = ('reservation_id', 'name', 'status', 'image', 'flavor',
1445 'ip', 'changes-since', 'all_tenants')
1446 if api_version_request.is_supported(req, min_version='2.5'):
1447 opt_list += ('ip6',)
1448 if api_version_request.is_supported(req, min_version='2.26'):
1449 opt_list += TAG_SEARCH_FILTERS
1450 if api_version_request.is_supported(req, min_version='2.66'):
1451 opt_list += ('changes-before',)
1452 if api_version_request.is_supported(req, min_version='2.73'):
1453 opt_list += ('locked',)
1454 if api_version_request.is_supported(req, min_version='2.83'):
1455 opt_list += ('availability_zone', 'config_drive', 'key_name',
1456 'created_at', 'launched_at', 'terminated_at',
1457 'power_state', 'task_state', 'vm_state', 'progress',
1458 'user_id',)
1459 if api_version_request.is_supported(req, min_version='2.90'):
1460 opt_list += ('hostname',)
1461 return opt_list
1463 def _get_instance(self, context, instance_uuid):
1464 try:
1465 attrs = ['system_metadata', 'metadata']
1466 mapping = objects.InstanceMapping.get_by_instance_uuid(
1467 context, instance_uuid)
1468 nova_context.set_target_cell(context, mapping.cell_mapping)
1469 return objects.Instance.get_by_uuid(
1470 context, instance_uuid, expected_attrs=attrs)
1471 except (exception.InstanceNotFound,
1472 exception.InstanceMappingNotFound) as e:
1473 raise webob.exc.HTTPNotFound(explanation=e.format_message())
1475 @wsgi.response(202)
1476 @wsgi.expected_errors((404, 409))
1477 @wsgi.action('os-start')
1478 @validation.schema(schema.start_server)
1479 @validation.response_body_schema(schema.start_server_response)
1480 def _start_server(self, req, id, body):
1481 """Start an instance."""
1482 context = req.environ['nova.context']
1483 instance = self._get_instance(context, id)
1484 context.can(server_policies.SERVERS % 'start',
1485 target={'user_id': instance.user_id,
1486 'project_id': instance.project_id})
1487 try:
1488 self.compute_api.start(context, instance)
1489 except (exception.InstanceNotReady, exception.InstanceIsLocked) as e:
1490 raise webob.exc.HTTPConflict(explanation=e.format_message())
1491 except exception.InstanceInvalidState as state_error:
1492 common.raise_http_conflict_for_instance_invalid_state(state_error,
1493 'start', id)
1495 @wsgi.response(202)
1496 @wsgi.expected_errors((404, 409))
1497 @wsgi.action('os-stop')
1498 @validation.schema(schema.stop_server)
1499 @validation.response_body_schema(schema.stop_server_response)
1500 def _stop_server(self, req, id, body):
1501 """Stop an instance."""
1502 context = req.environ['nova.context']
1503 instance = self._get_instance(context, id)
1504 context.can(server_policies.SERVERS % 'stop',
1505 target={'user_id': instance.user_id,
1506 'project_id': instance.project_id})
1507 try:
1508 self.compute_api.stop(context, instance)
1509 except (exception.InstanceNotReady, exception.InstanceIsLocked) as e:
1510 raise webob.exc.HTTPConflict(explanation=e.format_message())
1511 except exception.InstanceInvalidState as state_error:
1512 common.raise_http_conflict_for_instance_invalid_state(
1513 state_error, 'stop', id
1514 )
1516 @wsgi.Controller.api_version("2.17")
1517 @wsgi.response(202)
1518 @wsgi.expected_errors((400, 404, 409))
1519 @wsgi.action('trigger_crash_dump')
1520 @validation.schema(schema.trigger_crash_dump)
1521 @validation.response_body_schema(schema.trigger_crash_dump_response)
1522 def _action_trigger_crash_dump(self, req, id, body):
1523 """Trigger crash dump in an instance"""
1524 context = req.environ['nova.context']
1525 instance = self._get_instance(context, id)
1526 context.can(server_policies.SERVERS % 'trigger_crash_dump',
1527 target={'user_id': instance.user_id,
1528 'project_id': instance.project_id})
1529 try:
1530 self.compute_api.trigger_crash_dump(context, instance)
1531 except (exception.InstanceNotReady, exception.InstanceIsLocked) as e:
1532 raise webob.exc.HTTPConflict(explanation=e.format_message())
1533 except exception.InstanceInvalidState as state_error:
1534 common.raise_http_conflict_for_instance_invalid_state(
1535 state_error, 'trigger_crash_dump', id
1536 )
1539def remove_invalid_options(context, search_options, allowed_search_options):
1540 """Remove search options that are not permitted unless policy allows."""
1542 if context.can(server_policies.SERVERS % 'allow_all_filters',
1543 fatal=False):
1544 # Only remove parameters for sorting and pagination
1545 for key in PAGING_SORTING_PARAMS:
1546 search_options.pop(key, None)
1547 return
1548 # Otherwise, strip out all unknown options
1549 unknown_options = [opt for opt in search_options
1550 if opt not in allowed_search_options]
1551 if unknown_options:
1552 LOG.debug("Removing options '%s' from query",
1553 ", ".join(unknown_options))
1554 for opt in unknown_options:
1555 search_options.pop(opt, None)
1558def remove_invalid_sort_keys(context, sort_keys, sort_dirs,
1559 blacklist, admin_only_fields):
1560 key_list = copy.deepcopy(sort_keys)
1561 for key in key_list:
1562 # NOTE(Kevin Zheng): We are intend to remove the sort_key
1563 # in the blacklist and its' corresponding sort_dir, since
1564 # the sort_key and sort_dir are not strict to be provide
1565 # in pairs in the current implement, sort_dirs could be
1566 # less than sort_keys, in order to avoid IndexError, we
1567 # only pop sort_dir when number of sort_dirs is no less
1568 # than the sort_key index.
1569 if key in blacklist:
1570 if len(sort_dirs) > sort_keys.index(key):
1571 sort_dirs.pop(sort_keys.index(key))
1572 sort_keys.pop(sort_keys.index(key))
1573 elif key in admin_only_fields and not context.is_admin:
1574 msg = _("Only administrators can sort servers "
1575 "by %s") % key
1576 raise exc.HTTPForbidden(explanation=msg)
1578 return sort_keys, sort_dirs