diff --git a/nova/objects/migrate_data.py b/nova/objects/migrate_data.py index 24d26d97ce7b..76cb85213175 100644 --- a/nova/objects/migrate_data.py +++ b/nova/objects/migrate_data.py @@ -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'] diff --git a/nova/tests/unit/objects/test_migrate_data.py b/nova/tests/unit/objects/test_migrate_data.py index 3f1d3428dd64..39eb5e2e3628 100644 --- a/nova/tests/unit/objects/test_migrate_data.py +++ b/nova/tests/unit/objects/test_migrate_data.py @@ -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, diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index d881e3d25146..1d0053c9b3e9 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -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', diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 49bd689cad80..a58cd90958ec 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -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( diff --git a/nova/tests/unit/virt/libvirt/test_migration.py b/nova/tests/unit/virt/libvirt/test_migration.py index 9fb308105d48..e84740651013 100644 --- a/nova/tests/unit/virt/libvirt/test_migration.py +++ b/nova/tests/unit/virt/libvirt/test_migration.py @@ -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 = """ + + + + + + + + + + + + + d299a078-f0db-4993-bf03-f10fe44fd192 + +
+ + +""" + new_xml = """ + + + + + + + + + + + + + d299a078-f0db-4993-bf03-f10fe44fd192 + + + + +
+ + +""" % {'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 = """ + + + + + + + + + + + + + d299a078-f0db-4993-bf03-f10fe44fd192 + + + + +
+ + +""" % {'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']) diff --git a/nova/tests/unit/virt/libvirt/volume/test_volume.py b/nova/tests/unit/virt/libvirt/volume/test_volume.py index e0a7377b9057..c6bb8b93b237 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_volume.py +++ b/nova/tests/unit/virt/libvirt/volume/test_volume.py @@ -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")) diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index fc43e65005c7..1f921c1fefd0 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -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 diff --git a/nova/virt/libvirt/migration.py b/nova/virt/libvirt/migration.py index 4ba6ac2880b3..b530d4b2dff0 100644 --- a/nova/virt/libvirt/migration.py +++ b/nova/virt/libvirt/migration.py @@ -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') diff --git a/nova/virt/libvirt/volume/volume.py b/nova/virt/libvirt/volume/volume.py index 54577bc4ce39..9de99e67f3c2 100644 --- a/nova/virt/libvirt/volume/volume.py +++ b/nova/virt/libvirt/volume/volume.py @@ -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):