Coverage for nova/volume/cinder.py: 90%
438 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright 2010 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.
17"""
18Handles all requests relating to volumes + cinder.
19"""
21import collections
22import copy
23import functools
24import sys
25import urllib
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
38from nova import availability_zones as az
39import nova.conf
40from nova import exception
41from nova.i18n import _
42from nova import service_auth
45CONF = nova.conf.CONF
47LOG = logging.getLogger(__name__)
49_ADMIN_AUTH = None
50_SESSION = None
53def reset_globals():
54 """Testing method to reset globals.
55 """
56 global _ADMIN_AUTH
57 global _SESSION
59 _ADMIN_AUTH = None
60 _SESSION = None
63def _load_auth_plugin(conf):
64 auth_plugin = ks_loading.load_auth_from_conf_options(conf,
65 nova.conf.cinder.cinder_group.name)
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
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)
78def _load_session():
79 global _SESSION
81 if not _SESSION:
82 _SESSION = ks_loading.load_session_from_conf_options(
83 CONF, nova.conf.cinder.cinder_group.name)
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
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)
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.
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"
119 _load_session()
120 auth = _get_auth(context)
122 try:
123 u = urllib.parse.urlparse(url)
124 version_url = None
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
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
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))
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)
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.
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)
192def _get_cinderclient_parameters(context):
193 _load_session()
195 auth = _get_auth(context)
197 url = None
199 service_type, service_name, interface = CONF.cinder.catalog_info.split(':')
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
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)
213 return auth, service_parameters, url
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)
222def cinderclient(context, microversion=None, skip_version_check=False,
223 check_only=False):
224 """Constructs a cinder client object for making API requests.
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 """
245 endpoint_override = None
246 auth, service_parameters, url = _get_cinderclient_parameters(context)
248 if CONF.cinder.endpoint_template:
249 endpoint_override = url
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)
256 if version != '3':
257 raise exception.UnsupportedCinderAPIVersion(version=version)
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)
268 if check_only: 268 ↛ 269line 268 didn't jump to line 269 because the condition on line 268 was never true
269 return
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)
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
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)
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())
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
319 if hasattr(vol, 'volume_image_metadata'):
320 d['volume_image_metadata'] = copy.deepcopy(vol.volume_image_metadata)
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
329 if hasattr(vol, 'migration_status'):
330 d['migration_status'] = vol.migration_status
332 return d
335def _untranslate_volume_type_view(volume_type):
336 """Maps keys for volume type view."""
337 v = {}
339 v['id'] = volume_type.id
340 v['name'] = volume_type.name
342 return v
345def _untranslate_snapshot_summary_view(context, snapshot):
346 """Maps keys for snapshots summary view."""
347 d = {}
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
357 d['volume_id'] = snapshot.volume_id
358 d['project_id'] = snapshot.project_id
359 d['volume_size'] = snapshot.size
361 return d
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)
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
386 attachment_ref['connection_info'] = translated_con_info
388 return attachment_ref
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
413def translate_create_exception(method):
414 """Transforms the exception for create but keeps its traceback intact.
415 """
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)
428def translate_volume_exception(method):
429 """Transforms the exception for the volume but keeps its traceback intact.
430 """
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)
443def translate_attachment_exception(method):
444 """Transforms the exception for the attachment but keeps its traceback
445 intact.
446 """
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)
458def translate_snapshot_exception(method):
459 """Transforms the exception for the snapshot but keeps its traceback
460 intact.
461 """
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)
472def translate_mixed_exceptions(method):
473 """Transforms exceptions that can come from both volumes and snapshots."""
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)
486def _reraise(desired_exc):
487 raise desired_exc.with_traceback(sys.exc_info()[2])
490class API(object):
491 """API for interacting with the volume manager."""
493 @translate_volume_exception
494 def get(self, context, volume_id, microversion=None):
495 """Get the details about a volume given it's ID.
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)
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)
513 rval = []
515 for item in items:
516 rval.append(_untranslate_volume_summary_view(context, item))
518 return rval
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)
527 def check_availability_zone(self, context, volume, instance=None):
528 """Ensure that the availability zone is the same.
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 """
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)
551 @translate_volume_exception
552 def reserve_volume(self, context, volume_id):
553 cinderclient(context).volumes.reserve(volume_id)
555 @translate_volume_exception
556 def unreserve_volume(self, context, volume_id):
557 cinderclient(context).volumes.unreserve(volume_id)
559 @translate_volume_exception
560 def begin_detaching(self, context, volume_id):
561 cinderclient(context).volumes.begin_detaching(volume_id)
563 @translate_volume_exception
564 def roll_detaching(self, context, volume_id):
565 cinderclient(context).volumes.roll_detaching(volume_id)
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)
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})
605 client.volumes.detach(volume_id, attachment_id)
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})
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)
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)
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)
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
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)
672 item = client.volumes.create(size, **kwargs)
673 return _untranslate_volume_summary_view(context, item)
675 @translate_volume_exception
676 def delete(self, context, volume_id):
677 cinderclient(context).volumes.delete(volume_id)
679 @translate_volume_exception
680 def update(self, context, volume_id, fields):
681 raise NotImplementedError()
683 @translate_cinder_exception
684 def get_absolute_limits(self, context):
685 """Returns quota limit and usage information for the given tenant
687 See the <volumev3>/v3/{project_id}/limits API reference for details.
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}
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)
705 @translate_cinder_exception
706 def get_all_snapshots(self, context):
707 items = cinderclient(context).volume_snapshots.list(detailed=True)
708 rvals = []
710 for item in items:
711 rvals.append(_untranslate_snapshot_summary_view(context, item))
713 return rvals
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)
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)
730 return _untranslate_snapshot_summary_view(context, item)
732 @translate_snapshot_exception
733 def delete_snapshot(self, context, snapshot_id):
734 cinderclient(context).volume_snapshots.delete(snapshot_id)
736 @translate_cinder_exception
737 def get_all_volume_types(self, context):
738 items = cinderclient(context).volume_types.list()
739 rvals = []
741 for item in items:
742 rvals.append(_untranslate_volume_type_view(item))
744 return rvals
746 @translate_cinder_exception
747 def get_volume_encryption_metadata(self, context, volume_id):
748 return cinderclient(context).volumes.get_encryption_metadata(volume_id)
750 @translate_snapshot_exception
751 def update_snapshot_status(self, context, snapshot_id, status):
752 vs = cinderclient(context).volume_snapshots
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.)
760 vs.update_snapshot_status(
761 snapshot_id,
762 {'status': status,
763 'progress': '90%'}
764 )
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.
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.
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
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)
816 @translate_attachment_exception
817 def attachment_get(self, context, attachment_id):
818 """Gets a volume attachment.
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)})
841 def attachment_get_all(self, context, instance_id=None, volume_id=None):
842 """Get all attachments by instance id or volume id
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.")
854 search_opts = {}
856 if instance_id:
857 search_opts['instance_id'] = instance_id
858 if volume_id:
859 search_opts['volume_id'] = volume_id
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]
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.
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
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)})
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)})
947 @translate_attachment_exception
948 def attachment_complete(self, context, attachment_id):
949 """Marks a volume attachment complete.
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.
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)})
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)