Adds ephemeral storage encryption for LVM back-end images

This patch adds ephemeral storage encryption for LVM back-end instances.
Encryption is implemented by passing all data written to and read from
the logical volumes through a dm-crypt layer. Most instance operations
such as pause/continue, suspend/resume, reboot, etc. are supported.
Snapshots are also supported but are not encrypted at present. VM rescue
and migration are not supported at present.

The proposed code provides data-at-rest security for all ephemeral
storage disks, preventing access to data while an instance is
shut down, or in case the compute host is shut down while an instance is
running.

Options controlling the encryption state, cipher and key size are
specified in the "ephemeral_storage_encryption" options group. The boolean
"enabled" option turns encryption on and off and the "cipher" and "key_size"
options specify the cipher and key size, respectively.

Note: depends on cryptsetup being installed.

Implements: blueprint lvm-ephemeral-storage-encryption
Change-Id: I871af4018f99ddfcc8408708bdaaf480088ac477
DocImpact
SecurityImpact
This commit is contained in:
Daniel Genin 2014-09-02 18:16:04 -04:00
parent 844d0cafdb
commit 5fa74bc0b2
9 changed files with 730 additions and 43 deletions

View File

@ -46,6 +46,7 @@ from nova import hooks
from nova.i18n import _
from nova.i18n import _LE
from nova import image
from nova import keymgr
from nova import network
from nova.network import model as network_model
from nova.network.security_group import openstack_driver
@ -111,9 +112,32 @@ compute_opts = [
'boot from volume. A negative number means unlimited.'),
]
ephemeral_storage_encryption_group = cfg.OptGroup(
name='ephemeral_storage_encryption',
title='Ephemeral storage encryption options')
ephemeral_storage_encryption_opts = [
cfg.BoolOpt('enabled',
default=False,
help='Whether to encrypt ephemeral storage'),
cfg.StrOpt('cipher',
default='aes-xts-plain64',
help='The cipher and mode to be used to encrypt ephemeral '
'storage. Which ciphers are available ciphers depends '
'on kernel support. See /proc/crypto for the list of '
'available options.'),
cfg.IntOpt('key_size',
default=512,
help='The bit length of the encryption key to be used to '
'encrypt ephemeral storage (in XTS mode only half of '
'the bits are used for encryption key)')
]
CONF = cfg.CONF
CONF.register_opts(compute_opts)
CONF.register_group(ephemeral_storage_encryption_group)
CONF.register_opts(ephemeral_storage_encryption_opts,
group='ephemeral_storage_encryption')
CONF.import_opt('compute_topic', 'nova.compute.rpcapi')
CONF.import_opt('enable', 'nova.cells.opts', group='cells')
CONF.import_opt('default_ephemeral_format', 'nova.virt.driver')
@ -244,6 +268,8 @@ class API(base.Base):
self._compute_task_api = None
self.servicegroup_api = servicegroup.API()
self.notifier = rpc.get_notifier('compute', CONF.host)
if CONF.ephemeral_storage_encryption.enabled:
self.key_manager = keymgr.API()
super(API, self).__init__(**kwargs)
@ -1185,7 +1211,7 @@ class API(base.Base):
def _default_display_name(self, instance_uuid):
return "Server %s" % instance_uuid
def _populate_instance_for_create(self, instance, image,
def _populate_instance_for_create(self, context, instance, image,
index, security_groups, instance_type):
"""Build the beginning of a new instance."""
@ -1201,6 +1227,12 @@ class API(base.Base):
info_cache.instance_uuid = instance.uuid
info_cache.network_info = network_model.NetworkInfo()
instance.info_cache = info_cache
if CONF.ephemeral_storage_encryption.enabled:
instance.ephemeral_key_uuid = self.key_manager.create_key(
context,
length=CONF.ephemeral_storage_encryption.key_size)
else:
instance.ephemeral_key_uuid = None
# Store image properties so we can use them later
# (for notifications, etc). Only store what we can.
@ -1233,7 +1265,7 @@ class API(base.Base):
This is called by the scheduler after a location for the
instance has been determined.
"""
self._populate_instance_for_create(instance, image, index,
self._populate_instance_for_create(context, instance, image, index,
security_group, instance_type)
self._populate_instance_names(instance, num_instances)

View File

@ -7313,6 +7313,7 @@ class ComputeAPITestCase(BaseTestCase):
instance.update(base_options)
inst_type = flavors.get_flavor_by_name("m1.tiny")
instance = self.compute_api._populate_instance_for_create(
self.context,
instance,
self.fake_image,
1,
@ -7331,9 +7332,9 @@ class ComputeAPITestCase(BaseTestCase):
orig_populate = self.compute_api._populate_instance_for_create
def _fake_populate(base_options, *args, **kwargs):
def _fake_populate(context, base_options, *args, **kwargs):
base_options['uuid'] = fake_uuids.pop(0)
return orig_populate(base_options, *args, **kwargs)
return orig_populate(context, base_options, *args, **kwargs)
self.stubs.Set(self.compute_api,
'_populate_instance_for_create',

View File

@ -52,10 +52,12 @@ class Backend(object):
return FakeImage(instance, name)
def snapshot(self, path, image_type=''):
def snapshot(self, instance, disk_path, image_type=''):
# NOTE(bfilippov): this is done in favor for
# snapshot tests in test_libvirt.LibvirtConnTestCase
return imagebackend.Backend(True).snapshot(path, image_type)
return imagebackend.Backend(True).snapshot(instance,
disk_path,
image_type=image_type)
class Raw(imagebackend.Image):

View File

@ -156,7 +156,12 @@ def file_open(path, mode=None):
def find_disk(virt_dom):
return "filename"
if disk_type == 'lvm':
return "/dev/nova-vg/lv"
elif disk_type in ['raw', 'qcow2']:
return "filename"
else:
return "unknown_type_disk"
def load_file(path):

View File

@ -3634,6 +3634,83 @@ class LibvirtConnTestCase(test.TestCase,
self.assertEqual(snapshot['disk_format'], 'raw')
self.assertEqual(snapshot['name'], snapshot_name)
def test_lvm_snapshot_in_raw_format(self):
# Tests Lvm backend snapshot functionality with raw format
# snapshots.
xml = """
<domain type='kvm'>
<devices>
<disk type='block' device='disk'>
<source dev='/dev/some-vg/some-lv'/>
</disk>
</devices>
</domain>
"""
update_task_state_calls = [
mock.call(task_state=task_states.IMAGE_PENDING_UPLOAD),
mock.call(task_state=task_states.IMAGE_UPLOADING,
expected_state=task_states.IMAGE_PENDING_UPLOAD)]
mock_update_task_state = mock.Mock()
mock_lookupByName = mock.Mock(return_value=FakeVirtDomain(xml),
autospec=True)
volume_info = {'VG': 'nova-vg', 'LV': 'disk'}
mock_volume_info = mock.Mock(return_value=volume_info,
autospec=True)
mock_volume_info_calls = [mock.call('/dev/nova-vg/lv')]
mock_convert_image = mock.Mock()
def convert_image_side_effect(source, dest, out_format,
run_as_root=True):
libvirt_driver.libvirt_utils.files[dest] = ''
mock_convert_image.side_effect = convert_image_side_effect
self.flags(snapshots_directory='./',
snapshot_image_format='raw',
images_type='lvm',
images_volume_group='nova-vg', group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "lvm"
# Start test
image_service = nova.tests.image.fake.FakeImageService()
instance_ref = db.instance_create(self.context, self.test_instance)
properties = {'instance_id': instance_ref['id'],
'user_id': str(self.context.user_id)}
snapshot_name = 'test-snap'
sent_meta = {'name': snapshot_name, 'is_public': False,
'status': 'creating', 'properties': properties}
recv_meta = image_service.create(context, sent_meta)
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
with contextlib.nested(
mock.patch.object(libvirt_driver.LibvirtDriver,
'_conn',
autospec=True),
mock.patch.object(libvirt_driver.imagebackend.lvm,
'volume_info',
mock_volume_info),
mock.patch.object(libvirt_driver.imagebackend.images,
'convert_image',
mock_convert_image),
mock.patch.object(libvirt_driver.LibvirtDriver,
'_lookup_by_name',
mock_lookupByName)):
conn.snapshot(self.context, instance_ref, recv_meta['id'],
mock_update_task_state)
mock_lookupByName.assert_called_once_with("instance-00000001")
mock_volume_info.assert_has_calls(mock_volume_info_calls)
mock_convert_image.assert_called_once()
snapshot = image_service.show(context, recv_meta['id'])
mock_update_task_state.assert_has_calls(update_task_state_calls)
self.assertEqual('available', snapshot['properties']['image_state'])
self.assertEqual('active', snapshot['status'])
self.assertEqual('raw', snapshot['disk_format'])
self.assertEqual(snapshot_name, snapshot['name'])
# This is for all the subsequent tests that do not set the value of
# images type
self.flags(images_type='default', group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "qcow2"
def test_lxc_snapshot_in_raw_format(self):
expected_calls = [
{'args': (),
@ -3665,6 +3742,7 @@ class LibvirtConnTestCase(test.TestCase,
self.mox.StubOutWithMock(libvirt_driver.utils, 'execute')
libvirt_driver.utils.execute = self.fake_execute
self.stubs.Set(libvirt_driver.libvirt_utils, 'disk_type', 'raw')
libvirt_driver.libvirt_utils.disk_type = "raw"
def convert_image(source, dest, out_format):
libvirt_driver.libvirt_utils.files[dest] = ''
@ -3775,6 +3853,80 @@ class LibvirtConnTestCase(test.TestCase,
self.assertEqual(snapshot['disk_format'], 'qcow2')
self.assertEqual(snapshot['name'], snapshot_name)
def test_lvm_snapshot_in_qcow2_format(self):
# Tests Lvm backend snapshot functionality with raw format
# snapshots.
xml = """
<domain type='kvm'>
<devices>
<disk type='block' device='disk'>
<source dev='/dev/some-vg/some-lv'/>
</disk>
</devices>
</domain>
"""
update_task_state_calls = [
mock.call(task_state=task_states.IMAGE_PENDING_UPLOAD),
mock.call(task_state=task_states.IMAGE_UPLOADING,
expected_state=task_states.IMAGE_PENDING_UPLOAD)]
mock_update_task_state = mock.Mock()
mock_lookupByName = mock.Mock(return_value=FakeVirtDomain(xml),
autospec=True)
volume_info = {'VG': 'nova-vg', 'LV': 'disk'}
mock_volume_info = mock.Mock(return_value=volume_info, autospec=True)
mock_volume_info_calls = [mock.call('/dev/nova-vg/lv')]
mock_convert_image = mock.Mock()
def convert_image_side_effect(source, dest, out_format,
run_as_root=True):
libvirt_driver.libvirt_utils.files[dest] = ''
mock_convert_image.side_effect = convert_image_side_effect
self.flags(snapshots_directory='./',
snapshot_image_format='qcow2',
images_type='lvm',
images_volume_group='nova-vg', group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "lvm"
# Start test
image_service = nova.tests.image.fake.FakeImageService()
instance_ref = db.instance_create(self.context, self.test_instance)
properties = {'instance_id': instance_ref['id'],
'user_id': str(self.context.user_id)}
snapshot_name = 'test-snap'
sent_meta = {'name': snapshot_name, 'is_public': False,
'status': 'creating', 'properties': properties}
recv_meta = image_service.create(context, sent_meta)
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
with contextlib.nested(
mock.patch.object(libvirt_driver.LibvirtDriver,
'_conn',
autospec=True),
mock.patch.object(libvirt_driver.imagebackend.lvm,
'volume_info',
mock_volume_info),
mock.patch.object(libvirt_driver.imagebackend.images,
'convert_image',
mock_convert_image),
mock.patch.object(libvirt_driver.LibvirtDriver,
'_lookup_by_name',
mock_lookupByName)):
conn.snapshot(self.context, instance_ref, recv_meta['id'],
mock_update_task_state)
mock_lookupByName.assert_called_once_with("instance-00000001")
mock_volume_info.assert_has_calls(mock_volume_info_calls)
mock_convert_image.assert_called_once()
snapshot = image_service.show(context, recv_meta['id'])
mock_update_task_state.assert_has_calls(update_task_state_calls)
self.assertEqual('available', snapshot['properties']['image_state'])
self.assertEqual('active', snapshot['status'])
self.assertEqual('qcow2', snapshot['disk_format'])
self.assertEqual(snapshot_name, snapshot['name'])
self.flags(images_type='default', group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "qcow2"
def test_snapshot_no_image_architecture(self):
expected_calls = [
{'args': (),
@ -3857,6 +4009,7 @@ class LibvirtConnTestCase(test.TestCase,
libvirt_driver.LibvirtDriver._conn.lookupByName = self.fake_lookup
self.mox.StubOutWithMock(libvirt_driver.utils, 'execute')
libvirt_driver.utils.execute = self.fake_execute
libvirt_driver.libvirt_utils.disk_type = "qcow2"
self.mox.ReplayAll()
@ -3927,6 +4080,7 @@ class LibvirtConnTestCase(test.TestCase,
self.flags(snapshots_directory='./',
virt_type='lxc',
group='libvirt')
libvirt_driver.libvirt_utils.disk_type = "qcow2"
# Assign a non-existent image
test_instance = copy.deepcopy(self.test_instance)
@ -6281,7 +6435,6 @@ class LibvirtConnTestCase(test.TestCase,
self.mox.StubOutWithMock(libvirt_driver.LibvirtDriver,
'_undefine_domain')
libvirt_driver.LibvirtDriver._undefine_domain(instance)
self.mox.StubOutWithMock(db, 'instance_get_by_uuid')
db.instance_get_by_uuid(mox.IgnoreArg(), mox.IgnoreArg(),
columns_to_join=['info_cache',
@ -6304,8 +6457,7 @@ class LibvirtConnTestCase(test.TestCase,
'delete_instance_files')
(libvirt_driver.LibvirtDriver.delete_instance_files(mox.IgnoreArg()).
AndReturn(True))
self.mox.StubOutWithMock(libvirt_driver.LibvirtDriver, '_cleanup_lvm')
libvirt_driver.LibvirtDriver._cleanup_lvm(instance)
libvirt_driver.LibvirtDriver._undefine_domain(instance)
# Start test
self.mox.ReplayAll()

View File

@ -13,16 +13,19 @@
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
import inspect
import os
import shutil
import tempfile
import fixtures
import mock
from oslo.config import cfg
import inspect
from nova import context
from nova import exception
from nova import keymgr
from nova.openstack.common import units
from nova.openstack.common import uuidutils
from nova import test
@ -52,6 +55,7 @@ class _ImageTestCase(object):
self.INSTANCE['uuid'], 'disk.info')
self.NAME = 'fake.vm'
self.TEMPLATE = 'template'
self.CONTEXT = context.get_admin_context()
self.OLD_STYLE_INSTANCE_PATH = \
fake_libvirt_utils.get_instance_path(self.INSTANCE, forceold=True)
@ -462,10 +466,11 @@ class LvmTestCase(_ImageTestCase, test.NoDBTestCase):
self.image_class = imagebackend.Lvm
super(LvmTestCase, self).setUp()
self.flags(images_volume_group=self.VG, group='libvirt')
self.flags(enabled=False, group='ephemeral_storage_encryption')
self.INSTANCE['ephemeral_key_uuid'] = None
self.LV = '%s_%s' % (self.INSTANCE['uuid'], self.NAME)
self.OLD_STYLE_INSTANCE_PATH = None
self.PATH = os.path.join('/dev', self.VG, self.LV)
self.disk = imagebackend.disk
self.utils = imagebackend.utils
self.lvm = imagebackend.lvm
@ -656,6 +661,378 @@ class LvmTestCase(_ImageTestCase, test.NoDBTestCase):
self.assertEqual(fake_processutils.fake_execute_get_log(), [])
class EncryptedLvmTestCase(_ImageTestCase, test.TestCase):
VG = 'FakeVG'
TEMPLATE_SIZE = 512
SIZE = 1024
def setUp(self):
super(EncryptedLvmTestCase, self).setUp()
self.image_class = imagebackend.Lvm
self.flags(enabled=True, group='ephemeral_storage_encryption')
self.flags(cipher='aes-xts-plain64',
group='ephemeral_storage_encryption')
self.flags(key_size=512, group='ephemeral_storage_encryption')
self.flags(fixed_key='00000000000000000000000000000000'
'00000000000000000000000000000000',
group='keymgr')
self.flags(images_volume_group=self.VG, group='libvirt')
self.LV = '%s_%s' % (self.INSTANCE['uuid'], self.NAME)
self.OLD_STYLE_INSTANCE_PATH = None
self.LV_PATH = os.path.join('/dev', self.VG, self.LV)
self.PATH = os.path.join('/dev/mapper',
imagebackend.dmcrypt.volume_name(self.LV))
self.key_manager = keymgr.API()
self.INSTANCE['ephemeral_key_uuid'] =\
self.key_manager.create_key(self.CONTEXT)
self.KEY = self.key_manager.get_key(self.CONTEXT,
self.INSTANCE['ephemeral_key_uuid']).get_encoded()
self.lvm = imagebackend.lvm
self.disk = imagebackend.disk
self.utils = imagebackend.utils
self.libvirt_utils = imagebackend.libvirt_utils
self.dmcrypt = imagebackend.dmcrypt
def _create_image(self, sparse):
with contextlib.nested(
mock.patch.object(self.lvm, 'create_volume', mock.Mock()),
mock.patch.object(self.lvm, 'remove_volumes', mock.Mock()),
mock.patch.object(self.disk, 'resize2fs', mock.Mock()),
mock.patch.object(self.disk, 'get_disk_size',
mock.Mock(return_value=self.TEMPLATE_SIZE)),
mock.patch.object(self.dmcrypt, 'create_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'delete_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'list_volumes', mock.Mock()),
mock.patch.object(self.libvirt_utils, 'create_lvm_image',
mock.Mock()),
mock.patch.object(self.libvirt_utils, 'remove_logical_volumes',
mock.Mock()),
mock.patch.object(self.utils, 'execute', mock.Mock())):
fn = mock.Mock()
image = self.image_class(self.INSTANCE, self.NAME)
image.create_image(fn, self.TEMPLATE_PATH, self.TEMPLATE_SIZE,
context=self.CONTEXT)
fn.assert_called_with(context=self.CONTEXT,
max_size=self.TEMPLATE_SIZE,
target=self.TEMPLATE_PATH)
self.lvm.create_volume.assert_called_with(self.VG,
self.LV,
self.TEMPLATE_SIZE,
sparse=sparse)
self.dmcrypt.create_volume.assert_called_with(
self.PATH.rpartition('/')[2],
self.LV_PATH,
CONF.ephemeral_storage_encryption.cipher,
CONF.ephemeral_storage_encryption.key_size,
self.KEY)
cmd = ('qemu-img',
'convert',
'-O',
'raw',
self.TEMPLATE_PATH,
self.PATH)
self.utils.execute.assert_called_with(*cmd, run_as_root=True)
def _create_image_generated(self, sparse):
with contextlib.nested(
mock.patch.object(self.lvm, 'create_volume', mock.Mock()),
mock.patch.object(self.lvm, 'remove_volumes', mock.Mock()),
mock.patch.object(self.disk, 'resize2fs', mock.Mock()),
mock.patch.object(self.disk, 'get_disk_size',
mock.Mock(return_value=self.TEMPLATE_SIZE)),
mock.patch.object(self.dmcrypt, 'create_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'delete_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'list_volumes', mock.Mock()),
mock.patch.object(self.libvirt_utils, 'create_lvm_image',
mock.Mock()),
mock.patch.object(self.libvirt_utils, 'remove_logical_volumes',
mock.Mock()),
mock.patch.object(self.utils, 'execute', mock.Mock())):
fn = mock.Mock()
image = self.image_class(self.INSTANCE, self.NAME)
image.create_image(fn, self.TEMPLATE_PATH,
self.SIZE,
ephemeral_size=None,
context=self.CONTEXT)
self.lvm.create_volume.assert_called_with(
self.VG,
self.LV,
self.SIZE,
sparse=sparse)
self.dmcrypt.create_volume.assert_called_with(
self.PATH.rpartition('/')[2],
self.LV_PATH,
CONF.ephemeral_storage_encryption.cipher,
CONF.ephemeral_storage_encryption.key_size,
self.KEY)
fn.assert_called_with(target=self.PATH,
ephemeral_size=None, context=self.CONTEXT)
def _create_image_resize(self, sparse):
with contextlib.nested(
mock.patch.object(self.lvm, 'create_volume', mock.Mock()),
mock.patch.object(self.lvm, 'remove_volumes', mock.Mock()),
mock.patch.object(self.disk, 'resize2fs', mock.Mock()),
mock.patch.object(self.disk, 'get_disk_size',
mock.Mock(return_value=self.TEMPLATE_SIZE)),
mock.patch.object(self.dmcrypt, 'create_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'delete_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'list_volumes', mock.Mock()),
mock.patch.object(self.libvirt_utils, 'create_lvm_image',
mock.Mock()),
mock.patch.object(self.libvirt_utils, 'remove_logical_volumes',
mock.Mock()),
mock.patch.object(self.utils, 'execute', mock.Mock())):
fn = mock.Mock()
image = self.image_class(self.INSTANCE, self.NAME)
image.create_image(fn, self.TEMPLATE_PATH, self.SIZE,
context=self.CONTEXT)
fn.assert_called_with(context=self.CONTEXT, max_size=self.SIZE,
target=self.TEMPLATE_PATH)
self.disk.get_disk_size.assert_called_with(self.TEMPLATE_PATH)
self.lvm.create_volume.assert_called_with(
self.VG,
self.LV,
self.SIZE,
sparse=sparse)
self.dmcrypt.create_volume.assert_called_with(
self.PATH.rpartition('/')[2],
self.LV_PATH,
CONF.ephemeral_storage_encryption.cipher,
CONF.ephemeral_storage_encryption.key_size,
self.KEY)
cmd = ('qemu-img',
'convert',
'-O',
'raw',
self.TEMPLATE_PATH,
self.PATH)
self.utils.execute.assert_called_with(*cmd, run_as_root=True)
self.disk.resize2fs.assert_called_with(self.PATH, run_as_root=True)
def test_create_image(self):
self._create_image(False)
def test_create_image_sparsed(self):
self.flags(sparse_logical_volumes=True, group='libvirt')
self._create_image(True)
def test_create_image_generated(self):
self._create_image_generated(False)
def test_create_image_generated_sparsed(self):
self.flags(sparse_logical_volumes=True, group='libvirt')
self._create_image_generated(True)
def test_create_image_resize(self):
self._create_image_resize(False)
def test_create_image_resize_sparsed(self):
self.flags(sparse_logical_volumes=True, group='libvirt')
self._create_image_resize(True)
def test_create_image_negative(self):
with contextlib.nested(
mock.patch.object(self.lvm, 'create_volume', mock.Mock()),
mock.patch.object(self.lvm, 'remove_volumes', mock.Mock()),
mock.patch.object(self.disk, 'resize2fs', mock.Mock()),
mock.patch.object(self.disk, 'get_disk_size',
mock.Mock(return_value=self.TEMPLATE_SIZE)),
mock.patch.object(self.dmcrypt, 'create_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'delete_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'list_volumes', mock.Mock()),
mock.patch.object(self.libvirt_utils, 'create_lvm_image',
mock.Mock()),
mock.patch.object(self.libvirt_utils, 'remove_logical_volumes',
mock.Mock()),
mock.patch.object(self.utils, 'execute', mock.Mock())):
fn = mock.Mock()
self.lvm.create_volume.side_effect = RuntimeError()
image = self.image_class(self.INSTANCE, self.NAME)
self.assertRaises(
RuntimeError,
image.create_image,
fn,
self.TEMPLATE_PATH,
self.SIZE,
context=self.CONTEXT)
fn.assert_called_with(
context=self.CONTEXT,
max_size=self.SIZE,
target=self.TEMPLATE_PATH)
self.disk.get_disk_size.assert_called_with(
self.TEMPLATE_PATH)
self.lvm.create_volume.assert_called_with(
self.VG,
self.LV,
self.SIZE,
sparse=False)
self.dmcrypt.delete_volume.assert_called_with(
self.PATH.rpartition('/')[2])
self.lvm.remove_volumes.assert_called_with(self.LV_PATH)
def test_create_image_encrypt_negative(self):
with contextlib.nested(
mock.patch.object(self.lvm, 'create_volume', mock.Mock()),
mock.patch.object(self.lvm, 'remove_volumes', mock.Mock()),
mock.patch.object(self.disk, 'resize2fs', mock.Mock()),
mock.patch.object(self.disk, 'get_disk_size',
mock.Mock(return_value=self.TEMPLATE_SIZE)),
mock.patch.object(self.dmcrypt, 'create_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'delete_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'list_volumes', mock.Mock()),
mock.patch.object(self.libvirt_utils, 'create_lvm_image',
mock.Mock()),
mock.patch.object(self.libvirt_utils, 'remove_logical_volumes',
mock.Mock()),
mock.patch.object(self.utils, 'execute', mock.Mock())):
fn = mock.Mock()
self.dmcrypt.create_volume.side_effect = RuntimeError()
image = self.image_class(self.INSTANCE, self.NAME)
self.assertRaises(
RuntimeError,
image.create_image,
fn,
self.TEMPLATE_PATH,
self.SIZE,
context=self.CONTEXT)
fn.assert_called_with(
context=self.CONTEXT,
max_size=self.SIZE,
target=self.TEMPLATE_PATH)
self.disk.get_disk_size.assert_called_with(self.TEMPLATE_PATH)
self.lvm.create_volume.assert_called_with(
self.VG,
self.LV,
self.SIZE,
sparse=False)
self.dmcrypt.create_volume.assert_called_with(
self.dmcrypt.volume_name(self.LV),
self.LV_PATH,
CONF.ephemeral_storage_encryption.cipher,
CONF.ephemeral_storage_encryption.key_size,
self.KEY)
self.dmcrypt.delete_volume.assert_called_with(
self.PATH.rpartition('/')[2])
self.lvm.remove_volumes.assert_called_with(self.LV_PATH)
def test_create_image_generated_negative(self):
with contextlib.nested(
mock.patch.object(self.lvm, 'create_volume', mock.Mock()),
mock.patch.object(self.lvm, 'remove_volumes', mock.Mock()),
mock.patch.object(self.disk, 'resize2fs', mock.Mock()),
mock.patch.object(self.disk, 'get_disk_size',
mock.Mock(return_value=self.TEMPLATE_SIZE)),
mock.patch.object(self.dmcrypt, 'create_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'delete_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'list_volumes', mock.Mock()),
mock.patch.object(self.libvirt_utils, 'create_lvm_image',
mock.Mock()),
mock.patch.object(self.libvirt_utils, 'remove_logical_volumes',
mock.Mock()),
mock.patch.object(self.utils, 'execute', mock.Mock())):
fn = mock.Mock()
fn.side_effect = RuntimeError()
image = self.image_class(self.INSTANCE, self.NAME)
self.assertRaises(RuntimeError,
image.create_image,
fn,
self.TEMPLATE_PATH,
self.SIZE,
ephemeral_size=None,
context=self.CONTEXT)
self.lvm.create_volume.assert_called_with(
self.VG,
self.LV,
self.SIZE,
sparse=False)
self.dmcrypt.create_volume.assert_called_with(
self.PATH.rpartition('/')[2],
self.LV_PATH,
CONF.ephemeral_storage_encryption.cipher,
CONF.ephemeral_storage_encryption.key_size,
self.KEY)
fn.assert_called_with(
target=self.PATH,
ephemeral_size=None,
context=self.CONTEXT)
self.dmcrypt.delete_volume.assert_called_with(
self.PATH.rpartition('/')[2])
self.lvm.remove_volumes.assert_called_with(self.LV_PATH)
def test_create_image_generated_encrypt_negative(self):
with contextlib.nested(
mock.patch.object(self.lvm, 'create_volume', mock.Mock()),
mock.patch.object(self.lvm, 'remove_volumes', mock.Mock()),
mock.patch.object(self.disk, 'resize2fs', mock.Mock()),
mock.patch.object(self.disk, 'get_disk_size',
mock.Mock(return_value=self.TEMPLATE_SIZE)),
mock.patch.object(self.dmcrypt, 'create_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'delete_volume', mock.Mock()),
mock.patch.object(self.dmcrypt, 'list_volumes', mock.Mock()),
mock.patch.object(self.libvirt_utils, 'create_lvm_image',
mock.Mock()),
mock.patch.object(self.libvirt_utils, 'remove_logical_volumes',
mock.Mock()),
mock.patch.object(self.utils, 'execute', mock.Mock())):
fn = mock.Mock()
fn.side_effect = RuntimeError()
image = self.image_class(self.INSTANCE, self.NAME)
self.assertRaises(
RuntimeError,
image.create_image,
fn,
self.TEMPLATE_PATH,
self.SIZE,
ephemeral_size=None,
context=self.CONTEXT)
self.lvm.create_volume.assert_called_with(
self.VG,
self.LV,
self.SIZE,
sparse=False)
self.dmcrypt.create_volume.assert_called_with(
self.PATH.rpartition('/')[2],
self.LV_PATH,
CONF.ephemeral_storage_encryption.cipher,
CONF.ephemeral_storage_encryption.key_size,
self.KEY)
self.dmcrypt.delete_volume.assert_called_with(
self.PATH.rpartition('/')[2])
self.lvm.remove_volumes.assert_called_with(self.LV_PATH)
def test_prealloc_image(self):
self.flags(preallocate_images='space')
fake_processutils.fake_execute_clear_log()
fake_processutils.stub_out_processutils_execute(self.stubs)
image = self.image_class(self.INSTANCE, self.NAME)
def fake_fetch(target, *args, **kwargs):
return
self.stubs.Set(os.path, 'exists', lambda _: True)
self.stubs.Set(image, 'check_image_exists', lambda: True)
image.cache(fake_fetch, self.TEMPLATE_PATH, self.SIZE)
self.assertEqual(fake_processutils.fake_execute_get_log(), [])
class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
POOL = "FakePool"
USER = "FakeUser"
@ -862,6 +1239,8 @@ class BackendTestCase(test.NoDBTestCase):
def setUp(self):
super(BackendTestCase, self).setUp()
self.flags(enabled=False, group='ephemeral_storage_encryption')
self.INSTANCE['ephemeral_key_uuid'] = None
def get_image(self, use_cow, image_type):
return imagebackend.Backend(use_cow).image(self.INSTANCE,

View File

@ -29,6 +29,14 @@ def volume_name(base):
return base + _dmcrypt_suffix
def is_encrypted(path):
"""Returns true if the path corresponds to an encrypted disk."""
if path.startswith('/dev/mapper'):
return path.rpartition('/')[2].endswith(_dmcrypt_suffix)
else:
return False
def create_volume(target, device, cipher, key_size, key):
"""Sets up a dmcrypt mapping

View File

@ -92,6 +92,7 @@ from nova.virt import firewall
from nova.virt import hardware
from nova.virt.libvirt import blockinfo
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import dmcrypt
from nova.virt.libvirt import firewall as libvirt_firewall
from nova.virt.libvirt import imagebackend
from nova.virt.libvirt import imagecache
@ -243,6 +244,12 @@ CONF.import_opt('host', 'nova.netconf')
CONF.import_opt('my_ip', 'nova.netconf')
CONF.import_opt('default_ephemeral_format', 'nova.virt.driver')
CONF.import_opt('use_cow_images', 'nova.virt.driver')
CONF.import_opt('enabled', 'nova.compute.api',
group='ephemeral_storage_encryption')
CONF.import_opt('cipher', 'nova.compute.api',
group='ephemeral_storage_encryption')
CONF.import_opt('key_size', 'nova.compute.api',
group='ephemeral_storage_encryption')
CONF.import_opt('live_migration_retry_count', 'nova.compute.manager')
CONF.import_opt('vncserver_proxyclient_address', 'nova.vnc')
CONF.import_opt('server_proxyclient_address', 'nova.spice', group='spice')
@ -1045,7 +1052,6 @@ class LibvirtDriver(driver.ComputeDriver):
def cleanup(self, context, instance, network_info, block_device_info=None,
destroy_disks=True, migrate_data=None, destroy_vifs=True):
self._undefine_domain(instance)
if destroy_vifs:
self._unplug_vifs(instance, network_info, True)
@ -1119,16 +1125,27 @@ class LibvirtDriver(driver.ComputeDriver):
{'vol_id': vol.get('volume_id'), 'exc': exc},
instance=instance)
if destroy_disks:
# NOTE(haomai): destroy volumes if needed
if CONF.libvirt.images_type == 'lvm':
self._cleanup_lvm(instance)
if CONF.libvirt.images_type == 'rbd':
self._cleanup_rbd(instance)
if destroy_disks or (
migrate_data and migrate_data.get('is_shared_block_storage',
False)):
self._delete_instance_files(instance)
if destroy_disks:
self._cleanup_lvm(instance)
# NOTE(haomai): destroy volumes if needed
if CONF.libvirt.images_type == 'rbd':
self._cleanup_rbd(instance)
self._undefine_domain(instance)
def _detach_encrypted_volumes(self, instance):
"""Detaches encrypted volumes attached to instance."""
disks = jsonutils.loads(self.get_instance_disk_info(instance['name']))
encrypted_volumes = filter(dmcrypt.is_encrypted,
[disk['path'] for disk in disks])
for path in encrypted_volumes:
dmcrypt.delete_volume(path)
if CONF.serial_console.enabled:
for host, port in self._get_serial_ports_from_instance(instance):
@ -1162,6 +1179,9 @@ class LibvirtDriver(driver.ComputeDriver):
def _cleanup_lvm(self, instance):
"""Delete all LVM disks for given instance object."""
if instance.get('ephemeral_key_uuid') is not None:
self._detach_encrypted_volumes(instance)
disks = self._lvm_disks(instance)
if disks:
lvm.remove_volumes(disks)
@ -1593,10 +1613,16 @@ class LibvirtDriver(driver.ComputeDriver):
# NOTE(rmk): Live snapshots require QEMU 1.3 and Libvirt 1.0.0.
# These restrictions can be relaxed as other configurations
# can be validated.
if self._has_min_version(MIN_LIBVIRT_LIVESNAPSHOT_VERSION,
MIN_QEMU_LIVESNAPSHOT_VERSION,
REQ_HYPERVISOR_LIVESNAPSHOT) \
and not source_format == "lvm" and not source_format == 'rbd':
# NOTE(dgenin): Instances with LVM encrypted ephemeral storage require
# cold snapshots. Currently, checking for encryption is
# redundant because LVM supports only cold snapshots.
# It is necessary in case this situation changes in the
# future.
if (self._has_min_version(MIN_LIBVIRT_LIVESNAPSHOT_VERSION,
MIN_QEMU_LIVESNAPSHOT_VERSION,
REQ_HYPERVISOR_LIVESNAPSHOT)
and source_format not in ('lvm', 'rbd')
and not CONF.ephemeral_storage_encryption.enabled):
live_snapshot = True
# Abort is an idempotent operation, so make sure any block
# jobs which may have failed are ended. This operation also
@ -1626,7 +1652,8 @@ class LibvirtDriver(driver.ComputeDriver):
pci_manager.get_instance_pci_devs(instance))
virt_dom.managedSave(0)
snapshot_backend = self.image_backend.snapshot(disk_path,
snapshot_backend = self.image_backend.snapshot(instance,
disk_path,
image_type=source_format)
if live_snapshot:
@ -2732,7 +2759,7 @@ class LibvirtDriver(driver.ComputeDriver):
def _create_ephemeral(self, target, ephemeral_size,
fs_label, os_type, is_block_dev=False,
max_size=None, specified_fs=None):
max_size=None, context=None, specified_fs=None):
if not is_block_dev:
self._create_local(target, ephemeral_size)
@ -2741,7 +2768,7 @@ class LibvirtDriver(driver.ComputeDriver):
specified_fs=specified_fs)
@staticmethod
def _create_swap(target, swap_mb, max_size=None):
def _create_swap(target, swap_mb, max_size=None, context=None):
"""Create a swap file of specified size."""
libvirt_utils.create_image('raw', target, '%dM' % swap_mb)
utils.mkfs('swap', target)
@ -2961,6 +2988,7 @@ class LibvirtDriver(driver.ComputeDriver):
fname = "ephemeral_%s_%s" % (ephemeral_gb, os_type_with_default)
size = ephemeral_gb * units.Gi
disk_image.cache(fetch_func=fn,
context=context,
filename=fname,
size=size,
ephemeral_size=ephemeral_gb)
@ -2980,8 +3008,8 @@ class LibvirtDriver(driver.ComputeDriver):
is_block_dev=disk_image.is_block_dev)
size = eph['size'] * units.Gi
fname = "ephemeral_%s_%s" % (eph['size'], os_type_with_default)
disk_image.cache(
fetch_func=fn,
disk_image.cache(fetch_func=fn,
context=context,
filename=fname,
size=size,
ephemeral_size=eph['size'],
@ -3002,6 +3030,7 @@ class LibvirtDriver(driver.ComputeDriver):
if swap_mb > 0:
size = swap_mb * units.Mi
image('disk.swap').cache(fetch_func=self._create_swap,
context=context,
filename="swap_%s" % swap_mb,
size=size,
swap_mb=swap_mb)
@ -5281,7 +5310,7 @@ class LibvirtDriver(driver.ComputeDriver):
for cnt, path_node in enumerate(path_nodes):
disk_type = disk_nodes[cnt].get('type')
path = path_node.get('file')
path = path_node.get('file') or path_node.get('dev')
target = target_nodes[cnt].attrib['dev']
if not path:
@ -5289,8 +5318,8 @@ class LibvirtDriver(driver.ComputeDriver):
instance_name)
continue
if disk_type != 'file':
LOG.debug('skipping %s since it looks like volume', path)
if disk_type not in ['file', 'block']:
LOG.debug('skipping disk because it looks like a volume', path)
continue
if target in volume_devices:
@ -5300,7 +5329,10 @@ class LibvirtDriver(driver.ComputeDriver):
# get the real disk size or
# raise a localized error if image is unavailable
dk_size = int(os.path.getsize(path))
if disk_type == 'file':
dk_size = int(os.path.getsize(path))
elif disk_type == 'block':
dk_size = lvm.get_volume_size(path)
disk_type = driver_nodes[cnt].get('type')
if disk_type == "qcow2":

View File

@ -24,6 +24,7 @@ from nova import exception
from nova.i18n import _
from nova.i18n import _LE
from nova import image
from nova import keymgr
from nova.openstack.common import excutils
from nova.openstack.common import fileutils
from nova.openstack.common import jsonutils
@ -33,6 +34,7 @@ from nova import utils
from nova.virt.disk import api as disk
from nova.virt import images
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import dmcrypt
from nova.virt.libvirt import lvm
from nova.virt.libvirt import rbd_utils
from nova.virt.libvirt import utils as libvirt_utils
@ -69,6 +71,12 @@ CONF = cfg.CONF
CONF.register_opts(__imagebackend_opts, 'libvirt')
CONF.import_opt('image_cache_subdirectory_name', 'nova.virt.imagecache')
CONF.import_opt('preallocate_images', 'nova.virt.driver')
CONF.import_opt('enabled', 'nova.compute.api',
group='ephemeral_storage_encryption')
CONF.import_opt('cipher', 'nova.compute.api',
group='ephemeral_storage_encryption')
CONF.import_opt('key_size', 'nova.compute.api',
group='ephemeral_storage_encryption')
CONF.import_opt('rbd_user', 'nova.virt.libvirt.volume', group='libvirt')
CONF.import_opt('rbd_secret_uuid', 'nova.virt.libvirt.volume', group='libvirt')
@ -88,6 +96,12 @@ class Image(object):
:driver_format: raw or qcow2
:is_block_dev:
"""
if (CONF.ephemeral_storage_encryption.enabled and
not self._supports_encryption()):
raise exception.NovaException(_('Incompatible settings: '
'ephemeral storage encryption is supported '
'only for LVM images.'))
self.source_type = source_type
self.driver_format = driver_format
self.is_block_dev = is_block_dev
@ -103,6 +117,12 @@ class Image(object):
# are trying to create a base file at the same time
self.lock_path = os.path.join(CONF.instances_path, 'locks')
def _supports_encryption(self):
"""Used to test that the backend supports encryption.
Override in the subclass if backend supports encryption.
"""
return False
@abc.abstractmethod
def create_image(self, prepare_template, base, size, *args, **kwargs):
"""Create image from template.
@ -317,6 +337,7 @@ class Image(object):
class Raw(Image):
def __init__(self, instance=None, disk_name=None, path=None):
self.disk_name = disk_name
super(Raw, self).__init__("file", "raw", is_block_dev=False)
self.path = (path or
@ -331,6 +352,18 @@ class Raw(Image):
data = images.qemu_img_info(self.path)
return data.file_format or 'raw'
def _supports_encryption(self):
# NOTE(dgenin): Kernel, ramdisk and disk.config are fetched using
# the Raw backend regardless of which backend is configured for
# ephemeral storage. Encryption for the Raw backend is not yet
# implemented so this loophole is necessary to allow other
# backends already supporting encryption to function. This can
# be removed once encryption for Raw is implemented.
if self.disk_name not in ['kernel', 'ramdisk', 'disk.config']:
return False
else:
return True
def correct_format(self):
if os.path.exists(self.path):
self.driver_format = self.resolve_driver_format()
@ -436,11 +469,21 @@ class Lvm(Image):
def __init__(self, instance=None, disk_name=None, path=None):
super(Lvm, self).__init__("block", "raw", is_block_dev=True)
self.ephemeral_key_uuid = instance.get('ephemeral_key_uuid')
if self.ephemeral_key_uuid is not None:
self.key_manager = keymgr.API()
else:
self.key_manager = None
if path:
info = lvm.volume_info(path)
self.vg = info['VG']
self.lv = info['LV']
self.path = path
if self.ephemeral_key_uuid is None:
info = lvm.volume_info(path)
self.vg = info['VG']
self.lv = info['LV']
else:
self.vg = CONF.libvirt.images_volume_group
else:
if not CONF.libvirt.images_volume_group:
raise RuntimeError(_('You should specify'
@ -449,20 +492,32 @@ class Lvm(Image):
self.vg = CONF.libvirt.images_volume_group
self.lv = '%s_%s' % (instance['uuid'],
self.escape(disk_name))
self.path = os.path.join('/dev', self.vg, self.lv)
if self.ephemeral_key_uuid is None:
self.path = os.path.join('/dev', self.vg, self.lv)
else:
self.lv_path = os.path.join('/dev', self.vg, self.lv)
self.path = '/dev/mapper/' + dmcrypt.volume_name(self.lv)
# TODO(pbrady): possibly deprecate libvirt.sparse_logical_volumes
# for the more general preallocate_images
self.sparse = CONF.libvirt.sparse_logical_volumes
self.preallocate = not self.sparse
def _supports_encryption(self):
return True
def _can_fallocate(self):
return False
def create_image(self, prepare_template, base, size, *args, **kwargs):
filename = os.path.split(base)[-1]
def encrypt_lvm_image():
dmcrypt.create_volume(self.path.rpartition('/')[2],
self.lv_path,
CONF.ephemeral_storage_encryption.cipher,
CONF.ephemeral_storage_encryption.key_size,
key)
@utils.synchronized(filename, external=True, lock_path=self.lock_path)
@utils.synchronized(base, external=True, lock_path=self.lock_path)
def create_lvm_image(base, size):
base_size = disk.get_disk_size(base)
self.verify_base_size(base, size, base_size=base_size)
@ -470,17 +525,35 @@ class Lvm(Image):
size = size if resize else base_size
lvm.create_volume(self.vg, self.lv,
size, sparse=self.sparse)
if self.ephemeral_key_uuid is not None:
encrypt_lvm_image()
images.convert_image(base, self.path, 'raw', run_as_root=True)
if resize:
disk.resize2fs(self.path, run_as_root=True)
generated = 'ephemeral_size' in kwargs
if self.ephemeral_key_uuid is not None:
if 'context' in kwargs:
try:
# NOTE(dgenin): Key manager corresponding to the
# specific backend catches and reraises an
# an exception if key retrieval fails.
key = self.key_manager.get_key(kwargs['context'],
self.ephemeral_key_uuid).get_encoded()
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to retrieve ephemeral encryption"
" key"))
else:
raise exception.NovaException(
_("Instance disk to be encrypted but no context provided"))
# Generate images with specified size right on volume
if generated and size:
lvm.create_volume(self.vg, self.lv,
size, sparse=self.sparse)
with self.remove_volume_on_error(self.path):
if self.ephemeral_key_uuid is not None:
encrypt_lvm_image()
prepare_template(target=self.path, *args, **kwargs)
else:
if not os.path.exists(base):
@ -494,7 +567,11 @@ class Lvm(Image):
yield
except Exception:
with excutils.save_and_reraise_exception():
lvm.remove_volumes(path)
if self.ephemeral_key_uuid is None:
lvm.remove_volumes(path)
else:
dmcrypt.delete_volume(path.rpartition('/')[2])
lvm.remove_volumes(self.lv_path)
def snapshot_extract(self, target, out_format):
images.convert_image(self.path, target, out_format,
@ -655,16 +732,15 @@ class Backend(object):
:name: Image name.
:image_type: Image type.
Optional, is CONF.libvirt.images_type by default.
"""
backend = self.backend(image_type)
return backend(instance=instance, disk_name=disk_name)
def snapshot(self, disk_path, image_type=None):
def snapshot(self, instance, disk_path, image_type=None):
"""Returns snapshot for given image
:path: path to image
:image_type: type of image
"""
backend = self.backend(image_type)
return backend(path=disk_path)
return backend(instance=instance, path=disk_path)