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:
parent
844d0cafdb
commit
5fa74bc0b2
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue