diff options
Diffstat (limited to 'nova')
34 files changed, 1605 insertions, 72 deletions
diff --git a/nova/compute/manager.py b/nova/compute/manager.py index f59fd82d10..952ab3e199 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1509,15 +1509,21 @@ class ComputeManager(manager.Manager): to write our node identity uuid (if not already done) based on nodes assigned to us in the database. """ - if service_ref.version >= service_obj.NODE_IDENTITY_VERSION: - # Already new enough, nothing to do here - return - if 'ironic' in CONF.compute_driver.lower(): # We do not persist a single local node identity for # ironic return + if service_ref.version >= service_obj.NODE_IDENTITY_VERSION: + # Already new enough, nothing to do here, but make sure that we + # have a UUID file already, as this is not our first time starting. + if nova.virt.node.read_local_node_uuid() is None: + raise exception.InvalidConfiguration( + ('No local node identity found, but this is not our ' + 'first startup on this host. Refusing to start after ' + 'potentially having lost that state!')) + return + if nova.virt.node.read_local_node_uuid(): # We already have a local node identity, no migration needed return @@ -1604,12 +1610,6 @@ class ComputeManager(manager.Manager): # NOTE(gibi): At this point the compute_nodes of the resource tracker # has not been populated yet so we cannot rely on the resource tracker # here. - # NOTE(gibi): If ironic and vcenter virt driver slow start time - # becomes problematic here then we should consider adding a config - # option or a driver flag to tell us if we should thread - # _destroy_evacuated_instances and - # _error_out_instances_whose_build_was_interrupted out in the - # background on startup context = nova.context.get_admin_context() nodes_by_uuid = self._get_nodes(context) @@ -1627,6 +1627,12 @@ class ComputeManager(manager.Manager): self._validate_pinning_configuration(instances) self._validate_vtpm_configuration(instances) + # NOTE(gibi): If ironic and vcenter virt driver slow start time + # becomes problematic here then we should consider adding a config + # option or a driver flag to tell us if we should thread + # _destroy_evacuated_instances and + # _error_out_instances_whose_build_was_interrupted out in the + # background on startup try: # checking that instance was not already evacuated to other host evacuated_instances = self._destroy_evacuated_instances( @@ -10474,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/compute/rpcapi.py b/nova/compute/rpcapi.py index 4a97a90807..efc06300db 100644 --- a/nova/compute/rpcapi.py +++ b/nova/compute/rpcapi.py @@ -425,7 +425,7 @@ class ComputeAPI(object): 'xena': '6.0', 'yoga': '6.0', 'zed': '6.1', - 'antilope': '6.2', + 'antelope': '6.2', } @property diff --git a/nova/conf/libvirt.py b/nova/conf/libvirt.py index 16a3f63090..204fe5c4b8 100644 --- a/nova/conf/libvirt.py +++ b/nova/conf/libvirt.py @@ -1478,6 +1478,23 @@ Related options: """), ] +libvirt_cpu_mgmt_opts = [ + cfg.BoolOpt('cpu_power_management', + default=False, + help='Use libvirt to manage CPU cores performance.'), + cfg.StrOpt('cpu_power_management_strategy', + choices=['cpu_state', 'governor'], + default='cpu_state', + help='Tuning strategy to reduce CPU power consumption when ' + 'unused'), + cfg.StrOpt('cpu_power_governor_low', + default='powersave', + help='Governor to use in order ' + 'to reduce CPU power consumption'), + cfg.StrOpt('cpu_power_governor_high', + default='performance', + help='Governor to use in order to have best CPU performance'), +] ALL_OPTS = list(itertools.chain( libvirt_general_opts, @@ -1499,6 +1516,7 @@ ALL_OPTS = list(itertools.chain( libvirt_volume_nvmeof_opts, libvirt_pmem_opts, libvirt_vtpm_opts, + libvirt_cpu_mgmt_opts, )) 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 6d4798997f..0c0ffa85a1 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 with microversion 2.95, evacuate API will stop " "instance on destination. To evacuate before upgrades are " diff --git a/nova/filesystem.py b/nova/filesystem.py new file mode 100644 index 0000000000..5394d2d835 --- /dev/null +++ b/nova/filesystem.py @@ -0,0 +1,59 @@ +# 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. + +"""Functions to address filesystem calls, particularly sysfs.""" + +import os + +from oslo_log import log as logging + +from nova import exception + +LOG = logging.getLogger(__name__) + + +SYS = '/sys' + + +# NOTE(bauzas): this method is deliberately not wrapped in a privsep entrypoint +def read_sys(path: str) -> str: + """Reads the content of a file in the sys filesystem. + + :param path: relative or absolute. If relative, will be prefixed by /sys. + :returns: contents of that file. + :raises: nova.exception.FileNotFound if we can't read that file. + """ + try: + # The path can be absolute with a /sys prefix but that's fine. + with open(os.path.join(SYS, path), mode='r') as data: + return data.read() + except (OSError, ValueError) as exc: + raise exception.FileNotFound(file_path=path) from exc + + +# NOTE(bauzas): this method is deliberately not wrapped in a privsep entrypoint +# In order to correctly use it, you need to decorate the caller with a specific +# privsep entrypoint. +def write_sys(path: str, data: str) -> None: + """Writes the content of a file in the sys filesystem with data. + + :param path: relative or absolute. If relative, will be prefixed by /sys. + :param data: the data to write. + :returns: contents of that file. + :raises: nova.exception.FileNotFound if we can't write that file. + """ + try: + # The path can be absolute with a /sys prefix but that's fine. + with open(os.path.join(SYS, path), mode='w') as fd: + fd.write(data) + except (OSError, ValueError) as exc: + raise exception.FileNotFound(file_path=path) from exc 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/test.py b/nova/test.py index 562bd2516e..0f7965ea33 100644 --- a/nova/test.py +++ b/nova/test.py @@ -171,6 +171,12 @@ class TestCase(base.BaseTestCase): # base class when USES_DB is True. NUMBER_OF_CELLS = 1 + # The stable compute id stuff is intentionally singleton-ish, which makes + # it a nightmare for testing multiple host/node combinations in tests like + # we do. So, mock it out by default, unless the test is specifically + # designed to handle it. + STUB_COMPUTE_ID = True + def setUp(self): """Run before each test method to initialize test environment.""" # Ensure BaseTestCase's ConfigureLogging fixture is disabled since @@ -301,7 +307,8 @@ class TestCase(base.BaseTestCase): # Reset our local node uuid cache (and avoid writing to the # local filesystem when we generate a new one). - self.useFixture(nova_fixtures.ComputeNodeIdFixture()) + if self.STUB_COMPUTE_ID: + self.useFixture(nova_fixtures.ComputeNodeIdFixture()) def _setup_cells(self): """Setup a normal cellsv2 environment. diff --git a/nova/tests/fixtures/__init__.py b/nova/tests/fixtures/__init__.py index df254608fd..9ff4a2a601 100644 --- a/nova/tests/fixtures/__init__.py +++ b/nova/tests/fixtures/__init__.py @@ -16,6 +16,8 @@ from .cast_as_call import CastAsCallFixture # noqa: F401 from .cinder import CinderFixture # noqa: F401 from .conf import ConfFixture # noqa: F401, F403 from .cyborg import CyborgFixture # noqa: F401 +from .filesystem import SysFileSystemFixture # noqa: F401 +from .filesystem import TempFileSystemFixture # noqa: F401 from .glance import GlanceFixture # noqa: F401 from .libvirt import LibvirtFixture # noqa: F401 from .libvirt_imagebackend import LibvirtImageBackendFixture # noqa: F401 diff --git a/nova/tests/fixtures/filesystem.py b/nova/tests/fixtures/filesystem.py new file mode 100644 index 0000000000..932d42fe27 --- /dev/null +++ b/nova/tests/fixtures/filesystem.py @@ -0,0 +1,81 @@ +# 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. + +import os +import shutil +import tempfile +from unittest import mock + +import fixtures + +from nova import filesystem +from nova.virt.libvirt.cpu import core + + +SYS = 'sys' + + +class TempFileSystemFixture(fixtures.Fixture): + """Creates a fake / filesystem""" + + def _setUp(self): + self.temp_dir = tempfile.TemporaryDirectory(prefix='fake_fs') + # NOTE(sbauza): I/O disk errors may raise an exception here, as we + # don't ignore them. If that's causing a problem in our CI jobs, the + # recommended solution is to use shutil.rmtree instead of cleanup() + # with ignore_errors parameter set to True (or wait for the minimum + # python version to be 3.10 as TemporaryDirectory will provide + # ignore_cleanup_errors parameter) + self.addCleanup(self.temp_dir.cleanup) + + +class SysFileSystemFixture(TempFileSystemFixture): + """Creates a fake /sys filesystem""" + + def __init__(self, cpus_supported=None): + self.cpus_supported = cpus_supported or 10 + + def _setUp(self): + super()._setUp() + self.sys_path = os.path.join(self.temp_dir.name, SYS) + self.addCleanup(shutil.rmtree, self.sys_path, ignore_errors=True) + + sys_patcher = mock.patch( + 'nova.filesystem.SYS', + new_callable=mock.PropertyMock(return_value=self.sys_path)) + self.sys_mock = sys_patcher.start() + self.addCleanup(sys_patcher.stop) + + avail_path_patcher = mock.patch( + 'nova.virt.libvirt.cpu.core.AVAILABLE_PATH', + new_callable=mock.PropertyMock( + return_value=os.path.join(self.sys_path, + 'devices/system/cpu/present'))) + self.avail_path_mock = avail_path_patcher.start() + self.addCleanup(avail_path_patcher.stop) + + cpu_path_patcher = mock.patch( + 'nova.virt.libvirt.cpu.core.CPU_PATH_TEMPLATE', + new_callable=mock.PropertyMock( + return_value=os.path.join(self.sys_path, + 'devices/system/cpu/cpu%(core)s'))) + self.cpu_path_mock = cpu_path_patcher.start() + self.addCleanup(cpu_path_patcher.stop) + + for cpu_nr in range(self.cpus_supported): + cpu_dir = os.path.join(self.cpu_path_mock % {'core': cpu_nr}) + os.makedirs(os.path.join(cpu_dir, 'cpufreq')) + filesystem.write_sys( + os.path.join(cpu_dir, 'cpufreq/scaling_governor'), + data='powersave') + filesystem.write_sys(core.AVAILABLE_PATH, + f'0-{self.cpus_supported - 1}') diff --git a/nova/tests/fixtures/nova.py b/nova/tests/fixtures/nova.py index 76dc63755d..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) @@ -1863,3 +1889,7 @@ class ComputeNodeIdFixture(fixtures.Fixture): self.useFixture(fixtures.MockPatch( 'nova.virt.node.write_local_node_uuid', lambda uuid: None)) + self.useFixture(fixtures.MockPatch( + 'nova.compute.manager.ComputeManager.' + '_ensure_existing_node_identity', + mock.DEFAULT)) diff --git a/nova/tests/functional/libvirt/test_power_manage.py b/nova/tests/functional/libvirt/test_power_manage.py new file mode 100644 index 0000000000..fb1ac7d0cd --- /dev/null +++ b/nova/tests/functional/libvirt/test_power_manage.py @@ -0,0 +1,270 @@ +# 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 + +import fixtures + +from nova import context as nova_context +from nova import exception +from nova import objects +from nova.tests import fixtures as nova_fixtures +from nova.tests.fixtures import libvirt as fakelibvirt +from nova.tests.functional.libvirt import base +from nova.virt import hardware +from nova.virt.libvirt import cpu + + +class PowerManagementTestsBase(base.ServersTestBase): + + ADDITIONAL_FILTERS = ['NUMATopologyFilter'] + + ADMIN_API = True + + def setUp(self): + super(PowerManagementTestsBase, self).setUp() + + self.ctxt = nova_context.get_admin_context() + + # Mock the 'NUMATopologyFilter' filter, as most tests need to inspect + # this + host_manager = self.scheduler.manager.host_manager + numa_filter_class = host_manager.filter_cls_map['NUMATopologyFilter'] + host_pass_mock = mock.Mock(wraps=numa_filter_class().host_passes) + _p = mock.patch('nova.scheduler.filters' + '.numa_topology_filter.NUMATopologyFilter.host_passes', + side_effect=host_pass_mock) + self.mock_filter = _p.start() + self.addCleanup(_p.stop) + + # for the sake of resizing, we need to patch the two methods below + self.useFixture(fixtures.MockPatch( + 'nova.virt.libvirt.LibvirtDriver._get_instance_disk_info', + return_value=[])) + self.useFixture(fixtures.MockPatch('os.rename')) + + self.useFixture(nova_fixtures.PrivsepFixture()) + + # Defining the main flavor for 4 vCPUs all pinned + self.extra_spec = { + 'hw:cpu_policy': 'dedicated', + 'hw:cpu_thread_policy': 'prefer', + } + self.pcpu_flavor_id = self._create_flavor( + vcpu=4, extra_spec=self.extra_spec) + + def _assert_server_cpus_state(self, server, expected='online'): + inst = objects.Instance.get_by_uuid(self.ctxt, server['id']) + if not inst.numa_topology: + self.fail('Instance should have a NUMA topology in order to know ' + 'its physical CPUs') + instance_pcpus = inst.numa_topology.cpu_pinning + self._assert_cpu_set_state(instance_pcpus, expected=expected) + return instance_pcpus + + def _assert_cpu_set_state(self, cpu_set, expected='online'): + for i in cpu_set: + core = cpu.Core(i) + if expected == 'online': + self.assertTrue(core.online, f'{i} is not online') + elif expected == 'offline': + self.assertFalse(core.online, f'{i} is online') + elif expected == 'powersave': + self.assertEqual('powersave', core.governor) + elif expected == 'performance': + self.assertEqual('performance', core.governor) + + +class PowerManagementTests(PowerManagementTestsBase): + """Test suite for a single host with 9 dedicated cores and 1 used for OS""" + + def setUp(self): + super(PowerManagementTests, self).setUp() + + self.useFixture(nova_fixtures.SysFileSystemFixture()) + + # Definining the CPUs to be pinned. + self.flags(cpu_dedicated_set='1-9', cpu_shared_set=None, + group='compute') + self.flags(vcpu_pin_set=None) + self.flags(cpu_power_management=True, group='libvirt') + + self.flags(allow_resize_to_same_host=True) + self.host_info = fakelibvirt.HostInfo(cpu_nodes=1, cpu_sockets=1, + cpu_cores=5, cpu_threads=2) + self.compute1 = self.start_compute(host_info=self.host_info, + hostname='compute1') + + # All cores are shutdown at startup, let's check. + cpu_dedicated_set = hardware.get_cpu_dedicated_set() + self._assert_cpu_set_state(cpu_dedicated_set, expected='offline') + + def test_hardstop_compute_service_if_wrong_opt(self): + self.flags(cpu_dedicated_set=None, cpu_shared_set=None, + group='compute') + self.flags(vcpu_pin_set=None) + self.flags(cpu_power_management=True, group='libvirt') + self.assertRaises(exception.InvalidConfiguration, + self.start_compute, host_info=self.host_info, + hostname='compute2') + + def test_create_server(self): + server = self._create_server( + flavor_id=self.pcpu_flavor_id, + expected_state='ACTIVE') + # Let's verify that the pinned CPUs are now online + self._assert_server_cpus_state(server, expected='online') + + # Verify that the unused CPUs are still offline + inst = objects.Instance.get_by_uuid(self.ctxt, server['id']) + instance_pcpus = inst.numa_topology.cpu_pinning + cpu_dedicated_set = hardware.get_cpu_dedicated_set() + unused_cpus = cpu_dedicated_set - instance_pcpus + self._assert_cpu_set_state(unused_cpus, expected='offline') + + def test_stop_start_server(self): + server = self._create_server( + flavor_id=self.pcpu_flavor_id, + expected_state='ACTIVE') + + server = self._stop_server(server) + # Let's verify that the pinned CPUs are now stopped... + self._assert_server_cpus_state(server, expected='offline') + + server = self._start_server(server) + # ...and now, they should be back. + self._assert_server_cpus_state(server, expected='online') + + def test_resize(self): + server = self._create_server( + flavor_id=self.pcpu_flavor_id, + expected_state='ACTIVE') + server_pcpus = self._assert_server_cpus_state(server, + expected='online') + + new_flavor_id = self._create_flavor( + vcpu=5, extra_spec=self.extra_spec) + self._resize_server(server, new_flavor_id) + server2_pcpus = self._assert_server_cpus_state(server, + expected='online') + # Even if the resize is not confirmed yet, the original guest is now + # destroyed so the cores are now offline. + self._assert_cpu_set_state(server_pcpus, expected='offline') + + # let's revert the resize + self._revert_resize(server) + # So now the original CPUs will be online again, while the previous + # cores should be back offline. + self._assert_cpu_set_state(server_pcpus, expected='online') + self._assert_cpu_set_state(server2_pcpus, expected='offline') + + def test_changing_strategy_fails(self): + # As a reminder, all cores have been shutdown before. + # Now we want to change the strategy and then we restart the service + self.flags(cpu_power_management_strategy='governor', group='libvirt') + # See, this is not possible as we would have offline CPUs. + self.assertRaises(exception.InvalidConfiguration, + self.restart_compute_service, hostname='compute1') + + +class PowerManagementTestsGovernor(PowerManagementTestsBase): + """Test suite for speific governor usage (same 10-core host)""" + + def setUp(self): + super(PowerManagementTestsGovernor, self).setUp() + + self.useFixture(nova_fixtures.SysFileSystemFixture()) + + # Definining the CPUs to be pinned. + self.flags(cpu_dedicated_set='1-9', cpu_shared_set=None, + group='compute') + self.flags(vcpu_pin_set=None) + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_power_management_strategy='governor', group='libvirt') + + self.flags(allow_resize_to_same_host=True) + self.host_info = fakelibvirt.HostInfo(cpu_nodes=1, cpu_sockets=1, + cpu_cores=5, cpu_threads=2) + self.compute1 = self.start_compute(host_info=self.host_info, + hostname='compute1') + + def test_create(self): + cpu_dedicated_set = hardware.get_cpu_dedicated_set() + # With the governor strategy, cores are still online but run with a + # powersave governor. + self._assert_cpu_set_state(cpu_dedicated_set, expected='powersave') + + # Now, start an instance + server = self._create_server( + flavor_id=self.pcpu_flavor_id, + expected_state='ACTIVE') + # When pinned cores are run, the governor state is now performance + self._assert_server_cpus_state(server, expected='performance') + + def test_changing_strategy_fails(self): + # Arbitratly set a core governor strategy to be performance + cpu.Core(1).set_high_governor() + # and then forget about it while changing the strategy. + self.flags(cpu_power_management_strategy='cpu_state', group='libvirt') + # This time, this wouldn't be acceptable as some core would have a + # difference performance while Nova would only online/offline it. + self.assertRaises(exception.InvalidConfiguration, + self.restart_compute_service, hostname='compute1') + + +class PowerManagementMixedInstances(PowerManagementTestsBase): + """Test suite for a single host with 6 dedicated cores, 3 shared and one + OS-restricted. + """ + + def setUp(self): + super(PowerManagementMixedInstances, self).setUp() + + self.useFixture(nova_fixtures.SysFileSystemFixture()) + + # Definining 6 CPUs to be dedicated, not all of them in a series. + self.flags(cpu_dedicated_set='1-3,5-7', cpu_shared_set='4,8-9', + group='compute') + self.flags(vcpu_pin_set=None) + self.flags(cpu_power_management=True, group='libvirt') + + self.host_info = fakelibvirt.HostInfo(cpu_nodes=1, cpu_sockets=1, + cpu_cores=5, cpu_threads=2) + self.compute1 = self.start_compute(host_info=self.host_info, + hostname='compute1') + + # Make sure only 6 are offline now + cpu_dedicated_set = hardware.get_cpu_dedicated_set() + self._assert_cpu_set_state(cpu_dedicated_set, expected='offline') + + # cores 4 and 8-9 should be online + self._assert_cpu_set_state({4, 8, 9}, expected='online') + + def test_standard_server_works_and_passes(self): + + std_flavor_id = self._create_flavor(vcpu=2) + self._create_server(flavor_id=std_flavor_id, expected_state='ACTIVE') + + # Since this is an instance with floating vCPUs on the shared set, we + # can only lookup the host CPUs and see they haven't changed state. + cpu_dedicated_set = hardware.get_cpu_dedicated_set() + self._assert_cpu_set_state(cpu_dedicated_set, expected='offline') + self._assert_cpu_set_state({4, 8, 9}, expected='online') + + # We can now try to boot an instance with pinned CPUs to test the mix + pinned_server = self._create_server( + flavor_id=self.pcpu_flavor_id, + expected_state='ACTIVE') + # We'll see that its CPUs are now online + self._assert_server_cpus_state(pinned_server, expected='online') + # but it doesn't change the shared set + self._assert_cpu_set_state({4, 8, 9}, expected='online') 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_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index c5fed20377..16de724a42 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -91,6 +91,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, # os-brick>=5.1 now uses external file system locks instead of internal # locks so we need to set up locking REQUIRES_LOCKING = True + STUB_COMPUTE_ID = False def setUp(self): super(ComputeManagerUnitTestCase, self).setUp() @@ -6361,13 +6362,15 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, 'two-image': 'existing'}, r) @mock.patch.object(virt_node, 'write_local_node_uuid') - def test_ensure_node_uuid_not_needed_version(self, mock_node): + @mock.patch.object(virt_node, 'read_local_node_uuid') + def test_ensure_node_uuid_not_needed_version(self, mock_read, mock_write): # Make sure an up-to-date service bypasses the persistence service_ref = service_obj.Service() self.assertEqual(service_obj.SERVICE_VERSION, service_ref.version) - mock_node.assert_not_called() + mock_read.return_value = 'not none' + mock_write.assert_not_called() self.compute._ensure_existing_node_identity(service_ref) - mock_node.assert_not_called() + mock_write.assert_not_called() @mock.patch.object(virt_node, 'write_local_node_uuid') def test_ensure_node_uuid_not_needed_ironic(self, mock_node): @@ -6452,6 +6455,20 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase, mock_get_cn.assert_called_once_with(mock.ANY, self.compute.host) mock_write_node.assert_called_once_with(str(uuids.compute)) + @mock.patch.object(virt_node, 'read_local_node_uuid') + def test_ensure_node_uuid_missing_file_ironic(self, mock_read): + mock_service = mock.MagicMock( + version=service_obj.NODE_IDENTITY_VERSION) + mock_read.return_value = None + self.assertRaises(exception.InvalidConfiguration, + self.compute._ensure_existing_node_identity, + mock_service) + mock_read.assert_called_once_with() + + # Now make sure that ironic causes this exact configuration to pass + self.flags(compute_driver='ironic') + self.compute._ensure_existing_node_identity(mock_service) + def test_ensure_node_uuid_called_by_init_host(self): # test_init_host() above ensures that we do not call # _ensure_existing_node_identity() in the service_ref=None case. 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/test_filesystem.py b/nova/tests/unit/test_filesystem.py new file mode 100644 index 0000000000..85f16157ee --- /dev/null +++ b/nova/tests/unit/test_filesystem.py @@ -0,0 +1,52 @@ +# 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. + +import os +from unittest import mock + +from nova import exception +from nova import filesystem +from nova import test + + +class TestFSCommon(test.NoDBTestCase): + + def test_read_sys(self): + open_mock = mock.mock_open(read_data='bar') + with mock.patch('builtins.open', open_mock) as m_open: + self.assertEqual('bar', filesystem.read_sys('foo')) + expected_path = os.path.join(filesystem.SYS, 'foo') + m_open.assert_called_once_with(expected_path, mode='r') + + def test_read_sys_error(self): + with mock.patch('builtins.open', + side_effect=OSError('error')) as m_open: + self.assertRaises(exception.FileNotFound, + filesystem.read_sys, 'foo') + expected_path = os.path.join(filesystem.SYS, 'foo') + m_open.assert_called_once_with(expected_path, mode='r') + + def test_write_sys(self): + open_mock = mock.mock_open() + with mock.patch('builtins.open', open_mock) as m_open: + self.assertIsNone(filesystem.write_sys('foo', 'bar')) + expected_path = os.path.join(filesystem.SYS, 'foo') + m_open.assert_called_once_with(expected_path, mode='w') + open_mock().write.assert_called_once_with('bar') + + def test_write_sys_error(self): + with mock.patch('builtins.open', + side_effect=OSError('fake_error')) as m_open: + self.assertRaises(exception.FileNotFound, + filesystem.write_sys, 'foo', 'bar') + expected_path = os.path.join(filesystem.SYS, 'foo') + m_open.assert_called_once_with(expected_path, mode='w') diff --git a/nova/tests/unit/virt/libvirt/cpu/__init__.py b/nova/tests/unit/virt/libvirt/cpu/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/nova/tests/unit/virt/libvirt/cpu/__init__.py diff --git a/nova/tests/unit/virt/libvirt/cpu/test_api.py b/nova/tests/unit/virt/libvirt/cpu/test_api.py new file mode 100644 index 0000000000..b5bcb762f3 --- /dev/null +++ b/nova/tests/unit/virt/libvirt/cpu/test_api.py @@ -0,0 +1,194 @@ +# 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 nova import exception +from nova import objects +from nova import test +from nova.virt.libvirt.cpu import api +from nova.virt.libvirt.cpu import core + + +class TestAPI(test.NoDBTestCase): + + def setUp(self): + super(TestAPI, self).setUp() + self.core_1 = api.Core(1) + + # Create a fake instance with two pinned CPUs but only one is on the + # dedicated set + numa_topology = objects.InstanceNUMATopology(cells=[ + objects.InstanceNUMACell(cpu_pinning_raw={'0': '0', '2': '2'}), + ]) + self.fake_inst = objects.Instance(numa_topology=numa_topology) + + @mock.patch.object(core, 'get_online') + def test_online(self, mock_get_online): + mock_get_online.return_value = True + self.assertTrue(self.core_1.online) + mock_get_online.assert_called_once_with(self.core_1.ident) + + @mock.patch.object(core, 'set_online') + def test_set_online(self, mock_set_online): + self.core_1.online = True + mock_set_online.assert_called_once_with(self.core_1.ident) + + @mock.patch.object(core, 'set_offline') + def test_set_offline(self, mock_set_offline): + self.core_1.online = False + mock_set_offline.assert_called_once_with(self.core_1.ident) + + def test_hash(self): + self.assertEqual(hash(self.core_1.ident), hash(self.core_1)) + + @mock.patch.object(core, 'get_governor') + def test_governor(self, mock_get_governor): + mock_get_governor.return_value = 'fake_governor' + self.assertEqual('fake_governor', self.core_1.governor) + mock_get_governor.assert_called_once_with(self.core_1.ident) + + @mock.patch.object(core, 'set_governor') + def test_set_governor_low(self, mock_set_governor): + self.flags(cpu_power_governor_low='fake_low_gov', group='libvirt') + self.core_1.set_low_governor() + mock_set_governor.assert_called_once_with(self.core_1.ident, + 'fake_low_gov') + + @mock.patch.object(core, 'set_governor') + def test_set_governor_high(self, mock_set_governor): + self.flags(cpu_power_governor_high='fake_high_gov', group='libvirt') + self.core_1.set_high_governor() + mock_set_governor.assert_called_once_with(self.core_1.ident, + 'fake_high_gov') + + @mock.patch.object(core, 'set_online') + def test_power_up_online(self, mock_online): + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_dedicated_set='0-1', group='compute') + + api.power_up(self.fake_inst) + # only core #0 can be set as core #2 is not on the dedicated set + # As a reminder, core(i).online calls set_online(i) + mock_online.assert_called_once_with(0) + + @mock.patch.object(core, 'set_governor') + def test_power_up_governor(self, mock_set_governor): + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_power_management_strategy='governor', group='libvirt') + self.flags(cpu_dedicated_set='0-1', group='compute') + + api.power_up(self.fake_inst) + # only core #0 can be set as core #2 is not on the dedicated set + # As a reminder, core(i).set_high_governor calls set_governor(i) + mock_set_governor.assert_called_once_with(0, 'performance') + + @mock.patch.object(core, 'set_online') + def test_power_up_skipped(self, mock_online): + self.flags(cpu_power_management=False, group='libvirt') + api.power_up(self.fake_inst) + mock_online.assert_not_called() + + @mock.patch.object(core, 'set_online') + def test_power_up_skipped_if_standard_instance(self, mock_online): + self.flags(cpu_power_management=True, group='libvirt') + api.power_up(objects.Instance(numa_topology=None)) + mock_online.assert_not_called() + + @mock.patch.object(core, 'set_offline') + def test_power_down_offline(self, mock_offline): + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_dedicated_set='0-1', group='compute') + + api.power_down(self.fake_inst) + # only core #0 can be set as core #2 is not on the dedicated set + # As a reminder, core(i).online calls set_online(i) + mock_offline.assert_called_once_with(0) + + @mock.patch.object(core, 'set_governor') + def test_power_down_governor(self, mock_set_governor): + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_power_management_strategy='governor', group='libvirt') + self.flags(cpu_dedicated_set='0-1', group='compute') + + api.power_down(self.fake_inst) + # only core #0 can be set as core #2 is not on the dedicated set + # As a reminder, core(i).set_high_governor calls set_governor(i) + mock_set_governor.assert_called_once_with(0, 'powersave') + + @mock.patch.object(core, 'set_offline') + def test_power_down_skipped(self, mock_offline): + self.flags(cpu_power_management=False, group='libvirt') + api.power_down(self.fake_inst) + mock_offline.assert_not_called() + + @mock.patch.object(core, 'set_offline') + def test_power_down_skipped_if_standard_instance(self, mock_offline): + self.flags(cpu_power_management=True, group='libvirt') + api.power_down(objects.Instance(numa_topology=None)) + mock_offline.assert_not_called() + + @mock.patch.object(core, 'set_offline') + def test_power_down_all_dedicated_cpus_offline(self, mock_offline): + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_dedicated_set='0-1', group='compute') + + api.power_down_all_dedicated_cpus() + # All dedicated CPUs are turned offline + mock_offline.assert_has_calls([mock.call(0), mock.call(1)]) + + @mock.patch.object(core, 'set_governor') + def test_power_down_all_dedicated_cpus_governor(self, mock_set_governor): + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_power_management_strategy='governor', group='libvirt') + self.flags(cpu_dedicated_set='0-1', group='compute') + + api.power_down_all_dedicated_cpus() + # All dedicated CPUs are turned offline + mock_set_governor.assert_has_calls([mock.call(0, 'powersave'), + mock.call(1, 'powersave')]) + + @mock.patch.object(core, 'set_offline') + def test_power_down_all_dedicated_cpus_skipped(self, mock_offline): + self.flags(cpu_power_management=False, group='libvirt') + api.power_down_all_dedicated_cpus() + mock_offline.assert_not_called() + + def test_power_down_all_dedicated_cpus_wrong_config(self): + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_dedicated_set=None, group='compute') + self.assertRaises(exception.InvalidConfiguration, + api.power_down_all_dedicated_cpus) + + @mock.patch.object(core, 'get_governor') + @mock.patch.object(core, 'get_online') + def test_validate_all_dedicated_cpus_for_governor(self, mock_get_online, + mock_get_governor): + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_dedicated_set='0-1', group='compute') + self.flags(cpu_power_management_strategy='governor', group='libvirt') + mock_get_governor.return_value = 'performance' + mock_get_online.side_effect = (True, False) + self.assertRaises(exception.InvalidConfiguration, + api.validate_all_dedicated_cpus) + + @mock.patch.object(core, 'get_governor') + @mock.patch.object(core, 'get_online') + def test_validate_all_dedicated_cpus_for_cpu_state(self, mock_get_online, + mock_get_governor): + self.flags(cpu_power_management=True, group='libvirt') + self.flags(cpu_dedicated_set='0-1', group='compute') + self.flags(cpu_power_management_strategy='cpu_state', group='libvirt') + mock_get_online.return_value = True + mock_get_governor.side_effect = ('powersave', 'performance') + self.assertRaises(exception.InvalidConfiguration, + api.validate_all_dedicated_cpus) diff --git a/nova/tests/unit/virt/libvirt/cpu/test_core.py b/nova/tests/unit/virt/libvirt/cpu/test_core.py new file mode 100644 index 0000000000..a3cba00d3b --- /dev/null +++ b/nova/tests/unit/virt/libvirt/cpu/test_core.py @@ -0,0 +1,122 @@ +# 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 nova import exception +from nova import test +from nova.tests import fixtures +from nova.virt.libvirt.cpu import core + + +class TestCore(test.NoDBTestCase): + + @mock.patch.object(core.filesystem, 'read_sys') + @mock.patch.object(core.hardware, 'parse_cpu_spec') + def test_get_available_cores(self, mock_parse_cpu_spec, mock_read_sys): + mock_read_sys.return_value = '1-2' + mock_parse_cpu_spec.return_value = set([1, 2]) + self.assertEqual(set([1, 2]), core.get_available_cores()) + mock_read_sys.assert_called_once_with(core.AVAILABLE_PATH) + mock_parse_cpu_spec.assert_called_once_with('1-2') + + @mock.patch.object(core.filesystem, 'read_sys') + @mock.patch.object(core.hardware, 'parse_cpu_spec') + def test_get_available_cores_none( + self, mock_parse_cpu_spec, mock_read_sys): + mock_read_sys.return_value = '' + self.assertEqual(set(), core.get_available_cores()) + mock_parse_cpu_spec.assert_not_called() + + @mock.patch.object(core, 'get_available_cores') + def test_exists(self, mock_get_available_cores): + mock_get_available_cores.return_value = set([1]) + self.assertTrue(core.exists(1)) + mock_get_available_cores.assert_called_once_with() + self.assertFalse(core.exists(2)) + + @mock.patch.object( + core, 'CPU_PATH_TEMPLATE', + new_callable=mock.PropertyMock(return_value='/sys/blah%(core)s')) + @mock.patch.object(core, 'exists') + def test_gen_cpu_path(self, mock_exists, mock_cpu_path): + mock_exists.return_value = True + self.assertEqual('/sys/blah1', core.gen_cpu_path(1)) + mock_exists.assert_called_once_with(1) + + @mock.patch.object(core, 'exists') + def test_gen_cpu_path_raises(self, mock_exists): + mock_exists.return_value = False + self.assertRaises(ValueError, core.gen_cpu_path, 1) + self.assertIn('Unable to access CPU: 1', self.stdlog.logger.output) + + +class TestCoreHelpers(test.NoDBTestCase): + + def setUp(self): + super(TestCoreHelpers, self).setUp() + self.useFixture(fixtures.PrivsepFixture()) + _p1 = mock.patch.object(core, 'exists', return_value=True) + self.mock_exists = _p1.start() + self.addCleanup(_p1.stop) + + _p2 = mock.patch.object(core, 'gen_cpu_path', + side_effect=lambda x: '/fakesys/blah%s' % x) + self.mock_gen_cpu_path = _p2.start() + self.addCleanup(_p2.stop) + + @mock.patch.object(core.filesystem, 'read_sys') + def test_get_online(self, mock_read_sys): + mock_read_sys.return_value = '1' + self.assertTrue(core.get_online(1)) + mock_read_sys.assert_called_once_with('/fakesys/blah1/online') + + @mock.patch.object(core.filesystem, 'read_sys') + def test_get_online_not_exists(self, mock_read_sys): + mock_read_sys.side_effect = exception.FileNotFound(file_path='foo') + self.assertTrue(core.get_online(1)) + mock_read_sys.assert_called_once_with('/fakesys/blah1/online') + + @mock.patch.object(core.filesystem, 'write_sys') + @mock.patch.object(core, 'get_online') + def test_set_online(self, mock_get_online, mock_write_sys): + mock_get_online.return_value = True + self.assertTrue(core.set_online(1)) + mock_write_sys.assert_called_once_with('/fakesys/blah1/online', + data='1') + mock_get_online.assert_called_once_with(1) + + @mock.patch.object(core.filesystem, 'write_sys') + @mock.patch.object(core, 'get_online') + def test_set_offline(self, mock_get_online, mock_write_sys): + mock_get_online.return_value = False + self.assertTrue(core.set_offline(1)) + mock_write_sys.assert_called_once_with('/fakesys/blah1/online', + data='0') + mock_get_online.assert_called_once_with(1) + + @mock.patch.object(core.filesystem, 'read_sys') + def test_get_governor(self, mock_read_sys): + mock_read_sys.return_value = 'fake_gov' + self.assertEqual('fake_gov', core.get_governor(1)) + mock_read_sys.assert_called_once_with( + '/fakesys/blah1/cpufreq/scaling_governor') + + @mock.patch.object(core, 'get_governor') + @mock.patch.object(core.filesystem, 'write_sys') + def test_set_governor(self, mock_write_sys, mock_get_governor): + mock_get_governor.return_value = 'fake_gov' + self.assertEqual('fake_gov', + core.set_governor(1, 'fake_gov')) + mock_write_sys.assert_called_once_with( + '/fakesys/blah1/cpufreq/scaling_governor', data='fake_gov') + mock_get_governor.assert_called_once_with(1) 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 e9b7a2133e..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') @@ -9190,6 +9254,34 @@ class LibvirtConnTestCase(test.NoDBTestCase, drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) self.assertRaises(exception.Invalid, drvr._get_pcpu_available) + @mock.patch('nova.virt.libvirt.host.Host.get_available_cpus', + return_value=set([0, 1, 2, 3])) + def test_get_pcpu_available_for_power_mgmt(self, get_available_cpus): + """Test what happens when the '[compute] cpu_dedicated_set' config + option is set and power management is defined. + """ + self.flags(vcpu_pin_set=None) + self.flags(cpu_dedicated_set='2-3', cpu_shared_set=None, + group='compute') + self.flags(cpu_power_management=True, group='libvirt') + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + pcpus = drvr._get_pcpu_available() + self.assertEqual(set([2, 3]), pcpus) + + @mock.patch('nova.virt.libvirt.host.Host.get_available_cpus', + return_value=set([4, 5])) + def test_get_pcpu_available__cpu_dedicated_set_invalid_for_pm(self, + get_available_cpus): + """Test what happens when the '[compute] cpu_dedicated_set' config + option is set but it's invalid with power management set. + """ + self.flags(vcpu_pin_set=None) + self.flags(cpu_dedicated_set='4-6', cpu_shared_set=None, + group='compute') + self.flags(cpu_power_management=True, group='libvirt') + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + self.assertRaises(exception.Invalid, drvr._get_pcpu_available) + @mock.patch('nova.virt.libvirt.host.Host.get_online_cpus', return_value=set([0, 1, 2, 3])) def test_get_vcpu_available(self, get_online_cpus): diff --git a/nova/tests/unit/virt/libvirt/test_host.py b/nova/tests/unit/virt/libvirt/test_host.py index 3afd6c139d..631b10d81a 100644 --- a/nova/tests/unit/virt/libvirt/test_host.py +++ b/nova/tests/unit/virt/libvirt/test_host.py @@ -1052,6 +1052,12 @@ Active: 8381604 kB 'iowait': 6121490000000}, stats) + @mock.patch.object(fakelibvirt.virConnect, "getCPUMap") + def test_get_available_cpus(self, mock_map): + mock_map.return_value = (4, [True, True, False, False], None) + result = self.host.get_available_cpus() + self.assertEqual(result, {0, 1, 2, 3}) + @mock.patch.object(fakelibvirt.virConnect, "defineXML") def test_write_instance_config(self, mock_defineXML): fake_dom_xml = """ 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/cpu/__init__.py b/nova/virt/libvirt/cpu/__init__.py new file mode 100644 index 0000000000..4410a4e579 --- /dev/null +++ b/nova/virt/libvirt/cpu/__init__.py @@ -0,0 +1,22 @@ +# 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 nova.virt.libvirt.cpu import api + + +Core = api.Core + + +power_up = api.power_up +power_down = api.power_down +validate_all_dedicated_cpus = api.validate_all_dedicated_cpus +power_down_all_dedicated_cpus = api.power_down_all_dedicated_cpus diff --git a/nova/virt/libvirt/cpu/api.py b/nova/virt/libvirt/cpu/api.py new file mode 100644 index 0000000000..1c17458d6b --- /dev/null +++ b/nova/virt/libvirt/cpu/api.py @@ -0,0 +1,157 @@ +# 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 dataclasses import dataclass + +from oslo_log import log as logging + +import nova.conf +from nova import exception +from nova.i18n import _ +from nova import objects +from nova.virt import hardware +from nova.virt.libvirt.cpu import core + +LOG = logging.getLogger(__name__) + +CONF = nova.conf.CONF + + +@dataclass +class Core: + """Class to model a CPU core as reported by sysfs. + + It may be a physical CPU core or a hardware thread on a shared CPU core + depending on if the system supports SMT. + """ + + # NOTE(sbauza): ident is a mandatory field. + # The CPU core id/number + ident: int + + @property + def online(self) -> bool: + return core.get_online(self.ident) + + @online.setter + def online(self, state: bool) -> None: + if state: + core.set_online(self.ident) + else: + core.set_offline(self.ident) + + def __hash__(self): + return hash(self.ident) + + def __eq__(self, other): + return self.ident == other.ident + + def __str__(self): + return str(self.ident) + + @property + def governor(self) -> str: + return core.get_governor(self.ident) + + def set_high_governor(self) -> None: + core.set_governor(self.ident, CONF.libvirt.cpu_power_governor_high) + + def set_low_governor(self) -> None: + core.set_governor(self.ident, CONF.libvirt.cpu_power_governor_low) + + +def power_up(instance: objects.Instance) -> None: + if not CONF.libvirt.cpu_power_management: + return + if instance.numa_topology is None: + return + + cpu_dedicated_set = hardware.get_cpu_dedicated_set() or set() + pcpus = instance.numa_topology.cpu_pinning + powered_up = set() + for pcpu in pcpus: + if pcpu in cpu_dedicated_set: + pcpu = Core(pcpu) + if CONF.libvirt.cpu_power_management_strategy == 'cpu_state': + pcpu.online = True + else: + pcpu.set_high_governor() + powered_up.add(str(pcpu)) + LOG.debug("Cores powered up : %s", powered_up) + + +def power_down(instance: objects.Instance) -> None: + if not CONF.libvirt.cpu_power_management: + return + if instance.numa_topology is None: + return + + cpu_dedicated_set = hardware.get_cpu_dedicated_set() or set() + pcpus = instance.numa_topology.cpu_pinning + powered_down = set() + for pcpu in pcpus: + if pcpu in cpu_dedicated_set: + pcpu = Core(pcpu) + if CONF.libvirt.cpu_power_management_strategy == 'cpu_state': + pcpu.online = False + else: + pcpu.set_low_governor() + powered_down.add(str(pcpu)) + LOG.debug("Cores powered down : %s", powered_down) + + +def power_down_all_dedicated_cpus() -> None: + if not CONF.libvirt.cpu_power_management: + return + if (CONF.libvirt.cpu_power_management and + not CONF.compute.cpu_dedicated_set + ): + msg = _("'[compute]/cpu_dedicated_set' is mandatory to be set if " + "'[libvirt]/cpu_power_management' is set." + "Please provide the CPUs that can be pinned or don't use the " + "power management if you only use shared CPUs.") + raise exception.InvalidConfiguration(msg) + + cpu_dedicated_set = hardware.get_cpu_dedicated_set() or set() + for pcpu in cpu_dedicated_set: + pcpu = Core(pcpu) + if CONF.libvirt.cpu_power_management_strategy == 'cpu_state': + pcpu.online = False + else: + pcpu.set_low_governor() + LOG.debug("Cores powered down : %s", cpu_dedicated_set) + + +def validate_all_dedicated_cpus() -> None: + if not CONF.libvirt.cpu_power_management: + return + cpu_dedicated_set = hardware.get_cpu_dedicated_set() or set() + governors = set() + cpu_states = set() + for pcpu in cpu_dedicated_set: + pcpu = Core(pcpu) + # we need to collect the governors strategy and the CPU states + governors.add(pcpu.governor) + cpu_states.add(pcpu.online) + if CONF.libvirt.cpu_power_management_strategy == 'cpu_state': + # all the cores need to have the same governor strategy + if len(governors) > 1: + msg = _("All the cores need to have the same governor strategy" + "before modifying the CPU states. You can reboot the " + "compute node if you prefer.") + raise exception.InvalidConfiguration(msg) + elif CONF.libvirt.cpu_power_management_strategy == 'governor': + # all the cores need to be online + if False in cpu_states: + msg = _("All the cores need to be online before modifying the " + "governor strategy.") + raise exception.InvalidConfiguration(msg) diff --git a/nova/virt/libvirt/cpu/core.py b/nova/virt/libvirt/cpu/core.py new file mode 100644 index 0000000000..782f028fee --- /dev/null +++ b/nova/virt/libvirt/cpu/core.py @@ -0,0 +1,78 @@ +# 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. + +import os +import typing as ty + +from oslo_log import log as logging + +from nova import exception +from nova import filesystem +import nova.privsep +from nova.virt import hardware + +LOG = logging.getLogger(__name__) + +AVAILABLE_PATH = '/sys/devices/system/cpu/present' + +CPU_PATH_TEMPLATE = '/sys/devices/system/cpu/cpu%(core)s' + + +def get_available_cores() -> ty.Set[int]: + cores = filesystem.read_sys(AVAILABLE_PATH) + return hardware.parse_cpu_spec(cores) if cores else set() + + +def exists(core: int) -> bool: + return core in get_available_cores() + + +def gen_cpu_path(core: int) -> str: + if not exists(core): + LOG.warning('Unable to access CPU: %s', core) + raise ValueError('CPU: %(core)s does not exist', core) + return CPU_PATH_TEMPLATE % {'core': core} + + +def get_online(core: int) -> bool: + try: + online = filesystem.read_sys( + os.path.join(gen_cpu_path(core), 'online')).strip() + except exception.FileNotFound: + # The online file may not exist if we haven't written it yet. + # By default, this means that the CPU is online. + online = '1' + return online == '1' + + +@nova.privsep.sys_admin_pctxt.entrypoint +def set_online(core: int) -> bool: + filesystem.write_sys(os.path.join(gen_cpu_path(core), 'online'), data='1') + return get_online(core) + + +def set_offline(core: int) -> bool: + filesystem.write_sys(os.path.join(gen_cpu_path(core), 'online'), data='0') + return not get_online(core) + + +def get_governor(core: int) -> str: + return filesystem.read_sys( + os.path.join(gen_cpu_path(core), 'cpufreq/scaling_governor')).strip() + + +@nova.privsep.sys_admin_pctxt.entrypoint +def set_governor(core: int, governor: str) -> str: + filesystem.write_sys( + os.path.join(gen_cpu_path(core), 'cpufreq/scaling_governor'), + data=governor) + return get_governor(core) diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 542383cbad..869996f615 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -114,6 +114,7 @@ from nova.virt.image import model as imgmodel from nova.virt import images from nova.virt.libvirt import blockinfo from nova.virt.libvirt import config as vconfig +from nova.virt.libvirt import cpu as libvirt_cpu from nova.virt.libvirt import designer from nova.virt.libvirt import event as libvirtevent from nova.virt.libvirt import guest as libvirt_guest @@ -817,6 +818,18 @@ class LibvirtDriver(driver.ComputeDriver): "force_raw_images to True.") raise exception.InvalidConfiguration(msg) + # NOTE(sbauza): We verify first if the dedicated CPU performances were + # modified by Nova before. Note that it can provide an exception if + # either the governor strategies are different between the cores or if + # the cores are offline. + libvirt_cpu.validate_all_dedicated_cpus() + # NOTE(sbauza): We powerdown all dedicated CPUs but if some instances + # exist that are pinned for some CPUs, then we'll later powerup those + # CPUs when rebooting the instance in _init_instance() + # Note that it can provide an exception if the config options are + # wrongly modified. + libvirt_cpu.power_down_all_dedicated_cpus() + # TODO(sbauza): Remove this code once mediated devices are persisted # across reboots. self._recreate_assigned_mediated_devices() @@ -1512,6 +1525,8 @@ class LibvirtDriver(driver.ComputeDriver): # NOTE(GuanQiang): teardown container to avoid resource leak if CONF.libvirt.virt_type == 'lxc': self._teardown_container(instance) + # We're sure the instance is gone, we can shutdown the core if so + libvirt_cpu.power_down(instance) def destroy(self, context, instance, network_info, block_device_info=None, destroy_disks=True, destroy_secrets=True): @@ -3164,6 +3179,7 @@ class LibvirtDriver(driver.ComputeDriver): current_power_state = guest.get_power_state(self._host) + libvirt_cpu.power_up(instance) # TODO(stephenfin): Any reason we couldn't use 'self.resume' here? guest.launch(pause=current_power_state == power_state.PAUSED) @@ -7300,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 @@ -7641,6 +7662,7 @@ class LibvirtDriver(driver.ComputeDriver): post_xml_callback() if power_on or pause: + libvirt_cpu.power_up(instance) guest.launch(pause=pause) return guest @@ -7745,15 +7767,18 @@ class LibvirtDriver(driver.ComputeDriver): if not CONF.compute.cpu_dedicated_set: return set() - online_cpus = self._host.get_online_cpus() + if CONF.libvirt.cpu_power_management: + available_cpus = self._host.get_available_cpus() + else: + available_cpus = self._host.get_online_cpus() dedicated_cpus = hardware.get_cpu_dedicated_set() - if not dedicated_cpus.issubset(online_cpus): + if not dedicated_cpus.issubset(available_cpus): msg = _("Invalid '[compute] cpu_dedicated_set' config: one or " - "more of the configured CPUs is not online. Online " - "cpuset(s): %(online)s, configured cpuset(s): %(req)s") + "more of the configured CPUs is not available. Available " + "cpuset(s): %(available)s, configured cpuset(s): %(req)s") raise exception.Invalid(msg % { - 'online': sorted(online_cpus), + 'available': sorted(available_cpus), 'req': sorted(dedicated_cpus)}) return dedicated_cpus @@ -10061,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/nova/virt/libvirt/host.py b/nova/virt/libvirt/host.py index b986702401..9658a5791d 100644 --- a/nova/virt/libvirt/host.py +++ b/nova/virt/libvirt/host.py @@ -740,6 +740,14 @@ class Host(object): return doms + def get_available_cpus(self): + """Get the set of CPUs that exist on the host. + + :returns: set of CPUs, raises libvirtError on error + """ + cpus, cpu_map, online = self.get_connection().getCPUMap() + return {cpu for cpu in range(cpus)} + def get_online_cpus(self): """Get the set of CPUs that are online on the host |