diff options
22 files changed, 553 insertions, 52 deletions
diff --git a/doc/source/admin/compute-node-identification.rst b/doc/source/admin/compute-node-identification.rst new file mode 100644 index 0000000000..31d4802d0b --- /dev/null +++ b/doc/source/admin/compute-node-identification.rst @@ -0,0 +1,83 @@ +=========================== +Compute Node Identification +=========================== + +Nova requires that compute nodes maintain a constant and consistent identity +during their lifecycle. With the exception of the ironic driver, starting in +the 2023.1 release, this is achieved by use of a file containing the node +unique identifier that is persisted on disk. Prior to 2023.1, a combination of +the compute node's hostname and the :oslo.config:option:`host` value in the +configuration file were used. + +The 2023.1 and later compute node identification file must remain unchanged +during the lifecycle of the compute node. Changing the value or removing the +file will result in a failure to start and may require advanced techniques +for recovery. The file is read once at `nova-compute`` startup, at which point +it is validated for formatting and the corresponding node is located or +created in the database. + +.. note:: + + Even after 2023.1, the compute node's hostname may not be changed after + the initial registration with the controller nodes, it is just not used + as the primary method for identification. + +The behavior of ``nova-compute`` is different when using the ironic driver, +as the (UUID-based) identity and mapping of compute nodes to compute manager +service hosts is dynamic. In that case, no single node identity is maintained +by the compute host and thus no identity file is read or written. Thus none +of the sections below apply to hosts with :oslo.config:option:`compute_driver` +set to `ironic`. + +Self-provisioning of the node identity +-------------------------------------- + +By default, ``nova-compute`` will automatically generate and write a UUID to +disk the first time it starts up, and will use that going forward as its +stable identity. Using the :oslo.config:option:`state_path` +(which is ``/var/lib/nova`` on most systems), a ``compute_id`` file will be +created with a generated UUID. + +Since this file (and it's parent directory) is writable by nova, it may be +desirable to move this to one of the other locations that nova looks for the +identification file. + +Deployment provisioning of the node identity +-------------------------------------------- + +In addition to the location mentioned above, nova will also search the parent +directories of any config file in use (either the defaults or provided on +the command line) for a ``compute_id`` file. Thus, a deployment tool may, on +most systems, pre-provision the node's UUID by writing one to +``/etc/nova/compute_id``. + +The contents of the file should be a single UUID in canonical textual +representation with no additional whitespace or other characters. The following +should work on most Linux systems: + +.. code-block:: shell + + $ uuidgen > /etc/nova/compute_id + +.. note:: + + **Do not** execute the above command blindly in every run of a deployment + tool, as that will result in overwriting the ``compute_id`` file each time, + which *will* prevent nova from working properly. + +Upgrading from pre-2023.1 +------------------------- + +Before release 2023.1, ``nova-compute`` only used the hostname (combined with +:oslo.config:option:`host`, if set) to identify its compute node objects in +the database. When upgrading from a prior release, the compute node will +perform a one-time migration of the hostname-matched compute node UUID to the +``compute_id`` file in the :oslo.config:option:`state_path` location. + +.. note:: + + It is imperative that you allow the above migration to run and complete on + compute nodes that are being upgraded. Skipping this step by + pre-provisioning a ``compute_id`` file before the upgrade will **not** work + and will be equivalent to changing the compute node UUID after it has + already been created once. diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst index 93b4e6a554..8cb5bf7156 100644 --- a/doc/source/admin/index.rst +++ b/doc/source/admin/index.rst @@ -206,6 +206,7 @@ instance for these kind of workloads. secure-boot sev managing-resource-providers + compute-node-identification resource-limits cpu-models libvirt-misc diff --git a/doc/source/admin/remote-console-access.rst b/doc/source/admin/remote-console-access.rst index 015c6522d0..9b28646d27 100644 --- a/doc/source/admin/remote-console-access.rst +++ b/doc/source/admin/remote-console-access.rst @@ -366,6 +366,16 @@ Replace ``IP_ADDRESS`` with the IP address from which the proxy is accessible by the outside world. For example, this may be the management interface IP address of the controller or the VIP. +Optionally, the :program:`nova-compute` service supports the following +additional options to configure compression settings (algorithms and modes) +for SPICE consoles. + +- :oslo.config:option:`spice.image_compression` +- :oslo.config:option:`spice.jpeg_compression` +- :oslo.config:option:`spice.zlib_compression` +- :oslo.config:option:`spice.playback_compression` +- :oslo.config:option:`spice.streaming_mode` + Serial ------ diff --git a/doc/source/cli/nova-compute.rst b/doc/source/cli/nova-compute.rst index f190949efa..1346dab92e 100644 --- a/doc/source/cli/nova-compute.rst +++ b/doc/source/cli/nova-compute.rst @@ -41,6 +41,8 @@ Files * ``/etc/nova/policy.d/`` * ``/etc/nova/rootwrap.conf`` * ``/etc/nova/rootwrap.d/`` +* ``/etc/nova/compute_id`` +* ``/var/lib/nova/compute_id`` See Also ======== diff --git a/nova/compute/manager.py b/nova/compute/manager.py index c4537cd9a2..952ab3e199 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -10480,6 +10480,14 @@ class ComputeManager(manager.Manager): LOG.exception( "Error updating PCI resources for node %(node)s.", {'node': nodename}) + except exception.InvalidConfiguration as e: + if startup: + # If this happens during startup, we need to let it raise to + # abort our service startup. + raise + else: + LOG.error("Error updating resources for node %s: %s", + nodename, e) except Exception: LOG.exception("Error updating resources for node %(node)s.", {'node': nodename}) diff --git a/nova/compute/resource_tracker.py b/nova/compute/resource_tracker.py index 70c56fd2e3..3f911f3708 100644 --- a/nova/compute/resource_tracker.py +++ b/nova/compute/resource_tracker.py @@ -728,7 +728,13 @@ class ResourceTracker(object): cn = objects.ComputeNode(context) cn.host = self.host self._copy_resources(cn, resources, initial=True) - cn.create() + try: + cn.create() + except exception.DuplicateRecord: + raise exception.InvalidConfiguration( + 'Duplicate compute node record found for host %s node %s' % ( + cn.host, cn.hypervisor_hostname)) + # Only map the ComputeNode into compute_nodes if create() was OK # because if create() fails, on the next run through here nodename # would be in compute_nodes and we won't try to create again (because diff --git a/nova/conf/spice.py b/nova/conf/spice.py index 59ed4e80a0..e5854946f1 100644 --- a/nova/conf/spice.py +++ b/nova/conf/spice.py @@ -85,6 +85,59 @@ Agent. With the Spice agent installed the following features are enabled: needing to click inside the console or press keys to release it. The performance of mouse movement is also improved. """), + cfg.StrOpt('image_compression', + advanced=True, + choices=[ + ('auto_glz', 'enable image compression mode to choose between glz ' + 'and quic algorithm, based on image properties'), + ('auto_lz', 'enable image compression mode to choose between lz ' + 'and quic algorithm, based on image properties'), + ('quic', 'enable image compression based on the SFALIC algorithm'), + ('glz', 'enable image compression using lz with history based ' + 'global dictionary'), + ('lz', 'enable image compression with the Lempel-Ziv algorithm'), + ('off', 'disable image compression') + ], + help=""" +Configure the SPICE image compression (lossless). +"""), + cfg.StrOpt('jpeg_compression', + advanced=True, + choices=[ + ('auto', 'enable JPEG image compression automatically'), + ('never', 'disable JPEG image compression'), + ('always', 'enable JPEG image compression') + ], + help=""" +Configure the SPICE wan image compression (lossy for slow links). +"""), + cfg.StrOpt('zlib_compression', + advanced=True, + choices=[ + ('auto', 'enable zlib image compression automatically'), + ('never', 'disable zlib image compression'), + ('always', 'enable zlib image compression') + ], + help=""" +Configure the SPICE wan image compression (lossless for slow links). +"""), + cfg.BoolOpt('playback_compression', + advanced=True, + help=""" +Enable the SPICE audio stream compression (using celt). +"""), + cfg.StrOpt('streaming_mode', + advanced=True, + choices=[ + ('filter', 'SPICE server adds additional filters to decide if ' + 'video streaming should be activated'), + ('all', 'any fast-refreshing window can be encoded into a video ' + 'stream'), + ('off', 'no video detection and (lossy) compression is performed') + ], + help=""" +Configure the SPICE video stream detection and (lossy) compression. +"""), cfg.URIOpt('html5proxy_base_url', default='http://127.0.0.1:6082/spice_auto.html', help=""" diff --git a/nova/exception.py b/nova/exception.py index 20c112b628..f5993e79f8 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2512,6 +2512,10 @@ class InvalidNodeConfiguration(NovaException): msg_fmt = _('Invalid node identity configuration: %(reason)s') +class DuplicateRecord(NovaException): + msg_fmt = _('Unable to create duplicate record for %(target)s') + + class NotSupportedComputeForEvacuateV295(NotSupported): msg_fmt = _("Starting to microversion 2.95, evacuate API will stop " "instance on destination. To evacuate before upgrades are " diff --git a/nova/objects/compute_node.py b/nova/objects/compute_node.py index 528cfc0776..dfc1b2ae28 100644 --- a/nova/objects/compute_node.py +++ b/nova/objects/compute_node.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_db import exception as db_exc from oslo_serialization import jsonutils from oslo_utils import uuidutils from oslo_utils import versionutils @@ -339,7 +340,12 @@ class ComputeNode(base.NovaPersistentObject, base.NovaObject): self._convert_supported_instances_to_db_format(updates) self._convert_pci_stats_to_db_format(updates) - db_compute = db.compute_node_create(self._context, updates) + try: + db_compute = db.compute_node_create(self._context, updates) + except db_exc.DBDuplicateEntry: + target = 'compute node %s:%s' % (updates['hypervisor_hostname'], + updates['uuid']) + raise exception.DuplicateRecord(target=target) self._from_db_object(self._context, self, db_compute) @base.remotable diff --git a/nova/scheduler/filters/__init__.py b/nova/scheduler/filters/__init__.py index 74e24b7bc3..785a13279e 100644 --- a/nova/scheduler/filters/__init__.py +++ b/nova/scheduler/filters/__init__.py @@ -16,8 +16,12 @@ """ Scheduler host filters """ +from oslo_log import log as logging + from nova import filters +LOG = logging.getLogger(__name__) + class BaseHostFilter(filters.BaseFilter): """Base class for host filters.""" @@ -53,6 +57,43 @@ class BaseHostFilter(filters.BaseFilter): raise NotImplementedError() +class CandidateFilterMixin: + """Mixing that helps to implement a Filter that needs to filter host by + Placement allocation candidates. + """ + + def filter_candidates(self, host_state, filter_func): + """Checks still viable allocation candidates by the filter_func and + keep only those that are passing it. + + :param host_state: HostState object holding the list of still viable + allocation candidates + :param filter_func: A callable that takes an allocation candidate and + returns a True like object if the candidate passed the filter or a + False like object if it doesn't. + """ + good_candidates = [] + for candidate in host_state.allocation_candidates: + LOG.debug( + f'{self.__class__.__name__} tries allocation candidate: ' + f'{candidate}', + ) + if filter_func(candidate): + LOG.debug( + f'{self.__class__.__name__} accepted allocation ' + f'candidate: {candidate}', + ) + good_candidates.append(candidate) + else: + LOG.debug( + f'{self.__class__.__name__} rejected allocation ' + f'candidate: {candidate}', + ) + + host_state.allocation_candidates = good_candidates + return good_candidates + + class HostFilterHandler(filters.BaseFilterHandler): def __init__(self): super(HostFilterHandler, self).__init__(BaseHostFilter) diff --git a/nova/scheduler/filters/numa_topology_filter.py b/nova/scheduler/filters/numa_topology_filter.py index 7ec9ca5648..ae50db90e5 100644 --- a/nova/scheduler/filters/numa_topology_filter.py +++ b/nova/scheduler/filters/numa_topology_filter.py @@ -20,7 +20,10 @@ from nova.virt import hardware LOG = logging.getLogger(__name__) -class NUMATopologyFilter(filters.BaseHostFilter): +class NUMATopologyFilter( + filters.BaseHostFilter, + filters.CandidateFilterMixin, +): """Filter on requested NUMA topology.""" # NOTE(sean-k-mooney): In change I0322d872bdff68936033a6f5a54e8296a6fb343 @@ -97,34 +100,19 @@ class NUMATopologyFilter(filters.BaseHostFilter): if network_metadata: limits.network_metadata = network_metadata - good_candidates = [] - for candidate in host_state.allocation_candidates: - LOG.debug( - 'NUMATopologyFilter tries allocation candidate: %s, %s', - candidate, requested_topology - ) - instance_topology = (hardware.numa_fit_instance_to_host( - host_topology, requested_topology, + good_candidates = self.filter_candidates( + host_state, + lambda candidate: hardware.numa_fit_instance_to_host( + host_topology, + requested_topology, limits=limits, pci_requests=pci_requests, pci_stats=host_state.pci_stats, - provider_mapping=candidate['mappings'], - )) - if instance_topology: - LOG.debug( - 'NUMATopologyFilter accepted allocation candidate: %s', - candidate - ) - good_candidates.append(candidate) - else: - LOG.debug( - 'NUMATopologyFilter rejected allocation candidate: %s', - candidate - ) - - host_state.allocation_candidates = good_candidates - - if not host_state.allocation_candidates: + provider_mapping=candidate["mappings"], + ), + ) + + if not good_candidates: LOG.debug("%(host)s, %(node)s fails NUMA topology " "requirements. The instance does not fit on this " "host.", {'host': host_state.host, diff --git a/nova/scheduler/filters/pci_passthrough_filter.py b/nova/scheduler/filters/pci_passthrough_filter.py index 36f0b5901c..992879072a 100644 --- a/nova/scheduler/filters/pci_passthrough_filter.py +++ b/nova/scheduler/filters/pci_passthrough_filter.py @@ -20,7 +20,10 @@ from nova.scheduler import filters LOG = logging.getLogger(__name__) -class PciPassthroughFilter(filters.BaseHostFilter): +class PciPassthroughFilter( + filters.BaseHostFilter, + filters.CandidateFilterMixin, +): """Pci Passthrough Filter based on PCI request Filter that schedules instances on a host if the host has devices @@ -54,28 +57,12 @@ class PciPassthroughFilter(filters.BaseHostFilter): {'host_state': host_state, 'requests': pci_requests}) return False - good_candidates = [] - for candidate in host_state.allocation_candidates: - LOG.debug( - 'PciPassthroughFilter tries allocation candidate: %s', - candidate - ) - if host_state.pci_stats.support_requests( - pci_requests.requests, - provider_mapping=candidate['mappings'] - ): - LOG.debug( - 'PciPassthroughFilter accepted allocation candidate: %s', - candidate - ) - good_candidates.append(candidate) - else: - LOG.debug( - 'PciPassthroughFilter rejected allocation candidate: %s', - candidate - ) - - host_state.allocation_candidates = good_candidates + good_candidates = self.filter_candidates( + host_state, + lambda candidate: host_state.pci_stats.support_requests( + pci_requests.requests, provider_mapping=candidate["mappings"] + ), + ) if not good_candidates: LOG.debug("%(host_state)s doesn't have the required PCI devices" diff --git a/nova/tests/fixtures/nova.py b/nova/tests/fixtures/nova.py index 5fd893e7dc..9a652c02cb 100644 --- a/nova/tests/fixtures/nova.py +++ b/nova/tests/fixtures/nova.py @@ -1822,6 +1822,24 @@ class ImportModulePoisonFixture(fixtures.Fixture): def find_spec(self, fullname, path, target=None): if fullname in self.modules: + current = eventlet.getcurrent() + # NOTE(gibi) not all eventlet spawn is under our control, so + # there can be senders without test_case_id set, find the first + # ancestor that was spawned from nova.utils.spawn[_n] and + # therefore has the id set. + while ( + current is not None and + not getattr(current, 'test_case_id', None) + ): + current = current.parent + + if current is not None: + self.test.tc_id = current.test_case_id + LOG.warning( + "!!!---!!! TestCase ID %s hit the import poison while " + "importing %s. If you see this in a failed functional " + "test then please let #openstack-nova on IRC know " + "about it. !!!---!!!", current.test_case_id, fullname) self.test.fail_message = ( f"This test imports the '{fullname}' module, which it " f'should not in the test environment. Please add ' @@ -1832,6 +1850,7 @@ class ImportModulePoisonFixture(fixtures.Fixture): def __init__(self, module_names): self.module_names = module_names self.fail_message = '' + self.tc_id = None if isinstance(module_names, str): self.module_names = {module_names} self.meta_path_finder = self.ForbiddenModules(self, self.module_names) @@ -1849,6 +1868,13 @@ class ImportModulePoisonFixture(fixtures.Fixture): # there (which is also what self.assert* and self.fail() do underneath) # will not work to cause a failure in the test. if self.fail_message: + if self.tc_id is not None: + LOG.warning( + "!!!---!!! TestCase ID %s hit the import poison. If you " + "see this in a failed functional test then please let " + "#openstack-nova on IRC know about it. !!!---!!!", + self.tc_id + ) raise ImportError(self.fail_message) diff --git a/nova/tests/functional/test_service.py b/nova/tests/functional/test_service.py index 65b41594bd..21e9a519ee 100644 --- a/nova/tests/functional/test_service.py +++ b/nova/tests/functional/test_service.py @@ -10,8 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import functools from unittest import mock +import fixtures +from oslo_utils.fixture import uuidsentinel as uuids + from nova import context as nova_context from nova import exception from nova.objects import service @@ -19,6 +23,7 @@ from nova import test from nova.tests import fixtures as nova_fixtures from nova.tests.functional import fixtures as func_fixtures from nova.tests.functional import integrated_helpers +from nova.virt import node class ServiceTestCase(test.TestCase, @@ -137,3 +142,83 @@ class TestOldComputeCheck( return_value=old_version): self.assertRaises( exception.TooOldComputeService, self._start_compute, 'host1') + + +class TestComputeStartupChecks(test.TestCase): + STUB_COMPUTE_ID = False + + def setUp(self): + super().setUp() + self.useFixture(nova_fixtures.RealPolicyFixture()) + self.useFixture(nova_fixtures.NeutronFixture(self)) + self.useFixture(nova_fixtures.GlanceFixture(self)) + self.useFixture(func_fixtures.PlacementFixture()) + + self._local_uuid = str(uuids.node) + + self.useFixture(fixtures.MockPatch( + 'nova.virt.node.get_local_node_uuid', + functools.partial(self.local_uuid, True))) + self.useFixture(fixtures.MockPatch( + 'nova.virt.node.read_local_node_uuid', + self.local_uuid)) + self.useFixture(fixtures.MockPatch( + 'nova.virt.node.write_local_node_uuid', + mock.DEFAULT)) + self.flags(compute_driver='fake.FakeDriverWithoutFakeNodes') + + def local_uuid(self, get=False): + if get and not self._local_uuid: + # Simulate the get_local_node_uuid behavior of calling write once + self._local_uuid = str(uuids.node) + node.write_local_node_uuid(self._local_uuid) + return self._local_uuid + + def test_compute_node_identity_greenfield(self): + # Level-set test case to show that starting and re-starting without + # any error cases works as expected. + + # Start with no local compute_id + self._local_uuid = None + self.start_service('compute') + + # Start should have generated and written a compute id + node.write_local_node_uuid.assert_called_once_with(str(uuids.node)) + + # Starting again should succeed and not cause another write + self.start_service('compute') + node.write_local_node_uuid.assert_called_once_with(str(uuids.node)) + + def test_compute_node_identity_deleted(self): + self.start_service('compute') + + # Simulate the compute_id file being deleted + self._local_uuid = None + + # Should refuse to start because it's not our first time and the file + # being missing is a hard error. + exc = self.assertRaises(exception.InvalidConfiguration, + self.start_service, 'compute') + self.assertIn('lost that state', str(exc)) + + def test_compute_node_hostname_changed(self): + # Start our compute once to create the node record + self.start_service('compute') + + # Starting with a different hostname should trigger the abort + exc = self.assertRaises(exception.InvalidConfiguration, + self.start_service, 'compute', host='other') + self.assertIn('hypervisor_hostname', str(exc)) + + def test_compute_node_uuid_changed(self): + # Start our compute once to create the node record + self.start_service('compute') + + # Simulate a changed local compute_id file + self._local_uuid = str(uuids.othernode) + + # We should fail to create the compute node record again, but with a + # useful error message about why. + exc = self.assertRaises(exception.InvalidConfiguration, + self.start_service, 'compute') + self.assertIn('Duplicate compute node record', str(exc)) diff --git a/nova/tests/unit/compute/test_resource_tracker.py b/nova/tests/unit/compute/test_resource_tracker.py index dfea323a9a..cd36b8987f 100644 --- a/nova/tests/unit/compute/test_resource_tracker.py +++ b/nova/tests/unit/compute/test_resource_tracker.py @@ -1552,6 +1552,20 @@ class TestInitComputeNode(BaseTestCase): self.assertEqual('fake-host', node.host) mock_update.assert_called() + @mock.patch.object(resource_tracker.ResourceTracker, + '_get_compute_node', + return_value=None) + @mock.patch('nova.objects.compute_node.ComputeNode.create') + def test_create_failed_conflict(self, mock_create, mock_getcn): + self._setup_rt() + resources = {'hypervisor_hostname': 'node1', + 'uuid': uuids.node1} + mock_create.side_effect = exc.DuplicateRecord(target='foo') + self.assertRaises(exc.InvalidConfiguration, + self.rt._init_compute_node, + mock.MagicMock, + resources) + @ddt.ddt class TestUpdateComputeNode(BaseTestCase): diff --git a/nova/tests/unit/objects/test_compute_node.py b/nova/tests/unit/objects/test_compute_node.py index 63b070c543..84c4e87785 100644 --- a/nova/tests/unit/objects/test_compute_node.py +++ b/nova/tests/unit/objects/test_compute_node.py @@ -16,6 +16,7 @@ import copy from unittest import mock import netaddr +from oslo_db import exception as db_exc from oslo_serialization import jsonutils from oslo_utils.fixture import uuidsentinel from oslo_utils import timeutils @@ -341,6 +342,14 @@ class _TestComputeNodeObject(object): 'uuid': uuidsentinel.fake_compute_node} mock_create.assert_called_once_with(self.context, param_dict) + @mock.patch('nova.db.main.api.compute_node_create') + def test_create_duplicate(self, mock_create): + mock_create.side_effect = db_exc.DBDuplicateEntry + compute = compute_node.ComputeNode(context=self.context) + compute.service_id = 456 + compute.hypervisor_hostname = 'node1' + self.assertRaises(exception.DuplicateRecord, compute.create) + @mock.patch.object(db, 'compute_node_update') @mock.patch( 'nova.db.main.api.compute_node_get', return_value=fake_compute_node) diff --git a/nova/tests/unit/virt/libvirt/test_config.py b/nova/tests/unit/virt/libvirt/test_config.py index 8f840e8859..3d0b5ae685 100644 --- a/nova/tests/unit/virt/libvirt/test_config.py +++ b/nova/tests/unit/virt/libvirt/test_config.py @@ -1537,7 +1537,7 @@ class LibvirtConfigGuestInputTest(LibvirtConfigBaseTest): class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): - def test_config_graphics(self): + def test_config_graphics_vnc(self): obj = config.LibvirtConfigGuestGraphics() obj.type = "vnc" obj.autoport = True @@ -1549,6 +1549,30 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): <graphics type="vnc" autoport="yes" keymap="en_US" listen="127.0.0.1"/> """) + def test_config_graphics_spice(self): + obj = config.LibvirtConfigGuestGraphics() + obj.type = "spice" + obj.autoport = False + obj.keymap = "en_US" + obj.listen = "127.0.0.1" + + obj.image_compression = "auto_glz" + obj.jpeg_compression = "auto" + obj.zlib_compression = "always" + obj.playback_compression = True + obj.streaming_mode = "filter" + + xml = obj.to_xml() + self.assertXmlEqual(xml, """ + <graphics type="spice" autoport="no" keymap="en_US" listen="127.0.0.1"> + <image compression="auto_glz"/> + <jpeg compression="auto"/> + <zlib compression="always"/> + <playback compression="on"/> + <streaming mode="filter"/> + </graphics> + """) + class LibvirtConfigGuestHostdev(LibvirtConfigBaseTest): diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 2474020318..04c80d662b 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -5839,6 +5839,11 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertEqual(cfg.devices[3].type, 'vnc') self.assertEqual(cfg.devices[3].listen, '10.0.0.1') self.assertIsNone(cfg.devices[3].keymap) + self.assertIsNone(cfg.devices[3].image_compression) + self.assertIsNone(cfg.devices[3].jpeg_compression) + self.assertIsNone(cfg.devices[3].zlib_compression) + self.assertIsNone(cfg.devices[3].playback_compression) + self.assertIsNone(cfg.devices[3].streaming_mode) def test_get_guest_config_with_vnc_and_tablet(self): self.flags(enabled=True, group='vnc') @@ -5869,6 +5874,11 @@ class LibvirtConnTestCase(test.NoDBTestCase, vconfig.LibvirtConfigMemoryBalloon) self.assertEqual(cfg.devices[3].type, 'vnc') + self.assertIsNone(cfg.devices[3].image_compression) + self.assertIsNone(cfg.devices[3].jpeg_compression) + self.assertIsNone(cfg.devices[3].zlib_compression) + self.assertIsNone(cfg.devices[3].playback_compression) + self.assertIsNone(cfg.devices[3].streaming_mode) self.assertEqual(cfg.devices[5].type, 'tablet') def test_get_guest_config_with_spice_and_tablet(self): @@ -5905,6 +5915,11 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertEqual(cfg.devices[3].type, 'spice') self.assertEqual(cfg.devices[3].listen, '10.0.0.1') self.assertIsNone(cfg.devices[3].keymap) + self.assertIsNone(cfg.devices[3].image_compression) + self.assertIsNone(cfg.devices[3].jpeg_compression) + self.assertIsNone(cfg.devices[3].zlib_compression) + self.assertIsNone(cfg.devices[3].playback_compression) + self.assertIsNone(cfg.devices[3].streaming_mode) self.assertEqual(cfg.devices[5].type, 'tablet') @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) @@ -5964,8 +5979,57 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertEqual(cfg.devices[3].target_name, "com.redhat.spice.0") self.assertEqual(cfg.devices[3].type, 'spicevmc') self.assertEqual(cfg.devices[4].type, "spice") + self.assertIsNone(cfg.devices[4].image_compression) + self.assertIsNone(cfg.devices[4].jpeg_compression) + self.assertIsNone(cfg.devices[4].zlib_compression) + self.assertIsNone(cfg.devices[4].playback_compression) + self.assertIsNone(cfg.devices[4].streaming_mode) self.assertEqual(cfg.devices[5].type, video_type) + def test_get_guest_config_with_spice_compression(self): + self.flags(enabled=False, group='vnc') + self.flags(virt_type='kvm', group='libvirt') + self.flags(enabled=True, + agent_enabled=False, + image_compression='auto_lz', + jpeg_compression='never', + zlib_compression='always', + playback_compression=False, + streaming_mode='all', + server_listen='10.0.0.1', + group='spice') + self.flags(pointer_model='usbtablet') + + cfg = self._get_guest_config_with_graphics() + + self.assertEqual(len(cfg.devices), 9) + self.assertIsInstance(cfg.devices[0], + vconfig.LibvirtConfigGuestDisk) + self.assertIsInstance(cfg.devices[1], + vconfig.LibvirtConfigGuestDisk) + self.assertIsInstance(cfg.devices[2], + vconfig.LibvirtConfigGuestSerial) + self.assertIsInstance(cfg.devices[3], + vconfig.LibvirtConfigGuestGraphics) + self.assertIsInstance(cfg.devices[4], + vconfig.LibvirtConfigGuestVideo) + self.assertIsInstance(cfg.devices[5], + vconfig.LibvirtConfigGuestInput) + self.assertIsInstance(cfg.devices[6], + vconfig.LibvirtConfigGuestRng) + self.assertIsInstance(cfg.devices[7], + vconfig.LibvirtConfigGuestUSBHostController) + self.assertIsInstance(cfg.devices[8], + vconfig.LibvirtConfigMemoryBalloon) + + self.assertEqual(cfg.devices[3].type, 'spice') + self.assertEqual(cfg.devices[3].listen, '10.0.0.1') + self.assertEqual(cfg.devices[3].image_compression, 'auto_lz') + self.assertEqual(cfg.devices[3].jpeg_compression, 'never') + self.assertEqual(cfg.devices[3].zlib_compression, 'always') + self.assertFalse(cfg.devices[3].playback_compression) + self.assertEqual(cfg.devices[3].streaming_mode, 'all') + @mock.patch.object(host.Host, 'get_guest') @mock.patch.object(libvirt_driver.LibvirtDriver, '_get_serial_ports_from_guest') diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 2234bd068e..bf7dc8fc72 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -49,6 +49,7 @@ from nova.objects import migrate_data from nova.virt import driver from nova.virt import hardware from nova.virt.ironic import driver as ironic +import nova.virt.node from nova.virt import virtapi CONF = nova.conf.CONF @@ -1130,3 +1131,22 @@ class EphEncryptionDriverPLAIN(MediumFakeDriver): FakeDriver.capabilities, supports_ephemeral_encryption=True, supports_ephemeral_encryption_plain=True) + + +class FakeDriverWithoutFakeNodes(FakeDriver): + """FakeDriver that behaves like a real single-node driver. + + This behaves like a real virt driver from the perspective of its + nodes, with a stable nodename and use of the global node identity + stuff to provide a stable node UUID. + """ + + def get_available_resource(self, nodename): + resources = super().get_available_resource(nodename) + resources['uuid'] = nova.virt.node.get_local_node_uuid() + return resources + + def get_nodenames_by_uuid(self, refresh=False): + return { + nova.virt.node.get_local_node_uuid(): self.get_available_nodes()[0] + } diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index 0db2dc6b67..231283b8dd 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -2047,6 +2047,12 @@ class LibvirtConfigGuestGraphics(LibvirtConfigGuestDevice): self.keymap = None self.listen = None + self.image_compression = None + self.jpeg_compression = None + self.zlib_compression = None + self.playback_compression = None + self.streaming_mode = None + def format_dom(self): dev = super(LibvirtConfigGuestGraphics, self).format_dom() @@ -2057,6 +2063,24 @@ class LibvirtConfigGuestGraphics(LibvirtConfigGuestDevice): if self.listen: dev.set("listen", self.listen) + if self.type == "spice": + if self.image_compression is not None: + dev.append(etree.Element( + 'image', compression=self.image_compression)) + if self.jpeg_compression is not None: + dev.append(etree.Element( + 'jpeg', compression=self.jpeg_compression)) + if self.zlib_compression is not None: + dev.append(etree.Element( + 'zlib', compression=self.zlib_compression)) + if self.playback_compression is not None: + dev.append(etree.Element( + 'playback', compression=self.get_on_off_str( + self.playback_compression))) + if self.streaming_mode is not None: + dev.append(etree.Element( + 'streaming', mode=self.streaming_mode)) + return dev diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 01dec2532f..869996f615 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -7316,6 +7316,11 @@ class LibvirtDriver(driver.ComputeDriver): graphics = vconfig.LibvirtConfigGuestGraphics() graphics.type = "spice" graphics.listen = CONF.spice.server_listen + graphics.image_compression = CONF.spice.image_compression + graphics.jpeg_compression = CONF.spice.jpeg_compression + graphics.zlib_compression = CONF.spice.zlib_compression + graphics.playback_compression = CONF.spice.playback_compression + graphics.streaming_mode = CONF.spice.streaming_mode guest.add_device(graphics) add_video_driver = True @@ -10081,6 +10086,24 @@ class LibvirtDriver(driver.ComputeDriver): :param instance: instance object that is in migration """ + current = eventlet.getcurrent() + # NOTE(gibi) not all eventlet spawn is under our control, so + # there can be senders without test_case_id set, find the first + # ancestor that was spawned from nova.utils.spawn[_n] and + # therefore has the id set. + while ( + current is not None and + not getattr(current, 'test_case_id', None) + ): + current = current.parent + + if current is not None: + LOG.warning( + "!!!---!!! live_migration_abort thread was spawned by " + "TestCase ID: %s. If you see this in a failed functional test " + "then please let #openstack-nova on IRC know about it. " + "!!!---!!!", current.test_case_id + ) guest = self._host.get_guest(instance) dom = guest._domain diff --git a/releasenotes/notes/add-spice-compression-support-e41676f445544e8d.yaml b/releasenotes/notes/add-spice-compression-support-e41676f445544e8d.yaml new file mode 100644 index 0000000000..b370889171 --- /dev/null +++ b/releasenotes/notes/add-spice-compression-support-e41676f445544e8d.yaml @@ -0,0 +1,23 @@ +--- +features: + - | + The following SPICE-related options are added to the ``spice`` + configuration group of a Nova configuration: + + - ``image_compression`` + - ``jpeg_compression`` + - ``zlib_compression`` + - ``playback_compression`` + - ``streaming_mode`` + + These configuration options can be used to enable and set the + SPICE compression settings for libvirt (QEMU/KVM) provisioned + instances. Each configuration option is optional and can be set + explictly to configure the associated SPICE compression setting + for libvirt. If all configuration options are not set, then none + of the SPICE compression settings will be configured for libvirt, + which corresponds to the behavior before this change. In this case, + the built-in defaults from the libvirt backend (e.g. QEMU) are used. + + Note that those options are only taken into account if SPICE support + is enabled (and the VNC support is disabled). |