Coverage for nova/volume/cinder.py: 90%

438 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-17 15:08 +0000

1# Copyright 2010 United States Government as represented by the 

2# Administrator of the National Aeronautics and Space Administration. 

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 

17""" 

18Handles all requests relating to volumes + cinder. 

19""" 

20 

21import collections 

22import copy 

23import functools 

24import sys 

25import urllib 

26 

27from cinderclient import api_versions as cinder_api_versions 

28from cinderclient import client as cinder_client 

29from cinderclient import exceptions as cinder_exception 

30from keystoneauth1 import exceptions as keystone_exception 

31from keystoneauth1 import loading as ks_loading 

32from oslo_log import log as logging 

33from oslo_serialization import jsonutils 

34from oslo_utils import excutils 

35from oslo_utils import strutils 

36import retrying 

37 

38from nova import availability_zones as az 

39import nova.conf 

40from nova import exception 

41from nova.i18n import _ 

42from nova import service_auth 

43 

44 

45CONF = nova.conf.CONF 

46 

47LOG = logging.getLogger(__name__) 

48 

49_ADMIN_AUTH = None 

50_SESSION = None 

51 

52 

53def reset_globals(): 

54 """Testing method to reset globals. 

55 """ 

56 global _ADMIN_AUTH 

57 global _SESSION 

58 

59 _ADMIN_AUTH = None 

60 _SESSION = None 

61 

62 

63def _load_auth_plugin(conf): 

64 auth_plugin = ks_loading.load_auth_from_conf_options(conf, 

65 nova.conf.cinder.cinder_group.name) 

66 

67 if auth_plugin: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true

68 return auth_plugin 

69 

70 if conf.cinder.auth_type is None: 70 ↛ 74line 70 didn't jump to line 74 because the condition on line 70 was always true

71 LOG.error('The [cinder] section of your nova configuration file ' 

72 'must be configured for authentication with the ' 

73 'block-storage service endpoint.') 

74 err_msg = _('Unknown auth type: %s') % conf.cinder.auth_type 

75 raise cinder_exception.Unauthorized(401, message=err_msg) 

76 

77 

78def _load_session(): 

79 global _SESSION 

80 

81 if not _SESSION: 

82 _SESSION = ks_loading.load_session_from_conf_options( 

83 CONF, nova.conf.cinder.cinder_group.name) 

84 

85 

86def _get_auth(context): 

87 global _ADMIN_AUTH 

88 # NOTE(lixipeng): Auth token is none when call 

89 # cinder API from compute periodic tasks, context 

90 # from them generated from 'context.get_admin_context' 

91 # which only set is_admin=True but is without token. 

92 # So add load_auth_plugin when this condition appear. 

93 user_auth = None 

94 if context.is_admin and not context.auth_token: 

95 if not _ADMIN_AUTH: 

96 _ADMIN_AUTH = _load_auth_plugin(CONF) 

97 user_auth = _ADMIN_AUTH 

98 

99 # When user_auth = None, user_auth will be extracted from the context. 

100 return service_auth.get_auth_plugin(context, user_auth=user_auth) 

101 

102 

103# NOTE(efried): Bug #1752152 

104# This method is copied/adapted from cinderclient.client.get_server_version so 

105# we can use _SESSION.get rather than a raw requests.get to retrieve the 

106# version document. This enables HTTPS by gleaning cert info from the session 

107# config. 

108def _get_server_version(context, url): 

109 """Queries the server via the naked endpoint and gets version info. 

110 

111 :param context: The nova request context for auth. 

112 :param url: url of the cinder endpoint 

113 :returns: APIVersion object for min and max version supported by 

114 the server 

115 """ 

116 min_version = "2.0" 

117 current_version = "2.0" 

118 

119 _load_session() 

120 auth = _get_auth(context) 

121 

122 try: 

123 u = urllib.parse.urlparse(url) 

124 version_url = None 

125 

126 # NOTE(andreykurilin): endpoint URL has at least 2 formats: 

127 # 1. The classic (legacy) endpoint: 

128 # http://{host}:{optional_port}/v{2 or 3}/{project-id} 

129 # http://{host}:{optional_port}/v{2 or 3} 

130 # 3. Under wsgi: 

131 # http://{host}:{optional_port}/volume/v{2 or 3} 

132 for ver in ['v2', 'v3']: 

133 if u.path.endswith(ver) or "/{0}/".format(ver) in u.path: 

134 path = u.path[:u.path.rfind(ver)] 

135 version_url = '%s://%s%s' % (u.scheme, u.netloc, path) 

136 break 

137 

138 if not version_url: 

139 # NOTE(andreykurilin): probably, it is one of the next cases: 

140 # * https://volume.example.com/ 

141 # * https://example.com/volume 

142 # leave as is without cropping. 

143 version_url = url 

144 

145 response = _SESSION.get(version_url, auth=auth) 

