diff --git a/nova/compute/claims.py b/nova/compute/claims.py index 046d17169236..4f5356ce78a8 100644 --- a/nova/compute/claims.py +++ b/nova/compute/claims.py @@ -42,10 +42,6 @@ class NopClaim(object): def memory_mb(self): return 0 - @property - def vcpus(self): - return 0 - def __enter__(self): return self @@ -57,8 +53,8 @@ class NopClaim(object): pass def __str__(self): - return "[Claim: %d MB memory, %d GB disk, %d VCPUS]" % (self.memory_mb, - self.disk_gb, self.vcpus) + return "[Claim: %d MB memory, %d GB disk]" % (self.memory_mb, + self.disk_gb) class Claim(NopClaim): @@ -102,10 +98,6 @@ class Claim(NopClaim): def memory_mb(self): return self.instance['memory_mb'] + self.overhead['memory_mb'] - @property - def vcpus(self): - return self.instance['vcpus'] - def abort(self): """Compute operation requiring claimed resources has failed or been aborted. @@ -130,18 +122,16 @@ class Claim(NopClaim): # unlimited: memory_mb_limit = limits.get('memory_mb') disk_gb_limit = limits.get('disk_gb') - vcpu_limit = limits.get('vcpu') msg = _("Attempting claim: memory %(memory_mb)d MB, disk %(disk_gb)d " - "GB, VCPUs %(vcpus)d") - params = {'memory_mb': self.memory_mb, 'disk_gb': self.disk_gb, - 'vcpus': self.vcpus} + "GB") + params = {'memory_mb': self.memory_mb, 'disk_gb': self.disk_gb} LOG.audit(msg % params, instance=self.instance) reasons = [self._test_memory(resources, memory_mb_limit), self._test_disk(resources, disk_gb_limit), - self._test_cpu(resources, vcpu_limit), self._test_pci()] + reasons = reasons + self._test_ext_resources(limits) reasons = [r for r in reasons if r is not None] if len(reasons) > 0: raise exception.ComputeResourcesUnavailable(reason= @@ -176,14 +166,9 @@ class Claim(NopClaim): if not can_claim: return _('Claim pci failed.') - def _test_cpu(self, resources, limit): - type_ = _("CPUs") - unit = "VCPUs" - total = resources['vcpus'] - used = resources['vcpus_used'] - requested = self.vcpus - - return self._test(type_, unit, total, used, requested, limit) + def _test_ext_resources(self, limits): + return self.tracker.ext_resources_handler.test_resources( + self.instance, limits) def _test(self, type_, unit, total, used, requested, limit): """Test if the given type of resource needed for a claim can be safely @@ -235,10 +220,6 @@ class ResizeClaim(Claim): def memory_mb(self): return self.instance_type['memory_mb'] + self.overhead['memory_mb'] - @property - def vcpus(self): - return self.instance_type['vcpus'] - def _test_pci(self): pci_requests = pci_request.get_instance_pci_requests( self.instance, 'new_') @@ -248,6 +229,10 @@ class ResizeClaim(Claim): if not claim: return _('Claim pci failed.') + def _test_ext_resources(self, limits): + return self.tracker.ext_resources_handler.test_resources( + self.instance_type, limits) + def abort(self): """Compute operation requiring claimed resources has failed or been aborted. diff --git a/nova/compute/resource_tracker.py b/nova/compute/resource_tracker.py index fb65f77c3a98..d1eb96cf727c 100644 --- a/nova/compute/resource_tracker.py +++ b/nova/compute/resource_tracker.py @@ -24,6 +24,7 @@ from oslo.config import cfg from nova.compute import claims from nova.compute import flavors from nova.compute import monitors +from nova.compute import resources as ext_resources from nova.compute import task_states from nova.compute import vm_states from nova import conductor @@ -46,7 +47,10 @@ resource_tracker_opts = [ help='Amount of memory in MB to reserve for the host'), cfg.StrOpt('compute_stats_class', default='nova.compute.stats.Stats', - help='Class that will manage stats for the local compute host') + help='Class that will manage stats for the local compute host'), + cfg.ListOpt('compute_resources', + default=['vcpu'], + help='The names of the extra resources to track.'), ] CONF = cfg.CONF @@ -75,6 +79,8 @@ class ResourceTracker(object): self.conductor_api = conductor.API() monitor_handler = monitors.ResourceMonitorHandler() self.monitors = monitor_handler.choose_monitors(self) + self.ext_resources_handler = \ + ext_resources.ResourceHandler(CONF.compute_resources) self.notifier = rpc.get_notifier() self.old_resources = {} @@ -229,12 +235,10 @@ class ResourceTracker(object): instance_type = self._get_instance_type(ctxt, instance, prefix) if instance_type['id'] == itype['id']: - self.stats.update_stats_for_migration(itype, sign=-1) if self.pci_tracker: self.pci_tracker.update_pci_for_migration(instance, sign=-1) self._update_usage(self.compute_node, itype, sign=-1) - self.compute_node['stats'] = jsonutils.dumps(self.stats) ctxt = context.get_admin_context() self._update(ctxt, self.compute_node) @@ -377,9 +381,20 @@ class ResourceTracker(object): LOG.info(_('Compute_service record updated for %(host)s:%(node)s') % {'host': self.host, 'node': self.nodename}) + def _write_ext_resources(self, resources): + resources['stats'] = {} + resources['stats'].update(self.stats) + self.ext_resources_handler.write_resources(resources) + def _create(self, context, values): """Create the compute node in the DB.""" # initialize load stats from existing instances: + self._write_ext_resources(values) + # NOTE(pmurray): the stats field is stored as a json string. The + # json conversion will be done automatically by the ComputeNode object + # so this can be removed when using ComputeNode. + values['stats'] = jsonutils.dumps(values['stats']) + self.compute_node = self.conductor_api.compute_node_create(context, values) @@ -449,10 +464,17 @@ class ResourceTracker(object): def _update(self, context, values): """Persist the compute node updates to the DB.""" + self._write_ext_resources(values) + # NOTE(pmurray): the stats field is stored as a json string. The + # json conversion will be done automatically by the ComputeNode object + # so this can be removed when using ComputeNode. + values['stats'] = jsonutils.dumps(values['stats']) + if not self._resource_change(values): return if "service" in self.compute_node: del self.compute_node['service'] + self.compute_node = self.conductor_api.compute_node_update( context, self.compute_node, values) if self.pci_tracker: @@ -475,7 +497,7 @@ class ResourceTracker(object): resources['local_gb_used']) resources['running_vms'] = self.stats.num_instances - resources['vcpus_used'] = self.stats.num_vcpus_used + self.ext_resources_handler.update_from_instance(usage, sign) def _update_usage_from_migration(self, context, instance, resources, migration): @@ -518,11 +540,9 @@ class ResourceTracker(object): migration['old_instance_type_id']) if itype: - self.stats.update_stats_for_migration(itype) if self.pci_tracker: self.pci_tracker.update_pci_for_migration(instance) self._update_usage(resources, itype) - resources['stats'] = jsonutils.dumps(self.stats) if self.pci_tracker: resources['pci_stats'] = jsonutils.dumps( self.pci_tracker.stats) @@ -595,7 +615,6 @@ class ResourceTracker(object): self._update_usage(resources, instance, sign=sign) resources['current_workload'] = self.stats.calculate_workload() - resources['stats'] = jsonutils.dumps(self.stats) if self.pci_tracker: resources['pci_stats'] = jsonutils.dumps(self.pci_tracker.stats) else: @@ -615,7 +634,6 @@ class ResourceTracker(object): # set some initial values, reserve room for host/hypervisor: resources['local_gb_used'] = CONF.reserved_host_disk_mb / 1024 resources['memory_mb_used'] = CONF.reserved_host_memory_mb - resources['vcpus_used'] = 0 resources['free_ram_mb'] = (resources['memory_mb'] - resources['memory_mb_used']) resources['free_disk_gb'] = (resources['local_gb'] - @@ -623,6 +641,9 @@ class ResourceTracker(object): resources['current_workload'] = 0 resources['running_vms'] = 0 + # Reset values for extended resources + self.ext_resources_handler.reset_resources(resources, self.driver) + for instance in instances: if instance['vm_state'] == vm_states.DELETED: continue diff --git a/nova/compute/resources/__init__.py b/nova/compute/resources/__init__.py new file mode 100644 index 000000000000..cb023ea523d1 --- /dev/null +++ b/nova/compute/resources/__init__.py @@ -0,0 +1,133 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import stevedore + +from nova.i18n import _LW +from nova.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + +RESOURCE_NAMESPACE = 'nova.compute.resources' + + +class ResourceHandler(): + + def _log_missing_plugins(self, names): + for name in names: + if name not in self._mgr.names(): + LOG.warn(_LW('Compute resource plugin %s was not loaded') % + name) + + def __init__(self, names, propagate_map_exceptions=False): + """Initialise the resource handler by loading the plugins. + + The ResourceHandler uses stevedore to load the resource plugins. + The handler can handle and report exceptions raised in the plugins + depending on the value of the propagate_map_exceptions parameter. + It is useful in testing to propagate exceptions so they are exposed + as part of the test. If exceptions are not propagated they are + logged at error level. + + Any named plugins that are not located are logged. + + :param names: the list of plugins to load by name + :param propagate_map_exceptions: True indicates exceptions in the + plugins should be raised, False indicates they should be handled and + logged. + """ + self._mgr = stevedore.NamedExtensionManager( + namespace=RESOURCE_NAMESPACE, + names=names, + propagate_map_exceptions=propagate_map_exceptions, + invoke_on_load=True) + self._log_missing_plugins(names) + + def reset_resources(self, resources, driver): + """Reset the resources to their initial state. + + Each plugin is called to reset its state. The resources data provided + is initial state gathered from the hypervisor. The driver is also + provided in case the plugin needs to obtain additional information + from the driver, for example, the memory calculation obtains + the memory overhead from the driver. + + :param resources: the resources reported by the hypervisor + :param driver: the driver for the hypervisor + + :returns: None + """ + if self._mgr.extensions: + self._mgr.map_method('reset', resources, driver) + + def test_resources(self, usage, limits): + """Test the ability to support the given instance. + + Each resource plugin is called to determine if it's resource is able + to support the additional requirements of a new instance. The + plugins either return None to indicate they have sufficient resource + available or a human readable string to indicate why they can not. + + :param usage: the additional resource usage + :param limits: limits used for the calculation + + :returns: a list or return values from the plugins + """ + if not self._mgr.extensions: + return [] + + reasons = self._mgr.map_method('test', usage, limits) + return reasons + + def update_from_instance(self, usage, sign=1): + """Update the resource information to reflect the allocation for + an instance with the given resource usage. + + :param usage: the resource usage of the instance + :param sign: has value 1 or -1. 1 indicates the instance is being + added to the current usage, -1 indicates the instance is being removed. + + :returns: None + """ + if not self._mgr.extensions: + return + + if sign == 1: + self._mgr.map_method('add_instance', usage) + else: + self._mgr.map_method('remove_instance', usage) + + def write_resources(self, resources): + """Write the resource data to populate the resources. + + Each resource plugin is called to write its resource data to + resources. + + :param resources: the compute node resources + + :returns: None + """ + if self._mgr.extensions: + self._mgr.map_method('write', resources) + + def report_free_resources(self): + """Each resource plugin is called to log free resource information. + + :returns: None + """ + if not self._mgr.extensions: + return + + self._mgr.map_method('report_free') diff --git a/nova/compute/resources/base.py b/nova/compute/resources/base.py new file mode 100644 index 000000000000..aebd29fb40ef --- /dev/null +++ b/nova/compute/resources/base.py @@ -0,0 +1,93 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class Resource(object): + """This base class defines the interface used for compute resource + plugins. It is not necessary to use this base class, but all compute + resource plugins must implement the abstract methods found here. + An instance of the plugin object is instantiated when it is loaded + by calling __init__() with no parameters. + """ + + @abc.abstractmethod + def reset(self, resources, driver): + """Set the resource to an initial state based on the resource + view discovered from the hypervisor. + """ + pass + + @abc.abstractmethod + def test(self, usage, limits): + """Test to see if we have sufficient resources to allocate for + an instance with the given resource usage. + + :param usage: the resource usage of the instances + :param limits: limits to apply + + :returns: None if the test passes or a string describing the reason + why the test failed + """ + pass + + @abc.abstractmethod + def add_instance(self, usage): + """Update resource information adding allocation according to the + given resource usage. + + :param usage: the resource usage of the instance being added + + :returns: None + """ + pass + + @abc.abstractmethod + def remove_instance(self, usage): + """Update resource information removing allocation according to the + given resource usage. + + :param usage: the resource usage of the instance being removed + + :returns: None + + """ + pass + + @abc.abstractmethod + def write(self, resources): + """Write resource data to populate resources. + + :param resources: the resources data to be populated + + :returns: None + """ + pass + + @abc.abstractmethod + def report_free(self): + """Log free resources. + + This method logs how much free resource is held by + the resource plugin. + + :returns: None + """ + pass diff --git a/nova/compute/resources/vcpu.py b/nova/compute/resources/vcpu.py new file mode 100644 index 000000000000..e7290a3e1a09 --- /dev/null +++ b/nova/compute/resources/vcpu.py @@ -0,0 +1,83 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.compute.resources import base +from nova.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +class VCPU(base.Resource): + """VCPU compute resource plugin. + + This is effectively a simple counter based on the vcpu requirement of each + instance. + """ + def __init__(self): + # initialize to a 'zero' resource. + # reset will be called to set real resource values + self._total = 0 + self._used = 0 + + def reset(self, resources, driver): + # total vcpu is reset to the value taken from resources. + self._total = int(resources['vcpus']) + self._used = 0 + + def _get_requested(self, usage): + return int(usage.get('vcpus', 0)) + + def _get_limit(self, limits): + if limits and 'vcpu' in limits: + return int(limits.get('vcpu')) + + def test(self, usage, limits): + requested = self._get_requested(usage) + limit = self._get_limit(limits) + + LOG.debug('Total CPUs: %(total)d VCPUs, used: %(used).02f VCPUs' % + {'total': self._total, 'used': self._used}) + + if limit is None: + # treat resource as unlimited: + LOG.debug('CPUs limit not specified, defaulting to unlimited') + return + + free = limit - self._used + + # Oversubscribed resource policy info: + LOG.debug('CPUs limit: %(limit).02f VCPUs, free: %(free).02f VCPUs' % + {'limit': limit, 'free': free}) + + if requested > free: + return ('Free CPUs %(free).02f VCPUs < ' + 'requested %(requested)d VCPUs' % + {'free': free, 'requested': requested}) + + def add_instance(self, usage): + requested = int(usage.get('vcpus', 0)) + self._used += requested + + def remove_instance(self, usage): + requested = int(usage.get('vcpus', 0)) + self._used -= requested + + def write(self, resources): + resources['vcpus'] = self._total + resources['vcpus_used'] = self._used + + def report_free(self): + free_vcpus = self._total - self._used + LOG.debug('Free VCPUs: %s' % free_vcpus) diff --git a/nova/compute/stats.py b/nova/compute/stats.py index bf183b012cc3..b347b8d5c09c 100644 --- a/nova/compute/stats.py +++ b/nova/compute/stats.py @@ -73,10 +73,6 @@ class Stats(dict): key = "num_os_type_%s" % os_type return self.get(key, 0) - @property - def num_vcpus_used(self): - return self.get("num_vcpus_used", 0) - def update_stats_for_instance(self, instance): """Update stats after an instance is changed.""" @@ -91,14 +87,12 @@ class Stats(dict): self._decrement("num_task_%s" % old_state['task_state']) self._decrement("num_os_type_%s" % old_state['os_type']) self._decrement("num_proj_%s" % old_state['project_id']) - x = self.get("num_vcpus_used", 0) - self["num_vcpus_used"] = x - old_state['vcpus'] else: # new instance self._increment("num_instances") # Now update stats from the new instance state: - (vm_state, task_state, os_type, project_id, vcpus) = \ + (vm_state, task_state, os_type, project_id) = \ self._extract_state_from_instance(instance) if vm_state == vm_states.DELETED: @@ -110,16 +104,10 @@ class Stats(dict): self._increment("num_task_%s" % task_state) self._increment("num_os_type_%s" % os_type) self._increment("num_proj_%s" % project_id) - x = self.get("num_vcpus_used", 0) - self["num_vcpus_used"] = x + vcpus # save updated I/O workload in stats: self["io_workload"] = self.io_workload - def update_stats_for_migration(self, instance_type, sign=1): - x = self.get("num_vcpus_used", 0) - self["num_vcpus_used"] = x + (sign * instance_type['vcpus']) - def _decrement(self, key): x = self.get(key, 0) self[key] = x - 1 @@ -136,10 +124,8 @@ class Stats(dict): task_state = instance['task_state'] os_type = instance['os_type'] project_id = instance['project_id'] - vcpus = instance['vcpus'] self.states[uuid] = dict(vm_state=vm_state, task_state=task_state, - os_type=os_type, project_id=project_id, - vcpus=vcpus) + os_type=os_type, project_id=project_id) - return (vm_state, task_state, os_type, project_id, vcpus) + return (vm_state, task_state, os_type, project_id) diff --git a/nova/tests/compute/fake_resource_tracker.py b/nova/tests/compute/fake_resource_tracker.py index c8f1e14647e4..b0fec2042b67 100644 --- a/nova/tests/compute/fake_resource_tracker.py +++ b/nova/tests/compute/fake_resource_tracker.py @@ -20,10 +20,12 @@ class FakeResourceTracker(resource_tracker.ResourceTracker): """Version without a DB requirement.""" def _create(self, context, values): + self._write_ext_resources(values) self.compute_node = values self.compute_node['id'] = 1 def _update(self, context, values, prune_stats=False): + self._write_ext_resources(values) self.compute_node.update(values) def _get_service(self, context): diff --git a/nova/tests/compute/test_claims.py b/nova/tests/compute/test_claims.py index be60f5401608..0df1875c17bf 100644 --- a/nova/tests/compute/test_claims.py +++ b/nova/tests/compute/test_claims.py @@ -25,10 +25,21 @@ from nova.pci import pci_manager from nova import test +class FakeResourceHandler(object): + test_called = False + usage_is_instance = False + + def test_resources(self, usage, limits): + self.test_called = True + self.usage_is_itype = usage.get('name') is 'fakeitype' + return [] + + class DummyTracker(object): icalled = False rcalled = False pci_tracker = pci_manager.PciDevTracker() + ext_resources_handler = FakeResourceHandler() def abort_instance_claim(self, *args, **kwargs): self.icalled = True @@ -101,9 +112,6 @@ class ClaimTestCase(test.NoDBTestCase): except e as ee: self.assertTrue(re.search(re_obj, str(ee))) - def test_cpu_unlimited(self): - self._claim(vcpus=100000) - def test_memory_unlimited(self): self._claim(memory_mb=99999999) @@ -113,10 +121,6 @@ class ClaimTestCase(test.NoDBTestCase): def test_disk_unlimited_ephemeral(self): self._claim(ephemeral_gb=999999) - def test_cpu_oversubscription(self): - limits = {'vcpu': 16} - self._claim(limits, vcpus=8) - def test_memory_with_overhead(self): overhead = {'memory_mb': 8} limits = {'memory_mb': 2048} @@ -131,11 +135,6 @@ class ClaimTestCase(test.NoDBTestCase): self._claim, limits=limits, overhead=overhead, memory_mb=2040) - def test_cpu_insufficient(self): - limits = {'vcpu': 16} - self.assertRaises(exception.ComputeResourcesUnavailable, - self._claim, limits=limits, vcpus=17) - def test_memory_oversubscription(self): self._claim(memory_mb=4096) @@ -162,21 +161,6 @@ class ClaimTestCase(test.NoDBTestCase): self._claim, limits=limits, root_gb=10, ephemeral_gb=40, memory_mb=16384) - def test_disk_and_cpu_insufficient(self): - limits = {'disk_gb': 45, 'vcpu': 16} - self.assertRaisesRegexp(re.compile("disk.*vcpus", re.IGNORECASE), - exception.ComputeResourcesUnavailable, - self._claim, limits=limits, root_gb=10, ephemeral_gb=40, - vcpus=17) - - def test_disk_and_cpu_and_memory_insufficient(self): - limits = {'disk_gb': 45, 'vcpu': 16, 'memory_mb': 8192} - pat = "memory.*disk.*vcpus" - self.assertRaisesRegexp(re.compile(pat, re.IGNORECASE), - exception.ComputeResourcesUnavailable, - self._claim, limits=limits, root_gb=10, ephemeral_gb=40, - vcpus=17, memory_mb=16384) - def test_pci_pass(self): dev_dict = { 'compute_node_id': 1, @@ -224,6 +208,11 @@ class ClaimTestCase(test.NoDBTestCase): self._set_pci_request(claim) claim._test_pci() + def test_ext_resources(self): + self._claim() + self.assertTrue(self.tracker.ext_resources_handler.test_called) + self.assertFalse(self.tracker.ext_resources_handler.usage_is_itype) + def test_abort(self): claim = self._abort() self.assertTrue(claim.tracker.icalled) @@ -260,6 +249,11 @@ class ResizeClaimTestCase(ClaimTestCase): claim.instance.update( system_metadata={'new_pci_requests': jsonutils.dumps(request)}) + def test_ext_resources(self): + self._claim() + self.assertTrue(self.tracker.ext_resources_handler.test_called) + self.assertTrue(self.tracker.ext_resources_handler.usage_is_itype) + def test_abort(self): claim = self._abort() self.assertTrue(claim.tracker.rcalled) diff --git a/nova/tests/compute/test_resource_tracker.py b/nova/tests/compute/test_resource_tracker.py index 364cfd6e2d72..06112e245a7b 100644 --- a/nova/tests/compute/test_resource_tracker.py +++ b/nova/tests/compute/test_resource_tracker.py @@ -22,6 +22,7 @@ from oslo.config import cfg from nova.compute import flavors from nova.compute import resource_tracker +from nova.compute import resources from nova.compute import task_states from nova.compute import vm_states from nova import context @@ -45,6 +46,7 @@ ROOT_GB = 5 EPHEMERAL_GB = 1 FAKE_VIRT_LOCAL_GB = ROOT_GB + EPHEMERAL_GB FAKE_VIRT_VCPUS = 1 +RESOURCE_NAMES = ['vcpu'] CONF = cfg.CONF @@ -160,8 +162,10 @@ class BaseTestCase(test.TestCase): "current_workload": 1, "running_vms": 0, "cpu_info": None, - "stats": [{"key": "num_instances", "value": "1"}], - "hypervisor_hostname": "fakenode", + "stats": { + "num_instances": "1", + }, + "hypervisor_hostname": "fakenode", } if values: compute.update(values) @@ -314,6 +318,8 @@ class BaseTestCase(test.TestCase): driver = self._driver() tracker = resource_tracker.ResourceTracker(host, driver, node) + tracker.ext_resources_handler = \ + resources.ResourceHandler(RESOURCE_NAMES, True) return tracker @@ -566,6 +572,38 @@ class TrackerPciStatsTestCase(BaseTrackerTestCase): return FakeVirtDriver(pci_support=True) +class TrackerExtraResourcesTestCase(BaseTrackerTestCase): + + def setUp(self): + super(TrackerExtraResourcesTestCase, self).setUp() + self.driver = self._driver() + + def _driver(self): + return FakeVirtDriver() + + def test_set_empty_ext_resources(self): + resources = self.driver.get_available_resource(self.tracker.nodename) + self.assertNotIn('stats', resources) + self.tracker._write_ext_resources(resources) + self.assertIn('stats', resources) + + def test_set_extra_resources(self): + def fake_write_resources(resources): + resources['stats']['resA'] = '123' + resources['stats']['resB'] = 12 + + self.stubs.Set(self.tracker.ext_resources_handler, + 'write_resources', + fake_write_resources) + + resources = self.driver.get_available_resource(self.tracker.nodename) + self.tracker._write_ext_resources(resources) + + expected = {"resA": "123", "resB": 12} + self.assertEqual(sorted(expected), + sorted(resources['stats'])) + + class InstanceClaimTestCase(BaseTrackerTestCase): def test_update_usage_only_for_tracked(self): diff --git a/nova/tests/compute/test_resources.py b/nova/tests/compute/test_resources.py new file mode 100644 index 000000000000..db2722ccb543 --- /dev/null +++ b/nova/tests/compute/test_resources.py @@ -0,0 +1,344 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests for the compute extra resources framework.""" + + +from oslo.config import cfg +from stevedore import extension +from stevedore import named + +from nova.compute import resources +from nova.compute.resources import base +from nova.compute.resources import vcpu +from nova import context +from nova.i18n import _ +from nova.objects import flavor as flavor_obj +from nova import test +from nova.tests.fake_instance import fake_instance_obj + +CONF = cfg.CONF + + +class FakeResourceHandler(resources.ResourceHandler): + def __init__(self, extensions): + self._mgr = \ + named.NamedExtensionManager.make_test_instance(extensions) + + +class FakeResource(base.Resource): + + def __init__(self): + self.total_res = 0 + self.used_res = 0 + + def _get_requested(self, usage): + if 'extra_specs' not in usage: + return + if self.resource_name not in usage['extra_specs']: + return + req = usage['extra_specs'][self.resource_name] + return int(req) + + def _get_limit(self, limits): + if self.resource_name not in limits: + return + limit = limits[self.resource_name] + return int(limit) + + def reset(self, resources, driver): + self.total_res = 0 + self.used_res = 0 + + def test(self, usage, limits): + requested = self._get_requested(usage) + if not requested: + return + + limit = self._get_limit(limits) + if not limit: + return + + free = limit - self.used_res + if requested <= free: + return + else: + return (_('Free %(free)d < requested %(requested)d ') % + {'free': free, 'requested': requested}) + + def add_instance(self, usage): + requested = self._get_requested(usage) + if requested: + self.used_res += requested + + def remove_instance(self, usage): + requested = self._get_requested(usage) + if requested: + self.used_res -= requested + + def write(self, resources): + pass + + def report_free(self): + return "Free %s" % (self.total_res - self.used_res) + + +class ResourceA(FakeResource): + + def reset(self, resources, driver): + # ResourceA uses a configuration option + self.total_res = int(CONF.resA) + self.used_res = 0 + self.resource_name = 'resource:resA' + + def write(self, resources): + resources['resA'] = self.total_res + resources['used_resA'] = self.used_res + + +class ResourceB(FakeResource): + + def reset(self, resources, driver): + # ResourceB uses resource details passed in parameter resources + self.total_res = resources['resB'] + self.used_res = 0 + self.resource_name = 'resource:resB' + + def write(self, resources): + resources['resB'] = self.total_res + resources['used_resB'] = self.used_res + + +def fake_flavor_obj(**updates): + flavor = flavor_obj.Flavor() + flavor.id = 1 + flavor.name = 'fakeflavor' + flavor.memory_mb = 8000 + flavor.vcpus = 3 + flavor.root_gb = 11 + flavor.ephemeral_gb = 4 + flavor.swap = 0 + flavor.rxtx_factor = 1.0 + flavor.vcpu_weight = 1 + if updates: + flavor.update(updates) + return flavor + + +class BaseTestCase(test.TestCase): + + def _initialize_used_res_counter(self): + # Initialize the value for the used resource + for ext in self.r_handler._mgr.extensions: + ext.obj.used_res = 0 + + def setUp(self): + super(BaseTestCase, self).setUp() + + # initialize flavors and stub get_by_id to + # get flavors from here + self._flavors = {} + self.ctxt = context.get_admin_context() + + # Create a flavor without extra_specs defined + _flavor_id = 1 + _flavor = fake_flavor_obj(id=_flavor_id) + self._flavors[_flavor_id] = _flavor + + # Create a flavor with extra_specs defined + _flavor_id = 2 + requested_resA = 5 + requested_resB = 7 + requested_resC = 7 + _extra_specs = {'resource:resA': requested_resA, + 'resource:resB': requested_resB, + 'resource:resC': requested_resC} + _flavor = fake_flavor_obj(id=_flavor_id, + extra_specs=_extra_specs) + self._flavors[_flavor_id] = _flavor + + # create fake resource extensions and resource handler + _extensions = [ + extension.Extension('resA', None, ResourceA, ResourceA()), + extension.Extension('resB', None, ResourceB, ResourceB()), + ] + self.r_handler = FakeResourceHandler(_extensions) + + # Resources details can be passed to each plugin or can be specified as + # configuration options + driver_resources = {'resB': 5} + CONF.resA = '10' + + # initialise the resources + self.r_handler.reset_resources(driver_resources, None) + + def test_update_from_instance_with_extra_specs(self): + # Flavor with extra_specs + _flavor_id = 2 + sign = 1 + self.r_handler.update_from_instance(self._flavors[_flavor_id], sign) + + expected_resA = self._flavors[_flavor_id].extra_specs['resource:resA'] + expected_resB = self._flavors[_flavor_id].extra_specs['resource:resB'] + self.assertEqual(int(expected_resA), + self.r_handler._mgr['resA'].obj.used_res) + self.assertEqual(int(expected_resB), + self.r_handler._mgr['resB'].obj.used_res) + + def test_update_from_instance_without_extra_specs(self): + # Flavor id without extra spec + _flavor_id = 1 + self._initialize_used_res_counter() + self.r_handler.resource_list = [] + sign = 1 + self.r_handler.update_from_instance(self._flavors[_flavor_id], sign) + self.assertEqual(0, self.r_handler._mgr['resA'].obj.used_res) + self.assertEqual(0, self.r_handler._mgr['resB'].obj.used_res) + + def test_write_resources(self): + self._initialize_used_res_counter() + extra_resources = {} + expected = {'resA': 10, 'used_resA': 0, 'resB': 5, 'used_resB': 0} + self.r_handler.write_resources(extra_resources) + self.assertEqual(expected, extra_resources) + + def test_test_resources_without_extra_specs(self): + limits = {} + # Flavor id without extra_specs + flavor = self._flavors[1] + result = self.r_handler.test_resources(flavor, limits) + self.assertEqual([None, None], result) + + def test_test_resources_with_limits_for_different_resource(self): + limits = {'resource:resC': 20} + # Flavor id with extra_specs + flavor = self._flavors[2] + result = self.r_handler.test_resources(flavor, limits) + self.assertEqual([None, None], result) + + def test_passing_test_resources(self): + limits = {'resource:resA': 10, 'resource:resB': 20} + # Flavor id with extra_specs + flavor = self._flavors[2] + self._initialize_used_res_counter() + result = self.r_handler.test_resources(flavor, limits) + self.assertEqual([None, None], result) + + def test_failing_test_resources_for_single_resource(self): + limits = {'resource:resA': 4, 'resource:resB': 20} + # Flavor id with extra_specs + flavor = self._flavors[2] + self._initialize_used_res_counter() + result = self.r_handler.test_resources(flavor, limits) + expected = ['Free 4 < requested 5 ', None] + self.assertEqual(sorted(expected), + sorted(result)) + + def test_empty_resource_handler(self): + """An empty resource handler has no resource extensions, + should have no effect, and should raise no exceptions. + """ + empty_r_handler = FakeResourceHandler([]) + + resources = {} + empty_r_handler.reset_resources(resources, None) + + flavor = self._flavors[1] + sign = 1 + empty_r_handler.update_from_instance(flavor, sign) + + limits = {} + test_result = empty_r_handler.test_resources(flavor, limits) + self.assertEqual([], test_result) + + sign = -1 + empty_r_handler.update_from_instance(flavor, sign) + + extra_resources = {} + expected_extra_resources = extra_resources + empty_r_handler.write_resources(extra_resources) + self.assertEqual(expected_extra_resources, extra_resources) + + empty_r_handler.report_free_resources() + + def test_vcpu_resource_load(self): + # load the vcpu example + names = ['vcpu'] + real_r_handler = resources.ResourceHandler(names) + ext_names = real_r_handler._mgr.names() + self.assertEqual(names, ext_names) + + # check the extension loaded is the one we expect + # and an instance of the object has been created + ext = real_r_handler._mgr['vcpu'] + self.assertIsInstance(ext.obj, vcpu.VCPU) + + +class TestVCPU(test.TestCase): + + def setUp(self): + super(TestVCPU, self).setUp() + self._vcpu = vcpu.VCPU() + self._vcpu._total = 10 + self._vcpu._used = 0 + self._flavor = fake_flavor_obj(vcpus=5) + self._big_flavor = fake_flavor_obj(vcpus=20) + self._instance = fake_instance_obj(None) + + def test_reset(self): + # set vcpu values to something different to test reset + self._vcpu._total = 10 + self._vcpu._used = 5 + + driver_resources = {'vcpus': 20} + self._vcpu.reset(driver_resources, None) + self.assertEqual(20, self._vcpu._total) + self.assertEqual(0, self._vcpu._used) + + def test_add_and_remove_instance(self): + self._vcpu.add_instance(self._flavor) + self.assertEqual(10, self._vcpu._total) + self.assertEqual(5, self._vcpu._used) + + self._vcpu.remove_instance(self._flavor) + self.assertEqual(10, self._vcpu._total) + self.assertEqual(0, self._vcpu._used) + + def test_test_pass_limited(self): + result = self._vcpu.test(self._flavor, {'vcpu': 10}) + self.assertIsNone(result, 'vcpu test failed when it should pass') + + def test_test_pass_unlimited(self): + result = self._vcpu.test(self._big_flavor, {}) + self.assertIsNone(result, 'vcpu test failed when it should pass') + + def test_test_fail(self): + result = self._vcpu.test(self._flavor, {'vcpu': 2}) + expected = _('Free CPUs 2.00 VCPUs < requested 5 VCPUs') + self.assertEqual(expected, result) + + def test_write(self): + resources = {'stats': {}} + self._vcpu.write(resources) + expected = { + 'vcpus': 10, + 'vcpus_used': 0, + 'stats': { + 'num_vcpus': 10, + 'num_vcpus_used': 0 + } + } + self.assertEqual(sorted(expected), + sorted(resources)) diff --git a/nova/tests/compute/test_stats.py b/nova/tests/compute/test_stats.py index 1864ac7950c4..c90314b0fccf 100644 --- a/nova/tests/compute/test_stats.py +++ b/nova/tests/compute/test_stats.py @@ -136,8 +136,6 @@ class StatsTestCase(test.NoDBTestCase): self.assertEqual(1, self.stats["num_vm_None"]) self.assertEqual(2, self.stats["num_vm_" + vm_states.BUILDING]) - self.assertEqual(10, self.stats.num_vcpus_used) - def test_calculate_workload(self): self.stats._increment("num_task_None") self.stats._increment("num_task_" + task_states.SCHEDULING) @@ -191,7 +189,6 @@ class StatsTestCase(test.NoDBTestCase): self.assertEqual(0, self.stats.num_instances_for_project("1234")) self.assertEqual(0, self.stats.num_os_type("Linux")) self.assertEqual(0, self.stats["num_vm_" + vm_states.BUILDING]) - self.assertEqual(0, self.stats.num_vcpus_used) def test_io_workload(self): vms = [vm_states.ACTIVE, vm_states.BUILDING, vm_states.PAUSED] diff --git a/setup.cfg b/setup.cfg index cb8c651ff2cb..50c185cf3018 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,8 @@ packages = nova [entry_points] +nova.compute.resources = + vcpu = nova.compute.resources.vcpu:VCPU nova.image.download.modules = file = nova.image.download.file console_scripts =