Coverage for nova/api/openstack/common.py: 99%

215 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-24 11:16 +0000

1# Copyright 2010 OpenStack Foundation 

2# All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16import collections 

17import itertools 

18import re 

19from urllib import parse as urlparse 

20 

21from oslo_log import log as logging 

22from oslo_utils import strutils 

23import webob 

24from webob import exc 

25 

26from nova.api.openstack import api_version_request 

27from nova.compute import task_states 

28from nova.compute import vm_states 

29import nova.conf 

30from nova import exception 

31from nova.i18n import _ 

32from nova import objects 

33from nova import quota 

34from nova import utils 

35 

36CONF = nova.conf.CONF 

37 

38LOG = logging.getLogger(__name__) 

39QUOTAS = quota.QUOTAS 

40 

41 

42POWER_ON = 'POWER_ON' 

43POWER_OFF = 'POWER_OFF' 

44 

45_STATE_MAP = { 

46 vm_states.ACTIVE: { 

47 'default': 'ACTIVE', 

48 task_states.REBOOTING: 'REBOOT', 

49 task_states.REBOOT_PENDING: 'REBOOT', 

50 task_states.REBOOT_STARTED: 'REBOOT', 

51 task_states.REBOOTING_HARD: 'HARD_REBOOT', 

52 task_states.REBOOT_PENDING_HARD: 'HARD_REBOOT', 

53 task_states.REBOOT_STARTED_HARD: 'HARD_REBOOT', 

54 task_states.UPDATING_PASSWORD: 'PASSWORD', 

55 task_states.REBUILDING: 'REBUILD', 

56 task_states.REBUILD_BLOCK_DEVICE_MAPPING: 'REBUILD', 

57 task_states.REBUILD_SPAWNING: 'REBUILD', 

58 task_states.MIGRATING: 'MIGRATING', 

59 task_states.RESIZE_PREP: 'RESIZE', 

60 task_states.RESIZE_MIGRATING: 'RESIZE', 

61 task_states.RESIZE_MIGRATED: 'RESIZE', 

62 task_states.RESIZE_FINISH: 'RESIZE', 

63 }, 

64 vm_states.BUILDING: { 

65 'default': 'BUILD', 

66 }, 

67 vm_states.STOPPED: { 

68 'default': 'SHUTOFF', 

69 task_states.RESIZE_PREP: 'RESIZE', 

70 task_states.RESIZE_MIGRATING: 'RESIZE', 

71 task_states.RESIZE_MIGRATED: 'RESIZE', 

72 task_states.RESIZE_FINISH: 'RESIZE', 

73 task_states.REBUILDING: 'REBUILD', 

74 task_states.REBUILD_BLOCK_DEVICE_MAPPING: 'REBUILD', 

75 task_states.REBUILD_SPAWNING: 'REBUILD', 

76 }, 

77 vm_states.RESIZED: { 

78 'default': 'VERIFY_RESIZE', 

79 # Note(maoy): the OS API spec 1.1 doesn't have CONFIRMING_RESIZE 

80 # state so we comment that out for future reference only. 

81 # task_states.RESIZE_CONFIRMING: 'CONFIRMING_RESIZE', 

82 task_states.RESIZE_REVERTING: 'REVERT_RESIZE', 

83 }, 

84 vm_states.PAUSED: { 

85 'default': 'PAUSED', 

86 task_states.MIGRATING: 'MIGRATING', 

87 }, 

88 vm_states.SUSPENDED: { 

89 'default': 'SUSPENDED', 

90 }, 

91 vm_states.RESCUED: { 

92 'default': 'RESCUE', 

93 }, 

94 vm_states.ERROR: { 

95 'default': 'ERROR', 

96 task_states.REBUILDING: 'REBUILD', 

97 task_states.REBUILD_BLOCK_DEVICE_MAPPING: 'REBUILD', 

98 task_states.REBUILD_SPAWNING: 'REBUILD', 

99 }, 

100 vm_states.DELETED: { 

101 'default': 'DELETED', 

102 }, 

103 vm_states.SOFT_DELETED: { 

104 'default': 'SOFT_DELETED', 

105 }, 

106 vm_states.SHELVED: { 

107 'default': 'SHELVED', 

108 }, 

109 vm_states.SHELVED_OFFLOADED: { 

110 'default': 'SHELVED_OFFLOADED', 

111 }, 

112} 

113 

114 

