Glance: attach volume encryption key id to image

This is required to be able to handle encrypted volumes
that have been uploaded to Glance.

Clone the volume's encryption key, and store the new encryption
key id in the image metadata in a property called
"cinder_encryption_key_id".  This allows the key to be retrieved
when creating a new volume from this image.

Related-Bug: #1485449
Related bp: improve-encrypted-volume

Change-Id: Ia1771817e6a06cc51c5357536915a2c5f9f6248e
This commit is contained in:
Eric Harney 2017-04-04 15:27:43 -04:00
parent b1ebfa4f5d
commit d96e078f37
2 changed files with 49 additions and 8 deletions

View File

@ -27,6 +27,7 @@ from cinder.api.openstack import wsgi
from cinder import exception
from cinder.i18n import _
from cinder.image import image_utils
from cinder import keymgr
from cinder import utils
from cinder import volume
@ -42,8 +43,17 @@ def authorize(context, action_name):
class VolumeActionsController(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(VolumeActionsController, self).__init__(*args, **kwargs)
self._key_mgr = None
self.volume_api = volume.API()
@property
def _key_manager(self):
# Allows for lazy initialization of the key manager
if self._key_mgr is None:
self._key_mgr = keymgr.API(CONF)
return self._key_mgr
@wsgi.action('os-attach')
def _attach(self, req, id, body):
"""Add attachment metadata."""
@ -246,6 +256,19 @@ class VolumeActionsController(wsgi.Controller):
"disk_format": disk_format,
"name": params["image_name"]}
if volume.encryption_key_id:
# Clone volume encryption key: the current key cannot
# be reused because it will be deleted when the volume is
# deleted.
# TODO(eharney): Currently, there is no mechanism to remove
# these keys, because Glance will not delete the key from
# Barbican when the image is deleted.
encryption_key_id = self._key_manager.store(
context,
self._key_manager.get(context, volume.encryption_key_id))
image_metadata['cinder_encryption_key_id'] = encryption_key_id
if req_version >= api_version_request.APIVersionRequest('3.1'):
image_metadata['visibility'] = params.get('visibility', 'private')

View File

@ -749,6 +749,22 @@ def fake_volume_get(self, context, volume_id):
return volume
def fake_volume_get_obj(self, context, volume_id, **kwargs):
volume = fake_volume.fake_volume_obj(context,
id=volume_id,
display_description='displaydesc',
**kwargs)
if volume_id == fake.VOLUME3_ID:
volume.status = 'in-use'
else:
volume.status = 'available'
volume.volume_type = fake_volume.fake_volume_type_obj(
context,
name=v2_fakes.DEFAULT_VOL_TYPE)
return volume
def fake_upload_volume_to_image_service(self, context, volume, metadata,
force):
ret = {"id": volume['id'],
@ -770,6 +786,7 @@ class VolumeImageActionsTest(test.TestCase):
self.controller = volume_actions.VolumeActionsController()
self.context = context.RequestContext(fake.USER_ID, fake.PROJECT_ID,
is_admin=False)
self.maxDiff = 2000
def _get_os_volume_upload_image(self):
vol = {
@ -823,16 +840,16 @@ class VolumeImageActionsTest(test.TestCase):
def fake_rpc_copy_volume_to_image(self, *args):
pass
@mock.patch.object(volume_api.API, 'get', fake_volume_get)
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
@mock.patch.object(volume_api.API, "copy_volume_to_image",
fake_upload_volume_to_image_service)
def test_copy_volume_to_image(self):
id = fake.VOLUME_ID
vol = {"container_format": 'bare',
img = {"container_format": 'bare',
"disk_format": 'raw',
"image_name": 'image_name',
"force": True}
body = {"os-volume_upload_image": vol}
body = {"os-volume_upload_image": img}
req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' %
(fake.PROJECT_ID, id))
res_dict = self.controller._volume_upload_image(req, id, body)
@ -842,7 +859,8 @@ class VolumeImageActionsTest(test.TestCase):
'status': 'uploading',
'display_description': 'displaydesc',
'size': 1,
'volume_type': fake_volume.fake_db_volume_type(
'volume_type': fake_volume.fake_volume_type_obj(
context,
name='vol_type_name'),
'image_id': fake.IMAGE_ID,
'container_format': 'bare',
@ -870,7 +888,7 @@ class VolumeImageActionsTest(test.TestCase):
id,
body)
@mock.patch.object(volume_api.API, 'get', fake_volume_get)
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
@mock.patch.object(volume_api.API, 'copy_volume_to_image',
side_effect=exception.InvalidVolume(reason='blah'))
def test_copy_volume_to_image_invalidvolume(self, mock_copy):
@ -904,7 +922,7 @@ class VolumeImageActionsTest(test.TestCase):
id,
body)
@mock.patch.object(volume_api.API, 'get', fake_volume_get)
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
@mock.patch.object(volume_api.API, 'copy_volume_to_image',
side_effect=ValueError)
def test_copy_volume_to_image_valueerror(self, mock_copy):
@ -922,7 +940,7 @@ class VolumeImageActionsTest(test.TestCase):
id,
body)
@mock.patch.object(volume_api.API, 'get', fake_volume_get)
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
@mock.patch.object(volume_api.API, 'copy_volume_to_image',
side_effect=messaging.RemoteError)
def test_copy_volume_to_image_remoteerror(self, mock_copy):
@ -1045,7 +1063,7 @@ class VolumeImageActionsTest(test.TestCase):
self.assertEqual('uploading', vol_db.status)
self.assertEqual('available', vol_db.previous_status)
@mock.patch.object(volume_api.API, 'get', fake_volume_get)
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
def test_copy_volume_to_image_public_not_authorized(self):
"""Test unauthorized create public image from volume."""
id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'