Merge "libvirt: QEMU native LUKS decryption for encrypted volumes"

This commit is contained in:
Zuul 2018-01-31 08:15:40 +00:00 committed by Gerrit Code Review
commit e9ae961d92
9 changed files with 566 additions and 25 deletions

View File

@ -68,7 +68,10 @@ class LiveMigrateData(obj_base.NovaObject):
@obj_base.NovaObjectRegistry.register
class LibvirtLiveMigrateBDMInfo(obj_base.NovaObject):
VERSION = '1.0'
# VERSION 1.0 : Initial version
# VERSION 1.1 : Added encryption_secret_uuid for tracking volume secret
# uuid created on dest during migration with encrypted vols.
VERSION = '1.1'
fields = {
# FIXME(danms): some of these can be enums?
@ -79,8 +82,16 @@ class LibvirtLiveMigrateBDMInfo(obj_base.NovaObject):
'format': fields.StringField(nullable=True),
'boot_index': fields.IntegerField(nullable=True),
'connection_info_json': fields.StringField(),
'encryption_secret_uuid': fields.UUIDField(nullable=True),
}
def obj_make_compatible(self, primitive, target_version):
super(LibvirtLiveMigrateBDMInfo, self).obj_make_compatible(
primitive, target_version)
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 1) and 'encryption_secret_uuid' in primitive:
del primitive['encryption_secret_uuid']
# NOTE(danms): We don't have a connection_info object right
# now, and instead mostly store/pass it as JSON that we're
# careful with. When we get a connection_info object in the
@ -115,7 +126,8 @@ class LibvirtLiveMigrateData(LiveMigrateData):
# serial console.
# Version 1.3: Added 'supported_perf_events'
# Version 1.4: Added old_vol_attachment_ids
VERSION = '1.4'
# Version 1.5: Added src_supports_native_luks
VERSION = '1.5'
fields = {
'filename': fields.StringField(),
@ -134,12 +146,16 @@ class LibvirtLiveMigrateData(LiveMigrateData):
'bdms': fields.ListOfObjectsField('LibvirtLiveMigrateBDMInfo'),
'target_connect_addr': fields.StringField(nullable=True),
'supported_perf_events': fields.ListOfStringsField(),
'src_supports_native_luks': fields.BooleanField(),
}
def obj_make_compatible(self, primitive, target_version):
super(LibvirtLiveMigrateData, self).obj_make_compatible(
primitive, target_version)
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 5):
if 'src_supports_native_luks' in primitive:
del primitive['src_supports_native_luks']
if target_version < (1, 4):
if 'old_vol_attachment_ids' in primitive:
del primitive['old_vol_attachment_ids']

View File