115def status_from_state(vm_state, task_state='default'): 

116 """Given vm_state and task_state, return a status string.""" 

117 task_map = _STATE_MAP.get(vm_state, dict(default='UNKNOWN')) 

118 status = task_map.get(task_state, task_map['default']) 

119 if status == "UNKNOWN": 

120 LOG.error("status is UNKNOWN from vm_state=%(vm_state)s " 

121 "task_state=%(task_state)s. Bad upgrade or db " 

122 "corrupted?", 

123 {'vm_state': vm_state, 'task_state': task_state}) 

124 return status 

125 

126 

127def task_and_vm_state_from_status(statuses): 

128 """Map the server's multiple status strings to list of vm states and 

129 list of task states. 

130 """ 

131 vm_states = set() 

132 task_states = set() 

133 lower_statuses = [status.lower() for status in statuses] 

134 for state, task_map in _STATE_MAP.items(): 

135 for task_state, mapped_state in task_map.items(): 

136 status_string = mapped_state 

137 if status_string.lower() in lower_statuses: 

138 vm_states.add(state) 

139 task_states.add(task_state) 

140 # Add sort to avoid different order on set in Python 3 

141 return sorted(vm_states), sorted(task_states) 

142 

143 

144def get_sort_params(input_params, default_key='created_at', 

145 default_dir='desc'): 

146 """Retrieves sort keys/directions parameters. 

147 

148 Processes the parameters to create a list of sort keys and sort directions 

149 that correspond to the 'sort_key' and 'sort_dir' parameter values. These 

150 sorting parameters can be specified multiple times in order to generate 

151 the list of sort keys and directions. 

152 

153 The input parameters are not modified. 

154 

155 :param input_params: webob.multidict of request parameters (from 

156 nova.api.wsgi.Request.params) 

157 :param default_key: default sort key value, added to the list if no 

158 'sort_key' parameters are supplied 

159 :param default_dir: default sort dir value, added to the list if no 

160 'sort_dir' parameters are supplied 

161 :returns: list of sort keys, list of sort dirs 

162 """ 

163 params = input_params.copy() 

164 sort_keys = [] 

165 sort_dirs = [] 

166 while 'sort_key' in params: 

167 sort_keys.append(params.pop('sort_key').strip()) 

168 while 'sort_dir' in params: 

169 sort_dirs.append(params.pop('sort_dir').strip()) 

170 if len(sort_keys) == 0 and default_key: 

171 sort_keys.append(default_key) 

172 if len(sort_dirs) == 0 and default_dir: 

173 sort_dirs.append(default_dir) 

174 return sort_keys, sort_dirs 

175 

176 

177def get_pagination_params(request): 

178 """Return marker, limit tuple from request. 

179 

180 :param request: `wsgi.Request` possibly containing 'marker' and 'limit' 

181 GET variables. 'marker' is the id of the last element 

182 the client has seen, and 'limit' is the maximum number 

183 of items to return. If 'limit' is not specified, 0, or 

184 > max_limit, we default to max_limit. Negative values 

185 for either marker or limit will cause 

186 exc.HTTPBadRequest() exceptions to be raised. 

187 

188 """ 

189 params = {} 

190 if 'limit' in request.GET: 

191 params['limit'] = _get_int_param(request, 'limit') 

192 if 'page_size' in request.GET: 

193 params['page_size'] = _get_int_param(request, 'page_size') 

194 if 'marker' in request.GET: 

195 params['marker'] = _get_marker_param(request) 

196 if 'offset' in request.GET: 

197 params['offset'] = _get_int_param(request, 'offset') 

198 return params 

199 

200 

201def _get_int_param(request, param): 

202 """Extract integer param from request or fail.""" 

203 try: 

204 int_param = utils.validate_integer(request.GET[param], param, 

205 min_value=0) 

206 except exception.InvalidInput as e: 

207 raise webob.exc.HTTPBadRequest(explanation=e.format_message()) 

208 return int_param 

209 

210 

211def _get_marker_param(request): 

212 """Extract marker id from request or fail.""" 

213 return request.GET['marker'] 

214 

215 

216def limited(items, request): 

217 """Return a slice of items according to requested offset and limit. 

218 

219 :param items: A sliceable entity 

220 :param request: ``wsgi.Request`` possibly containing 'offset' and 'limit' 

221 GET variables. 'offset' is where to start in the list, 

222 and 'limit' is the maximum number of items to return. If 

223 'limit' is not specified, 0, or > max_limit, we default 

224 to max_limit. Negative values for either offset or limit 

225 will cause exc.HTTPBadRequest() exceptions to be raised. 

226 """ 