146 data = jsonutils.loads(response.text) 

147 versions = data['versions'] 

148 for version in versions: 

149 if '3.' in version['version']: 

150 min_version = version['min_version'] 

151 current_version = version['version'] 

152 break 

153 except cinder_exception.ClientException as e: 

154 LOG.warning("Error in server version query:%s\n" 

155 "Returning APIVersion 2.0", str(e.message)) 

156 return (cinder_api_versions.APIVersion(min_version), 

157 cinder_api_versions.APIVersion(current_version)) 

158 

159 

160# NOTE(efried): Bug #1752152 

161# This method is copied/adapted from 

162# cinderclient.client.get_highest_client_server_version. See note on 

163# _get_server_version. 

164def _get_highest_client_server_version(context, url): 

165 """Returns highest APIVersion supported version by client and server.""" 

166 min_server, max_server = _get_server_version(context, url) 

167 max_client = cinder_api_versions.APIVersion( 

168 cinder_api_versions.MAX_VERSION) 

169 return min(max_server, max_client) 

170 

171 

172def _check_microversion(context, url, microversion): 

173 """Checks to see if the requested microversion is supported by the current 

174 version of python-cinderclient and the volume API endpoint. 

175 

176 :param context: The nova request context for auth. 

177 :param url: Cinder API endpoint URL. 

178 :param microversion: Requested microversion. If not available at the given 

179 API endpoint URL, a CinderAPIVersionNotAvailable exception is raised. 

180 :returns: The microversion if it is available. This can be used to 

181 construct the cinder v3 client object. 

182 :raises: CinderAPIVersionNotAvailable if the microversion is not available. 

183 """ 

184 max_api_version = _get_highest_client_server_version(context, url) 

185 # Check if the max_api_version matches the requested minimum microversion. 

186 if max_api_version.matches(microversion): 

187 # The requested microversion is supported by the client and the server. 

188 return microversion 

189 raise exception.CinderAPIVersionNotAvailable(version=microversion) 

190 

191 

192def _get_cinderclient_parameters(context): 

193 _load_session() 

194 

195 auth = _get_auth(context) 

196 

197 url = None 

198 

199 service_type, service_name, interface = CONF.cinder.catalog_info.split(':') 

200 

201 service_parameters = {'service_type': service_type, 

202 'interface': interface, 

203 'region_name': CONF.cinder.os_region_name} 

204 # Only include the service_name if it's provided. 

205 if service_name: 

206 service_parameters['service_name'] = service_name 

207 

208 if CONF.cinder.endpoint_template: 

209 url = CONF.cinder.endpoint_template % context.to_dict() 

210 else: 

211 url = _SESSION.get_endpoint(auth, **service_parameters) 

212 

213 return auth, service_parameters, url 

214 

215 

216def is_microversion_supported(context, microversion): 

217 # NOTE(efried): Work around bug #1752152. Call the cinderclient() builder 

218 # in a way that just does a microversion check. 

219 cinderclient(context, microversion=microversion, check_only=True) 

220 

221 

222def cinderclient(context, microversion=None, skip_version_check=False, 

223 check_only=False): 

224 """Constructs a cinder client object for making API requests. 

225 

226 :param context: The nova request context for auth. 

227 :param microversion: Optional microversion to check against the client. 

228 This implies that Cinder v3 is required for any calls that require a 

229 microversion. If the microversion is not available, this method will 

230 raise an CinderAPIVersionNotAvailable exception. 

231 :param skip_version_check: If True and a specific microversion is 

232 requested, the version discovery check is skipped and the microversion 

233 is used directly. This should only be used if a previous check for the 

234 same microversion was successful. 

235 :param check_only: If True, don't build the actual client; just do the 

236 setup and version checking. 

237 :raises: UnsupportedCinderAPIVersion if a major version other than 3 is 

238 requested. 

239 :raises: CinderAPIVersionNotAvailable if microversion checking is requested 

240 and the specified microversion is higher than what the service can 

241 handle. 

242 :returns: A cinderclient.client.Client wrapper, unless check_only is False. 

243 """ 

244 

245 endpoint_override = None 

246 auth, service_parameters, url = _get_cinderclient_parameters(context) 

247 

248 if CONF.cinder.endpoint_template: 

249 endpoint_override = url 

250 

251 # TODO(jamielennox): This should be using proper version discovery from 

252 # the cinder service rather than just inspecting the URL for certain string 

253 # values. 

254 version = cinder_client.get_volume_api_from_url(url) 

255 

256 if version != '3': 

257 raise exception.UnsupportedCinderAPIVersion(version=version) 

258 

259 version = '3.0' 

260 # Check to see a specific microversion is requested and if so, can it 

261 # be handled by the backing server. 

262 if microversion is not None: 

263 if skip_version_check: 

264 version = microversion 

265 else: 

266 version = _check_microversion(context, url, microversion) 

267 

