Merge "RBD: Support encrypted volumes"

This commit is contained in:
Zuul 2018-01-25 05:12:20 +00:00 committed by Gerrit Code Review
commit 996817b2a7
5 changed files with 227 additions and 16 deletions

View File

@ -73,6 +73,7 @@ QEMU_IMG_FORMAT_MAP_INV = {v: k for k, v in QEMU_IMG_FORMAT_MAP.items()}
QEMU_IMG_VERSION = None
QEMU_IMG_MIN_FORCE_SHARE_VERSION = [2, 10, 0]
QEMU_IMG_MIN_CONVERT_LUKS_VERSION = '2.10'
def validate_disk_format(disk_format):
@ -140,7 +141,7 @@ def qemu_img_supports_force_share():
def _get_qemu_convert_cmd(src, dest, out_format, src_format=None,
out_subformat=None, cache_mode=None,
prefix=None):
prefix=None, cipher_spec=None, passphrase_file=None):
if out_format == 'vhd':
# qemu-img still uses the legacy vpc name
@ -165,6 +166,18 @@ def _get_qemu_convert_cmd(src, dest, out_format, src_format=None,
if (src_format or '').lower() not in ('', 'ami'):
cmd += ('-f', src_format) # prevent detection of format
# NOTE(lyarwood): When converting to LUKS add the cipher spec if present
# and create a secret for the passphrase, written to a temp file
if out_format == 'luks':
check_qemu_img_version(QEMU_IMG_MIN_CONVERT_LUKS_VERSION)
if cipher_spec:
cmd += ('-o', 'cipher-alg=%s,cipher-mode=%s,ivgen-alg=%s' %
(cipher_spec['cipher_alg'], cipher_spec['cipher_mode'],
cipher_spec['ivgen_alg']))
cmd += ('--object',
'secret,id=luks_sec,format=raw,file=%s' % passphrase_file,
'-o', 'key-secret=luks_sec')
cmd += [src, dest]
return cmd
@ -193,7 +206,7 @@ def check_qemu_img_version(minimum_version):
def _convert_image(prefix, source, dest, out_format,
out_subformat=None, src_format=None,
run_as_root=True):
run_as_root=True, cipher_spec=None, passphrase_file=None):
"""Convert image to other format."""
# Check whether O_DIRECT is supported and set '-t none' if it is
@ -219,7 +232,9 @@ def _convert_image(prefix, source, dest, out_format,
src_format=src_format,
out_subformat=out_subformat,
cache_mode=cache_mode,
prefix=prefix)
prefix=prefix,
cipher_spec=cipher_spec,
passphrase_file=passphrase_file)
start_time = timeutils.utcnow()
utils.execute(*cmd, run_as_root=run_as_root)
@ -254,7 +269,8 @@ def _convert_image(prefix, source, dest, out_format,
def convert_image(source, dest, out_format, out_subformat=None,
src_format=None, run_as_root=True, throttle=None):
src_format=None, run_as_root=True, throttle=None,
cipher_spec=None, passphrase_file=None):
if not throttle:
throttle = throttling.Throttle.get_default()
with throttle.subcommand(source, dest) as throttle_cmd:
@ -263,7 +279,9 @@ def convert_image(source, dest, out_format, out_subformat=None,
out_format,
out_subformat=out_subformat,
src_format=src_format,
run_as_root=run_as_root)
run_as_root=run_as_root,
cipher_spec=cipher_spec,
passphrase_file=passphrase_file)
def resize_image(source, size, run_as_root=False):
@ -699,6 +717,21 @@ def replace_xenserver_image_with_coalesced_vhd(image_file):
os.rename(coalesced, image_file)
def decode_cipher(cipher_spec, key_size):
"""Decode a dm-crypt style cipher specification string
The assumed format being cipher[:keycount]-chainmode-ivmode[:ivopts] as
documented under linux/Documentation/device-mapper/dm-crypt.txt in the
kernel source tree.
"""
cipher_alg, cipher_mode, ivgen_alg = cipher_spec.split('-')
cipher_alg = cipher_alg + '-' + str(key_size)
return {'cipher_alg': cipher_alg,
'cipher_mode': cipher_mode,
'ivgen_alg': ivgen_alg}
class TemporaryImages(object):
"""Manage temporarily downloaded images to avoid downloading it twice.

View File

@ -1703,3 +1703,10 @@ class TestImageUtils(test.TestCase):
virtual_size,
volume_size,
image_id)
def test_decode_cipher(self):
expected = {'cipher_alg': 'aes-256',
'cipher_mode': 'xts',
'ivgen_alg': 'essiv'}
result = image_utils.decode_cipher('aes-xts-essiv', 256)
self.assertEqual(expected, result)

View File

@ -14,11 +14,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import ddt
import math
import os
import tempfile
import castellan
import ddt
import mock
from mock import call
from oslo_utils import imageutils
@ -34,6 +35,7 @@ from cinder import test
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.tests.unit.keymgr import fake as fake_keymgr
from cinder.tests.unit import utils
from cinder.tests.unit.volume import test_driver
from cinder.volume import configuration as conf
@ -65,6 +67,11 @@ class MockImageExistsException(MockException):
"""Used as mock for rbd.ImageExists."""
class KeyObject(object):
def get_encoded(arg):
return "asdf".encode('utf-8')
def common_mocks(f):
"""Decorator to set mocks common to all tests.
@ -185,6 +192,13 @@ class RBDTestCase(test.TestCase):
'id': '0c7d1f44-5a06-403f-bb82-ae7ad0d693a6',
'size': 10})
self.volume_c = fake_volume.fake_volume_obj(
self.context,
**{'name': u'volume-0000000a',
'id': '55555555-222f-4b32-b585-9991b3bf0a99',
'size': 12,
'encryption_key_id': 'set_in_test'})
self.snapshot = fake_snapshot.fake_snapshot_obj(
self.context, name='snapshot-0000000a')
@ -459,14 +473,6 @@ class RBDTestCase(test.TestCase):
client.__enter__.assert_called_once_with()
client.__exit__.assert_called_once_with(None, None, None)
@common_mocks
def test_create_encrypted_volume(self):
self.volume_a.encryption_key_id = \
'00000000-0000-0000-0000-000000000000'
self.assertRaises(exception.VolumeDriverException,
self.driver.create_volume,
self.volume_a)
@common_mocks
def test_manage_existing_get_size(self):
with mock.patch.object(self.driver.rbd.Image(), 'size') as \
@ -2034,6 +2040,50 @@ class RBDTestCase(test.TestCase):
mock_delete.assert_called_once_with(self.volume_a)
self.assertEqual((True, None), ret)
@mock.patch('tempfile.NamedTemporaryFile')
@mock.patch('cinder.volume.drivers.rbd.RBDDriver.'
'_check_encryption_provider',
return_value={'encryption_key_id': fake.ENCRYPTION_KEY_ID})
def test_create_encrypted_volume(self,
mock_check_enc_prov,
mock_temp_file):
class DictObj(object):
# convert a dict to object w/ attributes
def __init__(self, d):
self.__dict__ = d
mock_temp_file.return_value.__enter__.side_effect = [
DictObj({'name': '/imgfile'}),
DictObj({'name': '/passfile'})]
key_mgr = fake_keymgr.fake_api()
self.mock_object(castellan.key_manager, 'API', return_value=key_mgr)
key_id = key_mgr.store(self.context, KeyObject())
self.volume_c.encryption_key_id = key_id
enc_info = {'encryption_key_id': key_id,
'cipher': 'aes-xts-essiv',
'key_size': 256}
with mock.patch('cinder.volume.drivers.rbd.RBDDriver.'
'_check_encryption_provider', return_value=enc_info), \
mock.patch('cinder.volume.drivers.rbd.open') as mock_open, \
mock.patch.object(self.driver, '_execute') as mock_exec:
self.driver._create_encrypted_volume(self.volume_c,
self.context)
mock_open.assert_called_with('/passfile', 'w')
mock_exec.assert_any_call(
'qemu-img', 'create', '-f', 'luks', '-o',
'cipher-alg=aes-256,cipher-mode=xts,ivgen-alg=essiv',
'--object',
'secret,id=luks_sec,format=raw,file=/passfile',
'-o', 'key-secret=luks_sec', '/imgfile', '12288M')
mock_exec.assert_any_call(
'rbd', 'import', '--pool', 'rbd', '--order', 22,
'/imgfile', self.volume_c.name)
class ManagedRBDTestCase(test_driver.BaseDriverTestCase):
driver_name = "cinder.volume.drivers.rbd.RBDDriver"

View File

@ -14,12 +14,15 @@
"""RADOS Block Device Driver"""
from __future__ import absolute_import
import binascii
import json
import math
import os
import tempfile
from castellan import key_manager
from eventlet import tpool
from os_brick import encryptors
from os_brick.initiator import linuxrbd
from oslo_config import cfg
from oslo_log import log as logging
@ -681,12 +684,81 @@ class RBDDriver(driver.CloneableImageVD,
return {'replication_status': fields.ReplicationStatus.DISABLED}
return None
def _check_encryption_provider(self, volume, context):
"""Check that this is a LUKS encryption provider.
:returns: encryption dict
"""
encryption = self.db.volume_encryption_metadata_get(context, volume.id)
provider = encryption['provider']
if provider in encryptors.LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP:
provider = encryptors.LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP[provider]
if provider != encryptors.LUKS:
message = _("Provider %s not supported.") % provider
raise exception.VolumeDriverException(message=message)
if 'cipher' not in encryption or 'key_size' not in encryption:
msg = _('encryption spec must contain "cipher" and'
'"key_size"')
raise exception.VolumeDriverException(message=msg)
return encryption
def _create_encrypted_volume(self, volume, context):
"""Create an encrypted volume.
This works by creating an encrypted image locally,
and then uploading it to the volume.
"""
encryption = self._check_encryption_provider(volume, context)
# Fetch the key associated with the volume and decode the passphrase
keymgr = key_manager.API(CONF)
key = keymgr.get(context, encryption['encryption_key_id'])
passphrase = binascii.hexlify(key.get_encoded()).decode('utf-8')
# create a file
tmp_dir = self._image_conversion_dir()
with tempfile.NamedTemporaryFile(dir=tmp_dir) as tmp_image:
with tempfile.NamedTemporaryFile(dir=tmp_dir) as tmp_key:
with open(tmp_key.name, 'w') as f:
f.write(passphrase)
cipher_spec = image_utils.decode_cipher(encryption['cipher'],
encryption['key_size'])
create_cmd = (
'qemu-img', 'create', '-f', 'luks',
'-o', 'cipher-alg=%(cipher_alg)s,'
'cipher-mode=%(cipher_mode)s,'
'ivgen-alg=%(ivgen_alg)s' % cipher_spec,
'--object', 'secret,id=luks_sec,'
'format=raw,file=%(passfile)s' % {'passfile':
tmp_key.name},
'-o', 'key-secret=luks_sec',
tmp_image.name,
'%sM' % (volume.size * 1024))
self._execute(*create_cmd)
# Copy image into RBD
chunk_size = self.configuration.rbd_store_chunk_size * units.Mi
order = int(math.log(chunk_size, 2))
cmd = ['rbd', 'import',
'--pool', self.configuration.rbd_pool,
'--order', order,
tmp_image.name, volume.name]
cmd.extend(self._ceph_args())
self._execute(*cmd)
def create_volume(self, volume):
"""Creates a logical volume."""
if volume.encryption_key_id:
message = _("Encryption is not yet supported.")
raise exception.VolumeDriverException(message=message)
return self._create_encrypted_volume(volume, volume.obj_context)
size = int(volume.size) * units.Gi
@ -1254,7 +1326,45 @@ class RBDDriver(driver.CloneableImageVD,
return tmpdir
def copy_image_to_encrypted_volume(self, context, volume, image_service,
image_id):
self._copy_image_to_volume(context, volume, image_service, image_id,
encrypted=True)
def copy_image_to_volume(self, context, volume, image_service, image_id):
self._copy_image_to_volume(context, volume, image_service, image_id)
def _encrypt_image(self, context, volume, tmp_dir, src_image_path):
encryption = self._check_encryption_provider(volume, context)
# Fetch the key associated with the volume and decode the passphrase
keymgr = key_manager.API(CONF)
key = keymgr.get(context, encryption['encryption_key_id'])
passphrase = binascii.hexlify(key.get_encoded()).decode('utf-8')
# Decode the dm-crypt style cipher spec into something qemu-img can use
cipher_spec = image_utils.decode_cipher(encryption['cipher'],
encryption['key_size'])
tmp_dir = self._image_conversion_dir()
with tempfile.NamedTemporaryFile(prefix='luks_',
dir=tmp_dir) as pass_file:
with open(pass_file.name, 'w') as f:
f.write(passphrase)
# Convert the raw image to luks
dest_image_path = src_image_path + '.luks'
image_utils.convert_image(src_image_path, dest_image_path,
'luks', src_format='raw',
cipher_spec=cipher_spec,
passphrase_file=pass_file.name)
# Replace the original image with the now encrypted image
os.rename(dest_image_path, src_image_path)
def _copy_image_to_volume(self, context, volume, image_service, image_id,
encrypted=False):
tmp_dir = self._image_conversion_dir()
@ -1264,6 +1374,9 @@ class RBDDriver(driver.CloneableImageVD,
self.configuration.volume_dd_blocksize,
size=volume.size)
if encrypted:
self._encrypt_image(context, volume, tmp_dir, tmp.name)
self.delete_volume(volume)
chunk_size = self.configuration.rbd_store_chunk_size * units.Mi

View File

@ -0,0 +1,8 @@
---
features:
- |
LUKS Encrypted RBD volumes can now be created by cinder-volume. This
capability was previously blocked by the rbd volume driver due to the lack
of any encryptors capable of attaching to an encrypted RBD volume. These
volumes can also be seeded with RAW image data from Glance through the use
of QEMU 2.10 and the qemu-img convert command.