227 params = get_pagination_params(request) 

228 offset = params.get('offset', 0) 

229 limit = CONF.api.max_limit 

230 limit = min(limit, params.get('limit') or limit) 

231 

232 return items[offset:(offset + limit)] 

233 

234 

235def get_limit_and_marker(request): 

236 """Get limited parameter from request.""" 

237 params = get_pagination_params(request) 

238 limit = CONF.api.max_limit 

239 limit = min(limit, params.get('limit', limit)) 

240 marker = params.get('marker', None) 

241 

242 return limit, marker 

243 

244 

245def get_id_from_href(href): 

246 """Return the id or uuid portion of a url. 

247 

248 Given: 'http://www.foo.com/bar/123?q=4' 

249 Returns: '123' 

250 

251 Given: 'http://www.foo.com/bar/abc123?q=4' 

252 Returns: 'abc123' 

253 

254 """ 

255 return urlparse.urlsplit("%s" % href).path.split('/')[-1] 

256 

257 

258def remove_trailing_version_from_href(href): 

259 """Removes the api version from the href. 

260 

261 Given: 'http://www.nova.com/compute/v1.1' 

262 Returns: 'http://www.nova.com/compute' 

263 

264 Given: 'http://www.nova.com/v1.1' 

265 Returns: 'http://www.nova.com' 

266 

267 """ 

268 parsed_url = urlparse.urlsplit(href) 

269 url_parts = parsed_url.path.rsplit('/', 1) 

270 

271 # NOTE: this should match vX.X or vX 

272 expression = re.compile(r'^v([0-9]+|[0-9]+\.[0-9]+)(/.*|$)') 

273 if not expression.match(url_parts.pop()): 

274 LOG.debug('href %s does not contain version', href) 

275 raise ValueError(_('href %s does not contain version') % href) 

276 

277 new_path = url_join(*url_parts) 

278 parsed_url = list(parsed_url) 

279 parsed_url[2] = new_path 

280 return urlparse.urlunsplit(parsed_url) 

281 

282 

283def check_img_metadata_properties_quota(context, metadata): 

284 if not metadata: 

285 return 

286 try: 

287 QUOTAS.limit_check(context, metadata_items=len(metadata)) 

288 except exception.OverQuota: 

289 expl = _("Image metadata limit exceeded") 

290 raise webob.exc.HTTPForbidden(explanation=expl) 

291 

292 

293def get_networks_for_instance_from_nw_info(nw_info): 

294 networks = collections.OrderedDict() 

295 for vif in nw_info: 

296 ips = vif.fixed_ips() 

297 floaters = vif.floating_ips() 

298 label = vif['network']['label'] 

299 if label not in networks: 

300 networks[label] = {'ips': [], 'floating_ips': []} 

301 for ip in itertools.chain(ips, floaters): 

302 ip['mac_address'] = vif['address'] 

303 networks[label]['ips'].extend(ips) 

304 networks[label]['floating_ips'].extend(floaters) 

305 return networks 

306 

307 

308def get_networks_for_instance(context, instance): 

309 """Returns a prepared nw_info list for passing into the view builders 

310 

311 We end up with a data structure like:: 

312 

313 {'public': {'ips': [{'address': '10.0.0.1', 

314 'version': 4, 

315 'mac_address': 'aa:aa:aa:aa:aa:aa'}, 

316 {'address': '2001::1', 

317 'version': 6, 

318 'mac_address': 'aa:aa:aa:aa:aa:aa'}], 

319 'floating_ips': [{'address': '172.16.0.1', 

320 'version': 4, 

321 'mac_address': 'aa:aa:aa:aa:aa:aa'}, 

322 {'address': '172.16.2.1', 

323 'version': 4, 

324 'mac_address': 'aa:aa:aa:aa:aa:aa'}]}, 

325 ...} 

326 """ 

327 nw_info = instance.get_network_info() 

328 return get_networks_for_instance_from_nw_info(nw_info) 

329 

330 

331def raise_http_conflict_for_instance_invalid_state(exc, action, server_id): 

332 """Raises a webob.exc.HTTPConflict instance containing a message 

333 appropriate to return via the API based on the original 

334 InstanceInvalidState exception. 

335 """ 