268 if check_only: 268 ↛ 269line 268 didn't jump to line 269 because the condition on line 268 was never true

269 return 

270 

271 return cinder_client.Client(version, 

272 session=_SESSION, 

273 auth=auth, 

274 endpoint_override=endpoint_override, 

275 connect_retries=CONF.cinder.http_retries, 

276 global_request_id=context.global_id, 

277 **service_parameters) 

278 

279 

280def _untranslate_volume_summary_view(context, vol): 

281 """Maps keys for volumes summary view.""" 

282 d = {} 

283 d['id'] = vol.id 

284 d['status'] = vol.status 

285 d['size'] = vol.size 

286 d['availability_zone'] = vol.availability_zone 

287 d['created_at'] = vol.created_at 

288 

289 # TODO(jdg): The calling code expects attach_time and 

290 # mountpoint to be set. When the calling 

291 # code is more defensive this can be 

292 # removed. 

293 d['attach_time'] = "" 

294 d['mountpoint'] = "" 

295 d['multiattach'] = getattr(vol, 'multiattach', False) 

296 

297 if vol.attachments: 

298 d['attachments'] = collections.OrderedDict() 

299 for attachment in vol.attachments: 

300 a = {attachment['server_id']: 

301 {'attachment_id': attachment.get('attachment_id'), 

302 'mountpoint': attachment.get('device')} 

303 } 

304 d['attachments'].update(a.items()) 

305 

306 d['attach_status'] = 'attached' 

307 else: 

308 d['attach_status'] = 'detached' 

309 d['display_name'] = vol.name 

310 d['display_description'] = vol.description 

311 # TODO(jdg): Information may be lost in this translation 

312 d['volume_type_id'] = vol.volume_type 

313 d['snapshot_id'] = vol.snapshot_id 

314 d['bootable'] = strutils.bool_from_string(vol.bootable) 

315 d['volume_metadata'] = {} 

316 for key, value in vol.metadata.items(): 316 ↛ 317line 316 didn't jump to line 317 because the loop on line 316 never started

317 d['volume_metadata'][key] = value 

318 

319 if hasattr(vol, 'volume_image_metadata'): 

320 d['volume_image_metadata'] = copy.deepcopy(vol.volume_image_metadata) 

321 

322 # The 3.48 microversion exposes a shared_targets boolean and service_uuid 

323 # string parameter which can be used with locks during volume attach 

324 # and detach. 

325 if hasattr(vol, 'shared_targets'): 

326 d['shared_targets'] = vol.shared_targets 

327 d['service_uuid'] = vol.service_uuid 

328 

329 if hasattr(vol, 'migration_status'): 

330 d['migration_status'] = vol.migration_status 

331 

332 return d 

333 

334 

335def _untranslate_volume_type_view(volume_type): 

336 """Maps keys for volume type view.""" 

337 v = {} 

338 

339 v['id'] = volume_type.id 

340 v['name'] = volume_type.name 

341 

342 return v 

343 

344 

345def _untranslate_snapshot_summary_view(context, snapshot): 

346 """Maps keys for snapshots summary view.""" 

347 d = {} 

348 

349 d['id'] = snapshot.id 

350 d['status'] = snapshot.status 

351 d['progress'] = snapshot.progress 

352 d['size'] = snapshot.size 

353 d['created_at'] = snapshot.created_at 

354 d['display_name'] = snapshot.name 

355 d['display_description'] = snapshot.description 

356 

357 d['volume_id'] = snapshot.volume_id 

358 d['project_id'] = snapshot.project_id 

359 d['volume_size'] = snapshot.size 

360 

361 return d 

362 

363 

364def _translate_attachment_ref(attachment_ref): 

365 """Building old style connection_info by adding the 'data' key back.""" 

366 translated_con_info = {} 

367 connection_info_data = attachment_ref.pop('connection_info', None) 

368 if connection_info_data: 

369 connection_info_data.pop('attachment_id', None) 

370 translated_con_info['driver_volume_type'] = \ 

371 connection_info_data.pop('driver_volume_type', None) 

372 translated_con_info['data'] = connection_info_data 

373 translated_con_info['status'] = attachment_ref.pop('status', None) 

374 translated_con_info['instance'] = attachment_ref.pop('instance', None) 

375 translated_con_info['attached_at'] = attachment_ref.pop('attached_at', 

376 None) 

377 translated_con_info['detached_at'] = attachment_ref.pop('detached_at', 

378 None) 

379 

380 # Now the catch all... 

381 for k, v in attachment_ref.items(): 

382 # Keep these as top-level fields on the attachment record. 

383 if k not in ("id", "attach_mode"): 

384 translated_con_info[k] = v 

385 

386 attachment_ref['connection_info'] = translated_con_info 

387 

388 return attachment_ref 

389 

390 

391def translate_cinder_exception(method): 

392 """Transforms a cinder exception but keeps its traceback intact.""" 

393 @functools.wraps(method) 

