summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--nova/compute/claims.py39
-rw-r--r--nova/compute/resource_tracker.py37
-rw-r--r--nova/compute/resources/__init__.py133
-rw-r--r--nova/compute/resources/base.py93
-rw-r--r--nova/compute/resources/vcpu.py83
-rw-r--r--nova/compute/stats.py20
-rw-r--r--nova/tests/compute/fake_resource_tracker.py2
-rw-r--r--nova/tests/compute/test_claims.py48
-rw-r--r--nova/tests/compute/test_resource_tracker.py42
-rw-r--r--nova/tests/compute/test_resources.py344
-rw-r--r--nova/tests/compute/test_stats.py3
-rw-r--r--setup.cfg2
12 files changed, 762 insertions, 84 deletions
diff --git a/nova/compute/claims.py b/nova/compute/claims.py
index 046d171692..4f5356ce78 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 fb65f77c3a..d1eb96cf72 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 0000000000..cb023ea523
--- /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 0000000000..aebd29fb40
--- /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 0000000000..e7290a3e1a
--- /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 bf183b012c..b347b8d5c0 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 c8f1e14647..b0fec2042b 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 be60f54016..0df1875c17 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 364cfd6e2d..06112e245a 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 0000000000..db2722ccb5
--- /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 1864ac7950..c90314b0fc 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 cb8c651ff2..50c185cf30 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 =