336 attr = exc.kwargs.get('attr') 

337 state = exc.kwargs.get('state') 

338 if attr is not None and state is not None: 

339 msg = _("Cannot '%(action)s' instance %(server_id)s while it is in " 

340 "%(attr)s %(state)s") % {'action': action, 'attr': attr, 

341 'state': state, 

342 'server_id': server_id} 

343 else: 

344 # At least give some meaningful message 

345 msg = _("Instance %(server_id)s is in an invalid state for " 

346 "'%(action)s'") % {'action': action, 'server_id': server_id} 

347 raise webob.exc.HTTPConflict(explanation=msg) 

348 

349 

350def url_join(*parts): 

351 """Convenience method for joining parts of a URL 

352 

353 Any leading and trailing '/' characters are removed, and the parts joined 

354 together with '/' as a separator. If last element of 'parts' is an empty 

355 string, the returned URL will have a trailing slash. 

356 """ 

357 parts = parts or [""] 

358 clean_parts = [part.strip("/") for part in parts if part] 

359 if not parts[-1]: 

360 # Empty last element should add a trailing slash 

361 clean_parts.append("") 

362 return "/".join(clean_parts) 

363 

364 

365class ViewBuilder(object): 

366 """Model API responses as dictionaries.""" 

367 

368 def _get_project_id(self, request): 

369 """Get project id from request url if present or empty string 

370 otherwise 

371 """ 

372 project_id = request.environ["nova.context"].project_id 

373 if project_id and project_id in request.url: 

374 return project_id 

375 return '' 

376 

377 def _get_links(self, request, identifier, collection_name): 

378 return [{ 

379 "rel": "self", 

380 "href": self._get_href_link(request, identifier, collection_name), 

381 }, 

382 { 

383 "rel": "bookmark", 

384 "href": self._get_bookmark_link(request, 

385 identifier, 

386 collection_name), 

387 }] 

388 

389 def _get_next_link(self, request, identifier, collection_name): 

390 """Return href string with proper limit and marker params.""" 

391 params = collections.OrderedDict(sorted(request.params.items())) 

392 params["marker"] = identifier 

393 prefix = self._update_compute_link_prefix(request.application_url) 

394 url = url_join(prefix, 

395 self._get_project_id(request), 

396 collection_name) 

397 return "%s?%s" % (url, urlparse.urlencode(params)) 

398 

399 def _get_href_link(self, request, identifier, collection_name): 

400 """Return an href string pointing to this object.""" 

401 prefix = self._update_compute_link_prefix(request.application_url) 

402 return url_join(prefix, 

403 self._get_project_id(request), 

404 collection_name, 

405 str(identifier)) 

406 

407 def _get_bookmark_link(self, request, identifier, collection_name): 

408 """Create a URL that refers to a specific resource.""" 

409 base_url = remove_trailing_version_from_href(request.application_url) 

410 base_url = self._update_compute_link_prefix(base_url) 

411 return url_join(base_url, 

412 self._get_project_id(request), 

413 collection_name, 

414 str(identifier)) 

415 

416 def _get_collection_links(self, 

417 request, 

418 items, 

419 collection_name, 

420 id_key="uuid"): 

421 """Retrieve 'next' link, if applicable. This is included if: 

422 1) 'limit' param is specified and equals the number of items. 

423 2) 'limit' param is specified but it exceeds CONF.api.max_limit, 

424 in this case the number of items is CONF.api.max_limit. 

425 3) 'limit' param is NOT specified but the number of items is 

426 CONF.api.max_limit. 

427 """ 

428 links = [] 

429 max_items = min( 

430 int(request.params.get("limit", CONF.api.max_limit)), 

431 CONF.api.max_limit) 

432 if max_items and max_items == len(items): 

433 last_item = items[-1] 

434 if id_key in last_item: 

435 last_item_id = last_item[id_key] 

436 elif 'id' in last_item: 436 ↛ 439line 436 didn't jump to line 439 because the condition on line 436 was always true

437 last_item_id = last_item["id"] 

438 else: 

439 last_item_id = last_item["flavorid"] 

440 links.append({ 

441 "rel": "next", 

442 "href": self._get_next_link(request, 

443 last_item_id, 

444 collection_name), 

445 }) 

446 return links 

447 

448 def _update_link_prefix(self, orig_url, prefix): 

