Merge "Add extensible resources to resource tracker"

This commit is contained in:
Jenkins 2014-07-23 13:03:34 +00:00 committed by Gerrit Code Review
commit 0701dcc247
12 changed files with 762 additions and 84 deletions

View File

@ -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.

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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))

View File

@ -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]

View File

@ -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 =