]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add support for encrypted volumes
authorJoel Coffman <joel.coffman@jhuapl.edu>
Wed, 14 Aug 2013 14:00:15 +0000 (10:00 -0400)
committerJoel Coffman <joel.coffman@jhuapl.edu>
Wed, 14 Aug 2013 14:00:15 +0000 (10:00 -0400)
This modification adds an encryption key UUID field to the volume
table, which is sufficient to make Cinder "aware" of encrypted volumes
as designated by predefined volume types. Integration with a key
manager is necessary to obtain an actual encryption key UUID (the
current implementation generates a random UUID when an encrypted
volumes is created). Cinder should *not* presume that it necessarily
will have access to the key itself -- this decision depends upon the
design, implementation, and policy for encrypted volumes. The key's
UUID is stored in Cinder because it is metadata about the volume.

Implements: blueprint encrypt-cinder-volumes
Change-Id: I164290e761a9922919a70f22f99af70dac213b61
SecurityImpact

30 files changed:
cinder/api/contrib/volume_encryption_metadata.py [new file with mode: 0644]
cinder/api/contrib/volume_type_encryption.py [new file with mode: 0644]
cinder/db/api.py
cinder/db/sqlalchemy/api.py
cinder/db/sqlalchemy/migrate_repo/versions/017_add_encryption_information.py [new file with mode: 0644]
cinder/db/sqlalchemy/models.py
cinder/exception.py
cinder/keymgr/__init__.py
cinder/keymgr/key_mgr.py
cinder/keymgr/not_implemented_key_mgr.py [new file with mode: 0644]
cinder/tests/api/contrib/test_quotas.py
cinder/tests/api/contrib/test_volume_encryption_metadata.py [new file with mode: 0644]
cinder/tests/api/contrib/test_volume_type_encryption.py [new file with mode: 0644]
cinder/tests/api/v1/test_snapshot_metadata.py
cinder/tests/api/v2/test_snapshot_metadata.py
cinder/tests/keymgr/fake.py [new file with mode: 0644]
cinder/tests/keymgr/mock_key_mgr.py
cinder/tests/keymgr/test_mock_key_mgr.py
cinder/tests/keymgr/test_not_implemented_key_mgr.py [new file with mode: 0644]
cinder/tests/policy.json
cinder/tests/test_db_api.py
cinder/tests/test_migrations.py
cinder/tests/test_quota.py
cinder/tests/test_volume.py
cinder/tests/test_volume_types.py
cinder/volume/api.py
cinder/volume/driver.py
cinder/volume/flows/create_volume.py
cinder/volume/volume_types.py
etc/cinder/policy.json

diff --git a/cinder/api/contrib/volume_encryption_metadata.py b/cinder/api/contrib/volume_encryption_metadata.py
new file mode 100644 (file)
index 0000000..d4bded6
--- /dev/null
@@ -0,0 +1,92 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
+# 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.
+
+"""The volume encryption metadata extension."""
+
+import webob
+
+from cinder.api import extensions
+from cinder.api.openstack import wsgi
+from cinder.api import xmlutil
+from cinder import db
+from cinder import exception
+from cinder.openstack.common.notifier import api as notifier_api
+from cinder.volume import volume_types
+
+authorize = extensions.extension_authorizer('volume',
+                                            'volume_encryption_metadata')
+
+
+class VolumeEncryptionMetadataTemplate(xmlutil.TemplateBuilder):
+    def construct(self):
+        root = xmlutil.make_flat_dict('encryption', selector='encryption')
+        return xmlutil.MasterTemplate(root, 1)
+
+
+class VolumeEncryptionMetadataController(wsgi.Controller):
+    """The volume encryption metadata API extension"""
+
+    def _get_volume_encryption_metadata(self, context, volume_id):
+        return db.volume_encryption_metadata_get(context, volume_id)
+
+    def _is_volume_type_encrypted(self, context, volume_id):
+        volume_ref = db.volume_get(context, volume_id)
+        volume_type_id = volume_ref['volume_type_id']
+        return volume_types.is_encrypted(context, volume_type_id)
+
+    def _get_metadata(self, req, volume_id):
+        context = req.environ['cinder.context']
+        authorize(context)
+        if self._is_volume_type_encrypted(context, volume_id):
+            return self._get_volume_encryption_metadata(context, volume_id)
+        else:
+            return {
+                'encryption_key_id': None,
+                # Additional metadata defaults could go here.
+            }
+
+    @wsgi.serializers(xml=VolumeEncryptionMetadataTemplate)
+    def index(self, req, volume_id):
+        """Returns the encryption metadata for a given volume."""
+        return self._get_metadata(req, volume_id)
+
+    @wsgi.serializers(xml=VolumeEncryptionMetadataTemplate)
+    def show(self, req, volume_id, id):
+        """Return a single encryption item."""
+        encryption_item = self.index(req, volume_id)
+        if encryption_item is not None:
+            return encryption_item[id]
+        else:
+            return None
+
+
+class Volume_encryption_metadata(extensions.ExtensionDescriptor):
+    """Volume encryption metadata retrieval support."""
+
+    name = "VolumeEncryptionMetadata"
+    alias = "os-volume-encryption-metadata"
+    namespace = ("http://docs.openstack.org/volume/ext/"
+                 "os-volume-encryption-metadata/api/v1")
+    updated = "2013-07-10T00:00:00+00:00"
+
+    def get_resources(self):
+        resources = []
+        res = extensions.ResourceExtension(
+            'encryption', VolumeEncryptionMetadataController(),
+            parent=dict(member_name='volume', collection_name='volumes'))
+        resources.append(res)
+        return resources
diff --git a/cinder/api/contrib/volume_type_encryption.py b/cinder/api/contrib/volume_type_encryption.py
new file mode 100644 (file)
index 0000000..aede70f
--- /dev/null
@@ -0,0 +1,161 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
+# 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.
+
+"""The volume types encryption extension."""
+
+import webob
+
+from cinder.api import extensions
+from cinder.api.openstack import wsgi
+from cinder.api import xmlutil
+from cinder import db
+from cinder import exception
+from cinder.openstack.common.notifier import api as notifier_api
+from cinder.volume import volume_types
+
+authorize = extensions.extension_authorizer('volume',
+                                            'volume_type_encryption')
+
+CONTROL_LOCATION = ['front-end', 'back-end']
+
+
+class VolumeTypeEncryptionTemplate(xmlutil.TemplateBuilder):
+    def construct(self):
+        root = xmlutil.make_flat_dict('encryption', selector='encryption')
+        return xmlutil.MasterTemplate(root, 1)
+
+
+class VolumeTypeEncryptionController(wsgi.Controller):
+    """The volume type encryption API controller for the OpenStack API """
+
+    def _get_volume_type_encryption(self, context, type_id):
+        encryption_ref = db.volume_type_encryption_get(context, type_id)
+        encryption_specs = {}
+        if not encryption_ref:
+            return encryption_specs
+        for key, value in encryption_ref.iteritems():
+            encryption_specs[key] = value
+        return encryption_specs
+
+    def _check_type(self, context, type_id):
+        try:
+            volume_types.get_volume_type(context, type_id)
+        except exception.NotFound as ex:
+            raise webob.exc.HTTPNotFound(explanation=unicode(ex))
+
+    def _check_encryption_input(self, encryption, create=True):
+        if 'key_size' in encryption.keys():
+            key_size = encryption['key_size']
+            if key_size is not None:
+                if isinstance(key_size, (int, long)):
+                    if key_size < 0:
+                        msg = _('key_size must be non-negative')
+                        raise exception.InvalidInput(reason=msg)
+                else:
+                    msg = _('key_size must be an integer')
+                    raise exception.InvalidInput(reason=msg)
+
+        if create:
+            msg = None
+            if 'provider' not in encryption.keys():
+                msg = _('provider must be defined')
+            elif 'control_location' not in encryption.keys():
+                msg = _('control_location must be defined')
+
+            if msg is not None:
+                raise exception.InvalidInput(reason=msg)
+
+        # Check control location
+        if 'control_location' in encryption.keys():
+            if encryption['control_location'] not in CONTROL_LOCATION:
+                msg = _("Valid control location are: %s") % CONTROL_LOCATION
+                raise exception.InvalidInput(reason=msg)
+
+    @wsgi.serializers(xml=VolumeTypeEncryptionTemplate)
+    def index(self, req, type_id):
+        """Returns the encryption specs for a given volume type."""
+        context = req.environ['cinder.context']
+        authorize(context)
+        self._check_type(context, type_id)
+        return self._get_volume_type_encryption(context, type_id)
+
+    @wsgi.serializers(xml=VolumeTypeEncryptionTemplate)
+    def create(self, req, type_id, body=None):
+        """Create encryption specs for an existing volume type."""
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        if not self.is_valid_body(body, 'encryption'):
+            expl = _('Create body is not valid.')
+            raise webob.exc.HTTPBadRequest(explanation=expl)
+
+        self._check_type(context, type_id)
+
+        encryption_specs = self._get_volume_type_encryption(context, type_id)
+        if encryption_specs:
+            raise exception.VolumeTypeEncryptionExists(type_id=type_id)
+
+        encryption_specs = body['encryption']
+
+        self._check_encryption_input(encryption_specs)
+
+        db.volume_type_encryption_update_or_create(context, type_id,
+                                                   encryption_specs)
+        notifier_info = dict(type_id=type_id, specs=encryption_specs)
+        notifier_api.notify(context, 'volumeTypeEncryption',
+                            'volume_type_encryption.create',
+                            notifier_api.INFO, notifier_info)
+        return body
+
+    @wsgi.serializers(xml=VolumeTypeEncryptionTemplate)
+    def show(self, req, type_id, id):
+        """Return a single encryption item."""
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        self._check_type(context, type_id)
+
+        encryption_specs = self._get_volume_type_encryption(context, type_id)
+
+        if id not in encryption_specs:
+            raise webob.exc.HTTPNotFound()
+
+        return {id: encryption_specs[id]}
+
+
+class Volume_type_encryption(extensions.ExtensionDescriptor):
+    """Encryption support for volume types."""
+
+    name = "VolumeTypeEncryption"
+    alias = "encryption"
+    namespace = ("http://docs.openstack.org/volume/ext/"
+                 "volume-type-encryption/api/v1")
+    updated = "2013-07-01T00:00:00+00:00"
+
+    def get_resources(self):
+        resources = []
+        res = extensions.ResourceExtension(
+            Volume_type_encryption.alias,
+            VolumeTypeEncryptionController(),
+            parent=dict(member_name='type', collection_name='types'))
+        resources.append(res)
+        return resources
+
+    def get_controller_extensions(self):
+        controller = VolumeTypeEncryptionController()
+        extension = extensions.ControllerExtension(self, 'types', controller)
+        return [extension]
index cc1536adb4e3198332dcf8f4b6246f93e6e6112f..54616635cfb0fe4d3b4a30b7c927573274d2f41e 100644 (file)
@@ -405,6 +405,37 @@ def volume_type_extra_specs_update_or_create(context,
 ###################
 
 
+def volume_type_encryption_get(context, volume_type_id, session=None):
+    return IMPL.volume_type_encryption_get(context, volume_type_id, session)
+
+
+def volume_type_encryption_delete(context, volume_type_id):
+    return IMPL.volume_type_encryption_delete(context, volume_type_id)
+
+
+# TODO(joel-coffman): split into two functions -- update and create
+def volume_type_encryption_update_or_create(context, volume_type_id,
+                                            encryption_specs):
+    return IMPL.volume_type_encryption_update_or_create(context,
+                                                        volume_type_id,
+                                                        encryption_specs)
+
+
+def volume_type_encryption_volume_get(context, volume_type_id, session=None):
+    return IMPL.volume_type_encryption_volume_get(context, volume_type_id,
+                                                  session)
+
+
+###################
+
+
+def volume_encryption_metadata_get(context, volume_id, session=None):
+    return IMPL.volume_encryption_metadata_get(context, volume_id, session)
+
+
+###################
+
+
 def volume_glance_metadata_create(context, volume_id, key, value):
     """Update the Glance metadata for the specified volume."""
     return IMPL.volume_glance_metadata_create(context,
index a8149807171e97d3b6a3079f426ca58269719aa0..025942f5e267d000acf2971f28d68bdf758a6363 100644 (file)
@@ -1799,6 +1799,74 @@ def volume_type_extra_specs_update_or_create(context, volume_type_id,
 ####################
 
 
+@require_context
+def volume_type_encryption_get(context, volume_type_id, session=None):
+    return model_query(context, models.Encryption, session=session,
+                       read_deleted="no").\
+        filter_by(volume_type_id=volume_type_id).first()
+
+
+@require_admin_context
+def volume_type_encryption_delete(context, volume_type_id):
+    session = get_session()
+    with session.begin():
+        encryption = volume_type_encryption_get(context, volume_type_id,
+                                                session)
+        encryption.update({'deleted': True,
+                           'deleted_at': timeutils.utcnow(),
+                           'updated_at': literal_column('updated_at')})
+
+
+# TODO(joel-coffman): split into two functions -- update and create
+@require_admin_context
+def volume_type_encryption_update_or_create(context, volume_type_id,
+                                            values):
+    session = get_session()
+    encryption = volume_type_encryption_get(context, volume_type_id,
+                                            session)
+
+    if not encryption:
+        encryption = models.Encryption()
+
+        if 'volume_type_id' not in values:
+            values['volume_type_id'] = volume_type_id
+
+    encryption.update(values)
+    encryption.save(session=session)
+
+    return encryption
+
+
+def volume_type_encryption_volume_get(context, volume_type_id, session=None):
+    volume_list = _volume_get_query(context, session=session,
+                                    project_only=False).\
+        filter_by(volume_type_id=volume_type_id).\
+        all()
+    return volume_list
+
+####################
+
+
+@require_admin_context
+def volume_encryption_metadata_get(context, volume_id, session=None):
+    """Return the encryption key id for a given volume."""
+
+    volume_ref = _volume_get(context, volume_id)
+    encryption_ref = volume_type_encryption_get(context,
+                                                volume_ref['volume_type_id'])
+
+    return {
+        'encryption_key_id': volume_ref['encryption_key_id'],
+        'control_location': encryption_ref['control_location'],
+        'cipher': encryption_ref['cipher'],
+        'key_size': encryption_ref['key_size'],
+        'provider': encryption_ref['provider'],
+    }
+
+
+####################
+
+
 @require_context
 @require_volume_exists
 def _volume_glance_metadata_get(context, volume_id, session=None):
diff --git a/cinder/db/sqlalchemy/migrate_repo/versions/017_add_encryption_information.py b/cinder/db/sqlalchemy/migrate_repo/versions/017_add_encryption_information.py
new file mode 100644 (file)
index 0000000..60d31ea
--- /dev/null
@@ -0,0 +1,169 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
+# 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.
+
+from sqlalchemy import Column, ForeignKey, MetaData, Table
+from sqlalchemy import Boolean, DateTime, Integer, String
+
+from cinder.openstack.common import log as logging
+from cinder.openstack.common import timeutils
+from cinder.openstack.common import uuidutils
+
+
+LOG = logging.getLogger(__name__)
+
+
+def _populate_encryption_types(volume_types, encryption):
+    # TODO(joel-coffman): The database currently doesn't enforce uniqueness
+    # for volume type names.
+    default_encryption_types = {
+        'dm-crypt': {
+            'cipher': 'aes-xts-plain64',
+            'control_location': 'front-end',
+            'key_size': 512,  # only half of key is used for cipher in XTS mode
+            'provider':
+            'nova.volume.encryptors.cryptsetup.CryptsetupEncryptor',
+        },
+        'LUKS': {
+            'cipher': 'aes-xts-plain64',
+            'control_location': 'front-end',
+            'key_size': 512,  # only half of key is used for cipher in XTS mode
+            'provider': 'nova.volume.encryptors.luks.LuksEncryptor',
+        },
+    }
+
+    try:
+        volume_types_insert = volume_types.insert()
+        encryption_insert = encryption.insert()
+
+        for key, values in default_encryption_types.iteritems():
+            current_time = timeutils.utcnow()
+            volume_type = {
+                'id': uuidutils.generate_uuid(),
+                'name': key,
+                'created_at': current_time,
+                'updated_at': current_time,
+                'deleted': False,
+            }
+            volume_types_insert.execute(volume_type)
+
+            values['id'] = uuidutils.generate_uuid()
+            values['volume_type_id'] = volume_type['id']
+
+            values['created_at'] = timeutils.utcnow()
+            values['updated_at'] = values['created_at']
+            values['deleted'] = False
+
+            encryption_insert.execute(values)
+    except Exception:
+        LOG.error(_("Error populating default encryption types!"))
+        # NOTE(joel-coffman): do not raise because deployed environment may
+        # have volume types already defined with the same name
+
+
+def upgrade(migrate_engine):
+    meta = MetaData(bind=migrate_engine)
+
+    # encryption key UUID -- must be stored per volume
+    volumes = Table('volumes', meta, autoload=True)
+    encryption_key = Column('encryption_key_id', String(36))
+    try:
+        volumes.create_column(encryption_key)
+    except Exception:
+        LOG.error(_("Column |%s| not created!"), repr(encryption_key))
+        raise
+
+    # encryption key UUID and volume type id -- must be stored per snapshot
+    snapshots = Table('snapshots', meta, autoload=True)
+    encryption_key = Column('encryption_key_id', String(36))
+    try:
+        snapshots.create_column(encryption_key)
+    except Exception:
+        LOG.error(_("Column |%s| not created!"), repr(encryption_key))
+        raise
+    volume_type = Column('volume_type_id', String(36))
+    try:
+        snapshots.create_column(volume_type)
+    except Exception:
+        LOG.error(_("Column |%s| not created!"), repr(volume_type))
+        raise
+
+    volume_types = Table('volume_types', meta, autoload=True)
+
+    # encryption types associated with particular volume type
+    encryption = Table(
+        'encryption', meta,
+        Column('created_at', DateTime(timezone=False)),
+        Column('updated_at', DateTime(timezone=False)),
+        Column('deleted_at', DateTime(timezone=False)),
+        Column('deleted', Boolean(create_constraint=True, name=None)),
+        Column('cipher', String(length=255)),
+        Column('control_location', String(length=255), nullable=False),
+        Column('key_size', Integer),
+        Column('provider', String(length=255), nullable=False),
+        # NOTE(joel-coffman): The volume_type_id must be unique or else the
+        # referenced volume type becomes ambiguous. That is, specifying the
+        # volume type is not sufficient to identify a particular encryption
+        # scheme unless each volume type is associated with at most one
+        # encryption scheme.
+        Column('volume_type_id', String(length=36),
+               ForeignKey(volume_types.c.id),
+               primary_key=True, nullable=False),
+        mysql_engine='InnoDB'
+    )
+
+    try:
+        encryption.create()
+    except Exception:
+        LOG.error(_("Table |%s| not created!"), repr(encryption))
+        raise
+
+    _populate_encryption_types(volume_types, encryption)
+
+
+def downgrade(migrate_engine):
+    meta = MetaData(bind=migrate_engine)
+
+    # drop encryption key UUID for volumes
+    volumes = Table('volumes', meta, autoload=True)
+    try:
+        volumes.c.encryption_key_id.drop()
+    except Exception:
+        LOG.error(_("encryption_key_id column not dropped from volumes"))
+        raise
+
+    # drop encryption key UUID and volume type id for snapshots
+    snapshots = Table('snapshots', meta, autoload=True)
+    try:
+        snapshots.c.encryption_key_id.drop()
+    except Exception:
+        LOG.error(_("encryption_key_id column not dropped from snapshots"))
+        raise
+    try:
+        snapshots.c.volume_type_id.drop()
+    except Exception:
+        LOG.error(_("volume_type_id column not dropped from snapshots"))
+        raise
+
+    # drop encryption types table
+    encryption = Table('encryption', meta, autoload=True)
+    try:
+        encryption.drop()
+    except Exception:
+        LOG.error(_("encryption table not dropped"))
+        raise
+
+    # TODO(joel-coffman): Should remove volume_types related to encryption...
index e9265e3d01c3e1ef5f3c76d22c607327e7482095..9da28cd34b0589d4a45e2c20cc30b74d4814a7f7 100644 (file)
@@ -124,6 +124,8 @@ class Volume(BASE, CinderBase):
 
     volume_type_id = Column(String(36))
     source_volid = Column(String(36))
+    encryption_key_id = Column(String(36))
+
     deleted = Column(Boolean, default=False)
     bootable = Column(Boolean, default=False)
 
@@ -291,6 +293,9 @@ class Snapshot(BASE, CinderBase):
     display_name = Column(String(255))
     display_description = Column(String(255))
 
+    encryption_key_id = Column(String(36))
+    volume_type_id = Column(String(36))
+
     provider_location = Column(String(255))
 
     volume = relationship(Volume, backref="snapshots",
@@ -358,6 +363,31 @@ class Backup(BASE, CinderBase):
     object_count = Column(Integer)
 
 
+class Encryption(BASE, CinderBase):
+    """Represents encryption requirement for a volume type.
+
+    Encryption here is a set of performance characteristics describing
+    cipher, provider, and key_size for a certain volume type.
+    """
+
+    __tablename__ = 'encryption'
+    cipher = Column(String(255))
+    key_size = Column(Integer)
+    provider = Column(String(255))
+    control_location = Column(String(255))
+    volume_type_id = Column(String(36),
+                            ForeignKey('volume_types.id'),
+                            primary_key=True)
+    volume_type = relationship(
+        VolumeTypes,
+        backref="encryption",
+        foreign_keys=volume_type_id,
+        primaryjoin='and_('
+        'Encryption.volume_type_id == VolumeTypes.id,'
+        'Encryption.deleted == False)'
+    )
+
+
 class Transfer(BASE, CinderBase):
     """Represents a volume transfer request."""
     __tablename__ = 'transfers'
index d49bc0daac44096ac2ba950b43498ba37da5b33b..1492574ee48daf255b8e3c99071f365779644240 100644 (file)
@@ -401,6 +401,10 @@ class VolumeTypeExists(Duplicate):
     message = _("Volume Type %(id)s already exists.")
 
 
+class VolumeTypeEncryptionExists(Invalid):
+    message = _("Volume type encryption for type %(type_id)s already exists.")
+
+
 class MigrationError(CinderException):
     message = _("Migration error") + ": %(reason)s"
 
index f8d2f0c29d7cccc6e5115f799cb79c970c03c000..5540c738cf2a1b8698d6721e11ec6d234b47f06b 100644 (file)
@@ -21,7 +21,8 @@ from cinder.openstack.common import log as logging
 
 keymgr_opts = [
     cfg.StrOpt('keymgr_api_class',
-               default='cinder.keymgr.key_mgr.KeyManager',
+               default='cinder.keymgr.'
+                       'not_implemented_key_mgr.NotImplementedKeyManager',
                help='The full class name of the key manager API class'),
 ]
 
index 4d48eee196e59105a848df019540742fceb906fa..bd5669fa56e1d9375074a4d4f203a85f740e77d1 100644 (file)
@@ -52,6 +52,21 @@ class KeyManager(object):
         """
         pass
 
+    @abc.abstractmethod
+    def copy_key(self, ctxt, key_id, **kwargs):
+        """Copies (i.e., clones) a key stored by the key manager.
+
+        This method copies the specified key and returns the copy's UUID. If
+        the specified context does not permit copying keys, then a
+        NotAuthorized error should be raised.
+
+        Implementation note: This method should behave identically to
+            store_key(context, get_key(context, <encryption key UUID>))
+        although it is preferable to perform this operation within the key
+        manager to avoid unnecessary handling of the key material.
+        """
+        pass
+
     @abc.abstractmethod
     def get_key(self, ctxt, key_id, **kwargs):
         """Retrieves the specified key.
diff --git a/cinder/keymgr/not_implemented_key_mgr.py b/cinder/keymgr/not_implemented_key_mgr.py
new file mode 100644 (file)
index 0000000..52d6b38
--- /dev/null
@@ -0,0 +1,42 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
+# 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.
+
+"""
+Key manager implementation that raises NotImplementedError
+"""
+
+from cinder.keymgr import key_mgr
+
+
+class NotImplementedKeyManager(key_mgr.KeyManager):
+    """Key Manager Interface that raises NotImplementedError for all operations
+    """
+
+    def create_key(self, ctxt, algorithm='AES', length=256, expiration=None,
+                   **kwargs):
+        raise NotImplementedError()
+
+    def store_key(self, ctxt, key, expiration=None, **kwargs):
+        raise NotImplementedError()
+
+    def copy_key(self, ctxt, key_id, **kwargs):
+        raise NotImplementedError()
+
+    def get_key(self, ctxt, key_id, **kwargs):
+        raise NotImplementedError()
+
+    def delete_key(self, ctxt, key_id, **kwargs):
+        raise NotImplementedError()
index 922be8392233f5362e609fd0d990603553cd6ec3..7bef4f2411e51ed6bda89dc1a4315413b4b40612 100644 (file)
@@ -25,6 +25,7 @@ import webob.exc
 
 from cinder.api.contrib import quotas
 from cinder import context
+from cinder import db
 from cinder import test
 
 
@@ -33,6 +34,13 @@ def make_body(root=True, gigabytes=1000, snapshots=10,
     resources = {'gigabytes': gigabytes,
                  'snapshots': snapshots,
                  'volumes': volumes}
+    # need to consider preexisting volume types as well
+    volume_types = db.volume_type_get_all(context.get_admin_context())
+    for volume_type in volume_types:
+        resources['gigabytes_' + volume_type] = -1
+        resources['snapshots_' + volume_type] = -1
+        resources['volumes_' + volume_type] = -1
+
     if tenant_id:
         resources['id'] = tenant_id
     if root:
diff --git a/cinder/tests/api/contrib/test_volume_encryption_metadata.py b/cinder/tests/api/contrib/test_volume_encryption_metadata.py
new file mode 100644 (file)
index 0000000..a14a2b3
--- /dev/null
@@ -0,0 +1,235 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
+# 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.
+
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+import json
+import webob
+
+from cinder.api.contrib import volume_encryption_metadata
+from cinder import context
+from cinder import db
+from cinder import test
+from cinder.tests.api import fakes
+from cinder.volume import volume_types
+
+
+def return_volume_type_encryption_metadata(context, volume_type_id):
+    return stub_volume_type_encryption()
+
+
+def stub_volume_type_encryption():
+    values = {
+        'cipher': 'cipher',
+        'key_size': 256,
+        'provider': 'nova.volume.encryptors.base.VolumeEncryptor',
+        'volume_type_id': 'volume_type',
+        'control_location': 'front-end',
+    }
+    return values
+
+
+class VolumeEncryptionMetadataTest(test.TestCase):
+    @staticmethod
+    def _create_volume(context,
+                       display_name='test_volume',
+                       display_description='this is a test volume',
+                       status='creating',
+                       availability_zone='fake_az',
+                       host='fake_host',
+                       size=1):
+        """Create a volume object."""
+        volume = {
+            'size': size,
+            'user_id': 'fake',
+            'project_id': 'fake',
+            'status': status,
+            'display_name': display_name,
+            'display_description': display_description,
+            'attach_status': 'detached',
+            'availability_zone': availability_zone,
+            'host': host,
+            'encryption_key_id': 'fake_key',
+        }
+        return db.volume_create(context, volume)['id']
+
+    def setUp(self):
+        super(VolumeEncryptionMetadataTest, self).setUp()
+        self.controller = (volume_encryption_metadata.
+                           VolumeEncryptionMetadataController())
+        self.stubs.Set(db.sqlalchemy.api, 'volume_type_encryption_get',
+                       return_volume_type_encryption_metadata)
+
+        self.ctxt = context.RequestContext('fake', 'fake', is_admin=True)
+        self.volume_id = self._create_volume(self.ctxt)
+
+    def tearDown(self):
+        db.volume_destroy(self.ctxt, self.volume_id)
+        super(VolumeEncryptionMetadataTest, self).tearDown()
+
+    def test_index(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: True)
+
+        req = webob.Request.blank('/v2/fake/volumes/%s/encryption'
+                                  % self.volume_id)
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+        self.assertEqual(200, res.status_code)
+        res_dict = json.loads(res.body)
+
+        expected = {
+            "encryption_key_id": "fake_key",
+            "control_location": "front-end",
+            "cipher": "cipher",
+            "provider": "nova.volume.encryptors.base.VolumeEncryptor",
+            "key_size": 256,
+        }
+        self.assertEqual(expected, res_dict)
+
+    def test_index_bad_tenant_id(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: True)
+
+        req = webob.Request.blank('/v2/%s/volumes/%s/encryption'
+                                  % ('bad-tenant-id', self.volume_id))
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+        self.assertEqual(400, res.status_code)
+
+        res_dict = json.loads(res.body)
+        expected = {'badRequest': {'code': 400,
+                                   'message': 'Malformed request url'}}
+        self.assertEqual(expected, res_dict)
+
+    def test_index_bad_volume_id(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: True)
+
+        bad_volume_id = 'bad_volume_id'
+        req = webob.Request.blank('/v2/fake/volumes/%s/encryption'
+                                  % bad_volume_id)
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+        self.assertEqual(404, res.status_code)
+
+        res_dict = json.loads(res.body)
+        expected = {'itemNotFound': {'code': 404,
+                                     'message': 'VolumeNotFound: Volume '
+                                                '%s could not be found.'
+                                                % bad_volume_id}}
+        self.assertEqual(expected, res_dict)
+
+    def test_show_key(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: True)
+
+        req = webob.Request.blank('/v2/fake/volumes/%s/encryption/'
+                                  'encryption_key_id' % self.volume_id)
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+        self.assertEqual(200, res.status_code)
+
+        self.assertEqual('fake_key', res.body)
+
+    def test_show_control(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: True)
+
+        req = webob.Request.blank('/v2/fake/volumes/%s/encryption/'
+                                  'control_location' % self.volume_id)
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+        self.assertEqual(200, res.status_code)
+
+        self.assertEqual('front-end', res.body)
+
+    def test_show_provider(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: True)
+
+        req = webob.Request.blank('/v2/fake/volumes/%s/encryption/'
+                                  'provider' % self.volume_id)
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+        self.assertEqual(200, res.status_code)
+
+        self.assertEqual('nova.volume.encryptors.base.VolumeEncryptor',
+                         res.body)
+
+    def test_show_bad_tenant_id(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: True)
+
+        req = webob.Request.blank('/v2/%s/volumes/%s/encryption/'
+                                  'encryption_key_id' % ('bad-tenant-id',
+                                                         self.volume_id))
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+        self.assertEqual(400, res.status_code)
+
+        res_dict = json.loads(res.body)
+        expected = {'badRequest': {'code': 400,
+                                   'message': 'Malformed request url'}}
+        self.assertEqual(expected, res_dict)
+
+    def test_show_bad_volume_id(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: True)
+
+        bad_volume_id = 'bad_volume_id'
+        req = webob.Request.blank('/v2/fake/volumes/%s/encryption/'
+                                  'encryption_key_id' % bad_volume_id)
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+        self.assertEqual(404, res.status_code)
+
+        res_dict = json.loads(res.body)
+        expected = {'itemNotFound': {'code': 404,
+                                     'message': 'VolumeNotFound: Volume '
+                                                '%s could not be found.'
+                                                % bad_volume_id}}
+        self.assertEqual(expected, res_dict)
+
+    def test_retrieve_key_not_admin(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: True)
+
+        ctxt = self.ctxt.deepcopy()
+        ctxt.is_admin = False
+
+        req = webob.Request.blank('/v2/fake/volumes/%s/encryption/'
+                                  'encryption_key_id' % self.volume_id)
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=ctxt))
+        self.assertEqual(403, res.status_code)
+        res_dict = json.loads(res.body)
+
+        expected = {
+            'forbidden': {
+                'code': 403,
+                'message': ("Policy doesn't allow volume_extension:"
+                            "volume_encryption_metadata to be performed.")
+            }
+        }
+        self.assertEqual(expected, res_dict)
+
+    def test_show_volume_not_encrypted_type(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: False)
+
+        req = webob.Request.blank('/v2/fake/volumes/%s/encryption/'
+                                  'encryption_key_id' % self.volume_id)
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+        self.assertEqual(200, res.status_code)
+        self.assertEqual(0, len(res.body))
+
+    def test_index_volume_not_encrypted_type(self):
+        self.stubs.Set(volume_types, 'is_encrypted', lambda *a, **kw: False)
+
+        req = webob.Request.blank('/v2/fake/volumes/%s/encryption'
+                                  % self.volume_id)
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=self.ctxt))
+
+        self.assertEqual(200, res.status_code)
+        res_dict = json.loads(res.body)
+
+        expected = {
+            'encryption_key_id': None
+        }
+        self.assertEqual(expected, res_dict)
diff --git a/cinder/tests/api/contrib/test_volume_type_encryption.py b/cinder/tests/api/contrib/test_volume_type_encryption.py
new file mode 100644 (file)
index 0000000..a6b1b8e
--- /dev/null
@@ -0,0 +1,378 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
+# 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.
+
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+import json
+import webob
+from xml.dom import minidom
+
+from cinder.api.contrib import volume_type_encryption
+from cinder import context
+from cinder import db
+from cinder import exception
+from cinder.openstack.common.notifier import api as notifier_api
+from cinder.openstack.common.notifier import test_notifier
+from cinder import test
+from cinder.tests.api import fakes
+from cinder.volume import volume_types
+
+
+def return_volume_type_encryption_db(context, volume_type_id, session):
+    return stub_volume_type_encryption()
+
+
+def return_volume_type_encryption(context, volume_type_id):
+    return stub_volume_type_encryption()
+
+
+def stub_volume_type_encryption():
+    values = {
+        'cipher': 'fake_cipher',
+        'control_location': 'front-end',
+        'key_size': 256,
+        'provider': 'fake_provider',
+        'volume_type_id': 'fake_type_id',
+    }
+    return values
+
+
+def volume_type_encryption_get(context, volume_type_id):
+    pass
+
+
+class VolumeTypeEncryptionTest(test.TestCase):
+
+    def setUp(self):
+        super(VolumeTypeEncryptionTest, self).setUp()
+        self.flags(connection_type='fake',
+                   host='fake',
+                   notification_driver=[test_notifier.__name__])
+        self.api_path = '/v2/fake/os-volume-types/1/encryption'
+        """to reset notifier drivers left over from other api/contrib tests"""
+        notifier_api._reset_drivers()
+        test_notifier.NOTIFICATIONS = []
+
+    def tearDown(self):
+        notifier_api._reset_drivers()
+        super(VolumeTypeEncryptionTest, self).tearDown()
+
+    def _get_response(self, volume_type, admin=True,
+                      url='/v2/fake/types/%s/encryption',
+                      req_method='GET', req_body=None,
+                      req_headers=None):
+        ctxt = context.RequestContext('fake', 'fake', is_admin=admin)
+
+        req = webob.Request.blank(url % volume_type['id'])
+        req.method = req_method
+        req.body = req_body
+        if req_headers:
+            req.headers['Content-Type'] = req_headers
+
+        return req.get_response(fakes.wsgi_app(fake_auth_context=ctxt))
+
+    def test_index(self):
+        self.stubs.Set(db, 'volume_type_encryption_get',
+                       return_volume_type_encryption)
+
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+        db.volume_type_create(context.get_admin_context(), volume_type)
+
+        res = self._get_response(volume_type)
+        self.assertEqual(200, res.status_code)
+        res_dict = json.loads(res.body)
+
+        expected = stub_volume_type_encryption()
+        self.assertEqual(expected, res_dict)
+
+        db.volume_type_destroy(context.get_admin_context(), volume_type['id'])
+
+    def test_index_invalid_type(self):
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+
+        res = self._get_response(volume_type)
+        self.assertEqual(404, res.status_code)
+        res_dict = json.loads(res.body)
+
+        expected = {
+            'itemNotFound': {
+                'code': 404,
+                'message': ('Volume type %s could not be found.'
+                            % volume_type['id'])
+            }
+        }
+        self.assertEqual(expected, res_dict)
+
+    def test_show_key_size(self):
+        self.stubs.Set(db, 'volume_type_encryption_get',
+                       return_volume_type_encryption)
+
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+        db.volume_type_create(context.get_admin_context(), volume_type)
+
+        res = self._get_response(volume_type,
+                                 url='/v2/fake/types/%s/encryption/key_size')
+        res_dict = json.loads(res.body)
+
+        self.assertEqual(200, res.status_code)
+        self.assertEqual(256, res_dict['key_size'])
+
+        db.volume_type_destroy(context.get_admin_context(), volume_type['id'])
+
+    def test_show_provider(self):
+        self.stubs.Set(db, 'volume_type_encryption_get',
+                       return_volume_type_encryption)
+
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+        db.volume_type_create(context.get_admin_context(), volume_type)
+
+        res = self._get_response(volume_type,
+                                 url='/v2/fake/types/%s/encryption/provider')
+        res_dict = json.loads(res.body)
+
+        self.assertEqual(200, res.status_code)
+        self.assertEqual('fake_provider', res_dict['provider'])
+        db.volume_type_destroy(context.get_admin_context(), volume_type['id'])
+
+    def test_show_item_not_found(self):
+        self.stubs.Set(db, 'volume_type_encryption_get',
+                       return_volume_type_encryption)
+
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+        db.volume_type_create(context.get_admin_context(), volume_type)
+
+        res = self._get_response(volume_type,
+                                 url='/v2/fake/types/%s/encryption/fake')
+        res_dict = json.loads(res.body)
+
+        self.assertEqual(404, res.status_code)
+        expected = {
+            'itemNotFound': {
+                'code': 404,
+                'message': ('The resource could not be found.')
+            }
+        }
+        self.assertEqual(expected, res_dict)
+        db.volume_type_destroy(context.get_admin_context(), volume_type['id'])
+
+    def _create(self, cipher, control_location, key_size, provider):
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+        db.volume_type_create(context.get_admin_context(), volume_type)
+
+        body = {"encryption": {'cipher': cipher,
+                               'control_location': control_location,
+                               'key_size': key_size,
+                               'provider': provider,
+                               'volume_type_id': volume_type['id']}}
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        res = self._get_response(volume_type)
+        res_dict = json.loads(res.body)
+        self.assertEqual(200, res.status_code)
+        # Confirm that volume type has no encryption information
+        # before create.
+        self.assertEqual('{}', res.body)
+
+        # Create encryption specs for the volume type
+        # with the defined body.
+        res = self._get_response(volume_type, req_method='POST',
+                                 req_body=json.dumps(body),
+                                 req_headers='application/json')
+        res_dict = json.loads(res.body)
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+        # check response
+        self.assertTrue('encryption' in res_dict)
+        self.assertEqual(cipher, res_dict['encryption']['cipher'])
+        self.assertEqual(control_location,
+                         res_dict['encryption']['control_location'])
+        self.assertEqual(key_size, res_dict['encryption']['key_size'])
+        self.assertEqual(provider, res_dict['encryption']['provider'])
+        self.assertEqual(volume_type['id'],
+                         res_dict['encryption']['volume_type_id'])
+
+        # check database
+        encryption = db.volume_type_encryption_get(context.get_admin_context(),
+                                                   volume_type['id'])
+        self.assertIsNotNone(encryption)
+        self.assertEqual(cipher, encryption['cipher'])
+        self.assertEqual(key_size, encryption['key_size'])
+        self.assertEqual(provider, encryption['provider'])
+        self.assertEqual(volume_type['id'], encryption['volume_type_id'])
+
+        db.volume_type_destroy(context.get_admin_context(), volume_type['id'])
+
+    def test_create_json(self):
+        self._create('fake_cipher', 'front-end', 128, 'fake_encryptor')
+
+    def test_create_xml(self):
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+        db.volume_type_create(context.get_admin_context(), volume_type)
+
+        ctxt = context.RequestContext('fake', 'fake', is_admin=True)
+
+        req = webob.Request.blank('/v2/fake/types/%s/encryption'
+                                  % volume_type['id'])
+        req.method = 'POST'
+        req.body = ('<encryption provider="test_provider" '
+                    'cipher="cipher" control_location="front-end" />')
+        req.headers['Content-Type'] = 'application/xml'
+        req.headers['Accept'] = 'application/xml'
+        res = req.get_response(fakes.wsgi_app(fake_auth_context=ctxt))
+
+        self.assertEqual(res.status_int, 200)
+
+        db.volume_type_destroy(context.get_admin_context(), volume_type['id'])
+
+    def test_create_invalid_volume_type(self):
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+
+        body = {"encryption": {'cipher': 'cipher',
+                               'control_location': 'front-end',
+                               'key_size': 128,
+                               'provider': 'fake_provider',
+                               'volume_type_id': 'volume_type'}}
+
+        res = self._get_response(volume_type, req_method='POST',
+                                 req_body=json.dumps(body),
+                                 req_headers='application/json')
+        res_dict = json.loads(res.body)
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        self.assertEqual(404, res.status_code)
+
+        expected = {
+            'itemNotFound': {
+                'code': 404,
+                'message': ('Volume type %s could not be found.'
+                            % volume_type['id'])
+            }
+        }
+        self.assertEqual(expected, res_dict)
+
+    def test_create_encryption_type_exists(self):
+        self.stubs.Set(db, 'volume_type_encryption_get',
+                       return_volume_type_encryption)
+
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+        db.volume_type_create(context.get_admin_context(), volume_type)
+
+        body = {"encryption": {'cipher': 'cipher',
+                               'control_location': 'front-end',
+                               'key_size': 128,
+                               'provider': 'fake_provider',
+                               'volume_type_id': volume_type['id']}}
+
+        # Try to create encryption specs for a volume type
+        # that already has them.
+        res = self._get_response(volume_type, req_method='POST',
+                                 req_body=json.dumps(body),
+                                 req_headers='application/json')
+        res_dict = json.loads(res.body)
+
+        expected = {
+            'badRequest': {
+                'code': 400,
+                'message': ('Volume type encryption for type '
+                            'fake_type_id already exists.')
+            }
+        }
+        self.assertEqual(expected, res_dict)
+        db.volume_type_destroy(context.get_admin_context(), volume_type['id'])
+
+    def _encryption_create_bad_body(self, body,
+                                    msg='Create body is not valid.'):
+        volume_type = {
+            'id': 'fake_type_id',
+            'name': 'fake_type',
+        }
+        db.volume_type_create(context.get_admin_context(), volume_type)
+        res = self._get_response(volume_type, req_method='POST',
+                                 req_body=json.dumps(body),
+                                 req_headers='application/json')
+
+        res_dict = json.loads(res.body)
+
+        expected = {
+            'badRequest': {
+                'code': 400,
+                'message': (msg)
+            }
+        }
+        self.assertEqual(expected, res_dict)
+        db.volume_type_destroy(context.get_admin_context(), volume_type['id'])
+
+    def test_create_no_body(self):
+        self._encryption_create_bad_body(body=None)
+
+    def test_create_malformed_entity(self):
+        body = {'encryption': 'string'}
+        self._encryption_create_bad_body(body=body)
+
+    def test_create_negative_key_size(self):
+        body = {"encryption": {'cipher': 'cipher',
+                               'key_size': -128,
+                               'provider': 'fake_provider',
+                               'volume_type_id': 'volume_type'}}
+        msg = 'Invalid input received: key_size must be non-negative'
+        self._encryption_create_bad_body(body=body, msg=msg)
+
+    def test_create_none_key_size(self):
+        self._create('fake_cipher', 'front-end', None, 'fake_encryptor')
+
+    def test_create_invalid_control_location(self):
+        body = {"encryption": {'cipher': 'cipher',
+                               'control_location': 'fake_control',
+                               'provider': 'fake_provider',
+                               'volume_type_id': 'volume_type'}}
+        msg = ("Invalid input received: Valid control location are: "
+               "['front-end', 'back-end']")
+        self._encryption_create_bad_body(body=body, msg=msg)
+
+    def test_create_no_provider(self):
+        body = {"encryption": {'cipher': 'cipher',
+                               'volume_type_id': 'volume_type'}}
+        msg = ("Invalid input received: provider must be defined")
+        self._encryption_create_bad_body(body=body, msg=msg)
index 3b7e907f7b0b5a01fde00adc0340b24258383ba8..80dc0ea582d11e1f19f807140d1905c98cee5550 100644 (file)
@@ -88,6 +88,8 @@ def return_volume(context, volume_id):
             'name': 'fake',
             'host': 'fake-host',
             'status': 'available',
+            'encryption_key_id': None,
+            'volume_type_id': None,
             'metadata': {}}
 
 