449 if not prefix: 

450 return orig_url 

451 url_parts = list(urlparse.urlsplit(orig_url)) 

452 prefix_parts = list(urlparse.urlsplit(prefix)) 

453 url_parts[0:2] = prefix_parts[0:2] 

454 url_parts[2] = prefix_parts[2] + url_parts[2] 

455 return urlparse.urlunsplit(url_parts).rstrip('/') 

456 

457 def _update_glance_link_prefix(self, orig_url): 

458 return self._update_link_prefix(orig_url, CONF.api.glance_link_prefix) 

459 

460 def _update_compute_link_prefix(self, orig_url): 

461 return self._update_link_prefix(orig_url, CONF.api.compute_link_prefix) 

462 

463 

464def get_instance(compute_api, context, instance_id, expected_attrs=None, 

465 cell_down_support=False): 

466 """Fetch an instance from the compute API, handling error checking.""" 

467 try: 

468 return compute_api.get(context, instance_id, 

469 expected_attrs=expected_attrs, 

470 cell_down_support=cell_down_support) 

471 except exception.InstanceNotFound as e: 

472 raise exc.HTTPNotFound(explanation=e.format_message()) 

473 

474 

475def normalize_name(name): 

476 # NOTE(alex_xu): This method is used by v2.1 legacy v2 compat mode. 

477 # In the legacy v2 API, some of APIs strip the spaces and some of APIs not. 

478 # The v2.1 disallow leading/trailing, for compatible v2 API and consistent, 

479 # we enable leading/trailing spaces and strip spaces in legacy v2 compat 

480 # mode. Althrough in legacy v2 API there are some APIs didn't strip spaces, 

481 # but actually leading/trailing spaces(that means user depend on leading/ 

482 # trailing spaces distinguish different instance) is pointless usecase. 

483 return name.strip() 

484 

485 

486def raise_feature_not_supported(msg=None): 

487 if msg is None: 

488 msg = _("The requested functionality is not supported.") 

489 raise webob.exc.HTTPNotImplemented(explanation=msg) 

490 

491 

492def get_flavor(context, flavor_id): 

493 try: 

494 return objects.Flavor.get_by_flavor_id(context, flavor_id) 

495 except exception.FlavorNotFound as error: 

496 raise exc.HTTPNotFound(explanation=error.format_message()) 

497 

498 

499def is_all_tenants(search_opts): 

500 """Checks to see if the all_tenants flag is in search_opts 

501 

502 :param dict search_opts: The search options for a request 

503 :returns: boolean indicating if all_tenants are being requested or not 

504 """ 

505 all_tenants = search_opts.get('all_tenants') 

506 if all_tenants: 

507 try: 

508 all_tenants = strutils.bool_from_string(all_tenants, True) 

509 except ValueError as err: 

510 raise exception.InvalidInput(str(err)) 

511 else: 

512 # The empty string is considered enabling all_tenants 

513 all_tenants = 'all_tenants' in search_opts 

514 return all_tenants 

515 

516 

517def is_locked(search_opts): 

518 """Converts the value of the locked parameter to a boolean. Note that 

519 this function will be called only if locked exists in search_opts. 

520 

521 :param dict search_opts: The search options for a request 

522 :returns: boolean indicating if locked is being requested or not 

523 """ 

524 locked = search_opts.get('locked') 

525 try: 

526 locked = strutils.bool_from_string(locked, strict=True) 

527 except ValueError as err: 

528 raise exception.InvalidInput(str(err)) 

529 return locked 

530 

531 

532def supports_multiattach_volume(req): 

533 """Check to see if the requested API version is high enough for multiattach 

534 

535 Microversion 2.60 adds support for booting from a multiattach volume. 

536 The actual validation for a multiattach volume is done in the compute 

537 API code, this is just checking the version so we can tell the API 

538 code if the request version is high enough to even support it. 

539 

540 :param req: The incoming API request 

541 :returns: True if the requested API microversion is high enough for 

542 volume multiattach support, False otherwise. 

543 """ 

544 return api_version_request.is_supported(req, '2.60') 

545 

546 

547def supports_port_resource_request(req): 

548 """Check to see if the requested API version is high enough for resource 

549 request 

550 

551 :param req: The incoming API request 

552 :returns: True if the requested API microversion is high enough for 

553 port resource request support, False otherwise. 

554 """ 

555 return api_version_request.is_supported(req, '2.72')