diff --git a/nova/api/openstack/compute/contrib/zones.py b/nova/api/openstack/compute/contrib/zones.py index 28e6f0772004..b68f3f4f5c6f 100644 --- a/nova/api/openstack/compute/contrib/zones.py +++ b/nova/api/openstack/compute/contrib/zones.py @@ -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): diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py index e913e5191621..b05148651366 100644 --- a/nova/scheduler/api.py +++ b/nova/scheduler/api.py @@ -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): diff --git a/nova/scheduler/chance.py b/nova/scheduler/chance.py index be66eb3c610b..079c1cfc359e 100644 --- a/nova/scheduler/chance.py +++ b/nova/scheduler/chance.py @@ -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) diff --git a/nova/scheduler/distributed_scheduler.py b/nova/scheduler/distributed_scheduler.py index 754945fe539a..4e3c7c2774e5 100644 --- a/nova/scheduler/distributed_scheduler.py +++ b/nova/scheduler/distributed_scheduler.py @@ -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 diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index b3b598de99e9..46d7046da01d 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -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'] diff --git a/nova/scheduler/filters/__init__.py b/nova/scheduler/filters/__init__.py index b86fb795feff..f9bf6641ba02 100644 --- a/nova/scheduler/filters/__init__.py +++ b/nova/scheduler/filters/__init__.py @@ -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 diff --git a/nova/scheduler/filters/abstract_filter.py b/nova/scheduler/filters/abstract_filter.py index 2ce2350942b7..235eaa74bdbe 100644 --- a/nova/scheduler/filters/abstract_filter.py +++ b/nova/scheduler/filters/abstract_filter.py @@ -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.""" diff --git a/nova/scheduler/filters/all_hosts_filter.py b/nova/scheduler/filters/all_hosts_filter.py index 9e3bc5a39741..1099e425dc70 100644 --- a/nova/scheduler/filters/all_hosts_filter.py +++ b/nova/scheduler/filters/all_hosts_filter.py @@ -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 diff --git a/nova/scheduler/filters/instance_type_filter.py b/nova/scheduler/filters/compute_filter.py similarity index 53% rename from nova/scheduler/filters/instance_type_filter.py rename to nova/scheduler/filters/compute_filter.py index 1b490dd5ac0f..a31fffc6a272 100644 --- a/nova/scheduler/filters/instance_type_filter.py +++ b/nova/scheduler/filters/compute_filter.py @@ -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 diff --git a/nova/scheduler/filters/json_filter.py b/nova/scheduler/filters/json_filter.py index a64a5f5ed98a..53752df8cdf2 100644 --- a/nova/scheduler/filters/json_filter.py +++ b/nova/scheduler/filters/json_filter.py @@ -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 diff --git a/nova/scheduler/host_manager.py b/nova/scheduler/host_manager.py new file mode 100644 index 000000000000..04eff39b5bfd --- /dev/null +++ b/nova/scheduler/host_manager.py @@ -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 = {} # { : { : { 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 + _min and _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 = {} # { _ : (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 diff --git a/nova/scheduler/least_cost.py b/nova/scheduler/least_cost.py index bb9c78e80c48..c14e937474d3 100644 --- a/nova/scheduler/least_cost.py +++ b/nova/scheduler/least_cost.py @@ -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) diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index c74988cdab01..33e77cd0543d 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -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) diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index 23a58e5bc92f..fb800756dc6e 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -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) diff --git a/nova/scheduler/vsa.py b/nova/scheduler/vsa.py index e4433694b3c6..dd17b19c6c00 100644 --- a/nova/scheduler/vsa.py +++ b/nova/scheduler/vsa.py @@ -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): diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index 3835b6233c73..0e86a89c081d 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -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 = {} # { : ZoneState } - self.service_states = {} # { : { : { 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 - _min and _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 = {} # { _ : (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() diff --git a/nova/tests/scheduler/fake_zone_manager.py b/nova/tests/scheduler/fakes.py similarity index 51% rename from nova/tests/scheduler/fake_zone_manager.py rename to nova/tests/scheduler/fakes.py index c1991d9b0ff2..5fb60a206d82 100644 --- a/nova/tests/scheduler/fake_zone_manager.py +++ b/nova/tests/scheduler/fakes.py @@ -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) diff --git a/nova/tests/scheduler/test_distributed_scheduler.py b/nova/tests/scheduler/test_distributed_scheduler.py index 412c981c583e..05c5d18e1629 100644 --- a/nova/tests/scheduler/test_distributed_scheduler.py +++ b/nova/tests/scheduler/test_distributed_scheduler.py @@ -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') diff --git a/nova/tests/scheduler/test_host_filter.py b/nova/tests/scheduler/test_host_filter.py deleted file mode 100644 index 797ec3fe9540..000000000000 --- a/nova/tests/scheduler/test_host_filter.py +++ /dev/null @@ -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']]), {})) diff --git a/nova/tests/scheduler/test_host_filters.py b/nova/tests/scheduler/test_host_filters.py new file mode 100644 index 000000000000..40f86990226b --- /dev/null +++ b/nova/tests/scheduler/test_host_filters.py @@ -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)) diff --git a/nova/tests/scheduler/test_host_manager.py b/nova/tests/scheduler/test_host_manager.py new file mode 100644 index 000000000000..ed0fb3d63bba --- /dev/null +++ b/nova/tests/scheduler/test_host_manager.py @@ -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) diff --git a/nova/tests/scheduler/test_least_cost.py b/nova/tests/scheduler/test_least_cost.py index 65a4268d371b..6b72b026d04c 100644 --- a/nova/tests/scheduler/test_least_cost.py +++ b/nova/tests/scheduler/test_least_cost.py @@ -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') diff --git a/nova/tests/scheduler/test_zone_manager.py b/nova/tests/scheduler/test_zone_manager.py new file mode 100644 index 000000000000..364384c1c329 --- /dev/null +++ b/nova/tests/scheduler/test_zone_manager.py @@ -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) diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py deleted file mode 100644 index 703ff0bf9a96..000000000000 --- a/nova/tests/test_zones.py +++ /dev/null @@ -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, {})