index 906f1cd54a55ad0f7b34e0a1f801ca6851af7b06..b5d12eb7864e70e9bb0ef586f05c170d9041d441 100644 (file)
@@ -88,6 +88,8 @@ def return_volume(context, volume_id):
             'name': 'fake',
             'host': 'fake-host',
             'status': 'available',
+            'encryption_key_id': None,
+            'volume_type_id': None,
             'metadata': {}}
 
 
diff --git a/cinder/tests/keymgr/fake.py b/cinder/tests/keymgr/fake.py
new file mode 100644 (file)
index 0000000..9fa5367
--- /dev/null
@@ -0,0 +1,26 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Justin Santa Barbara
+# Copyright 2012 OpenStack LLC
+# 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.
+
+"""Implementation of a fake key manager."""
+
+
+from cinder.tests.keymgr import mock_key_mgr
+
+
+def fake_api():
+    return mock_key_mgr.MockKeyManager()
index d74200911d785ced2f3a772623edc70e067139d7..62b7c4a3d612d31127025feb5ce7e79362d65c0b 100644 (file)
@@ -68,21 +68,33 @@ class MockKeyManager(key_mgr.KeyManager):
 
         return self.store_key(ctxt, _key)
 
+    def _generate_key_id(self):
+        key_id = uuidutils.generate_uuid()
+        while key_id in self.keys:
+            key_id = uuidutils.generate_uuid()
+
+        return key_id
+
     def store_key(self, ctxt, key, **kwargs):
         """Stores (i.e., registers) a key with the key manager.
         """
         if ctxt is None:
             raise exception.NotAuthorized()
 
