NFS encrypted volume support
Volume encryption helps provide basic data protection in case the volume back-end is either compromised or outright stolen. The contents of an encrypted volume can only be read with the use of a specific key; Volume encryption: Check volume encryption key in '_do_create_volume' method (remotefs) and use '_create_encrypted_volume_file' when encryption is required. Snapshot encryption: To create an encrypted volume from a snapshot, we need to pass the Barbican key of the snapshot.volume to 'convert_image' method. Because of this I've added 'src_passphrase_file' parameter. This patch doesn't handle encrypted volume -> unencrypted Current error prompted: 'Invalid input received: Invalid volume_type provided: aeac5517-6bc8-4b59-9eb2-76e84369bd0 (requested type is not compatible; recommend omitting the type argument). (HTTP 400)' Implements: blueprint nfs-volume-encryption Co-Authored-By: Sofia Enriquez <lsofia.enriquez@gmail.com> Change-Id: I896f70d204ad103e968ab242ba9045ca984827c4
This commit is contained in:
parent
307fccea4c
commit
44c7da9a44
@ -164,13 +164,63 @@ def get_qemu_img_version():
|
||||
|
||||
|
||||
def qemu_img_supports_force_share():
|
||||
return get_qemu_img_version() > [2, 10, 0]
|
||||
return get_qemu_img_version() >= [2, 10, 0]
|
||||
|
||||
|
||||
def _get_qemu_convert_luks_cmd(src, dest, out_format, src_format=None,
|
||||
out_subformat=None, cache_mode=None,
|
||||
prefix=None, cipher_spec=None,
|
||||
passphrase_file=None,
|
||||
src_passphrase_file=None):
|
||||
|
||||
cmd = ['qemu-img', 'convert']
|
||||
|
||||
if prefix:
|
||||
cmd = list(prefix) + cmd
|
||||
|
||||
if cache_mode:
|
||||
cmd += ('-t', cache_mode)
|
||||
|
||||
obj1 = ['--object',
|
||||
'secret,id=sec1,format=raw,file=%s' % src_passphrase_file]
|
||||
obj2 = ['--object',
|
||||
'secret,id=sec2,format=raw,file=%s' % passphrase_file]
|
||||
|
||||
src_opts = 'encrypt.format=luks,encrypt.key-secret=sec1,' \
|
||||
'file.filename=%s' % src
|
||||
|
||||
image_opts = ['--image-opts', src_opts]
|
||||
output_opts = ['-O', 'luks', '-o', 'key-secret=sec2', dest]
|
||||
|
||||
command = cmd + obj1 + obj2 + image_opts + output_opts
|
||||
return command
|
||||
|
||||
|
||||
def _get_qemu_convert_cmd(src, dest, out_format, src_format=None,
|
||||
out_subformat=None, cache_mode=None,
|
||||
prefix=None, cipher_spec=None,
|
||||
passphrase_file=None, compress=False):
|
||||
passphrase_file=None, compress=False,
|
||||
src_passphrase_file=None):
|
||||
|
||||
if src_passphrase_file is not None:
|
||||
if passphrase_file is None:
|
||||
message = _("Can't create unencrypted volume %(format)s "
|
||||
"from an encrypted source volume."
|
||||
) % {'format': out_format}
|
||||
LOG.error(message)
|
||||
# TODO(enriquetaso): handle encrypted->unencrypted
|
||||
raise exception.NotSupportedOperation(operation=message)
|
||||
return _get_qemu_convert_luks_cmd(
|
||||
src,
|
||||
dest,
|
||||
out_format,
|
||||
src_format=src_format,
|
||||
out_subformat=out_subformat,
|
||||
cache_mode=cache_mode,
|
||||
prefix=None,
|
||||
cipher_spec=cipher_spec,
|
||||
passphrase_file=passphrase_file,
|
||||
src_passphrase_file=src_passphrase_file)
|
||||
|
||||
if out_format == 'vhd':
|
||||
# qemu-img still uses the legacy vpc name
|
||||
@ -240,7 +290,8 @@ 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, cipher_spec=None,
|
||||
passphrase_file=None, compress=False):
|
||||
passphrase_file=None, compress=False,
|
||||
src_passphrase_file=None):
|
||||
"""Convert image to other format.
|
||||
|
||||
:param prefix: command prefix, i.e. cgexec for throttling
|
||||
@ -253,6 +304,8 @@ def _convert_image(prefix, source, dest, out_format,
|
||||
:param cipher_spec: encryption details
|
||||
:param passphrase_file: filename containing luks passphrase
|
||||
:param compress: compress w/ qemu-img when possible (best effort)
|
||||
:param src_passphrase_file: filename containing source volume's
|
||||
luks passphrase
|
||||
"""
|
||||
|
||||
# Check whether O_DIRECT is supported and set '-t none' if it is
|
||||
@ -281,7 +334,8 @@ def _convert_image(prefix, source, dest, out_format,
|
||||
prefix=prefix,
|
||||
cipher_spec=cipher_spec,
|
||||
passphrase_file=passphrase_file,
|
||||
compress=compress)
|
||||
compress=compress,
|
||||
src_passphrase_file=src_passphrase_file)
|
||||
|
||||
start_time = timeutils.utcnow()
|
||||
|
||||
@ -333,7 +387,7 @@ 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,
|
||||
cipher_spec=None, passphrase_file=None,
|
||||
compress=False):
|
||||
compress=False, src_passphrase_file=None):
|
||||
if not throttle:
|
||||
throttle = throttling.Throttle.get_default()
|
||||
with throttle.subcommand(source, dest) as throttle_cmd:
|
||||
@ -345,7 +399,8 @@ def convert_image(source, dest, out_format, out_subformat=None,
|
||||
run_as_root=run_as_root,
|
||||
cipher_spec=cipher_spec,
|
||||
passphrase_file=passphrase_file,
|
||||
compress=compress)
|
||||
compress=compress,
|
||||
src_passphrase_file=src_passphrase_file)
|
||||
|
||||
|
||||
def resize_image(source, size, run_as_root=False):
|
||||
|
@ -18,6 +18,7 @@ import errno
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
import castellan
|
||||
import ddt
|
||||
from oslo_utils import imageutils
|
||||
from oslo_utils import units
|
||||
@ -28,6 +29,7 @@ from cinder.image import image_utils
|
||||
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 test
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers import nfs
|
||||
@ -35,6 +37,11 @@ from cinder.volume.drivers import remotefs
|
||||
from cinder.volume import volume_utils
|
||||
|
||||
|
||||
class KeyObject(object):
|
||||
def get_encoded(arg):
|
||||
return "asdf".encode('utf-8')
|
||||
|
||||
|
||||
class RemoteFsDriverTestCase(test.TestCase):
|
||||
TEST_FILE_NAME = 'test.txt'
|
||||
TEST_EXPORT = 'nas-host1:/export'
|
||||
@ -374,6 +381,57 @@ Format specific information:
|
||||
corrupt: false
|
||||
"""
|
||||
|
||||
QEMU_IMG_INFO_OUT5 = """image: volume-%(volid)s.%(snapid)s
|
||||
file format: qcow2
|
||||
virtual size: %(size_gb)sG (%(size_b)s bytes)
|
||||
disk size: 196K
|
||||
encrypted: yes
|
||||
cluster_size: 65536
|
||||
backing file: volume-%(volid)s
|
||||
backing file format: raw
|
||||
Format specific information:
|
||||
compat: 1.1
|
||||
lazy refcounts: false
|
||||
refcount bits: 16
|
||||
encrypt:
|
||||
ivgen alg: plain64
|
||||
hash alg: sha256
|
||||
cipher alg: aes-256
|
||||
uuid: 386f8626-33f0-4683-a517-78ddfe385e33
|
||||
format: luks
|
||||
cipher mode: xts
|
||||
slots:
|
||||
[0]:
|
||||
active: true
|
||||
iters: 1892498
|
||||
key offset: 4096
|
||||
stripes: 4000
|
||||
[1]:
|
||||
active: false
|
||||
key offset: 262144
|
||||
[2]:
|
||||
active: false
|
||||
key offset: 520192
|
||||
[3]:
|
||||
active: false
|
||||
key offset: 778240
|
||||
[4]:
|
||||
active: false
|
||||
key offset: 1036288
|
||||
[5]:
|
||||
active: false
|
||||
key offset: 1294336
|
||||
[6]:
|
||||
active: false
|
||||
key offset: 1552384
|
||||
[7]:
|
||||
active: false
|
||||
key offset: 1810432
|
||||
payload offset: 2068480
|
||||
master key iters: 459347
|
||||
corrupt: false
|
||||
"""
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class NfsDriverTestCase(test.TestCase):
|
||||
@ -691,6 +749,17 @@ class NfsDriverTestCase(test.TestCase):
|
||||
provider_location=loc,
|
||||
size=size)
|
||||
|
||||
def _simple_encrypted_volume(self, size=10):
|
||||
loc = self.TEST_NFS_EXPORT1
|
||||
info_dic = {'name': u'volume-0000000a',
|
||||
'id': '55555555-222f-4b32-b585-9991b3bf0a99',
|
||||
'size': size,
|
||||
'encryption_key_id': fake.ENCRYPTION_KEY_ID}
|
||||
|
||||
return fake_volume.fake_volume_obj(self.context,
|
||||
provider_location=loc,
|
||||
**info_dic)
|
||||
|
||||
def test_get_provisioned_capacity(self):
|
||||
self._set_driver()
|
||||
drv = self._driver
|
||||
@ -1124,14 +1193,15 @@ class NfsDriverTestCase(test.TestCase):
|
||||
"""Case where the mount works the first time."""
|
||||
|
||||
self._set_driver()
|
||||
self.mock_object(self._driver._remotefsclient, 'mount')
|
||||
self.mock_object(self._driver._remotefsclient, 'mount', autospec=True)
|
||||
drv = self._driver
|
||||
drv.configuration.nfs_mount_attempts = 3
|
||||
drv.shares = {self.TEST_NFS_EXPORT1: ''}
|
||||
|
||||
drv._ensure_share_mounted(self.TEST_NFS_EXPORT1)
|
||||
|
||||
drv._remotefsclient.mount.called_once()
|
||||
drv._remotefsclient.mount.assert_called_once_with(
|
||||
self.TEST_NFS_EXPORT1, [])
|
||||
|
||||
@mock.patch('time.sleep')
|
||||
def test_ensure_share_mounted_exception(self, _mock_sleep):
|
||||
@ -1169,16 +1239,48 @@ class NfsDriverTestCase(test.TestCase):
|
||||
self.assertEqual(min_num_attempts,
|
||||
drv._remotefsclient.mount.call_count)
|
||||
|
||||
@ddt.data([NFS_CONFIG1, QEMU_IMG_INFO_OUT3],
|
||||
[NFS_CONFIG2, QEMU_IMG_INFO_OUT4],
|
||||
[NFS_CONFIG3, QEMU_IMG_INFO_OUT3],
|
||||
[NFS_CONFIG4, QEMU_IMG_INFO_OUT4])
|
||||
@mock.patch('tempfile.NamedTemporaryFile')
|
||||
@ddt.data([NFS_CONFIG1, QEMU_IMG_INFO_OUT3, False],
|
||||
[NFS_CONFIG2, QEMU_IMG_INFO_OUT4, False],
|
||||
[NFS_CONFIG3, QEMU_IMG_INFO_OUT3, False],
|
||||
[NFS_CONFIG4, QEMU_IMG_INFO_OUT4, False],
|
||||
[NFS_CONFIG4, QEMU_IMG_INFO_OUT5, True])
|
||||
@ddt.unpack
|
||||
def test_copy_volume_from_snapshot(self, nfs_conf, qemu_img_info):
|
||||
def test_copy_volume_from_snapshot(self, nfs_conf, qemu_img_info,
|
||||
encryption, mock_temp_file):
|
||||
|
||||
class DictObj(object):
|
||||
# convert a dict to object w/ attributes
|
||||
def __init__(self, d):
|
||||
self.__dict__ = d
|
||||
|
||||
self._set_driver(extra_confs=nfs_conf)
|
||||
drv = self._driver
|
||||
dest_volume = self._simple_volume()
|
||||
src_volume = self._simple_volume()
|
||||
|
||||
src_encryption_key_id = None
|
||||
dest_encryption_key_id = None
|
||||
|
||||
if encryption:
|
||||
mock_temp_file.return_value.__enter__.side_effect = [
|
||||
DictObj({'name': '/tmp/imgfile'}),
|
||||
DictObj({'name': '/tmp/passfile'})]
|
||||
|
||||
dest_volume = self._simple_encrypted_volume()
|
||||
src_volume = self._simple_encrypted_volume()
|
||||
|
||||
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())
|
||||
|
||||
src_volume.encryption_key_id = key_id
|
||||
dest_volume.encryption_key_id = key_id
|
||||
|
||||
src_encryption_key_id = src_volume.encryption_key_id
|
||||
dest_encryption_key_id = dest_volume.encryption_key_id
|
||||
else:
|
||||
dest_volume = self._simple_volume()
|
||||
src_volume = self._simple_volume()
|
||||
|
||||
fake_snap = fake_snapshot.fake_snapshot_obj(self.context)
|
||||
fake_snap.volume = src_volume
|
||||
@ -1209,16 +1311,25 @@ class NfsDriverTestCase(test.TestCase):
|
||||
|
||||
mock_permission = self.mock_object(drv, '_set_rw_permissions_for_all')
|
||||
|
||||
drv._copy_volume_from_snapshot(fake_snap, dest_volume, size)
|
||||
drv._copy_volume_from_snapshot(fake_snap, dest_volume, size,
|
||||
src_encryption_key_id,
|
||||
dest_encryption_key_id)
|
||||
|
||||
mock_read_info_file.assert_called_once_with(info_path)
|
||||
mock_img_info.assert_called_once_with(snap_path,
|
||||
force_share=True,
|
||||
run_as_root=True)
|
||||
used_qcow = nfs_conf['nfs_qcow2_volumes']
|
||||
mock_convert_image.assert_called_once_with(
|
||||
src_vol_path, dest_vol_path, 'qcow2' if used_qcow else 'raw',
|
||||
run_as_root=True)
|
||||
if encryption:
|
||||
mock_convert_image.assert_called_once_with(
|
||||
src_vol_path, dest_vol_path, 'luks',
|
||||
passphrase_file='/tmp/passfile',
|
||||
run_as_root=True,
|
||||
src_passphrase_file='/tmp/imgfile')
|
||||
else:
|
||||
mock_convert_image.assert_called_once_with(
|
||||
src_vol_path, dest_vol_path, 'qcow2' if used_qcow else 'raw',
|
||||
run_as_root=True)
|
||||
mock_permission.assert_called_once_with(dest_vol_path)
|
||||
|
||||
@ddt.data([NFS_CONFIG1, QEMU_IMG_INFO_OUT3],
|
||||
|
@ -19,20 +19,28 @@ import re
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
import castellan
|
||||
import ddt
|
||||
|
||||
from cinder import context
|
||||
from cinder import exception
|
||||
from cinder.image import image_utils
|
||||
from cinder.objects import fields
|
||||
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 test
|
||||
from cinder import utils
|
||||
from cinder.volume.drivers import remotefs
|
||||
from cinder.volume import volume_utils
|
||||
|
||||
|
||||
class KeyObject(object):
|
||||
def get_encoded(arg):
|
||||
return "asdf".encode('utf-8')
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
|
||||
@ -56,6 +64,20 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
self._fake_snapshot.id)
|
||||
self._fake_snapshot.volume = self._fake_volume
|
||||
|
||||
# Encrypted volume and snapshot
|
||||
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': fake.ENCRYPTION_KEY_ID})
|
||||
self._fake_snap_c = fake_snapshot.fake_snapshot_obj(self.context)
|
||||
self._fake_snap_c.volume = self.volume_c
|
||||
self.volume_c_path = os.path.join(self._FAKE_MNT_POINT,
|
||||
self._fake_snap_c.name)
|
||||
self._fake_snap_c_path = (self.volume_c_path + '.' +
|
||||
self._fake_snap_c.id)
|
||||
|
||||
@ddt.data({'current_state': 'in-use',
|
||||
'acceptable_states': ['available', 'in-use']},
|
||||
{'current_state': 'in-use',
|
||||
@ -75,19 +97,43 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
def _test_delete_snapshot(self, volume_in_use=False,
|
||||
stale_snapshot=False,
|
||||
is_active_image=True,
|
||||
is_tmp_snap=False):
|
||||
is_tmp_snap=False,
|
||||
encryption=False):
|
||||
# If the snapshot is not the active image, it is guaranteed that
|
||||
# another snapshot exists having it as backing file.
|
||||
|
||||
fake_snapshot_name = os.path.basename(self._fake_snapshot_path)
|
||||
fake_info = {'active': fake_snapshot_name,
|
||||
self._fake_snapshot.id: fake_snapshot_name}
|
||||
fake_upper_snap_id = 'fake_upper_snap_id'
|
||||
if encryption:
|
||||
fake_snapshot_name = os.path.basename(self._fake_snap_c_path)
|
||||
fake_info = {'active': fake_snapshot_name,
|
||||
self._fake_snap_c.id: fake_snapshot_name}
|
||||
expected_info = fake_info
|
||||
|
||||
fake_upper_snap_path = (
|
||||
self.volume_c_path + '-snapshot' + fake_upper_snap_id)
|
||||
|
||||
snapshot = self._fake_snap_c
|
||||
snapshot_path = self._fake_snap_c_path
|
||||
volume_name = self.volume_c.name
|
||||
else:
|
||||
fake_snapshot_name = os.path.basename(self._fake_snapshot_path)
|
||||
fake_info = {'active': fake_snapshot_name,
|
||||
self._fake_snapshot.id: fake_snapshot_name}
|
||||
expected_info = fake_info
|
||||
|
||||
fake_upper_snap_path = (
|
||||
self._fake_volume_path + '-snapshot' + fake_upper_snap_id)
|
||||
|
||||
snapshot = self._fake_snapshot
|
||||
snapshot_path = self._fake_snapshot_path
|
||||
volume_name = self._fake_volume.name
|
||||
|
||||
fake_snap_img_info = mock.Mock()
|
||||
fake_base_img_info = mock.Mock()
|
||||
if stale_snapshot:
|
||||
fake_snap_img_info.backing_file = None
|
||||
else:
|
||||
fake_snap_img_info.backing_file = self._fake_volume.name
|
||||
fake_snap_img_info.backing_file = volume_name
|
||||
fake_snap_img_info.file_format = 'qcow2'
|
||||
fake_base_img_info.backing_file = None
|
||||
fake_base_img_info.file_format = 'raw'
|
||||
@ -107,61 +153,53 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
self._driver._delete_stale_snapshot = mock.Mock()
|
||||
self._driver._delete_snapshot_online = mock.Mock()
|
||||
|
||||
expected_info = {
|
||||
'active': fake_snapshot_name,
|
||||
self._fake_snapshot.id: fake_snapshot_name
|
||||
}
|
||||
|
||||
exp_acceptable_states = ['available', 'in-use', 'backing-up',
|
||||
'deleting', 'downloading']
|
||||
|
||||
if volume_in_use:
|
||||
self._fake_snapshot.volume.status = 'backing-up'
|
||||
self._fake_snapshot.volume.attach_status = 'attached'
|
||||
snapshot.volume.status = 'backing-up'
|
||||
snapshot.volume.attach_status = 'attached'
|
||||
|
||||
self._driver._read_info_file.return_value = fake_info
|
||||
|
||||
self._driver._delete_snapshot(self._fake_snapshot)
|
||||
self._driver._delete_snapshot(snapshot)
|
||||
self._driver._validate_state.assert_called_once_with(
|
||||
self._fake_snapshot.volume.status,
|
||||
snapshot.volume.status,
|
||||
exp_acceptable_states)
|
||||
if stale_snapshot:
|
||||
self._driver._delete_stale_snapshot.assert_called_once_with(
|
||||
self._fake_snapshot)
|
||||
snapshot)
|
||||
else:
|
||||
expected_online_delete_info = {
|
||||
'active_file': fake_snapshot_name,
|
||||
'snapshot_file': fake_snapshot_name,
|
||||
'base_file': self._fake_volume.name,
|
||||
'base_file': volume_name,
|
||||
'base_id': None,
|
||||
'new_base_file': None
|
||||
}
|
||||
self._driver._delete_snapshot_online.assert_called_once_with(
|
||||
self.context, self._fake_snapshot,
|
||||
self.context, snapshot,
|
||||
expected_online_delete_info)
|
||||
|
||||
elif is_active_image:
|
||||
self._driver._read_info_file.return_value = fake_info
|
||||
|
||||
self._driver._delete_snapshot(self._fake_snapshot)
|
||||
self._driver._delete_snapshot(snapshot)
|
||||
|
||||
self._driver._img_commit.assert_called_once_with(
|
||||
self._fake_snapshot_path)
|
||||
self.assertNotIn(self._fake_snapshot.id, fake_info)
|
||||
snapshot_path)
|
||||
self.assertNotIn(snapshot.id, fake_info)
|
||||
self._driver._write_info_file.assert_called_once_with(
|
||||
mock.sentinel.fake_info_path, fake_info)
|
||||
else:
|
||||
fake_upper_snap_id = 'fake_upper_snap_id'
|
||||
fake_upper_snap_path = (
|
||||
self._fake_volume_path + '-snapshot' + fake_upper_snap_id)
|
||||
fake_upper_snap_name = os.path.basename(fake_upper_snap_path)
|
||||
|
||||
fake_backing_chain = [
|
||||
{'filename': fake_upper_snap_name,
|
||||
'backing-filename': fake_snapshot_name},
|
||||
{'filename': fake_snapshot_name,
|
||||
'backing-filename': self._fake_volume.name},
|
||||
{'filename': self._fake_volume.name,
|
||||
'backing-filename': volume_name},
|
||||
{'filename': volume_name,
|
||||
'backing-filename': None}]
|
||||
|
||||
fake_info[fake_upper_snap_id] = fake_upper_snap_name
|
||||
@ -169,42 +207,62 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
fake_info['active'] = fake_upper_snap_name
|
||||
|
||||
expected_info = copy.deepcopy(fake_info)
|
||||
del expected_info[self._fake_snapshot.id]
|
||||
del expected_info[snapshot.id]
|
||||
|
||||
self._driver._read_info_file.return_value = fake_info
|
||||
self._driver._get_backing_chain_for_path = mock.Mock(
|
||||
return_value=fake_backing_chain)
|
||||
|
||||
self._driver._delete_snapshot(self._fake_snapshot)
|
||||
self._driver._delete_snapshot(snapshot)
|
||||
|
||||
self._driver._img_commit.assert_called_once_with(
|
||||
self._fake_snapshot_path)
|
||||
snapshot_path)
|
||||
self._driver._rebase_img.assert_called_once_with(
|
||||
fake_upper_snap_path, self._fake_volume.name,
|
||||
fake_upper_snap_path, volume_name,
|
||||
fake_base_img_info.file_format)
|
||||
self._driver._write_info_file.assert_called_once_with(
|
||||
mock.sentinel.fake_info_path, expected_info)
|
||||
|
||||
def test_delete_snapshot_when_active_file(self):
|
||||
self._test_delete_snapshot()
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_snapshot_when_active_file(self, encryption):
|
||||
self._test_delete_snapshot(encryption=encryption)
|
||||
|
||||
def test_delete_snapshot_in_use(self):
|
||||
self._test_delete_snapshot(volume_in_use=True)
|
||||
|
||||
def test_delete_snapshot_in_use_stale_snapshot(self):
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_snapshot_in_use(self, encryption):
|
||||
self._test_delete_snapshot(volume_in_use=True,
|
||||
stale_snapshot=True)
|
||||
encryption=encryption)
|
||||
|
||||
def test_delete_snapshot_with_one_upper_file(self):
|
||||
self._test_delete_snapshot(is_active_image=False)
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_snapshot_in_use_stale_snapshot(self,
|
||||
encryption):
|
||||
self._test_delete_snapshot(volume_in_use=True,
|
||||
stale_snapshot=True,
|
||||
encryption=encryption)
|
||||
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_snapshot_with_one_upper_file(self,
|
||||
encryption):
|
||||
self._test_delete_snapshot(is_active_image=False,
|
||||
encryption=encryption)
|
||||
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_stale_snapshot(self, encryption):
|
||||
if encryption:
|
||||
fake_snapshot_name = os.path.basename(self._fake_snap_c_path)
|
||||
volume_name = self.volume_c.name
|
||||
snapshot = self._fake_snap_c
|
||||
snapshot_path = self._fake_snap_c_path
|
||||
else:
|
||||
fake_snapshot_name = os.path.basename(self._fake_snapshot_path)
|
||||
volume_name = self._fake_volume.name
|
||||
snapshot = self._fake_snapshot
|
||||
snapshot_path = self._fake_snapshot_path
|
||||
|
||||
def test_delete_stale_snapshot(self):
|
||||
fake_snapshot_name = os.path.basename(self._fake_snapshot_path)
|
||||
fake_snap_info = {
|
||||
'active': self._fake_volume.name,
|
||||
self._fake_snapshot.id: fake_snapshot_name
|
||||
'active': volume_name,
|
||||
snapshot.id: fake_snapshot_name
|
||||
}
|
||||
expected_info = {'active': self._fake_volume.name}
|
||||
expected_info = {'active': volume_name}
|
||||
|
||||
self._driver._local_path_volume_info = mock.Mock(
|
||||
return_value=mock.sentinel.fake_info_path)
|
||||
@ -214,9 +272,9 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
return_value=self._FAKE_MNT_POINT)
|
||||
self._driver._write_info_file = mock.Mock()
|
||||
|
||||
self._driver._delete_stale_snapshot(self._fake_snapshot)
|
||||
self._driver._delete_stale_snapshot(snapshot)
|
||||
|
||||
self._driver._delete.assert_called_once_with(self._fake_snapshot_path)
|
||||
self._driver._delete.assert_called_once_with(snapshot_path)
|
||||
self._driver._write_info_file.assert_called_once_with(
|
||||
mock.sentinel.fake_info_path, expected_info)
|
||||
|
||||
@ -256,9 +314,20 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
mock.call(*command3, run_as_root=True)]
|
||||
self._driver._execute.assert_has_calls(calls)
|
||||
|
||||
def _test_create_snapshot(self, volume_in_use=False, tmp_snap=False):
|
||||
def _test_create_snapshot(self, volume_in_use=False, tmp_snap=False,
|
||||
encryption=False):
|
||||
fake_snapshot_info = {}
|
||||
fake_snapshot_file_name = os.path.basename(self._fake_snapshot_path)
|
||||
if encryption:
|
||||
fake_snapshot_file_name = os.path.basename(self._fake_snap_c_path)
|
||||
volume_name = self.volume_c.name
|
||||
snapshot = self._fake_snap_c
|
||||
snapshot_path = self._fake_snap_c_path
|
||||
else:
|
||||
fake_snapshot_file_name = os.path.basename(
|
||||
self._fake_snapshot_path)
|
||||
volume_name = self._fake_volume.name
|
||||
snapshot = self._fake_snapshot
|
||||
snapshot_path = self._fake_snapshot_path
|
||||
|
||||
self._driver._local_path_volume_info = mock.Mock(
|
||||
return_value=mock.sentinel.fake_info_path)
|
||||
@ -268,14 +337,14 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
self._driver._create_snapshot_online = mock.Mock()
|
||||
self._driver._write_info_file = mock.Mock()
|
||||
self._driver.get_active_image_from_info = mock.Mock(
|
||||
return_value=self._fake_volume.name)
|
||||
return_value=volume_name)
|
||||
self._driver._get_new_snap_path = mock.Mock(
|
||||
return_value=self._fake_snapshot_path)
|
||||
return_value=snapshot_path)
|
||||
self._driver._validate_state = mock.Mock()
|
||||
|
||||
expected_snapshot_info = {
|
||||
'active': fake_snapshot_file_name,
|
||||
self._fake_snapshot.id: fake_snapshot_file_name
|
||||
snapshot.id: fake_snapshot_file_name
|
||||
}
|
||||
exp_acceptable_states = ['available', 'in-use', 'backing-up']
|
||||
if tmp_snap:
|
||||
@ -285,31 +354,34 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
self._fake_snapshot.display_name = display_name
|
||||
|
||||
if volume_in_use:
|
||||
self._fake_snapshot.volume.status = 'backing-up'
|
||||
self._fake_snapshot.volume.attach_status = 'attached'
|
||||
snapshot.volume.status = 'backing-up'
|
||||
snapshot.volume.attach_status = 'attached'
|
||||
expected_method_called = '_create_snapshot_online'
|
||||
else:
|
||||
self._fake_snapshot.volume.status = 'available'
|
||||
snapshot.volume.status = 'available'
|
||||
expected_method_called = '_do_create_snapshot'
|
||||
|
||||
self._driver._create_snapshot(self._fake_snapshot)
|
||||
self._driver._create_snapshot(snapshot)
|
||||
|
||||
self._driver._validate_state.assert_called_once_with(
|
||||
self._fake_snapshot.volume.status,
|
||||
snapshot.volume.status,
|
||||
exp_acceptable_states)
|
||||
fake_method = getattr(self._driver, expected_method_called)
|
||||
fake_method.assert_called_with(
|
||||
self._fake_snapshot, self._fake_volume.name,
|
||||
self._fake_snapshot_path)
|
||||
snapshot, volume_name,
|
||||
snapshot_path)
|
||||
self._driver._write_info_file.assert_called_with(
|
||||
mock.sentinel.fake_info_path,
|
||||
expected_snapshot_info)
|
||||
|
||||
def test_create_snapshot_volume_available(self):
|
||||
self._test_create_snapshot()
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_create_snapshot_volume_available(self, encryption):
|
||||
self._test_create_snapshot(encryption=encryption)
|
||||
|
||||
def test_create_snapshot_volume_in_use(self):
|
||||
self._test_create_snapshot(volume_in_use=True)
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_create_snapshot_volume_in_use(self, encryption):
|
||||
self._test_create_snapshot(volume_in_use=True,
|
||||
encryption=encryption)
|
||||
|
||||
def test_create_snapshot_invalid_volume(self):
|
||||
self._fake_snapshot.volume.status = 'error'
|
||||
@ -624,14 +696,15 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
src_vref = fake_volume.fake_volume_obj(
|
||||
self.context,
|
||||
id=src_vref_id,
|
||||
name='volume-%s' % src_vref_id)
|
||||
name='volume-%s' % src_vref_id,
|
||||
obj_context=self.context)
|
||||
src_vref.context = self.context
|
||||
|
||||
mock_snapshots_exist.return_value = snapshots_exist
|
||||
drv._always_use_temp_snap_when_cloning = force_temp_snap
|
||||
|
||||
vol_attrs = ['provider_location', 'size', 'id', 'name', 'status',
|
||||
'volume_type', 'metadata']
|
||||
'volume_type', 'metadata', 'obj_context']
|
||||
Volume = collections.namedtuple('Volume', vol_attrs)
|
||||
|
||||
volume_ref = Volume(id=volume.id,
|
||||
@ -640,7 +713,8 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
provider_location=volume.provider_location,
|
||||
status=volume.status,
|
||||
size=volume.size,
|
||||
volume_type=volume.volume_type,)
|
||||
volume_type=volume.volume_type,
|
||||
obj_context=self.context,)
|
||||
|
||||
snap_args_creation = {
|
||||
'volume_id': src_vref.id,
|
||||
@ -679,7 +753,8 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
mock_create_snapshot.assert_called_once_with(
|
||||
mock_obj_snap.return_value)
|
||||
mock_copy_volume_from_snapshot.assert_called_once_with(
|
||||
mock_obj_snap.return_value, volume_ref, volume['size'])
|
||||
mock_obj_snap.return_value, volume_ref, volume['size'],
|
||||
src_encryption_key_id=None, new_encryption_key_id=None)
|
||||
mock_delete_snapshot.called_once_with(snap_args_deletion)
|
||||
else:
|
||||
self.assertFalse(mock_create_snapshot.called)
|
||||
@ -693,6 +768,47 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
[mock.call(src_vref), mock.call(volume_ref)])
|
||||
mock_extend_volume.assert_called_once_with(volume_ref, volume.size)
|
||||
|
||||
@mock.patch('tempfile.NamedTemporaryFile')
|
||||
@mock.patch('cinder.volume.volume_utils.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
|
||||
|
||||
drv = self._driver
|
||||
|
||||
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}
|
||||
|
||||
remotefs_path = 'cinder.volume.drivers.remotefs.open'
|
||||
with mock.patch('cinder.volume.volume_utils.check_encryption_provider',
|
||||
return_value=enc_info), \
|
||||
mock.patch(remotefs_path) as mock_open, \
|
||||
mock.patch.object(drv, '_execute') as mock_exec:
|
||||
|
||||
drv._create_encrypted_volume_file("/passfile",
|
||||
self.volume_c.size,
|
||||
enc_info,
|
||||
self.context)
|
||||
|
||||
mock_open.assert_called_with('/imgfile', 'w')
|
||||
mock_exec.assert_called()
|
||||
|
||||
@mock.patch('shutil.copyfile')
|
||||
@mock.patch.object(remotefs.RemoteFSSnapDriver, '_set_rw_permissions')
|
||||
def test_copy_volume_image(self, mock_set_perm, mock_copyfile):
|
||||
|
@ -14,10 +14,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import binascii
|
||||
import errno
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from castellan import key_manager
|
||||
from os_brick.remotefs import remotefs as remotefs_brick
|
||||
from oslo_concurrency import processutils as putils
|
||||
from oslo_config import cfg
|
||||
@ -122,6 +125,8 @@ class NfsDriver(remotefs.RemoteFSSnapDriverDistributed):
|
||||
self.configuration.max_over_subscription_ratio,
|
||||
supports_auto=supports_auto_mosr))
|
||||
|
||||
self._supports_encryption = True
|
||||
|
||||
@staticmethod
|
||||
def get_driver_options():
|
||||
return nfs_opts + remotefs.nas_opts
|
||||
@ -577,7 +582,9 @@ class NfsDriver(remotefs.RemoteFSSnapDriverDistributed):
|
||||
self._check_snapshot_support()
|
||||
return self._delete_snapshot(snapshot)
|
||||
|
||||
def _copy_volume_from_snapshot(self, snapshot, volume, volume_size):
|
||||
def _copy_volume_from_snapshot(self, snapshot, volume, volume_size,
|
||||
src_encryption_key_id=None,
|
||||
new_encryption_key_id=None):
|
||||
"""Copy data from snapshot to destination volume.
|
||||
|
||||
This is done with a qemu-img convert to raw/qcow2 from the snapshot
|
||||
@ -610,9 +617,46 @@ class NfsDriver(remotefs.RemoteFSSnapDriverDistributed):
|
||||
else:
|
||||
out_format = 'raw'
|
||||
|
||||
image_utils.convert_image(path_to_snap_img,
|
||||
path_to_new_vol,
|
||||
out_format,
|
||||
run_as_root=self._execute_as_root)
|
||||
if new_encryption_key_id is not None:
|
||||
if src_encryption_key_id is None:
|
||||
message = _("Can't create an encrypted volume %(format)s "
|
||||
"from an unencrypted source."
|
||||
) % {'format': out_format}
|
||||
LOG.error(message)
|
||||
# TODO(enriquetaso): handle unencrypted snap->encrypted vol
|
||||
raise exception.NfsException(message)
|
||||
keymgr = key_manager.API(CONF)
|
||||
new_key = keymgr.get(volume.obj_context, new_encryption_key_id)
|
||||
new_passphrase = \
|
||||
binascii.hexlify(new_key.get_encoded()).decode('utf-8')
|
||||
|
||||
# volume.obj_context is the owner of this request
|
||||
src_key = keymgr.get(volume.obj_context, src_encryption_key_id)
|
||||
src_passphrase = \
|
||||
binascii.hexlify(src_key.get_encoded()).decode('utf-8')
|
||||
|
||||
tmp_dir = volume_utils.image_conversion_dir()
|
||||
with tempfile.NamedTemporaryFile(prefix='luks_',
|
||||
dir=tmp_dir) as src_pass_file:
|
||||
with open(src_pass_file.name, 'w') as f:
|
||||
f.write(src_passphrase)
|
||||
|
||||
with tempfile.NamedTemporaryFile(prefix='luks_',
|
||||
dir=tmp_dir) as new_pass_file:
|
||||
with open(new_pass_file.name, 'w') as f:
|
||||
f.write(new_passphrase)
|
||||
|
||||
image_utils.convert_image(
|
||||
path_to_snap_img,
|
||||
path_to_new_vol,
|
||||
'luks',
|
||||
passphrase_file=new_pass_file.name,
|
||||
src_passphrase_file=src_pass_file.name,
|
||||
run_as_root=self._execute_as_root)
|
||||
else:
|
||||
image_utils.convert_image(path_to_snap_img,
|
||||
path_to_new_vol,
|
||||
out_format,
|
||||
run_as_root=self._execute_as_root)
|
||||
|
||||
self._set_rw_permissions_for_all(path_to_new_vol)
|
||||
|
@ -14,6 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import binascii
|
||||
import collections
|
||||
import errno
|
||||
import hashlib
|
||||
@ -24,10 +25,13 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import string
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from castellan import key_manager
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import units
|
||||
import six
|
||||
|
||||
@ -295,8 +299,20 @@ class RemoteFSDriver(driver.BaseVD):
|
||||
volume_path = self.local_path(volume)
|
||||
volume_size = volume.size
|
||||
|
||||
if getattr(self.configuration,
|
||||
self.driver_prefix + '_qcow2_volumes', False):
|
||||
encrypted = volume.encryption_key_id is not None
|
||||
|
||||
if encrypted:
|
||||
encryption = volume_utils.check_encryption_provider(
|
||||
self.db,
|
||||
volume,
|
||||
volume.obj_context)
|
||||
|
||||
self._create_encrypted_volume_file(volume_path,
|
||||
volume_size,
|
||||
encryption,
|
||||
volume.obj_context)
|
||||
elif getattr(self.configuration,
|
||||
self.driver_prefix + '_qcow2_volumes', False):
|
||||
# QCOW2 volumes are inherently sparse, so this setting
|
||||
# will override the _sparsed_volumes setting.
|
||||
self._create_qcow2_file(volume_path, volume_size)
|
||||
@ -401,6 +417,47 @@ class RemoteFSDriver(driver.BaseVD):
|
||||
path, str(size_gb * units.Gi),
|
||||
run_as_root=self._execute_as_root)
|
||||
|
||||
def _create_encrypted_volume_file(self,
|
||||
path,
|
||||
size_gb,
|
||||
encryption,
|
||||
context):
|
||||
"""Create an encrypted volume.
|
||||
|
||||
This works by creating an encrypted image locally,
|
||||
and then uploading it to the volume.
|
||||
"""
|
||||
|
||||
cipher_spec = image_utils.decode_cipher(encryption['cipher'],
|
||||
encryption['key_size'])
|
||||
|
||||
# TODO(enriquetaso): share this code w/ the RBD driver
|
||||
# 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 = volume_utils.image_conversion_dir()
|
||||
|
||||
with tempfile.NamedTemporaryFile(dir=tmp_dir) as tmp_key:
|
||||
# TODO(enriquetaso): encrypt w/ aes256 cipher text
|
||||
# (qemu-img feature) ?
|
||||
with open(tmp_key.name, 'w') as f:
|
||||
f.write(passphrase)
|
||||
|
||||
self._execute(
|
||||
'qemu-img', 'create', '-f', 'qcow2',
|
||||
'-o',
|
||||
'encrypt.format=luks,'
|
||||
'encrypt.key-secret=sec1,'
|
||||
'encrypt.cipher-alg=%(cipher_alg)s,'
|
||||
'encrypt.cipher-mode=%(cipher_mode)s,'
|
||||
'encrypt.ivgen-alg=%(ivgen_alg)s' % cipher_spec,
|
||||
'--object', 'secret,id=sec1,format=raw,file=' + tmp_key.name,
|
||||
path, str(size_gb * units.Gi),
|
||||
run_as_root=self._execute_as_root)
|
||||
|
||||
def _set_rw_permissions(self, path):
|
||||
"""Sets access permissions for given NFS path.
|
||||
|
||||
@ -820,22 +877,53 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
def _qemu_img_info(self, path, volume_name):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _img_commit(self, path):
|
||||
def _img_commit(self, path, passphrase_file=None, backing_file=None):
|
||||
# TODO(eharney): this is not using the correct permissions for
|
||||
# NFS snapshots
|
||||
# It needs to run as root for volumes attached to instances, but
|
||||
# does not when in secure mode.
|
||||
self._execute('qemu-img', 'commit', '-d', path,
|
||||
run_as_root=self._execute_as_root)
|
||||
cmd = ['qemu-img', 'commit']
|
||||
if passphrase_file:
|
||||
obj = ['--object',
|
||||
'secret,id=s0,format=raw,file=%s' % passphrase_file]
|
||||
image_opts = ['--image-opts']
|
||||
|
||||
src_opts = \
|
||||
"file.filename=%(filename)s,encrypt.format=luks," \
|
||||
"encrypt.key-secret=s0,backing.file.filename=%(backing)s," \
|
||||
"backing.encrypt.key-secret=s0" % {
|
||||
'filename': path,
|
||||
'backing': backing_file,
|
||||
}
|
||||
|
||||
path_no_to_delete = ['-d', src_opts]
|
||||
cmd += obj + image_opts + path_no_to_delete
|
||||
else:
|
||||
cmd += ['-d', path]
|
||||
|
||||
self._execute(*cmd, run_as_root=self._execute_as_root)
|
||||
self._delete(path)
|
||||
|
||||
def _rebase_img(self, image, backing_file, volume_format):
|
||||
def _rebase_img(self, image, backing_file, volume_format,
|
||||
passphrase_file=None):
|
||||
# qemu-img create must run as root, because it reads from the
|
||||
# backing file, which will be owned by qemu:qemu if attached to an
|
||||
# instance.
|
||||
# TODO(erlon): Sanity check this.
|
||||
self._execute('qemu-img', 'rebase', '-u', '-b', backing_file, image,
|
||||
'-F', volume_format, run_as_root=self._execute_as_root)
|
||||
command = ['qemu-img', 'rebase', '-u']
|
||||
# if encrypted
|
||||
if passphrase_file:
|
||||
objectdef = "secret,id=s0,file=%s" % passphrase_file
|
||||
filename = "encrypt.key-secret=s0,"\
|
||||
"file.filename=%(filename)s" % {'filename': image}
|
||||
|
||||
command += ['--object', objectdef, '-b', backing_file,
|
||||
'-F', volume_format, '--image-opts', filename]
|
||||
# not encrypted
|
||||
else:
|
||||
command += ['-b', backing_file, image, '-F', volume_format]
|
||||
|
||||
self._execute(*command, run_as_root=self._execute_as_root)
|
||||
|
||||
def _read_info_file(self, info_path, empty_if_missing=False):
|
||||
"""Return dict of snapshot information.
|
||||
@ -1041,7 +1129,7 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
|
||||
# Create fake volume and snapshot objects
|
||||
vol_attrs = ['provider_location', 'size', 'id', 'name', 'status',
|
||||
'volume_type', 'metadata']
|
||||
'volume_type', 'metadata', 'obj_context']
|
||||
Volume = collections.namedtuple('Volume', vol_attrs)
|
||||
volume_info = Volume(provider_location=src_vref.provider_location,
|
||||
size=src_vref.size,
|
||||
@ -1049,7 +1137,8 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
name=volume_name,
|
||||
status=src_vref.status,
|
||||
volume_type=src_vref.volume_type,
|
||||
metadata=src_vref.metadata)
|
||||
metadata=src_vref.metadata,
|
||||
obj_context=volume.obj_context)
|
||||
|
||||
if (self._always_use_temp_snap_when_cloning or
|
||||
self._snapshots_exist(src_vref)):
|
||||
@ -1071,9 +1160,13 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
|
||||
self._create_snapshot(temp_snapshot)
|
||||
try:
|
||||
self._copy_volume_from_snapshot(temp_snapshot,
|
||||
volume_info,
|
||||
volume.size)
|
||||
self._copy_volume_from_snapshot(
|
||||
temp_snapshot,
|
||||
volume_info,
|
||||
volume.size,
|
||||
src_encryption_key_id=src_vref.encryption_key_id,
|
||||
new_encryption_key_id=volume.encryption_key_id)
|
||||
|
||||
# remove temp snapshot after the cloning is done
|
||||
temp_snapshot.status = fields.SnapshotStatus.DELETING
|
||||
temp_snapshot.context = context.elevated()
|
||||
@ -1134,6 +1227,7 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
self._validate_state(volume_status, acceptable_states)
|
||||
|
||||
vol_path = self._local_volume_dir(snapshot.volume)
|
||||
volume_path = os.path.join(vol_path, snapshot.volume.name)
|
||||
|
||||
# Determine the true snapshot file for this snapshot
|
||||
# based on the .info file
|
||||
@ -1206,14 +1300,33 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
snapshot,
|
||||
online_delete_info)
|
||||
|
||||
encrypted = snapshot.encryption_key_id is not None
|
||||
|
||||
if encrypted:
|
||||
keymgr = key_manager.API(CONF)
|
||||
encryption_key = snapshot.encryption_key_id
|
||||
new_key = keymgr.get(snapshot.obj_context, encryption_key)
|
||||
src_passphrase = \
|
||||
binascii.hexlify(new_key.get_encoded()).decode('utf-8')
|
||||
|
||||
tmp_dir = volume_utils.image_conversion_dir()
|
||||
|
||||
if utils.paths_normcase_equal(snapshot_file, active_file):
|
||||
# There is no top file
|
||||
# T0 | T1 |
|
||||
# base | snapshot_file | None
|
||||
# (guaranteed to| (being deleted, |
|
||||
# exist) | committed down) |
|
||||
|
||||
self._img_commit(snapshot_path)
|
||||
if encrypted:
|
||||
with tempfile.NamedTemporaryFile(prefix='luks_',
|
||||
dir=tmp_dir) as src_file:
|
||||
with open(src_file.name, 'w') as f:
|
||||
f.write(src_passphrase)
|
||||
self._img_commit(snapshot_path,
|
||||
passphrase_file=src_file.name,
|
||||
backing_file=volume_path)
|
||||
else:
|
||||
self._img_commit(snapshot_path)
|
||||
# Active file has changed
|
||||
snap_info['active'] = base_file
|
||||
else:
|
||||
@ -1241,11 +1354,25 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
higher_file
|
||||
raise exception.RemoteFSException(msg)
|
||||
|
||||
self._img_commit(snapshot_path)
|
||||
if encrypted:
|
||||
with tempfile.NamedTemporaryFile(prefix='luks_',
|
||||
dir=tmp_dir) as src_file:
|
||||
with open(src_file.name, 'w') as f:
|
||||
f.write(src_passphrase)
|
||||
self._img_commit(snapshot_path,
|
||||
passphrase_file=src_file.name,
|
||||
backing_file=volume_path)
|
||||
|
||||
higher_file_path = os.path.join(vol_path, higher_file)
|
||||
base_file_fmt = base_file_img_info.file_format
|
||||
self._rebase_img(higher_file_path, base_file, base_file_fmt)
|
||||
higher_file_path = os.path.join(vol_path, higher_file)
|
||||
base_file_fmt = base_file_img_info.file_format
|
||||
self._rebase_img(higher_file_path, volume_path,
|
||||
base_file_fmt, src_file.name)
|
||||
else:
|
||||
self._img_commit(snapshot_path)
|
||||
|
||||
higher_file_path = os.path.join(vol_path, higher_file)
|
||||
base_file_fmt = base_file_img_info.file_format
|
||||
self._rebase_img(higher_file_path, base_file, base_file_fmt)
|
||||
|
||||
# Remove snapshot_file from info
|
||||
del(snap_info[snapshot.id])
|
||||
@ -1274,11 +1401,15 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
|
||||
self._copy_volume_from_snapshot(snapshot,
|
||||
volume,
|
||||
volume.size)
|
||||
volume.size,
|
||||
snapshot.volume.encryption_key_id,
|
||||
volume.encryption_key_id)
|
||||
|
||||
return {'provider_location': volume.provider_location}
|
||||
|
||||
def _copy_volume_from_snapshot(self, snapshot, volume, volume_size):
|
||||
def _copy_volume_from_snapshot(self, snapshot, volume, volume_size,
|
||||
src_encryption_key_id=None,
|
||||
new_encryption_key_id=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _do_create_snapshot(self, snapshot, backing_filename,
|
||||
@ -1294,24 +1425,86 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
self._local_volume_dir(snapshot.volume),
|
||||
backing_filename)
|
||||
|
||||
volume_path = os.path.join(
|
||||
self._local_volume_dir(snapshot.volume),
|
||||
snapshot.volume.name)
|
||||
|
||||
info = self._qemu_img_info(backing_path_full_path,
|
||||
snapshot.volume.name)
|
||||
backing_fmt = info.file_format
|
||||
obj_context = snapshot.volume.obj_context
|
||||
|
||||
command = ['qemu-img', 'create', '-f', 'qcow2', '-o',
|
||||
'backing_file=%s,backing_fmt=%s' %
|
||||
(backing_path_full_path, backing_fmt),
|
||||
new_snap_path,
|
||||
"%dG" % snapshot.volume.size]
|
||||
self._execute(*command, run_as_root=self._execute_as_root)
|
||||
# create new qcow2 file
|
||||
if snapshot.volume.encryption_key_id is None:
|
||||
command = ['qemu-img', 'create', '-f', 'qcow2', '-o',
|
||||
'backing_file=%s,backing_fmt=%s' %
|
||||
(backing_path_full_path, backing_fmt),
|
||||
new_snap_path,
|
||||
"%dG" % snapshot.volume.size]
|
||||
|
||||
command = ['qemu-img', 'rebase', '-u',
|
||||
'-b', backing_filename,
|
||||
'-F', backing_fmt,
|
||||
new_snap_path]
|
||||
self._execute(*command, run_as_root=self._execute_as_root)
|
||||
|
||||
# qemu-img rebase must run as root for the same reasons as above
|
||||
self._execute(*command, run_as_root=self._execute_as_root)
|
||||
command = ['qemu-img', 'rebase', '-u',
|
||||
'-b', backing_filename,
|
||||
'-F', backing_fmt,
|
||||
new_snap_path]
|
||||
|
||||
# qemu-img rebase must run as root for the same reasons as above
|
||||
self._execute(*command, run_as_root=self._execute_as_root)
|
||||
|
||||
else:
|
||||
# encrypted
|
||||
keymgr = key_manager.API(CONF)
|
||||
# Get key for the source volume using the context of this request.
|
||||
key = keymgr.get(obj_context,
|
||||
snapshot.volume.encryption_key_id)
|
||||
passphrase = binascii.hexlify(key.get_encoded()).decode('utf-8')
|
||||
|
||||
tmp_dir = volume_utils.image_conversion_dir()
|
||||
with tempfile.NamedTemporaryFile(dir=tmp_dir) as tmp_key:
|
||||
with open(tmp_key.name, 'w') as f:
|
||||
f.write(passphrase)
|
||||
|
||||
file_json_dict = {"driver": "qcow2",
|
||||
"encrypt.key-secret": "s0",
|
||||
"backing.encrypt.key-secret": "s0",
|
||||
"backing.file.filename": volume_path,
|
||||
"file": {"driver": "file",
|
||||
"filename": backing_path_full_path,
|
||||
}}
|
||||
file_json = jsonutils.dumps(file_json_dict)
|
||||
|
||||
encryption = volume_utils.check_encryption_provider(
|
||||
db=db,
|
||||
volume=snapshot.volume,
|
||||
context=obj_context)
|
||||
|
||||
cipher_spec = image_utils.decode_cipher(encryption['cipher'],
|
||||
encryption['key_size'])
|
||||
|
||||
command = ('qemu-img', 'create', '-f' 'qcow2',
|
||||
'-o', 'encrypt.format=luks,encrypt.key-secret=s1,'
|
||||
'encrypt.cipher-alg=%(cipher_alg)s,'
|
||||
'encrypt.cipher-mode=%(cipher_mode)s,'
|
||||
'encrypt.ivgen-alg=%(ivgen_alg)s' % cipher_spec,
|
||||
'-b', 'json:' + file_json,
|
||||
'--object', 'secret,id=s0,file=' + tmp_key.name,
|
||||
'--object', 'secret,id=s1,file=' + tmp_key.name,
|
||||
new_snap_path)
|
||||
self._execute(*command, run_as_root=self._execute_as_root)
|
||||
|
||||
command_path = 'encrypt.key-secret=s0,file.filename='
|
||||
command = ['qemu-img', 'rebase',
|
||||
'--object', 'secret,id=s0,file=' + tmp_key.name,
|
||||
'--image-opts',
|
||||
command_path + new_snap_path,
|
||||
'-u',
|
||||
'-b', backing_filename,
|
||||
'-F', backing_fmt]
|
||||
|
||||
# qemu-img rebase must run as root for the same reasons as
|
||||
# above
|
||||
self._execute(*command, run_as_root=self._execute_as_root)
|
||||
|
||||
self._set_rw_permissions(new_snap_path)
|
||||
|
||||
|
@ -1194,6 +1194,11 @@ def check_encryption_provider(db, volume, context):
|
||||
"""
|
||||
|
||||
encryption = db.volume_encryption_metadata_get(context, volume.id)
|
||||
|
||||
if 'provider' not in encryption:
|
||||
message = _("Invalid encryption spec.")
|
||||
raise exception.VolumeDriverException(message=message)
|
||||
|
||||
provider = encryption['provider']
|
||||
if provider in encryptors.LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP:
|
||||
provider = encryptors.LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP[provider]
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The NFS driver now supports the creation of encrypted volumes.
|
Loading…
Reference in New Issue
Block a user