Merge "Add extensible resources to resource tracker"
This commit is contained in:
commit
0701dcc247
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue