Add volume multi attach support

This patch includes the Cinder changes needed
to support volume multiple attaches.  Nova and
python-cinderclient also need patches associated
to provide support for multiple attachments.

This adds the multiattach flag to volumes.  When a
volume is created, a multiattach flag can be set,
which allows a volume to be attached to more than
one Nova instance or host.  If the multiattach flag is
not set on a volume, it cannot be attached to more
than one Nova instance or host

Each volume attachment is tracked in a
new volume_attachment table.  The attachment id is
the unique identifier for each attachment to an
instance or host.

When a volume is to be detached the attachment
uuid must be passed in to the detach call in
order to determine which attachment should be
removed.  Since a volume can be attached to an
instance and a host, the attachment id is used
as the attachment identifier.

Nova:
https://review.openstack.org/#/c/153033/
https://review.openstack.org/#/c/153038/

python-cinderclient:
https://review.openstack.org/#/c/85856/

Change-Id: I950fa00ed5a30e7758245d5b0557f6df42dc58a3
Implements: blueprint multi-attach-volume
APIImpact
This commit is contained in:
Walter A. Boring IV 2014-12-10 01:03:39 +00:00
parent 490f03b48a
commit 10d5421687
38 changed files with 1413 additions and 487 deletions

View File

@ -185,7 +185,10 @@ class VolumeAdminController(AdminController):
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')

View File

@ -127,7 +127,11 @@ class VolumeActionsController(wsgi.Controller):
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')

View File

@ -48,18 +48,18 @@ def _translate_attachment_detail_view(_context, vol):
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
@ -91,10 +91,14 @@ def _translate_volume_summary_view(context, vol, image_id=None):
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']
@ -146,6 +150,7 @@ def make_volume(elem):
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',
@ -373,6 +378,8 @@ class VolumeController(wsgi.Controller):
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

View File

@ -70,7 +70,8 @@ class ViewBuilder(common.ViewBuilder):
'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')
}
}
@ -83,19 +84,17 @@ class ViewBuilder(common.ViewBuilder):
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

View File

@ -44,6 +44,7 @@ SCHEDULER_HINTS_NAMESPACE =\
def make_attachment(elem):
elem.set('id')
elem.set('attachment_id')
elem.set('server_id')
elem.set('host_name')
elem.set('volume_id')
@ -63,6 +64,7 @@ def make_volume(elem):
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',
@ -412,6 +414,8 @@ class VolumeController(wsgi.Controller):
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,

View File

@ -197,16 +197,26 @@ class BackupManager(manager.SchedulerDependentManager):
for volume in volumes:
volume_host = volume_utils.extract_host(volume['host'], 'backend')
backend = self._get_volume_backend(host=volume_host)
attachments = volume['volume_attachment']
if attachments:
if volume['status'] == 'backing-up':
LOG.info(_LI('Resetting volume %s to available '
'(was backing-up).') % volume['id'])
'(was backing-up).'), volume['id'])
mgr = self._get_manager(backend)
mgr.detach_volume(ctxt, volume['id'])
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('Resetting volume %s to error_restoring '
'(was restoring-backup).') % volume['id'])
LOG.info(_LI('setting volume %s to error_restoring '
'(was restoring-backup).'), volume['id'])
mgr = self._get_manager(backend)
mgr.detach_volume(ctxt, volume['id'])
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'})

View File

@ -136,10 +136,16 @@ def iscsi_target_create_safe(context, values):
###############
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):
@ -169,9 +175,9 @@ def volume_destroy(context, volume_id):
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):
@ -219,6 +225,27 @@ def volume_update(context, volume_id, values):
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)
####################

View File

@ -975,18 +975,51 @@ def reservation_expire(context):
@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
@ -1134,18 +1167,57 @@ def volume_destroy(context, volume_id):
@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 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['mountpoint'] = None
volume_ref['attach_status'] = 'detached'
volume_ref['instance_uuid'] = None
volume_ref['attached_host'] = None
volume_ref['attach_time'] = None
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
@ -1156,12 +1228,14 @@ def _volume_get_query(context, session=None, project_only=False):
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'))
@ -1177,6 +1251,54 @@ def _volume_get(context, volume_id, session=None):
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)
@ -1544,6 +1666,17 @@ def volume_update(context, volume_id, values):
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):

View File

@ -0,0 +1,147 @@
# (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

View File

@ -0,0 +1,87 @@
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;

View File

@ -129,10 +129,6 @@ class Volume(BASE, CinderBase):
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))
@ -157,6 +153,7 @@ class Volume(BASE, CinderBase):
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))
@ -197,6 +194,26 @@ class VolumeAdminMetadata(BASE, CinderBase):
'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"
@ -576,6 +593,7 @@ def register_models():
Volume,
VolumeMetadata,
VolumeAdminMetadata,
VolumeAttachment,
SnapshotMetadata,
Transfer,
VolumeTypeExtraSpecs,

View File

@ -252,6 +252,11 @@ class VolumeNotFound(NotFound):
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.")

View File

@ -45,10 +45,6 @@ class Volume(base.CinderPersistentObject, base.CinderObject,
'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),

View File

@ -391,15 +391,14 @@ class AdminActionsTest(test.TestCase):
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')
@ -415,7 +414,8 @@ class AdminActionsTest(test.TestCase):
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
@ -423,12 +423,12 @@ class AdminActionsTest(test.TestCase):
# 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')
@ -445,17 +445,18 @@ class AdminActionsTest(test.TestCase):
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')
@ -470,7 +471,8 @@ class AdminActionsTest(test.TestCase):
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
@ -478,12 +480,11 @@ class AdminActionsTest(test.TestCase):
# 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')
@ -502,11 +503,10 @@ class AdminActionsTest(test.TestCase):
# 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,
@ -514,15 +514,7 @@ class AdminActionsTest(test.TestCase):
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()
@ -538,9 +530,9 @@ class AdminActionsTest(test.TestCase):
# 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'
@ -549,16 +541,8 @@ class AdminActionsTest(test.TestCase):
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()
@ -587,19 +571,23 @@ class AdminActionsTest(test.TestCase):
'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):

View File

@ -37,7 +37,7 @@ CONF = cfg.CONF
class VolumeActionsTest(test.TestCase):
_actions = ('os-detach', 'os-reserve', 'os-unreserve')
_actions = ('os-reserve', 'os-unreserve')
_methods = ('attach', 'detach', 'reserve_volume', 'unreserve_volume')
@ -179,6 +179,16 @@ class VolumeActionsTest(test.TestCase):
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'}}

View File

@ -29,9 +29,6 @@ def stub_volume(id, **kwargs):
'host': 'fakehost',
'size': 1,
'availability_zone': 'fakeaz',
'instance_uuid': 'fakeuuid',
'attached_host': None,
'mountpoint': '/',
'attached_mode': 'rw',
'status': 'fakestatus',
'migration_status': None,
@ -46,6 +43,8 @@ def stub_volume(id, **kwargs):
'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)

View File

@ -85,11 +85,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -176,11 +173,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -258,13 +252,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -294,13 +283,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -328,6 +312,10 @@ class VolumeApiTest(test.TestCase):
{"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",
@ -339,18 +327,20 @@ class VolumeApiTest(test.TestCase):
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,
@ -400,11 +390,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -430,21 +417,28 @@ class VolumeApiTest(test.TestCase):
{"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',
'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,
@ -469,11 +463,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -499,21 +490,28 @@ class VolumeApiTest(test.TestCase):
{"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',
'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,
@ -536,11 +534,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -569,6 +564,7 @@ class VolumeApiTest(test.TestCase):
'display_name': 'displayname',
'encrypted': False,
'attachments': [],
'multiattach': 'false',
'bootable': 'false',
'volume_type': 'vol_type_name',
'snapshot_id': None,
@ -594,11 +590,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -661,21 +654,28 @@ class VolumeApiTest(test.TestCase):
{"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',
'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,

View File

@ -30,9 +30,6 @@ def stub_volume(id, **kwargs):
'host': 'fakehost',
'size': 1,
'availability_zone': 'fakeaz',
'instance_uuid': 'fakeuuid',
'attached_host': None,
'mountpoint': '/',
'status': 'fakestatus',
'migration_status': None,
'attach_status': 'attached',
@ -53,7 +50,10 @@ def stub_volume(id, **kwargs):
'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):

View File

@ -87,12 +87,7 @@ class VolumeApiTest(test.TestCase):
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,
@ -107,6 +102,7 @@ class VolumeApiTest(test.TestCase):
'metadata': {},
'name': 'Volume Test Name',
'replication_status': 'disabled',
'multiattach': False,
'size': 100,
'snapshot_id': None,
'source_volid': None,
@ -222,11 +218,7 @@ class VolumeApiTest(test.TestCase):
"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,
@ -242,6 +234,7 @@ class VolumeApiTest(test.TestCase):
'metadata': {},
'name': 'Volume Test Name',
'replication_status': 'disabled',
'multiattach': False,
'size': '1',
'snapshot_id': None,
'source_volid': None,
@ -312,11 +305,7 @@ class VolumeApiTest(test.TestCase):
"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,
@ -332,6 +321,7 @@ class VolumeApiTest(test.TestCase):
'metadata': {},
'name': 'Volume Test Name',
'replication_status': 'disabled',
'multiattach': False,
'size': '1',
'snapshot_id': None,
'source_volid': None,
@ -406,11 +396,7 @@ class VolumeApiTest(test.TestCase):
"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,
@ -426,6 +412,7 @@ class VolumeApiTest(test.TestCase):
'metadata': {},
'name': 'Volume Test Name',
'replication_status': 'disabled',
'multiattach': False,
'size': '1',
'snapshot_id': None,
'source_volid': None,
@ -500,15 +487,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -554,15 +534,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -611,15 +584,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -663,13 +629,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -707,6 +668,10 @@ class VolumeApiTest(test.TestCase):
{"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",
@ -718,7 +683,7 @@ class VolumeApiTest(test.TestCase):
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',
@ -726,10 +691,12 @@ class VolumeApiTest(test.TestCase):
'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': '/',
}],
@ -753,8 +720,8 @@ class VolumeApiTest(test.TestCase):
}
],
}}
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 = {}
@ -831,15 +798,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -877,6 +837,10 @@ class VolumeApiTest(test.TestCase):
{"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)
@ -885,7 +849,7 @@ class VolumeApiTest(test.TestCase):
expected = {
'volumes': [
{
'status': 'fakestatus',
'status': 'in-use',
'description': 'displaydesc',
'encrypted': False,
'availability_zone': 'fakeaz',
@ -893,10 +857,12 @@ class VolumeApiTest(test.TestCase):
'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'
@ -1311,15 +1277,8 @@ class VolumeApiTest(test.TestCase):
'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,
@ -1362,6 +1321,7 @@ class VolumeApiTest(test.TestCase):
'consistencygroup_id': None,
'name': 'displayname',
'replication_status': 'disabled',
'multiattach': False,
'attachments': [],
'user_id': 'fakeuser',
'volume_type': 'vol_type_name',
@ -1406,6 +1366,10 @@ class VolumeApiTest(test.TestCase):
{"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)
@ -1413,7 +1377,7 @@ class VolumeApiTest(test.TestCase):
res_dict = self.controller.show(req, '1')
expected = {
'volume': {
'status': 'fakestatus',
'status': 'in-use',
'description': 'displaydesc',
'encrypted': False,
'availability_zone': 'fakeaz',
@ -1421,10 +1385,12 @@ class VolumeApiTest(test.TestCase):
'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'

View File

@ -105,6 +105,13 @@ class BaseBackupTest(test.TestCase):
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',
@ -138,8 +145,12 @@ class BackupTestCase(BaseBackupTest):
"""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')

View File

@ -232,26 +232,36 @@ class DBAPIVolumeTestCase(BaseTest):
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):
@ -276,28 +286,38 @@ class DBAPIVolumeTestCase(BaseTest):
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, {})

View File

@ -1770,7 +1770,8 @@ class HP3PARBaseDriver(object):
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,
@ -1784,7 +1785,7 @@ class HP3PARBaseDriver(object):
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

View File

@ -772,6 +772,41 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin):
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)

View File

@ -430,7 +430,6 @@ class VolumeTestCase(BaseVolumeTestCase):
'volume_id': volume_id,
'volume_type': None,
'snapshot_id': None,
'instance_uuid': None,
'user_id': 'fake',
'launched_at': 'DONTCARE',
'size': 1,
@ -1748,14 +1747,15 @@ class VolumeTestCase(BaseVolumeTestCase):
**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')
@ -1767,15 +1767,220 @@ class VolumeTestCase(BaseVolumeTestCase):
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,
@ -1792,17 +1997,17 @@ class VolumeTestCase(BaseVolumeTestCase):
**self.volume_params)
volume_id = volume['id']
self.volume.create_volume(self.context, volume_id)
self.volume.attach_volume(self.context, volume_id, None,
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:
@ -1812,13 +2017,13 @@ class VolumeTestCase(BaseVolumeTestCase):
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')
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)
self.volume.detach_volume(self.context, volume_id, attachment['id'])
vol = db.volume_get(self.context, volume_id)
self.assertEqual(vol['status'], "available")
@ -1828,6 +2033,131 @@ class VolumeTestCase(BaseVolumeTestCase):
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, 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.volume.delete_volume(self.context, volume_id)
self.assertRaises(exception.VolumeNotFound,
db.volume_get,
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"
@ -1835,21 +2165,18 @@ class VolumeTestCase(BaseVolumeTestCase):
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:
@ -1859,30 +2186,30 @@ class VolumeTestCase(BaseVolumeTestCase):
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:
@ -1891,19 +2218,19 @@ class VolumeTestCase(BaseVolumeTestCase):
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,
@ -1929,10 +2256,10 @@ class VolumeTestCase(BaseVolumeTestCase):
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:
@ -1949,10 +2276,10 @@ class VolumeTestCase(BaseVolumeTestCase):
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:
@ -1978,11 +2305,11 @@ class VolumeTestCase(BaseVolumeTestCase):
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,
@ -1994,11 +2321,11 @@ class VolumeTestCase(BaseVolumeTestCase):
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')
@ -2022,7 +2349,7 @@ class VolumeTestCase(BaseVolumeTestCase):
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:
@ -2033,20 +2360,31 @@ class VolumeTestCase(BaseVolumeTestCase):
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'}]
volume_get.return_value = fake_volume
volume_attachment_get_used_by_volume_id.return_value = fake_attachments
volume_update.return_value = fake_volume
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)
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."""
@ -2350,7 +2688,11 @@ class VolumeTestCase(BaseVolumeTestCase):
# 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()
@ -2369,7 +2711,11 @@ class VolumeTestCase(BaseVolumeTestCase):
# 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()
@ -2827,8 +3173,11 @@ class VolumeTestCase(BaseVolumeTestCase):
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'])
@ -3358,9 +3707,12 @@ class VolumeTestCase(BaseVolumeTestCase):
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,
@ -3381,15 +3733,16 @@ class VolumeTestCase(BaseVolumeTestCase):
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)
@ -4072,8 +4425,6 @@ class CopyVolumeToImageTestCase(BaseVolumeTestCase):
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
@ -4082,7 +4433,7 @@ class CopyVolumeToImageTestCase(BaseVolumeTestCase):
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

View File

@ -221,7 +221,9 @@ class VolumeRpcAPITestCase(test.TestCase):
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',

View File

@ -17,6 +17,8 @@ from cinder import context
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()
@ -61,6 +63,21 @@ def create_volume(ctxt,
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',

View File

@ -162,7 +162,7 @@ class API(base.Base):
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
@ -237,6 +237,7 @@ class API(base.Base):
'optional_args': {'is_quota_committed': False},
'consistencygroup': consistencygroup,
'cgsnapshot': cgsnapshot,
'multiattach': multiattach,
}
try:
if cgsnapshot:
@ -480,6 +481,13 @@ class API(base.Base):
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)
@ -487,7 +495,13 @@ class API(base.Base):
@wrap_check_policy
def unreserve_volume(self, context, volume):
if volume['status'] == "attaching":
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
@ -538,8 +552,9 @@ class API(base.Base):
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):

View File

@ -771,7 +771,7 @@ class BaseVD(object):
"""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

View File

@ -172,7 +172,7 @@ class DateraDriver(san.SanISCSIDriver):
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')

View File

@ -169,10 +169,11 @@ class HP3PARCommon(object):
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 = {}
@ -1537,6 +1538,11 @@ class HP3PARCommon(object):
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,
@ -1546,7 +1552,12 @@ class HP3PARCommon(object):
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')

View File

@ -75,10 +75,11 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
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)
@ -438,10 +439,10 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
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)

View File

@ -82,10 +82,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
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)
@ -691,10 +692,10 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
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)

View File

@ -220,7 +220,7 @@ class ScalityDriver(driver.VolumeDriver):
"""Disallow connection from connector."""
pass
def detach_volume(self, context, volume):
def detach_volume(self, context, volume, attachment=None):
"""Callback for volume detached."""
pass

View File

@ -923,7 +923,7 @@ class SolidFireDriver(san.SanISCSIDriver):
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'])

View File

@ -482,7 +482,7 @@ class EntryCreateTask(flow_utils.CinderTask):
'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
@ -508,6 +508,7 @@ class EntryCreateTask(flow_utils.CinderTask):
'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

View File

@ -137,6 +137,25 @@ def locked_volume_operation(f):
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.
@ -162,7 +181,7 @@ def locked_snapshot_operation(f):
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)
@ -692,45 +711,46 @@ class VolumeManager(manager.SchedulerDependentManager):
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):
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?
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)
if instance_uuid and not uuidutils.is_uuid_like(instance_uuid):
self.db.volume_update(context, volume_id,
{'status': 'error_attaching'})
raise exception.InvalidUUID(uuid=instance_uuid)
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")
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_attachment_update(context, attachment_id,
{'attach_status':
'error_attaching'})
raise exception.InvalidUUID(uuid=instance_uuid)
volume = self.db.volume_get(context, volume_id)
@ -739,6 +759,7 @@ class VolumeManager(manager.SchedulerDependentManager):
{'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
@ -752,24 +773,55 @@ class VolumeManager(manager.SchedulerDependentManager):
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")
@ -779,14 +831,15 @@ class VolumeManager(manager.SchedulerDependentManager):
# 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')
@ -851,8 +904,7 @@ class VolumeManager(manager.SchedulerDependentManager):
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:
@ -1144,8 +1196,8 @@ class VolumeManager(manager.SchedulerDependentManager):
# 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
@ -1156,8 +1208,11 @@ class VolumeManager(manager.SchedulerDependentManager):
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")
@ -1190,8 +1245,8 @@ class VolumeManager(manager.SchedulerDependentManager):
{'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'
@ -1255,11 +1310,12 @@ class VolumeManager(manager.SchedulerDependentManager):
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'],
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']

View File

@ -63,6 +63,7 @@ class VolumeAPI(object):
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'
@ -72,7 +73,7 @@ class VolumeAPI(object):
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)
@ -171,10 +172,11 @@ class VolumeAPI(object):
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'])

View File

@ -46,7 +46,6 @@ def _usage_from_volume(context, volume_ref, **kw):
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'],