# Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Handles all requests relating to volumes + cinder. """ import collections import copy import functools import sys import urllib from cinderclient import api_versions as cinder_api_versions from cinderclient import client as cinder_client from cinderclient import exceptions as cinder_exception from keystoneauth1 import exceptions as keystone_exception from keystoneauth1 import loading as ks_loading from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import encodeutils from oslo_utils import excutils from oslo_utils import strutils import retrying from nova import availability_zones as az import nova.conf from nova import exception from nova.i18n import _ from nova import service_auth CONF = nova.conf.CONF LOG = logging.getLogger(__name__) _ADMIN_AUTH = None _SESSION = None def reset_globals(): """Testing method to reset globals. """ global _ADMIN_AUTH global _SESSION _ADMIN_AUTH = None _SESSION = None def _load_auth_plugin(conf): auth_plugin = ks_loading.load_auth_from_conf_options(conf, nova.conf.cinder.cinder_group.name) if auth_plugin: return auth_plugin if conf.cinder.auth_type is None: LOG.error('The [cinder] section of your nova configuration file ' 'must be configured for authentication with the ' 'block-storage service endpoint.') err_msg = _('Unknown auth type: %s') % conf.cinder.auth_type raise cinder_exception.Unauthorized(401, message=err_msg) def _load_session(): global _SESSION if not _SESSION: _SESSION = ks_loading.load_session_from_conf_options( CONF, nova.conf.cinder.cinder_group.name) def _get_auth(context): global _ADMIN_AUTH # NOTE(lixipeng): Auth token is none when call # cinder API from compute periodic tasks, context # from them generated from 'context.get_admin_context' # which only set is_admin=True but is without token. # So add load_auth_plugin when this condition appear. user_auth = None if context.is_admin and not context.auth_token: if not _ADMIN_AUTH: _ADMIN_AUTH = _load_auth_plugin(CONF) user_auth = _ADMIN_AUTH # When user_auth = None, user_auth will be extracted from the context. return service_auth.get_auth_plugin(context, user_auth=user_auth) # NOTE(efried): Bug #1752152 # This method is copied/adapted from cinderclient.client.get_server_version so # we can use _SESSION.get rather than a raw requests.get to retrieve the # version document. This enables HTTPS by gleaning cert info from the session # config. def _get_server_version(context, url): """Queries the server via the naked endpoint and gets version info. :param context: The nova request context for auth. :param url: url of the cinder endpoint :returns: APIVersion object for min and max version supported by the server """ min_version = "2.0" current_version = "2.0" _load_session() auth = _get_auth(context) try: u = urllib.parse.urlparse(url) version_url = None # NOTE(andreykurilin): endpoint URL has at least 2 formats: # 1. The classic (legacy) endpoint: # http://{host}:{optional_port}/v{2 or 3}/{project-id} # http://{host}:{optional_port}/v{2 or 3} # 3. Under wsgi: # http://{host}:{optional_port}/volume/v{2 or 3} for ver in ['v2', 'v3']: if u.path.endswith(ver) or "/{0}/".format(ver) in u.path: path = u.path[:u.path.rfind(ver)] version_url = '%s://%s%s' % (u.scheme, u.netloc, path) break if not version_url: # NOTE(andreykurilin): probably, it is one of the next cases: # * https://volume.example.com/ # * https://example.com/volume # leave as is without cropping. version_url = url response = _SESSION.get(version_url, auth=auth) data = jsonutils.loads(response.text) versions = data['versions'] for version in versions: if '3.' in version['version']: min_version = version['min_version'] current_version = version['version'] break except cinder_exception.ClientException as e: LOG.warning("Error in server version query:%s\n" "Returning APIVersion 2.0", str(e.message)) return (cinder_api_versions.APIVersion(min_version), cinder_api_versions.APIVersion(current_version)) # NOTE(efried): Bug #1752152 # This method is copied/adapted from # cinderclient.client.get_highest_client_server_version. See note on # _get_server_version. def _get_highest_client_server_version(context, url): """Returns highest APIVersion supported version by client and server.""" min_server, max_server = _get_server_version(context, url) max_client = cinder_api_versions.APIVersion( cinder_api_versions.MAX_VERSION) return min(max_server, max_client) def _check_microversion(context, url, microversion): """Checks to see if the requested microversion is supported by the current version of python-cinderclient and the volume API endpoint. :param context: The nova request context for auth. :param url: Cinder API endpoint URL. :param microversion: Requested microversion. If not available at the given API endpoint URL, a CinderAPIVersionNotAvailable exception is raised. :returns: The microversion if it is available. This can be used to construct the cinder v3 client object. :raises: CinderAPIVersionNotAvailable if the microversion is not available. """ max_api_version = _get_highest_client_server_version(context, url) # Check if the max_api_version matches the requested minimum microversion. if max_api_version.matches(microversion): # The requested microversion is supported by the client and the server. return microversion raise exception.CinderAPIVersionNotAvailable(version=microversion) def _get_cinderclient_parameters(context): _load_session() auth = _get_auth(context) url = None service_type, service_name, interface = CONF.cinder.catalog_info.split(':') service_parameters = {'service_type': service_type, 'interface': interface, 'region_name': CONF.cinder.os_region_name} # Only include the service_name if it's provided. if service_name: service_parameters['service_name'] = service_name if CONF.cinder.endpoint_template: url = CONF.cinder.endpoint_template % context.to_dict() else: url = _SESSION.get_endpoint(auth, **service_parameters) return auth, service_parameters, url def is_microversion_supported(context, microversion): # NOTE(efried): Work around bug #1752152. Call the cinderclient() builder # in a way that just does a microversion check. cinderclient(context, microversion=microversion, check_only=True) def cinderclient(context, microversion=None, skip_version_check=False, check_only=False): """Constructs a cinder client object for making API requests. :param context: The nova request context for auth. :param microversion: Optional microversion to check against the client. This implies that Cinder v3 is required for any calls that require a microversion. If the microversion is not available, this method will raise an CinderAPIVersionNotAvailable exception. :param skip_version_check: If True and a specific microversion is requested, the version discovery check is skipped and the microversion is used directly. This should only be used if a previous check for the same microversion was successful. :param check_only: If True, don't build the actual client; just do the setup and version checking. :raises: UnsupportedCinderAPIVersion if a major version other than 3 is requested. :raises: CinderAPIVersionNotAvailable if microversion checking is requested and the specified microversion is higher than what the service can handle. :returns: A cinderclient.client.Client wrapper, unless check_only is False. """ endpoint_override = None auth, service_parameters, url = _get_cinderclient_parameters(context) if CONF.cinder.endpoint_template: endpoint_override = url # TODO(jamielennox): This should be using proper version discovery from # the cinder service rather than just inspecting the URL for certain string # values. version = cinder_client.get_volume_api_from_url(url) if version != '3': raise exception.UnsupportedCinderAPIVersion(version=version) version = '3.0' # Check to see a specific microversion is requested and if so, can it # be handled by the backing server. if microversion is not None: if skip_version_check: version = microversion else: version = _check_microversion(context, url, microversion) if check_only: return return cinder_client.Client(version, session=_SESSION, auth=auth, endpoint_override=endpoint_override, connect_retries=CONF.cinder.http_retries, global_request_id=context.global_id, **service_parameters) def _untranslate_volume_summary_view(context, vol): """Maps keys for volumes summary view.""" d = {} d['id'] = vol.id d['status'] = vol.status d['size'] = vol.size d['availability_zone'] = vol.availability_zone d['created_at'] = vol.created_at # TODO(jdg): The calling code expects attach_time and # mountpoint to be set. When the calling # code is more defensive this can be # removed. d['attach_time'] = "" d['mountpoint'] = "" d['multiattach'] = getattr(vol, 'multiattach', False) if vol.attachments: d['attachments'] = collections.OrderedDict() for attachment in vol.attachments: a = {attachment['server_id']: {'attachment_id': attachment.get('attachment_id'), 'mountpoint': attachment.get('device')} } d['attachments'].update(a.items()) d['attach_status'] = 'attached' else: d['attach_status'] = 'detached' d['display_name'] = vol.name d['display_description'] = vol.description # TODO(jdg): Information may be lost in this translation d['volume_type_id'] = vol.volume_type d['snapshot_id'] = vol.snapshot_id d['bootable'] = strutils.bool_from_string(vol.bootable) d['volume_metadata'] = {} for key, value in vol.metadata.items(): d['volume_metadata'][key] = value if hasattr(vol, 'volume_image_metadata'): d['volume_image_metadata'] = copy.deepcopy(vol.volume_image_metadata) # The 3.48 microversion exposes a shared_targets boolean and service_uuid # string parameter which can be used with locks during volume attach # and detach. if hasattr(vol, 'shared_targets'): d['shared_targets'] = vol.shared_targets d['service_uuid'] = vol.service_uuid if hasattr(vol, 'migration_status'): d['migration_status'] = vol.migration_status return d def _untranslate_volume_type_view(volume_type): """Maps keys for volume type view.""" v = {} v['id'] = volume_type.id v['name'] = volume_type.name return v def _untranslate_snapshot_summary_view(context, snapshot): """Maps keys for snapshots summary view.""" d = {} d['id'] = snapshot.id d['status'] = snapshot.status d['progress'] = snapshot.progress d['size'] = snapshot.size d['created_at'] = snapshot.created_at d['display_name'] = snapshot.name d['display_description'] = snapshot.description d['volume_id'] = snapshot.volume_id d['project_id'] = snapshot.project_id d['volume_size'] = snapshot.size return d def _translate_attachment_ref(attachment_ref): """Building old style connection_info by adding the 'data' key back.""" translated_con_info = {} connection_info_data = attachment_ref.pop('connection_info', None) if connection_info_data: connection_info_data.pop('attachment_id', None) translated_con_info['driver_volume_type'] = \ connection_info_data.pop('driver_volume_type', None) translated_con_info['data'] = connection_info_data translated_con_info['status'] = attachment_ref.pop('status', None) translated_con_info['instance'] = attachment_ref.pop('instance', None) translated_con_info['attached_at'] = attachment_ref.pop('attached_at', None) translated_con_info['detached_at'] = attachment_ref.pop('detached_at', None) # Now the catch all... for k, v in attachment_ref.items(): # Keep these as top-level fields on the attachment record. if k not in ("id", "attach_mode"): translated_con_info[k] = v attachment_ref['connection_info'] = translated_con_info return attachment_ref def translate_cinder_exception(method): """Transforms a cinder exception but keeps its traceback intact.""" @functools.wraps(method) def wrapper(self, ctx, *args, **kwargs): try: res = method(self, ctx, *args, **kwargs) except (cinder_exception.ConnectionError, keystone_exception.ConnectionError) as exc: err_msg = encodeutils.exception_to_unicode(exc) _reraise(exception.CinderConnectionFailed(reason=err_msg)) except (keystone_exception.BadRequest, cinder_exception.BadRequest) as exc: err_msg = encodeutils.exception_to_unicode(exc) _reraise(exception.InvalidInput(reason=err_msg)) except (keystone_exception.Forbidden, cinder_exception.Forbidden) as exc: err_msg = encodeutils.exception_to_unicode(exc) _reraise(exception.Forbidden(err_msg)) return res return wrapper def translate_create_exception(method): """Transforms the exception for create but keeps its traceback intact. """ def wrapper(self, ctx, size, *args, **kwargs): try: res = method(self, ctx, size, *args, **kwargs) except (keystone_exception.NotFound, cinder_exception.NotFound) as e: _reraise(exception.NotFound(message=e.message)) except cinder_exception.OverLimit as e: _reraise(exception.OverQuota(message=e.message)) return res return translate_cinder_exception(wrapper) def translate_volume_exception(method): """Transforms the exception for the volume but keeps its traceback intact. """ def wrapper(self, ctx, volume_id, *args, **kwargs): try: res = method(self, ctx, volume_id, *args, **kwargs) except (keystone_exception.NotFound, cinder_exception.NotFound): _reraise(exception.VolumeNotFound(volume_id=volume_id)) except cinder_exception.OverLimit as e: _reraise(exception.OverQuota(message=e.message)) return res return translate_cinder_exception(wrapper) def translate_attachment_exception(method): """Transforms the exception for the attachment but keeps its traceback intact. """ def wrapper(self, ctx, attachment_id, *args, **kwargs): try: res = method(self, ctx, attachment_id, *args, **kwargs) except (keystone_exception.NotFound, cinder_exception.NotFound): _reraise(exception.VolumeAttachmentNotFound( attachment_id=attachment_id)) return res return translate_cinder_exception(wrapper) def translate_snapshot_exception(method): """Transforms the exception for the snapshot but keeps its traceback intact. """ def wrapper(self, ctx, snapshot_id, *args, **kwargs): try: res = method(self, ctx, snapshot_id, *args, **kwargs) except (keystone_exception.NotFound, cinder_exception.NotFound): _reraise(exception.SnapshotNotFound(snapshot_id=snapshot_id)) return res return translate_cinder_exception(wrapper) def translate_mixed_exceptions(method): """Transforms exceptions that can come from both volumes and snapshots.""" def wrapper(self, ctx, res_id, *args, **kwargs): try: res = method(self, ctx, res_id, *args, **kwargs) except (keystone_exception.NotFound, cinder_exception.NotFound): _reraise(exception.VolumeNotFound(volume_id=res_id)) except cinder_exception.OverLimit: _reraise(exception.OverQuota(overs='snapshots')) return res return translate_cinder_exception(wrapper) def _reraise(desired_exc): raise desired_exc.with_traceback(sys.exc_info()[2]) class API(object): """API for interacting with the volume manager.""" @translate_volume_exception def get(self, context, volume_id, microversion=None): """Get the details about a volume given it's ID. :param context: the nova request context :param volume_id: the id of the volume to get :param microversion: optional string microversion value :raises: CinderAPIVersionNotAvailable if the specified microversion is not available. """ item = cinderclient( context, microversion=microversion).volumes.get(volume_id) return _untranslate_volume_summary_view(context, item) @translate_cinder_exception def get_all(self, context, search_opts=None): search_opts = search_opts or {} items = cinderclient(context).volumes.list(detailed=True, search_opts=search_opts) rval = [] for item in items: rval.append(_untranslate_volume_summary_view(context, item)) return rval def check_attached(self, context, volume): if volume['status'] != "in-use": msg = _("volume '%(vol)s' status must be 'in-use'. Currently in " "'%(status)s' status") % {"vol": volume['id'], "status": volume['status']} raise exception.InvalidVolume(reason=msg) def check_availability_zone(self, context, volume, instance=None): """Ensure that the availability zone is the same. :param context: the nova request context :param volume: the volume attached to the instance :param instance: nova.objects.instance.Instance object :raises: InvalidVolume if the instance availability zone does not equal the volume's availability zone """ # TODO(walter-boring): move this check to Cinder as part of # the reserve call. if instance and not CONF.cinder.cross_az_attach: instance_az = az.get_instance_availability_zone(context, instance) if instance_az != volume['availability_zone']: msg = _("Instance %(instance)s and volume %(vol)s are not in " "the same availability_zone. Instance is in " "%(ins_zone)s. Volume is in %(vol_zone)s") % { "instance": instance.uuid, "vol": volume['id'], 'ins_zone': instance_az, 'vol_zone': volume['availability_zone']} raise exception.InvalidVolume(reason=msg) @translate_volume_exception def reserve_volume(self, context, volume_id): cinderclient(context).volumes.reserve(volume_id) @translate_volume_exception def unreserve_volume(self, context, volume_id): cinderclient(context).volumes.unreserve(volume_id) @translate_volume_exception def begin_detaching(self, context, volume_id): cinderclient(context).volumes.begin_detaching(volume_id) @translate_volume_exception def roll_detaching(self, context, volume_id): cinderclient(context).volumes.roll_detaching(volume_id) @translate_volume_exception def attach(self, context, volume_id, instance_uuid, mountpoint, mode='rw'): cinderclient(context).volumes.attach(volume_id, instance_uuid, mountpoint, mode=mode) @translate_volume_exception @retrying.retry(stop_max_attempt_number=5, retry_on_exception=lambda e: (isinstance(e, cinder_exception.ClientException) and e.code == 500)) def detach(self, context, volume_id, instance_uuid=None, attachment_id=None): client = cinderclient(context) if attachment_id is None: volume = self.get(context, volume_id) if volume['multiattach']: attachments = volume.get('attachments', {}) if instance_uuid: attachment_id = attachments.get(instance_uuid, {}).\ get('attachment_id') if not attachment_id: LOG.warning("attachment_id couldn't be retrieved " "for volume %(volume_id)s with " "instance_uuid %(instance_id)s. The " "volume has the 'multiattach' flag " "enabled, without the attachment_id " "Cinder most probably cannot perform " "the detach.", {'volume_id': volume_id, 'instance_id': instance_uuid}) else: LOG.warning("attachment_id couldn't be retrieved for " "volume %(volume_id)s. The volume has the " "'multiattach' flag enabled, without the " "attachment_id Cinder most probably " "cannot perform the detach.", {'volume_id': volume_id}) client.volumes.detach(volume_id, attachment_id) @translate_volume_exception def initialize_connection(self, context, volume_id, connector): try: connection_info = cinderclient( context).volumes.initialize_connection(volume_id, connector) connection_info['connector'] = connector return connection_info except cinder_exception.ClientException as ex: with excutils.save_and_reraise_exception(): LOG.error( 'Initialize connection failed for volume %(vol)s on host ' '%(host)s. Error: %(msg)s Code: %(code)s. ' 'Attempting to terminate connection.', {'vol': volume_id, 'host': connector.get('host'), 'msg': str(ex), 'code': ex.code}) try: self.terminate_connection(context, volume_id, connector) except Exception as exc: LOG.error( 'Connection between volume %(vol)s and host %(host)s ' 'might have succeeded, but attempt to terminate ' 'connection has failed. Validate the connection and ' 'determine if manual cleanup is needed. ' 'Error: %(msg)s Code: %(code)s.', {'vol': volume_id, 'host': connector.get('host'), 'msg': str(exc), 'code': exc.code if hasattr(exc, 'code') else None}) @translate_volume_exception @retrying.retry(stop_max_attempt_number=5, retry_on_exception=lambda e: (isinstance(e, cinder_exception.ClientException) and e.code == 500)) def terminate_connection(self, context, volume_id, connector): return cinderclient(context).volumes.terminate_connection(volume_id, connector) @translate_cinder_exception def migrate_volume_completion(self, context, old_volume_id, new_volume_id, error=False): return cinderclient(context).volumes.migrate_volume_completion( old_volume_id, new_volume_id, error) @translate_create_exception def create(self, context, size, name, description, snapshot=None, image_id=None, volume_type=None, metadata=None, availability_zone=None): client = cinderclient(context) if snapshot is not None: snapshot_id = snapshot['id'] else: snapshot_id = None kwargs = dict(snapshot_id=snapshot_id, volume_type=volume_type, availability_zone=availability_zone, metadata=metadata, imageRef=image_id, name=name, description=description) item = client.volumes.create(size, **kwargs) return _untranslate_volume_summary_view(context, item) @translate_volume_exception def delete(self, context, volume_id): cinderclient(context).volumes.delete(volume_id) @translate_volume_exception def update(self, context, volume_id, fields): raise NotImplementedError() @translate_cinder_exception def get_absolute_limits(self, context): """Returns quota limit and usage information for the given tenant See the /v3/{project_id}/limits API reference for details. :param context: The nova RequestContext for the user request. Note that the limit information returned from Cinder is specific to the project_id within this context. :returns: dict of absolute limits """ # cinderclient returns a generator of AbsoluteLimit objects, so iterate # over the generator and return a dictionary which is easier for the # nova client-side code to handle. limits = cinderclient(context).limits.get().absolute return {limit.name: limit.value for limit in limits} @translate_snapshot_exception def get_snapshot(self, context, snapshot_id): item = cinderclient(context).volume_snapshots.get(snapshot_id) return _untranslate_snapshot_summary_view(context, item) @translate_cinder_exception def get_all_snapshots(self, context): items = cinderclient(context).volume_snapshots.list(detailed=True) rvals = [] for item in items: rvals.append(_untranslate_snapshot_summary_view(context, item)) return rvals @translate_mixed_exceptions def create_snapshot(self, context, volume_id, name, description): item = cinderclient(context).volume_snapshots.create(volume_id, False, name, description) return _untranslate_snapshot_summary_view(context, item) @translate_mixed_exceptions def create_snapshot_force(self, context, volume_id, name, description): item = cinderclient(context).volume_snapshots.create(volume_id, True, name, description) return _untranslate_snapshot_summary_view(context, item) @translate_snapshot_exception def delete_snapshot(self, context, snapshot_id): cinderclient(context).volume_snapshots.delete(snapshot_id) @translate_cinder_exception def get_all_volume_types(self, context): items = cinderclient(context).volume_types.list() rvals = [] for item in items: rvals.append(_untranslate_volume_type_view(item)) return rvals @translate_cinder_exception def get_volume_encryption_metadata(self, context, volume_id): return cinderclient(context).volumes.get_encryption_metadata(volume_id) @translate_snapshot_exception def update_snapshot_status(self, context, snapshot_id, status): vs = cinderclient(context).volume_snapshots # '90%' here is used to tell Cinder that Nova is done # with its portion of the 'creating' state. This can # be removed when we are able to split the Cinder states # into 'creating' and a separate state of # 'creating_in_nova'. (Same for 'deleting' state.) vs.update_snapshot_status( snapshot_id, {'status': status, 'progress': '90%'} ) @translate_volume_exception def attachment_create(self, context, volume_id, instance_id, connector=None, mountpoint=None): """Create a volume attachment. This requires microversion >= 3.44. The attachment_create call was introduced in microversion 3.27. We need 3.44 as minimum here as we need attachment_complete to finish the attaching process and it which was introduced in version 3.44. :param context: The nova request context. :param volume_id: UUID of the volume on which to create the attachment. :param instance_id: UUID of the instance to which the volume will be attached. :param connector: host connector dict; if None, the attachment will be 'reserved' but not yet attached. :param mountpoint: Optional mount device name for the attachment, e.g. "/dev/vdb". This is only used if a connector is provided. :returns: a dict created from the cinderclient.v3.attachments.VolumeAttachment object with a backward compatible connection_info dict """ # NOTE(mriedem): Due to a limitation in the POST /attachments/ # API in Cinder, we have to pass the mountpoint in via the # host connector rather than pass it in as a top-level parameter # like in the os-attach volume action API. Hopefully this will be # fixed some day with a new Cinder microversion but until then we # work around it client-side. _connector = connector if _connector and mountpoint and 'mountpoint' not in _connector: # Make a copy of the connector so we don't modify it by # reference. _connector = copy.deepcopy(connector) _connector['mountpoint'] = mountpoint try: attachment_ref = cinderclient(context, '3.44').attachments.create( volume_id, _connector, instance_id) return _translate_attachment_ref(attachment_ref) except cinder_exception.ClientException as ex: with excutils.save_and_reraise_exception(): # NOTE: It is unnecessary to output BadRequest(400) error log, # because operators don't need to debug such cases. if getattr(ex, 'code', None) != 400: LOG.error('Create attachment failed for volume ' '%(volume_id)s. Error: %(msg)s Code: %(code)s', {'volume_id': volume_id, 'msg': str(ex), 'code': getattr(ex, 'code', None)}, instance_uuid=instance_id) @translate_attachment_exception def attachment_get(self, context, attachment_id): """Gets a volume attachment. :param context: The nova request context. :param attachment_id: UUID of the volume attachment to get. :returns: a dict created from the cinderclient.v3.attachments.VolumeAttachment object with a backward compatible connection_info dict """ try: attachment_ref = cinderclient( context, '3.44', skip_version_check=True).attachments.show( attachment_id) translated_attach_ref = _translate_attachment_ref( attachment_ref.to_dict()) return translated_attach_ref except cinder_exception.ClientException as ex: with excutils.save_and_reraise_exception(): LOG.error('Show attachment failed for attachment ' '%(id)s. Error: %(msg)s Code: %(code)s', {'id': attachment_id, 'msg': str(ex), 'code': getattr(ex, 'code', None)}) def attachment_get_all(self, context, instance_id=None, volume_id=None): """Get all attachments by instance id or volume id :param context: The nova request context. :param instance_id: UUID of the instance attachment to get. :param volume_id: UUID of the volume attachment to get. :returns: a list of cinderclient.v3.attachments.VolumeAttachment objects. """ if not instance_id and not volume_id: raise exception.InvalidRequest( "Either instance or volume id must be passed.") search_opts = {} if instance_id: search_opts['instance_id'] = instance_id if volume_id: search_opts['volume_id'] = volume_id try: attachments = cinderclient( context, '3.44', skip_version_check=True).attachments.list( search_opts=search_opts) except cinder_exception.ClientException as ex: with excutils.save_and_reraise_exception(): LOG.error('Get all attachment failed. ' 'Error: %(msg)s Code: %(code)s', {'msg': str(ex), 'code': getattr(ex, 'code', None)}) return [_translate_attachment_ref( each.to_dict()) for each in attachments] @translate_attachment_exception @retrying.retry(stop_max_attempt_number=5, retry_on_exception=lambda e: (isinstance(e, cinder_exception.ClientException) and e.code in (500, 504))) def attachment_update(self, context, attachment_id, connector, mountpoint=None): """Updates the connector on the volume attachment. An attachment without a connector is considered reserved but not fully attached. :param context: The nova request context. :param attachment_id: UUID of the volume attachment to update. :param connector: host connector dict. This is required when updating a volume attachment. To terminate a connection, the volume attachment for that connection must be deleted. :param mountpoint: Optional mount device name for the attachment, e.g. "/dev/vdb". Theoretically this is optional per volume backend, but in practice it's normally required so it's best to always provide a value. :returns: a dict created from the cinderclient.v3.attachments.VolumeAttachment object with a backward compatible connection_info dict """ # NOTE(mriedem): Due to a limitation in the PUT /attachments/{id} # API in Cinder, we have to pass the mountpoint in via the # host connector rather than pass it in as a top-level parameter # like in the os-attach volume action API. Hopefully this will be # fixed some day with a new Cinder microversion but until then we # work around it client-side. _connector = connector if mountpoint and 'mountpoint' not in connector: # Make a copy of the connector so we don't modify it by # reference. _connector = copy.deepcopy(connector) _connector['mountpoint'] = mountpoint try: attachment_ref = cinderclient( context, '3.44', skip_version_check=True).attachments.update( attachment_id, _connector) translated_attach_ref = _translate_attachment_ref( attachment_ref.to_dict()) return translated_attach_ref except cinder_exception.ClientException as ex: with excutils.save_and_reraise_exception(): LOG.error('Update attachment failed for attachment ' '%(id)s. Error: %(msg)s Code: %(code)s', {'id': attachment_id, 'msg': str(ex), 'code': getattr(ex, 'code', None)}) @translate_attachment_exception @retrying.retry(stop_max_attempt_number=5, retry_on_exception=lambda e: (isinstance(e, cinder_exception.ClientException) and e.code in (500, 504))) def attachment_delete(self, context, attachment_id): try: cinderclient( context, '3.44', skip_version_check=True).attachments.delete( attachment_id) except cinder_exception.ClientException as ex: if ex.code == 404: LOG.warning('Attachment %(id)s does not exist. Ignoring.', {'id': attachment_id}) else: with excutils.save_and_reraise_exception(): LOG.error('Delete attachment failed for attachment ' '%(id)s. Error: %(msg)s Code: %(code)s', {'id': attachment_id, 'msg': str(ex), 'code': getattr(ex, 'code', None)}) @translate_attachment_exception def attachment_complete(self, context, attachment_id): """Marks a volume attachment complete. This call should be used to inform Cinder that a volume attachment is fully connected on the compute host so Cinder can apply the necessary state changes to the volume info in its database. :param context: The nova request context. :param attachment_id: UUID of the volume attachment to update. """ try: cinderclient( context, '3.44', skip_version_check=True).attachments.complete( attachment_id) except cinder_exception.ClientException as ex: with excutils.save_and_reraise_exception(): LOG.error('Complete attachment failed for attachment ' '%(id)s. Error: %(msg)s Code: %(code)s', {'id': attachment_id, 'msg': str(ex), 'code': getattr(ex, 'code', None)}) @translate_volume_exception def reimage_volume(self, context, volume_id, image_id, reimage_reserved=False): cinderclient(context, '3.68').volumes.reimage( volume_id, image_id, reimage_reserved)