-        # generate UUID and ensure that it isn't in use
-        key_id = uuidutils.generate_uuid()
-        while key_id in self.keys:
-            key_id = uuidutils.generate_uuid()
-
+        key_id = self._generate_key_id()
         self.keys[key_id] = key
 
         return key_id
 
+    def copy_key(self, ctxt, key_id, **kwargs):
+        if ctxt is None:
+            raise exception.NotAuthorized()
+
+        copied_key_id = self._generate_key_id()
+        self.keys[copied_key_id] = self.keys[key_id]
+
+        return copied_key_id
+
     def get_key(self, ctxt, key_id, **kwargs):
         """Retrieves the key identified by the specified id.
 
index 586b159c118927048479f9b77d2045bcf97c0aa1..a542620932c14ec23a2c94b1cda4be43a9b244b2 100644 (file)
@@ -66,6 +66,20 @@ class MockKeyManagerTestCase(test_key_mgr.KeyManagerTestCase):
         self.assertRaises(exception.NotAuthorized,
                           self.key_mgr.store_key, None, None)
 
+    def test_copy_key(self):
+        key_id = self.key_mgr.create_key(self.ctxt)
+        key = self.key_mgr.get_key(self.ctxt, key_id)
+
+        copied_key_id = self.key_mgr.copy_key(self.ctxt, key_id)
+        copied_key = self.key_mgr.get_key(self.ctxt, copied_key_id)
+
+        self.assertNotEqual(key_id, copied_key_id)
+        self.assertEqual(key, copied_key)
+
+    def test_copy_null_context(self):
+        self.assertRaises(exception.NotAuthorized,
+                          self.key_mgr.copy_key, None, None)
+
     def test_get_key(self):
         pass
 
diff --git a/cinder/tests/keymgr/test_not_implemented_key_mgr.py b/cinder/tests/keymgr/test_not_implemented_key_mgr.py
new file mode 100644 (file)
index 0000000..6d60d90
--- /dev/null
@@ -0,0 +1,51 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
+# 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.
+
+"""
+Test cases for the not implemented key manager.
+"""
+
+from cinder.keymgr import not_implemented_key_mgr
+from cinder.tests.keymgr import test_key_mgr
+
+
+class NotImplementedKeyManagerTestCase(test_key_mgr.KeyManagerTestCase):
+
+    def _create_key_manager(self):
+        return not_implemented_key_mgr.NotImplementedKeyManager()
+
+    def setUp(self):
+        super(NotImplementedKeyManagerTestCase, self).setUp()
+
+    def test_create_key(self):
+        self.assertRaises(NotImplementedError,
+                          self.key_mgr.create_key, None)
+
+    def test_store_key(self):
+        self.assertRaises(NotImplementedError,
+                          self.key_mgr.store_key, None, None)
+
+    def test_copy_key(self):
+        self.assertRaises(NotImplementedError,
+                          self.key_mgr.copy_key, None, None)
+
+    def test_get_key(self):
+        self.assertRaises(NotImplementedError,
+                          self.key_mgr.get_key, None, None)
+
+    def test_delete_key(self):
+        self.assertRaises(NotImplementedError,
+                          self.key_mgr.delete_key, None, None)
index 5002931e53fb833f379d1e2d82ede9b2ba240aa0..ab48d86d4f958dc175098eb24730376dc19131fb 100644 (file)
@@ -36,6 +36,8 @@
     "volume_extension:volume_actions:upload_image": [],
     "volume_extension:types_manage": [],
     "volume_extension:types_extra_specs": [],
+    "volume_extension:volume_type_encryption": [["rule:admin_api"]],
+    "volume_extension:volume_encryption_metadata": [["rule:admin_api"]],
     "volume_extension:extended_snapshot_attributes": [],
     "volume_extension:volume_image_metadata": [],
     "volume_extension:volume_host_attribute": [["rule:admin_api"]],
index 891063672f66bb1bbde3dca16ca747021ce84d6c..83805e4575a76b84be20d367cd46f135d2c2d25a 100644 (file)
@@ -494,6 +494,83 @@ class DBAPISnapshotTestCase(BaseTest):
         self.assertEquals(should_be, db.snapshot_metadata_get(self.ctxt, 1))
 
 
+class DBAPIEncryptionTestCase(BaseTest):
+
+    """Tests for the db.api.volume_type_encryption_* methods."""
+
+    _ignored_keys = [
+        'deleted',
+        'deleted_at',
+        'created_at',
+        'updated_at',
+    ]
+
+    def setUp(self):
+        super(DBAPIEncryptionTestCase, self).setUp()
+        self.created = \
+            [db.volume_type_encryption_update_or_create(self.ctxt, 'fake_type',
+                                                        values)
+             for values in self._get_values()]
+
+    def _get_values(self, one=False):
+        base_values = {
+            'cipher': 'fake_cipher',
+            'key_size': 256,
+            'provider': 'fake_provider',
+            'volume_type_id': 'fake_type',
+            'control_location': 'front-end',
+        }
+        if one:
+            return base_values
+
+        def compose(val, step):
+            if isinstance(val, str):
+                step = str(step)
+            return val + step
+
+        return [dict([(k, compose(v, i)) for k, v in base_values.items()])
+                for i in range(1, 4)]
+
+    def test_volume_type_encryption_update_or_create(self):
+        values = self._get_values()
+        for i, encryption in enumerate(self.created):
+            print "values[%s] = %s" % (i, values[i])
+            print "encryption = %s" % encryption.__dict__
+            self._assertEqualObjects(values[i], encryption,
+                                     self._ignored_keys)
+
+    def test_volume_type_encryption_get(self):
+        for encryption in self.created:
+            print "encryption = %s" % encryption.__dict__
+            encryption_get = \
+                db.volume_type_encryption_get(self.ctxt,
+                                              encryption['volume_type_id'])
+            print "encryption_get = %s" % encryption_get.__dict__
+            self._assertEqualObjects(encryption, encryption_get,
+                                     self._ignored_keys)
+
+    def test_volume_type_encryption_delete(self):
+        values = {
+            'cipher': 'fake_cipher',
+            'key_size': 256,
+            'provider': 'fake_provider',
+            'volume_type_id': 'fake_type',
+            'control_location': 'front-end',
+        }
+
+        encryption = db.volume_type_encryption_update_or_create(self.ctxt,
+                                                                'fake_type',
+                                                                values)
+        self._assertEqualObjects(values, encryption, self._ignored_keys)
+
+        db.volume_type_encryption_delete(self.ctxt,
+                                         encryption['volume_type_id'])
+        encryption_get = \
+            db.volume_type_encryption_get(self.ctxt,
+                                          encryption['volume_type_id'])
+        self.assertEqual(None, encryption_get)
+
+
 class DBAPIReservationTestCase(BaseTest):
 
     """Tests for db.api.reservation_* methods."""
index 18ea31c8edbfb5b158976aa50124025bb11532f5..9073aca620ebd4dd175e792b448dfacc00fdd852 100644 (file)
@@ -842,3 +842,58 @@ class TestMigrations(test.TestCase):
                                                      'sm_backend_config'))
             self.assertTrue(engine.dialect.has_table(engine.connect(),
                                                      'sm_volume'))
+
+    def test_migration_017(self):
+        """Test that added encryption information works correctly."""
+        for (key, engine) in self.engines.items():
+            migration_api.version_control(engine,
+                                          TestMigrations.REPOSITORY,
+                                          migration.INIT_VERSION)
+
+            # upgrade schema
+            migration_api.upgrade(engine, TestMigrations.REPOSITORY, 16)
+            metadata = sqlalchemy.schema.MetaData()
+            metadata.bind = engine
+
+            migration_api.upgrade(engine, TestMigrations.REPOSITORY, 17)
+
+            # encryption key UUID
+            volumes = sqlalchemy.Table('volumes', metadata, autoload=True)
+            self.assertTrue('encryption_key_id' in volumes.c)
+            self.assertTrue(isinstance(volumes.c.encryption_key_id.type,
+                                       sqlalchemy.types.VARCHAR))
+
+            snapshots = sqlalchemy.Table('snapshots', metadata, autoload=True)
+            self.assertTrue('encryption_key_id' in snapshots.c)
+            self.assertTrue(isinstance(snapshots.c.encryption_key_id.type,
+                                       sqlalchemy.types.VARCHAR))
+            self.assertTrue('volume_type_id' in snapshots.c)
+            self.assertTrue(isinstance(snapshots.c.volume_type_id.type,
+                                       sqlalchemy.types.VARCHAR))
+
+            # encryption types table
+            encryption = sqlalchemy.Table('encryption',
+                                          metadata,
+                                          autoload=True)
+            self.assertTrue(isinstance(encryption.c.volume_type_id.type,
+                                       sqlalchemy.types.VARCHAR))
+            self.assertTrue(isinstance(encryption.c.cipher.type,
+                                       sqlalchemy.types.VARCHAR))
+            self.assertTrue(isinstance(encryption.c.key_size.type,
+                                       sqlalchemy.types.INTEGER))
+            self.assertTrue(isinstance(encryption.c.provider.type,
+                                       sqlalchemy.types.VARCHAR))
+
+            # downgrade schema
+            migration_api.downgrade(engine, TestMigrations.REPOSITORY, 16)
+            metadata = sqlalchemy.schema.MetaData()
+            metadata.bind = engine
+
+            volumes = sqlalchemy.Table('volumes', metadata, autoload=True)
+            self.assertTrue('encryption_key_id' not in volumes.c)
+
+            snapshots = sqlalchemy.Table('snapshots', metadata, autoload=True)
+            self.assertTrue('encryption_key_id' not in snapshots.c)
+
+            self.assertFalse(engine.dialect.has_table(engine.connect(),
+                                                      'encryption'))
index acacadd5ec36eb9b27c85eed6d780c0be715d8d6..3c2c1fb878ecc24b617a83ee798f00fbe4e7f243 100644 (file)
@@ -680,6 +680,10 @@ class QuotaEngineTestCase(test.TestCase):
 
 class VolumeTypeQuotaEngineTestCase(test.TestCase):
     def test_default_resources(self):
+        def fake_vtga(context, inactive=False, filters=None):
+            return {}
+        self.stubs.Set(db, 'volume_type_get_all', fake_vtga)
+
         engine = quota.VolumeTypeQuotaEngine()
         self.assertEqual(engine.resource_names,
                          ['gigabytes', 'snapshots', 'volumes'])
@@ -688,6 +692,22 @@ class VolumeTypeQuotaEngineTestCase(test.TestCase):
         ctx = context.RequestContext('admin', 'admin', is_admin=True)
         vtype = db.volume_type_create(ctx, {'name': 'type1'})
         vtype2 = db.volume_type_create(ctx, {'name': 'type_2'})
+
+        def fake_vtga(context, inactive=False, filters=None):
+            return {
+                'type1': {
+                    'id': vtype['id'],
+                    'name': 'type1',
+                    'extra_specs': {},
+                },
+                'type_2': {
+                    'id': vtype['id'],
+                    'name': 'type_2',
+                    'extra_specs': {},
+                },
+            }
+        self.stubs.Set(db, 'volume_type_get_all', fake_vtga)
+
         engine = quota.VolumeTypeQuotaEngine()
         self.assertEqual(engine.resource_names,
                          ['gigabytes', 'gigabytes_type1', 'gigabytes_type_2',
@@ -722,6 +742,7 @@ class DbQuotaDriverTestCase(test.TestCase):
     def test_get_defaults(self):
         # Use our pre-defined resources
         self._stub_quota_class_get_default()
+        self._stub_volume_type_get_all()
         result = self.driver.get_defaults(None, quota.QUOTAS.resources)
 
         self.assertEqual(
@@ -740,6 +761,11 @@ class DbQuotaDriverTestCase(test.TestCase):
                         gigabytes=1000,)
         self.stubs.Set(db, 'quota_class_get_default', fake_qcgd)
 
+    def _stub_volume_type_get_all(self):
+        def fake_vtga(context, inactive=False, filters=None):
+            return {}
+        self.stubs.Set(db, 'volume_type_get_all', fake_vtga)
+
     def _stub_quota_class_get_all_by_name(self):
         # Stub out quota_class_get_all_by_name
         def fake_qcgabn(context, quota_class):
@@ -750,6 +776,7 @@ class DbQuotaDriverTestCase(test.TestCase):
 
     def test_get_class_quotas(self):
         self._stub_quota_class_get_all_by_name()
+        self._stub_volume_type_get_all()
         result = self.driver.get_class_quotas(None, quota.QUOTAS.resources,
                                               'test_class')
 
@@ -789,6 +816,7 @@ class DbQuotaDriverTestCase(test.TestCase):
 
     def test_get_project_quotas(self):
         self._stub_get_by_project()
+        self._stub_volume_type_get_all()
         result = self.driver.get_project_quotas(
             FakeContext('test_project', 'test_class'),
             quota.QUOTAS.resources, 'test_project')
@@ -809,6 +837,7 @@ class DbQuotaDriverTestCase(test.TestCase):
 
     def test_get_project_quotas_alt_context_no_class(self):
         self._stub_get_by_project()
+        self._stub_volume_type_get_all()
         result = self.driver.get_project_quotas(
             FakeContext('other_project', 'other_class'),
             quota.QUOTAS.resources, 'test_project')
@@ -828,6 +857,7 @@ class DbQuotaDriverTestCase(test.TestCase):
 
     def test_get_project_quotas_alt_context_with_class(self):
         self._stub_get_by_project()
+        self._stub_volume_type_get_all()
         result = self.driver.get_project_quotas(
             FakeContext('other_project', 'other_class'),
             quota.QUOTAS.resources, 'test_project', quota_class='test_class')
@@ -848,6 +878,7 @@ class DbQuotaDriverTestCase(test.TestCase):
 
     def test_get_project_quotas_no_defaults(self):
         self._stub_get_by_project()
+        self._stub_volume_type_get_all()
         result = self.driver.get_project_quotas(
             FakeContext('test_project', 'test_class'),
             quota.QUOTAS.resources, 'test_project', defaults=False)
@@ -869,6 +900,7 @@ class DbQuotaDriverTestCase(test.TestCase):
 
     def test_get_project_quotas_no_usages(self):
         self._stub_get_by_project()
+        self._stub_volume_type_get_all()
         result = self.driver.get_project_quotas(
             FakeContext('test_project', 'test_class'),
             quota.QUOTAS.resources, 'test_project', usages=False)
index 82d5c57bc5945d4a8f76b8c5cdcb2d1b7ee2fcda..c000e01ed630e309f44722ea48880eedb92f3e2c 100644 (file)
@@ -37,6 +37,7 @@ from cinder import context
 from cinder import db
 from cinder import exception
 from cinder.image import image_utils
+from cinder import keymgr
 from cinder.openstack.common import importutils
 from cinder.openstack.common.notifier import api as notifier_api
 from cinder.openstack.common.notifier import test_notifier
@@ -46,6 +47,8 @@ from cinder import quota
 from cinder import test
 from cinder.tests import conf_fixture
 from cinder.tests.image import fake as fake_image
+from cinder.tests.keymgr import fake as fake_keymgr
+import cinder.volume
 from cinder.volume import configuration as conf
 from cinder.volume import driver
 from cinder.volume.drivers import lvm
@@ -137,6 +140,7 @@ class VolumeTestCase(test.TestCase):
 
         volume = self._create_volume()
         volume_id = volume['id']
+        self.assertIsNone(volume['encryption_key_id'])
         self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
         self.volume.create_volume(self.context, volume_id)
         self.assertEquals(len(test_notifier.NOTIFICATIONS), 2)
@@ -272,6 +276,7 @@ class VolumeTestCase(test.TestCase):
                                    'name',
                                    'description')
         self.assertEquals(volume['volume_type_id'], None)
+        self.assertEquals(volume['encryption_key_id'], None)
 
         # Create default volume type
         vol_type = conf_fixture.def_vol_type
@@ -287,6 +292,7 @@ class VolumeTestCase(test.TestCase):
                                    'name',
                                    'description')
         self.assertEquals(volume['volume_type_id'], db_vol_type.get('id'))
+        self.assertIsNone(volume['encryption_key_id'])
 
         # Create volume with specific volume type
         vol_type = 'test'
@@ -302,6 +308,22 @@ class VolumeTestCase(test.TestCase):
                                    volume_type=db_vol_type)
         self.assertEquals(volume['volume_type_id'], db_vol_type.get('id'))
 
+    def test_create_volume_with_encrypted_volume_type(self):
+        self.stubs.Set(keymgr, "API", fake_keymgr.fake_api)
+
+        volume_api = cinder.volume.api.API()
+
+        db_vol_type = db.volume_type_get_by_name(context.get_admin_context(),
+                                                 'LUKS')
+
+        volume = volume_api.create(self.context,
+                                   1,
+                                   'name',
+                                   'description',
+                                   volume_type=db_vol_type)
+        self.assertEquals(volume['volume_type_id'], db_vol_type.get('id'))
+        self.assertIsNotNone(volume['encryption_key_id'])
+
     def test_delete_busy_volume(self):
         """Test volume survives deletion if driver reports it as busy."""
         volume = self._create_volume()
@@ -343,6 +365,87 @@ class VolumeTestCase(test.TestCase):
         self.volume.delete_snapshot(self.context, snapshot_id)
         self.volume.delete_volume(self.context, volume_src['id'])
 
+    def test_create_volume_from_snapshot_with_encryption(self):
+        """Test volume can be created from a snapshot of
+        an encrypted volume.
+        """
+        self.stubs.Set(keymgr, 'API', fake_keymgr.fake_api)
+
+        volume_api = cinder.volume.api.API()
+
+        db_vol_type = db.volume_type_get_by_name(context.get_admin_context(),
+                                                 'LUKS')
+        volume_src = volume_api.create(self.context,
+                                       1,
+                                       'name',
+                                       'description',
+                                       volume_type=db_vol_type)
+        snapshot_ref = volume_api.create_snapshot_force(self.context,
+                                                        volume_src,
+                                                        'name',
+                                                        'description')
+        snapshot_ref['status'] = 'available'  # status must be available
+        volume_dst = volume_api.create(self.context,
+                                       1,
+                                       'name',
+                                       'description',
+                                       snapshot=snapshot_ref)
+        self.assertEqual(volume_dst['id'],
+                         db.volume_get(
+                             context.get_admin_context(),
+                             volume_dst['id']).id)
+        self.assertEqual(snapshot_ref['id'],
+                         db.volume_get(context.get_admin_context(),
+                                       volume_dst['id']).snapshot_id)
+
+        # ensure encryption keys match
+        self.assertIsNotNone(volume_src['encryption_key_id'])
+        self.assertIsNotNone(volume_dst['encryption_key_id'])
+
+        key_manager = volume_api.key_manager  # must use *same* key manager
+        volume_src_key = key_manager.get_key(self.context,
+                                             volume_src['encryption_key_id'])
+        volume_dst_key = key_manager.get_key(self.context,
+                                             volume_dst['encryption_key_id'])
+        self.assertEqual(volume_src_key, volume_dst_key)
+
+    def test_create_volume_from_encrypted_volume(self):
+        """Test volume can be created from an encrypted volume."""
+        self.stubs.Set(keymgr, 'API', fake_keymgr.fake_api)
+
+        volume_api = cinder.volume.api.API()
+
+        db_vol_type = db.volume_type_get_by_name(context.get_admin_context(),
+                                                 'LUKS')
+        volume_src = volume_api.create(self.context,
+                                       1,
+                                       'name',
+                                       'description',
+                                       volume_type=db_vol_type)
+        volume_src['status'] = 'available'  # status must be available
+        volume_dst = volume_api.create(self.context,
+                                       1,
+                                       'name',
+                                       'description',
+                                       source_volume=volume_src)
+        self.assertEqual(volume_dst['id'],
+                         db.volume_get(context.get_admin_context(),
+                                       volume_dst['id']).id)
+        self.assertEqual(volume_src['id'],
+                         db.volume_get(context.get_admin_context(),
+                                       volume_dst['id']).source_volid)
+
+        # ensure encryption keys match
+        self.assertIsNotNone(volume_src['encryption_key_id'])
+        self.assertIsNotNone(volume_dst['encryption_key_id'])
+
+        key_manager = volume_api.key_manager  # must use *same* key manager
+        volume_src_key = key_manager.get_key(self.context,
+                                             volume_src['encryption_key_id'])
+        volume_dst_key = key_manager.get_key(self.context,
+                                             volume_dst['encryption_key_id'])
+        self.assertEqual(volume_src_key, volume_dst_key)
+
     def test_create_volume_from_snapshot_fail_bad_size(self):
         """Test volume can't be created from snapshot with bad volume size."""
         volume_api = cinder.volume.api.API()
index c4f99d0d9639e275820ebe1f627f61f8720936cb..60102a75badc54ccb9aa186d068112e782d9501e 100644 (file)
@@ -187,3 +187,17 @@ class VolumeTypeTestCase(test.TestCase):
                          {"key1": "val1", "key2": "val2", "key3": "val3"})
         self.assertEqual(vol_types['type3']['extra_specs'],
                          {"key1": "val1", "key3": "val3", "key4": "val4"})
+
+    def test_is_encrypted(self):
+        volume_type = volume_types.create(self.ctxt, "type1")
+        volume_type_id = volume_type.get('id')
+        self.assertFalse(volume_types.is_encrypted(self.ctxt, volume_type_id))
+
+        encryption = {
+            'control_location': 'front-end',
+            'provider': 'fake_provider',
+        }
+        db_api.volume_type_encryption_update_or_create(self.ctxt,
+                                                       volume_type_id,
+                                                       encryption)
+        self.assertTrue(volume_types.is_encrypted(self.ctxt, volume_type_id))
index bf9d82db8862c0d687584ce888eed7e3a321cfa2..16a81457741de7c4c318b884ed9afd0ebf8e9760 100644 (file)
@@ -29,6 +29,7 @@ from cinder import context
 from cinder.db import base
 from cinder import exception
 from cinder.image import glance
+from cinder import keymgr
 from cinder.openstack.common import excutils
 from cinder.openstack.common import log as logging
 from cinder.openstack.common import timeutils
@@ -95,6 +96,7 @@ class API(base.Base):
         self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
         self.volume_rpcapi = volume_rpcapi.VolumeAPI()
         self.availability_zone_names = ()
+        self.key_manager = keymgr.API()
         super(API, self).__init__(db_driver)
 
     def _valid_availabilty_zone(self, availability_zone):
@@ -132,7 +134,7 @@ class API(base.Base):
     def create(self, context, size, name, description, snapshot=None,
                image_id=None, volume_type=None, metadata=None,
                availability_zone=None, source_volume=None,
-               scheduler_hints=None):
+               scheduler_hints=None, backup_source_volume=None):
 
         def check_volume_az_zone(availability_zone):
             try:
@@ -153,6 +155,8 @@ class API(base.Base):
             'availability_zone': availability_zone,
             'source_volume': source_volume,
             'scheduler_hints': scheduler_hints,
+            'key_manager': self.key_manager,
+            'backup_source_volume': backup_source_volume,
         }
         (flow, uuid) = create_volume.get_api_flow(self.scheduler_rpcapi,
                                                   self.volume_rpcapi,
@@ -226,6 +230,13 @@ class API(base.Base):
             msg = _("Volume still has %d dependent snapshots") % len(snapshots)
             raise exception.InvalidVolume(reason=msg)
 
+        # If the volume is encrypted, delete its encryption key from the key
+        # manager. This operation makes volume deletion an irreversible process
+        # because the volume cannot be decrypted without its key.
+        encryption_key_id = volume.get('encryption_key_id', None)
+        if encryption_key_id is not None:
+            self.key_manager.delete_key(encryption_key_id)
+
         now = timeutils.utcnow()
         self.db.volume_update(context, volume_id, {'status': 'deleting',
                                                    'terminated_at': now})
@@ -478,6 +489,8 @@ class API(base.Base):
                    'volume_size': volume['size'],
                    'display_name': name,
                    'display_description': description,
+                   'volume_type_id': volume['volume_type_id'],
+                   'encryption_key_id': volume['encryption_key_id'],
                    'metadata': metadata}
 
         try:
index e509fe4054129b5e706bac3af6c8674e837c0c23..4a32bad703e3a88b72bd322de491d85bd251ccb9 100644 (file)
@@ -503,6 +503,9 @@ class ISCSIDriver(VolumeDriver):
             properties['physical_block_size'] = physical_block_size
             properties['logical_block_size'] = logical_block_size
 
+        encryption_key_id = volume.get('encryption_key_id', None)
+        properties['encrypted'] = encryption_key_id is not None
+
         return properties
 
     def _run_iscsiadm(self, iscsi_properties, iscsi_command, **kwargs):
index fb11e178cb2cb4f243d36c147729bcaa869e3a81..5ac9e1e553734f628d45c491509575b69b3e2ba9 100644 (file)
@@ -208,7 +208,8 @@ class ExtractVolumeRequestTask(CinderTask):
         # saved to durable storage in the future so that the flow can be
         # reconstructed elsewhere and continued).
         self.provides.update(['availability_zone', 'size', 'snapshot_id',
-                              'source_volid', 'volume_type', 'volume_type_id'])
+                              'source_volid', 'volume_type', 'volume_type_id',
+                              'encryption_key_id'])
         # This task requires the following inputs to operate (provided
         # automatically to __call__(). This is done so that the flow can
         # be reconstructed elsewhere and continue running (in the future).
