diff --git a/nova/conf/libvirt.py b/nova/conf/libvirt.py index d2f5a09dd968..dbbeb86edaf9 100644 --- a/nova/conf/libvirt.py +++ b/nova/conf/libvirt.py @@ -458,6 +458,44 @@ Determine the snapshot image format when sending to the image service. If set, this decides what format is used when sending the snapshot to the image service. If not set, defaults to same type as source image. +"""), + cfg.BoolOpt('live_migration_with_native_tls', + default=False, + help=""" + +This option will allow both migration stream (guest RAM plus device +state) *and* disk stream to be transported over native TLS, i.e. TLS +support built into QEMU. + +Prerequisite: TLS environment is configured correctly on all relevant +Compute nodes. This means, Certificate Authority (CA), server, client +certificates, their corresponding keys, and their file permisssions are +in place, and are validated. + +Notes: + +* To have encryption for migration stream and disk stream (also called: + "block migration"), ``live_migration_with_native_tls`` is the + preferred config attribute instead of ``live_migration_tunnelled``. + +* The ``live_migration_tunnelled`` will be deprecated in the long-term, + for two main reasons: (a) it incurs a huge performance penalty; and + it's not compatible with block migration. + +* The ``live_migration_tunnelled`` and + ``live_migration_with_native_tls`` should not be used at the same + time. + +* Unlike ``live_migration_tunnelled``, the + ``live_migration_with_native_tls`` *is* compatible with block + migration. That is, with this option, NBD stream, over which disks + are migrated to a target host, will be encrypted. + +Related options: + +``live_migration_tunnelled``: This transports migration stream (but not +disk stream) over libvirtd. + """), cfg.StrOpt('disk_prefix', help=""" diff --git a/nova/tests/unit/virt/libvirt/fakelibvirt.py b/nova/tests/unit/virt/libvirt/fakelibvirt.py index 9140594ad560..c335b9d2af29 100644 --- a/nova/tests/unit/virt/libvirt/fakelibvirt.py +++ b/nova/tests/unit/virt/libvirt/fakelibvirt.py @@ -98,6 +98,7 @@ VIR_MIGRATE_UNDEFINE_SOURCE = 16 VIR_MIGRATE_NON_SHARED_INC = 128 VIR_MIGRATE_AUTO_CONVERGE = 8192 VIR_MIGRATE_POSTCOPY = 32768 +VIR_MIGRATE_TLS = 65536 VIR_NODE_CPU_STATS_ALL_CPUS = -1 diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index a27a5ec86de4..3868742e8721 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -1284,6 +1284,22 @@ class LibvirtConnTestCase(test.NoDBTestCase, libvirt_driver.libvirt.VIR_MIGRATE_NON_SHARED_INC | libvirt_driver.libvirt.VIR_MIGRATE_TUNNELLED)) + @mock.patch.object(host.Host, 'has_min_version', return_value=True) + def test_live_migration_with_native_tls(self, host): + self.flags(live_migration_with_native_tls=True, group='libvirt') + self._do_test_parse_migration_flags( + lm_expected=(libvirt_driver.libvirt.VIR_MIGRATE_LIVE | + libvirt_driver.libvirt.VIR_MIGRATE_PEER2PEER | + libvirt_driver.libvirt.VIR_MIGRATE_UNDEFINE_SOURCE | + libvirt_driver.libvirt.VIR_MIGRATE_PERSIST_DEST | + libvirt_driver.libvirt.VIR_MIGRATE_TLS), + bm_expected=(libvirt_driver.libvirt.VIR_MIGRATE_LIVE | + libvirt_driver.libvirt.VIR_MIGRATE_PEER2PEER | + libvirt_driver.libvirt.VIR_MIGRATE_UNDEFINE_SOURCE | + libvirt_driver.libvirt.VIR_MIGRATE_PERSIST_DEST | + libvirt_driver.libvirt.VIR_MIGRATE_NON_SHARED_INC | + libvirt_driver.libvirt.VIR_MIGRATE_TLS)) + @mock.patch.object(host.Host, 'has_min_version', return_value=True) def test_live_migration_permit_postcopy_true(self, host): self.flags(live_migration_permit_post_copy=True, group='libvirt') @@ -10439,6 +10455,52 @@ class LibvirtConnTestCase(test.NoDBTestCase, drvr._live_migration_uri(target_connection), params=params, flags=expected_flags) + @mock.patch.object(host.Host, 'has_min_version', return_value=True) + @mock.patch.object(fakelibvirt.virDomain, "migrateToURI3") + @mock.patch('nova.virt.libvirt.migration.get_updated_guest_xml', + return_value='') + @mock.patch('nova.virt.libvirt.guest.Guest.get_xml_desc', return_value='') + def test_block_live_migration_native_tls_migrateToURI3( + self, mock_old_xml, mock_new_xml, + mock_migrateToURI3, mock_min_version): + self.flags(live_migration_with_native_tls=True, group='libvirt') + + target_connection = None + disk_paths = ['vda', 'vdb'] + + params = { + 'bandwidth': CONF.libvirt.live_migration_bandwidth, + 'migrate_disks': disk_paths + } + + # Start test + migrate_data = objects.LibvirtLiveMigrateData( + graphics_listen_addr_vnc='0.0.0.0', + graphics_listen_addr_spice='0.0.0.0', + serial_listen_addr='127.0.0.1', + target_connect_addr=target_connection, + bdms=[], + block_migration=True) + + dom = fakelibvirt.virDomain + guest = libvirt_guest.Guest(dom) + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + drvr._parse_migration_flags() + instance = objects.Instance(**self.test_instance) + drvr._live_migration_operation(self.context, instance, + target_connection, True, migrate_data, + guest, disk_paths) + + expected_flags = (fakelibvirt.VIR_MIGRATE_UNDEFINE_SOURCE | + fakelibvirt.VIR_MIGRATE_PERSIST_DEST | + fakelibvirt.VIR_MIGRATE_PEER2PEER | + fakelibvirt.VIR_MIGRATE_NON_SHARED_INC | + fakelibvirt.VIR_MIGRATE_TLS | + fakelibvirt.VIR_MIGRATE_LIVE) + mock_migrateToURI3.assert_called_once_with( + drvr._live_migration_uri(target_connection), + params=params, flags=expected_flags) + @mock.patch.object(host.Host, 'has_min_version', return_value=True) @mock.patch.object(fakelibvirt.virDomain, "migrateToURI3") @mock.patch('nova.virt.libvirt.guest.Guest.get_xml_desc', diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 2776aa3bf687..4a298616d971 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -288,6 +288,9 @@ MIN_QEMU_FILE_BACKED_VERSION = (2, 6, 0) MIN_LIBVIRT_FILE_BACKED_DISCARD_VERSION = (4, 4, 0) MIN_QEMU_FILE_BACKED_DISCARD_VERSION = (2, 10, 0) +MIN_LIBVIRT_NATIVE_TLS_VERSION = (4, 4, 0) +MIN_QEMU_NATIVE_TLS_VERSION = (2, 11, 0) + VGPU_RESOURCE_SEMAPHORE = "vgpu_resources" @@ -543,6 +546,20 @@ class LibvirtDriver(driver.ComputeDriver): 'libvirt_ver': libvirt_utils.version_to_string( MIN_LIBVIRT_OTHER_ARCH.get(kvm_arch))}) + # Allowing both "tunnelling via libvirtd" (which will be + # deprecated once the MIN_{LIBVIRT,QEMU}_VERSION is sufficiently + # new enough) and "native TLS" options at the same time is + # nonsensical. + if (CONF.libvirt.live_migration_tunnelled and + CONF.libvirt.live_migration_with_native_tls): + msg = _("Setting both 'live_migration_tunnelled' and" + "'live_migration_with_native_tls' at the same" + "time is invalid. If you have the relevant" + "libvirt and QEMU versions, and TLS configured" + "in your environment, pick" + "'live_migration_with_native_tls'.") + raise exception.Invalid(msg) + # TODO(sbauza): Remove this code once mediated devices are persisted # across reboots. if self._host.has_min_version(MIN_LIBVIRT_MDEV_SUPPORT): @@ -643,6 +660,16 @@ class LibvirtDriver(driver.ComputeDriver): migration_flags |= libvirt.VIR_MIGRATE_TUNNELLED return migration_flags + def _is_native_tls_available(self): + return self._host.has_min_version(MIN_LIBVIRT_NATIVE_TLS_VERSION, + MIN_QEMU_NATIVE_TLS_VERSION) + + def _handle_native_tls(self, migration_flags): + if (CONF.libvirt.live_migration_with_native_tls and + self._is_native_tls_available()): + migration_flags |= libvirt.VIR_MIGRATE_TLS + return migration_flags + def _is_post_copy_available(self): return self._host.has_min_version(lv_ver=MIN_LIBVIRT_POSTCOPY_VERSION) @@ -682,6 +709,11 @@ class LibvirtDriver(driver.ComputeDriver): block_migration_flags = self._handle_live_migration_tunnelled( block_migration_flags) + live_migration_flags = self._handle_native_tls( + live_migration_flags) + block_migration_flags = self._handle_native_tls( + block_migration_flags) + live_migration_flags = self._handle_live_migration_post_copy( live_migration_flags) block_migration_flags = self._handle_live_migration_post_copy( diff --git a/nova/virt/libvirt/guest.py b/nova/virt/libvirt/guest.py index 79251fa3e7d4..b4ea4a4ef27f 100644 --- a/nova/virt/libvirt/guest.py +++ b/nova/virt/libvirt/guest.py @@ -636,6 +636,7 @@ class Guest(object): VIR_MIGRATE_UNSAFE Force migration even if it is considered unsafe. VIR_MIGRATE_OFFLINE Migrate offline + VIR_MIGRATE_TLS Use QEMU-native TLS :param bandwidth: The maximum bandwidth in MiB/s """ params = {} diff --git a/releasenotes/notes/support-qemu-native-tls-for-migration-31d8b0ae9eb2c893.yaml b/releasenotes/notes/support-qemu-native-tls-for-migration-31d8b0ae9eb2c893.yaml new file mode 100644 index 000000000000..54913d05c40b --- /dev/null +++ b/releasenotes/notes/support-qemu-native-tls-for-migration-31d8b0ae9eb2c893.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + The libvirt driver now supports "QEMU-native TLS" transport for live + migration. This will provide encryption for all migration streams, + namely: guest RAM, device state and disks on a non-shared setup that are + transported over NBD (Network Block Device), also called as "block + migration". + + This can be configured via a new configuration attribute + ``[libvirt]/live_migration_with_native_tls``. Refer to its + documentation in ``nova.conf`` for usage details. Note that this is + the preferred the way to secure all migration streams in an + OpenStack network, instead of + ``[libvirt]/live_migration_tunnelled``.