diff --git a/nova/conf/libvirt.py b/nova/conf/libvirt.py index 8b1c148dbd2b..ac66d20f63a0 100644 --- a/nova/conf/libvirt.py +++ b/nova/conf/libvirt.py @@ -916,6 +916,28 @@ Related options: * ``compute_driver`` (libvirt) * ``virt_type`` (qemu) +"""), + + cfg.StrOpt('migration_inbound_addr', + default='$my_ip', + help=""" +The address used as the migration address for this host. + +This option indicates the IP address, hostname, or FQDN which should be used as +the target for cold migration, resize, and evacuate traffic when moving to this +hypervisor. This metadata is then used by the source of the migration traffic +to construct the commands used to copy data (e.g. disk image) to the +destination. + +An included "%s" is replaced with the hostname of the migration target +hypervisor. + + +Related options: + +* ``my_ip`` +* ``live_migration_inbound_addr`` + """), ] diff --git a/nova/tests/functional/libvirt/test_migration_addr.py b/nova/tests/functional/libvirt/test_migration_addr.py new file mode 100644 index 000000000000..3dabd8c31bde --- /dev/null +++ b/nova/tests/functional/libvirt/test_migration_addr.py @@ -0,0 +1,91 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from oslo_config import cfg + +from nova.tests.functional.libvirt import base + +CONF = cfg.CONF + + +class LibvirtMigrationAddrTest(base.ServersTestBase): + ADMIN_API = True + + def setUp(self): + super().setUp() + + def _test_move_op(self, move_op, migration_inbound_addr=None): + if migration_inbound_addr: + CONF.set_default( + "migration_inbound_addr", migration_inbound_addr, + group="libvirt") + + self.start_compute(hostname='compute1') + self.start_compute(hostname='compute2') + + server = self._create_server(host='compute1', networks="none") + + move_op(server) + + migration = self._wait_for_migration_status( + server, ["finished", "done"]) + + if migration_inbound_addr: + self.assertEqual(migration['dest_host'], "compute2") + else: + self.assertEqual(migration['dest_host'], CONF.my_ip) + + def _cold_migrate(self, server): + with mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver' + + '.migrate_disk_and_power_off', + return_value='{}' + ): + self._migrate_server(server) + + def _resize(self, server): + flavors = self.api.get_flavors() + + with mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver' + + '.migrate_disk_and_power_off', + return_value='{}' + ): + self._resize_server(server, flavors[1]['id']) + + def _evacuate(self, server): + service_id = self.admin_api.get_services( + host="compute1", binary='nova-compute')[0]['id'] + self.admin_api.put_service_force_down(service_id, True) + self._evacuate_server(server, expected_state='ACTIVE') + + def test_cold_migrate_with_ip(self): + self._test_move_op(self._cold_migrate, migration_inbound_addr=None) + + def test_cold_migrate_with_hostname(self): + self._test_move_op(self._cold_migrate, migration_inbound_addr="%s") + + def test_resize_with_ip(self): + self._test_move_op(self._resize, migration_inbound_addr=None) + + def test_resize_with_hostname(self): + self._test_move_op(self._resize, migration_inbound_addr="%s") + + def test_evacuate_with_ip(self): + self._test_move_op(self._evacuate, migration_inbound_addr=None) + + def test_evacuate_with_hostname(self): + self._test_move_op(self._evacuate, migration_inbound_addr="%s") diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index baabe14487b6..db07842abffc 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -16624,6 +16624,29 @@ class LibvirtConnTestCase(test.NoDBTestCase, ip = drvr.get_host_ip_addr() self.assertEqual(ip, CONF.my_ip) + def test_get_host_ip_addr_defaults_to_my_ip(self): + CONF.set_default("my_ip", "10.0.0.3") + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + ip = drvr.get_host_ip_addr() + self.assertEqual(ip, "10.0.0.3") + + def test_get_host_ip_addr_override_via_migration_inbound_addr(self): + CONF.set_default("my_ip", "10.0.0.3") + CONF.set_default( + "migration_inbound_addr", "my-migration-hostname", group='libvirt') + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + ip = drvr.get_host_ip_addr() + self.assertEqual(ip, "my-migration-hostname") + + def test_get_host_ip_addr_override_via_migration_inbound_addr_template( + self + ): + CONF.set_default("my_ip", "10.0.0.3") + CONF.set_default("migration_inbound_addr", "%s", group='libvirt') + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + ip = drvr.get_host_ip_addr() + self.assertEqual(ip, "compute1") + @mock.patch.object(libvirt_driver.LOG, 'warning') def test_check_my_ip(self, mock_log): @@ -19331,6 +19354,43 @@ class LibvirtConnTestCase(test.NoDBTestCase, mock_exists.assert_not_called() mock_unlink.assert_not_called() + @mock.patch.object(os, 'unlink') + @mock.patch.object(os.path, 'exists') + @mock.patch('oslo_concurrency.processutils.execute') + def test_shared_storage_detection_same_host_migration_inbound_addr( + self, mock_exec, mock_exists, mock_unlink + ): + CONF.set_default( + "migration_inbound_addr", "source-hostname", group="libvirt") + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + self.assertTrue(drvr._is_path_shared_with('source-hostname', '/path')) + mock_exec.assert_not_called() + mock_exists.assert_not_called() + mock_unlink.assert_not_called() + + @mock.patch.object(os.path, 'exists') + def test_shared_storage_detection_migration_inbound_addr( + self, mock_exists + ): + CONF.set_default( + "migration_inbound_addr", "source-hostname", group="libvirt") + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + mock_exists.return_value = False + with test.nested( + mock.patch.object(drvr._remotefs, 'create_file'), + mock.patch.object(drvr._remotefs, 'remove_file') + ) as (mock_rem_fs_create, mock_rem_fs_remove): + self.assertFalse( + drvr._is_path_shared_with('dest-hostname', '/path')) + + mock_rem_fs_create.assert_any_call('dest-hostname', mock.ANY) + create_args, create_kwargs = mock_rem_fs_create.call_args + self.assertTrue(create_args[1].startswith('/path')) + + mock_rem_fs_remove.assert_called_with('dest-hostname', mock.ANY) + remove_args, remove_kwargs = mock_rem_fs_remove.call_args + self.assertTrue(remove_args[1].startswith('/path')) + def test_store_pid_remove_pid(self): instance = objects.Instance(**self.test_instance) drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 863ac60af097..791cf00aa6fe 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -4512,7 +4512,15 @@ class LibvirtDriver(driver.ComputeDriver): return self._get_console_output_file(instance, console_path) def get_host_ip_addr(self): - return CONF.my_ip + # NOTE(gibi): We should rename this as we might return a hostname + # instead of an IP address. But this is a virt driver interface + # method, so it probably does not worth the hashle. Only the + # resource_tracker use this today outside the virt driver to set up + # the Migration object. + addr = CONF.libvirt.migration_inbound_addr + if "%s" in addr: + addr = addr % self._host.get_hostname() + return addr def get_vnc_console(self, context, instance): def get_vnc_port_for_instance(instance_name): @@ -11461,9 +11469,10 @@ class LibvirtDriver(driver.ComputeDriver): def _is_path_shared_with(self, dest, path): # NOTE (rmk): There are two methods of determining whether we are - # on the same filesystem: the source and dest IP are the - # same, or we create a file on the dest system via SSH - # and check whether the source system can also see it. + # on the same filesystem: the source and dest migration + # address are the same, or we create a file on the dest + # system via SSH and check whether the source system can + # also see it. shared_path = (dest == self.get_host_ip_addr()) if not shared_path: tmp_file = uuidutils.generate_uuid(dashed=False) + '.tmp' diff --git a/releasenotes/notes/bp-libvirt-migrate-with-hostname-instead-of-ip-98d42c25575590b1.yaml b/releasenotes/notes/bp-libvirt-migrate-with-hostname-instead-of-ip-98d42c25575590b1.yaml new file mode 100644 index 000000000000..e53a7f43563c --- /dev/null +++ b/releasenotes/notes/bp-libvirt-migrate-with-hostname-instead-of-ip-98d42c25575590b1.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + The new config option ``[libvirt]migration_inbound_addr`` is now used to + determine the address for incoming move operations (cold migrate, resize, + evacuate). This config is defaulted to [DEFAULT]my_ip to keep the + configuration backward compatible. However it allows an explicit hostname + or FQDN to be specified, or allows to specify '%s' that is then resolved to + the hostname of compute host. + Note that this config should only be changed from its default after every + compute is upgraded.