@ -225,11 +225,31 @@ class _TestLibvirtLiveMigrateData(object):
def test_obj_make_compatible(self):
obj = migrate_data.LibvirtLiveMigrateData(
old_vol_attachment_ids={uuids.volume: uuids.attachment})
src_supports_native_luks=True,
old_vol_attachment_ids={uuids.volume: uuids.attachment},
supported_perf_events=[],
serial_listen_addr='127.0.0.1',
target_connect_addr='127.0.0.1')
primitive = obj.obj_to_primitive(target_version='1.0')
self.assertNotIn('target_connect_addr', primitive)
self.assertNotIn('serial_listen_addr=', primitive)
self.assertNotIn('supported_perf_events', primitive)
self.assertNotIn('old_vol_attachment_ids', primitive)
self.assertNotIn('src_supports_native_luks', primitive)
primitive = obj.obj_to_primitive(target_version='1.1')
self.assertNotIn('serial_listen_addr=', primitive)
primitive = obj.obj_to_primitive(target_version='1.2')
self.assertNotIn('supported_perf_events', primitive)
primitive = obj.obj_to_primitive(target_version='1.3')
self.assertNotIn('old_vol_attachment_ids', primitive)
primitive = obj.obj_to_primitive(target_version='1.4')
self.assertNotIn('src_supports_native_luks', primitive)
def test_bdm_obj_make_compatible(self):
obj = migrate_data.LibvirtLiveMigrateBDMInfo(
encryption_secret_uuid=uuids.encryption_secret_uuid)
primitive = obj.obj_to_primitive(target_version='1.0')
self.assertNotIn('encryption_secret_uuid', primitive)
class TestLibvirtLiveMigrateData(test_objects._LocalTest,

View File

@ -1118,8 +1118,8 @@ object_data = {
'InstanceNUMATopology': '1.3-ec0030cb0402a49c96da7051c037082a',
'InstancePCIRequest': '1.2-6344dd8bd1bf873e7325c07afe47f774',
'InstancePCIRequests': '1.1-65e38083177726d806684cb1cc0136d2',
'LibvirtLiveMigrateBDMInfo': '1.0-252aabb723ca79d5469fa56f64b57811',
'LibvirtLiveMigrateData': '1.4-ae5f344e7f78d3b45c259a0f80ea69f5',
'LibvirtLiveMigrateBDMInfo': '1.1-5f4a68873560b6f834b74e7861d71aaf',
'LibvirtLiveMigrateData': '1.5-26f8beff5fe9489efe3dfd3ab7a9eaec',
'KeyPair': '1.4-1244e8d1b103cc69d038ed78ab3a8cc6',
'KeyPairList': '1.3-94aad3ac5c938eef4b5e83da0212f506',
'MemoryDiagnostics': '1.0-2c995ae0f2223bb0f8e523c5cc0b83da',

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import binascii
from collections import deque
from collections import OrderedDict
import contextlib
@ -28,6 +29,7 @@ import signal
import threading
import time
from castellan import key_manager
import ddt
import eventlet
from eventlet import greenthread
@ -3471,7 +3473,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
instance_ref = objects.Instance(**self.test_instance)
image_meta = objects.ImageMeta.from_dict(self.test_image_meta)
conn_info = {'driver_volume_type': 'fake'}
conn_info = {'driver_volume_type': 'fake', 'data': {}}
bdms = block_device_obj.block_device_make_list_from_dicts(
self.context, [
fake_block_device.FakeDbBlockDeviceDict(
@ -3514,7 +3516,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
instance_ref = objects.Instance(**self.test_instance)
image_meta = objects.ImageMeta.from_dict(self.test_image_meta)
conn_info = {'driver_volume_type': 'fake'}
conn_info = {'driver_volume_type': 'fake', 'data': {}}
bdms = block_device_obj.block_device_make_list_from_dicts(
self.context, [
fake_block_device.FakeDbBlockDeviceDict(
@ -3631,7 +3633,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
"properties": {"hw_scsi_model": "virtio-scsi",
"hw_disk_bus": "scsi"}})
instance_ref = objects.Instance(**self.test_instance)
conn_info = {'driver_volume_type': 'fake'}
conn_info = {'driver_volume_type': 'fake', 'data': {}}
bdms = block_device_obj.block_device_make_list_from_dicts(
self.context, [
fake_block_device.FakeDbBlockDeviceDict(
@ -6595,6 +6597,113 @@ class LibvirtConnTestCase(test.NoDBTestCase,
_set_cache_mode.assert_called_once_with(config)
self.assertEqual(config_guest_disk.to_xml(), config.to_xml())
@mock.patch.object(key_manager, 'API')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_volume_encryption')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_use_native_luks')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_volume_encryptor')
@mock.patch('nova.virt.libvirt.host.Host')
@mock.patch('os_brick.encryptors.luks.is_luks')
def test_connect_volume_native_luks(self, mock_is_luks, mock_host,
mock_get_volume_encryptor, mock_use_native_luks,
mock_get_volume_encryption, mock_get_key_mgr):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
connection_info = {'driver_volume_type': 'fake',
'data': {'device_path': '/fake',
'access_mode': 'rw',
'volume_id': uuids.volume_id}}
encryption = {'provider': encryptors.LUKS,
'encryption_key_id': uuids.encryption_key_id}
instance = mock.sentinel.instance
# Mock out the encryptors
mock_encryptor = mock.Mock()
mock_get_volume_encryptor.return_value = mock_encryptor
mock_is_luks.return_value = True
# Mock out the key manager
key = u'3734363537333734'
key_encoded = binascii.unhexlify(key)
mock_key = mock.Mock()
mock_key_mgr = mock.Mock()
mock_get_key_mgr.return_value = mock_key_mgr
mock_key_mgr.get.return_value = mock_key
mock_key.get_encoded.return_value = key_encoded
# assert that the secret is created for the encrypted volume during
# _connect_volume when use_native_luks is True
mock_get_volume_encryption.return_value = encryption
mock_use_native_luks.return_value = True
drvr._connect_volume(self.context, connection_info, instance,
encryption=encryption)
drvr._host.create_secret.assert_called_once_with('volume',
uuids.volume_id, password=key)
mock_encryptor.attach_volume.assert_not_called()
# assert that the encryptor is used if use_native_luks is False
drvr._host.create_secret.reset_mock()
mock_get_volume_encryption.reset_mock()
mock_use_native_luks.return_value = False
drvr._connect_volume(self.context, connection_info, instance,
encryption=encryption)
drvr._host.create_secret.assert_not_called()
mock_encryptor.attach_volume.assert_called_once_with(self.context,
**encryption)
# assert that we format the volume if is_luks is False
mock_use_native_luks.return_value = True
mock_is_luks.return_value = False
drvr._connect_volume(self.context, connection_info, instance,
encryption=encryption)
mock_encryptor._format_volume.assert_called_once_with(key,
**encryption)
# assert that os-brick is used when allow_native_luks is False
mock_encryptor.attach_volume.reset_mock()
mock_is_luks.return_value = True
drvr._connect_volume(self.context, connection_info, instance,
encryption=encryption, allow_native_luks=False)
mock_encryptor.attach_volume.assert_called_once_with(self.context,
**encryption)
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_volume_encryptor')
def test_disconnect_volume_native_luks(self, mock_get_volume_encryptor):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
drvr._host = mock.Mock()
drvr._host.find_secret.return_value = mock.Mock()
connection_info = {'driver_volume_type': 'fake',
'data': {'device_path': '/fake',
'access_mode': 'rw',
'volume_id': uuids.volume_id}}
encryption = {'provider': encryptors.LUKS,
'encryption_key_id': uuids.encryption_key_id}
instance = mock.sentinel.instance
# Mock out the encryptors
mock_encryptor = mock.Mock()
mock_get_volume_encryptor.return_value = mock_encryptor
# assert that a secret is deleted if found
drvr._disconnect_volume(self.context, connection_info, instance)
drvr._host.delete_secret.assert_called_once_with('volume',
uuids.volume_id)
mock_encryptor.detach_volume.assert_not_called()
# assert that the encryptor is used if no secret is found
drvr._host.find_secret.reset_mock()
drvr._host.delete_secret.reset_mock()
drvr._host.find_secret.return_value = None
drvr._disconnect_volume(self.context, connection_info, instance,
encryption=encryption)
drvr._host.delete_secret.assert_not_called()
mock_encryptor.detach_volume.called_once_with(self.context,
**encryption)
def test_attach_invalid_volume_type(self):
self.create_fake_libvirt_mock()
libvirt_driver.LibvirtDriver._conn.lookupByUUIDString \
@ -6930,7 +7039,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
connection_info = {'data': {}}
drvr._attach_encryptor(self.context, connection_info, None)
drvr._attach_encryptor(self.context, connection_info, None, False)
mock_get_metadata.assert_not_called()
mock_get_encryptor.assert_not_called()
@ -6948,7 +7057,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
connection_info = {'data': {'volume_id': uuids.volume_id}}
mock_get_metadata.return_value = encryption
drvr._attach_encryptor(self.context, connection_info, None)
drvr._attach_encryptor(self.context, connection_info, None, False)
mock_get_metadata.assert_called_once_with(self.context,
drvr._volume_api, uuids.volume_id, connection_info)
@ -6966,8 +7075,8 @@ class LibvirtConnTestCase(test.NoDBTestCase,
encryption = {}
connection_info = {'data': {'volume_id': uuids.volume_id}}
drvr._attach_encryptor(self.context, connection_info,
encryption=encryption)
drvr._attach_encryptor(self.context, connection_info, encryption,
False)
mock_get_metadata.assert_not_called()
mock_get_encryptor.assert_not_called()
@ -6986,7 +7095,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
mock_get_metadata.return_value = encryption
connection_info = {'data': {'volume_id': uuids.volume_id}}
drvr._attach_encryptor(self.context, connection_info, None)
drvr._attach_encryptor(self.context, connection_info, None, False)
mock_get_metadata.assert_called_once_with(self.context,
drvr._volume_api, uuids.volume_id, connection_info)
@ -7010,7 +7119,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
connection_info = {'data': {'volume_id': uuids.volume_id}}
drvr._attach_encryptor(self.context, connection_info,
encryption=encryption)
encryption, False)
mock_get_metadata.assert_not_called()
mock_get_encryptor.assert_called_once_with(connection_info,
@ -7111,6 +7220,46 @@ class LibvirtConnTestCase(test.NoDBTestCase,
encryption)
mock_encryptor.detach_volume.assert_called_once_with(**encryption)
@mock.patch.object(host.Host, "has_min_version")
def test_use_native_luks(self, mock_has_min_version):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
# True only when the required QEMU and Libvirt versions are available
# on the host and a valid LUKS provider is present within the
# encryption metadata dict.
mock_has_min_version.return_value = True
self.assertFalse(drvr._use_native_luks({}))
self.assertFalse(drvr._use_native_luks({
'provider': 'nova.volume.encryptors.cryptsetup.CryptSetupEncryptor'
}))
self.assertFalse(drvr._use_native_luks({
'provider': 'CryptSetupEncryptor'}))
self.assertFalse(drvr._use_native_luks({
'provider': encryptors.PLAIN}))
self.assertTrue(drvr._use_native_luks({
'provider': 'nova.volume.encryptors.luks.LuksEncryptor'}))
self.assertTrue(drvr._use_native_luks({
'provider': 'LuksEncryptor'}))
self.assertTrue(drvr._use_native_luks({
'provider': encryptors.LUKS}))
# Always False when the required QEMU and Libvirt versions are not
# available on the host.
mock_has_min_version.return_value = False
self.assertFalse(drvr._use_native_luks({}))
self.assertFalse(drvr._use_native_luks({
'provider': 'nova.volume.encryptors.cryptsetup.CryptSetupEncryptor'
}))
self.assertFalse(drvr._use_native_luks({
'provider': 'CryptSetupEncryptor'}))
self.assertFalse(drvr._use_native_luks({
'provider': encryptors.PLAIN}))
self.assertFalse(drvr._use_native_luks({
'provider': 'nova.volume.encryptors.luks.LuksEncryptor'}))
self.assertFalse(drvr._use_native_luks({
'provider': 'LuksEncryptor'}))
self.assertFalse(drvr._use_native_luks({
'provider': encryptors.LUKS}))
def test_multi_nic(self):
network_info = _fake_network_info(self, 2)
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
@ -10306,8 +10455,23 @@ class LibvirtConnTestCase(test.NoDBTestCase,
target_ret = self._generate_target_ret('127.0.0.2')
self._test_pre_live_migration_works_correctly_mocked(target_ret)
def test_pre_live_migration_only_dest_supports_native_luks(self):
# Assert that allow_native_luks is False when src_supports_native_luks
# is missing from migrate data during a P to Q LM.
self._test_pre_live_migration_works_correctly_mocked(
src_supports_native_luks=None, dest_supports_native_luks=True,
allow_native_luks=False)
def test_pre_live_migration_only_src_supports_native_luks(self):
# Assert that allow_native_luks is False when dest_supports_native_luks
# is False due to unmet QEMU and Libvirt deps on the dest compute.
self._test_pre_live_migration_works_correctly_mocked(
src_supports_native_luks=True, dest_supports_native_luks=False,
allow_native_luks=False)
def _test_pre_live_migration_works_correctly_mocked(self,
target_ret=None):
target_ret=None, src_supports_native_luks=True,
dest_supports_native_luks=True, allow_native_luks=True):
# Creating testdata
vol = {'block_device_mapping': [
{'connection_info': {'serial': '12345', u'data':
@ -10329,6 +10493,8 @@ class LibvirtConnTestCase(test.NoDBTestCase,
return
self.stubs.Set(drvr, '_create_images_and_backing', fake_none)
self.stubs.Set(drvr, '_is_native_luks_available',
lambda: dest_supports_native_luks)
instance = objects.Instance(**self.test_instance)
c = context.get_admin_context()
@ -10340,7 +10506,8 @@ class LibvirtConnTestCase(test.NoDBTestCase,
).AndReturn(vol['block_device_mapping'])
self.mox.StubOutWithMock(drvr, "_connect_volume")
for v in vol['block_device_mapping']:
drvr._connect_volume(c, v['connection_info'], instance)
drvr._connect_volume(c, v['connection_info'], instance,
allow_native_luks=allow_native_luks)
self.mox.StubOutWithMock(drvr, 'plug_vifs')
drvr.plug_vifs(mox.IsA(instance), nw_info)
@ -10354,6 +10521,10 @@ class LibvirtConnTestCase(test.NoDBTestCase,
graphics_listen_addr_spice='127.0.0.1',
serial_listen_addr='127.0.0.1',
)
if src_supports_native_luks:
migrate_data.src_supports_native_luks = True
result = drvr.pre_live_migration(
c, instance, vol, nw_info, None,
migrate_data=migrate_data)
@ -10462,6 +10633,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
return
self.stubs.Set(drvr, '_create_images_and_backing', fake_none)
self.stubs.Set(drvr, '_is_native_luks_available', lambda: True)
class FakeNetworkInfo(object):
def fixed_ips(self):
@ -10472,7 +10644,8 @@ class LibvirtConnTestCase(test.NoDBTestCase,
# Creating mocks
self.mox.StubOutWithMock(drvr, "_connect_volume")
for v in vol['block_device_mapping']:
drvr._connect_volume(c, v['connection_info'], inst_ref)
drvr._connect_volume(c, v['connection_info'], inst_ref,
allow_native_luks=True)
self.mox.StubOutWithMock(drvr, 'plug_vifs')
drvr.plug_vifs(mox.IsA(inst_ref), nw_info)
self.mox.ReplayAll()
@ -10486,6 +10659,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
disk_available_mb=123,
image_type='qcow2',
filename='foo',
src_supports_native_luks=True,
)
ret = drvr.pre_live_migration(c, inst_ref, vol, nw_info, None,
migrate_data)
@ -15402,6 +15576,16 @@ class LibvirtConnTestCase(test.NoDBTestCase,
self.assertTrue(instance.cleaned)
save.assert_called_once_with()
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_volume_encryption')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_use_native_luks')
def test_swap_volume_native_luks_blocked(self, mock_use_native_luks,
mock_get_encryption):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI())
mock_get_encryption.return_value = {'provider': 'luks'}
mock_use_native_luks.return_value = True
self.assertRaises(NotImplementedError, drvr.swap_volume, self.context,
{}, {}, None, None, None)
@mock.patch('nova.virt.libvirt.guest.BlockDevice.is_job_complete',
return_value=True)
def _test_swap_volume(self, mock_is_job_complete, source_type,
@ -15541,6 +15725,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
old_connection_info,
instance)
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_volume_encryption')
@mock.patch('nova.virt.libvirt.guest.BlockDevice.rebase')
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._disconnect_volume')
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._connect_volume')
@ -15550,7 +15735,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
@mock.patch('nova.virt.libvirt.host.Host.write_instance_config')
def test_swap_volume_disconnect_new_volume_on_rebase_error(self,
write_config, get_guest, get_disk, get_volume_config,
connect_volume, disconnect_volume, rebase):
connect_volume, disconnect_volume, rebase, get_volume_encryption):
"""Assert that disconnect_volume is called for the new volume if an
error is encountered while rebasing
"""
@ -15558,6 +15743,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
instance = objects.Instance(**self.test_instance)
guest = libvirt_guest.Guest(mock.MagicMock())
get_guest.return_value = guest
get_volume_encryption.return_value = {}
exc = fakelibvirt.make_libvirtError(fakelibvirt.libvirtError,
'internal error', error_code=fakelibvirt.VIR_ERR_INTERNAL_ERROR)
rebase.side_effect = exc
@ -15571,6 +15757,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
disconnect_volume.assert_called_once_with(self.context,
mock.sentinel.new_connection_info, instance)
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_volume_encryption')
@mock.patch('nova.virt.libvirt.guest.BlockDevice.is_job_complete')
@mock.patch('nova.virt.libvirt.guest.BlockDevice.abort_job')
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._disconnect_volume')
@ -15581,7 +15768,8 @@ class LibvirtConnTestCase(test.NoDBTestCase,
@mock.patch('nova.virt.libvirt.host.Host.write_instance_config')
def test_swap_volume_disconnect_new_volume_on_pivot_error(self,
write_config, get_guest, get_disk, get_volume_config,
connect_volume, disconnect_volume, abort_job, is_job_complete):
connect_volume, disconnect_volume, abort_job, is_job_complete,
get_volume_encryption):
"""Assert that disconnect_volume is called for the new volume if an
error is encountered while pivoting to the new volume
"""
@ -15589,6 +15777,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
instance = objects.Instance(**self.test_instance)
guest = libvirt_guest.Guest(mock.MagicMock())
get_guest.return_value = guest
get_volume_encryption.return_value = {}
exc = fakelibvirt.make_libvirtError(fakelibvirt.libvirtError,
'internal error', error_code=fakelibvirt.VIR_ERR_INTERNAL_ERROR)
is_job_complete.return_value = True
@ -15905,7 +16094,7 @@ class LibvirtConnTestCase(test.NoDBTestCase,
instance_ref = objects.Instance(**ct_instance)
image_meta = objects.ImageMeta.from_dict(self.test_image_meta)
conn_info = {'driver_volume_type': 'fake'}
conn_info = {'driver_volume_type': 'fake', 'data': {}}
bdm = objects.BlockDeviceMapping(
self.context,
**fake_block_device.FakeDbBlockDeviceDict(

View File

@ -24,6 +24,7 @@ from nova import objects
from nova import test
from nova.tests.unit import matchers
from nova.tests.unit.virt.libvirt import fakelibvirt
from nova.tests import uuidsentinel as uuids
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import guest as libvirt_guest
from nova.virt.libvirt import host
@ -314,6 +315,163 @@ class UtilityMigrationTestCase(test.NoDBTestCase):
'sdc')
self.assertThat(res, matchers.XMLMatches(new_xml))
def test_update_volume_xml_add_encryption(self):
connection_info = {
'driver_volume_type': 'rbd',
'serial': 'd299a078-f0db-4993-bf03-f10fe44fd192',
'data': {
'access_mode': 'rw',
'secret_type': 'ceph',
'name': 'cinder-volumes/volume-d299a078',
'encrypted': False,
'discard': True,
'cluster_name': 'ceph',
'secret_uuid': '1a790a26-dd49-4825-8d16-3dd627cf05a9',
'qos_specs': None,
'auth_enabled': True,
'volume_id': 'd299a078-f0db-4993-bf03-f10fe44fd192',
'hosts': ['172.16.128.101', '172.16.128.121'],
'auth_username': 'cinder',
'ports': ['6789', '6789', '6789']}}
bdm = objects.LibvirtLiveMigrateBDMInfo(
serial='d299a078-f0db-4993-bf03-f10fe44fd192',
bus='scsi', type='disk', dev='sdb',
connection_info=connection_info,
encryption_secret_uuid=uuids.encryption_secret_uuid)
data = objects.LibvirtLiveMigrateData(
target_connect_addr=None,
bdms=[bdm],
block_migration=False)
xml = """<domain>
<devices>
<disk type='network' device='disk'>
<driver name='qemu' type='raw' cache='writeback' discard='unmap'/>
<auth username='cinder'>
<secret type='ceph' uuid='1a790a26-dd49-4825-8d16-3dd627cf05a9'/>
</auth>
<source protocol='rbd' name='cinder-volumes/volume-d299a078'>
<host name='172.16.128.101' port='6789'/>
<host name='172.16.128.121' port='6789'/>
</source>
<backingStore/>
<target dev='sdb' bus='scsi'/>
<serial>d299a078-f0db-4993-bf03-f10fe44fd192</serial>
<alias name='scsi0-0-0-1'/>
<address type='drive' controller='0' bus='0' target='0' unit='1'/>
</disk>
</devices>
</domain>"""
new_xml = """<domain>
<devices>
<disk type='network' device='disk'>
<driver name='qemu' type='raw' cache='writeback' discard='unmap'/>
<auth username='cinder'>
<secret type='ceph' uuid='1a790a26-dd49-4825-8d16-3dd627cf05a9'/>
</auth>
<source protocol='rbd' name='cinder-volumes/volume-d299a078'>
<host name='172.16.128.101' port='6789'/>
<host name='172.16.128.121' port='6789'/>
</source>
<backingStore/>
<target dev='sdb' bus='scsi'/>
<serial>d299a078-f0db-4993-bf03-f10fe44fd192</serial>
<alias name='scsi0-0-0-1'/>
<encryption format='luks'>
<secret type='passphrase' uuid='%(encryption_secret_uuid)s'/>
</encryption>
<address type='drive' controller='0' bus='0' target='0' unit='1'/>
</disk>
</devices>
</domain>""" % {'encryption_secret_uuid': uuids.encryption_secret_uuid}
conf = vconfig.LibvirtConfigGuestDisk()
conf.source_device = bdm.type
conf.driver_name = "qemu"
conf.driver_format = "raw"
conf.driver_cache = "writeback"
conf.target_dev = bdm.dev
conf.target_bus = bdm.bus
conf.serial = bdm.connection_info.get('serial')
conf.source_type = "network"
conf.driver_discard = 'unmap'
conf.device_addr = vconfig.LibvirtConfigGuestDeviceAddressDrive()
conf.device_addr.controller = 0
get_volume_config = mock.MagicMock(return_value=conf)
doc = etree.fromstring(xml)
res = etree.tostring(migration._update_volume_xml(
doc, data, get_volume_config), encoding='unicode')
self.assertThat(res, matchers.XMLMatches(new_xml))
def test_update_volume_xml_update_encryption(self):
connection_info = {
'driver_volume_type': 'rbd',
'serial': 'd299a078-f0db-4993-bf03-f10fe44fd192',
'data': {
'access_mode': 'rw',
'secret_type': 'ceph',
'name': 'cinder-volumes/volume-d299a078',
'encrypted': False,
'discard': True,
'cluster_name': 'ceph',
'secret_uuid': '1a790a26-dd49-4825-8d16-3dd627cf05a9',
'qos_specs': None,
'auth_enabled': True,
'volume_id': 'd299a078-f0db-4993-bf03-f10fe44fd192',
'hosts': ['172.16.128.101', '172.16.128.121'],
'auth_username': 'cinder',
'ports': ['6789', '6789', '6789']}}
bdm = objects.LibvirtLiveMigrateBDMInfo(
serial='d299a078-f0db-4993-bf03-f10fe44fd192',
bus='scsi', type='disk', dev='sdb',
connection_info=connection_info,
encryption_secret_uuid=uuids.encryption_secret_uuid_new)
data = objects.LibvirtLiveMigrateData(
target_connect_addr=None,
bdms=[bdm],
block_migration=False)
xml = """<domain>
<devices>
<disk type='network' device='disk'>
<driver name='qemu' type='raw' cache='writeback' discard='unmap'/>
<auth username='cinder'>
<secret type='ceph' uuid='1a790a26-dd49-4825-8d16-3dd627cf05a9'/>
</auth>
<source protocol='rbd' name='cinder-volumes/volume-d299a078'>
<host name='172.16.128.101' port='6789'/>
<host name='172.16.128.121' port='6789'/>
</source>
<backingStore/>
<target dev='sdb' bus='scsi'/>
<serial>d299a078-f0db-4993-bf03-f10fe44fd192</serial>
<alias name='scsi0-0-0-1'/>
<encryption format='luks'>
<secret type='passphrase' uuid='%(encryption_secret_uuid)s'/>
</encryption>
<address type='drive' controller='0' bus='0' target='0' unit='1'/>
</disk>
</devices>
</domain>""" % {'encryption_secret_uuid': uuids.encryption_secret_uuid_old}
conf = vconfig.LibvirtConfigGuestDisk()
conf.source_device = bdm.type
conf.driver_name = "qemu"
conf.driver_format = "raw"
conf.driver_cache = "writeback"
conf.target_dev = bdm.dev
conf.target_bus = bdm.bus
conf.serial = bdm.connection_info.get('serial')
conf.source_type = "network"
conf.driver_discard = 'unmap'
conf.device_addr = vconfig.LibvirtConfigGuestDeviceAddressDrive()
conf.device_addr.controller = 0
get_volume_config = mock.MagicMock(return_value=conf)
doc = etree.fromstring(xml)
res = etree.tostring(migration._update_volume_xml(
doc, data, get_volume_config), encoding='unicode')
new_xml = xml.replace(uuids.encryption_secret_uuid_old,
uuids.encryption_secret_uuid_new)
self.assertThat(res, matchers.XMLMatches(new_xml))
def test_update_perf_events_xml(self):
data = objects.LibvirtLiveMigrateData(
supported_perf_events=['cmt'])

View File

@ -18,6 +18,7 @@ import mock
from nova import exception
from nova import test
from nova.tests.unit.virt.libvirt import fakelibvirt
from nova.tests import uuidsentinel as uuids
from nova.virt import fake
from nova.virt.libvirt import driver
from nova.virt.libvirt import host
@ -330,3 +331,45 @@ class LibvirtVolumeTestCase(LibvirtISCSIVolumeBaseTestCase):
conf = libvirt_driver.get_config(connection_info, self.disk_info)
tree = conf.format_dom()
self.assertIsNone(tree.find("driver[@discard]"))
def test_libvirt_volume_driver_encryption(self):
fake_secret = FakeSecret()
fake_host = mock.Mock(spec=host.Host)
fake_host.find_secret.return_value = fake_secret
libvirt_driver = volume.LibvirtVolumeDriver(fake_host)
connection_info = {
'driver_volume_type': 'fake',
'data': {
'volume_id': uuids.volume_id,
'device_path': '/foo',
'discard': False,
},
'serial': 'fake_serial',
}
conf = libvirt_driver.get_config(connection_info, self.disk_info)
tree = conf.format_dom()
encryption = tree.find("encryption")
secret = encryption.find("secret")
self.assertEqual('luks', encryption.attrib['format'])
self.assertEqual('passphrase', secret.attrib['type'])
self.assertEqual(SECRET_UUID, secret.attrib['uuid'])
def test_libvirt_volume_driver_encryption_missing_secret(self):
fake_host = mock.Mock(spec=host.Host)
fake_host.find_secret.return_value = None
libvirt_driver = volume.LibvirtVolumeDriver(fake_host)
connection_info = {
'driver_volume_type': 'fake',
'data': {
'volume_id': uuids.volume_id,
'device_path': '/foo',
'discard': False,
},
'serial': 'fake_serial',
}
conf = libvirt_driver.get_config(connection_info, self.disk_info)
tree = conf.format_dom()
self.assertIsNone(tree.find("encryption"))

View File

@ -25,6 +25,7 @@ Supports KVM, LXC, QEMU, UML, XEN and Parallels.
"""
import binascii
import collections
from collections import deque
import contextlib
@ -46,6 +47,7 @@ from eventlet import greenthread
from eventlet import tpool
from lxml import etree
from os_brick import encryptors
from os_brick.encryptors import luks as luks_encryptor
from os_brick import exception as brick_exception
from os_brick.initiator import connector
from oslo_concurrency import processutils
@ -304,6 +306,9 @@ MIN_LIBVIRT_MDEV_SUPPORT = (3, 4, 0)
# for details.
MIN_LIBVIRT_MULTIATTACH = (3, 10, 0)
MIN_LIBVIRT_LUKS_VERSION = (2, 2, 0)
MIN_QEMU_LUKS_VERSION = (2, 6, 0)
VGPU_RESOURCE_SEMAPHORE = "vgpu_resources"
@ -647,6 +652,10 @@ class LibvirtDriver(driver.ComputeDriver):
return self._host.has_min_version(MIN_LIBVIRT_VIRTLOGD,
MIN_QEMU_VIRTLOGD)
def _is_native_luks_available(self):
return self._host.has_min_version(MIN_LIBVIRT_LUKS_VERSION,
MIN_QEMU_LUKS_VERSION)
def _handle_live_migration_post_copy(self, migration_flags):
if CONF.libvirt.live_migration_permit_post_copy:
if self._is_post_copy_available():
@ -1222,10 +1231,11 @@ class LibvirtDriver(driver.ComputeDriver):
return self.volume_drivers[driver_type]
def _connect_volume(self, context, connection_info, instance,
encryption=None):
encryption=None, allow_native_luks=True):
vol_driver = self._get_volume_driver(connection_info)
vol_driver.connect_volume(connection_info, instance)
self._attach_encryptor(context, connection_info, encryption=encryption)
self._attach_encryptor(context, connection_info, encryption,
allow_native_luks)
def _disconnect_volume(self, context, connection_info, instance,
encryption=None):
@ -1237,6 +1247,16 @@ class LibvirtDriver(driver.ComputeDriver):
vol_driver = self._get_volume_driver(connection_info)
return vol_driver.extend_volume(connection_info, instance)
def _use_native_luks(self, encryption=None):
"""Is LUKS the required provider and native QEMU LUKS available
"""
provider = None
if encryption:
provider = encryption.get('provider', None)
if provider in encryptors.LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP:
provider = encryptors.LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP[provider]
return provider == encryptors.LUKS and self._is_native_luks_available()
def _get_volume_config(self, connection_info, disk_info):
vol_driver = self._get_volume_driver(connection_info)
conf = vol_driver.get_config(connection_info, disk_info)
@ -1260,16 +1280,52 @@ class LibvirtDriver(driver.ComputeDriver):
self._volume_api, volume_id, connection_info)
return encryption
def _attach_encryptor(self, context, connection_info, encryption):
def _attach_encryptor(self, context, connection_info, encryption,
allow_native_luks):
"""Attach the frontend encryptor if one is required by the volume.
The request context is only used when an encryption metadata dict is
not provided. The encryption metadata dict being populated is then used
to determine if an attempt to attach the encryptor should be made.
If native LUKS decryption is enabled then create a Libvirt volume
secret containing the LUKS passphrase for the volume.
"""
if encryption is None:
encryption = self._get_volume_encryption(context, connection_info)
if encryption:
if (encryption and allow_native_luks and
self._use_native_luks(encryption)):
# NOTE(lyarwood): Fetch the associated key for the volume and
# decode the passphrase from the key.
# FIXME(lyarwood): c-vol currently creates symmetric keys for use
# with volumes, leading to the binary to hex to string conversion
# below.
keymgr = key_manager.API(CONF)
key = keymgr.get(context, encryption['encryption_key_id'])
key_encoded = key.get_encoded()
passphrase = binascii.hexlify(key_encoded).decode('utf-8')
# NOTE(lyarwood): Retain the behaviour of the original os-brick
# encryptors and format any volume that does not identify as
# encrypted with LUKS.
# FIXME(lyarwood): Remove this once c-vol correctly formats
# encrypted volumes during their initial creation:
# https://bugs.launchpad.net/cinder/+bug/1739442
device_path = connection_info.get('data').get('device_path')
if device_path:
root_helper = utils.get_root_helper()
if not luks_encryptor.is_luks(root_helper, device_path):
encryptor = self._get_volume_encryptor(connection_info,
encryption)
encryptor._format_volume(passphrase, **encryption)
# NOTE(lyarwood): Store the passphrase as a libvirt secret locally
# on the compute node. This secret is used later when generating
# the volume config.
volume_id = connection_info.get('data', {}).get('volume_id')
self._host.create_secret('volume', volume_id, password=passphrase)
elif encryption:
encryptor = self._get_volume_encryptor(connection_info,
encryption)
encryptor.attach_volume(context, **encryption)
@ -1280,7 +1336,13 @@ class LibvirtDriver(driver.ComputeDriver):
The request context is only used when an encryption metadata dict is
not provided. The encryption metadata dict being populated is then used
to determine if an attempt to detach the encryptor should be made.
If native LUKS decryption is enabled then delete previously created
Libvirt volume secret from the host.
"""
volume_id = connection_info.get('data', {}).get('volume_id')
if volume_id and self._host.find_secret('volume', volume_id):
return self._host.delete_secret('volume', volume_id)
if encryption is None:
encryption = self._get_volume_encryption(context, connection_info)
if encryption:
@ -1423,6 +1485,12 @@ class LibvirtDriver(driver.ComputeDriver):
def swap_volume(self, context, old_connection_info,
new_connection_info, instance, mountpoint, resize_to):
# NOTE(lyarwood): https://bugzilla.redhat.com/show_bug.cgi?id=760547
encryption = self._get_volume_encryption(context, old_connection_info)
if encryption and self._use_native_luks(encryption):
raise NotImplementedError(_("Swap volume is not supported for"
"encrypted volumes when native LUKS decryption is enabled."))
guest = self._host.get_guest(instance)
disk_dev = mountpoint.rpartition("/")[2]
@ -6389,6 +6457,14 @@ class LibvirtDriver(driver.ComputeDriver):
relative=True)
dest_check_data.instance_relative_path = instance_path
# NOTE(lyarwood): Used to indicate to the dest that the src is capable
# of wiring up the encrypted disk configuration for the domain.
# Note that this does not require the QEMU and Libvirt versions to
# decrypt LUKS to be installed on the source node. Only the Nova
# utility code to generate the correct XML is required, so we can
# default to True here for all computes >= Queens.
dest_check_data.src_supports_native_luks = True
return dest_check_data
def _is_shared_block_storage(self, instance, dest_check_data,
@ -7285,7 +7361,17 @@ class LibvirtDriver(driver.ComputeDriver):
for bdm in block_device_mapping:
connection_info = bdm['connection_info']
self._connect_volume(context, connection_info, instance)
# NOTE(lyarwood): Handle the P to Q LM during upgrade use case
# where an instance has encrypted volumes attached using the
# os-brick encryptors. Do not attempt to attach the encrypted
# volume using native LUKS decryption on the destionation.
src_native_luks = False
if migrate_data.obj_attr_is_set('src_supports_native_luks'):
src_native_luks = migrate_data.src_supports_native_luks
dest_native_luks = self._is_native_luks_available()
allow_native_luks = src_native_luks and dest_native_luks
self._connect_volume(context, connection_info, instance,
allow_native_luks=allow_native_luks)
# We call plug_vifs before the compute manager calls
# ensure_filtering_rules_for_instance, to ensure bridge is set up
@ -7341,6 +7427,13 @@ class LibvirtDriver(driver.ComputeDriver):
bdmi.type = disk_info['type']
bdmi.format = disk_info.get('format')
bdmi.boot_index = disk_info.get('boot_index')
volume_id = connection_info.get('volume_id')
volume_secret = None
if volume_id:
volume_secret = self._host.find_secret('volume', volume_id)
if volume_secret:
bdmi.encryption_secret_uuid = volume_secret.UUIDString()
migrate_data.bdms.append(bdmi)
return migrate_data

View File

@ -24,6 +24,7 @@ from oslo_log import log as logging
from nova.compute import power_state
import nova.conf
from nova.virt.libvirt import config as vconfig
LOG = logging.getLogger(__name__)
@ -148,6 +149,15 @@ def _update_volume_xml(xml_doc, migrate_data, get_volume_config):
continue
conf = get_volume_config(
bdm_info.connection_info, bdm_info.as_disk_info())
if bdm_info.obj_attr_is_set('encryption_secret_uuid'):
conf.encryption = vconfig.LibvirtConfigGuestDiskEncryption()
conf.encryption.format = 'luks'
secret = vconfig.LibvirtConfigGuestDiskEncryptionSecret()
secret.type = 'passphrase'
secret.uuid = bdm_info.encryption_secret_uuid
conf.encryption.secret = secret
xml_doc2 = etree.XML(conf.to_xml(), parser)
serial_dest = xml_doc2.findtext('serial')

View File

@ -109,6 +109,18 @@ class LibvirtBaseVolumeDriver(object):
# a shareable disk.
conf.shareable = True
volume_id = connection_info.get('data', {}).get('volume_id')
volume_secret = None
if volume_id:
volume_secret = self.host.find_secret('volume', volume_id)
if volume_secret:
conf.encryption = vconfig.LibvirtConfigGuestDiskEncryption()
secret = vconfig.LibvirtConfigGuestDiskEncryptionSecret()
secret.type = 'passphrase'
secret.uuid = volume_secret.UUIDString()
conf.encryption.format = 'luks'
conf.encryption.secret = secret
return conf
def connect_volume(self, connection_info, instance):