394 def wrapper(self, ctx, *args, **kwargs): 

395 try: 

396 res = method(self, ctx, *args, **kwargs) 

397 except (cinder_exception.ConnectionError, 

398 keystone_exception.ConnectionError) as exc: 

399 err_msg = str(exc) 

400 _reraise(exception.CinderConnectionFailed(reason=err_msg)) 

401 except (keystone_exception.BadRequest, 

402 cinder_exception.BadRequest) as exc: 

403 err_msg = str(exc) 

404 _reraise(exception.InvalidInput(reason=err_msg)) 

405 except (keystone_exception.Forbidden, 

406 cinder_exception.Forbidden) as exc: 

407 err_msg = str(exc) 

408 _reraise(exception.Forbidden(err_msg)) 

409 return res 

410 return wrapper 

411 

412 

413def translate_create_exception(method): 

414 """Transforms the exception for create but keeps its traceback intact. 

415 """ 

416 

417 def wrapper(self, ctx, size, *args, **kwargs): 

418 try: 

419 res = method(self, ctx, size, *args, **kwargs) 

420 except (keystone_exception.NotFound, cinder_exception.NotFound) as e: 

421 _reraise(exception.NotFound(message=e.message)) 

422 except cinder_exception.OverLimit as e: 

423 _reraise(exception.OverQuota(message=e.message)) 

424 return res 

425 return translate_cinder_exception(wrapper) 

426 

427 

428def translate_volume_exception(method): 

429 """Transforms the exception for the volume but keeps its traceback intact. 

430 """ 

431 

432 def wrapper(self, ctx, volume_id, *args, **kwargs): 

433 try: 

434 res = method(self, ctx, volume_id, *args, **kwargs) 

435 except (keystone_exception.NotFound, cinder_exception.NotFound): 

436 _reraise(exception.VolumeNotFound(volume_id=volume_id)) 

437 except cinder_exception.OverLimit as e: 

438 _reraise(exception.OverQuota(message=e.message)) 

439 return res 

440 return translate_cinder_exception(wrapper) 

441 

442 

443def translate_attachment_exception(method): 

444 """Transforms the exception for the attachment but keeps its traceback 

445 intact. 

446 """ 

447 

448 def wrapper(self, ctx, attachment_id, *args, **kwargs): 

449 try: 

450 res = method(self, ctx, attachment_id, *args, **kwargs) 

451 except (keystone_exception.NotFound, cinder_exception.NotFound): 

452 _reraise(exception.VolumeAttachmentNotFound( 

453 attachment_id=attachment_id)) 

454 return res 

455 return translate_cinder_exception(wrapper) 

456 

457 

458def translate_snapshot_exception(method): 

459 """Transforms the exception for the snapshot but keeps its traceback 

460 intact. 

461 """ 

462 

463 def wrapper(self, ctx, snapshot_id, *args, **kwargs): 

464 try: 

465 res = method(self, ctx, snapshot_id, *args, **kwargs) 

466 except (keystone_exception.NotFound, cinder_exception.NotFound): 

467 _reraise(exception.SnapshotNotFound(snapshot_id=snapshot_id)) 

468 return res 

469 return translate_cinder_exception(wrapper) 

470 

471 

472def translate_mixed_exceptions(method): 

473 """Transforms exceptions that can come from both volumes and snapshots.""" 

474 

475 def wrapper(self, ctx, res_id, *args, **kwargs): 

476 try: 

477 res = method(self, ctx, res_id, *args, **kwargs) 

478 except (keystone_exception.NotFound, cinder_exception.NotFound): 

479 _reraise(exception.VolumeNotFound(volume_id=res_id)) 

480 except cinder_exception.OverLimit: 

481 _reraise(exception.OverQuota(overs='snapshots')) 

482 return res 

483 return translate_cinder_exception(wrapper) 

484 

485 

486def _reraise(desired_exc): 

487 raise desired_exc.with_traceback(sys.exc_info()[2]) 

488 

489 

490class API(object): 

491 """API for interacting with the volume manager.""" 

492 

493 @translate_volume_exception 

494 def get(self, context, volume_id, microversion=None): 

495 """Get the details about a volume given it's ID. 

496 

497 :param context: the nova request context 

498 :param volume_id: the id of the volume to get 

499 :param microversion: optional string microversion value 

500 :raises: CinderAPIVersionNotAvailable if the specified microversion is 

501 not available. 

502 """ 

503 item = cinderclient( 

504 context, microversion=microversion).volumes.get(volume_id) 

505 return _untranslate_volume_summary_view(context, item) 

506 

507 @translate_cinder_exception 

508 def get_all(self, context, search_opts=None): 

509 search_opts = search_opts or {} 

510 items = cinderclient(context).volumes.list(detailed=True, 

511 search_opts=search_opts) 

512 

513 rval = [] 

514 

515 for item in items: 

516 rval.append(_untranslate_volume_summary_view(context, item)) 

517 

518 return rval 

519 

520 def check_attached(self, context, volume): 

521 if volume['status'] != "in-use": 

522 msg = _("volume '%(vol)s' status must be 'in-use'. Currently in " 

523 "'%(status)s' status") % {"vol": volume['id'], 

524 "status": volume['status']} 

525 raise exception.InvalidVolume(reason=msg) 

526 

527 def check_availability_zone(self, context, volume, instance=None): 

528 """Ensure that the availability zone is the same. 

529 

530 :param context: the nova request context 

531 :param volume: the volume attached to the instance 

532 :param instance: nova.objects.instance.Instance object 

533 :raises: InvalidVolume if the instance availability zone does not 

534 equal the volume's availability zone 

535 """ 

536 

537 # TODO(walter-boring): move this check to Cinder as part of 

538 # the reserve call. 

539 if instance and not CONF.cinder.cross_az_attach: 

540 instance_az = az.get_instance_availability_zone(context, instance) 

541 if instance_az != volume['availability_zone']: 541 ↛ exitline 541 didn't return from function 'check_availability_zone' because the condition on line 541 was always true

542 msg = _("Instance %(instance)s and volume %(vol)s are not in " 

543 "the same availability_zone. Instance is in " 

544 "%(ins_zone)s. Volume is in %(vol_zone)s") % { 

545 "instance": instance.uuid, 

546 "vol": volume['id'], 

547 'ins_zone': instance_az, 

548 'vol_zone': volume['availability_zone']} 

549 raise exception.InvalidVolume(reason=msg) 

550 

551 @translate_volume_exception 

552 def reserve_volume(self, context, volume_id): 

553 cinderclient(context).volumes.reserve(volume_id) 

554 

555 @translate_volume_exception 

556 def unreserve_volume(self, context, volume_id): 

557 cinderclient(context).volumes.unreserve(volume_id) 

558 

559 @translate_volume_exception 

560 def begin_detaching(self, context, volume_id): 

561 cinderclient(context).volumes.begin_detaching(volume_id) 

562 

563 @translate_volume_exception 

564 def roll_detaching(self, context, volume_id): 

565 cinderclient(context).volumes.roll_detaching(volume_id) 

566 

567 @translate_volume_exception 

568 def attach(self, context, volume_id, instance_uuid, mountpoint, mode='rw'): 

569 cinderclient(context).volumes.attach(volume_id, instance_uuid, 

570 mountpoint, mode=mode) 

571 

572 @translate_volume_exception 

573 @retrying.retry(stop_max_attempt_number=5, 

574 retry_on_exception=lambda e: 

575 (isinstance(e, cinder_exception.ClientException) and 

576 e.code == 500)) 

577 def detach(self, context, volume_id, instance_uuid=None, 

578 attachment_id=None): 

579 client = cinderclient(context) 

580 if attachment_id is None: 

581 volume = self.get(context, volume_id) 

582 if volume['multiattach']: 

583 attachments = volume.get('attachments', {}) 

584 if instance_uuid: 584 ↛ 598line 584 didn't jump to line 598 because the condition on line 584 was always true

585 attachment_id = attachments.get(instance_uuid, {}).\ 

586 get('attachment_id') 

587 if not attachment_id: 

588 LOG.warning("attachment_id couldn't be retrieved " 

589 "for volume %(volume_id)s with " 

590 "instance_uuid %(instance_id)s. The " 

591 "volume has the 'multiattach' flag " 

592 "enabled, without the attachment_id " 

593 "Cinder most probably cannot perform " 

594 "the detach.", 

595 {'volume_id': volume_id, 

596 'instance_id': instance_uuid}) 

597 else: 

598 LOG.warning("attachment_id couldn't be retrieved for " 

599 "volume %(volume_id)s. The volume has the " 

600 "'multiattach' flag enabled, without the " 

601 "attachment_id Cinder most probably " 

602 "cannot perform the detach.", 

603 {'volume_id': volume_id}) 

604 

605 client.volumes.detach(volume_id, attachment_id) 

606 

607 @translate_volume_exception 

608 def initialize_connection(self, context, volume_id, connector): 

609 try: 

610 connection_info = cinderclient( 

611 context).volumes.initialize_connection(volume_id, connector) 

612 connection_info['connector'] = connector 

613 return connection_info 

614 except cinder_exception.ClientException as ex: 

615 with excutils.save_and_reraise_exception(): 

616 LOG.error( 

617 'Initialize connection failed for volume %(vol)s on host ' 

618 '%(host)s. Error: %(msg)s Code: %(code)s. ' 

619 'Attempting to terminate connection.', 

620 {'vol': volume_id, 

621 'host': connector.get('host'), 

622 'msg': str(ex), 

623 'code': ex.code}) 

624 try: 

625 self.terminate_connection(context, volume_id, connector) 

626 except Exception as exc: 

627 LOG.error( 

628 'Connection between volume %(vol)s and host %(host)s ' 

629 'might have succeeded, but attempt to terminate ' 

630 'connection has failed. Validate the connection and ' 

631 'determine if manual cleanup is needed. ' 

632 'Error: %(msg)s Code: %(code)s.', 

633 {'vol': volume_id, 

634 'host': connector.get('host'), 

635 'msg': str(exc), 

636 'code': exc.code if hasattr(exc, 'code') else None}) 

637 

638 @translate_volume_exception 

639 @retrying.retry(stop_max_attempt_number=5, 

640 retry_on_exception=lambda e: 

641 (isinstance(e, cinder_exception.ClientException) and 

642 e.code == 500)) 

643 def terminate_connection(self, context, volume_id, connector): 

644 return cinderclient(context).volumes.terminate_connection(volume_id, 

645 connector) 

646 

647 @translate_cinder_exception 

648 def migrate_volume_completion(self, context, old_volume_id, new_volume_id, 

649 error=False): 

650 return cinderclient(context).volumes.migrate_volume_completion( 

651 old_volume_id, new_volume_id, error) 

652 

653 @translate_create_exception 

654 def create(self, context, size, name, description, snapshot=None, 

655 image_id=None, volume_type=None, metadata=None, 

656 availability_zone=None): 

657 client = cinderclient(context) 

658 

659 if snapshot is not None: 659 ↛ 660line 659 didn't jump to line 660 because the condition on line 659 was never true

660 snapshot_id = snapshot['id'] 

661 else: 

662 snapshot_id = None 

663 

664 kwargs = dict(snapshot_id=snapshot_id, 

665 volume_type=volume_type, 

666 availability_zone=availability_zone, 

667 metadata=metadata, 

668 imageRef=image_id, 

669 name=name, 

670 description=description) 

671 

672 item = client.volumes.create(size, **kwargs) 

673 return _untranslate_volume_summary_view(context, item) 

674 

675 @translate_volume_exception 

676 def delete(self, context, volume_id): 

677 cinderclient(context).volumes.delete(volume_id) 

678 

679 @translate_volume_exception 

680 def update(self, context, volume_id, fields): 

681 raise NotImplementedError() 

682 

683 @translate_cinder_exception 

684 def get_absolute_limits(self, context): 

685 """Returns quota limit and usage information for the given tenant 

686 

687 See the <volumev3>/v3/{project_id}/limits API reference for details. 

688 

689 :param context: The nova RequestContext for the user request. Note 

690 that the limit information returned from Cinder is specific to 

691 the project_id within this context. 

692 :returns: dict of absolute limits 

693 """ 

694 # cinderclient returns a generator of AbsoluteLimit objects, so iterate 

695 # over the generator and return a dictionary which is easier for the 

696 # nova client-side code to handle. 

697 limits = cinderclient(context).limits.get().absolute 

698 return {limit.name: limit.value for limit in limits} 

699 

700 @translate_snapshot_exception 

701 def get_snapshot(self, context, snapshot_id): 

702 item = cinderclient(context).volume_snapshots.get(snapshot_id) 

703 return _untranslate_snapshot_summary_view(context, item) 

704 

705 @translate_cinder_exception 

706 def get_all_snapshots(self, context): 

707 items = cinderclient(context).volume_snapshots.list(detailed=True) 

708 rvals = [] 

709 

710 for item in items: 

711 rvals.append(_untranslate_snapshot_summary_view(context, item)) 

712 

713 return rvals 

714 

715 @translate_mixed_exceptions 

716 def create_snapshot(self, context, volume_id, name, description): 

717 item = cinderclient(context).volume_snapshots.create(volume_id, 

718 False, 

719 name, 

720 description) 

721 return _untranslate_snapshot_summary_view(context, item) 

722 

723 @translate_mixed_exceptions 

724 def create_snapshot_force(self, context, volume_id, name, description): 

725 item = cinderclient(context).volume_snapshots.create(volume_id, 

726 True, 

727 name, 

728 description) 

729 

730 return _untranslate_snapshot_summary_view(context, item) 

731 

732 @translate_snapshot_exception 

733 def delete_snapshot(self, context, snapshot_id): 

734 cinderclient(context).volume_snapshots.delete(snapshot_id) 

735 

736 @translate_cinder_exception 

737 def get_all_volume_types(self, context): 

738 items = cinderclient(context).volume_types.list() 

739 rvals = [] 

740 

741 for item in items: 

742 rvals.append(_untranslate_volume_type_view(item)) 

743 

744 return rvals 

745 

746 @translate_cinder_exception 

747 def get_volume_encryption_metadata(self, context, volume_id): 

748 return cinderclient(context).volumes.get_encryption_metadata(volume_id) 

749 

750 @translate_snapshot_exception 

751 def update_snapshot_status(self, context, snapshot_id, status): 

