raise exc.HTTPNotFound()
self.volume_api.terminate_connection(context, volume,
{}, force=True)
- self.volume_api.detach(context, volume)
+
+ attachment_id = body['os-force_detach'].get('attachment_id', None)
+
+ self.volume_api.detach(context, volume, attachment_id)
return webob.Response(status_int=202)
@wsgi.action('os-migrate_volume')
except exception.VolumeNotFound as error:
raise webob.exc.HTTPNotFound(explanation=error.msg)
- self.volume_api.detach(context, volume)
+ attachment_id = None
+ if body['os-detach']:
+ attachment_id = body['os-detach'].get('attachment_id', None)
+
+ self.volume_api.detach(context, volume, attachment_id)
return webob.Response(status_int=202)
@wsgi.action('os-reserve')
def _translate_attachment_summary_view(_context, vol):
"""Maps keys for attachment summary view."""
- d = {}
-
- volume_id = vol['id']
-
- # NOTE(justinsb): We use the volume id as the id of the attachment object
- d['id'] = volume_id
-
- d['volume_id'] = volume_id
- d['server_id'] = vol['instance_uuid']
- d['host_name'] = vol['attached_host']
- if vol.get('mountpoint'):
- d['device'] = vol['mountpoint']
+ d = []
+ attachments = vol.get('volume_attachment', [])
+ for attachment in attachments:
+ if attachment.get('attach_status') == 'attached':
+ a = {'id': attachment.get('volume_id'),
+ 'attachment_id': attachment.get('id'),
+ 'volume_id': attachment.get('volume_id'),
+ 'server_id': attachment.get('instance_uuid'),
+ 'host_name': attachment.get('attached_host'),
+ 'device': attachment.get('mountpoint'),
+ }
+ d.append(a)
return d
else:
d['bootable'] = 'false'
+ if vol['multiattach']:
+ d['multiattach'] = 'true'
+ else:
+ d['multiattach'] = 'false'
+
d['attachments'] = []
if vol['attach_status'] == 'attached':
- attachment = _translate_attachment_detail_view(context, vol)
- d['attachments'].append(attachment)
+ d['attachments'] = _translate_attachment_detail_view(context, vol)
d['display_name'] = vol['display_name']
d['display_description'] = vol['display_description']
elem.set('volume_type')
elem.set('snapshot_id')
elem.set('source_volid')
+ elem.set('multiattach')
attachments = xmlutil.SubTemplateElement(elem, 'attachments')
attachment = xmlutil.SubTemplateElement(attachments, 'attachment',
size = kwargs['source_volume']['size']
LOG.info(_LI("Create volume of %s GB"), size, context=context)
+ multiattach = volume.get('multiattach', False)
+ kwargs['multiattach'] = multiattach
image_href = None
image_uuid = None
'bootable': str(volume.get('bootable')).lower(),
'encrypted': self._is_volume_encrypted(volume),
'replication_status': volume.get('replication_status'),
- 'consistencygroup_id': volume.get('consistencygroup_id')
+ 'consistencygroup_id': volume.get('consistencygroup_id'),
+ 'multiattach': volume.get('multiattach')
}
}
attachments = []
if volume['attach_status'] == 'attached':
- d = {}
- volume_id = volume['id']
-
- # note(justinsb): we use the volume id as the id of the attachments
- # object
- d['id'] = volume_id
-
- d['volume_id'] = volume_id
- d['server_id'] = volume['instance_uuid']
- d['host_name'] = volume['attached_host']
- if volume.get('mountpoint'):
- d['device'] = volume['mountpoint']
- attachments.append(d)
+ attaches = volume.get('volume_attachment', [])
+ for attachment in attaches:
+ if attachment.get('attach_status') == 'attached':
+ a = {'id': attachment.get('volume_id'),
+ 'attachment_id': attachment.get('id'),
+ 'volume_id': attachment.get('volume_id'),
+ 'server_id': attachment.get('instance_uuid'),
+ 'host_name': attachment.get('attached_host'),
+ 'device': attachment.get('mountpoint'),
+ }
+ attachments.append(a)
return attachments
def make_attachment(elem):
elem.set('id')
+ elem.set('attachment_id')
elem.set('server_id')
elem.set('host_name')
elem.set('volume_id')
elem.set('snapshot_id')
elem.set('source_volid')
elem.set('consistencygroup_id')
+ elem.set('multiattach')
attachments = xmlutil.SubTemplateElement(elem, 'attachments')
attachment = xmlutil.SubTemplateElement(attachments, 'attachment',
kwargs['availability_zone'] = volume.get('availability_zone', None)
kwargs['scheduler_hints'] = volume.get('scheduler_hints', None)
+ multiattach = volume.get('multiattach', False)
+ kwargs['multiattach'] = multiattach
new_volume = self.volume_api.create(context,
size,
for volume in volumes:
volume_host = volume_utils.extract_host(volume['host'], 'backend')
backend = self._get_volume_backend(host=volume_host)
- if volume['status'] == 'backing-up':
- LOG.info(_LI('Resetting volume %s to available '
- '(was backing-up).') % volume['id'])
- mgr = self._get_manager(backend)
- mgr.detach_volume(ctxt, volume['id'])
- if volume['status'] == 'restoring-backup':
- LOG.info(_LI('Resetting volume %s to error_restoring '
- '(was restoring-backup).') % volume['id'])
- mgr = self._get_manager(backend)
- mgr.detach_volume(ctxt, volume['id'])
- self.db.volume_update(ctxt, volume['id'],
- {'status': 'error_restoring'})
+ attachments = volume['volume_attachment']
+ if attachments:
+ if volume['status'] == 'backing-up':
+ LOG.info(_LI('Resetting volume %s to available '
+ '(was backing-up).'), volume['id'])
+ mgr = self._get_manager(backend)
+ for attachment in attachments:
+ if (attachment['attached_host'] == self.host and
+ attachment['instance_uuid'] is None):
+ mgr.detach_volume(ctxt, volume['id'],
+ attachment['id'])
+ if volume['status'] == 'restoring-backup':
+ LOG.info(_LI('setting volume %s to error_restoring '
+ '(was restoring-backup).'), volume['id'])
+ mgr = self._get_manager(backend)
+ for attachment in attachments:
+ if (attachment['attached_host'] == self.host and
+ attachment['instance_uuid'] is None):
+ mgr.detach_volume(ctxt, volume['id'],
+ attachment['id'])
+ self.db.volume_update(ctxt, volume['id'],
+ {'status': 'error_restoring'})
# TODO(smulcahy) implement full resume of backup and restore
# operations on restart (rather than simply resetting)
###############
-def volume_attached(context, volume_id, instance_id, host_name, mountpoint):
+def volume_attach(context, values):
+ """Attach a volume."""
+ return IMPL.volume_attach(context, values)
+
+
+def volume_attached(context, volume_id, instance_id, host_name, mountpoint,
+ attach_mode='rw'):
"""Ensure that a volume is set as attached."""
return IMPL.volume_attached(context, volume_id, instance_id, host_name,
- mountpoint)
+ mountpoint, attach_mode)
def volume_create(context, values):
return IMPL.volume_destroy(context, volume_id)
-def volume_detached(context, volume_id):
+def volume_detached(context, volume_id, attachment_id):
"""Ensure that a volume is set as detached."""
- return IMPL.volume_detached(context, volume_id)
+ return IMPL.volume_detached(context, volume_id, attachment_id)
def volume_get(context, volume_id):
return IMPL.volume_update(context, volume_id, values)
+def volume_attachment_update(context, attachment_id, values):
+ return IMPL.volume_attachment_update(context, attachment_id, values)
+
+
+def volume_attachment_get(context, attachment_id, session=None):
+ return IMPL.volume_attachment_get(context, attachment_id, session)
+
+
+def volume_attachment_get_used_by_volume_id(context, volume_id):
+ return IMPL.volume_attachment_get_used_by_volume_id(context, volume_id)
+
+
+def volume_attachment_get_by_host(context, volume_id, host):
+ return IMPL.volume_attachment_get_by_host(context, volume_id, host)
+
+
+def volume_attachment_get_by_instance_uuid(context, volume_id, instance_uuid):
+ return IMPL.volume_attachment_get_by_instance_uuid(context, volume_id,
+ instance_uuid)
+
+
####################
@require_admin_context
-def volume_attached(context, volume_id, instance_uuid, host_name, mountpoint):
+def volume_attach(context, values):
+ volume_attachment_ref = models.VolumeAttachment()
+ if not values.get('id'):
+ values['id'] = str(uuid.uuid4())
+
+ volume_attachment_ref.update(values)
+ session = get_session()
+ with session.begin():
+ volume_attachment_ref.save(session=session)
+ return volume_attachment_get(context, values['id'],
+ session=session)
+
+
+@require_admin_context
+def volume_attached(context, attachment_id, instance_uuid, host_name,
+ mountpoint, attach_mode='rw'):
+ """This method updates a volume attachment entry.
+
+ This function saves the information related to a particular
+ attachment for a volume. It also updates the volume record
+ to mark the volume as attached.
+
+ """
if instance_uuid and not uuidutils.is_uuid_like(instance_uuid):
raise exception.InvalidUUID(uuid=instance_uuid)
session = get_session()
with session.begin():
- volume_ref = _volume_get(context, volume_id, session=session)
+ volume_attachment_ref = volume_attachment_get(context, attachment_id,
+ session=session)
+
+ volume_attachment_ref['mountpoint'] = mountpoint
+ volume_attachment_ref['attach_status'] = 'attached'
+ volume_attachment_ref['instance_uuid'] = instance_uuid
+ volume_attachment_ref['attached_host'] = host_name
+ volume_attachment_ref['attach_time'] = timeutils.utcnow()
+ volume_attachment_ref['attach_mode'] = attach_mode
+
+ volume_ref = _volume_get(context, volume_attachment_ref['volume_id'],
+ session=session)
+ volume_attachment_ref.save(session=session)
+
volume_ref['status'] = 'in-use'
- volume_ref['mountpoint'] = mountpoint
volume_ref['attach_status'] = 'attached'
- volume_ref['instance_uuid'] = instance_uuid
- volume_ref['attached_host'] = host_name
+ volume_ref.save(session=session)
return volume_ref
@require_admin_context
-def volume_detached(context, volume_id):
+def volume_detach(context, attachment_id):
+ session = get_session()
+ with session.begin():
+ volume_attachment_ref = volume_attachment_get(context, attachment_id,
+ session=session)
+ volume_attachment_ref['attach_status'] = 'detaching'
+ volume_attachment_ref.save(session=session)
+
+
+@require_admin_context
+def volume_detached(context, volume_id, attachment_id):
+ """This updates a volume attachment and marks it as detached.
+
+ This method also ensures that the volume entry is correctly
+ marked as either still attached/in-use or detached/available
+ if this was the last detachment made.
+
+ """
session = get_session()
with session.begin():
+ attachment = volume_attachment_get(context, attachment_id,
+ session=session)
+
+ # If this is already detached, attachment will be None
+ if attachment:
+ now = timeutils.utcnow()
+ attachment['attach_status'] = 'detached'
+ attachment['detach_time'] = now
+ attachment['deleted'] = True
+ attachment['deleted_at'] = now
+ attachment.save(session=session)
+
+ attachment_list = volume_attachment_get_used_by_volume_id(
+ context, volume_id, session=session)
+ remain_attachment = False
+ if attachment_list and len(attachment_list) > 0:
+ remain_attachment = True
+
volume_ref = _volume_get(context, volume_id, session=session)
- # Hide status update from user if we're performing a volume migration
- if not volume_ref['migration_status']:
- volume_ref['status'] = 'available'
- volume_ref['mountpoint'] = None
- volume_ref['attach_status'] = 'detached'
- volume_ref['instance_uuid'] = None
- volume_ref['attached_host'] = None
- volume_ref['attach_time'] = None
+ if not remain_attachment:
+ # Hide status update from user if we're performing volume migration
+ if not volume_ref['migration_status']:
+ volume_ref['status'] = 'available'
+
+ volume_ref['attach_status'] = 'detached'
+ volume_ref.save(session=session)
+ else:
+ # Volume is still attached
+ volume_ref['status'] = 'in-use'
+ volume_ref['attach_status'] = 'attached'
+ volume_ref.save(session=session)
@require_context
options(joinedload('volume_metadata')).\
options(joinedload('volume_admin_metadata')).\
options(joinedload('volume_type')).\
+ options(joinedload('volume_attachment')).\
options(joinedload('consistencygroup'))
else:
return model_query(context, models.Volume, session=session,
project_only=project_only).\
options(joinedload('volume_metadata')).\
options(joinedload('volume_type')).\
+ options(joinedload('volume_attachment')).\
options(joinedload('consistencygroup'))
return result
+@require_context
+def volume_attachment_get(context, attachment_id, session=None):
+ result = model_query(context, models.VolumeAttachment,
+ session=session).\
+ filter_by(id=attachment_id).\
+ first()
+ if not result:
+ raise exception.VolumeAttachmentNotFound(filter='attachment_id = %s' %
+ attachment_id)
+ return result
+
+
+@require_context
+def volume_attachment_get_used_by_volume_id(context, volume_id, session=None):
+ result = model_query(context, models.VolumeAttachment,
+ session=session).\
+ filter_by(volume_id=volume_id).\
+ filter(models.VolumeAttachment.attach_status != 'detached').\
+ all()
+ return result
+
+
+@require_context
+def volume_attachment_get_by_host(context, volume_id, host):
+ session = get_session()
+ with session.begin():
+ result = model_query(context, models.VolumeAttachment,
+ session=session).\
+ filter_by(volume_id=volume_id).\
+ filter_by(attached_host=host).\
+ filter(models.VolumeAttachment.attach_status != 'detached').\
+ first()
+ return result
+
+
+@require_context
+def volume_attachment_get_by_instance_uuid(context, volume_id, instance_uuid):
+ session = get_session()
+ with session.begin():
+ result = model_query(context, models.VolumeAttachment,
+ session=session).\
+ filter_by(volume_id=volume_id).\
+ filter_by(instance_uuid=instance_uuid).\
+ filter(models.VolumeAttachment.attach_status != 'detached').\
+ first()
+ return result
+
+
@require_context
def volume_get(context, volume_id):
return _volume_get(context, volume_id)
return volume_ref
+@require_context
+def volume_attachment_update(context, attachment_id, values):
+ session = get_session()
+ with session.begin():
+ volume_attachment_ref = volume_attachment_get(context, attachment_id,
+ session=session)
+ volume_attachment_ref.update(values)
+ volume_attachment_ref.save(session=session)
+ return volume_attachment_ref
+
+
####################
def _volume_x_metadata_get_query(context, volume_id, model, session=None):
--- /dev/null
+# (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P.
+# 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.
+
+import datetime
+import uuid
+
+import six
+from sqlalchemy import Boolean, Column, DateTime
+from sqlalchemy import ForeignKey, MetaData, String, Table
+
+from cinder.i18n import _LE
+from cinder.openstack.common import log as logging
+
+LOG = logging.getLogger(__name__)
+
+CREATED_AT = datetime.datetime.now()
+
+
+def upgrade(migrate_engine):
+ """Add volume multi attachment table."""
+ meta = MetaData()
+ meta.bind = migrate_engine
+
+ # add the multiattach flag to the volumes table.
+ volumes = Table('volumes', meta, autoload=True)
+ multiattach = Column('multiattach', Boolean)
+ volumes.create_column(multiattach)
+ volumes.update().values(multiattach=False).execute()
+
+ # The new volume_attachment table
+ volume_attachment = Table(
+ 'volume_attachment', meta,
+ Column('created_at', DateTime),
+ Column('updated_at', DateTime),
+ Column('deleted_at', DateTime),
+ Column('deleted', Boolean),
+ Column('id', String(length=36), primary_key=True, nullable=False),
+ Column('volume_id', String(length=36), ForeignKey('volumes.id'),
+ nullable=False),
+ Column('attached_host', String(length=255)),
+ Column('instance_uuid', String(length=36)),
+ Column('mountpoint', String(length=255)),
+ Column('attach_time', DateTime),
+ Column('detach_time', DateTime),
+ Column('attach_mode', String(length=36)),
+ Column('attach_status', String(length=255)),
+ mysql_engine='InnoDB'
+ )
+
+ try:
+ volume_attachment.create()
+ except Exception:
+ LOG.error(_LE("Table volume_attachment not created!"))
+ raise
+
+ # now migrate existing volume attachment info into the
+ # new volume_attachment table
+ volumes_list = list(volumes.select().execute())
+ for volume in volumes_list:
+ if volume.attach_status == 'attached':
+ attachment = volume_attachment.insert()
+ values = {'id': six.text_type(uuid.uuid4()),
+ 'created_at': CREATED_AT,
+ 'deleted_at': None,
+ 'deleted': 0,
+ 'volume_id': volume.id,
+ 'attached_host': volume.host,
+ 'instance_uuid': volume.instance_uuid,
+ 'mountpoint': volume.mountpoint,
+ 'attach_time': volume.attach_time,
+ 'attach_mode': 'rw',
+ 'attach_status': 'attached',
+ }
+ attachment.execute(values)
+
+ # we have no reason to keep the columns that now
+ # exist in the volume_attachment table
+ mountpoint = volumes.columns.mountpoint
+ volumes.drop_column(mountpoint)
+ instance_uuid = volumes.columns.instance_uuid
+ volumes.drop_column(instance_uuid)
+ attach_time = volumes.columns.attach_time
+ volumes.drop_column(attach_time)
+ attached_host = volumes.columns.attached_host
+ volumes.drop_column(attached_host)
+
+
+def downgrade(migrate_engine):
+ """Remove volume_attachment table."""
+ meta = MetaData()
+ meta.bind = migrate_engine
+
+ # Put the needed volumes table columns back
+ volumes = Table('volumes', meta, autoload=True)
+ multiattach = volumes.columns.multiattach
+ volumes.drop_column(multiattach)
+
+ attached_host = Column('attached_host', String(length=255))
+ volumes.create_column(attached_host)
+ volumes.update().values(attached_host=None).execute()
+
+ attach_time = Column('attach_time', String(length=255))
+ volumes.create_column(attach_time)
+ volumes.update().values(attach_time=None).execute()
+
+ instance_uuid = Column('instance_uuid', String(length=36))
+ volumes.create_column(instance_uuid)
+ volumes.update().values(instance_uuid=None).execute()
+
+ mountpoint = Column('mountpoint', String(length=255))
+ volumes.create_column(mountpoint)
+ volumes.update().values(mountpoint=None).execute()
+
+ volume_attachment = Table('volume_attachment', meta, autoload=True)
+ attachments = list(volume_attachment.select().execute())
+ for attachment in attachments:
+ # we are going to lose data here for
+ # multiple attaches. We'll migrate and the
+ # last update wins.
+
+ if not attachment.deleted_at:
+ volume_id = attachment.volume_id
+ volumes.update().\
+ where(volumes.c.id == volume_id).\
+ values(mountpoint=attachment.mountpoint,
+ attached_host=attachment.attached_host,
+ attach_time=attachment.attach_time,
+ instance_uuid=attachment.instance_uuid).\
+ execute()
+ try:
+ volume_attachment.drop()
+
+ except Exception:
+ LOG.error(_LE("Dropping volume_attachment table failed."))
+ raise
--- /dev/null
+BEGIN TRANSACTION;
+
+CREATE TABLE volumes_v39 (
+ created_at DATETIME,
+ updated_at DATETIME,
+ deleted_at DATETIME,
+ deleted BOOLEAN,
+ id VARCHAR(36) NOT NULL,
+ ec2_id INTEGER,
+ user_id VARCHAR(255),
+ project_id VARCHAR(255),
+ snapshot_id VARCHAR(36),
+ host VARCHAR(255),
+ size INTEGER,
+ availability_zone VARCHAR(255),
+ status VARCHAR(255),
+ attach_status VARCHAR(255),
+ scheduled_at DATETIME,
+ launched_at DATETIME,
+ terminated_at DATETIME,
+ display_name VARCHAR(255),
+ display_description VARCHAR(255),
+ provider_location VARCHAR(255),
+ provider_auth VARCHAR(255),
+ volume_type_id VARCHAR(36),
+ source_volid VARCHAR(36),
+ bootable INTEGER,
+ provider_geometry VARCHAR(255),
+ _name_id VARCHAR(36),
+ encryption_key_id VARCHAR(36),
+ migration_status VARCHAR(255),
+ attached_host VARCHAR(255),
+ attach_time VARCHAR(255),
+ instance_uuid VARCHAR(36),
+ mountpoint VARCHAR(255),
+ consistencygroup_id VARCHAR(36),
+ replication_status VARCHAR(255),
+ replication_extended_status VARCHAR(255),
+ replication_driver_data VARCHAR(255),
+ PRIMARY KEY (id)
+);
+
+INSERT INTO volumes_v39
+ SELECT volumes.created_at,
+ volumes.updated_at,
+ volumes.deleted_at,
+ volumes.deleted,
+ volumes.id,
+ volumes.ec2_id,
+ volumes.user_id,
+ volumes.project_id,
+ volumes.snapshot_id,
+ volumes.host,
+ volumes.size,
+ volumes.availability_zone,
+ volumes.status,
+ volumes.attach_status,
+ volumes.scheduled_at,
+ volumes.launched_at,
+ volumes.terminated_at,
+ volumes.display_name,
+ volumes.display_description,
+ volumes.provider_location,
+ volumes.provider_auth,
+ volumes.volume_type_id,
+ volumes.source_volid,
+ volumes.bootable,
+ volumes.provider_geometry,
+ volumes._name_id,
+ volumes.encryption_key_id,
+ volumes.migration_status,
+ volume_attachment.attached_host,
+ volume_attachment.attach_time,
+ volume_attachment.instance_uuid,
+ volume_attachment.mountpoint,
+ volumes.consistencygroup_id,
+ volumes.replication_status,
+ volumes.replication_extended_status,
+ volumes.replication_driver_data
+ FROM volumes
+ LEFT OUTER JOIN volume_attachment
+ ON volumes.id=volume_attachment.volume_id;
+
+DROP TABLE volumes;
+ALTER TABLE volumes_v39 RENAME TO volumes;
+DROP TABLE volume_attachment;
+COMMIT;
host = Column(String(255)) # , ForeignKey('hosts.id'))
size = Column(Integer)
availability_zone = Column(String(255)) # TODO(vish): foreign key?
- instance_uuid = Column(String(36))
- attached_host = Column(String(255))
- mountpoint = Column(String(255))
- attach_time = Column(String(255)) # TODO(vish): datetime
status = Column(String(255)) # TODO(vish): enum?
attach_status = Column(String(255)) # TODO(vish): enum
migration_status = Column(String(255))
deleted = Column(Boolean, default=False)
bootable = Column(Boolean, default=False)
+ multiattach = Column(Boolean, default=False)
replication_status = Column(String(255))
replication_extended_status = Column(String(255))
'VolumeAdminMetadata.deleted == False)')
+class VolumeAttachment(BASE, CinderBase):
+ """Represents a volume attachment for a vm."""
+ __tablename__ = 'volume_attachment'
+ id = Column(String(36), primary_key=True)
+
+ volume_id = Column(String(36), ForeignKey('volumes.id'), nullable=False)
+ volume = relationship(Volume, backref="volume_attachment",
+ foreign_keys=volume_id,
+ primaryjoin='and_('
+ 'VolumeAttachment.volume_id == Volume.id,'
+ 'VolumeAttachment.deleted == False)')
+ instance_uuid = Column(String(36))
+ attached_host = Column(String(255))
+ mountpoint = Column(String(255))
+ attach_time = Column(DateTime)
+ detach_time = Column(DateTime)
+ attach_status = Column(String(255))
+ attach_mode = Column(String(255))
+
+
class VolumeTypes(BASE, CinderBase):
"""Represent possible volume_types of volumes offered."""
__tablename__ = "volume_types"
Volume,
VolumeMetadata,
VolumeAdminMetadata,
+ VolumeAttachment,
SnapshotMetadata,
Transfer,
VolumeTypeExtraSpecs,
message = _("Volume %(volume_id)s could not be found.")
+class VolumeAttachmentNotFound(NotFound):
+ message = _("Volume attachment could not be found with "
+ "filter: %(filter)s .")
+
+
class VolumeMetadataNotFound(NotFound):
message = _("Volume %(volume_id)s has no metadata with "
"key %(metadata_key)s.")
'host': fields.StringField(nullable=True),
'size': fields.IntegerField(),
'availability_zone': fields.StringField(),
- 'instance_uuid': fields.UUIDField(nullable=True),
- 'attached_host': fields.StringField(nullable=True),
- 'mountpoint': fields.StringField(nullable=True),
- 'attach_time': fields.StringField(nullable=True),
'status': fields.StringField(),
'attach_status': fields.StringField(),
'migration_status': fields.StringField(nullable=True),
svc = self.start_service('volume', host='test')
self.volume_api.reserve_volume(ctx, volume)
mountpoint = '/dev/vbd'
- self.volume_api.attach(ctx, volume, stubs.FAKE_UUID, None,
- mountpoint, 'rw')
+ attachment = self.volume_api.attach(ctx, volume, stubs.FAKE_UUID,
+ None, mountpoint, 'rw')
# volume is attached
volume = db.volume_get(ctx, volume['id'])
self.assertEqual(volume['status'], 'in-use')
- self.assertEqual(volume['instance_uuid'], stubs.FAKE_UUID)
- self.assertIsNone(volume['attached_host'])
- self.assertEqual(volume['mountpoint'], mountpoint)
- self.assertEqual(volume['attach_status'], 'attached')
+ self.assertEqual(attachment['instance_uuid'], stubs.FAKE_UUID)
+ self.assertEqual(attachment['mountpoint'], mountpoint)
+ self.assertEqual(attachment['attach_status'], 'attached')
admin_metadata = volume['volume_admin_metadata']
self.assertEqual(len(admin_metadata), 2)
self.assertEqual(admin_metadata[0]['key'], 'readonly')
req.method = 'POST'
req.headers['content-type'] = 'application/json'
# request status of 'error'
- req.body = jsonutils.dumps({'os-force_detach': None})
+ req.body = jsonutils.dumps({'os-force_detach':
+ {'attachment_id': attachment['id']}})
# attach admin context to request
req.environ['cinder.context'] = ctx
# make request
# request is accepted
self.assertEqual(resp.status_int, 202)
volume = db.volume_get(ctx, volume['id'])
+ self.assertRaises(exception.VolumeAttachmentNotFound,
+ db.volume_attachment_get,
+ ctx, attachment['id'])
+
# status changed to 'available'
self.assertEqual(volume['status'], 'available')
- self.assertIsNone(volume['instance_uuid'])
- self.assertIsNone(volume['attached_host'])
- self.assertIsNone(volume['mountpoint'])
- self.assertEqual(volume['attach_status'], 'detached')
admin_metadata = volume['volume_admin_metadata']
self.assertEqual(len(admin_metadata), 1)
self.assertEqual(admin_metadata[0]['key'], 'readonly')
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
# start service to handle rpc messages for attach requests
svc = self.start_service('volume', host='test')
- self.volume_api.reserve_volume(ctx, volume)
+ self.volume_api.initialize_connection(ctx, volume, connector)
mountpoint = '/dev/vbd'
host_name = 'fake-host'
- self.volume_api.attach(ctx, volume, None, host_name, mountpoint, 'ro')
+ attachment = self.volume_api.attach(ctx, volume, None, host_name,
+ mountpoint, 'ro')
# volume is attached
volume = db.volume_get(ctx, volume['id'])
self.assertEqual(volume['status'], 'in-use')
- self.assertIsNone(volume['instance_uuid'])
- self.assertEqual(volume['attached_host'], host_name)
- self.assertEqual(volume['mountpoint'], mountpoint)
- self.assertEqual(volume['attach_status'], 'attached')
+ self.assertIsNone(attachment['instance_uuid'])
+ self.assertEqual(attachment['attached_host'], host_name)
+ self.assertEqual(attachment['mountpoint'], mountpoint)
+ self.assertEqual(attachment['attach_status'], 'attached')
admin_metadata = volume['volume_admin_metadata']
self.assertEqual(len(admin_metadata), 2)
self.assertEqual(admin_metadata[0]['key'], 'readonly')
req.method = 'POST'
req.headers['content-type'] = 'application/json'
# request status of 'error'
- req.body = jsonutils.dumps({'os-force_detach': None})
+ req.body = jsonutils.dumps({'os-force_detach':
+ {'attachment_id': attachment['id']}})
# attach admin context to request
req.environ['cinder.context'] = ctx
# make request
# request is accepted
self.assertEqual(resp.status_int, 202)
volume = db.volume_get(ctx, volume['id'])
+ self.assertRaises(exception.VolumeAttachmentNotFound,
+ db.volume_attachment_get,
+ ctx, attachment['id'])
# status changed to 'available'
self.assertEqual(volume['status'], 'available')
- self.assertIsNone(volume['instance_uuid'])
- self.assertIsNone(volume['attached_host'])
- self.assertIsNone(volume['mountpoint'])
- self.assertEqual(volume['attach_status'], 'detached')
admin_metadata = volume['volume_admin_metadata']
self.assertEqual(len(admin_metadata), 1)
self.assertEqual(admin_metadata[0]['key'], 'readonly')
# start service to handle rpc messages for attach requests
svc = self.start_service('volume', host='test')
self.volume_api.reserve_volume(ctx, volume)
- mountpoint = '/dev/vbd'
- self.volume_api.attach(ctx, volume, stubs.FAKE_UUID, None,
- mountpoint, 'rw')
conn_info = self.volume_api.initialize_connection(ctx,
volume, connector)
+ self.volume_api.attach(ctx, volume, fakes.get_fake_uuid(), None,
+ '/dev/vbd0', 'rw')
self.assertEqual(conn_info['data']['access_mode'], 'rw')
self.assertRaises(exception.InvalidVolume,
self.volume_api.attach,
volume,
fakes.get_fake_uuid(),
None,
- mountpoint,
- 'rw')
- self.assertRaises(exception.InvalidVolume,
- self.volume_api.attach,
- ctx,
- volume,
- fakes.get_fake_uuid(),
- None,
- mountpoint,
+ '/dev/vdb1',
'ro')
# cleanup
svc.stop()
# start service to handle rpc messages for attach requests
svc = self.start_service('volume', host='test')
self.volume_api.reserve_volume(ctx, volume)
- mountpoint = '/dev/vbd'
- host_name = 'fake_host'
- self.volume_api.attach(ctx, volume, None, host_name, mountpoint, 'rw')
+ self.volume_api.initialize_connection(ctx, volume, connector)
+ self.volume_api.attach(ctx, volume, None, 'fake_host1',
+ '/dev/vbd0', 'rw')
conn_info = self.volume_api.initialize_connection(ctx,
volume, connector)
conn_info['data']['access_mode'] = 'rw'
ctx,
volume,
None,
- host_name,
- mountpoint,
- 'rw')
- self.assertRaises(exception.InvalidVolume,
- self.volume_api.attach,
- ctx,
- volume,
- None,
- host_name,
- mountpoint,
+ 'fake_host2',
+ '/dev/vbd1',
'ro')
# cleanup
svc.stop()
'provider_location': '', 'size': 1})
# start service to handle rpc messages for attach requests
svc = self.start_service('volume', host='test')
- values = {'status': 'attaching',
- 'instance_uuid': fakes.get_fake_uuid()}
- db.volume_update(ctx, volume['id'], values)
+ self.volume_api.reserve_volume(ctx, volume)
+ values = {'volume_id': volume['id'],
+ 'attach_status': 'attaching',
+ 'attach_time': timeutils.utcnow(),
+ 'instance_uuid': 'abc123',
+ }
+ db.volume_attach(ctx, values)
+ db.volume_admin_metadata_update(ctx, volume['id'],
+ {"attached_mode": 'rw'}, False)
mountpoint = '/dev/vbd'
- self.assertRaises(exception.InvalidVolume,
- self.volume_api.attach,
- ctx,
- volume,
- stubs.FAKE_UUID,
- None,
- mountpoint,
- 'rw')
- # cleanup
+ attachment = self.volume_api.attach(ctx, volume,
+ stubs.FAKE_UUID, None,
+ mountpoint, 'rw')
+
+ self.assertEqual(stubs.FAKE_UUID, attachment['instance_uuid'])
+ self.assertEqual(volume['id'], attachment['volume_id'], volume['id'])
+ self.assertEqual('attached', attachment['attach_status'])
svc.stop()
def test_attach_attaching_volume_with_different_mode(self):
class VolumeActionsTest(test.TestCase):
- _actions = ('os-detach', 'os-reserve', 'os-unreserve')
+ _actions = ('os-reserve', 'os-unreserve')
_methods = ('attach', 'detach', 'reserve_volume', 'unreserve_volume')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
+ def test_detach(self):
+ body = {'os-detach': {'attachment_id': 'fakeuuid'}}
+ req = webob.Request.blank('/v2/fake/volumes/1/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, res.status_int)
+
def test_attach_with_invalid_arguments(self):
# Invalid request to attach volume an invalid target
body = {'os-attach': {'mountpoint': '/dev/vdc'}}
'host': 'fakehost',
'size': 1,
'availability_zone': 'fakeaz',
- 'instance_uuid': 'fakeuuid',
- 'attached_host': None,
- 'mountpoint': '/',
'attached_mode': 'rw',
'status': 'fakestatus',
'migration_status': None,
'volume_type_id': '3e196c20-3c06-11e2-81c1-0800200c9a66',
'volume_metadata': [],
'volume_type': {'name': 'vol_type_name'},
+ 'volume_attachment': [],
+ 'multiattach': False,
'readonly': 'False'}
volume.update(kwargs)
'availability_zone': 'zone1:host1',
'display_name': 'Volume Test Name',
'encrypted': False,
- 'attachments': [{'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'}],
+ 'attachments': [],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'availability_zone': 'nova',
'display_name': 'Volume Test Name',
'encrypted': False,
- 'attachments': [{'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'}],
+ 'attachments': [],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': 'vol_type_name',
'image_id': test_id,
'availability_zone': 'fakeaz',
'display_name': 'Updated Test Name',
'encrypted': False,
- 'attachments': [{
- 'id': '1',
- 'volume_id': '1',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'device': '/'
- }],
+ 'attachments': [],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'availability_zone': 'fakeaz',
'display_name': 'displayname',
'encrypted': False,
- 'attachments': [{
- 'id': '1',
- 'volume_id': '1',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'device': '/'
- }],
+ 'attachments': [],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
{"readonly": "True",
"invisible_key": "invisible_value"},
False)
+ values = {'volume_id': '1', }
+ attachment = db.volume_attach(context.get_admin_context(), values)
+ db.volume_attached(context.get_admin_context(),
+ attachment['id'], stubs.FAKE_UUID, None, '/')
updates = {
"display_name": "Updated Test Name",
req.environ['cinder.context'] = admin_ctx
res_dict = self.controller.update(req, '1', body)
expected = {'volume': {
- 'status': 'fakestatus',
+ 'status': 'in-use',
'display_description': 'displaydesc',
'availability_zone': 'fakeaz',
'display_name': 'Updated Test Name',
'encrypted': False,
'attachments': [{
+ 'attachment_id': attachment['id'],
'id': '1',
'volume_id': '1',
- 'server_id': 'fakeuuid',
+ 'server_id': stubs.FAKE_UUID,
'host_name': None,
'device': '/'
}],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': None,
'snapshot_id': None,
'availability_zone': 'fakeaz',
'display_name': 'displayname',
'encrypted': False,
- 'attachments': [{'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'}],
+ 'attachments': [],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
{"readonly": "True",
"invisible_key": "invisible_value"},
False)
+ values = {'volume_id': '1', }
+ attachment = db.volume_attach(context.get_admin_context(), values)
+ db.volume_attached(context.get_admin_context(),
+ attachment['id'], stubs.FAKE_UUID, None, '/')
req = fakes.HTTPRequest.blank('/v1/volumes')
admin_ctx = context.RequestContext('admin', 'fakeproject', True)
req.environ['cinder.context'] = admin_ctx
res_dict = self.controller.index(req)
- expected = {'volumes': [{'status': 'fakestatus',
+ expected = {'volumes': [{'status': 'in-use',
'display_description': 'displaydesc',
'availability_zone': 'fakeaz',
'display_name': 'displayname',
'encrypted': False,
- 'attachments': [{'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'}],
+ 'attachments': [
+ {'attachment_id': attachment['id'],
+ 'device': '/',
+ 'server_id': stubs.FAKE_UUID,
+ 'host_name': None,
+ 'id': '1',
+ 'volume_id': '1'}],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': None,
'snapshot_id': None,
'availability_zone': 'fakeaz',
'display_name': 'displayname',
'encrypted': False,
- 'attachments': [{'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'}],
+ 'attachments': [],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
{"readonly": "True",
"invisible_key": "invisible_value"},
False)
+ values = {'volume_id': '1', }
+ attachment = db.volume_attach(context.get_admin_context(), values)
+ db.volume_attached(context.get_admin_context(),
+ attachment['id'], stubs.FAKE_UUID, None, '/')
req = fakes.HTTPRequest.blank('/v1/volumes/detail')
admin_ctx = context.RequestContext('admin', 'fakeproject', True)
req.environ['cinder.context'] = admin_ctx
res_dict = self.controller.index(req)
- expected = {'volumes': [{'status': 'fakestatus',
+ expected = {'volumes': [{'status': 'in-use',
'display_description': 'displaydesc',
'availability_zone': 'fakeaz',
'display_name': 'displayname',
'encrypted': False,
- 'attachments': [{'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'}],
+ 'attachments': [
+ {'attachment_id': attachment['id'],
+ 'device': '/',
+ 'server_id': stubs.FAKE_UUID,
+ 'host_name': None,
+ 'id': '1',
+ 'volume_id': '1'}],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': None,
'snapshot_id': None,
'availability_zone': 'fakeaz',
'display_name': 'displayname',
'encrypted': False,
- 'attachments': [{'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'}],
+ 'attachments': [],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'display_name': 'displayname',
'encrypted': False,
'attachments': [],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'availability_zone': 'fakeaz',
'display_name': 'displayname',
'encrypted': False,
- 'attachments': [{'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'}],
+ 'attachments': [],
+ 'multiattach': 'false',
'bootable': 'true',
'volume_type': 'vol_type_name',
'snapshot_id': None,
{"readonly": "True",
"invisible_key": "invisible_value"},
False)
+ values = {'volume_id': '1', }
+ attachment = db.volume_attach(context.get_admin_context(), values)
+ db.volume_attached(context.get_admin_context(),
+ attachment['id'], stubs.FAKE_UUID, None, '/')
req = fakes.HTTPRequest.blank('/v1/volumes/1')
admin_ctx = context.RequestContext('admin', 'fakeproject', True)
req.environ['cinder.context'] = admin_ctx
res_dict = self.controller.show(req, '1')
- expected = {'volume': {'status': 'fakestatus',
+ expected = {'volume': {'status': 'in-use',
'display_description': 'displaydesc',
'availability_zone': 'fakeaz',
'display_name': 'displayname',
'encrypted': False,
- 'attachments': [{'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'}],
+ 'attachments': [
+ {'attachment_id': attachment['id'],
+ 'device': '/',
+ 'server_id': stubs.FAKE_UUID,
+ 'host_name': None,
+ 'id': '1',
+ 'volume_id': '1'}],
+ 'multiattach': 'false',
'bootable': 'false',
'volume_type': None,
'snapshot_id': None,
'host': 'fakehost',
'size': 1,
'availability_zone': 'fakeaz',
- 'instance_uuid': 'fakeuuid',
- 'attached_host': None,
- 'mountpoint': '/',
'status': 'fakestatus',
'migration_status': None,
'attach_status': 'attached',
'volume_type': {'name': 'vol_type_name'},
'replication_status': 'disabled',
'replication_extended_status': None,
- 'replication_driver_data': None}
+ 'replication_driver_data': None,
+ 'volume_attachment': [],
+ 'multiattach': False,
+ }
volume.update(kwargs)
if kwargs.get('volume_glance_metadata', None):
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v2/volumes')
res_dict = self.controller.create(req, body)
- ex = {'volume': {'attachments':
- [{'device': '/',
- 'host_name': None,
- 'id': '1',
- 'server_id': 'fakeuuid',
- 'volume_id': '1'}],
+ ex = {'volume': {'attachments': [],
'availability_zone': 'zone1:host1',
'bootable': 'false',
'consistencygroup_id': None,
'metadata': {},
'name': 'Volume Test Name',
'replication_status': 'disabled',
+ 'multiattach': False,
'size': 100,
'snapshot_id': None,
'source_volid': None,
"description": "Volume Test Desc",
"availability_zone": "nova",
"imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'}
- ex = {'volume': {'attachments': [{'device': '/',
- 'host_name': None,
- 'id': '1',
- 'server_id': 'fakeuuid',
- 'volume_id': '1'}],
+ ex = {'volume': {'attachments': [],
'availability_zone': 'nova',
'bootable': 'false',
'consistencygroup_id': None,
'metadata': {},
'name': 'Volume Test Name',
'replication_status': 'disabled',
+ 'multiattach': False,
'size': '1',
'snapshot_id': None,
'source_volid': None,
"description": "Volume Test Desc",
"availability_zone": "nova",
"image_id": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'}
- ex = {'volume': {'attachments': [{'device': '/',
- 'host_name': None,
- 'id': '1',
- 'server_id': 'fakeuuid',
- 'volume_id': '1'}],
+ ex = {'volume': {'attachments': [],
'availability_zone': 'nova',
'bootable': 'false',
'consistencygroup_id': None,
'metadata': {},
'name': 'Volume Test Name',
'replication_status': 'disabled',
+ 'multiattach': False,
'size': '1',
'snapshot_id': None,
'source_volid': None,
"description": "Volume Test Desc",
"availability_zone": "nova",
"imageRef": test_id}
- ex = {'volume': {'attachments': [{'device': '/',
- 'host_name': None,
- 'id': '1',
- 'server_id': 'fakeuuid',
- 'volume_id': '1'}],
+ ex = {'volume': {'attachments': [],
'availability_zone': 'nova',
'bootable': 'false',
'consistencygroup_id': None,
'metadata': {},
'name': 'Volume Test Name',
'replication_status': 'disabled',
+ 'multiattach': False,
'size': '1',
'snapshot_id': None,
'source_volid': None,
'consistencygroup_id': None,
'name': 'Updated Test Name',
'replication_status': 'disabled',
- 'attachments': [
- {
- 'id': '1',
- 'volume_id': '1',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'device': '/',
- }
- ],
+ 'multiattach': False,
+ 'attachments': [],
'user_id': 'fakeuser',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'consistencygroup_id': None,
'name': 'Updated Test Name',
'replication_status': 'disabled',
- 'attachments': [
- {
- 'id': '1',
- 'volume_id': '1',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'device': '/',
- }
- ],
+ 'multiattach': False,
+ 'attachments': [],
'user_id': 'fakeuser',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'consistencygroup_id': None,
'name': 'New Name',
'replication_status': 'disabled',
- 'attachments': [
- {
- 'id': '1',
- 'volume_id': '1',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'device': '/',
- }
- ],
+ 'multiattach': False,
+ 'attachments': [],
'user_id': 'fakeuser',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'consistencygroup_id': None,
'name': 'displayname',
'replication_status': 'disabled',
- 'attachments': [{
- 'id': '1',
- 'volume_id': '1',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'device': '/',
- }],
+ 'multiattach': False,
+ 'attachments': [],
'user_id': 'fakeuser',
'volume_type': 'vol_type_name',
'snapshot_id': None,
{"readonly": "True",
"invisible_key": "invisible_value"},
False)
+ values = {'volume_id': '1', }
+ attachment = db.volume_attach(context.get_admin_context(), values)
+ db.volume_attached(context.get_admin_context(),
+ attachment['id'], stubs.FAKE_UUID, None, '/')
updates = {
"name": "Updated Test Name",
req.environ['cinder.context'] = admin_ctx
res_dict = self.controller.update(req, '1', body)
expected = {'volume': {
- 'status': 'fakestatus',
+ 'status': 'in-use',
'description': 'displaydesc',
'encrypted': False,
'availability_zone': 'fakeaz',
'consistencygroup_id': None,
'name': 'Updated Test Name',
'replication_status': 'disabled',
+ 'multiattach': False,
'attachments': [{
'id': '1',
+ 'attachment_id': attachment['id'],
'volume_id': '1',
- 'server_id': 'fakeuuid',
+ 'server_id': stubs.FAKE_UUID,
'host_name': None,
'device': '/',
}],
}
],
}}
- self.assertEqual(res_dict, expected)
- self.assertEqual(len(fake_notifier.NOTIFICATIONS), 2)
+ self.assertEqual(expected, res_dict)
+ self.assertEqual(2, len(fake_notifier.NOTIFICATIONS))
def test_update_empty_body(self):
body = {}
'consistencygroup_id': None,
'name': 'displayname',
'replication_status': 'disabled',
- 'attachments': [
- {
- 'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'
- }
- ],
+ 'multiattach': False,
+ 'attachments': [],
'user_id': 'fakeuser',
'volume_type': 'vol_type_name',
'snapshot_id': None,
{"readonly": "True",
"invisible_key": "invisible_value"},
False)
+ values = {'volume_id': '1', }
+ attachment = db.volume_attach(context.get_admin_context(), values)
+ db.volume_attached(context.get_admin_context(),
+ attachment['id'], stubs.FAKE_UUID, None, '/')
req = fakes.HTTPRequest.blank('/v2/volumes/detail')
admin_ctx = context.RequestContext('admin', 'fakeproject', True)
expected = {
'volumes': [
{
- 'status': 'fakestatus',
+ 'status': 'in-use',
'description': 'displaydesc',
'encrypted': False,
'availability_zone': 'fakeaz',
'consistencygroup_id': None,
'name': 'displayname',
'replication_status': 'disabled',
+ 'multiattach': False,
'attachments': [
{
+ 'attachment_id': attachment['id'],
'device': '/',
- 'server_id': 'fakeuuid',
+ 'server_id': stubs.FAKE_UUID,
'host_name': None,
'id': '1',
'volume_id': '1'
'consistencygroup_id': None,
'name': 'displayname',
'replication_status': 'disabled',
- 'attachments': [
- {
- 'device': '/',
- 'server_id': 'fakeuuid',
- 'host_name': None,
- 'id': '1',
- 'volume_id': '1'
- }
- ],
+ 'multiattach': False,
+ 'attachments': [],
'user_id': 'fakeuser',
'volume_type': 'vol_type_name',
'snapshot_id': None,
'consistencygroup_id': None,
'name': 'displayname',
'replication_status': 'disabled',
+ 'multiattach': False,
'attachments': [],
'user_id': 'fakeuser',
'volume_type': 'vol_type_name',
{"readonly": "True",
"invisible_key": "invisible_value"},
False)
+ values = {'volume_id': '1', }
+ attachment = db.volume_attach(context.get_admin_context(), values)
+ db.volume_attached(context.get_admin_context(),
+ attachment['id'], stubs.FAKE_UUID, None, '/')
req = fakes.HTTPRequest.blank('/v2/volumes/1')
admin_ctx = context.RequestContext('admin', 'fakeproject', True)
res_dict = self.controller.show(req, '1')
expected = {
'volume': {
- 'status': 'fakestatus',
+ 'status': 'in-use',
'description': 'displaydesc',
'encrypted': False,
'availability_zone': 'fakeaz',
'consistencygroup_id': None,
'name': 'displayname',
'replication_status': 'disabled',
+ 'multiattach': False,
'attachments': [
{
+ 'attachment_id': attachment['id'],
'device': '/',
- 'server_id': 'fakeuuid',
+ 'server_id': stubs.FAKE_UUID,
'host_name': None,
'id': '1',
'volume_id': '1'
vol['attach_status'] = 'detached'
return db.volume_create(self.ctxt, vol)['id']
+ def _create_volume_attach(self, volume_id):
+ values = {'volume_id': volume_id,
+ 'attach_status': 'attached', }
+ attachment = db.volume_attach(self.ctxt, values)
+ db.volume_attached(self.ctxt, attachment['id'], None, 'testhost',
+ '/dev/vd0')
+
def _create_exported_record_entry(self, vol_size=1):
"""Create backup metadata export entry."""
vol_id = self._create_volume_db_entry(status='available',
"""Make sure stuck volumes and backups are reset to correct
states when backup_manager.init_host() is called
"""
- vol1_id = self._create_volume_db_entry(status='backing-up')
- vol2_id = self._create_volume_db_entry(status='restoring-backup')
+ vol1_id = self._create_volume_db_entry()
+ self._create_volume_attach(vol1_id)
+ db.volume_update(self.ctxt, vol1_id, {'status': 'backing-up'})
+ vol2_id = self._create_volume_db_entry()
+ self._create_volume_attach(vol2_id)
+ db.volume_update(self.ctxt, vol2_id, {'status': 'restoring-backup'})
backup1_id = self._create_backup_db_entry(status='creating')
backup2_id = self._create_backup_db_entry(status='restoring')
backup3_id = self._create_backup_db_entry(status='deleting')
def test_volume_attached_to_instance(self):
volume = db.volume_create(self.ctxt, {'host': 'host1'})
instance_uuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
- db.volume_attached(self.ctxt, volume['id'],
+ values = {'volume_id': volume['id'],
+ 'instance_uuid': instance_uuid,
+ 'attach_status': 'attaching', }
+ attachment = db.volume_attach(self.ctxt, values)
+ db.volume_attached(self.ctxt, attachment['id'],
instance_uuid, None, '/tmp')
volume = db.volume_get(self.ctxt, volume['id'])
- self.assertEqual(volume['status'], 'in-use')
- self.assertEqual(volume['mountpoint'], '/tmp')
- self.assertEqual(volume['attach_status'], 'attached')
- self.assertEqual(volume['instance_uuid'], instance_uuid)
- self.assertIsNone(volume['attached_host'])
+ attachment = db.volume_attachment_get(self.ctxt, attachment['id'])
+ self.assertEqual('in-use', volume['status'])
+ self.assertEqual('/tmp', attachment['mountpoint'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertEqual(instance_uuid, attachment['instance_uuid'])
+ self.assertIsNone(attachment['attached_host'])
def test_volume_attached_to_host(self):
volume = db.volume_create(self.ctxt, {'host': 'host1'})
host_name = 'fake_host'
- db.volume_attached(self.ctxt, volume['id'],
+ values = {'volume_id': volume['id'],
+ 'attached_host': host_name,
+ 'attach_status': 'attaching', }
+ attachment = db.volume_attach(self.ctxt, values)
+ db.volume_attached(self.ctxt, attachment['id'],
None, host_name, '/tmp')
volume = db.volume_get(self.ctxt, volume['id'])
- self.assertEqual(volume['status'], 'in-use')
- self.assertEqual(volume['mountpoint'], '/tmp')
- self.assertEqual(volume['attach_status'], 'attached')
- self.assertIsNone(volume['instance_uuid'])
- self.assertEqual(volume['attached_host'], host_name)
+ attachment = db.volume_attachment_get(self.ctxt, attachment['id'])
+ self.assertEqual('in-use', volume['status'])
+ self.assertEqual('/tmp', attachment['mountpoint'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertIsNone(attachment['instance_uuid'])
+ self.assertEqual(attachment['attached_host'], host_name)
def test_volume_data_get_for_host(self):
for i in xrange(3):
def test_volume_detached_from_instance(self):
volume = db.volume_create(self.ctxt, {})
- db.volume_attached(self.ctxt, volume['id'],
- 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
+ instance_uuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ values = {'volume_id': volume['id'],
+ 'instance_uuid': instance_uuid,
+ 'attach_status': 'attaching', }
+ attachment = db.volume_attach(self.ctxt, values)
+ db.volume_attached(self.ctxt, attachment['id'],
+ instance_uuid,
None, '/tmp')
- db.volume_detached(self.ctxt, volume['id'])
+ db.volume_detached(self.ctxt, volume['id'], attachment['id'])
volume = db.volume_get(self.ctxt, volume['id'])
+ self.assertRaises(exception.VolumeAttachmentNotFound,
+ db.volume_attachment_get,
+ self.ctxt,
+ attachment['id'])
self.assertEqual('available', volume['status'])
- self.assertEqual('detached', volume['attach_status'])
- self.assertIsNone(volume['mountpoint'])
- self.assertIsNone(volume['instance_uuid'])
- self.assertIsNone(volume['attached_host'])
def test_volume_detached_from_host(self):
volume = db.volume_create(self.ctxt, {})
- db.volume_attached(self.ctxt, volume['id'],
- None, 'fake_host', '/tmp')
- db.volume_detached(self.ctxt, volume['id'])
+ host_name = 'fake_host'
+ values = {'volume_id': volume['id'],
+ 'attach_host': host_name,
+ 'attach_status': 'attaching', }
+ attachment = db.volume_attach(self.ctxt, values)
+ db.volume_attached(self.ctxt, attachment['id'],
+ None, host_name, '/tmp')
+ db.volume_detached(self.ctxt, volume['id'], attachment['id'])
volume = db.volume_get(self.ctxt, volume['id'])
+ self.assertRaises(exception.VolumeAttachmentNotFound,
+ db.volume_attachment_get,
+ self.ctxt,
+ attachment['id'])
self.assertEqual('available', volume['status'])
- self.assertEqual('detached', volume['attach_status'])
- self.assertIsNone(volume['mountpoint'])
- self.assertIsNone(volume['instance_uuid'])
- self.assertIsNone(volume['attached_host'])
def test_volume_get(self):
volume = db.volume_create(self.ctxt, {})
with mock.patch.object(hpcommon.HP3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
- self.driver.detach_volume(context.get_admin_context(), self.volume)
+ self.driver.detach_volume(context.get_admin_context(), self.volume,
+ None)
expected = [
mock.call.removeVolumeMetaData(
self.VOLUME_3PAR_NAME,
self.assertRaises(exception.CinderException,
self.driver.detach_volume,
context.get_admin_context(),
- self.volume)
+ self.volume, None)
def test_create_snapshot(self):
# setup_mock_client drive with default configuration
backups = db_utils.get_table(engine, 'backups')
self.assertNotIn('parent_id', backups.c)
+ def _check_40(self, engine, data):
+ volumes = db_utils.get_table(engine, 'volumes')
+ self.assertNotIn('instance_uuid', volumes.c)
+ self.assertNotIn('attached_host', volumes.c)
+ self.assertNotIn('attach_time', volumes.c)
+ self.assertNotIn('mountpoint', volumes.c)
+ self.assertIsInstance(volumes.c.multiattach.type,
+ self.BOOL_TYPE)
+
+ attachments = db_utils.get_table(engine, 'volume_attachment')
+ self.assertIsInstance(attachments.c.attach_mode.type,
+ sqlalchemy.types.VARCHAR)
+ self.assertIsInstance(attachments.c.instance_uuid.type,
+ sqlalchemy.types.VARCHAR)
+ self.assertIsInstance(attachments.c.attached_host.type,
+ sqlalchemy.types.VARCHAR)
+ self.assertIsInstance(attachments.c.mountpoint.type,
+ sqlalchemy.types.VARCHAR)
+ self.assertIsInstance(attachments.c.attach_status.type,
+ sqlalchemy.types.VARCHAR)
+
+ def _post_downgrade_040(self, engine):
+ self.assertFalse(engine.dialect.has_table(engine.connect(),
+ "volume_attachment"))
+ volumes = db_utils.get_table(engine, 'volumes')
+ self.assertNotIn('multiattach', volumes.c)
+ self.assertIsInstance(volumes.c.instance_uuid.type,
+ sqlalchemy.types.VARCHAR)
+ self.assertIsInstance(volumes.c.attached_host.type,
+ sqlalchemy.types.VARCHAR)
+ self.assertIsInstance(volumes.c.attach_time.type,
+ sqlalchemy.types.VARCHAR)
+ self.assertIsInstance(volumes.c.mountpoint.type,
+ sqlalchemy.types.VARCHAR)
+
def test_walk_versions(self):
self.walk_versions(True, False)
'volume_id': volume_id,
'volume_type': None,
'snapshot_id': None,
- 'instance_uuid': None,
'user_id': 'fake',
'launched_at': 'DONTCARE',
'size': 1,
**self.volume_params)
volume_id = volume['id']
self.volume.create_volume(self.context, volume_id)
- self.volume.attach_volume(self.context, volume_id, instance_uuid,
- None, mountpoint, 'ro')
+ attachment = self.volume.attach_volume(self.context, volume_id,
+ instance_uuid, None,
+ mountpoint, 'ro')
vol = db.volume_get(context.get_admin_context(), volume_id)
self.assertEqual(vol['status'], "in-use")
- self.assertEqual(vol['attach_status'], "attached")
- self.assertEqual(vol['mountpoint'], mountpoint)
- self.assertEqual(vol['instance_uuid'], instance_uuid)
- self.assertIsNone(vol['attached_host'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertEqual(instance_uuid, attachment['instance_uuid'])
+ self.assertIsNone(attachment['attached_host'])
admin_metadata = vol['volume_admin_metadata']
self.assertEqual(len(admin_metadata), 2)
expected = dict(readonly='True', attached_mode='ro')
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
conn_info = self.volume.initialize_connection(self.context,
volume_id, connector)
- self.assertEqual(conn_info['data']['access_mode'], 'ro')
+ self.assertEqual('ro', conn_info['data']['access_mode'])
self.assertRaises(exception.VolumeAttached,
self.volume.delete_volume,
self.context,
volume_id)
+ self.volume.detach_volume(self.context, volume_id, attachment['id'])
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual('available', vol['status'])
+
+ self.volume.delete_volume(self.context, volume_id)
+ self.assertRaises(exception.VolumeNotFound,
+ db.volume_get,
+ self.context,
+ volume_id)
+
+ def test_detach_invalid_attachment_id(self):
+ """Make sure if the attachment id isn't found we raise."""
+ attachment_id = "notfoundid"
+ volume_id = "abc123"
+ self.assertRaises(exception.VolumeAttachmentNotFound,
+ self.volume.detach_volume,
+ self.context,
+ volume_id,
+ attachment_id)
+
+ def test_run_attach_detach_volume_for_instance_no_attachment_id(self):
+ """Make sure volume can be attached and detached from instance."""
+ mountpoint = "/dev/sdf"
+ # attach volume to the instance then to detach
+ instance_uuid = '12345678-1234-5678-1234-567812345678'
+ instance_uuid_2 = '12345678-4321-8765-4321-567812345678'
+ volume = tests_utils.create_volume(self.context,
+ admin_metadata={'readonly': 'True'},
+ multiattach=True,
+ **self.volume_params)
+ volume_id = volume['id']
+ self.volume.create_volume(self.context, volume_id)
+ attachment = self.volume.attach_volume(self.context, volume_id,
+ instance_uuid, None,
+ mountpoint, 'ro')
+ vol = db.volume_get(context.get_admin_context(), volume_id)
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertEqual(instance_uuid, attachment['instance_uuid'])
+ self.assertIsNone(attachment['attached_host'])
+ admin_metadata = vol['volume_admin_metadata']
+ self.assertEqual(2, len(admin_metadata))
+ expected = dict(readonly='True', attached_mode='ro')
+ ret = {}
+ for item in admin_metadata:
+ ret.update({item['key']: item['value']})
+ self.assertDictMatch(ret, expected)
+ attachment2 = self.volume.attach_volume(self.context, volume_id,
+ instance_uuid_2, None,
+ mountpoint, 'ro')
+
+ connector = {'initiator': 'iqn.2012-07.org.fake:01'}
+ conn_info = self.volume.initialize_connection(self.context,
+ volume_id, connector)
+ self.assertEqual('ro', conn_info['data']['access_mode'])
+ self.assertRaises(exception.VolumeAttached,
+ self.volume.delete_volume,
+ self.context,
+ volume_id)
+
+ self.assertRaises(exception.InvalidVolume,
+ self.volume.detach_volume,
+ self.context, volume_id)
+
+ self.volume.detach_volume(self.context, volume_id, attachment['id'])
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual('in-use', vol['status'])
+
+ self.volume.detach_volume(self.context, volume_id, attachment2['id'])
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual('available', vol['status'])
+
+ attachment = self.volume.attach_volume(self.context, volume_id,
+ instance_uuid, None,
+ mountpoint, 'ro')
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual('in-use', vol['status'])
self.volume.detach_volume(self.context, volume_id)
vol = db.volume_get(self.context, volume_id)
- self.assertEqual(vol['status'], "available")
+ self.assertEqual('available', vol['status'])
+
+ self.volume.delete_volume(self.context, volume_id)
+ self.assertRaises(exception.VolumeNotFound,
+ db.volume_get,
+ self.context,
+ volume_id)
+
+ def test_run_attach_detach_multiattach_volume_for_instances(self):
+ """Make sure volume can be attached to multiple instances."""
+ mountpoint = "/dev/sdf"
+ # attach volume to the instance then to detach
+ instance_uuid = '12345678-1234-5678-1234-567812345678'
+ volume = tests_utils.create_volume(self.context,
+ admin_metadata={'readonly': 'True'},
+ multiattach=True,
+ **self.volume_params)
+ volume_id = volume['id']
+ self.volume.create_volume(self.context, volume_id)
+ attachment = self.volume.attach_volume(self.context, volume_id,
+ instance_uuid, None,
+ mountpoint, 'ro')
+ vol = db.volume_get(context.get_admin_context(), volume_id)
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual(True, vol['multiattach'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertEqual(instance_uuid, attachment['instance_uuid'])
+ self.assertIsNone(attachment['attached_host'])
+ admin_metadata = vol['volume_admin_metadata']
+ self.assertEqual(2, len(admin_metadata))
+ expected = dict(readonly='True', attached_mode='ro')
+ ret = {}
+ for item in admin_metadata:
+ ret.update({item['key']: item['value']})
+ self.assertDictMatch(ret, expected)
+ connector = {'initiator': 'iqn.2012-07.org.fake:01'}
+ conn_info = self.volume.initialize_connection(self.context,
+ volume_id, connector)
+ self.assertEqual('ro', conn_info['data']['access_mode'])
+
+ instance2_uuid = '12345678-1234-5678-1234-567812345000'
+ mountpoint2 = "/dev/sdx"
+ attachment2 = self.volume.attach_volume(self.context, volume_id,
+ instance2_uuid, None,
+ mountpoint2, 'ro')
+ vol = db.volume_get(context.get_admin_context(), volume_id)
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual(True, vol['multiattach'])
+ self.assertEqual('attached', attachment2['attach_status'])
+ self.assertEqual(mountpoint2, attachment2['mountpoint'])
+ self.assertEqual(instance2_uuid, attachment2['instance_uuid'])
+ self.assertIsNone(attachment2['attached_host'])
+ self.assertNotEqual(attachment, attachment2)
+
+ self.assertRaises(exception.VolumeAttached,
+ self.volume.delete_volume,
+ self.context,
+ volume_id)
+ self.volume.detach_volume(self.context, volume_id, attachment['id'])
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual('in-use', vol['status'])
+
+ self.assertRaises(exception.VolumeAttached,
+ self.volume.delete_volume,
+ self.context,
+ volume_id)
+
+ self.volume.detach_volume(self.context, volume_id, attachment2['id'])
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual('available', vol['status'])
+
+ self.volume.delete_volume(self.context, volume_id)
+ self.assertRaises(exception.VolumeNotFound,
+ db.volume_get,
+ self.context,
+ volume_id)
+
+ def test_attach_detach_not_multiattach_volume_for_instances(self):
+ """Make sure volume can't be attached to more than one instance."""
+ mountpoint = "/dev/sdf"
+ # attach volume to the instance then to detach
+ instance_uuid = '12345678-1234-5678-1234-567812345678'
+ volume = tests_utils.create_volume(self.context,
+ admin_metadata={'readonly': 'True'},
+ multiattach=False,
+ **self.volume_params)
+ volume_id = volume['id']
+ self.volume.create_volume(self.context, volume_id)
+ attachment = self.volume.attach_volume(self.context, volume_id,
+ instance_uuid, None,
+ mountpoint, 'ro')
+ vol = db.volume_get(context.get_admin_context(), volume_id)
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual(False, vol['multiattach'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertEqual(instance_uuid, attachment['instance_uuid'])
+ self.assertIsNone(attachment['attached_host'])
+ admin_metadata = vol['volume_admin_metadata']
+ self.assertEqual(2, len(admin_metadata))
+ expected = dict(readonly='True', attached_mode='ro')
+ ret = {}
+ for item in admin_metadata:
+ ret.update({item['key']: item['value']})
+ self.assertDictMatch(ret, expected)
+ connector = {'initiator': 'iqn.2012-07.org.fake:01'}
+ conn_info = self.volume.initialize_connection(self.context,
+ volume_id, connector)
+ self.assertEqual('ro', conn_info['data']['access_mode'])
+
+ instance2_uuid = '12345678-1234-5678-1234-567812345000'
+ mountpoint2 = "/dev/sdx"
+ self.assertRaises(exception.InvalidVolume,
+ self.volume.attach_volume,
+ self.context,
+ volume_id,
+ instance2_uuid,
+ None,
+ mountpoint2, 'ro')
+
+ self.assertRaises(exception.VolumeAttached,
+ self.volume.delete_volume,
+ self.context,
+ volume_id)
+ self.volume.detach_volume(self.context, volume_id, attachment['id'])
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual('available', vol['status'])
self.volume.delete_volume(self.context, volume_id)
self.assertRaises(exception.VolumeNotFound,
**self.volume_params)
volume_id = volume['id']
self.volume.create_volume(self.context, volume_id)
- self.volume.attach_volume(self.context, volume_id, None,
- 'fake_host', mountpoint, 'rw')
+ attachment = self.volume.attach_volume(self.context, volume_id, None,
+ 'fake_host', mountpoint, 'rw')
vol = db.volume_get(context.get_admin_context(), volume_id)
- self.assertEqual(vol['status'], "in-use")
- self.assertEqual(vol['attach_status'], "attached")
- self.assertEqual(vol['mountpoint'], mountpoint)
- self.assertIsNone(vol['instance_uuid'])
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertIsNone(attachment['instance_uuid'])
# sanitized, conforms to RFC-952 and RFC-1123 specs.
- self.assertEqual(vol['attached_host'], 'fake-host')
+ self.assertEqual(attachment['attached_host'], 'fake-host')
admin_metadata = vol['volume_admin_metadata']
- self.assertEqual(len(admin_metadata), 2)
+ self.assertEqual(2, len(admin_metadata))
expected = dict(readonly='False', attached_mode='rw')
ret = {}
for item in admin_metadata:
ret.update({item['key']: item['value']})
self.assertDictMatch(ret, expected)
+ connector = {'initiator': 'iqn.2012-07.org.fake:01'}
+ conn_info = self.volume.initialize_connection(self.context,
+ volume_id, connector)
+ self.assertEqual('rw', conn_info['data']['access_mode'])
+
+ self.assertRaises(exception.VolumeAttached,
+ self.volume.delete_volume,
+ self.context,
+ volume_id)
+ self.volume.detach_volume(self.context, volume_id, attachment['id'])
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual(vol['status'], "available")
+
+ self.volume.delete_volume(self.context, volume_id)
+ self.assertRaises(exception.VolumeNotFound,
+ db.volume_get,
+ self.context,
+ volume_id)
+
+ def test_run_attach_detach_multiattach_volume_for_hosts(self):
+ """Make sure volume can be attached and detached from hosts."""
+ mountpoint = "/dev/sdf"
+ volume = tests_utils.create_volume(
+ self.context,
+ admin_metadata={'readonly': 'False'},
+ multiattach=True,
+ **self.volume_params)
+ volume_id = volume['id']
+ self.volume.create_volume(self.context, volume_id)
+ attachment = self.volume.attach_volume(self.context, volume_id, None,
+ 'fake_host', mountpoint, 'rw')
+ vol = db.volume_get(context.get_admin_context(), volume_id)
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual(True, vol['multiattach'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertIsNone(attachment['instance_uuid'])
+ # sanitized, conforms to RFC-952 and RFC-1123 specs.
+ self.assertEqual(attachment['attached_host'], 'fake-host')
+ admin_metadata = vol['volume_admin_metadata']
+ self.assertEqual(2, len(admin_metadata))
+ expected = dict(readonly='False', attached_mode='rw')
+ ret = {}
+ for item in admin_metadata:
+ ret.update({item['key']: item['value']})
+ self.assertDictMatch(ret, expected)
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
conn_info = self.volume.initialize_connection(self.context,
volume_id, connector)
self.assertEqual(conn_info['data']['access_mode'], 'rw')
+ mountpoint2 = "/dev/sdx"
+ attachment2 = self.volume.attach_volume(self.context, volume_id, None,
+ 'fake_host2', mountpoint2,
+ 'rw')
+ vol = db.volume_get(context.get_admin_context(), volume_id)
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual('attached', attachment2['attach_status'])
+ self.assertEqual(mountpoint2, attachment2['mountpoint'])
+ self.assertIsNone(attachment2['instance_uuid'])
+ # sanitized, conforms to RFC-952 and RFC-1123 specs.
+ self.assertEqual('fake-host2', attachment2['attached_host'])
+
self.assertRaises(exception.VolumeAttached,
self.volume.delete_volume,
self.context,
volume_id)
- self.volume.detach_volume(self.context, volume_id)
+ self.volume.detach_volume(self.context, volume_id, attachment['id'])
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual(vol['status'], "in-use")
+
+ self.volume.detach_volume(self.context, volume_id, attachment2['id'])
vol = db.volume_get(self.context, volume_id)
self.assertEqual(vol['status'], "available")
self.context,
volume_id)
+ def test_run_attach_detach_not_multiattach_volume_for_hosts(self):
+ """Make sure volume can't be attached to more than one host."""
+ mountpoint = "/dev/sdf"
+ volume = tests_utils.create_volume(
+ self.context,
+ admin_metadata={'readonly': 'False'},
+ multiattach=False,
+ **self.volume_params)
+ volume_id = volume['id']
+ self.volume.create_volume(self.context, volume_id)
+ attachment = self.volume.attach_volume(self.context, volume_id, None,
+ 'fake_host', mountpoint, 'rw')
+ vol = db.volume_get(context.get_admin_context(), volume_id)
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual(False, vol['multiattach'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertIsNone(attachment['instance_uuid'])
+ # sanitized, conforms to RFC-952 and RFC-1123 specs.
+ self.assertEqual(attachment['attached_host'], 'fake-host')
+ admin_metadata = vol['volume_admin_metadata']
+ self.assertEqual(2, len(admin_metadata))
+ expected = dict(readonly='False', attached_mode='rw')
+ ret = {}
+ for item in admin_metadata:
+ ret.update({item['key']: item['value']})
+ self.assertDictMatch(ret, expected)
+ connector = {'initiator': 'iqn.2012-07.org.fake:01'}
+ conn_info = self.volume.initialize_connection(self.context,
+ volume_id, connector)
+ self.assertEqual('rw', conn_info['data']['access_mode'])
+
+ mountpoint2 = "/dev/sdx"
+ self.assertRaises(exception.InvalidVolume,
+ self.volume.attach_volume,
+ self.context,
+ volume_id,
+ None,
+ 'fake_host2',
+ mountpoint2,
+ 'rw')
+ vol = db.volume_get(context.get_admin_context(), volume_id)
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual('attached', attachment['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertIsNone(attachment['instance_uuid'])
+ # sanitized, conforms to RFC-952 and RFC-1123 specs.
+ self.assertEqual('fake-host', attachment['attached_host'])
+
+ self.assertRaises(exception.VolumeAttached,
+ self.volume.delete_volume,
+ self.context,
+ volume_id)
+ self.volume.detach_volume(self.context, volume_id, attachment['id'])
+ vol = db.volume_get(self.context, volume_id)
+ self.assertEqual('available', vol['status'])
+
+ self.volume.delete_volume(self.context, volume_id)
+ self.assertRaises(exception.VolumeNotFound,
+ db.volume_get,
+ self.context,
+ volume_id)
+
def test_run_attach_detach_volume_with_attach_mode(self):
instance_uuid = '12345678-1234-5678-1234-567812345678'
mountpoint = "/dev/sdf"
admin_metadata={'readonly': 'True'},
**self.volume_params)
volume_id = volume['id']
- db.volume_update(self.context, volume_id, {'status': 'available',
- 'mountpoint': None,
- 'instance_uuid': None,
- 'attached_host': None,
- 'attached_mode': None})
+ db.volume_update(self.context, volume_id, {'status': 'available', })
self.volume.attach_volume(self.context, volume_id, instance_uuid,
None, mountpoint, 'ro')
vol = db.volume_get(context.get_admin_context(), volume_id)
- self.assertEqual(vol['status'], "in-use")
- self.assertEqual(vol['attach_status'], "attached")
- self.assertEqual(vol['mountpoint'], mountpoint)
- self.assertEqual(vol['instance_uuid'], instance_uuid)
- self.assertIsNone(vol['attached_host'])
+ attachment = vol['volume_attachment'][0]
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual('attached', vol['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertEqual(instance_uuid, attachment['instance_uuid'])
+ self.assertIsNone(attachment['attached_host'])
admin_metadata = vol['volume_admin_metadata']
- self.assertEqual(len(admin_metadata), 2)
+ self.assertEqual(2, len(admin_metadata))
expected = dict(readonly='True', attached_mode='ro')
ret = {}
for item in admin_metadata:
conn_info = self.volume.initialize_connection(self.context,
volume_id, connector)
- self.assertEqual(conn_info['data']['access_mode'], 'ro')
+ self.assertEqual('ro', conn_info['data']['access_mode'])
- self.volume.detach_volume(self.context, volume_id)
+ self.volume.detach_volume(self.context, volume_id, attachment['id'])
vol = db.volume_get(self.context, volume_id)
- self.assertEqual(vol['status'], "available")
- self.assertEqual(vol['attach_status'], "detached")
- self.assertIsNone(vol['mountpoint'])
- self.assertIsNone(vol['instance_uuid'])
- self.assertIsNone(vol['attached_host'])
+ attachment = vol['volume_attachment']
+ self.assertEqual('available', vol['status'])
+ self.assertEqual('detached', vol['attach_status'])
+ self.assertEqual(attachment, [])
admin_metadata = vol['volume_admin_metadata']
- self.assertEqual(len(admin_metadata), 1)
- self.assertEqual(admin_metadata[0]['key'], 'readonly')
- self.assertEqual(admin_metadata[0]['value'], 'True')
+ self.assertEqual(1, len(admin_metadata))
+ self.assertEqual('readonly', admin_metadata[0]['key'])
+ self.assertEqual('True', admin_metadata[0]['value'])
self.volume.attach_volume(self.context, volume_id, None,
'fake_host', mountpoint, 'ro')
vol = db.volume_get(context.get_admin_context(), volume_id)
- self.assertEqual(vol['status'], "in-use")
- self.assertEqual(vol['attach_status'], "attached")
- self.assertEqual(vol['mountpoint'], mountpoint)
- self.assertIsNone(vol['instance_uuid'])
- self.assertEqual(vol['attached_host'], 'fake-host')
+ attachment = vol['volume_attachment'][0]
+ self.assertEqual('in-use', vol['status'])
+ self.assertEqual('attached', vol['attach_status'])
+ self.assertEqual(mountpoint, attachment['mountpoint'])
+ self.assertIsNone(attachment['instance_uuid'])
+ self.assertEqual('fake-host', attachment['attached_host'])
admin_metadata = vol['volume_admin_metadata']
- self.assertEqual(len(admin_metadata), 2)
+ self.assertEqual(2, len(admin_metadata))
expected = dict(readonly='True', attached_mode='ro')
ret = {}
for item in admin_metadata:
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
conn_info = self.volume.initialize_connection(self.context,
volume_id, connector)
- self.assertEqual(conn_info['data']['access_mode'], 'ro')
+ self.assertEqual('ro', conn_info['data']['access_mode'])
- self.volume.detach_volume(self.context, volume_id)
+ self.volume.detach_volume(self.context, volume_id,
+ attachment['id'])
vol = db.volume_get(self.context, volume_id)
- self.assertEqual(vol['status'], "available")
- self.assertEqual(vol['attach_status'], "detached")
- self.assertIsNone(vol['mountpoint'])
- self.assertIsNone(vol['instance_uuid'])
- self.assertIsNone(vol['attached_host'])
+ attachment = vol['volume_attachment']
+ self.assertEqual('available', vol['status'])
+ self.assertEqual('detached', vol['attach_status'])
+ self.assertEqual(attachment, [])
admin_metadata = vol['volume_admin_metadata']
- self.assertEqual(len(admin_metadata), 1)
- self.assertEqual(admin_metadata[0]['key'], 'readonly')
- self.assertEqual(admin_metadata[0]['value'], 'True')
+ self.assertEqual(1, len(admin_metadata))
+ self.assertEqual('readonly', admin_metadata[0]['key'])
+ self.assertEqual('True', admin_metadata[0]['value'])
self.volume.delete_volume(self.context, volume_id)
self.assertRaises(exception.VolumeNotFound,
mountpoint,
'rw')
vol = db.volume_get(context.get_admin_context(), volume_id)
- self.assertEqual(vol['status'], "error_attaching")
- self.assertEqual(vol['attach_status'], "detached")
+ self.assertEqual('error_attaching', vol['status'])
+ self.assertEqual('detached', vol['attach_status'])
admin_metadata = vol['volume_admin_metadata']
- self.assertEqual(len(admin_metadata), 2)
+ self.assertEqual(2, len(admin_metadata))
expected = dict(readonly='True', attached_mode='rw')
ret = {}
for item in admin_metadata:
mountpoint,
'rw')
vol = db.volume_get(context.get_admin_context(), volume_id)
- self.assertEqual(vol['status'], "error_attaching")
- self.assertEqual(vol['attach_status'], "detached")
+ self.assertEqual('error_attaching', vol['status'])
+ self.assertEqual('detached', vol['attach_status'])
admin_metadata = vol['volume_admin_metadata']
- self.assertEqual(len(admin_metadata), 2)
+ self.assertEqual(2, len(admin_metadata))
expected = dict(readonly='True', attached_mode='rw')
ret = {}
for item in admin_metadata:
mountpoint,
'rw')
vol = db.volume_get(context.get_admin_context(), volume_id)
- self.assertEqual(vol['attach_status'], "detached")
+ self.assertEqual('detached', vol['attach_status'])
admin_metadata = vol['volume_admin_metadata']
- self.assertEqual(len(admin_metadata), 1)
- self.assertEqual(admin_metadata[0]['key'], 'readonly')
- self.assertEqual(admin_metadata[0]['value'], 'True')
+ self.assertEqual(1, len(admin_metadata))
+ self.assertEqual('readonly', admin_metadata[0]['key'])
+ self.assertEqual('True', admin_metadata[0]['value'])
db.volume_update(self.context, volume_id, {'status': 'available'})
self.assertRaises(exception.InvalidVolumeAttachMode,
mountpoint,
'rw')
vol = db.volume_get(context.get_admin_context(), volume_id)
- self.assertEqual(vol['attach_status'], "detached")
+ self.assertEqual('detached', vol['attach_status'])
admin_metadata = vol['volume_admin_metadata']
- self.assertEqual(len(admin_metadata), 1)
- self.assertEqual(admin_metadata[0]['key'], 'readonly')
- self.assertEqual(admin_metadata[0]['value'], 'True')
+ self.assertEqual(1, len(admin_metadata))
+ self.assertEqual('readonly', admin_metadata[0]['key'])
+ self.assertEqual('True', admin_metadata[0]['value'])
@mock.patch.object(cinder.volume.api.API, 'update')
@mock.patch.object(db, 'volume_get')
def test_reserve_volume_bad_status(self):
fake_volume = {
'id': FAKE_UUID,
- 'status': 'in-use'
+ 'status': 'attaching'
}
with mock.patch.object(db, 'volume_get') as mock_volume_get:
fake_volume)
self.assertTrue(mock_volume_get.called)
- def test_unreserve_volume_success(self):
+ @mock.patch.object(db, 'volume_get')
+ @mock.patch.object(db, 'volume_attachment_get_used_by_volume_id')
+ @mock.patch.object(cinder.volume.api.API, 'update')
+ def test_unreserve_volume_success(self, volume_get,
+ volume_attachment_get_used_by_volume_id,
+ volume_update):
fake_volume = {
'id': FAKE_UUID,
'status': 'attaching'
}
+ fake_attachments = [{'volume_id': FAKE_UUID,
+ 'instance_uuid': 'fake_instance_uuid'}]
- with mock.patch.object(cinder.volume.api.API,
- 'update') as mock_volume_update:
- mock_volume_update.return_value = fake_volume
- self.assertIsNone(cinder.volume.api.API().unreserve_volume(
- self.context,
- fake_volume
- ))
- self.assertTrue(mock_volume_update.called)
+ volume_get.return_value = fake_volume
+ volume_attachment_get_used_by_volume_id.return_value = fake_attachments
+ volume_update.return_value = fake_volume
+
+ self.assertIsNone(cinder.volume.api.API().unreserve_volume(
+ self.context,
+ fake_volume
+ ))
+
+ self.assertTrue(volume_get.called)
+ self.assertTrue(volume_attachment_get_used_by_volume_id.called)
+ self.assertTrue(volume_update.called)
def test_concurrent_volumes_get_different_targets(self):
"""Ensure multiple concurrent volumes get different targets."""
# create volume and attach to the instance
volume = tests_utils.create_volume(self.context, **self.volume_params)
self.volume.create_volume(self.context, volume['id'])
- db.volume_attached(self.context, volume['id'], instance_uuid,
+ values = {'volume_id': volume['id'],
+ 'instance_uuid': instance_uuid,
+ 'attach_status': 'attaching', }
+ attachment = db.volume_attach(self.context, values)
+ db.volume_attached(self.context, attachment['id'], instance_uuid,
None, '/dev/sda1')
volume_api = cinder.volume.api.API()
# create volume and attach to the host
volume = tests_utils.create_volume(self.context, **self.volume_params)
self.volume.create_volume(self.context, volume['id'])
- db.volume_attached(self.context, volume['id'], None,
+ values = {'volume_id': volume['id'],
+ 'attached_host': 'fake_host',
+ 'attach_status': 'attaching', }
+ attachment = db.volume_attach(self.context, values)
+ db.volume_attached(self.context, attachment['id'], None,
'fake_host', '/dev/sda1')
volume_api = cinder.volume.api.API()
instance_uuid = '12345678-1234-5678-1234-567812345678'
volume = tests_utils.create_volume(self.context, **self.volume_params)
+ attachment = db.volume_attach(self.context,
+ {'volume_id': volume['id'],
+ 'attached_host': 'fake-host'})
volume = db.volume_attached(
- self.context, volume['id'], instance_uuid, 'fake-host', 'vdb')
+ self.context, attachment['id'], instance_uuid, 'fake-host', 'vdb')
volume_api = cinder.volume.api.API()
volume_api.begin_detaching(self.context, volume)
volume = db.volume_get(self.context, volume['id'])
old_volume = tests_utils.create_volume(self.context, size=0,
host=CONF.host,
status=initial_status,
- migration_status='migrating',
- instance_uuid=instance_uuid,
- attached_host=attached_host)
+ migration_status='migrating')
+ if status == 'in-use':
+ vol = tests_utils.attach_volume(self.context, old_volume['id'],
+ instance_uuid, attached_host,
+ '/dev/vda')
+ self.assertEqual(vol['status'], 'in-use')
target_status = 'target:%s' % old_volume['id']
new_volume = tests_utils.create_volume(self.context, size=0,
host=CONF.host,
self.stubs.Set(self.volume.driver, 'attach_volume',
lambda *args, **kwargs: None)
- with mock.patch.object(self.volume.driver, 'detach_volume') as detach:
+ with mock.patch.object(self.volume.driver, 'detach_volume'):
self.volume.migrate_volume_completion(self.context, old_volume[
'id'], new_volume['id'])
- volume = db.volume_get(elevated, old_volume['id'])
- self.assertEqual(volume['status'], status)
- self.assertEqual(volume['attached_host'], attached_host)
- self.assertEqual(volume['instance_uuid'], instance_uuid)
- self.assertEqual(status == 'in-use', detach.called)
+ if status == 'in-use':
+ attachment = db.volume_attachment_get_by_instance_uuid(
+ self.context, old_volume['id'], instance_uuid)
+ self.assertIsNotNone(attachment)
+ self.assertEqual(attachment['attached_host'], attached_host)
+ self.assertEqual(attachment['instance_uuid'], instance_uuid)
def test_migrate_volume_completion_retype_available(self):
self._test_migrate_volume_completion('available', retyping=True)
def test_copy_volume_to_image_status_use(self):
self.image_meta['id'] = 'a440c04b-79fa-479c-bed1-0b816eaec379'
# creating volume testdata
- self.volume_attrs['instance_uuid'] = 'b21f957d-a72f-4b93-b5a5-' \
- '45b1161abb02'
db.volume_create(self.context, self.volume_attrs)
# start test
self.image_meta)
volume = db.volume_get(self.context, self.volume_id)
- self.assertEqual(volume['status'], 'in-use')
+ self.assertEqual(volume['status'], 'available')
def test_copy_volume_to_image_exception(self):
self.image_meta['id'] = FAKE_UUID
def test_detach_volume(self):
self._test_volume_api('detach_volume',
rpc_method='call',
- volume=self.fake_volume)
+ volume=self.fake_volume,
+ attachment_id='fake_uuid',
+ version="1.20")
def test_copy_volume_to_image(self):
self._test_volume_api('copy_volume_to_image',
from cinder import db
from cinder.openstack.common import loopingcall
+from oslo_utils import timeutils
+
def get_test_admin_context():
return context.get_admin_context()
return db.volume_create(ctxt, vol)
+def attach_volume(ctxt, volume_id, instance_uuid, attached_host,
+ mountpoint, mode='rw'):
+
+ now = timeutils.utcnow()
+ values = {}
+ values['volume_id'] = volume_id
+ values['attached_host'] = attached_host
+ values['mountpoint'] = mountpoint
+ values['attach_time'] = now
+
+ attachment = db.volume_attach(ctxt, values)
+ return db.volume_attached(ctxt, attachment['id'], instance_uuid,
+ attached_host, mountpoint, mode)
+
+
def create_snapshot(ctxt,
volume_id,
display_name='test_snapshot',
availability_zone=None, source_volume=None,
scheduler_hints=None,
source_replica=None, consistencygroup=None,
- cgsnapshot=None):
+ cgsnapshot=None, multiattach=False):
# NOTE(jdg): we can have a create without size if we're
# doing a create from snap or volume. Currently
'optional_args': {'is_quota_committed': False},
'consistencygroup': consistencygroup,
'cgsnapshot': cgsnapshot,
+ 'multiattach': multiattach,
}
try:
if cgsnapshot:
volume = self.db.volume_get(context, volume['id'])
if volume['status'] == 'available':
self.update(context, volume, {"status": "attaching"})
+ elif volume['status'] == 'in-use':
+ if volume['multiattach']:
+ self.update(context, volume, {"status": "attaching"})
+ else:
+ msg = _("Volume must be multiattachable to reserve again.")
+ LOG.error(msg)
+ raise exception.InvalidVolume(reason=msg)
else:
msg = _("Volume status must be available to reserve.")
LOG.error(msg)
@wrap_check_policy
def unreserve_volume(self, context, volume):
- if volume['status'] == "attaching":
- self.update(context, volume, {"status": "available"})
+ volume = self.db.volume_get(context, volume['id'])
+ if volume['status'] == 'attaching':
+ attaches = self.db.volume_attachment_get_used_by_volume_id(
+ context, volume['id'])
+ if attaches:
+ self.update(context, volume, {"status": "in-use"})
+ else:
+ self.update(context, volume, {"status": "available"})
@wrap_check_policy
def begin_detaching(self, context, volume):
mode)
@wrap_check_policy
- def detach(self, context, volume):
- return self.volume_rpcapi.detach_volume(context, volume)
+ def detach(self, context, volume, attachment_id):
+ return self.volume_rpcapi.detach_volume(context, volume,
+ attachment_id)
@wrap_check_policy
def initialize_connection(self, context, volume, connector):
"""Callback for volume attached to instance or host."""
pass
- def detach_volume(self, context, volume):
+ def detach_volume(self, context, volume, attachment=None):
"""Callback for volume detached."""
pass
def create_export(self, context, volume):
return self._do_export(context, volume)
- def detach_volume(self, context, volume):
+ def detach_volume(self, context, volume, attachment=None):
try:
self._issue_api_request('volumes', 'delete', resource=volume['id'],
action='export')
2.0.36 - Added support for dedup provisioning
2.0.37 - Added support for enabling Flash Cache
2.0.38 - Add stats for hp3par goodness_function and filter_function
+ 2.0.39 - Added support for updated detach_volume attachment.
"""
- VERSION = "2.0.38"
+ VERSION = "2.0.39"
stats = {}
raise exception.VolumeBackendAPIException(data=msg)
def attach_volume(self, volume, instance_uuid):
+ """Save the instance UUID in the volume.
+
+ TODO: add support for multi-attach
+
+ """
LOG.debug("Attach Volume\n%s", pprint.pformat(volume))
try:
self.update_volume_key_value_pair(volume,
with excutils.save_and_reraise_exception():
LOG.error(_LE("Error attaching volume %s"), volume)
- def detach_volume(self, volume):
+ def detach_volume(self, volume, attachment=None):
+ """Remove the instance uuid from the volume.
+
+ TODO: add support for multi-attach.
+
+ """
LOG.debug("Detach Volume\n%s", pprint.pformat(volume))
try:
self.clear_volume_key_value_pair(volume, 'HPQ-CS-instance_uuid')
2.0.12 - Fix queryHost call to specify wwns bug #1398206
2.0.13 - Fix missing host name during attach bug #1398206
2.0.14 - Removed usage of host name cache #1398914
+ 2.0.15 - Added support for updated detach_volume attachment.
"""
- VERSION = "2.0.14"
+ VERSION = "2.0.15"
def __init__(self, *args, **kwargs):
super(HP3PARFCDriver, self).__init__(*args, **kwargs)
finally:
self._logout(common)
- def detach_volume(self, context, volume):
+ def detach_volume(self, context, volume, attachment=None):
common = self._login()
try:
- common.detach_volume(volume)
+ common.detach_volume(volume, attachment)
finally:
self._logout(common)
2.0.13 - Update LOG usage to fix translations. bug #1384312
2.0.14 - Do not allow a different iSCSI IP (hp3par_iscsi_ips) to be
used during live-migration. bug #1423958
+ 2.0.15 - Added support for updated detach_volume attachment.
"""
- VERSION = "2.0.14"
+ VERSION = "2.0.15"
def __init__(self, *args, **kwargs):
super(HP3PARISCSIDriver, self).__init__(*args, **kwargs)
finally:
self._logout(common)
- def detach_volume(self, context, volume):
+ def detach_volume(self, context, volume, attachment=None):
common = self._login()
try:
- common.detach_volume(volume)
+ common.detach_volume(volume, attachment)
finally:
self._logout(common)
"""Disallow connection from connector."""
pass
- def detach_volume(self, context, volume):
+ def detach_volume(self, context, volume, attachment=None):
"""Callback for volume detached."""
pass
if 'result' not in data:
raise exception.SolidFireAPIDataException(data=data)
- def detach_volume(self, context, volume):
+ def detach_volume(self, context, volume, attachment=None):
LOG.debug("Entering SolidFire attach_volume...")
sfaccount = self._get_sfaccount(volume['project_id'])
'name', 'reservations', 'size', 'snapshot_id',
'source_volid', 'volume_type_id', 'encryption_key_id',
'source_replicaid', 'consistencygroup_id',
- 'cgsnapshot_id', ]
+ 'cgsnapshot_id', 'multiattach']
super(EntryCreateTask, self).__init__(addons=[ACTION],
requires=requires)
self.db = db
'display_description': kwargs.pop('description'),
'display_name': kwargs.pop('name'),
'replication_status': 'disabled',
+ 'multiattach': kwargs.pop('multiattach'),
}
# Merge in the other required arguments which should provide the rest
return lvo_inner1
+def locked_detach_operation(f):
+ """Lock decorator for volume detach operations.
+
+ Takes a named lock prior to executing the detach call. The lock is named
+ with the operation executed and the id of the volume. This lock can then
+ be used by other operations to avoid operation conflicts on shared volumes.
+
+ This locking mechanism is only for detach calls. We can't use the
+ locked_volume_operation, because detach requires an additional
+ attachment_id in the parameter list.
+ """
+ def ldo_inner1(inst, context, volume_id, attachment_id=None, **kwargs):
+ @utils.synchronized("%s-%s" % (volume_id, f.__name__), external=True)
+ def ldo_inner2(*_args, **_kwargs):
+ return f(*_args, **_kwargs)
+ return ldo_inner2(inst, context, volume_id, attachment_id, **kwargs)
+ return ldo_inner1
+
+
def locked_snapshot_operation(f):
"""Lock decorator for snapshot operations.
class VolumeManager(manager.SchedulerDependentManager):
"""Manages attachable block storage devices."""
- RPC_API_VERSION = '1.22'
+ RPC_API_VERSION = '1.23'
target = messaging.Target(version=RPC_API_VERSION)
volume_metadata = self.db.volume_admin_metadata_get(
context.elevated(), volume_id)
if volume['status'] == 'attaching':
- if (volume['instance_uuid'] and volume['instance_uuid'] !=
- instance_uuid):
- msg = _("being attached by another instance")
- raise exception.InvalidVolume(reason=msg)
- if (volume['attached_host'] and volume['attached_host'] !=
- host_name):
- msg = _("being attached by another host")
- raise exception.InvalidVolume(reason=msg)
if (volume_metadata.get('attached_mode') and
- volume_metadata.get('attached_mode') != mode):
+ volume_metadata.get('attached_mode') != mode):
msg = _("being attached by different mode")
raise exception.InvalidVolume(reason=msg)
- elif (not volume['migration_status'] and
- volume['status'] != "available"):
- msg = _("status must be available or attaching")
+
+ if (volume['status'] == 'in-use' and not volume['multiattach']
+ and not volume['migration_status']):
+ msg = _("volume is already attached")
raise exception.InvalidVolume(reason=msg)
- # TODO(jdg): attach_time column is currently varchar
- # we should update this to a date-time object
- # also consider adding detach_time?
+ attachment = None
+ host_name_sanitized = utils.sanitize_hostname(
+ host_name) if host_name else None
+ if instance_uuid:
+ attachment = \
+ self.db.volume_attachment_get_by_instance_uuid(
+ context, volume_id, instance_uuid)
+ else:
+ attachment = \
+ self.db.volume_attachment_get_by_host(context, volume_id,
+ host_name_sanitized)
+ if attachment is not None:
+ return
+
self._notify_about_volume_usage(context, volume,
"attach.start")
- self.db.volume_update(context, volume_id,
- {"instance_uuid": instance_uuid,
- "attached_host": host_name,
- "status": "attaching",
- "attach_time": timeutils.strtime()})
- self.db.volume_admin_metadata_update(context.elevated(),
- volume_id,
- {"attached_mode": mode},
- False)
+ values = {'volume_id': volume_id,
+ 'attach_status': 'attaching', }
+
+ attachment = self.db.volume_attach(context.elevated(), values)
+ volume_metadata = self.db.volume_admin_metadata_update(
+ context.elevated(), volume_id,
+ {"attached_mode": mode}, False)
+ attachment_id = attachment['id']
if instance_uuid and not uuidutils.is_uuid_like(instance_uuid):
- self.db.volume_update(context, volume_id,
- {'status': 'error_attaching'})
+ self.db.volume_attachment_update(context, attachment_id,
+ {'attach_status':
+ 'error_attaching'})
raise exception.InvalidUUID(uuid=instance_uuid)
- host_name_sanitized = utils.sanitize_hostname(
- host_name) if host_name else None
-
volume = self.db.volume_get(context, volume_id)
if volume_metadata.get('readonly') == 'True' and mode != 'ro':
{'status': 'error_attaching'})
raise exception.InvalidVolumeAttachMode(mode=mode,
volume_id=volume_id)
+
try:
# NOTE(flaper87): Verify the driver is enabled
# before going forward. The exception will be caught
mountpoint)
except Exception:
with excutils.save_and_reraise_exception():
- self.db.volume_update(context, volume_id,
- {'status': 'error_attaching'})
+ self.db.volume_attachment_update(
+ context, attachment_id,
+ {'attach_status': 'error_attaching'})
volume = self.db.volume_attached(context.elevated(),
- volume_id,
+ attachment_id,
instance_uuid,
host_name_sanitized,
- mountpoint)
+ mountpoint,
+ mode)
if volume['migration_status']:
self.db.volume_update(context, volume_id,
{'migration_status': None})
self._notify_about_volume_usage(context, volume, "attach.end")
+ return self.db.volume_attachment_get(context, attachment_id)
return do_attach()
- @locked_volume_operation
- def detach_volume(self, context, volume_id):
+ @locked_detach_operation
+ def detach_volume(self, context, volume_id, attachment_id=None):
"""Updates db to show volume is detached."""
# TODO(vish): refactor this into a more general "unreserve"
+ attachment = None
+ if attachment_id:
+ try:
+ attachment = self.db.volume_attachment_get(context,
+ attachment_id)
+ except exception.VolumeAttachmentNotFound:
+ LOG.error(_LE("We couldn't find the volume attachment"
+ " for volume %(volume_id)s and"
+ " attachment id %(id)s"),
+ {"volume_id": volume_id,
+ "id": attachment_id})
+ raise
+ else:
+ # We can try and degrade gracefuly here by trying to detach
+ # a volume without the attachment_id here if the volume only has
+ # one attachment. This is for backwards compatibility.
+ attachments = self.db.volume_attachment_get_used_by_volume_id(
+ context, volume_id)
+ if len(attachments) > 1:
+ # There are more than 1 attachments for this volume
+ # we have to have an attachment id.
+ msg = _("Volume %(id)s is attached to more than one instance"
+ ". A valid attachment_id must be passed to detach"
+ " this volume") % {'id': volume_id}
+ LOG.error(msg)
+ raise exception.InvalidVolume(reason=msg)
+ else:
+ attachment = attachments[0]
volume = self.db.volume_get(context, volume_id)
self._notify_about_volume_usage(context, volume, "detach.start")
# and the volume status updated.
utils.require_driver_initialized(self.driver)
- self.driver.detach_volume(context, volume)
+ self.driver.detach_volume(context, volume, attachment)
except Exception:
with excutils.save_and_reraise_exception():
- self.db.volume_update(context,
- volume_id,
- {'status': 'error_detaching'})
+ self.db.volume_attachment_update(
+ context, attachment.get('id'),
+ {'attach_status': 'error_detaching'})
- self.db.volume_detached(context.elevated(), volume_id)
+ self.db.volume_detached(context.elevated(), volume_id,
+ attachment.get('id'))
self.db.volume_admin_metadata_delete(context.elevated(), volume_id,
'attached_mode')
with excutils.save_and_reraise_exception():
payload['message'] = unicode(error)
finally:
- if (volume['instance_uuid'] is None and
- volume['attached_host'] is None):
+ if not volume['volume_attachment']:
self.db.volume_update(context, volume_id,
{'status': 'available'})
else:
# Copy the source volume to the destination volume
try:
- if (volume['instance_uuid'] is None and
- volume['attached_host'] is None):
+ attachments = volume['volume_attachment']
+ if not attachments:
self.driver.copy_volume_data(ctxt, volume, new_volume,
remote='dest')
# The above call is synchronous so we complete the migration
nova_api = compute.API()
# This is an async call to Nova, which will call the completion
# when it's done
- nova_api.update_server_volume(ctxt, volume['instance_uuid'],
- volume['id'], new_volume['id'])
+ for attachment in attachments:
+ instance_uuid = attachment['instance_uuid']
+ nova_api.update_server_volume(ctxt, instance_uuid,
+ volume['id'],
+ new_volume['id'])
except Exception:
with excutils.save_and_reraise_exception():
msg = _("Failed to copy volume %(vol1)s to %(vol2)s")
{'vol': new_volume['id']})
def _get_original_status(self, volume):
- if (volume['instance_uuid'] is None and
- volume['attached_host'] is None):
+ attachments = volume['volume_attachment']
+ if not attachments:
return 'available'
else:
return 'in-use'
self.db.volume_update(ctxt, volume_id, updates)
if orig_volume_status == 'in-use':
- rpcapi.attach_volume(ctxt,
- volume,
- volume['instance_uuid'],
- volume['attached_host'],
- volume['mountpoint'],
- 'rw')
+ attachments = volume['volume_attachment']
+ for attachment in attachments:
+ rpcapi.attach_volume(ctxt, volume,
+ attachment['instance_uuid'],
+ attachment['attached_host'],
+ attachment['mountpoint'],
+ 'rw')
return volume['id']
def migrate_volume(self, ctxt, volume_id, host, force_host_copy=False,
and delete_snapshot()
1.21 - Adds update_consistencygroup.
1.22 - Adds create_consistencygroup_from_src.
+ 1.23 - Adds attachment_id to detach_volume
'''
BASE_RPC_API_VERSION = '1.0'
target = messaging.Target(topic=CONF.volume_topic,
version=self.BASE_RPC_API_VERSION)
serializer = objects_base.CinderObjectSerializer()
- self.client = rpc.get_client(target, '1.22', serializer=serializer)
+ self.client = rpc.get_client(target, '1.23', serializer=serializer)
def create_consistencygroup(self, ctxt, group, host):
new_host = utils.extract_host(host)
mountpoint=mountpoint,
mode=mode)
- def detach_volume(self, ctxt, volume):
+ def detach_volume(self, ctxt, volume, attachment_id):
new_host = utils.extract_host(volume['host'])
- cctxt = self.client.prepare(server=new_host)
- return cctxt.call(ctxt, 'detach_volume', volume_id=volume['id'])
+ cctxt = self.client.prepare(server=new_host, version='1.20')
+ return cctxt.call(ctxt, 'detach_volume', volume_id=volume['id'],
+ attachment_id=attachment_id)
def copy_volume_to_image(self, ctxt, volume, image_meta):
new_host = utils.extract_host(volume['host'])
usage_info = dict(tenant_id=volume_ref['project_id'],
host=volume_ref['host'],
user_id=volume_ref['user_id'],
- instance_uuid=volume_ref['instance_uuid'],
availability_zone=volume_ref['availability_zone'],
volume_id=volume_ref['id'],
volume_type=volume_ref['volume_type_id'],