@@ -218,7 +219,8 @@ class ExtractVolumeRequestTask(CinderTask):
         # mostly automatic).
         self.requires.update(['availability_zone', 'image_id', 'metadata',
                               'size', 'snapshot', 'source_volume',
-                              'volume_type'])
+                              'volume_type', 'key_manager',
+                              'backup_source_volume'])
         self.image_service = image_service
         self.az_check_functor = az_check_functor
         if not self.az_check_functor:
@@ -432,8 +434,53 @@ class ExtractVolumeRequestTask(CinderTask):
 
         return availability_zone
 
+    def _get_encryption_key_id(self, key_manager, context, volume_type_id,
+                               snapshot, source_volume, backup_source_volume):
+        encryption_key_id = None
+        if volume_types.is_encrypted(context, volume_type_id):
+            if snapshot is not None:  # creating from snapshot
+                encryption_key_id = snapshot['encryption_key_id']
+            elif source_volume is not None:  # cloning volume
+                encryption_key_id = source_volume['encryption_key_id']
+            elif backup_source_volume is not None:  # creating from backup
+                encryption_key_id = backup_source_volume['encryption_key_id']
+
+            # NOTE(joel-coffman): References to the encryption key should *not*
+            # be copied because the key is deleted when the volume is deleted.
+            # Clone the existing key and associate a separate -- but
+            # identical -- key with each volume.
+            if encryption_key_id is not None:
+                encryption_key_id = key_manager.copy_key(context,
+                                                         encryption_key_id)
+            else:
+                encryption_key_id = key_manager.create_key(context)
+
+        return encryption_key_id
+
+    def _get_volume_type_id(self, volume_type, source_volume, snapshot,
+                            backup_source_volume):
+        volume_type_id = None
+        if not volume_type and source_volume:
+            volume_type_id = source_volume['volume_type_id']
+        elif snapshot is not None:
+            if volume_type:
+                current_volume_type_id = volume_type.get('id')
+                if (current_volume_type_id !=
+                        snapshot['volume_type_id']):
+                    msg = _("Volume type will be changed to "
+                            "be the same as the source volume.")
+                    LOG.warn(msg)
+            volume_type_id = snapshot['volume_type_id']
+        elif backup_source_volume is not None:
+            volume_type_id = backup_source_volume['volume_type_id']
+        else:
+            volume_type_id = volume_type.get('id')
+
+        return volume_type_id
+
     def __call__(self, context, size, snapshot, image_id, source_volume,
-                 availability_zone, volume_type, metadata):
+                 availability_zone, volume_type, metadata,
+                 key_manager, backup_source_volume):
 
         utils.check_exclusive_options(snapshot=snapshot,
                                       imageRef=image_id,
@@ -452,12 +499,24 @@ class ExtractVolumeRequestTask(CinderTask):
                                                             snapshot,
                                                             source_volume)
 
-        if not volume_type and not source_volume:
+        # TODO(joel-coffman): This special handling of snapshots to ensure that
+        # their volume type matches the source volume is too convoluted. We
+        # should copy encryption metadata from the encrypted volume type to the
+        # volume upon creation and propogate that information to each snapshot.
+        # This strategy avoid any dependency upon the encrypted volume type.
+        if not volume_type and not source_volume and not snapshot:
             volume_type = volume_types.get_default_volume_type()
-        if not volume_type and source_volume:
-            volume_type_id = source_volume['volume_type_id']
-        else:
-            volume_type_id = volume_type.get('id')
+
+        volume_type_id = self._get_volume_type_id(volume_type,
+                                                  source_volume, snapshot,
+                                                  backup_source_volume)
+
+        encryption_key_id = self._get_encryption_key_id(key_manager,
+                                                        context,
+                                                        volume_type_id,
+                                                        snapshot,
+                                                        source_volume,
+                                                        backup_source_volume)
 
         self._check_metadata_properties(metadata)
 
@@ -468,6 +527,7 @@ class ExtractVolumeRequestTask(CinderTask):
             'availability_zone': availability_zone,
             'volume_type': volume_type,
             'volume_type_id': volume_type_id,
+            'encryption_key_id': encryption_key_id,
         }
 
 
@@ -482,7 +542,8 @@ class EntryCreateTask(CinderTask):
         self.db = db
         self.requires.update(['availability_zone', 'description', 'metadata',
                               'name', 'reservations', 'size', 'snapshot_id',
-                              'source_volid', 'volume_type_id'])
+                              'source_volid', 'volume_type_id',
+                              'encryption_key_id'])
         self.provides.update(['volume_properties', 'volume_id'])
 
     def __call__(self, context, **kwargs):
@@ -501,6 +562,7 @@ class EntryCreateTask(CinderTask):
             'project_id': context.project_id,
             'status': 'creating',
             'attach_status': 'detached',
+            'encryption_key_id': kwargs.pop('encryption_key_id'),
             # Rename these to the internal name.
             'display_description': kwargs.pop('description'),
             'display_name': kwargs.pop('name'),
index 87c1b48a1c38e328247dd5e80ee586636c8a246b..e0161c3a72a0c81c477df60ede0c48d138805134 100644 (file)
@@ -160,3 +160,11 @@ def get_volume_type_extra_specs(volume_type_id, key=False):
             return False
     else:
         return extra_specs
+
+
+def is_encrypted(context, volume_type_id):
+    if volume_type_id is None:
+        return False
+
+    encryption = db.volume_type_encryption_get(context, volume_type_id)
+    return encryption is not None
index c62d83138a4b871fd3336e5d396bb2bdb25ed622..f311bba845a79ec8f3ce4a99ea3518dbacb29c64 100644 (file)
@@ -14,6 +14,8 @@
 
     "volume_extension:types_manage": [["rule:admin_api"]],
     "volume_extension:types_extra_specs": [["rule:admin_api"]],
+    "volume_extension:volume_type_encryption": [["rule:admin_api"]],
+    "volume_extension:volume_encryption_metadata": [["rule:admin_api"]],
     "volume_extension:extended_snapshot_attributes": [],
     "volume_extension:volume_image_metadata": [],