Separate scheduler host management
This is a bit of a scheduler refactoring to support future scaling work as a part of blueprint scaling-zones. Also fixes bug 891971 (remove unused set_network_host in scheduler) With or without that blueprint work, this cleans a number of things up and paves the way for combining a lot of the schedulers by using this new 'HostManager' for filtering and weighing (future work :) On to the goodies: Introduces new HostManager, splitting code out from ZoneManager. Zone communication and management is handlded in the ZoneManager. Host filtering and weighing is handled in the HostManager. ZoneManager is removed from the SchedulerManager and direct calls to it from the SchedulerManager now occur via the scheduler driver. This simplifies a number of things. ZoneManager and HostManager classes to use are now flags. This allows one to extend the current classes and use them. HostManager uses a HostState class to keep info about hosts. This class needs to be extendable. Since it's very much tied to the HostManager, the HostState class to use is not a flag. It is, instead, a class variable in HostManager. Filtering functions now accept a single host to filter. This improves performance by not having to build a new array of hosts within every filter function. Filtering functions now accept a more generic 'filter_properties' dictionary which we can fill with information available for filtering. Adding additional data to this 'filter_properties' can be done by subclassing. Weighing functions now accept this 'filter_properties', also, although it's poorly named ATM. Will be cleaned up in a future branch when I move some weighing functions into the host manager. Filtering tests are true unit tests now. test_zones was moved from top level to under scheduler as zone_manager tests and refactored to be true unit tests. Host tests are true unit tests now. Other minor cleanups Change-Id: I0ef2acef6639b4500c400c18cf2c673cb80f0150
This commit is contained in:
parent
ad88437401
commit
d328ddcadb
@ -133,16 +133,12 @@ class Controller(object):
|
||||
def info(self, req):
|
||||
"""Return name and capabilities for this zone."""
|
||||
context = req.environ['nova.context']
|
||||
items = nova.scheduler.api.get_zone_capabilities(context)
|
||||
|
||||
zone = dict(name=FLAGS.zone_name)
|
||||
caps = FLAGS.zone_capabilities
|
||||
for cap in caps:
|
||||
key, value = cap.split('=')
|
||||
zone[key] = value
|
||||
for item, (min_value, max_value) in items.iteritems():
|
||||
zone[item] = "%s,%s" % (min_value, max_value)
|
||||
return dict(zone=zone)
|
||||
zone_capabs = nova.scheduler.api.get_zone_capabilities(context)
|
||||
# NOTE(comstud): This should probably return, instead:
|
||||
# {'zone': {'name': FLAGS.zone_name,
|
||||
# 'capabilities': zone_capabs}}
|
||||
zone_capabs['name'] = FLAGS.zone_name
|
||||
return dict(zone=zone_capabs)
|
||||
|
||||
@wsgi.serializers(xml=ZoneTemplate)
|
||||
def show(self, req, id):
|
||||
|
@ -87,7 +87,20 @@ def zone_update(context, zone_id, data):
|
||||
|
||||
def get_zone_capabilities(context):
|
||||
"""Returns a dict of key, value capabilities for this zone."""
|
||||
return _call_scheduler('get_zone_capabilities', context=context)
|
||||
|
||||
zone_capabs = {}
|
||||
|
||||
# First grab the capabilities of combined services.
|
||||
service_capabs = _call_scheduler('get_service_capabilities', context)
|
||||
for item, (min_value, max_value) in service_capabs.iteritems():
|
||||
zone_capabs[item] = "%s,%s" % (min_value, max_value)
|
||||
|
||||
# Add the capabilities defined by FLAGS
|
||||
caps = FLAGS.zone_capabilities
|
||||
for cap in caps:
|
||||
key, value = cap.split('=')
|
||||
zone_capabs[key] = value
|
||||
return zone_capabs
|
||||
|
||||
|
||||
def select(context, specs=None):
|
||||
|
@ -67,12 +67,11 @@ class ChanceScheduler(driver.Scheduler):
|
||||
|
||||
def schedule_run_instance(self, context, request_spec, *_args, **kwargs):
|
||||
"""Create and run an instance or instances"""
|
||||
elevated = context.elevated()
|
||||
num_instances = request_spec.get('num_instances', 1)
|
||||
instances = []
|
||||
for num in xrange(num_instances):
|
||||
host = self._schedule(context, 'compute', request_spec, **kwargs)
|
||||
instance = self.create_instance_db_entry(elevated, request_spec)
|
||||
instance = self.create_instance_db_entry(context, request_spec)
|
||||
driver.cast_to_compute_host(context, host,
|
||||
'run_instance', instance_uuid=instance['uuid'], **kwargs)
|
||||
instances.append(driver.encode_instance(instance))
|
||||
@ -85,4 +84,4 @@ class ChanceScheduler(driver.Scheduler):
|
||||
def schedule_prep_resize(self, context, request_spec, *args, **kwargs):
|
||||
"""Select a target for resize."""
|
||||
host = self._schedule(context, 'compute', request_spec, **kwargs)
|
||||
driver.cast_to_host(context, 'compute', host, 'prep_resize', **kwargs)
|
||||
driver.cast_to_compute_host(context, host, 'prep_resize', **kwargs)
|
||||
|
@ -33,16 +33,13 @@ from nova import flags
|
||||
from nova import log as logging
|
||||
from nova.scheduler import api
|
||||
from nova.scheduler import driver
|
||||
from nova.scheduler import host_manager
|
||||
from nova.scheduler import least_cost
|
||||
from nova.scheduler import scheduler_options
|
||||
from nova import utils
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_list('default_host_filters', ['InstanceTypeFilter'],
|
||||
'Which filters to use for filtering hosts when not specified '
|
||||
'in the request.')
|
||||
|
||||
LOG = logging.getLogger('nova.scheduler.distributed_scheduler')
|
||||
|
||||
|
||||
@ -108,11 +105,11 @@ class DistributedScheduler(driver.Scheduler):
|
||||
weighted_host = weighted_hosts.pop(0)
|
||||
|
||||
instance = None
|
||||
if weighted_host.host:
|
||||
instance = self._provision_resource_locally(elevated,
|
||||
if weighted_host.zone:
|
||||
instance = self._ask_child_zone_to_create_instance(elevated,
|
||||
weighted_host, request_spec, kwargs)
|
||||
else:
|
||||
instance = self._ask_child_zone_to_create_instance(elevated,
|
||||
instance = self._provision_resource_locally(elevated,
|
||||
weighted_host, request_spec, kwargs)
|
||||
|
||||
if instance:
|
||||
@ -145,8 +142,8 @@ class DistributedScheduler(driver.Scheduler):
|
||||
host = hosts.pop(0)
|
||||
|
||||
# Forward off to the host
|
||||
driver.cast_to_host(context, 'compute', host.host, 'prep_resize',
|
||||
**kwargs)
|
||||
driver.cast_to_compute_host(context, host.host_state.host,
|
||||
'prep_resize', **kwargs)
|
||||
|
||||
def select(self, context, request_spec, *args, **kwargs):
|
||||
"""Select returns a list of weights and zone/host information
|
||||
@ -167,7 +164,7 @@ class DistributedScheduler(driver.Scheduler):
|
||||
kwargs):
|
||||
"""Create the requested resource in this Zone."""
|
||||
instance = self.create_instance_db_entry(context, request_spec)
|
||||
driver.cast_to_compute_host(context, weighted_host.host,
|
||||
driver.cast_to_compute_host(context, weighted_host.host_state.host,
|
||||
'run_instance', instance_uuid=instance['uuid'], **kwargs)
|
||||
inst = driver.encode_instance(instance, local=True)
|
||||
# So if another instance is created, create_instance_db_entry will
|
||||
@ -189,7 +186,8 @@ class DistributedScheduler(driver.Scheduler):
|
||||
blob = wh_dict.get('blob', None)
|
||||
zone = wh_dict.get('zone', None)
|
||||
return least_cost.WeightedHost(wh_dict['weight'],
|
||||
host=host, blob=blob, zone=zone)
|
||||
host_state=host_manager.HostState(host, 'compute'),
|
||||
blob=blob, zone=zone)
|
||||
|
||||
except M2Crypto.EVP.EVPError:
|
||||
raise InvalidBlob()
|
||||
@ -265,8 +263,8 @@ class DistributedScheduler(driver.Scheduler):
|
||||
cooked_weight = offset + scale * raw_weight
|
||||
|
||||
weighted_hosts.append(least_cost.WeightedHost(
|
||||
host=None, weight=cooked_weight,
|
||||
zone=zone_id, blob=item['blob']))
|
||||
cooked_weight, zone=zone_id,
|
||||
blob=item['blob']))
|
||||
except KeyError:
|
||||
LOG.exception(_("Bad child zone scaling values "
|
||||
"for Zone: %(zone_id)s") % locals())
|
||||
@ -280,6 +278,17 @@ class DistributedScheduler(driver.Scheduler):
|
||||
"""Fetch options dictionary. Broken out for testing."""
|
||||
return self.options.get_configuration()
|
||||
|
||||
def populate_filter_properties(self, request_spec, filter_properties):
|
||||
"""Stuff things into filter_properties. Can be overriden in a
|
||||
subclass to add more data.
|
||||
"""
|
||||
try:
|
||||
if request_spec['avoid_original_host']:
|
||||
original_host = request_spec['instance_properties']['host']
|
||||
filter_properties['ignore_hosts'].append(original_host)
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
def _schedule(self, elevated, topic, request_spec, *args, **kwargs):
|
||||
"""Returns a list of hosts that meet the required specs,
|
||||
ordered by their fitness.
|
||||
@ -288,6 +297,7 @@ class DistributedScheduler(driver.Scheduler):
|
||||
msg = _("Scheduler only understands Compute nodes (for now)")
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
instance_properties = request_spec['instance_properties']
|
||||
instance_type = request_spec.get("instance_type", None)
|
||||
if not instance_type:
|
||||
msg = _("Scheduler only understands InstanceType-based" \
|
||||
@ -299,7 +309,13 @@ class DistributedScheduler(driver.Scheduler):
|
||||
ram_requirement_mb = instance_type['memory_mb']
|
||||
disk_requirement_gb = instance_type['local_gb']
|
||||
|
||||
options = self._get_configuration_options()
|
||||
config_options = self._get_configuration_options()
|
||||
|
||||
filter_properties = {'config_options': config_options,
|
||||
'instance_type': instance_type,
|
||||
'ignore_hosts': []}
|
||||
|
||||
self.populate_filter_properties(request_spec, filter_properties)
|
||||
|
||||
# Find our local list of acceptable hosts by repeatedly
|
||||
# filtering and weighing our options. Each time we choose a
|
||||
@ -307,33 +323,37 @@ class DistributedScheduler(driver.Scheduler):
|
||||
# selections can adjust accordingly.
|
||||
|
||||
# unfiltered_hosts_dict is {host : ZoneManager.HostInfo()}
|
||||
unfiltered_hosts_dict = self.zone_manager.get_all_host_data(elevated)
|
||||
unfiltered_hosts = unfiltered_hosts_dict.items()
|
||||
unfiltered_hosts_dict = self.host_manager.get_all_host_states(
|
||||
elevated, topic)
|
||||
hosts = unfiltered_hosts_dict.itervalues()
|
||||
|
||||
num_instances = request_spec.get('num_instances', 1)
|
||||
selected_hosts = []
|
||||
for num in xrange(num_instances):
|
||||
# Filter local hosts based on requirements ...
|
||||
filtered_hosts = self._filter_hosts(topic, request_spec,
|
||||
unfiltered_hosts, options)
|
||||
|
||||
if not filtered_hosts:
|
||||
hosts = self.host_manager.filter_hosts(hosts,
|
||||
filter_properties)
|
||||
if not hosts:
|
||||
# Can't get any more locally.
|
||||
break
|
||||
|
||||
LOG.debug(_("Filtered %(filtered_hosts)s") % locals())
|
||||
LOG.debug(_("Filtered %(hosts)s") % locals())
|
||||
|
||||
# weighted_host = WeightedHost() ... the best
|
||||
# host for the job.
|
||||
# TODO(comstud): filter_properties will also be used for
|
||||
# weighing and I plan fold weighing into the host manager
|
||||
# in a future patch. I'll address the naming of this
|
||||
# variable at that time.
|
||||
weighted_host = least_cost.weighted_sum(cost_functions,
|
||||
filtered_hosts, options)
|
||||
hosts, filter_properties)
|
||||
LOG.debug(_("Weighted %(weighted_host)s") % locals())
|
||||
selected_hosts.append(weighted_host)
|
||||
|
||||
# Now consume the resources so the filter/weights
|
||||
# will change for the next instance.
|
||||
weighted_host.hostinfo.consume_resources(disk_requirement_gb,
|
||||
ram_requirement_mb)
|
||||
weighted_host.host_state.consume_from_instance(
|
||||
instance_properties)
|
||||
|
||||
# Next, tack on the host weights from the child zones
|
||||
if not request_spec.get('local_zone', False):
|
||||
@ -346,72 +366,6 @@ class DistributedScheduler(driver.Scheduler):
|
||||
selected_hosts.sort(key=operator.attrgetter('weight'))
|
||||
return selected_hosts[:num_instances]
|
||||
|
||||
def _get_filter_classes(self):
|
||||
# Imported here to avoid circular imports
|
||||
from nova.scheduler import filters
|
||||
|
||||
def get_itm(nm):
|
||||
return getattr(filters, nm)
|
||||
|
||||
return [get_itm(itm) for itm in dir(filters)
|
||||
if isinstance(get_itm(itm), type)
|
||||
and issubclass(get_itm(itm), filters.AbstractHostFilter)
|
||||
and get_itm(itm) is not filters.AbstractHostFilter]
|
||||
|
||||
def _choose_host_filters(self, filters=None):
|
||||
"""Since the caller may specify which filters to use we need
|
||||
to have an authoritative list of what is permissible. This
|
||||
function checks the filter names against a predefined set
|
||||
of acceptable filters.
|
||||
"""
|
||||
if not filters:
|
||||
filters = FLAGS.default_host_filters
|
||||
if not isinstance(filters, (list, tuple)):
|
||||
filters = [filters]
|
||||
good_filters = []
|
||||
bad_filters = []
|
||||
filter_classes = self._get_filter_classes()
|
||||
for filter_name in filters:
|
||||
found_class = False
|
||||
for cls in filter_classes:
|
||||
if cls.__name__ == filter_name:
|
||||
good_filters.append(cls())
|
||||
found_class = True
|
||||
break
|
||||
if not found_class:
|
||||
bad_filters.append(filter_name)
|
||||
if bad_filters:
|
||||
msg = ", ".join(bad_filters)
|
||||
raise exception.SchedulerHostFilterNotFound(filter_name=msg)
|
||||
return good_filters
|
||||
|
||||
def _filter_hosts(self, topic, request_spec, hosts, options):
|
||||
"""Filter the full host list. hosts = [(host, HostInfo()), ...].
|
||||
This method returns a subset of hosts, in the same format."""
|
||||
selected_filters = self._choose_host_filters()
|
||||
|
||||
# Filter out original host
|
||||
try:
|
||||
if request_spec['avoid_original_host']:
|
||||
original_host = request_spec['instance_properties']['host']
|
||||
hosts = [(h, hi) for h, hi in hosts if h != original_host]
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
# TODO(sandy): We're only using InstanceType-based specs
|
||||
# currently. Later we'll need to snoop for more detailed
|
||||
# host filter requests.
|
||||
instance_type = request_spec.get("instance_type", None)
|
||||
if instance_type is None:
|
||||
# No way to select; return the specified hosts.
|
||||
return hosts
|
||||
|
||||
for selected_filter in selected_filters:
|
||||
query = selected_filter.instance_type_to_filter(instance_type)
|
||||
hosts = selected_filter.filter_hosts(hosts, query, options)
|
||||
|
||||
return hosts
|
||||
|
||||
def get_cost_functions(self, topic=None):
|
||||
"""Returns a list of tuples containing weights and cost functions to
|
||||
use for weighing hosts
|
||||
|
@ -21,22 +21,30 @@
|
||||
Scheduler base class that all Schedulers should inherit from
|
||||
"""
|
||||
|
||||
from nova.api.ec2 import ec2utils
|
||||
from nova.compute import api as compute_api
|
||||
from nova.compute import power_state
|
||||
from nova.compute import vm_states
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import rpc
|
||||
from nova.scheduler import host_manager
|
||||
from nova.scheduler import zone_manager
|
||||
from nova import utils
|
||||
from nova.compute import api as compute_api
|
||||
from nova.compute import power_state
|
||||
from nova.compute import vm_states
|
||||
from nova.api.ec2 import ec2utils
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
LOG = logging.getLogger('nova.scheduler.driver')
|
||||
flags.DEFINE_integer('service_down_time', 60,
|
||||
'maximum time since last check-in for up service')
|
||||
flags.DEFINE_string('scheduler_host_manager',
|
||||
'nova.scheduler.host_manager.HostManager',
|
||||
'The scheduler host manager class to use')
|
||||
flags.DEFINE_string('scheduler_zone_manager',
|
||||
'nova.scheduler.zone_manager.ZoneManager',
|
||||
'The scheduler zone manager class to use')
|
||||
flags.DECLARE('instances_path', 'nova.compute.manager')
|
||||
|
||||
|
||||
@ -113,20 +121,43 @@ def encode_instance(instance, local=True):
|
||||
if local:
|
||||
return dict(id=instance['id'], _is_precooked=False)
|
||||
else:
|
||||
instance['_is_precooked'] = True
|
||||
return instance
|
||||
inst = dict(instance)
|
||||
inst['_is_precooked'] = True
|
||||
return inst
|
||||
|
||||
|
||||
class Scheduler(object):
|
||||
"""The base class that all Scheduler classes should inherit from."""
|
||||
|
||||
def __init__(self):
|
||||
self.zone_manager = None
|
||||
self.zone_manager = utils.import_object(
|
||||
FLAGS.scheduler_zone_manager)
|
||||
self.host_manager = utils.import_object(
|
||||
FLAGS.scheduler_host_manager)
|
||||
self.compute_api = compute_api.API()
|
||||
|
||||
def set_zone_manager(self, zone_manager):
|
||||
"""Called by the Scheduler Service to supply a ZoneManager."""
|
||||
self.zone_manager = zone_manager
|
||||
def get_host_list(self):
|
||||
"""Get a list of hosts from the HostManager."""
|
||||
return self.host_manager.get_host_list()
|
||||
|
||||
def get_zone_list(self):
|
||||
"""Get a list of zones from the ZoneManager."""
|
||||
return self.zone_manager.get_zone_list()
|
||||
|
||||
def get_service_capabilities(self):
|
||||
"""Get the normalized set of capabilities for the services
|
||||
in this zone.
|
||||
"""
|
||||
return self.host_manager.get_service_capabilities()
|
||||
|
||||
def update_service_capabilities(self, service_name, host, capabilities):
|
||||
"""Process a capability update from a service node."""
|
||||
self.host_manager.update_service_capabilities(service_name,
|
||||
host, capabilities)
|
||||
|
||||
def poll_child_zones(self, context):
|
||||
"""Poll child zones periodically to get status."""
|
||||
return self.zone_manager.update(context)
|
||||
|
||||
@staticmethod
|
||||
def service_is_up(service):
|
||||
@ -140,7 +171,7 @@ class Scheduler(object):
|
||||
"""Return the list of hosts that have a running service for topic."""
|
||||
|
||||
services = db.service_get_all_by_topic(context, topic)
|
||||
return [service.host
|
||||
return [service['host']
|
||||
for service in services
|
||||
if self.service_is_up(service)]
|
||||
|
||||
@ -168,6 +199,10 @@ class Scheduler(object):
|
||||
"""Must override at least this method for scheduler to work."""
|
||||
raise NotImplementedError(_("Must implement a fallback schedule"))
|
||||
|
||||
def select(self, context, topic, method, *_args, **_kwargs):
|
||||
"""Must override this for zones to work."""
|
||||
raise NotImplementedError(_("Must implement 'select' method"))
|
||||
|
||||
def schedule_live_migration(self, context, instance_id, dest,
|
||||
block_migration=False,
|
||||
disk_over_commit=False):
|
||||
@ -232,7 +267,7 @@ class Scheduler(object):
|
||||
# to the instance.
|
||||
if len(instance_ref['volumes']) != 0:
|
||||
services = db.service_get_all_by_topic(context, 'volume')
|
||||
if len(services) < 1 or not self.service_is_up(services[0]):
|
||||
if len(services) < 1 or not self.service_is_up(services[0]):
|
||||
raise exception.VolumeServiceUnavailable()
|
||||
|
||||
# Checking src host exists and compute node
|
||||
@ -302,6 +337,7 @@ class Scheduler(object):
|
||||
reason = _("Block migration can not be used "
|
||||
"with shared storage.")
|
||||
raise exception.InvalidSharedStorage(reason=reason, path=dest)
|
||||
# FIXME(comstud): See LP891756.
|
||||
except exception.FileNotFound:
|
||||
if not block_migration:
|
||||
src = instance_ref['host']
|
||||
|
@ -32,5 +32,5 @@ InstanceType filter.
|
||||
|
||||
from abstract_filter import AbstractHostFilter
|
||||
from all_hosts_filter import AllHostsFilter
|
||||
from instance_type_filter import InstanceTypeFilter
|
||||
from compute_filter import ComputeFilter
|
||||
from json_filter import JsonFilter
|
||||
|
@ -16,13 +16,9 @@
|
||||
|
||||
class AbstractHostFilter(object):
|
||||
"""Base class for host filters."""
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Convert instance_type into a filter for most common use-case."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def filter_hosts(self, host_list, query, options):
|
||||
"""Return a list of hosts that fulfill the filter."""
|
||||
raise NotImplementedError()
|
||||
def host_passes(self, host_state, filter_properties):
|
||||
return True
|
||||
|
||||
def _full_name(self):
|
||||
"""module.classname of the filter."""
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# Copyright (c) 2011-2012 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -18,13 +18,7 @@ import abstract_filter
|
||||
|
||||
|
||||
class AllHostsFilter(abstract_filter.AbstractHostFilter):
|
||||
"""NOP host filter. Returns all hosts in ZoneManager."""
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Return anything to prevent base-class from raising
|
||||
exception.
|
||||
"""
|
||||
return instance_type
|
||||
"""NOP host filter. Returns all hosts."""
|
||||
|
||||
def filter_hosts(self, host_list, query, options):
|
||||
"""Return the entire list of supplied hosts."""
|
||||
return list(host_list)
|
||||
def host_passes(self, host_state, filter_properties):
|
||||
return True
|
||||
|
@ -13,19 +13,15 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from nova import log as logging
|
||||
from nova.scheduler.filters import abstract_filter
|
||||
|
||||
|
||||
LOG = logging.getLogger('nova.scheduler.filter.instance_type_filter')
|
||||
LOG = logging.getLogger('nova.scheduler.filter.compute_filter')
|
||||
|
||||
|
||||
class InstanceTypeFilter(abstract_filter.AbstractHostFilter):
|
||||
class ComputeFilter(abstract_filter.AbstractHostFilter):
|
||||
"""HostFilter hard-coded to work with InstanceType records."""
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Use instance_type to filter hosts."""
|
||||
return instance_type
|
||||
|
||||
def _satisfies_extra_specs(self, capabilities, instance_type):
|
||||
"""Check that the capabilities provided by the compute service
|
||||
@ -36,35 +32,28 @@ class InstanceTypeFilter(abstract_filter.AbstractHostFilter):
|
||||
# NOTE(lorinh): For now, we are just checking exact matching on the
|
||||
# values. Later on, we want to handle numerical
|
||||
# values so we can represent things like number of GPU cards
|
||||
try:
|
||||
for key, value in instance_type['extra_specs'].iteritems():
|
||||
if capabilities[key] != value:
|
||||
return False
|
||||
except KeyError, e:
|
||||
return False
|
||||
for key, value in instance_type['extra_specs'].iteritems():
|
||||
if capabilities.get(key, None) != value:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _basic_ram_filter(self, host_name, host_info, instance_type):
|
||||
def _basic_ram_filter(self, host_state, instance_type):
|
||||
"""Only return hosts with sufficient available RAM."""
|
||||
requested_ram = instance_type['memory_mb']
|
||||
free_ram_mb = host_info.free_ram_mb
|
||||
free_ram_mb = host_state.free_ram_mb
|
||||
return free_ram_mb >= requested_ram
|
||||
|
||||
def filter_hosts(self, host_list, query, options):
|
||||
def host_passes(self, host_state, filter_properties):
|
||||
"""Return a list of hosts that can create instance_type."""
|
||||
instance_type = query
|
||||
selected_hosts = []
|
||||
for hostname, host_info in host_list:
|
||||
if not self._basic_ram_filter(hostname, host_info,
|
||||
instance_type):
|
||||
continue
|
||||
capabilities = host_info.compute
|
||||
if capabilities:
|
||||
if not capabilities.get("enabled", True):
|
||||
continue
|
||||
if not self._satisfies_extra_specs(capabilities,
|
||||
instance_type):
|
||||
continue
|
||||
instance_type = filter_properties.get('instance_type')
|
||||
if host_state.topic != 'compute' or not instance_type:
|
||||
return True
|
||||
capabilities = host_state.capabilities or {}
|
||||
|
||||
selected_hosts.append((hostname, host_info))
|
||||
return selected_hosts
|
||||
if not self._basic_ram_filter(host_state, instance_type):
|
||||
return False
|
||||
if not capabilities.get("enabled", True):
|
||||
return False
|
||||
if not self._satisfies_extra_specs(capabilities, instance_type):
|
||||
return False
|
||||
return True
|
@ -86,18 +86,11 @@ class JsonFilter(abstract_filter.AbstractHostFilter):
|
||||
'and': _and,
|
||||
}
|
||||
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Convert instance_type into JSON filter object."""
|
||||
required_ram = instance_type['memory_mb']
|
||||
required_disk = instance_type['local_gb']
|
||||
query = ['and',
|
||||
['>=', '$compute.host_memory_free', required_ram],
|
||||
['>=', '$compute.disk_available', required_disk]]
|
||||
return json.dumps(query)
|
||||
|
||||
def _parse_string(self, string, host, hostinfo):
|
||||
def _parse_string(self, string, host_state):
|
||||
"""Strings prefixed with $ are capability lookups in the
|
||||
form '$service.capability[.subcap*]'.
|
||||
form '$variable' where 'variable' is an attribute in the
|
||||
HostState class. If $variable is a dictionary, you may
|
||||
use: $variable.dictkey
|
||||
"""
|
||||
if not string:
|
||||
return None
|
||||
@ -105,18 +98,16 @@ class JsonFilter(abstract_filter.AbstractHostFilter):
|
||||
return string
|
||||
|
||||
path = string[1:].split(".")
|
||||
services = dict(compute=hostinfo.compute, network=hostinfo.network,
|
||||
volume=hostinfo.volume)
|
||||
service = services.get(path[0], None)
|
||||
if not service:
|
||||
obj = getattr(host_state, path[0], None)
|
||||
if obj is None:
|
||||
return None
|
||||
for item in path[1:]:
|
||||
service = service.get(item, None)
|
||||
if not service:
|
||||
obj = obj.get(item, None)
|
||||
if obj is None:
|
||||
return None
|
||||
return service
|
||||
return obj
|
||||
|
||||
def _process_filter(self, query, host, hostinfo):
|
||||
def _process_filter(self, query, host_state):
|
||||
"""Recursively parse the query structure."""
|
||||
if not query:
|
||||
return True
|
||||
@ -125,30 +116,31 @@ class JsonFilter(abstract_filter.AbstractHostFilter):
|
||||
cooked_args = []
|
||||
for arg in query[1:]:
|
||||
if isinstance(arg, list):
|
||||
arg = self._process_filter(arg, host, hostinfo)
|
||||
arg = self._process_filter(arg, host_state)
|
||||
elif isinstance(arg, basestring):
|
||||
arg = self._parse_string(arg, host, hostinfo)
|
||||
arg = self._parse_string(arg, host_state)
|
||||
if arg is not None:
|
||||
cooked_args.append(arg)
|
||||
result = method(self, cooked_args)
|
||||
return result
|
||||
|
||||
def filter_hosts(self, host_list, query, options):
|
||||
def host_passes(self, host_state, filter_properties):
|
||||
"""Return a list of hosts that can fulfill the requirements
|
||||
specified in the query.
|
||||
"""
|
||||
expanded = json.loads(query)
|
||||
filtered_hosts = []
|
||||
for host, hostinfo in host_list:
|
||||
if not hostinfo:
|
||||
continue
|
||||
if hostinfo.compute and not hostinfo.compute.get("enabled", True):
|
||||
# Host is disabled
|
||||
continue
|
||||
result = self._process_filter(expanded, host, hostinfo)
|
||||
if isinstance(result, list):
|
||||
# If any succeeded, include the host
|
||||
result = any(result)
|
||||
if result:
|
||||
filtered_hosts.append((host, hostinfo))
|
||||
return filtered_hosts
|
||||
capabilities = host_state.capabilities or {}
|
||||
if not capabilities.get("enabled", True):
|
||||
return False
|
||||
|
||||
query = filter_properties.get('query', None)
|
||||
if not query:
|
||||
return True
|
||||
|
||||
result = self._process_filter(json.loads(query), host_state)
|
||||
if isinstance(result, list):
|
||||
# If any succeeded, include the host
|
||||
result = any(result)
|
||||
if result:
|
||||
# Filter it out.
|
||||
return True
|
||||
return False
|
||||
|
310
nova/scheduler/host_manager.py
Normal file
310
nova/scheduler/host_manager.py
Normal file
@ -0,0 +1,310 @@
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Manage hosts in the current zone.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import types
|
||||
import UserDict
|
||||
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import utils
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_integer('reserved_host_disk_mb', 0,
|
||||
'Amount of disk in MB to reserve for host/dom0')
|
||||
flags.DEFINE_integer('reserved_host_memory_mb', 512,
|
||||
'Amount of memory in MB to reserve for host/dom0')
|
||||
flags.DEFINE_list('default_host_filters', ['ComputeFilter'],
|
||||
'Which filters to use for filtering hosts when not specified '
|
||||
'in the request.')
|
||||
|
||||
LOG = logging.getLogger('nova.scheduler.host_manager')
|
||||
|
||||
|
||||
class ReadOnlyDict(UserDict.IterableUserDict):
|
||||
"""A read-only dict."""
|
||||
def __init__(self, source=None):
|
||||
self.data = {}
|
||||
self.update(source)
|
||||
|
||||
def __setitem__(self, key, item):
|
||||
raise TypeError
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise TypeError
|
||||
|
||||
def clear(self):
|
||||
raise TypeError
|
||||
|
||||
def pop(self, key, *args):
|
||||
raise TypeError
|
||||
|
||||
def popitem(self):
|
||||
raise TypeError
|
||||
|
||||
def update(self, source=None):
|
||||
if source is None:
|
||||
return
|
||||
elif isinstance(source, UserDict.UserDict):
|
||||
self.data = source.data
|
||||
elif isinstance(source, type({})):
|
||||
self.data = source
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
|
||||
class HostState(object):
|
||||
"""Mutable and immutable information tracked for a host.
|
||||
This is an attempt to remove the ad-hoc data structures
|
||||
previously used and lock down access.
|
||||
"""
|
||||
|
||||
def __init__(self, host, topic, capabilities=None):
|
||||
self.host = host
|
||||
self.topic = topic
|
||||
|
||||
# Read-only capability dicts
|
||||
|
||||
if capabilities is None:
|
||||
capabilities = {}
|
||||
self.capabilities = ReadOnlyDict(capabilities.get(topic, None))
|
||||
# Mutable available resources.
|
||||
# These will change as resources are virtually "consumed".
|
||||
self.free_ram_mb = 0
|
||||
self.free_disk_mb = 0
|
||||
|
||||
def update_from_compute_node(self, compute):
|
||||
"""Update information about a host from its compute_node info."""
|
||||
all_disk_mb = compute['local_gb'] * 1024
|
||||
all_ram_mb = compute['memory_mb']
|
||||
if FLAGS.reserved_host_disk_mb > 0:
|
||||
all_disk_mb -= FLAGS.reserved_host_disk_mb
|
||||
if FLAGS.reserved_host_memory_mb > 0:
|
||||
all_ram_mb -= FLAGS.reserved_host_memory_mb
|
||||
self.free_ram_mb = all_ram_mb
|
||||
self.free_disk_mb = all_disk_mb
|
||||
|
||||
def consume_from_instance(self, instance):
|
||||
"""Update information about a host from instance info."""
|
||||
disk_mb = instance['local_gb'] * 1024
|
||||
ram_mb = instance['memory_mb']
|
||||
self.free_ram_mb -= ram_mb
|
||||
self.free_disk_mb -= disk_mb
|
||||
|
||||
def passes_filters(self, filter_fns, filter_properties):
|
||||
"""Return whether or not this host passes filters."""
|
||||
|
||||
if self.host in filter_properties.get('ignore_hosts', []):
|
||||
return False
|
||||
for filter_fn in filter_fns:
|
||||
if not filter_fn(self, filter_properties):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return "host '%s': free_ram_mb:%s free_disk_mb:%s" % \
|
||||
(self.host, self.free_ram_mb, self.free_disk_mb)
|
||||
|
||||
|
||||
class HostManager(object):
|
||||
"""Base HostManager class."""
|
||||
|
||||
# Can be overriden in a subclass
|
||||
host_state_cls = HostState
|
||||
|
||||
def __init__(self):
|
||||
self.service_states = {} # { <host> : { <service> : { cap k : v }}}
|
||||
self.filter_classes = self._get_filter_classes()
|
||||
|
||||
def _get_filter_classes(self):
|
||||
"""Get the list of possible filter classes"""
|
||||
# Imported here to avoid circular imports
|
||||
from nova.scheduler import filters
|
||||
|
||||
def get_itm(nm):
|
||||
return getattr(filters, nm)
|
||||
|
||||
return [get_itm(itm) for itm in dir(filters)
|
||||
if (type(get_itm(itm)) is types.TypeType)
|
||||
and issubclass(get_itm(itm), filters.AbstractHostFilter)
|
||||
and get_itm(itm) is not filters.AbstractHostFilter]
|
||||
|
||||
def _choose_host_filters(self, filters):
|
||||
"""Since the caller may specify which filters to use we need
|
||||
to have an authoritative list of what is permissible. This
|
||||
function checks the filter names against a predefined set
|
||||
of acceptable filters.
|
||||
"""
|
||||
if filters is None:
|
||||
filters = FLAGS.default_host_filters
|
||||
if not isinstance(filters, (list, tuple)):
|
||||
filters = [filters]
|
||||
good_filters = []
|
||||
bad_filters = []
|
||||
for filter_name in filters:
|
||||
found_class = False
|
||||
for cls in self.filter_classes:
|
||||
if cls.__name__ == filter_name:
|
||||
found_class = True
|
||||
filter_instance = cls()
|
||||
# Get the filter function
|
||||
filter_func = getattr(filter_instance,
|
||||
'host_passes', None)
|
||||
if filter_func:
|
||||
good_filters.append(filter_func)
|
||||
break
|
||||
if not found_class:
|
||||
bad_filters.append(filter_name)
|
||||
if bad_filters:
|
||||
msg = ", ".join(bad_filters)
|
||||
raise exception.SchedulerHostFilterNotFound(filter_name=msg)
|
||||
return good_filters
|
||||
|
||||
def filter_hosts(self, hosts, filter_properties, filters=None):
|
||||
"""Filter hosts and return only ones passing all filters"""
|
||||
filtered_hosts = []
|
||||
filter_fns = self._choose_host_filters(filters)
|
||||
for host in hosts:
|
||||
if host.passes_filters(filter_fns, filter_properties):
|
||||
filtered_hosts.append(host)
|
||||
return filtered_hosts
|
||||
|
||||
def get_host_list(self):
|
||||
"""Returns a list of dicts for each host that the Zone Manager
|
||||
knows about. Each dict contains the host_name and the service
|
||||
for that host.
|
||||
"""
|
||||
all_hosts = self.service_states.keys()
|
||||
ret = []
|
||||
for host in self.service_states:
|
||||
for svc in self.service_states[host]:
|
||||
ret.append({"service": svc, "host_name": host})
|
||||
return ret
|
||||
|
||||
def get_service_capabilities(self):
|
||||
"""Roll up all the individual host info to generic 'service'
|
||||
capabilities. Each capability is aggregated into
|
||||
<cap>_min and <cap>_max values."""
|
||||
hosts_dict = self.service_states
|
||||
|
||||
# TODO(sandy) - be smarter about fabricating this structure.
|
||||
# But it's likely to change once we understand what the Best-Match
|
||||
# code will need better.
|
||||
combined = {} # { <service>_<cap> : (min, max), ... }
|
||||
stale_host_services = {} # { host1 : [svc1, svc2], host2 :[svc1]}
|
||||
for host, host_dict in hosts_dict.iteritems():
|
||||
for service_name, service_dict in host_dict.iteritems():
|
||||
if not service_dict.get("enabled", True):
|
||||
# Service is disabled; do no include it
|
||||
continue
|
||||
|
||||
# Check if the service capabilities became stale
|
||||
if self.host_service_caps_stale(host, service_name):
|
||||
if host not in stale_host_services:
|
||||
stale_host_services[host] = [] # Adding host key once
|
||||
stale_host_services[host].append(service_name)
|
||||
continue
|
||||
for cap, value in service_dict.iteritems():
|
||||
if cap == "timestamp": # Timestamp is not needed
|
||||
continue
|
||||
key = "%s_%s" % (service_name, cap)
|
||||
min_value, max_value = combined.get(key, (value, value))
|
||||
min_value = min(min_value, value)
|
||||
max_value = max(max_value, value)
|
||||
combined[key] = (min_value, max_value)
|
||||
|
||||
# Delete the expired host services
|
||||
self.delete_expired_host_services(stale_host_services)
|
||||
return combined
|
||||
|
||||
def update_service_capabilities(self, service_name, host, capabilities):
|
||||
"""Update the per-service capabilities based on this notification."""
|
||||
logging.debug(_("Received %(service_name)s service update from "
|
||||
"%(host)s.") % locals())
|
||||
service_caps = self.service_states.get(host, {})
|
||||
# Copy the capabilities, so we don't modify the original dict
|
||||
capab_copy = dict(capabilities)
|
||||
capab_copy["timestamp"] = utils.utcnow() # Reported time
|
||||
service_caps[service_name] = capab_copy
|
||||
self.service_states[host] = service_caps
|
||||
|
||||
def host_service_caps_stale(self, host, service):
|
||||
"""Check if host service capabilites are not recent enough."""
|
||||
allowed_time_diff = FLAGS.periodic_interval * 3
|
||||
caps = self.service_states[host][service]
|
||||
if (utils.utcnow() - caps["timestamp"]) <= \
|
||||
datetime.timedelta(seconds=allowed_time_diff):
|
||||
return False
|
||||
return True
|
||||
|
||||
def delete_expired_host_services(self, host_services_dict):
|
||||
"""Delete all the inactive host services information."""
|
||||
for host, services in host_services_dict.iteritems():
|
||||
service_caps = self.service_states[host]
|
||||
for service in services:
|
||||
del service_caps[service]
|
||||
if len(service_caps) == 0: # Delete host if no services
|
||||
del self.service_states[host]
|
||||
|
||||
def get_all_host_states(self, context, topic):
|
||||
"""Returns a dict of all the hosts the HostManager
|
||||
knows about. Also, each of the consumable resources in HostState
|
||||
are pre-populated and adjusted based on data in the db.
|
||||
|
||||
For example:
|
||||
{'192.168.1.100': HostState(), ...}
|
||||
|
||||
Note: this can be very slow with a lot of instances.
|
||||
InstanceType table isn't required since a copy is stored
|
||||
with the instance (in case the InstanceType changed since the
|
||||
instance was created)."""
|
||||
|
||||
if topic != 'compute':
|
||||
raise NotImplementedError(_(
|
||||
"host_manager only implemented for 'compute'"))
|
||||
|
||||
host_state_map = {}
|
||||
|
||||
# Make a compute node dict with the bare essential metrics.
|
||||
compute_nodes = db.compute_node_get_all(context)
|
||||
for compute in compute_nodes:
|
||||
service = compute['service']
|
||||
if not service:
|
||||
logging.warn(_("No service for compute ID %s") % compute['id'])
|
||||
continue
|
||||
host = service['host']
|
||||
capabilities = self.service_states.get(host, None)
|
||||
host_state = self.host_state_cls(host, topic,
|
||||
capabilities=capabilities)
|
||||
host_state.update_from_compute_node(compute)
|
||||
host_state_map[host] = host_state
|
||||
|
||||
# "Consume" resources from the host the instance resides on.
|
||||
instances = db.instance_get_all(context)
|
||||
for instance in instances:
|
||||
host = instance['host']
|
||||
if not host:
|
||||
continue
|
||||
host_state = host_state_map.get(host, None)
|
||||
if not host_state:
|
||||
continue
|
||||
host_state.consume_from_instance(instance)
|
||||
return host_state_map
|
@ -47,38 +47,37 @@ class WeightedHost(object):
|
||||
This is an attempt to remove some of the ad-hoc dict structures
|
||||
previously used."""
|
||||
|
||||
def __init__(self, weight, host=None, blob=None, zone=None, hostinfo=None):
|
||||
def __init__(self, weight, host_state=None, blob=None, zone=None):
|
||||
self.weight = weight
|
||||
self.blob = blob
|
||||
self.host = host
|
||||
self.zone = zone
|
||||
|
||||
# Local members. These are not returned outside of the Zone.
|
||||
self.hostinfo = hostinfo
|
||||
self.host_state = host_state
|
||||
|
||||
def to_dict(self):
|
||||
x = dict(weight=self.weight)
|
||||
if self.blob:
|
||||
x['blob'] = self.blob
|
||||
if self.host:
|
||||
x['host'] = self.host
|
||||
if self.host_state:
|
||||
x['host'] = self.host_state.host
|
||||
if self.zone:
|
||||
x['zone'] = self.zone
|
||||
return x
|
||||
|
||||
|
||||
def noop_cost_fn(host_info, options=None):
|
||||
def noop_cost_fn(host_state, weighing_properties):
|
||||
"""Return a pre-weight cost of 1 for each host"""
|
||||
return 1
|
||||
|
||||
|
||||
def compute_fill_first_cost_fn(host_info, options=None):
|
||||
def compute_fill_first_cost_fn(host_state, weighing_properties):
|
||||
"""More free ram = higher weight. So servers will less free
|
||||
ram will be preferred."""
|
||||
return host_info.free_ram_mb
|
||||
return host_state.free_ram_mb
|
||||
|
||||
|
||||
def weighted_sum(weighted_fns, host_list, options):
|
||||
def weighted_sum(weighted_fns, host_states, weighing_properties):
|
||||
"""Use the weighted-sum method to compute a score for an array of objects.
|
||||
Normalize the results of the objective-functions so that the weights are
|
||||
meaningful regardless of objective-function's range.
|
||||
@ -86,7 +85,8 @@ def weighted_sum(weighted_fns, host_list, options):
|
||||
host_list - [(host, HostInfo()), ...]
|
||||
weighted_fns - list of weights and functions like:
|
||||
[(weight, objective-functions), ...]
|
||||
options is an arbitrary dict of values.
|
||||
weighing_properties is an arbitrary dict of values that can influence
|
||||
weights.
|
||||
|
||||
Returns a single WeightedHost object which represents the best
|
||||
candidate.
|
||||
@ -96,8 +96,8 @@ def weighted_sum(weighted_fns, host_list, options):
|
||||
# One row per host. One column per function.
|
||||
scores = []
|
||||
for weight, fn in weighted_fns:
|
||||
scores.append([fn(host_info, options) for hostname, host_info
|
||||
in host_list])
|
||||
scores.append([fn(host_state, weighing_properties)
|
||||
for host_state in host_states])
|
||||
|
||||
# Adjust the weights in the grid by the functions weight adjustment
|
||||
# and sum them up to get a final list of weights.
|
||||
@ -106,16 +106,16 @@ def weighted_sum(weighted_fns, host_list, options):
|
||||
adjusted_scores.append([weight * score for score in row])
|
||||
|
||||
# Now, sum down the columns to get the final score. Column per host.
|
||||
final_scores = [0.0] * len(host_list)
|
||||
final_scores = [0.0] * len(host_states)
|
||||
for row in adjusted_scores:
|
||||
for idx, col in enumerate(row):
|
||||
final_scores[idx] += col
|
||||
|
||||
# Super-impose the hostinfo into the scores so
|
||||
# Super-impose the host_state into the scores so
|
||||
# we don't lose it when we sort.
|
||||
final_scores = [(final_scores[idx], host_tuple)
|
||||
for idx, host_tuple in enumerate(host_list)]
|
||||
final_scores = [(final_scores[idx], host_state)
|
||||
for idx, host_state in enumerate(host_states)]
|
||||
|
||||
final_scores = sorted(final_scores)
|
||||
weight, (host, hostinfo) = final_scores[0] # Lowest score is the winner!
|
||||
return WeightedHost(weight, host=host, hostinfo=hostinfo)
|
||||
weight, host_state = final_scores[0] # Lowest score is the winner!
|
||||
return WeightedHost(weight, host_state=host_state)
|
||||
|
@ -30,7 +30,6 @@ from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import manager
|
||||
from nova import rpc
|
||||
from nova.scheduler import zone_manager
|
||||
from nova import utils
|
||||
|
||||
LOG = logging.getLogger('nova.scheduler.manager')
|
||||
@ -44,11 +43,9 @@ class SchedulerManager(manager.Manager):
|
||||
"""Chooses a host to run instances on."""
|
||||
|
||||
def __init__(self, scheduler_driver=None, *args, **kwargs):
|
||||
self.zone_manager = zone_manager.ZoneManager()
|
||||
if not scheduler_driver:
|
||||
scheduler_driver = FLAGS.scheduler_driver
|
||||
self.driver = utils.import_object(scheduler_driver)
|
||||
self.driver.set_zone_manager(self.zone_manager)
|
||||
super(SchedulerManager, self).__init__(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, key):
|
||||
@ -58,29 +55,29 @@ class SchedulerManager(manager.Manager):
|
||||
@manager.periodic_task
|
||||
def _poll_child_zones(self, context):
|
||||
"""Poll child zones periodically to get status."""
|
||||
self.zone_manager.ping(context)
|
||||
self.driver.poll_child_zones(context)
|
||||
|
||||
def get_host_list(self, context=None):
|
||||
"""Get a list of hosts from the ZoneManager."""
|
||||
return self.zone_manager.get_host_list()
|
||||
def get_host_list(self, context):
|
||||
"""Get a list of hosts from the HostManager."""
|
||||
return self.driver.get_host_list()
|
||||
|
||||
def get_zone_list(self, context=None):
|
||||
def get_zone_list(self, context):
|
||||
"""Get a list of zones from the ZoneManager."""
|
||||
return self.zone_manager.get_zone_list()
|
||||
return self.driver.get_zone_list()
|
||||
|
||||
def get_zone_capabilities(self, context=None):
|
||||
def get_service_capabilities(self, context):
|
||||
"""Get the normalized set of capabilities for this zone."""
|
||||
return self.zone_manager.get_zone_capabilities(context)
|
||||
return self.driver.get_service_capabilities()
|
||||
|
||||
def update_service_capabilities(self, context=None, service_name=None,
|
||||
host=None, capabilities=None):
|
||||
def update_service_capabilities(self, context, service_name=None,
|
||||
host=None, capabilities=None, **kwargs):
|
||||
"""Process a capability update from a service node."""
|
||||
if not capabilities:
|
||||
if capabilities is None:
|
||||
capabilities = {}
|
||||
self.zone_manager.update_service_capabilities(service_name,
|
||||
host, capabilities)
|
||||
self.driver.update_service_capabilities(service_name, host,
|
||||
capabilities)
|
||||
|
||||
def select(self, context=None, *args, **kwargs):
|
||||
def select(self, context, *args, **kwargs):
|
||||
"""Select a list of hosts best matching the provided specs."""
|
||||
return self.driver.select(context, *args, **kwargs)
|
||||
|
||||
|
@ -141,20 +141,3 @@ class SimpleScheduler(chance.ChanceScheduler):
|
||||
return None
|
||||
msg = _("Is the appropriate service running?")
|
||||
raise exception.NoValidHost(reason=msg)
|
||||
|
||||
def schedule_set_network_host(self, context, *_args, **_kwargs):
|
||||
"""Picks a host that is up and has the fewest networks."""
|
||||
elevated = context.elevated()
|
||||
|
||||
results = db.service_get_all_network_sorted(elevated)
|
||||
for result in results:
|
||||
(service, instance_count) = result
|
||||
if instance_count >= FLAGS.max_networks:
|
||||
msg = _("Not enough allocatable networks remaining")
|
||||
raise exception.NoValidHost(reason=msg)
|
||||
if self.service_is_up(service):
|
||||
driver.cast_to_network_host(context, service['host'],
|
||||
'set_network_host', **_kwargs)
|
||||
return None
|
||||
msg = _("Is the appropriate service running?")
|
||||
raise exception.NoValidHost(reason=msg)
|
||||
|
@ -97,7 +97,7 @@ class VsaScheduler(simple.SimpleScheduler):
|
||||
return True
|
||||
|
||||
def _get_service_states(self):
|
||||
return self.zone_manager.service_states
|
||||
return self.host_manager.service_states
|
||||
|
||||
def _filter_hosts(self, topic, request_spec, host_list=None):
|
||||
|
||||
|
@ -14,12 +14,11 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
ZoneManager oversees all communications with child Zones.
|
||||
Manage communication with child zones and keep state for them.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import traceback
|
||||
import UserDict
|
||||
|
||||
from eventlet import greenpool
|
||||
from novaclient import v1_1 as novaclient
|
||||
@ -34,149 +33,83 @@ flags.DEFINE_integer('zone_db_check_interval', 60,
|
||||
'Seconds between getting fresh zone info from db.')
|
||||
flags.DEFINE_integer('zone_failures_to_offline', 3,
|
||||
'Number of consecutive errors before marking zone offline')
|
||||
flags.DEFINE_integer('reserved_host_disk_mb', 0,
|
||||
'Amount of disk in MB to reserve for host/dom0')
|
||||
flags.DEFINE_integer('reserved_host_memory_mb', 512,
|
||||
'Amount of memory in MB to reserve for host/dom0')
|
||||
|
||||
LOG = logging.getLogger('nova.scheduler.zone_manager')
|
||||
|
||||
|
||||
class ZoneState(object):
|
||||
"""Holds the state of all connected child zones."""
|
||||
"""Holds state for a particular zone."""
|
||||
def __init__(self):
|
||||
self.is_active = True
|
||||
self.name = None
|
||||
self.capabilities = None
|
||||
self.capabilities = {}
|
||||
self.attempt = 0
|
||||
self.last_seen = datetime.datetime.min
|
||||
self.last_exception = None
|
||||
self.last_exception_time = None
|
||||
self.zone_info = {}
|
||||
|
||||
def update_credentials(self, zone):
|
||||
def update_zone_info(self, zone):
|
||||
"""Update zone credentials from db"""
|
||||
self.zone_id = zone.id
|
||||
self.name = zone.name
|
||||
self.api_url = zone.api_url
|
||||
self.username = zone.username
|
||||
self.password = zone.password
|
||||
self.weight_offset = zone.weight_offset
|
||||
self.weight_scale = zone.weight_scale
|
||||
self.zone_info = dict(zone.iteritems())
|
||||
|
||||
def update_metadata(self, zone_metadata):
|
||||
"""Update zone metadata after successful communications with
|
||||
child zone."""
|
||||
self.last_seen = utils.utcnow()
|
||||
self.attempt = 0
|
||||
self.capabilities = ", ".join(["%s=%s" % (k, v)
|
||||
for k, v in zone_metadata.iteritems() if k != 'name'])
|
||||
self.capabilities = dict(
|
||||
[(k, v) for k, v in zone_metadata.iteritems() if k != 'name'])
|
||||
self.is_active = True
|
||||
|
||||
def to_dict(self):
|
||||
return dict(name=self.name, capabilities=self.capabilities,
|
||||
is_active=self.is_active, api_url=self.api_url,
|
||||
id=self.zone_id, weight_scale=self.weight_scale,
|
||||
weight_offset=self.weight_offset)
|
||||
def get_zone_info(self):
|
||||
db_fields_to_return = ['api_url', 'id', 'weight_scale',
|
||||
'weight_offset']
|
||||
zone_info = dict(is_active=self.is_active,
|
||||
capabilities=self.capabilities)
|
||||
for field in db_fields_to_return:
|
||||
zone_info[field] = self.zone_info[field]
|
||||
return zone_info
|
||||
|
||||
def log_error(self, exception):
|
||||
"""Something went wrong. Check to see if zone should be
|
||||
marked as offline."""
|
||||
self.last_exception = exception
|
||||
self.last_exception_time = utils.utcnow()
|
||||
api_url = self.api_url
|
||||
logging.warning(_("'%(exception)s' error talking to "
|
||||
api_url = self.zone_info['api_url']
|
||||
LOG.warning(_("'%(exception)s' error talking to "
|
||||
"zone %(api_url)s") % locals())
|
||||
|
||||
max_errors = FLAGS.zone_failures_to_offline
|
||||
self.attempt += 1
|
||||
if self.attempt >= max_errors:
|
||||
self.is_active = False
|
||||
logging.error(_("No answer from zone %(api_url)s "
|
||||
LOG.error(_("No answer from zone %(api_url)s "
|
||||
"after %(max_errors)d "
|
||||
"attempts. Marking inactive.") % locals())
|
||||
|
||||
def call_novaclient(self):
|
||||
"""Call novaclient. Broken out for testing purposes. Note that
|
||||
we have to use the admin credentials for this since there is no
|
||||
available context."""
|
||||
username = self.zone_info['username']
|
||||
password = self.zone_info['password']
|
||||
api_url = self.zone_info['api_url']
|
||||
region_name = self.zone_info['name']
|
||||
client = novaclient.Client(username, password, None, api_url,
|
||||
region_name)
|
||||
return client.zones.info()._info
|
||||
|
||||
def _call_novaclient(zone):
|
||||
"""Call novaclient. Broken out for testing purposes. Note that
|
||||
we have to use the admin credentials for this since there is no
|
||||
available context."""
|
||||
client = novaclient.Client(zone.username, zone.password, None,
|
||||
zone.api_url, region_name=zone.name)
|
||||
return client.zones.info()._info
|
||||
|
||||
|
||||
def _poll_zone(zone):
|
||||
"""Eventlet worker to poll a zone."""
|
||||
name = zone.name
|
||||
url = zone.api_url
|
||||
logging.debug(_("Polling zone: %(name)s @ %(url)s") % locals())
|
||||
try:
|
||||
zone.update_metadata(_call_novaclient(zone))
|
||||
except Exception, e:
|
||||
zone.log_error(traceback.format_exc())
|
||||
|
||||
|
||||
class ReadOnlyDict(UserDict.IterableUserDict):
|
||||
"""A read-only dict."""
|
||||
def __init__(self, source=None):
|
||||
self.update(source)
|
||||
|
||||
def __setitem__(self, key, item):
|
||||
raise TypeError
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise TypeError
|
||||
|
||||
def clear(self):
|
||||
raise TypeError
|
||||
|
||||
def pop(self, key, *args):
|
||||
raise TypeError
|
||||
|
||||
def popitem(self):
|
||||
raise TypeError
|
||||
|
||||
def update(self, source=None):
|
||||
if source is None:
|
||||
def poll(self):
|
||||
"""Eventlet worker to poll a self."""
|
||||
if 'api_url' not in self.zone_info:
|
||||
return
|
||||
elif isinstance(source, UserDict.UserDict):
|
||||
self.data = source.data
|
||||
elif isinstance(source, dict):
|
||||
self.data = source
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
|
||||
class HostInfo(object):
|
||||
"""Mutable and immutable information on hosts tracked
|
||||
by the ZoneManager. This is an attempt to remove the
|
||||
ad-hoc data structures previously used and lock down
|
||||
access."""
|
||||
|
||||
def __init__(self, host, caps=None, free_ram_mb=0, free_disk_gb=0):
|
||||
self.host = host
|
||||
|
||||
# Read-only capability dicts
|
||||
self.compute = None
|
||||
self.volume = None
|
||||
self.network = None
|
||||
|
||||
if caps:
|
||||
self.compute = ReadOnlyDict(caps.get('compute', None))
|
||||
self.volume = ReadOnlyDict(caps.get('volume', None))
|
||||
self.network = ReadOnlyDict(caps.get('network', None))
|
||||
|
||||
# Mutable available resources.
|
||||
# These will change as resources are virtually "consumed".
|
||||
self.free_ram_mb = free_ram_mb
|
||||
self.free_disk_gb = free_disk_gb
|
||||
|
||||
def consume_resources(self, disk_gb, ram_mb):
|
||||
"""Consume some of the mutable resources."""
|
||||
self.free_disk_gb -= disk_gb
|
||||
self.free_ram_mb -= ram_mb
|
||||
|
||||
def __repr__(self):
|
||||
return "%s ram:%s disk:%s" % \
|
||||
(self.host, self.free_ram_mb, self.free_disk_gb)
|
||||
name = self.zone_info['name']
|
||||
api_url = self.zone_info['api_url']
|
||||
LOG.debug(_("Polling zone: %(name)s @ %(api_url)s") % locals())
|
||||
try:
|
||||
self.update_metadata(self.call_novaclient())
|
||||
except Exception, e:
|
||||
self.log_error(traceback.format_exc())
|
||||
|
||||
|
||||
class ZoneManager(object):
|
||||
@ -184,116 +117,11 @@ class ZoneManager(object):
|
||||
def __init__(self):
|
||||
self.last_zone_db_check = datetime.datetime.min
|
||||
self.zone_states = {} # { <zone_id> : ZoneState }
|
||||
self.service_states = {} # { <host> : { <service> : { cap k : v }}}
|
||||
self.green_pool = greenpool.GreenPool()
|
||||
|
||||
def get_zone_list(self):
|
||||
"""Return the list of zones we know about."""
|
||||
return [zone.to_dict() for zone in self.zone_states.values()]
|
||||
|
||||
def get_host_list(self):
|
||||
"""Returns a list of dicts for each host that the Zone Manager
|
||||
knows about. Each dict contains the host_name and the service
|
||||
for that host.
|
||||
"""
|
||||
all_hosts = self.service_states.keys()
|
||||
ret = []
|
||||
for host in self.service_states:
|
||||
for svc in self.service_states[host]:
|
||||
ret.append({"service": svc, "host_name": host})
|
||||
return ret
|
||||
|
||||
def _compute_node_get_all(self, context):
|
||||
"""Broken out for testing."""
|
||||
return db.compute_node_get_all(context)
|
||||
|
||||
def _instance_get_all(self, context):
|
||||
"""Broken out for testing."""
|
||||
return db.instance_get_all(context)
|
||||
|
||||
def get_all_host_data(self, context):
|
||||
"""Returns a dict of all the hosts the ZoneManager
|
||||
knows about. Also, each of the consumable resources in HostInfo
|
||||
are pre-populated and adjusted based on data in the db.
|
||||
|
||||
For example:
|
||||
{'192.168.1.100': HostInfo(), ...}
|
||||
|
||||
Note: this can be very slow with a lot of instances.
|
||||
InstanceType table isn't required since a copy is stored
|
||||
with the instance (in case the InstanceType changed since the
|
||||
instance was created)."""
|
||||
|
||||
# Make a compute node dict with the bare essential metrics.
|
||||
compute_nodes = self._compute_node_get_all(context)
|
||||
host_info_map = {}
|
||||
for compute in compute_nodes:
|
||||
all_disk = compute['local_gb']
|
||||
all_ram = compute['memory_mb']
|
||||
service = compute['service']
|
||||
if not service:
|
||||
logging.warn(_("No service for compute ID %s") % compute['id'])
|
||||
continue
|
||||
|
||||
host = service['host']
|
||||
caps = self.service_states.get(host, None)
|
||||
host_info = HostInfo(host, caps=caps,
|
||||
free_disk_gb=all_disk, free_ram_mb=all_ram)
|
||||
# Reserve resources for host/dom0
|
||||
host_info.consume_resources(FLAGS.reserved_host_disk_mb * 1024,
|
||||
FLAGS.reserved_host_memory_mb)
|
||||
host_info_map[host] = host_info
|
||||
|
||||
# "Consume" resources from the host the instance resides on.
|
||||
instances = self._instance_get_all(context)
|
||||
for instance in instances:
|
||||
host = instance['host']
|
||||
if not host:
|
||||
continue
|
||||
host_info = host_info_map.get(host, None)
|
||||
if not host_info:
|
||||
continue
|
||||
disk = instance['local_gb']
|
||||
ram = instance['memory_mb']
|
||||
host_info.consume_resources(disk, ram)
|
||||
|
||||
return host_info_map
|
||||
|
||||
def get_zone_capabilities(self, context):
|
||||
"""Roll up all the individual host info to generic 'service'
|
||||
capabilities. Each capability is aggregated into
|
||||
<cap>_min and <cap>_max values."""
|
||||
hosts_dict = self.service_states
|
||||
|
||||
# TODO(sandy) - be smarter about fabricating this structure.
|
||||
# But it's likely to change once we understand what the Best-Match
|
||||
# code will need better.
|
||||
combined = {} # { <service>_<cap> : (min, max), ... }
|
||||
stale_host_services = {} # { host1 : [svc1, svc2], host2 :[svc1]}
|
||||
for host, host_dict in hosts_dict.iteritems():
|
||||
for service_name, service_dict in host_dict.iteritems():
|
||||
if not service_dict.get("enabled", True):
|
||||
# Service is disabled; do no include it
|
||||
continue
|
||||
|
||||
#Check if the service capabilities became stale
|
||||
if self.host_service_caps_stale(host, service_name):
|
||||
if host not in stale_host_services:
|
||||
stale_host_services[host] = [] # Adding host key once
|
||||
stale_host_services[host].append(service_name)
|
||||
continue
|
||||
for cap, value in service_dict.iteritems():
|
||||
if cap == "timestamp": # Timestamp is not needed
|
||||
continue
|
||||
key = "%s_%s" % (service_name, cap)
|
||||
min_value, max_value = combined.get(key, (value, value))
|
||||
min_value = min(min_value, value)
|
||||
max_value = max(max_value, value)
|
||||
combined[key] = (min_value, max_value)
|
||||
|
||||
# Delete the expired host services
|
||||
self.delete_expired_host_services(stale_host_services)
|
||||
return combined
|
||||
return [zone.get_zone_info() for zone in self.zone_states.values()]
|
||||
|
||||
def _refresh_from_db(self, context):
|
||||
"""Make our zone state map match the db."""
|
||||
@ -302,10 +130,11 @@ class ZoneManager(object):
|
||||
existing = self.zone_states.keys()
|
||||
db_keys = []
|
||||
for zone in zones:
|
||||
db_keys.append(zone.id)
|
||||
if zone.id not in existing:
|
||||
self.zone_states[zone.id] = ZoneState()
|
||||
self.zone_states[zone.id].update_credentials(zone)
|
||||
zone_id = zone['id']
|
||||
db_keys.append(zone_id)
|
||||
if zone_id not in existing:
|
||||
self.zone_states[zone_id] = ZoneState()
|
||||
self.zone_states[zone_id].update_zone_info(zone)
|
||||
|
||||
# Cleanup zones removed from db ...
|
||||
keys = self.zone_states.keys() # since we're deleting
|
||||
@ -313,42 +142,19 @@ class ZoneManager(object):
|
||||
if zone_id not in db_keys:
|
||||
del self.zone_states[zone_id]
|
||||
|
||||
def _poll_zones(self, context):
|
||||
def _poll_zones(self):
|
||||
"""Try to connect to each child zone and get update."""
|
||||
self.green_pool.imap(_poll_zone, self.zone_states.values())
|
||||
def _worker(zone_state):
|
||||
zone_state.poll()
|
||||
self.green_pool.imap(_worker, self.zone_states.values())
|
||||
|
||||
def ping(self, context):
|
||||
"""Ping should be called periodically to update zone status."""
|
||||
def update(self, context):
|
||||
"""Update status for all zones. This should be called
|
||||
periodically to refresh the zone states.
|
||||
"""
|
||||
diff = utils.utcnow() - self.last_zone_db_check
|
||||
if diff.seconds >= FLAGS.zone_db_check_interval:
|
||||
logging.debug(_("Updating zone cache from db."))
|
||||
LOG.debug(_("Updating zone cache from db."))
|
||||
self.last_zone_db_check = utils.utcnow()
|
||||
self._refresh_from_db(context)
|
||||
self._poll_zones(context)
|
||||
|
||||
def update_service_capabilities(self, service_name, host, capabilities):
|
||||
"""Update the per-service capabilities based on this notification."""
|
||||
logging.debug(_("Received %(service_name)s service update from "
|
||||
"%(host)s.") % locals())
|
||||
service_caps = self.service_states.get(host, {})
|
||||
capabilities["timestamp"] = utils.utcnow() # Reported time
|
||||
service_caps[service_name] = capabilities
|
||||
self.service_states[host] = service_caps
|
||||
|
||||
def host_service_caps_stale(self, host, service):
|
||||
"""Check if host service capabilites are not recent enough."""
|
||||
allowed_time_diff = FLAGS.periodic_interval * 3
|
||||
caps = self.service_states[host][service]
|
||||
if (utils.utcnow() - caps["timestamp"]) <= \
|
||||
datetime.timedelta(seconds=allowed_time_diff):
|
||||
return False
|
||||
return True
|
||||
|
||||
def delete_expired_host_services(self, host_services_dict):
|
||||
"""Delete all the inactive host services information."""
|
||||
for host, services in host_services_dict.iteritems():
|
||||
service_caps = self.service_states[host]
|
||||
for service in services:
|
||||
del service_caps[service]
|
||||
if len(service_caps) == 0: # Delete host if no services
|
||||
del self.service_states[host]
|
||||
self._poll_zones()
|
||||
|
@ -13,25 +13,52 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Fakes For Distributed Scheduler tests.
|
||||
Fakes For Scheduler tests.
|
||||
"""
|
||||
|
||||
from nova import db
|
||||
from nova.scheduler import distributed_scheduler
|
||||
from nova.scheduler import host_manager
|
||||
from nova.scheduler import zone_manager
|
||||
|
||||
|
||||
COMPUTE_NODES = [
|
||||
dict(id=1, local_gb=1024, memory_mb=1024, service=dict(host='host1')),
|
||||
dict(id=2, local_gb=2048, memory_mb=2048, service=dict(host='host2')),
|
||||
dict(id=3, local_gb=4096, memory_mb=4096, service=dict(host='host3')),
|
||||
dict(id=4, local_gb=8192, memory_mb=8192, service=dict(host='host4')),
|
||||
# Broken entry
|
||||
dict(id=5, local_gb=1024, memory_mb=1024, service=None),
|
||||
]
|
||||
|
||||
INSTANCES = [
|
||||
dict(local_gb=512, memory_mb=512, host='host1'),
|
||||
dict(local_gb=512, memory_mb=512, host='host2'),
|
||||
dict(local_gb=512, memory_mb=512, host='host2'),
|
||||
dict(local_gb=1024, memory_mb=1024, host='host3'),
|
||||
# Broken host
|
||||
dict(local_gb=1024, memory_mb=1024, host=None),
|
||||
# No matching host
|
||||
dict(local_gb=1024, memory_mb=1024, host='host5'),
|
||||
]
|
||||
|
||||
|
||||
class FakeDistributedScheduler(distributed_scheduler.DistributedScheduler):
|
||||
# No need to stub anything at the moment
|
||||
pass
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FakeDistributedScheduler, self).__init__(*args, **kwargs)
|
||||
self.zone_manager = zone_manager.ZoneManager()
|
||||
self.host_manager = host_manager.HostManager()
|
||||
|
||||
|
||||
class FakeZoneManager(zone_manager.ZoneManager):
|
||||
class FakeHostManager(host_manager.HostManager):
|
||||
"""host1: free_ram_mb=1024-512-512=0, free_disk_gb=1024-512-512=0
|
||||
host2: free_ram_mb=2048-512=1536 free_disk_gb=2048-512=1536
|
||||
host3: free_ram_mb=4096-1024=3072 free_disk_gb=4096-1024=3072
|
||||
host4: free_ram_mb=8192 free_disk_gb=8192"""
|
||||
|
||||
def __init__(self):
|
||||
super(FakeHostManager, self).__init__()
|
||||
|
||||
self.service_states = {
|
||||
'host1': {
|
||||
'compute': {'host_memory_free': 1073741824},
|
||||
@ -55,18 +82,17 @@ class FakeZoneManager(zone_manager.ZoneManager):
|
||||
('host4', dict(free_disk_gb=8192, free_ram_mb=8192)),
|
||||
]
|
||||
|
||||
def _compute_node_get_all(self, context):
|
||||
return [
|
||||
dict(local_gb=1024, memory_mb=1024, service=dict(host='host1')),
|
||||
dict(local_gb=2048, memory_mb=2048, service=dict(host='host2')),
|
||||
dict(local_gb=4096, memory_mb=4096, service=dict(host='host3')),
|
||||
dict(local_gb=8192, memory_mb=8192, service=dict(host='host4')),
|
||||
]
|
||||
|
||||
def _instance_get_all(self, context):
|
||||
return [
|
||||
dict(local_gb=512, memory_mb=512, host='host1'),
|
||||
dict(local_gb=512, memory_mb=512, host='host1'),
|
||||
dict(local_gb=512, memory_mb=512, host='host2'),
|
||||
dict(local_gb=1024, memory_mb=1024, host='host3'),
|
||||
]
|
||||
class FakeHostState(host_manager.HostState):
|
||||
def __init__(self, host, topic, attribute_dict):
|
||||
super(FakeHostState, self).__init__(host, topic)
|
||||
for (key, val) in attribute_dict.iteritems():
|
||||
setattr(self, key, val)
|
||||
|
||||
|
||||
def mox_host_manager_db_calls(mox, context):
|
||||
mox.StubOutWithMock(db, 'compute_node_get_all')
|
||||
mox.StubOutWithMock(db, 'instance_get_all')
|
||||
|
||||
db.compute_node_get_all(context).AndReturn(COMPUTE_NODES)
|
||||
db.instance_get_all(context).AndReturn(INSTANCES)
|
@ -18,29 +18,15 @@ Tests For Distributed Scheduler.
|
||||
|
||||
import json
|
||||
|
||||
import nova.db
|
||||
|
||||
from nova.compute import api as compute_api
|
||||
from nova import context
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.scheduler import distributed_scheduler
|
||||
from nova.scheduler import least_cost
|
||||
from nova.scheduler import zone_manager
|
||||
from nova.tests.scheduler import fake_zone_manager as ds_fakes
|
||||
|
||||
|
||||
class FakeEmptyZoneManager(zone_manager.ZoneManager):
|
||||
def __init__(self):
|
||||
self.service_states = {}
|
||||
|
||||
def get_host_list_from_db(self, context):
|
||||
return []
|
||||
|
||||
def _compute_node_get_all(*args, **kwargs):
|
||||
return []
|
||||
|
||||
def _instance_get_all(*args, **kwargs):
|
||||
return []
|
||||
from nova.scheduler import host_manager
|
||||
from nova import test
|
||||
from nova.tests.scheduler import fakes
|
||||
|
||||
|
||||
def fake_call_zone_method(context, method, specs, zones):
|
||||
@ -80,8 +66,8 @@ def fake_zone_get_all(context):
|
||||
]
|
||||
|
||||
|
||||
def fake_filter_hosts(topic, request_info, unfiltered_hosts, options):
|
||||
return unfiltered_hosts
|
||||
def fake_filter_hosts(hosts, filter_properties):
|
||||
return list(hosts)
|
||||
|
||||
|
||||
class DistributedSchedulerTestCase(test.TestCase):
|
||||
@ -92,7 +78,7 @@ class DistributedSchedulerTestCase(test.TestCase):
|
||||
properly adjusted based on the scale/offset in the zone
|
||||
db entries.
|
||||
"""
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
sched = fakes.FakeDistributedScheduler()
|
||||
child_results = fake_call_zone_method(None, None, None, None)
|
||||
zones = fake_zone_get_all(None)
|
||||
weighted_hosts = sched._adjust_child_weights(child_results, zones)
|
||||
@ -113,14 +99,14 @@ class DistributedSchedulerTestCase(test.TestCase):
|
||||
def _fake_empty_call_zone_method(*args, **kwargs):
|
||||
return []
|
||||
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
sched.zone_manager = FakeEmptyZoneManager()
|
||||
sched = fakes.FakeDistributedScheduler()
|
||||
self.stubs.Set(sched, '_call_zone_method',
|
||||
_fake_empty_call_zone_method)
|
||||
self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all)
|
||||
self.stubs.Set(db, 'zone_get_all', fake_zone_get_all)
|
||||
|
||||
fake_context = context.RequestContext('user', 'project')
|
||||
request_spec = dict(instance_type=dict(memory_mb=1, local_gb=1))
|
||||
request_spec = {'instance_type': {'memory_mb': 1, 'local_gb': 1},
|
||||
'instance_properties': {'project_id': 1}}
|
||||
self.assertRaises(exception.NoValidHost, sched.schedule_run_instance,
|
||||
fake_context, request_spec)
|
||||
|
||||
@ -150,7 +136,7 @@ class DistributedSchedulerTestCase(test.TestCase):
|
||||
self.child_zone_called = True
|
||||
return 2
|
||||
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
sched = fakes.FakeDistributedScheduler()
|
||||
self.stubs.Set(sched, '_schedule', _fake_schedule)
|
||||
self.stubs.Set(sched, '_make_weighted_host_from_blob',
|
||||
_fake_make_weighted_host_from_blob)
|
||||
@ -185,7 +171,7 @@ class DistributedSchedulerTestCase(test.TestCase):
|
||||
self.was_admin = context.is_admin
|
||||
return []
|
||||
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
sched = fakes.FakeDistributedScheduler()
|
||||
self.stubs.Set(sched, '_schedule', fake_schedule)
|
||||
|
||||
fake_context = context.RequestContext('user', 'project')
|
||||
@ -196,15 +182,16 @@ class DistributedSchedulerTestCase(test.TestCase):
|
||||
|
||||
def test_schedule_bad_topic(self):
|
||||
"""Parameter checking."""
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
sched = fakes.FakeDistributedScheduler()
|
||||
self.assertRaises(NotImplementedError, sched._schedule, None, "foo",
|
||||
{})
|
||||
|
||||
def test_schedule_no_instance_type(self):
|
||||
"""Parameter checking."""
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
sched = fakes.FakeDistributedScheduler()
|
||||
request_spec = {'instance_properties': {}}
|
||||
self.assertRaises(NotImplementedError, sched._schedule, None,
|
||||
"compute", {})
|
||||
"compute", request_spec=request_spec)
|
||||
|
||||
def test_schedule_happy_day(self):
|
||||
"""Make sure there's nothing glaringly wrong with _schedule()
|
||||
@ -218,26 +205,31 @@ class DistributedSchedulerTestCase(test.TestCase):
|
||||
return least_cost.WeightedHost(self.next_weight, host=host,
|
||||
hostinfo=hostinfo)
|
||||
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
fake_context = context.RequestContext('user', 'project')
|
||||
sched.zone_manager = ds_fakes.FakeZoneManager()
|
||||
self.stubs.Set(sched, '_filter_hosts', fake_filter_hosts)
|
||||
sched = fakes.FakeDistributedScheduler()
|
||||
fake_context = context.RequestContext('user', 'project',
|
||||
is_admin=True)
|
||||
|
||||
self.stubs.Set(sched.host_manager, 'filter_hosts',
|
||||
fake_filter_hosts)
|
||||
self.stubs.Set(least_cost, 'weighted_sum', _fake_weighted_sum)
|
||||
self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all)
|
||||
self.stubs.Set(db, 'zone_get_all', fake_zone_get_all)
|
||||
self.stubs.Set(sched, '_call_zone_method', fake_call_zone_method)
|
||||
|
||||
instance_type = dict(memory_mb=512, local_gb=512)
|
||||
request_spec = dict(num_instances=10, instance_type=instance_type)
|
||||
request_spec = {'num_instances': 10,
|
||||
'instance_type': {'memory_mb': 512, 'local_gb': 512},
|
||||
'instance_properties': {'project_id': 1}}
|
||||
self.mox.ReplayAll()
|
||||
weighted_hosts = sched._schedule(fake_context, 'compute',
|
||||
request_spec)
|
||||
request_spec)
|
||||
self.mox.VerifyAll()
|
||||
self.assertEquals(len(weighted_hosts), 10)
|
||||
for weighted_host in weighted_hosts:
|
||||
# We set this up so remote hosts have even weights ...
|
||||
if int(weighted_host.weight) % 2 == 0:
|
||||
self.assertTrue(weighted_host.zone is not None)
|
||||
self.assertTrue(weighted_host.host is None)
|
||||
self.assertTrue(weighted_host.host_state is None)
|
||||
else:
|
||||
self.assertTrue(weighted_host.host is not None)
|
||||
self.assertTrue(weighted_host.host_state is not None)
|
||||
self.assertTrue(weighted_host.zone is None)
|
||||
|
||||
def test_schedule_local_zone(self):
|
||||
@ -248,33 +240,41 @@ class DistributedSchedulerTestCase(test.TestCase):
|
||||
|
||||
def _fake_weighted_sum(functions, hosts, options):
|
||||
self.next_weight += 2.0
|
||||
host, hostinfo = hosts[0]
|
||||
return least_cost.WeightedHost(self.next_weight, host=host,
|
||||
hostinfo=hostinfo)
|
||||
host = hosts[0]
|
||||
return least_cost.WeightedHost(self.next_weight, host_state=host)
|
||||
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
fake_context = context.RequestContext('user', 'project')
|
||||
sched.zone_manager = ds_fakes.FakeZoneManager()
|
||||
self.stubs.Set(sched, '_filter_hosts', fake_filter_hosts)
|
||||
sched = fakes.FakeDistributedScheduler()
|
||||
fake_context = context.RequestContext('user', 'project',
|
||||
is_admin=True)
|
||||
|
||||
fakes.mox_host_manager_db_calls(self.mox, fake_context)
|
||||
|
||||
self.stubs.Set(sched.host_manager, 'filter_hosts',
|
||||
fake_filter_hosts)
|
||||
self.stubs.Set(least_cost, 'weighted_sum', _fake_weighted_sum)
|
||||
self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all)
|
||||
self.stubs.Set(db, 'zone_get_all', fake_zone_get_all)
|
||||
self.stubs.Set(sched, '_call_zone_method', fake_call_zone_method)
|
||||
|
||||
instance_type = dict(memory_mb=512, local_gb=512)
|
||||
request_spec = dict(num_instances=10, instance_type=instance_type,
|
||||
local_zone=True)
|
||||
request_spec = {'num_instances': 10,
|
||||
'instance_type': {'memory_mb': 512, 'local_gb': 512},
|
||||
'instance_properties': {'project_id': 1,
|
||||
'memory_mb': 512,
|
||||
'local_gb': 512},
|
||||
'local_zone': True}
|
||||
self.mox.ReplayAll()
|
||||
weighted_hosts = sched._schedule(fake_context, 'compute',
|
||||
request_spec)
|
||||
self.mox.VerifyAll()
|
||||
self.assertEquals(len(weighted_hosts), 10)
|
||||
for weighted_host in weighted_hosts:
|
||||
# There should be no remote hosts
|
||||
self.assertTrue(weighted_host.host is not None)
|
||||
self.assertTrue(weighted_host.host_state is not None)
|
||||
self.assertTrue(weighted_host.zone is None)
|
||||
|
||||
def test_decrypt_blob(self):
|
||||
"""Test that the decrypt method works."""
|
||||
|
||||
fixture = ds_fakes.FakeDistributedScheduler()
|
||||
fixture = fakes.FakeDistributedScheduler()
|
||||
test_data = {'weight': 1, 'host': 'x', 'blob': 'y', 'zone': 'z'}
|
||||
|
||||
class StubDecryptor(object):
|
||||
@ -290,49 +290,42 @@ class DistributedSchedulerTestCase(test.TestCase):
|
||||
blob='y', zone='z'))
|
||||
|
||||
def test_get_cost_functions(self):
|
||||
fixture = ds_fakes.FakeDistributedScheduler()
|
||||
self.flags(reserved_host_memory_mb=128)
|
||||
fixture = fakes.FakeDistributedScheduler()
|
||||
fns = fixture.get_cost_functions()
|
||||
self.assertEquals(len(fns), 1)
|
||||
weight, fn = fns[0]
|
||||
self.assertEquals(weight, 1.0)
|
||||
hostinfo = zone_manager.HostInfo('host', free_ram_mb=1000)
|
||||
self.assertEquals(1000, fn(hostinfo))
|
||||
hostinfo = host_manager.HostState('host', 'compute')
|
||||
hostinfo.update_from_compute_node(dict(memory_mb=1000,
|
||||
local_gb=0))
|
||||
self.assertEquals(1000 - 128, fn(hostinfo, {}))
|
||||
|
||||
def test_filter_hosts_avoid(self):
|
||||
"""Test to make sure _filter_hosts() filters original hosts if
|
||||
avoid_original_host is True."""
|
||||
def test_populate_filter_properties(self):
|
||||
request_spec = {'instance_properties': {}}
|
||||
fixture = fakes.FakeDistributedScheduler()
|
||||
filter_properties = {'ignore_hosts': []}
|
||||
fixture.populate_filter_properties(request_spec, filter_properties)
|
||||
self.assertEqual(len(filter_properties['ignore_hosts']), 0)
|
||||
|
||||
def _fake_choose_host_filters():
|
||||
return []
|
||||
# No original host results in not ignoring
|
||||
request_spec = {'instance_properties': {},
|
||||
'avoid_original_host': True}
|
||||
fixture = fakes.FakeDistributedScheduler()
|
||||
fixture.populate_filter_properties(request_spec, filter_properties)
|
||||
self.assertEqual(len(filter_properties['ignore_hosts']), 0)
|
||||
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
fake_context = context.RequestContext('user', 'project')
|
||||
self.stubs.Set(sched, '_choose_host_filters',
|
||||
_fake_choose_host_filters)
|
||||
# Original host but avoid is False should not ignore it
|
||||
request_spec = {'instance_properties': {'host': 'foo'},
|
||||
'avoid_original_host': False}
|
||||
fixture = fakes.FakeDistributedScheduler()
|
||||
fixture.populate_filter_properties(request_spec, filter_properties)
|
||||
self.assertEqual(len(filter_properties['ignore_hosts']), 0)
|
||||
|
||||
hosts = [('host1', '1info'), ('host2', '2info'), ('host3', '3info')]
|
||||
request_spec = dict(instance_properties=dict(host='host2'),
|
||||
avoid_original_host=True)
|
||||
|
||||
filtered = sched._filter_hosts('compute', request_spec, hosts, {})
|
||||
self.assertEqual(filtered,
|
||||
[('host1', '1info'), ('host3', '3info')])
|
||||
|
||||
def test_filter_hosts_no_avoid(self):
|
||||
"""Test to make sure _filter_hosts() does not filter original
|
||||
hosts if avoid_original_host is False."""
|
||||
|
||||
def _fake_choose_host_filters():
|
||||
return []
|
||||
|
||||
sched = ds_fakes.FakeDistributedScheduler()
|
||||
fake_context = context.RequestContext('user', 'project')
|
||||
self.stubs.Set(sched, '_choose_host_filters',
|
||||
_fake_choose_host_filters)
|
||||
|
||||
hosts = [('host1', '1info'), ('host2', '2info'), ('host3', '3info')]
|
||||
request_spec = dict(instance_properties=dict(host='host2'),
|
||||
avoid_original_host=False)
|
||||
|
||||
filtered = sched._filter_hosts('compute', request_spec, hosts, {})
|
||||
self.assertEqual(filtered, hosts)
|
||||
# Original host but avoid is True should ignore it
|
||||
request_spec = {'instance_properties': {'host': 'foo'},
|
||||
'avoid_original_host': True}
|
||||
fixture = fakes.FakeDistributedScheduler()
|
||||
fixture.populate_filter_properties(request_spec, filter_properties)
|
||||
self.assertEqual(len(filter_properties['ignore_hosts']), 1)
|
||||
self.assertEqual(filter_properties['ignore_hosts'][0], 'foo')
|
||||
|
@ -1,252 +0,0 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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 Scheduler Host Filters.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import nova
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.scheduler import distributed_scheduler as dist
|
||||
from nova.tests.scheduler import fake_zone_manager as ds_fakes
|
||||
|
||||
|
||||
class HostFilterTestCase(test.TestCase):
|
||||
"""Test case for host filters."""
|
||||
|
||||
def _host_caps(self, multiplier):
|
||||
# Returns host capabilities in the following way:
|
||||
# host1 = memory:free 10 (100max)
|
||||
# disk:available 100 (1000max)
|
||||
# hostN = memory:free 10 + 10N
|
||||
# disk:available 100 + 100N
|
||||
# in other words: hostN has more resources than host0
|
||||
# which means ... don't go above 10 hosts.
|
||||
return {'host_name-description': 'XenServer %s' % multiplier,
|
||||
'host_hostname': 'xs-%s' % multiplier,
|
||||
'host_memory_total': 100,
|
||||
'host_memory_overhead': 10,
|
||||
'host_memory_free': 10 + multiplier * 10,
|
||||
'host_memory_free-computed': 10 + multiplier * 10,
|
||||
'host_other-config': {},
|
||||
'host_ip_address': '192.168.1.%d' % (100 + multiplier),
|
||||
'host_cpu_info': {},
|
||||
'disk_available': 100 + multiplier * 100,
|
||||
'disk_total': 1000,
|
||||
'disk_used': 0,
|
||||
'host_uuid': 'xxx-%d' % multiplier,
|
||||
'host_name-label': 'xs-%s' % multiplier,
|
||||
'enabled': True}
|
||||
|
||||
def setUp(self):
|
||||
super(HostFilterTestCase, self).setUp()
|
||||
default_host_filters = ['AllHostsFilter']
|
||||
self.flags(default_host_filters=default_host_filters,
|
||||
reserved_host_disk_mb=0, reserved_host_memory_mb=0)
|
||||
self.instance_type = dict(name='tiny',
|
||||
memory_mb=30,
|
||||
vcpus=10,
|
||||
local_gb=300,
|
||||
flavorid=1,
|
||||
swap=500,
|
||||
rxtx_quota=30000,
|
||||
rxtx_cap=200,
|
||||
extra_specs={})
|
||||
self.gpu_instance_type = dict(name='tiny.gpu',
|
||||
memory_mb=30,
|
||||
vcpus=10,
|
||||
local_gb=300,
|
||||
flavorid=2,
|
||||
swap=500,
|
||||
rxtx_quota=30000,
|
||||
rxtx_cap=200,
|
||||
extra_specs={'xpu_arch': 'fermi',
|
||||
'xpu_info': 'Tesla 2050'})
|
||||
|
||||
self.zone_manager = ds_fakes.FakeZoneManager()
|
||||
states = {}
|
||||
for x in xrange(4):
|
||||
states['host%d' % (x + 1)] = {'compute': self._host_caps(x)}
|
||||
self.zone_manager.service_states = states
|
||||
|
||||
# Add some extra capabilities to some hosts
|
||||
host4 = self.zone_manager.service_states['host4']['compute']
|
||||
host4['xpu_arch'] = 'fermi'
|
||||
host4['xpu_info'] = 'Tesla 2050'
|
||||
|
||||
host2 = self.zone_manager.service_states['host2']['compute']
|
||||
host2['xpu_arch'] = 'radeon'
|
||||
|
||||
host3 = self.zone_manager.service_states['host3']['compute']
|
||||
host3['xpu_arch'] = 'fermi'
|
||||
host3['xpu_info'] = 'Tesla 2150'
|
||||
|
||||
def _get_all_hosts(self):
|
||||
return self.zone_manager.get_all_host_data(None).items()
|
||||
|
||||
def test_choose_filter(self):
|
||||
# Test default filter ...
|
||||
sched = dist.DistributedScheduler()
|
||||
hfs = sched._choose_host_filters()
|
||||
hf = hfs[0]
|
||||
self.assertEquals(hf._full_name().split(".")[-1], 'AllHostsFilter')
|
||||
# Test valid filter ...
|
||||
hfs = sched._choose_host_filters('InstanceTypeFilter')
|
||||
hf = hfs[0]
|
||||
self.assertEquals(hf._full_name().split(".")[-1], 'InstanceTypeFilter')
|
||||
# Test invalid filter ...
|
||||
try:
|
||||
sched._choose_host_filters('does not exist')
|
||||
self.fail("Should not find host filter.")
|
||||
except exception.SchedulerHostFilterNotFound:
|
||||
pass
|
||||
|
||||
def test_all_host_filter(self):
|
||||
sched = dist.DistributedScheduler()
|
||||
hfs = sched._choose_host_filters('AllHostsFilter')
|
||||
hf = hfs[0]
|
||||
all_hosts = self._get_all_hosts()
|
||||
cooked = hf.instance_type_to_filter(self.instance_type)
|
||||
hosts = hf.filter_hosts(all_hosts, cooked, {})
|
||||
self.assertEquals(4, len(hosts))
|
||||
for host, capabilities in hosts:
|
||||
self.assertTrue(host.startswith('host'))
|
||||
|
||||
def test_instance_type_filter(self):
|
||||
hf = nova.scheduler.filters.InstanceTypeFilter()
|
||||
# filter all hosts that can support 30 ram and 300 disk
|
||||
cooked = hf.instance_type_to_filter(self.instance_type)
|
||||
all_hosts = self._get_all_hosts()
|
||||
hosts = hf.filter_hosts(all_hosts, cooked, {})
|
||||
self.assertEquals(3, len(hosts))
|
||||
just_hosts = [host for host, hostinfo in hosts]
|
||||
just_hosts.sort()
|
||||
self.assertEquals('host4', just_hosts[2])
|
||||
self.assertEquals('host3', just_hosts[1])
|
||||
self.assertEquals('host2', just_hosts[0])
|
||||
|
||||
def test_instance_type_filter_reserved_memory(self):
|
||||
self.flags(reserved_host_memory_mb=2048)
|
||||
hf = nova.scheduler.filters.InstanceTypeFilter()
|
||||
# filter all hosts that can support 30 ram and 300 disk after
|
||||
# reserving 2048 ram
|
||||
cooked = hf.instance_type_to_filter(self.instance_type)
|
||||
all_hosts = self._get_all_hosts()
|
||||
hosts = hf.filter_hosts(all_hosts, cooked, {})
|
||||
self.assertEquals(2, len(hosts))
|
||||
just_hosts = [host for host, hostinfo in hosts]
|
||||
just_hosts.sort()
|
||||
self.assertEquals('host4', just_hosts[1])
|
||||
self.assertEquals('host3', just_hosts[0])
|
||||
|
||||
def test_instance_type_filter_extra_specs(self):
|
||||
hf = nova.scheduler.filters.InstanceTypeFilter()
|
||||
# filter all hosts that can support 30 ram and 300 disk
|
||||
cooked = hf.instance_type_to_filter(self.gpu_instance_type)
|
||||
all_hosts = self._get_all_hosts()
|
||||
hosts = hf.filter_hosts(all_hosts, cooked, {})
|
||||
self.assertEquals(1, len(hosts))
|
||||
just_hosts = [host for host, caps in hosts]
|
||||
self.assertEquals('host4', just_hosts[0])
|
||||
|
||||
def test_json_filter(self):
|
||||
hf = nova.scheduler.filters.JsonFilter()
|
||||
# filter all hosts that can support 30 ram and 300 disk
|
||||
cooked = hf.instance_type_to_filter(self.instance_type)
|
||||
all_hosts = self._get_all_hosts()
|
||||
hosts = hf.filter_hosts(all_hosts, cooked, {})
|
||||
self.assertEquals(2, len(hosts))
|
||||
just_hosts = [host for host, caps in hosts]
|
||||
just_hosts.sort()
|
||||
self.assertEquals('host3', just_hosts[0])
|
||||
self.assertEquals('host4', just_hosts[1])
|
||||
|
||||
# Try some custom queries
|
||||
|
||||
raw = ['or',
|
||||
['and',
|
||||
['<', '$compute.host_memory_free', 30],
|
||||
['<', '$compute.disk_available', 300],
|
||||
],
|
||||
['and',
|
||||
['>', '$compute.host_memory_free', 30],
|
||||
['>', '$compute.disk_available', 300],
|
||||
]
|
||||
]
|
||||
cooked = json.dumps(raw)
|
||||
hosts = hf.filter_hosts(all_hosts, cooked, {})
|
||||
|
||||
self.assertEquals(3, len(hosts))
|
||||
just_hosts = [host for host, caps in hosts]
|
||||
just_hosts.sort()
|
||||
for index, host in zip([1, 2, 4], just_hosts):
|
||||
self.assertEquals('host%d' % index, host)
|
||||
|
||||
raw = ['not',
|
||||
['=', '$compute.host_memory_free', 30],
|
||||
]
|
||||
cooked = json.dumps(raw)
|
||||
hosts = hf.filter_hosts(all_hosts, cooked, {})
|
||||
|
||||
self.assertEquals(3, len(hosts))
|
||||
just_hosts = [host for host, caps in hosts]
|
||||
just_hosts.sort()
|
||||
for index, host in zip([1, 2, 4], just_hosts):
|
||||
self.assertEquals('host%d' % index, host)
|
||||
|
||||
raw = ['in', '$compute.host_memory_free', 20, 40, 60, 80, 100]
|
||||
cooked = json.dumps(raw)
|
||||
hosts = hf.filter_hosts(all_hosts, cooked, {})
|
||||
self.assertEquals(2, len(hosts))
|
||||
just_hosts = [host for host, caps in hosts]
|
||||
just_hosts.sort()
|
||||
for index, host in zip([2, 4], just_hosts):
|
||||
self.assertEquals('host%d' % index, host)
|
||||
|
||||
# Try some bogus input ...
|
||||
raw = ['unknown command', ]
|
||||
cooked = json.dumps(raw)
|
||||
try:
|
||||
hf.filter_hosts(all_hosts, cooked, {})
|
||||
self.fail("Should give KeyError")
|
||||
except KeyError, e:
|
||||
pass
|
||||
|
||||
self.assertTrue(hf.filter_hosts(all_hosts, json.dumps([]), {}))
|
||||
self.assertTrue(hf.filter_hosts(all_hosts, json.dumps({}), {}))
|
||||
self.assertTrue(hf.filter_hosts(all_hosts, json.dumps(
|
||||
['not', True, False, True, False],
|
||||
), {}))
|
||||
|
||||
try:
|
||||
hf.filter_hosts(all_hosts, json.dumps(
|
||||
'not', True, False, True, False,), {})
|
||||
self.fail("Should give KeyError")
|
||||
except KeyError, e:
|
||||
pass
|
||||
|
||||
self.assertFalse(hf.filter_hosts(all_hosts,
|
||||
json.dumps(['=', '$foo', 100]), {}))
|
||||
self.assertFalse(hf.filter_hosts(all_hosts,
|
||||
json.dumps(['=', '$.....', 100]), {}))
|
||||
self.assertFalse(hf.filter_hosts(all_hosts,
|
||||
json.dumps(
|
||||
['>', ['and', ['or', ['not', ['<', ['>=', ['<=', ['in', ]]]]]]]]),
|
||||
{}))
|
||||
|
||||
self.assertFalse(hf.filter_hosts(all_hosts,
|
||||
json.dumps(['=', {}, ['>', '$missing....foo']]), {}))
|
333
nova/tests/scheduler/test_host_filters.py
Normal file
333
nova/tests/scheduler/test_host_filters.py
Normal file
@ -0,0 +1,333 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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 Scheduler Host Filters.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from nova.scheduler import filters
|
||||
from nova import test
|
||||
from nova.tests.scheduler import fakes
|
||||
|
||||
|
||||
class HostFiltersTestCase(test.TestCase):
|
||||
"""Test case for host filters."""
|
||||
|
||||
def setUp(self):
|
||||
super(HostFiltersTestCase, self).setUp()
|
||||
self.json_query = json.dumps(
|
||||
['and', ['>=', '$free_ram_mb', 1024],
|
||||
['>=', '$free_disk_mb', 200 * 1024]])
|
||||
|
||||
def test_all_host_filter(self):
|
||||
filt_cls = filters.AllHostsFilter()
|
||||
host = fakes.FakeHostState('host1', 'compute', {})
|
||||
self.assertTrue(filt_cls.host_passes(host, {}))
|
||||
|
||||
def test_compute_filter_passes(self):
|
||||
filt_cls = filters.ComputeFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024}}
|
||||
capabilities = {'enabled': True}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1024, 'capabilities': capabilities})
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_compute_filter_fails_on_memory(self):
|
||||
filt_cls = filters.ComputeFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024}}
|
||||
capabilities = {'enabled': True}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1023, 'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_compute_filter_fails_on_disabled(self):
|
||||
filt_cls = filters.ComputeFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024}}
|
||||
capabilities = {'enabled': False}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1024, 'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_compute_filter_passes_on_volume(self):
|
||||
filt_cls = filters.ComputeFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024}}
|
||||
capabilities = {'enabled': False}
|
||||
host = fakes.FakeHostState('host1', 'volume',
|
||||
{'free_ram_mb': 1024, 'capabilities': capabilities})
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_compute_filter_passes_on_no_instance_type(self):
|
||||
filt_cls = filters.ComputeFilter()
|
||||
filter_properties = {}
|
||||
capabilities = {'enabled': False}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1024, 'capabilities': capabilities})
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_compute_filter_passes_extra_specs(self):
|
||||
filt_cls = filters.ComputeFilter()
|
||||
extra_specs = {'opt1': 1, 'opt2': 2}
|
||||
capabilities = {'enabled': True, 'opt1': 1, 'opt2': 2}
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024,
|
||||
'extra_specs': extra_specs}}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1024, 'capabilities': capabilities})
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_compute_filter_fails_extra_specs(self):
|
||||
filt_cls = filters.ComputeFilter()
|
||||
extra_specs = {'opt1': 1, 'opt2': 3}
|
||||
capabilities = {'enabled': True, 'opt1': 1, 'opt2': 2}
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024,
|
||||
'extra_specs': extra_specs}}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1024, 'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_passes(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024,
|
||||
'local_gb': 200},
|
||||
'query': self.json_query}
|
||||
capabilities = {'enabled': True}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1024,
|
||||
'free_disk_mb': 200 * 1024,
|
||||
'capabilities': capabilities})
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_passes_with_no_query(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024,
|
||||
'local_gb': 200}}
|
||||
capabilities = {'enabled': True}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 0,
|
||||
'free_disk_mb': 0,
|
||||
'capabilities': capabilities})
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_fails_on_memory(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024,
|
||||
'local_gb': 200},
|
||||
'query': self.json_query}
|
||||
capabilities = {'enabled': True}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1023,
|
||||
'free_disk_mb': 200 * 1024,
|
||||
'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_fails_on_disk(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024,
|
||||
'local_gb': 200},
|
||||
'query': self.json_query}
|
||||
capabilities = {'enabled': True}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1024,
|
||||
'free_disk_mb': (200 * 1024) - 1,
|
||||
'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_fails_on_disk(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024,
|
||||
'local_gb': 200},
|
||||
'query': self.json_query}
|
||||
capabilities = {'enabled': True}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1024,
|
||||
'free_disk_mb': (200 * 1024) - 1,
|
||||
'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_fails_on_disabled(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
filter_properties = {'instance_type': {'memory_mb': 1024,
|
||||
'local_gb': 200},
|
||||
'query': self.json_query}
|
||||
capabilities = {'enabled': False}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 1024,
|
||||
'free_disk_mb': 200 * 1024,
|
||||
'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_happy_day(self):
|
||||
"""Test json filter more thoroughly"""
|
||||
filt_cls = filters.JsonFilter()
|
||||
raw = ['and',
|
||||
['=', '$capabilities.opt1', 'match'],
|
||||
['or',
|
||||
['and',
|
||||
['<', '$free_ram_mb', 30],
|
||||
['<', '$free_disk_mb', 300]],
|
||||
['and',
|
||||
['>', '$free_ram_mb', 30],
|
||||
['>', '$free_disk_mb', 300]]]]
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
|
||||
# Passes
|
||||
capabilities = {'enabled': True, 'opt1': 'match'}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 10,
|
||||
'free_disk_mb': 200,
|
||||
'capabilities': capabilities})
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
# Passes
|
||||
capabilities = {'enabled': True, 'opt1': 'match'}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 40,
|
||||
'free_disk_mb': 400,
|
||||
'capabilities': capabilities})
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
# Failes due to disabled
|
||||
capabilities = {'enabled': False, 'opt1': 'match'}
|
||||
host = fakes.FakeHostState('host1', 'instance_type',
|
||||
{'free_ram_mb': 40,
|
||||
'free_disk_mb': 400,
|
||||
'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
# Fails due to being exact memory/disk we don't want
|
||||
capabilities = {'enabled': True, 'opt1': 'match'}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 30,
|
||||
'free_disk_mb': 300,
|
||||
'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
# Fails due to memory lower but disk higher
|
||||
capabilities = {'enabled': True, 'opt1': 'match'}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 20,
|
||||
'free_disk_mb': 400,
|
||||
'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
# Fails due to capabilities 'opt1' not equal
|
||||
capabilities = {'enabled': True, 'opt1': 'no-match'}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'free_ram_mb': 20,
|
||||
'free_disk_mb': 400,
|
||||
'capabilities': capabilities})
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_basic_operators(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'capabilities': {'enabled': True}})
|
||||
# (operator, arguments, expected_result)
|
||||
ops_to_test = [
|
||||
['=', [1, 1], True],
|
||||
['=', [1, 2], False],
|
||||
['<', [1, 2], True],
|
||||
['<', [1, 1], False],
|
||||
['<', [2, 1], False],
|
||||
['>', [2, 1], True],
|
||||
['>', [2, 2], False],
|
||||
['>', [2, 3], False],
|
||||
['<=', [1, 2], True],
|
||||
['<=', [1, 1], True],
|
||||
['<=', [2, 1], False],
|
||||
['>=', [2, 1], True],
|
||||
['>=', [2, 2], True],
|
||||
['>=', [2, 3], False],
|
||||
['in', [1, 1], True],
|
||||
['in', [1, 1, 2, 3], True],
|
||||
['in', [4, 1, 2, 3], False],
|
||||
['not', [True], False],
|
||||
['not', [False], True],
|
||||
['or', [True, False], True],
|
||||
['or', [False, False], False],
|
||||
['and', [True, True], True],
|
||||
['and', [False, False], False],
|
||||
['and', [True, False], False],
|
||||
# Nested ((True or False) and (2 > 1)) == Passes
|
||||
['and', [['or', True, False], ['>', 2, 1]], True]]
|
||||
|
||||
for (op, args, expected) in ops_to_test:
|
||||
raw = [op] + args
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
self.assertEqual(expected,
|
||||
filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
# This results in [False, True, False, True] and if any are True
|
||||
# then it passes...
|
||||
raw = ['not', True, False, True, False]
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
# This results in [False, False, False] and if any are True
|
||||
# then it passes...which this doesn't
|
||||
raw = ['not', True, True, True]
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_unknown_operator_raises(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
raw = ['!=', 1, 2]
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
capabilities = {'enabled': True, 'opt1': 'no-match'}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'capabilities': {'enabled': True}})
|
||||
self.assertRaises(KeyError,
|
||||
filt_cls.host_passes, host, filter_properties)
|
||||
|
||||
def test_json_filter_empty_filters_pass(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
capabilities = {'enabled': True, 'opt1': 'no-match'}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'capabilities': {'enabled': True}})
|
||||
|
||||
raw = []
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
raw = {}
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_invalid_num_arguments_fails(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
capabilities = {'enabled': True, 'opt1': 'no-match'}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'capabilities': {'enabled': True}})
|
||||
|
||||
raw = ['>', ['and', ['or', ['not', ['<', ['>=', ['<=', ['in', ]]]]]]]]
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
raw = ['>', 1]
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
self.assertFalse(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
def test_json_filter_unknown_variable_ignored(self):
|
||||
filt_cls = filters.JsonFilter()
|
||||
capabilities = {'enabled': True, 'opt1': 'no-match'}
|
||||
host = fakes.FakeHostState('host1', 'compute',
|
||||
{'capabilities': {'enabled': True}})
|
||||
|
||||
raw = ['=', '$........', 1, 1]
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
||||
|
||||
raw = ['=', '$foo', 2, 2]
|
||||
filter_properties = {'query': json.dumps(raw)}
|
||||
self.assertTrue(filt_cls.host_passes(host, filter_properties))
|
360
nova/tests/scheduler/test_host_manager.py
Normal file
360
nova/tests/scheduler/test_host_manager.py
Normal file
@ -0,0 +1,360 @@
|
||||
# Copyright (c) 2011 Openstack, LLC
|
||||
# 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 HostManager
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import mox
|
||||
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova import log as logging
|
||||
from nova.scheduler import host_manager
|
||||
from nova import test
|
||||
from nova.tests.scheduler import fakes
|
||||
from nova import utils
|
||||
|
||||
|
||||
class ComputeFilterClass1(object):
|
||||
def host_passes(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class ComputeFilterClass2(object):
|
||||
def host_passes(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class HostManagerTestCase(test.TestCase):
|
||||
"""Test case for HostManager class"""
|
||||
|
||||
def setUp(self):
|
||||
super(HostManagerTestCase, self).setUp()
|
||||
self.host_manager = host_manager.HostManager()
|
||||
|
||||
def test_choose_host_filters_not_found(self):
|
||||
self.flags(default_host_filters='ComputeFilterClass3')
|
||||
self.host_manager.filter_classes = [ComputeFilterClass1,
|
||||
ComputeFilterClass2]
|
||||
self.assertRaises(exception.SchedulerHostFilterNotFound,
|
||||
self.host_manager._choose_host_filters, None)
|
||||
|
||||
def test_choose_host_filters(self):
|
||||
self.flags(default_host_filters=['ComputeFilterClass2'])
|
||||
self.host_manager.filter_classes = [ComputeFilterClass1,
|
||||
ComputeFilterClass2]
|
||||
|
||||
# Test 'compute' returns 1 correct function
|
||||
filter_fns = self.host_manager._choose_host_filters(None)
|
||||
self.assertEqual(len(filter_fns), 1)
|
||||
self.assertEqual(filter_fns[0].__func__,
|
||||
ComputeFilterClass2.host_passes.__func__)
|
||||
|
||||
def test_filter_hosts(self):
|
||||
topic = 'fake_topic'
|
||||
|
||||
filters = ['fake-filter1', 'fake-filter2']
|
||||
fake_host1 = host_manager.HostState('host1', topic)
|
||||
fake_host2 = host_manager.HostState('host2', topic)
|
||||
hosts = [fake_host1, fake_host2]
|
||||
filter_properties = 'fake_properties'
|
||||
|
||||
self.mox.StubOutWithMock(self.host_manager,
|
||||
'_choose_host_filters')
|
||||
self.mox.StubOutWithMock(fake_host1, 'passes_filters')
|
||||
self.mox.StubOutWithMock(fake_host2, 'passes_filters')
|
||||
|
||||
self.host_manager._choose_host_filters(None).AndReturn(filters)
|
||||
fake_host1.passes_filters(filters, filter_properties).AndReturn(
|
||||
False)
|
||||
fake_host2.passes_filters(filters, filter_properties).AndReturn(
|
||||
True)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
filtered_hosts = self.host_manager.filter_hosts(hosts,
|
||||
filter_properties, filters=None)
|
||||
self.mox.VerifyAll()
|
||||
self.assertEqual(len(filtered_hosts), 1)
|
||||
self.assertEqual(filtered_hosts[0], fake_host2)
|
||||
|
||||
def test_update_service_capabilities(self):
|
||||
service_states = self.host_manager.service_states
|
||||
self.assertDictMatch(service_states, {})
|
||||
self.mox.StubOutWithMock(utils, 'utcnow')
|
||||
utils.utcnow().AndReturn(31337)
|
||||
utils.utcnow().AndReturn(31338)
|
||||
utils.utcnow().AndReturn(31339)
|
||||
|
||||
host1_compute_capabs = dict(free_memory=1234, host_memory=5678,
|
||||
timestamp=1)
|
||||
host1_volume_capabs = dict(free_disk=4321, timestamp=1)
|
||||
host2_compute_capabs = dict(free_memory=8756, timestamp=1)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
self.host_manager.update_service_capabilities('compute', 'host1',
|
||||
host1_compute_capabs)
|
||||
self.host_manager.update_service_capabilities('volume', 'host1',
|
||||
host1_volume_capabs)
|
||||
self.host_manager.update_service_capabilities('compute', 'host2',
|
||||
host2_compute_capabs)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
# Make sure dictionary isn't re-assigned
|
||||
self.assertEqual(self.host_manager.service_states, service_states)
|
||||
# Make sure original dictionary wasn't copied
|
||||
self.assertEqual(host1_compute_capabs['timestamp'], 1)
|
||||
|
||||
host1_compute_capabs['timestamp'] = 31337
|
||||
host1_volume_capabs['timestamp'] = 31338
|
||||
host2_compute_capabs['timestamp'] = 31339
|
||||
|
||||
expected = {'host1': {'compute': host1_compute_capabs,
|
||||
'volume': host1_volume_capabs},
|
||||
'host2': {'compute': host2_compute_capabs}}
|
||||
self.assertDictMatch(service_states, expected)
|
||||
|
||||
def test_host_service_caps_stale(self):
|
||||
self.flags(periodic_interval=5)
|
||||
|
||||
host1_compute_capabs = dict(free_memory=1234, host_memory=5678,
|
||||
timestamp=datetime.datetime.fromtimestamp(3000))
|
||||
host1_volume_capabs = dict(free_disk=4321,
|
||||
timestamp=datetime.datetime.fromtimestamp(3005))
|
||||
host2_compute_capabs = dict(free_memory=8756,
|
||||
timestamp=datetime.datetime.fromtimestamp(3010))
|
||||
|
||||
service_states = {'host1': {'compute': host1_compute_capabs,
|
||||
'volume': host1_volume_capabs},
|
||||
'host2': {'compute': host2_compute_capabs}}
|
||||
|
||||
self.host_manager.service_states = service_states
|
||||
|
||||
self.mox.StubOutWithMock(utils, 'utcnow')
|
||||
utils.utcnow().AndReturn(datetime.datetime.fromtimestamp(3020))
|
||||
utils.utcnow().AndReturn(datetime.datetime.fromtimestamp(3020))
|
||||
utils.utcnow().AndReturn(datetime.datetime.fromtimestamp(3020))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
res1 = self.host_manager.host_service_caps_stale('host1', 'compute')
|
||||
res2 = self.host_manager.host_service_caps_stale('host1', 'volume')
|
||||
res3 = self.host_manager.host_service_caps_stale('host2', 'compute')
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEqual(res1, True)
|
||||
self.assertEqual(res2, False)
|
||||
self.assertEqual(res3, False)
|
||||
|
||||
def test_delete_expired_host_services(self):
|
||||
host1_compute_capabs = dict(free_memory=1234, host_memory=5678,
|
||||
timestamp=datetime.datetime.fromtimestamp(3000))
|
||||
host1_volume_capabs = dict(free_disk=4321,
|
||||
timestamp=datetime.datetime.fromtimestamp(3005))
|
||||
host2_compute_capabs = dict(free_memory=8756,
|
||||
timestamp=datetime.datetime.fromtimestamp(3010))
|
||||
|
||||
service_states = {'host1': {'compute': host1_compute_capabs,
|
||||
'volume': host1_volume_capabs},
|
||||
'host2': {'compute': host2_compute_capabs}}
|
||||
self.host_manager.service_states = service_states
|
||||
|
||||
to_delete = {'host1': {'volume': host1_volume_capabs},
|
||||
'host2': {'compute': host2_compute_capabs}}
|
||||
|
||||
self.host_manager.delete_expired_host_services(to_delete)
|
||||
# Make sure dictionary isn't re-assigned
|
||||
self.assertEqual(self.host_manager.service_states, service_states)
|
||||
|
||||
expected = {'host1': {'compute': host1_compute_capabs}}
|
||||
self.assertEqual(service_states, expected)
|
||||
|
||||
def test_get_service_capabilities(self):
|
||||
host1_compute_capabs = dict(free_memory=1000, host_memory=5678,
|
||||
timestamp=datetime.datetime.fromtimestamp(3000))
|
||||
host1_volume_capabs = dict(free_disk=4321,
|
||||
timestamp=datetime.datetime.fromtimestamp(3005))
|
||||
host2_compute_capabs = dict(free_memory=8756,
|
||||
timestamp=datetime.datetime.fromtimestamp(3010))
|
||||
host2_volume_capabs = dict(free_disk=8756,
|
||||
enabled=False,
|
||||
timestamp=datetime.datetime.fromtimestamp(3010))
|
||||
host3_compute_capabs = dict(free_memory=1234, host_memory=4000,
|
||||
timestamp=datetime.datetime.fromtimestamp(3010))
|
||||
host3_volume_capabs = dict(free_disk=2000,
|
||||
timestamp=datetime.datetime.fromtimestamp(3010))
|
||||
|
||||
service_states = {'host1': {'compute': host1_compute_capabs,
|
||||
'volume': host1_volume_capabs},
|
||||
'host2': {'compute': host2_compute_capabs,
|
||||
'volume': host2_volume_capabs},
|
||||
'host3': {'compute': host3_compute_capabs,
|
||||
'volume': host3_volume_capabs}}
|
||||
self.host_manager.service_states = service_states
|
||||
|
||||
info = {'called': 0}
|
||||
|
||||
# This tests with 1 volume disabled (host2), and 1 volume node
|
||||
# as stale (host1)
|
||||
def _fake_host_service_caps_stale(host, service):
|
||||
info['called'] += 1
|
||||
if host == 'host1':
|
||||
if service == 'compute':
|
||||
return False
|
||||
elif service == 'volume':
|
||||
return True
|
||||
elif host == 'host2':
|
||||
# Shouldn't get here for 'volume' because the service
|
||||
# is disabled
|
||||
self.assertEqual(service, 'compute')
|
||||
return False
|
||||
self.assertEqual(host, 'host3')
|
||||
return False
|
||||
|
||||
self.stubs.Set(self.host_manager, 'host_service_caps_stale',
|
||||
_fake_host_service_caps_stale)
|
||||
|
||||
self.mox.StubOutWithMock(self.host_manager,
|
||||
'delete_expired_host_services')
|
||||
self.host_manager.delete_expired_host_services({'host1': ['volume']})
|
||||
|
||||
self.mox.ReplayAll()
|
||||
result = self.host_manager.get_service_capabilities()
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEqual(info['called'], 5)
|
||||
|
||||
# only 1 volume node active == 'host3', so min/max is 2000
|
||||
expected = {'volume_free_disk': (2000, 2000),
|
||||
'compute_host_memory': (4000, 5678),
|
||||
'compute_free_memory': (1000, 8756)}
|
||||
|
||||
self.assertDictMatch(result, expected)
|
||||
|
||||
def test_get_all_host_states(self):
|
||||
self.flags(reserved_host_memory_mb=512,
|
||||
reserved_host_disk_mb=1024)
|
||||
|
||||
context = 'fake_context'
|
||||
topic = 'compute'
|
||||
|
||||
self.mox.StubOutWithMock(db, 'compute_node_get_all')
|
||||
self.mox.StubOutWithMock(logging, 'warn')
|
||||
self.mox.StubOutWithMock(db, 'instance_get_all')
|
||||
|
||||
db.compute_node_get_all(context).AndReturn(fakes.COMPUTE_NODES)
|
||||
# Invalid service
|
||||
logging.warn("No service for compute ID 5")
|
||||
db.instance_get_all(context).AndReturn(fakes.INSTANCES)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
host_states = self.host_manager.get_all_host_states(context, topic)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEqual(len(host_states), 4)
|
||||
self.assertEqual(host_states['host1'].free_ram_mb, 0)
|
||||
# 511GB
|
||||
self.assertEqual(host_states['host1'].free_disk_mb, 523264)
|
||||
self.assertEqual(host_states['host2'].free_ram_mb, 512)
|
||||
# 1023GB
|
||||
self.assertEqual(host_states['host2'].free_disk_mb, 1047552)
|
||||
self.assertEqual(host_states['host3'].free_ram_mb, 2560)
|
||||
# 3071GB
|
||||
self.assertEqual(host_states['host3'].free_disk_mb, 3144704)
|
||||
self.assertEqual(host_states['host4'].free_ram_mb, 7680)
|
||||
# 8191GB
|
||||
self.assertEqual(host_states['host4'].free_disk_mb, 8387584)
|
||||
|
||||
|
||||
class HostStateTestCase(test.TestCase):
|
||||
"""Test case for HostState class"""
|
||||
|
||||
def setUp(self):
|
||||
super(HostStateTestCase, self).setUp()
|
||||
|
||||
# update_from_compute_node() and consume_from_instance() are tested
|
||||
# in HostManagerTestCase.test_get_all_host_states()
|
||||
|
||||
def test_host_state_passes_filters_passes(self):
|
||||
fake_host = host_manager.HostState('host1', 'compute')
|
||||
filter_properties = {}
|
||||
|
||||
cls1 = ComputeFilterClass1()
|
||||
cls2 = ComputeFilterClass2()
|
||||
self.mox.StubOutWithMock(cls1, 'host_passes')
|
||||
self.mox.StubOutWithMock(cls2, 'host_passes')
|
||||
filter_fns = [cls1.host_passes, cls2.host_passes]
|
||||
|
||||
cls1.host_passes(fake_host, filter_properties).AndReturn(True)
|
||||
cls2.host_passes(fake_host, filter_properties).AndReturn(True)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
result = fake_host.passes_filters(filter_fns, filter_properties)
|
||||
self.mox.VerifyAll()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_host_state_passes_filters_passes_with_ignore(self):
|
||||
fake_host = host_manager.HostState('host1', 'compute')
|
||||
filter_properties = {'ignore_hosts': ['host2']}
|
||||
|
||||
cls1 = ComputeFilterClass1()
|
||||
cls2 = ComputeFilterClass2()
|
||||
self.mox.StubOutWithMock(cls1, 'host_passes')
|
||||
self.mox.StubOutWithMock(cls2, 'host_passes')
|
||||
filter_fns = [cls1.host_passes, cls2.host_passes]
|
||||
|
||||
cls1.host_passes(fake_host, filter_properties).AndReturn(True)
|
||||
cls2.host_passes(fake_host, filter_properties).AndReturn(True)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
result = fake_host.passes_filters(filter_fns, filter_properties)
|
||||
self.mox.VerifyAll()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_host_state_passes_filters_fails(self):
|
||||
fake_host = host_manager.HostState('host1', 'compute')
|
||||
filter_properties = {}
|
||||
|
||||
cls1 = ComputeFilterClass1()
|
||||
cls2 = ComputeFilterClass2()
|
||||
self.mox.StubOutWithMock(cls1, 'host_passes')
|
||||
self.mox.StubOutWithMock(cls2, 'host_passes')
|
||||
filter_fns = [cls1.host_passes, cls2.host_passes]
|
||||
|
||||
cls1.host_passes(fake_host, filter_properties).AndReturn(False)
|
||||
# cls2.host_passes() not called because of short circuit
|
||||
|
||||
self.mox.ReplayAll()
|
||||
result = fake_host.passes_filters(filter_fns, filter_properties)
|
||||
self.mox.VerifyAll()
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_host_state_passes_filters_fails_from_ignore(self):
|
||||
fake_host = host_manager.HostState('host1', 'compute')
|
||||
filter_properties = {'ignore_hosts': ['host1']}
|
||||
|
||||
cls1 = ComputeFilterClass1()
|
||||
cls2 = ComputeFilterClass2()
|
||||
self.mox.StubOutWithMock(cls1, 'host_passes')
|
||||
self.mox.StubOutWithMock(cls2, 'host_passes')
|
||||
filter_fns = [cls1.host_passes, cls2.host_passes]
|
||||
|
||||
# cls[12].host_passes() not called because of short circuit
|
||||
# with matching host to ignore
|
||||
|
||||
self.mox.ReplayAll()
|
||||
result = fake_host.passes_filters(filter_fns, filter_properties)
|
||||
self.mox.VerifyAll()
|
||||
self.assertFalse(result)
|
@ -15,9 +15,10 @@
|
||||
"""
|
||||
Tests For Least Cost functions.
|
||||
"""
|
||||
from nova import context
|
||||
from nova.scheduler import least_cost
|
||||
from nova import test
|
||||
from nova.tests.scheduler import fake_zone_manager
|
||||
from nova.tests.scheduler import fakes
|
||||
|
||||
|
||||
def offset(hostinfo, options):
|
||||
@ -32,38 +33,47 @@ class LeastCostTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(LeastCostTestCase, self).setUp()
|
||||
self.flags(reserved_host_disk_mb=0, reserved_host_memory_mb=0)
|
||||
|
||||
self.zone_manager = fake_zone_manager.FakeZoneManager()
|
||||
self.host_manager = fakes.FakeHostManager()
|
||||
|
||||
def tearDown(self):
|
||||
super(LeastCostTestCase, self).tearDown()
|
||||
|
||||
def _get_all_hosts(self):
|
||||
ctxt = context.get_admin_context()
|
||||
fakes.mox_host_manager_db_calls(self.mox, ctxt)
|
||||
self.mox.ReplayAll()
|
||||
host_states = self.host_manager.get_all_host_states(ctxt,
|
||||
'compute').values()
|
||||
self.mox.VerifyAll()
|
||||
self.mox.ResetAll()
|
||||
return host_states
|
||||
|
||||
def test_weighted_sum_happy_day(self):
|
||||
fn_tuples = [(1.0, offset), (1.0, scale)]
|
||||
hostinfo_list = self.zone_manager.get_all_host_data(None).items()
|
||||
hostinfo_list = self._get_all_hosts()
|
||||
|
||||
# host1: free_ram_mb=0
|
||||
# host2: free_ram_mb=1536
|
||||
# host1: free_ram_mb=512
|
||||
# host2: free_ram_mb=1024
|
||||
# host3: free_ram_mb=3072
|
||||
# host4: free_ram_mb=8192
|
||||
|
||||
# [offset, scale]=
|
||||
# [10000, 11536, 13072, 18192]
|
||||
# [0, 768, 1536, 4096]
|
||||
# [10512, 11024, 13072, 18192]
|
||||
# [1024, 2048, 6144, 16384]
|
||||
|
||||
# adjusted [ 1.0 * x + 1.0 * y] =
|
||||
# [10000, 12304, 14608, 22288]
|
||||
# [11536, 13072, 19216, 34576]
|
||||
|
||||
# so, host1 should win:
|
||||
options = {}
|
||||
weighted_host = least_cost.weighted_sum(fn_tuples, hostinfo_list,
|
||||
options)
|
||||
self.assertEqual(weighted_host.weight, 10000)
|
||||
self.assertEqual(weighted_host.host, 'host1')
|
||||
options)
|
||||
self.assertEqual(weighted_host.weight, 11536)
|
||||
self.assertEqual(weighted_host.host_state.host, 'host1')
|
||||
|
||||
def test_weighted_sum_single_function(self):
|
||||
fn_tuples = [(1.0, offset), ]
|
||||
hostinfo_list = self.zone_manager.get_all_host_data(None).items()
|
||||
hostinfo_list = self._get_all_hosts()
|
||||
|
||||
# host1: free_ram_mb=0
|
||||
# host2: free_ram_mb=1536
|
||||
@ -71,11 +81,11 @@ class LeastCostTestCase(test.TestCase):
|
||||
# host4: free_ram_mb=8192
|
||||
|
||||
# [offset, ]=
|
||||
# [10000, 11536, 13072, 18192]
|
||||
# [10512, 11024, 13072, 18192]
|
||||
|
||||
# so, host1 should win:
|
||||
options = {}
|
||||
weighted_host = least_cost.weighted_sum(fn_tuples, hostinfo_list,
|
||||
options)
|
||||
self.assertEqual(weighted_host.weight, 10000)
|
||||
self.assertEqual(weighted_host.host, 'host1')
|
||||
self.assertEqual(weighted_host.weight, 10512)
|
||||
self.assertEqual(weighted_host.host_state.host, 'host1')
|
||||
|
189
nova/tests/scheduler/test_zone_manager.py
Normal file
189
nova/tests/scheduler/test_zone_manager.py
Normal file
@ -0,0 +1,189 @@
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# All Rights Reserved.
|
||||
# Copyright 2011 OpenStack LLC
|
||||
#
|
||||
# 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 ZoneManager
|
||||
"""
|
||||
|
||||
import mox
|
||||
|
||||
from nova import db
|
||||
from nova import flags
|
||||
from nova.scheduler import zone_manager
|
||||
from nova import test
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
def _create_zone(zone_id=1, name=None, api_url=None, username=None):
|
||||
if api_url is None:
|
||||
api_url = "http://foo.com"
|
||||
if username is None:
|
||||
username = "user1"
|
||||
if name is None:
|
||||
name = "child1"
|
||||
return dict(id=zone_id, name=name, api_url=api_url,
|
||||
username=username, password="pass1", weight_offset=0.0,
|
||||
weight_scale=1.0)
|
||||
|
||||
|
||||
def exploding_novaclient(zone):
|
||||
"""Used when we want to simulate a novaclient call failing."""
|
||||
raise Exception("kaboom")
|
||||
|
||||
|
||||
class ZoneManagerTestCase(test.TestCase):
|
||||
"""Test case for zone manager"""
|
||||
|
||||
zone_manager_cls = zone_manager.ZoneManager
|
||||
zone_state_cls = zone_manager.ZoneState
|
||||
|
||||
def setUp(self):
|
||||
super(ZoneManagerTestCase, self).setUp()
|
||||
self.zone_manager = self.zone_manager_cls()
|
||||
|
||||
def _create_zone_state(self, zone_id=1, name=None, api_url=None,
|
||||
username=None):
|
||||
zone = self.zone_state_cls()
|
||||
zone.zone_info = _create_zone(zone_id, name, api_url, username)
|
||||
return zone
|
||||
|
||||
def test_update(self):
|
||||
zm = self.zone_manager
|
||||
self.mox.StubOutWithMock(zm, '_refresh_from_db')
|
||||
self.mox.StubOutWithMock(zm, '_poll_zones')
|
||||
zm._refresh_from_db(mox.IgnoreArg())
|
||||
zm._poll_zones()
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm.update(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_refresh_from_db_new(self):
|
||||
zone = _create_zone(zone_id=1, username='user1')
|
||||
self.mox.StubOutWithMock(db, 'zone_get_all')
|
||||
db.zone_get_all(mox.IgnoreArg()).AndReturn([zone])
|
||||
|
||||
zm = self.zone_manager
|
||||
self.assertEquals(len(zm.zone_states), 0)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm._refresh_from_db(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
self.assertIn(1, zm.zone_states)
|
||||
self.assertEquals(zm.zone_states[1].zone_info['username'], 'user1')
|
||||
|
||||
def test_refresh_from_db_replace_existing(self):
|
||||
zone_state = self._create_zone_state(zone_id=1, username='user1')
|
||||
zm = self.zone_manager
|
||||
zm.zone_states[1] = zone_state
|
||||
|
||||
zone = _create_zone(zone_id=1, username='user2')
|
||||
self.mox.StubOutWithMock(db, 'zone_get_all')
|
||||
db.zone_get_all(mox.IgnoreArg()).AndReturn([zone])
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm._refresh_from_db(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
self.assertEquals(zm.zone_states[1].zone_info['username'], 'user2')
|
||||
|
||||
def test_refresh_from_db_missing(self):
|
||||
zone_state = self._create_zone_state(zone_id=1, username='user1')
|
||||
zm = self.zone_manager
|
||||
zm.zone_states[1] = zone_state
|
||||
|
||||
self.mox.StubOutWithMock(db, 'zone_get_all')
|
||||
db.zone_get_all(mox.IgnoreArg()).AndReturn([])
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm._refresh_from_db(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 0)
|
||||
|
||||
def test_refresh_from_db_add(self):
|
||||
zone_state = self._create_zone_state(zone_id=1, username='user1')
|
||||
zm = self.zone_manager
|
||||
zm.zone_states[1] = zone_state
|
||||
|
||||
zone1 = _create_zone(zone_id=1, username='user1')
|
||||
zone2 = _create_zone(zone_id=2, username='user2')
|
||||
self.mox.StubOutWithMock(db, 'zone_get_all')
|
||||
db.zone_get_all(mox.IgnoreArg()).AndReturn([zone1, zone2])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm._refresh_from_db(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 2)
|
||||
self.assertIn(1, zm.zone_states)
|
||||
self.assertIn(2, zm.zone_states)
|
||||
self.assertEquals(zm.zone_states[1].zone_info['username'], 'user1')
|
||||
self.assertEquals(zm.zone_states[2].zone_info['username'], 'user2')
|
||||
|
||||
def test_refresh_from_db_add_and_delete(self):
|
||||
zone_state = self._create_zone_state(zone_id=1, username='user1')
|
||||
zm = self.zone_manager
|
||||
zm.zone_states[1] = zone_state
|
||||
|
||||
zone2 = _create_zone(zone_id=2, username='user2')
|
||||
self.mox.StubOutWithMock(db, 'zone_get_all')
|
||||
db.zone_get_all(mox.IgnoreArg()).AndReturn([zone2])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm._refresh_from_db(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
self.assertIn(2, zm.zone_states)
|
||||
self.assertEquals(zm.zone_states[2].zone_info['username'], 'user2')
|
||||
|
||||
def test_poll_zone(self):
|
||||
zone_state = self._create_zone_state(zone_id=1, name='child1')
|
||||
zone_state.attempt = 1
|
||||
|
||||
self.mox.StubOutWithMock(zone_state, 'call_novaclient')
|
||||
zone_state.call_novaclient().AndReturn(
|
||||
dict(name=zone_state.zone_info['name'],
|
||||
hairdresser='dietz'))
|
||||
self.assertDictMatch(zone_state.capabilities, {})
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zone_state.poll()
|
||||
self.mox.VerifyAll()
|
||||
self.assertEquals(zone_state.attempt, 0)
|
||||
self.assertDictMatch(zone_state.capabilities,
|
||||
dict(hairdresser='dietz'))
|
||||
self.assertTrue(zone_state.is_active)
|
||||
|
||||
def test_poll_zones_with_failure(self):
|
||||
zone_state = self._create_zone_state(zone_id=1)
|
||||
zone_state.attempt = FLAGS.zone_failures_to_offline - 1
|
||||
|
||||
self.mox.StubOutWithMock(zone_state, 'call_novaclient')
|
||||
zone_state.call_novaclient().AndRaise(Exception('foo'))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zone_state.poll()
|
||||
self.mox.VerifyAll()
|
||||
self.assertEquals(zone_state.attempt, 3)
|
||||
self.assertFalse(zone_state.is_active)
|
@ -1,377 +0,0 @@
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# 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 ZoneManager
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import mox
|
||||
|
||||
from nova import db
|
||||
from nova import flags
|
||||
from nova import test
|
||||
from nova import utils
|
||||
from nova.scheduler import zone_manager
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
class FakeZone:
|
||||
"""Represents a fake zone from the db"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
def exploding_novaclient(zone):
|
||||
"""Used when we want to simulate a novaclient call failing."""
|
||||
raise Exception("kaboom")
|
||||
|
||||
|
||||
class ZoneManagerTestCase(test.TestCase):
|
||||
"""Test case for zone manager"""
|
||||
def test_ping(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
self.mox.StubOutWithMock(zm, '_refresh_from_db')
|
||||
self.mox.StubOutWithMock(zm, '_poll_zones')
|
||||
zm._refresh_from_db(mox.IgnoreArg())
|
||||
zm._poll_zones(mox.IgnoreArg())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm.ping(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_refresh_from_db_new(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
|
||||
self.mox.StubOutWithMock(db, 'zone_get_all')
|
||||
db.zone_get_all(mox.IgnoreArg()).AndReturn([
|
||||
FakeZone(id=1, api_url='http://foo.com', username='user1',
|
||||
password='pass1', name='child', weight_offset=0.0,
|
||||
weight_scale=1.0),
|
||||
])
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 0)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm._refresh_from_db(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
self.assertEquals(zm.zone_states[1].username, 'user1')
|
||||
|
||||
def test_service_capabilities(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, {})
|
||||
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2)))
|
||||
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=2, b=3))
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(2, 2), svc1_b=(3, 3)))
|
||||
|
||||
zm.update_service_capabilities("svc1", "host2", dict(a=20, b=30))
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30)))
|
||||
|
||||
zm.update_service_capabilities("svc10", "host1", dict(a=99, b=99))
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30),
|
||||
svc10_a=(99, 99), svc10_b=(99, 99)))
|
||||
|
||||
zm.update_service_capabilities("svc1", "host3", dict(c=5))
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30),
|
||||
svc1_c=(5, 5), svc10_a=(99, 99),
|
||||
svc10_b=(99, 99)))
|
||||
|
||||
def test_refresh_from_db_replace_existing(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
zone_state = zone_manager.ZoneState()
|
||||
zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com',
|
||||
username='user1', password='pass1', name='child',
|
||||
weight_offset=0.0, weight_scale=1.0))
|
||||
zm.zone_states[1] = zone_state
|
||||
|
||||
self.mox.StubOutWithMock(db, 'zone_get_all')
|
||||
db.zone_get_all(mox.IgnoreArg()).AndReturn([
|
||||
FakeZone(id=1, api_url='http://foo.com', username='user2',
|
||||
password='pass2', name='child',
|
||||
weight_offset=0.0, weight_scale=1.0),
|
||||
])
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm._refresh_from_db(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
self.assertEquals(zm.zone_states[1].username, 'user2')
|
||||
|
||||
def test_refresh_from_db_missing(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
zone_state = zone_manager.ZoneState()
|
||||
zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com',
|
||||
username='user1', password='pass1', name='child',
|
||||
weight_offset=0.0, weight_scale=1.0))
|
||||
zm.zone_states[1] = zone_state
|
||||
|
||||
self.mox.StubOutWithMock(db, 'zone_get_all')
|
||||
db.zone_get_all(mox.IgnoreArg()).AndReturn([])
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm._refresh_from_db(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 0)
|
||||
|
||||
def test_refresh_from_db_add_and_delete(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
zone_state = zone_manager.ZoneState()
|
||||
zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com',
|
||||
username='user1', password='pass1', name='child',
|
||||
weight_offset=2.0, weight_scale=3.0))
|
||||
zm.zone_states[1] = zone_state
|
||||
|
||||
self.mox.StubOutWithMock(db, 'zone_get_all')
|
||||
|
||||
db.zone_get_all(mox.IgnoreArg()).AndReturn([
|
||||
FakeZone(id=2, api_url='http://foo.com', username='user2',
|
||||
password='pass2', name='child', weight_offset=2.0,
|
||||
weight_scale=3.0),
|
||||
])
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zm._refresh_from_db(None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
self.assertEquals(len(zm.zone_states), 1)
|
||||
self.assertEquals(zm.zone_states[2].username, 'user2')
|
||||
|
||||
def test_poll_zone(self):
|
||||
self.mox.StubOutWithMock(zone_manager, '_call_novaclient')
|
||||
zone_manager._call_novaclient(mox.IgnoreArg()).AndReturn(
|
||||
dict(name='child', capabilities='hairdresser'))
|
||||
|
||||
zone_state = zone_manager.ZoneState()
|
||||
zone_state.update_credentials(FakeZone(id=2,
|
||||
api_url='http://foo.com', username='user2',
|
||||
password='pass2', name='child',
|
||||
weight_offset=0.0, weight_scale=1.0))
|
||||
zone_state.attempt = 1
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zone_manager._poll_zone(zone_state)
|
||||
self.mox.VerifyAll()
|
||||
self.assertEquals(zone_state.attempt, 0)
|
||||
self.assertEquals(zone_state.name, 'child')
|
||||
|
||||
def test_poll_zone_fails(self):
|
||||
self.stubs.Set(zone_manager, "_call_novaclient", exploding_novaclient)
|
||||
|
||||
zone_state = zone_manager.ZoneState()
|
||||
zone_state.update_credentials(FakeZone(id=2,
|
||||
api_url='http://foo.com', username='user2',
|
||||
password='pass2', name='child',
|
||||
weight_offset=0.0, weight_scale=1.0))
|
||||
zone_state.attempt = FLAGS.zone_failures_to_offline - 1
|
||||
|
||||
self.mox.ReplayAll()
|
||||
zone_manager._poll_zone(zone_state)
|
||||
self.mox.VerifyAll()
|
||||
self.assertEquals(zone_state.attempt, 3)
|
||||
self.assertFalse(zone_state.is_active)
|
||||
|
||||
def test_host_service_caps_stale_no_stale_service(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
|
||||
# services just updated capabilities
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
|
||||
self.assertFalse(zm.host_service_caps_stale("host1", "svc1"))
|
||||
self.assertFalse(zm.host_service_caps_stale("host1", "svc2"))
|
||||
|
||||
def test_host_service_caps_stale_all_stale_services(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
expiry_time = (FLAGS.periodic_interval * 3) + 1
|
||||
|
||||
# Both services became stale
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
|
||||
time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time)
|
||||
utils.set_time_override(time_future)
|
||||
self.assertTrue(zm.host_service_caps_stale("host1", "svc1"))
|
||||
self.assertTrue(zm.host_service_caps_stale("host1", "svc2"))
|
||||
utils.clear_time_override()
|
||||
|
||||
def test_host_service_caps_stale_one_stale_service(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
expiry_time = (FLAGS.periodic_interval * 3) + 1
|
||||
|
||||
# One service became stale
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
|
||||
caps = zm.service_states["host1"]["svc1"]
|
||||
caps["timestamp"] = utils.utcnow() - \
|
||||
datetime.timedelta(seconds=expiry_time)
|
||||
self.assertTrue(zm.host_service_caps_stale("host1", "svc1"))
|
||||
self.assertFalse(zm.host_service_caps_stale("host1", "svc2"))
|
||||
|
||||
def test_delete_expired_host_services_del_one_service(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
|
||||
# Delete one service in a host
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
|
||||
stale_host_services = {"host1": ["svc1"]}
|
||||
zm.delete_expired_host_services(stale_host_services)
|
||||
self.assertFalse("svc1" in zm.service_states["host1"])
|
||||
self.assertTrue("svc2" in zm.service_states["host1"])
|
||||
|
||||
def test_delete_expired_host_services_del_all_hosts(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
|
||||
# Delete all services in a host
|
||||
zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4))
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
stale_host_services = {"host1": ["svc1", "svc2"]}
|
||||
zm.delete_expired_host_services(stale_host_services)
|
||||
self.assertFalse("host1" in zm.service_states)
|
||||
|
||||
def test_delete_expired_host_services_del_one_service_per_host(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
|
||||
# Delete one service per host
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
|
||||
stale_host_services = {"host1": ["svc1"], "host2": ["svc1"]}
|
||||
zm.delete_expired_host_services(stale_host_services)
|
||||
self.assertFalse("host1" in zm.service_states)
|
||||
self.assertFalse("host2" in zm.service_states)
|
||||
|
||||
def test_get_zone_capabilities_one_host(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
|
||||
# Service capabilities recent
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2)))
|
||||
|
||||
def test_get_zone_capabilities_expired_host(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
expiry_time = (FLAGS.periodic_interval * 3) + 1
|
||||
|
||||
# Service capabilities stale
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time)
|
||||
utils.set_time_override(time_future)
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, {})
|
||||
utils.clear_time_override()
|
||||
|
||||
def test_get_zone_capabilities_multiple_hosts(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
|
||||
# Both host service capabilities recent
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(1, 3), svc1_b=(2, 4)))
|
||||
|
||||
def test_get_zone_capabilities_one_stale_host(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
expiry_time = (FLAGS.periodic_interval * 3) + 1
|
||||
|
||||
# One host service capabilities become stale
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
|
||||
serv_caps = zm.service_states["host1"]["svc1"]
|
||||
serv_caps["timestamp"] = utils.utcnow() - \
|
||||
datetime.timedelta(seconds=expiry_time)
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(3, 3), svc1_b=(4, 4)))
|
||||
|
||||
def test_get_zone_capabilities_multiple_service_per_host(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
|
||||
# Multiple services per host
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
|
||||
zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6))
|
||||
zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8))
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(1, 3), svc1_b=(2, 4),
|
||||
svc2_a=(5, 7), svc2_b=(6, 8)))
|
||||
|
||||
def test_get_zone_capabilities_one_stale_service_per_host(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
expiry_time = (FLAGS.periodic_interval * 3) + 1
|
||||
|
||||
# Two host services among four become stale
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
|
||||
zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6))
|
||||
zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8))
|
||||
serv_caps_1 = zm.service_states["host1"]["svc2"]
|
||||
serv_caps_1["timestamp"] = utils.utcnow() - \
|
||||
datetime.timedelta(seconds=expiry_time)
|
||||
serv_caps_2 = zm.service_states["host2"]["svc1"]
|
||||
serv_caps_2["timestamp"] = utils.utcnow() - \
|
||||
datetime.timedelta(seconds=expiry_time)
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2),
|
||||
svc2_a=(7, 7), svc2_b=(8, 8)))
|
||||
|
||||
def test_get_zone_capabilities_three_stale_host_services(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
expiry_time = (FLAGS.periodic_interval * 3) + 1
|
||||
|
||||
# Three host services among four become stale
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
|
||||
zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6))
|
||||
zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8))
|
||||
serv_caps_1 = zm.service_states["host1"]["svc2"]
|
||||
serv_caps_1["timestamp"] = utils.utcnow() - \
|
||||
datetime.timedelta(seconds=expiry_time)
|
||||
serv_caps_2 = zm.service_states["host2"]["svc1"]
|
||||
serv_caps_2["timestamp"] = utils.utcnow() - \
|
||||
datetime.timedelta(seconds=expiry_time)
|
||||
serv_caps_3 = zm.service_states["host2"]["svc2"]
|
||||
serv_caps_3["timestamp"] = utils.utcnow() - \
|
||||
datetime.timedelta(seconds=expiry_time)
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2)))
|
||||
|
||||
def test_get_zone_capabilities_all_stale_host_services(self):
|
||||
zm = zone_manager.ZoneManager()
|
||||
expiry_time = (FLAGS.periodic_interval * 3) + 1
|
||||
|
||||
# All the host services become stale
|
||||
zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
|
||||
zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4))
|
||||
zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6))
|
||||
zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8))
|
||||
time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time)
|
||||
utils.set_time_override(time_future)
|
||||
caps = zm.get_zone_capabilities(None)
|
||||
self.assertEquals(caps, {})
|
Loading…
Reference in New Issue
Block a user