752 vs = cinderclient(context).volume_snapshots 

753 

754 # '90%' here is used to tell Cinder that Nova is done 

755 # with its portion of the 'creating' state. This can 

756 # be removed when we are able to split the Cinder states 

757 # into 'creating' and a separate state of 

758 # 'creating_in_nova'. (Same for 'deleting' state.) 

759 

760 vs.update_snapshot_status( 

761 snapshot_id, 

762 {'status': status, 

763 'progress': '90%'} 

764 ) 

765 

766 @translate_volume_exception 

767 def attachment_create(self, context, volume_id, instance_id, 

768 connector=None, mountpoint=None): 

769 """Create a volume attachment. This requires microversion >= 3.44. 

770 

771 The attachment_create call was introduced in microversion 3.27. We 

772 need 3.44 as minimum here as we need attachment_complete to finish the 

773 attaching process and it which was introduced in version 3.44. 

774 

775 :param context: The nova request context. 

776 :param volume_id: UUID of the volume on which to create the attachment. 

777 :param instance_id: UUID of the instance to which the volume will be 

778 attached. 

779 :param connector: host connector dict; if None, the attachment will 

780 be 'reserved' but not yet attached. 

781 :param mountpoint: Optional mount device name for the attachment, 

782 e.g. "/dev/vdb". This is only used if a connector is provided. 

783 :returns: a dict created from the 

784 cinderclient.v3.attachments.VolumeAttachment object with a backward 

785 compatible connection_info dict 

786 """ 

787 # NOTE(mriedem): Due to a limitation in the POST /attachments/ 

788 # API in Cinder, we have to pass the mountpoint in via the 

789 # host connector rather than pass it in as a top-level parameter 

790 # like in the os-attach volume action API. Hopefully this will be 

791 # fixed some day with a new Cinder microversion but until then we 

792 # work around it client-side. 

793 _connector = connector 

794 if _connector and mountpoint and 'mountpoint' not in _connector: 

795 # Make a copy of the connector so we don't modify it by 

796 # reference. 

797 _connector = copy.deepcopy(connector) 

798 _connector['mountpoint'] = mountpoint 

799 

800 try: 

801 attachment_ref = cinderclient(context, '3.44').attachments.create( 

802 volume_id, _connector, instance_id) 

803 return _translate_attachment_ref(attachment_ref) 

804 except cinder_exception.ClientException as ex: 

805 with excutils.save_and_reraise_exception(): 

806 # NOTE: It is unnecessary to output BadRequest(400) error log, 

807 # because operators don't need to debug such cases. 

808 if getattr(ex, 'code', None) != 400: 808 ↛ exitline 808 didn't jump to the function exit

809 LOG.error('Create attachment failed for volume ' 

810 '%(volume_id)s. Error: %(msg)s Code: %(code)s', 

811 {'volume_id': volume_id, 

812 'msg': str(ex), 

813 'code': getattr(ex, 'code', None)}, 

814 instance_uuid=instance_id) 

815 

816 @translate_attachment_exception 

817 def attachment_get(self, context, attachment_id): 

818 """Gets a volume attachment. 

819 

820 :param context: The nova request context. 

821 :param attachment_id: UUID of the volume attachment to get. 

822 :returns: a dict created from the 

823 cinderclient.v3.attachments.VolumeAttachment object with a backward 

824 compatible connection_info dict 

825 """ 

826 try: 

827 attachment_ref = cinderclient( 

828 context, '3.44', skip_version_check=True).attachments.show( 

829 attachment_id) 

830 translated_attach_ref = _translate_attachment_ref( 

831 attachment_ref.to_dict()) 

832 return translated_attach_ref 

833 except cinder_exception.ClientException as ex: 

834 with excutils.save_and_reraise_exception(): 

835 LOG.error('Show attachment failed for attachment ' 

836 '%(id)s. Error: %(msg)s Code: %(code)s', 

837 {'id': attachment_id, 

838 'msg': str(ex), 

839 'code': getattr(ex, 'code', None)}) 

840 

841 def attachment_get_all(self, context, instance_id=None, volume_id=None): 

842 """Get all attachments by instance id or volume id 

843 

844 :param context: The nova request context. 

845 :param instance_id: UUID of the instance attachment to get. 

846 :param volume_id: UUID of the volume attachment to get. 

847 :returns: a list of cinderclient.v3.attachments.VolumeAttachment 

848 objects. 

849 """ 

850 if not instance_id and not volume_id: 

851 raise exception.InvalidRequest( 

852 "Either instance or volume id must be passed.") 

853 

854 search_opts = {} 

855 

856 if instance_id: 

857 search_opts['instance_id'] = instance_id 

858 if volume_id: 

859 search_opts['volume_id'] = volume_id 

860 

861 try: 

862 attachments = cinderclient( 

863 context, '3.44', skip_version_check=True).attachments.list( 

864 search_opts=search_opts) 

