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

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. 

16 

17import copy 

18 

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 

26 

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 

46 

47TAG_SEARCH_FILTERS = ('tags', 'tags-any', 'not-tags', 'not-tags-any') 

48PAGING_SORTING_PARAMS = ('sort_key', 'sort_dir', 'limit', 'marker') 

49 

50CONF = nova.conf.CONF 

51 

52LOG = logging.getLogger(__name__) 

53 

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) 

90 

91 

92class ServersController(wsgi.Controller): 

93 """The Server API base controller class for the OpenStack API.""" 

94 

95 _view_builder_class = views_servers.ViewBuilder 

96 

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 

102 

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'] 

107 

108 # Convenience return 

109 return robj 

110 

111 def __init__(self): 

112 super(ServersController, self).__init__() 

113 self.compute_api = compute.API() 

114 

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 

130 

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 

146 

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') 

151 

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 

172 

173 def _get_servers(self, req, is_detail): 

174 """Returns a list of servers, based on any search options specified.""" 

175 

176 search_opts = {} 

177 search_opts.update(req.GET) 

178 

179 context = req.environ['nova.context'] 

180 remove_invalid_options(context, search_opts, 

181 self._get_server_search_options(req)) 

182 

183 cell_down_support = self._is_cell_down_supported(req, search_opts) 

184 

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) 

191 

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) 

206 

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 

213 

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) 

225 

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) 

239 

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. 

246 

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) 

259 

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) 

266 

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(',') 

272 

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) 

281 

282 if 'locked' in search_opts: 

283 search_opts['locked'] = common.is_locked(search_opts) 

284 

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 

300 

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')) 

308 

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") 

319 

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) 

324 

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() 

338 

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 

348 

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. 

352 

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 

380 

381 @staticmethod 

382 def _validate_network_id(net_id, network_uuids): 

383 """Validates that a requested network id. 

384 

385 This method checks that the network id is in the proper UUID format. 

386 

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) 

396 

397 def _get_requested_networks(self, requested_networks): 

398 """Create a list of requested networks from the networks attribute.""" 

399 

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)]) 

407 

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) 

420 

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) 

430 

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) 

441 

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) 

449 

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) 

457 

458 return objects.NetworkRequestList(objects=networks) 

459 

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') 

469 

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}) 

476 

477 return self._view_builder.show( 

478 req, instance, cell_down_support=cell_down_support, 

479 show_server_groups=show_server_groups) 

480 

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 

485 

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 []) 

498 

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) 

503 

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 

527 

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) 

532 

533 def _process_networks_for_create( 

534 self, context, target, server_dict, create_kwargs): 

535 """Processes networks request parameter for server create 

536 

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) 

546 

547 if requested_networks is not None: 

548 requested_networks = self._get_requested_networks( 

549 requested_networks) 

550 

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) 

557 

558 create_kwargs['requested_networks'] = requested_networks 

559 

560 @staticmethod 

561 def _validate_host_availability_zone(context, availability_zone, host): 

562 """Ensure the host belongs in the availability zone. 

563 

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. 

576 

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. 

588 

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 

601 

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 

609 

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 ) 

616 

617 if availability_zone == host_availability_zone: 

618 # if there's an exact match, use what the user requested 

619 return availability_zone 

620 

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 

628 

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 

637 

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 

642 

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) 

666 

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') 

695 

696 # Arguments to be passed to instance create function 

697 create_kwargs = {} 

698 

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'])) 

712 

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 

719 

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 

731 

732 availability_zone = server_dict.pop("availability_zone", None) 

733 

734 if api_version_request.is_supported(req, min_version='2.52'): 

735 create_kwargs['tags'] = server_dict.get('tags') 

736 

737 helpers.translate_attributes(helpers.CREATE, 

738 server_dict, create_kwargs) 

739 

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) 

745 

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) 

753 

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) 

765 

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) 

769 

770 self._process_bdms_for_create( 

771 context, target, server_dict, create_kwargs) 

772 

773 image_uuid = self._image_from_req_data(server_dict, create_kwargs) 

774 

775 self._process_networks_for_create( 

776 context, target, server_dict, create_kwargs) 

777 

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") 

782 

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()) 

877 

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}) 

881 

882 server = self._view_builder.create(req, instances[0]) 

883 

884 if CONF.api.enable_instance_password: 

885 server['server']['adminPass'] = password 

886 

887 robj = wsgi.ResponseObject(server) 

888 

889 return self._add_location(robj) 

890 

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) 

906 

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.""" 

915 

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') 

924 

925 server = body['server'] 

926 

927 if 'name' in server: 

928 update_dict['display_name'] = common.normalize_name( 

929 server['name']) 

930 

931 if 'description' in server: 

932 # This is allowed to be None (remove description) 

933 update_dict['display_description'] = server['description'] 

934 

935 if 'hostname' in server: 

936 update_dict['hostname'] = server['hostname'] 

937 

938 helpers.translate_attributes(helpers.UPDATE, server, update_dict) 

939 

940 try: 

941 instance = self.compute_api.update_instance( 

942 ctxt, instance, update_dict) 

943 

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 

961 

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) 

978 

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) 

1005 

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) 

1029 

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): 

1036 

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}) 

1042 

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) 

1050 

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}) 

1063 

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()) 

1105 

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) 

1121 

1122 def _image_from_req_data(self, server_dict, create_kwargs): 

1123 """Get image data from the request or raise appropriate 

1124 exceptions. 

1125 

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') 

1130 

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) 

1138 

1139 def _flavor_id_from_req_data(self, data): 

1140 flavor_ref = data['server']['flavorRef'] 

1141 return common.get_id_from_href(flavor_ref) 

1142 

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"]) 

1152 

1153 kwargs = {} 

1154 helpers.translate_attributes(helpers.RESIZE, resize_dict, kwargs) 

1155 

1156 self._resize(req, id, flavor_ref, **kwargs) 

1157 

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'] 

1199 

1200 image_href = rebuild_dict["imageRef"] 

1201 

1202 password = self._get_server_admin_password(rebuild_dict) 

1203 

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 } 

1219 

1220 kwargs = {} 

1221 

1222 helpers.translate_attributes(helpers.REBUILD, rebuild_dict, kwargs) 

1223 

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') 

1229 

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'] 

1236 

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) 

1251 

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'] 

1257 

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 

1260 

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 

1271 

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()) 

1305 

1306 instance = self._get_server(context, req, id, is_detail=True) 

1307 

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') 

1314 

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 

1331 

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) 

1349 

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 

1354 

1355 if include_user_data: 

1356 view['server']['user_data'] = instance.user_data 

1357 

1358 robj = wsgi.ResponseObject(view) 

1359 return self._add_location(robj) 

1360 

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) 

1376 

1377 entity = body["createImage"] 

1378 image_name = common.normalize_name(entity["name"]) 

1379 metadata = entity.get('metadata', {}) 

1380 

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) 

1386 

1387 bdms = objects.BlockDeviceMappingList.get_by_instance_uuid( 

1388 context, instance.uuid) 

1389 

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()) 

1415 

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']} 

1420 

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) 

1424 

1425 resp = webob.Response(status_int=202) 

1426 resp.headers['Location'] = image_ref 

1427 return resp 

1428 

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 

1436 

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 

1462 

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()) 

1474 

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) 

1494 

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 ) 

1515 

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 ) 

1537 

1538 

1539def remove_invalid_options(context, search_options, allowed_search_options): 

1540 """Remove search options that are not permitted unless policy allows.""" 

1541 

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) 

1556 

1557 

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) 

1577 

1578 return sort_keys, sort_dirs