865 except cinder_exception.ClientException as ex: 

866 with excutils.save_and_reraise_exception(): 

867 LOG.error('Get all attachment failed. ' 

868 'Error: %(msg)s Code: %(code)s', 

869 {'msg': str(ex), 

870 'code': getattr(ex, 'code', None)}) 

871 return [_translate_attachment_ref( 

872 each.to_dict()) for each in attachments] 

873 

874 @translate_attachment_exception 

875 @retrying.retry(stop_max_attempt_number=5, 

876 retry_on_exception=lambda e: 

877 (isinstance(e, cinder_exception.ClientException) and 

878 e.code in (500, 504))) 

879 def attachment_update(self, context, attachment_id, connector, 

880 mountpoint=None): 

881 """Updates the connector on the volume attachment. An attachment 

882 without a connector is considered reserved but not fully attached. 

883 

884 :param context: The nova request context. 

885 :param attachment_id: UUID of the volume attachment to update. 

886 :param connector: host connector dict. This is required when updating 

887 a volume attachment. To terminate a connection, the volume 

888 attachment for that connection must be deleted. 

889 :param mountpoint: Optional mount device name for the attachment, 

890 e.g. "/dev/vdb". Theoretically this is optional per volume backend, 

891 but in practice it's normally required so it's best to always 

892 provide a value. 

893 :returns: a dict created from the 

894 cinderclient.v3.attachments.VolumeAttachment object with a backward 

895 compatible connection_info dict 

896 """ 

897 # NOTE(mriedem): Due to a limitation in the PUT /attachments/{id} 

898 # API in Cinder, we have to pass the mountpoint in via the 

899 # host connector rather than pass it in as a top-level parameter 

900 # like in the os-attach volume action API. Hopefully this will be 

901 # fixed some day with a new Cinder microversion but until then we 

902 # work around it client-side. 

903 _connector = connector 

904 if mountpoint and 'mountpoint' not in connector: 

905 # Make a copy of the connector so we don't modify it by 

906 # reference. 

907 _connector = copy.deepcopy(connector) 

908 _connector['mountpoint'] = mountpoint 

909 

910 try: 

911 attachment_ref = cinderclient( 

912 context, '3.44', skip_version_check=True).attachments.update( 

913 attachment_id, _connector) 

914 translated_attach_ref = _translate_attachment_ref( 

915 attachment_ref.to_dict()) 

916 return translated_attach_ref 

917 except cinder_exception.ClientException as ex: 

918 with excutils.save_and_reraise_exception(): 

919 LOG.error('Update attachment failed for attachment ' 

920 '%(id)s. Error: %(msg)s Code: %(code)s', 

921 {'id': attachment_id, 

922 'msg': str(ex), 

923 'code': getattr(ex, 'code', None)}) 

924 

925 @translate_attachment_exception 

926 @retrying.retry(stop_max_attempt_number=5, 

927 retry_on_exception=lambda e: 

928 (isinstance(e, cinder_exception.ClientException) and 

929 e.code in (500, 504))) 

930 def attachment_delete(self, context, attachment_id): 

931 try: 

932 cinderclient( 

933 context, '3.44', skip_version_check=True).attachments.delete( 

934 attachment_id) 

935 except cinder_exception.ClientException as ex: 

936 if ex.code == 404: 

937 LOG.warning('Attachment %(id)s does not exist. Ignoring.', 

938 {'id': attachment_id}) 

939 else: 

940 with excutils.save_and_reraise_exception(): 

941 LOG.error('Delete attachment failed for attachment ' 

942 '%(id)s. Error: %(msg)s Code: %(code)s', 

943 {'id': attachment_id, 

944 'msg': str(ex), 

945 'code': getattr(ex, 'code', None)}) 

946 

947 @translate_attachment_exception 

948 def attachment_complete(self, context, attachment_id): 

949 """Marks a volume attachment complete. 

950 

951 This call should be used to inform Cinder that a volume attachment is 

952 fully connected on the compute host so Cinder can apply the necessary 

953 state changes to the volume info in its database. 

954 

955 :param context: The nova request context. 

956 :param attachment_id: UUID of the volume attachment to update. 

957 """ 

958 try: 

959 cinderclient( 

960 context, '3.44', skip_version_check=True).attachments.complete( 

961 attachment_id) 

962 except cinder_exception.ClientException as ex: 

963 with excutils.save_and_reraise_exception(): 

964 LOG.error('Complete attachment failed for attachment ' 

965 '%(id)s. Error: %(msg)s Code: %(code)s', 

966 {'id': attachment_id, 

967 'msg': str(ex), 

968 'code': getattr(ex, 'code', None)}) 

969 

970 @translate_volume_exception 

971 def reimage_volume(self, context, volume_id, image_id, 

972 reimage_reserved=False): 

973 cinderclient(context, '3.68').volumes.reimage( 

974 volume_id, image_id, reimage_reserved)