From 80e196069fa94edb8981415f9b8d432bbf92888f Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Fri, 30 Sep 2011 15:42:38 +0000 Subject: Restructure host filtering to be easier to use. The original design for host filtering in the scheduler required the entire filtering process be contained in a single class; contrast this with the design for weighting the hosts, which allowed you to specify a list of functions that would apply various weighting factors to the hosts. This commit modifies the filtering process to resemble the way that the weighting process is designed. Filters can now be small, focused classes, and you specify which filters to apply by setting the 'FLAGS.default_host_filters' flag to a list of the filter classes that match your needs. This is a port of the code from Launchpad, where it was orphaned: https://code.launchpad.net/~ed-leafe/nova/scheduler-multifilter/+merge/72478 Change-Id: I5f3eff6f21409a9f0eddda3392e9ff9d03039ebe --- nova/scheduler/abstract_scheduler.py | 4 +- nova/scheduler/base_scheduler.py | 27 ++++++-- nova/scheduler/filters/abstract_filter.py | 7 +- nova/scheduler/filters/all_hosts_filter.py | 11 ++-- nova/scheduler/filters/instance_type_filter.py | 14 ++-- nova/scheduler/filters/json_filter.py | 20 +++--- nova/scheduler/host_filter.py | 41 ++++++++---- nova/scheduler/least_cost.py | 9 ++- nova/tests/scheduler/test_abstract_scheduler.py | 3 +- nova/tests/scheduler/test_host_filter.py | 74 ++++++++++++---------- nova/tests/scheduler/test_least_cost_scheduler.py | 40 ++---------- .../xenserver/xenapi/etc/xapi.d/plugins/xenhost | 1 + 12 files changed, 131 insertions(+), 120 deletions(-) diff --git a/nova/scheduler/abstract_scheduler.py b/nova/scheduler/abstract_scheduler.py index e8712e5df..725f0b342 100644 --- a/nova/scheduler/abstract_scheduler.py +++ b/nova/scheduler/abstract_scheduler.py @@ -271,7 +271,7 @@ class AbstractScheduler(driver.Scheduler): # weigh the selected hosts. # weighted_hosts = [{weight=weight, hostname=hostname, # capabilities=capabs}, ...] - weighted_hosts = self.weigh_hosts(topic, request_spec, filtered_hosts) + weighted_hosts = self.weigh_hosts(request_spec, filtered_hosts) # Next, tack on the host weights from the child zones json_spec = json.dumps(request_spec) all_zones = db.zone_get_all(context.elevated()) @@ -306,7 +306,7 @@ class AbstractScheduler(driver.Scheduler): return [(host, services) for host, services in host_list if basic_ram_filter(host, services, request_spec)] - def weigh_hosts(self, topic, request_spec, hosts): + def weigh_hosts(self, request_spec, hosts): """This version assigns a weight of 1 to all hosts, making selection of any host basically a random event. Override this method in your subclass to add logic to prefer one potential host over another. diff --git a/nova/scheduler/base_scheduler.py b/nova/scheduler/base_scheduler.py index 132b8a493..77617a190 100644 --- a/nova/scheduler/base_scheduler.py +++ b/nova/scheduler/base_scheduler.py @@ -38,9 +38,20 @@ class BaseScheduler(abstract_scheduler.AbstractScheduler): """ def filter_hosts(self, topic, request_spec, hosts=None): """Filter the full host list (from the ZoneManager)""" - filter_name = request_spec.get('filter', None) - # Make sure that the requested filter is legitimate. - selected_filter = host_filter.choose_host_filter(filter_name) + filters = request_spec.get('filter') + if filters is None: + # Not specified; use the default + filters = FLAGS.default_host_filters + if not isinstance(filters, (list, tuple)): + filters = [filters] + if hosts is None: + # Get the full list (only considering 'compute' services) + all_hosts = self.zone_manager.service_states.iteritems() + hosts = [(host, services["compute"]) + for host, services in all_hosts + if "compute" in services] + # Make sure that the requested filters are legitimate. + selected_filters = host_filter.choose_host_filters(filters) # TODO(sandy): We're only using InstanceType-based specs # currently. Later we'll need to snoop for more detailed @@ -48,11 +59,13 @@ class BaseScheduler(abstract_scheduler.AbstractScheduler): instance_type = request_spec.get("instance_type", None) if instance_type is None: # No way to select; return the specified hosts - return hosts or [] - name, query = selected_filter.instance_type_to_filter(instance_type) - return selected_filter.filter_hosts(self.zone_manager, query) + return hosts + for selected_filter in selected_filters: + query = selected_filter.instance_type_to_filter(instance_type) + hosts = selected_filter.filter_hosts(hosts, query) + return hosts - def weigh_hosts(self, topic, request_spec, hosts): + def weigh_hosts(self, request_spec, hosts): """Derived classes may override this to provide more sophisticated scheduling objectives """ diff --git a/nova/scheduler/filters/abstract_filter.py b/nova/scheduler/filters/abstract_filter.py index a1d00d562..5784f8791 100644 --- a/nova/scheduler/filters/abstract_filter.py +++ b/nova/scheduler/filters/abstract_filter.py @@ -15,11 +15,6 @@ import nova.scheduler -from nova import flags - -FLAGS = flags.FLAGS -flags.DEFINE_string('default_host_filter', 'AllHostsFilter', - 'Which filter to use for filtering hosts') class AbstractHostFilter(object): @@ -28,7 +23,7 @@ class AbstractHostFilter(object): """Convert instance_type into a filter for most common use-case.""" raise NotImplementedError() - def filter_hosts(self, zone_manager, query): + def filter_hosts(self, host_list, query): """Return a list of hosts that fulfill the filter.""" raise NotImplementedError() diff --git a/nova/scheduler/filters/all_hosts_filter.py b/nova/scheduler/filters/all_hosts_filter.py index e80d829ca..7e6d2573b 100644 --- a/nova/scheduler/filters/all_hosts_filter.py +++ b/nova/scheduler/filters/all_hosts_filter.py @@ -15,7 +15,7 @@ import nova.scheduler -from nova.scheduler.filters import abstract_filter +import abstract_filter class AllHostsFilter(abstract_filter.AbstractHostFilter): @@ -24,9 +24,8 @@ class AllHostsFilter(abstract_filter.AbstractHostFilter): """Return anything to prevent base-class from raising exception. """ - return (self._full_name(), instance_type) + return instance_type - def filter_hosts(self, zone_manager, query): - """Return a list of hosts from ZoneManager list.""" - return [(host, services) - for host, services in zone_manager.service_states.iteritems()] + def filter_hosts(self, host_list, query): + """Return the entire list of supplied hosts.""" + return list(host_list) diff --git a/nova/scheduler/filters/instance_type_filter.py b/nova/scheduler/filters/instance_type_filter.py index 62b9ee414..43f1cf8d1 100644 --- a/nova/scheduler/filters/instance_type_filter.py +++ b/nova/scheduler/filters/instance_type_filter.py @@ -22,7 +22,7 @@ class InstanceTypeFilter(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 (self._full_name(), instance_type) + return instance_type def _satisfies_extra_specs(self, capabilities, instance_type): """Check that the capabilities provided by the compute service @@ -40,14 +40,19 @@ class InstanceTypeFilter(abstract_filter.AbstractHostFilter): return False return True - def filter_hosts(self, zone_manager, query): + def filter_hosts(self, host_list, query): """Return a list of hosts that can create instance_type.""" instance_type = query selected_hosts = [] - for host, services in zone_manager.service_states.iteritems(): - capabilities = services.get('compute', {}) + for host, capabilities in host_list: + # In case the capabilities have not yet been extracted from + # the zone manager's services dict... + capabilities = capabilities.get("compute", capabilities) if not capabilities: continue + if not capabilities.get("enabled", True): + # Host is disabled + continue host_ram_mb = capabilities['host_memory_free'] disk_bytes = capabilities['disk_available'] spec_ram = instance_type['memory_mb'] @@ -70,6 +75,7 @@ class InstanceTypeFilter(abstract_filter.AbstractHostFilter): # 'host_other_config': {}, # 'host_ip_address': '192.168.1.109', # 'host_cpu_info': {}, +# 'enabled': True, # 'disk_available': 32954957824, # 'disk_total': 50394562560, # 'disk_used': 17439604736, diff --git a/nova/scheduler/filters/json_filter.py b/nova/scheduler/filters/json_filter.py index caf22f5d5..6e30fda4d 100644 --- a/nova/scheduler/filters/json_filter.py +++ b/nova/scheduler/filters/json_filter.py @@ -94,7 +94,7 @@ class JsonFilter(abstract_filter.AbstractHostFilter): query = ['and', ['>=', '$compute.host_memory_free', required_ram], ['>=', '$compute.disk_available', required_disk]] - return (self._full_name(), json.dumps(query)) + return json.dumps(query) def _parse_string(self, string, host, services): """Strings prefixed with $ are capability lookups in the @@ -112,7 +112,7 @@ class JsonFilter(abstract_filter.AbstractHostFilter): return None return services - def _process_filter(self, zone_manager, query, host, services): + def _process_filter(self, query, host, services): """Recursively parse the query structure.""" if not query: return True @@ -121,7 +121,7 @@ class JsonFilter(abstract_filter.AbstractHostFilter): cooked_args = [] for arg in query[1:]: if isinstance(arg, list): - arg = self._process_filter(zone_manager, arg, host, services) + arg = self._process_filter(arg, host, services) elif isinstance(arg, basestring): arg = self._parse_string(arg, host, services) if arg is not None: @@ -129,18 +129,22 @@ class JsonFilter(abstract_filter.AbstractHostFilter): result = method(self, cooked_args) return result - def filter_hosts(self, zone_manager, query): + def filter_hosts(self, host_list, query): """Return a list of hosts that can fulfill the requirements specified in the query. """ expanded = json.loads(query) filtered_hosts = [] - for host, services in zone_manager.service_states.iteritems(): - result = self._process_filter(zone_manager, expanded, host, - services) + for host, capabilities in host_list: + if not capabilities: + continue + if not capabilities.get("enabled", True): + # Host is disabled + continue + result = self._process_filter(expanded, host, capabilities) if isinstance(result, list): # If any succeeded, include the host result = any(result) if result: - filtered_hosts.append((host, services)) + filtered_hosts.append((host, capabilities)) return filtered_hosts diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 9f7d34ea7..cb77f1565 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -32,17 +32,16 @@ from nova import exception from nova import flags import nova.scheduler -# NOTE(Vek): Even though we don't use filters in here anywhere, we -# depend on default_host_filter being available in FLAGS, -# and that happens only when filters/abstract_filter.py is -# imported. from nova.scheduler import filters FLAGS = flags.FLAGS +flags.DEFINE_list('default_host_filters', ['AllHostsFilter'], + 'Which filters to use for filtering hosts when not specified ' + 'in the request.') -def _get_filters(): +def _get_filter_classes(): # Imported here to avoid circular imports from nova.scheduler import filters @@ -55,15 +54,29 @@ def _get_filters(): and get_itm(itm) is not filters.AbstractHostFilter] -def choose_host_filter(filter_name=None): - """Since the caller may specify which filter to use we need +def choose_host_filters(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 name against a predefined set + function checks the filter names against a predefined set of acceptable filters. """ - if not filter_name: - filter_name = FLAGS.default_host_filter - for filter_class in _get_filters(): - if filter_class.__name__ == filter_name: - return filter_class() - raise exception.SchedulerHostFilterNotFound(filter_name=filter_name) + if not filters: + filters = FLAGS.default_host_filters + if not isinstance(filters, (list, tuple)): + filters = [filters] + good_filters = [] + bad_filters = [] + filter_classes = _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 diff --git a/nova/scheduler/least_cost.py b/nova/scheduler/least_cost.py index 1a6ef990c..b35e31601 100644 --- a/nova/scheduler/least_cost.py +++ b/nova/scheduler/least_cost.py @@ -114,10 +114,13 @@ class LeastCostScheduler(base_scheduler.BaseScheduler): self.cost_fns_cache = {} super(LeastCostScheduler, self).__init__(*args, **kwargs) - def get_cost_fns(self, topic): + def get_cost_fns(self, topic=None): """Returns a list of tuples containing weights and cost functions to use for weighing hosts """ + if topic is None: + # Schedulers only support compute right now. + topic = "compute" if topic in self.cost_fns_cache: return self.cost_fns_cache[topic] cost_fns = [] @@ -151,11 +154,11 @@ class LeastCostScheduler(base_scheduler.BaseScheduler): self.cost_fns_cache[topic] = cost_fns return cost_fns - def weigh_hosts(self, topic, request_spec, hosts): + def weigh_hosts(self, request_spec, hosts): """Returns a list of dictionaries of form: [ {weight: weight, hostname: hostname, capabilities: capabs} ] """ - cost_fns = self.get_cost_fns(topic) + cost_fns = self.get_cost_fns() costs = weighted_sum(domain=hosts, weighted_fns=cost_fns) weighted = [] diff --git a/nova/tests/scheduler/test_abstract_scheduler.py b/nova/tests/scheduler/test_abstract_scheduler.py index da25f1544..08b0b9cde 100644 --- a/nova/tests/scheduler/test_abstract_scheduler.py +++ b/nova/tests/scheduler/test_abstract_scheduler.py @@ -455,8 +455,7 @@ class BaseSchedulerTestCase(test.TestCase): # Call weigh_hosts() num_instances = len(hostlist) * 2 + len(hostlist) / 2 - instlist = sched.weigh_hosts('compute', - dict(num_instances=num_instances), + instlist = sched.weigh_hosts(dict(num_instances=num_instances), hostlist) # Should be enough entries to cover all instances diff --git a/nova/tests/scheduler/test_host_filter.py b/nova/tests/scheduler/test_host_filter.py index 17431fc7e..a21f4c380 100644 --- a/nova/tests/scheduler/test_host_filter.py +++ b/nova/tests/scheduler/test_host_filter.py @@ -18,10 +18,10 @@ Tests For Scheduler Host Filters. import json +import nova from nova import exception from nova import test from nova.scheduler import host_filter -from nova.scheduler import filters class FakeZoneManager: @@ -52,12 +52,13 @@ class HostFilterTestCase(test.TestCase): 'disk_total': 1000, 'disk_used': 0, 'host_uuid': 'xxx-%d' % multiplier, - 'host_name-label': 'xs-%s' % multiplier} + 'host_name-label': 'xs-%s' % multiplier, + 'enabled': True} def setUp(self): super(HostFilterTestCase, self).setUp() - default_host_filter = 'AllHostsFilter' - self.flags(default_host_filter=default_host_filter) + default_host_filters = ['AllHostsFilter'] + self.flags(default_host_filters=default_host_filters) self.instance_type = dict(name='tiny', memory_mb=50, vcpus=10, @@ -96,34 +97,41 @@ class HostFilterTestCase(test.TestCase): host09['xpu_arch'] = 'fermi' host09['xpu_info'] = 'Tesla 2150' + def _get_all_hosts(self): + return self.zone_manager.service_states.items() + def test_choose_filter(self): # Test default filter ... - hf = host_filter.choose_host_filter() + hfs = host_filter.choose_host_filters() + hf = hfs[0] self.assertEquals(hf._full_name().split(".")[-1], 'AllHostsFilter') # Test valid filter ... - hf = host_filter.choose_host_filter('InstanceTypeFilter') + hfs = host_filter.choose_host_filters('InstanceTypeFilter') + hf = hfs[0] self.assertEquals(hf._full_name().split(".")[-1], 'InstanceTypeFilter') # Test invalid filter ... try: - host_filter.choose_host_filter('does not exist') + host_filter.choose_host_filters('does not exist') self.fail("Should not find host filter.") except exception.SchedulerHostFilterNotFound: pass def test_all_host_filter(self): - hf = filters.AllHostsFilter() + hfs = host_filter.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(self.zone_manager, cooked) + hosts = hf.filter_hosts(all_hosts, cooked) self.assertEquals(10, len(hosts)) for host, capabilities in hosts: self.assertTrue(host.startswith('host')) def test_instance_type_filter(self): - hf = filters.InstanceTypeFilter() + hf = nova.scheduler.filters.InstanceTypeFilter() # filter all hosts that can support 50 ram and 500 disk - name, cooked = hf.instance_type_to_filter(self.instance_type) - self.assertEquals(name.split(".")[-1], 'InstanceTypeFilter') - hosts = hf.filter_hosts(self.zone_manager, cooked) + cooked = hf.instance_type_to_filter(self.instance_type) + all_hosts = self._get_all_hosts() + hosts = hf.filter_hosts(all_hosts, cooked) self.assertEquals(6, len(hosts)) just_hosts = [host for host, caps in hosts] just_hosts.sort() @@ -131,21 +139,21 @@ class HostFilterTestCase(test.TestCase): self.assertEquals('host10', just_hosts[5]) def test_instance_type_filter_extra_specs(self): - hf = filters.InstanceTypeFilter() + hf = nova.scheduler.filters.InstanceTypeFilter() # filter all hosts that can support 50 ram and 500 disk - name, cooked = hf.instance_type_to_filter(self.gpu_instance_type) - self.assertEquals(name.split(".")[-1], 'InstanceTypeFilter') - hosts = hf.filter_hosts(self.zone_manager, cooked) + 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('host07', just_hosts[0]) def test_json_filter(self): - hf = filters.JsonFilter() + hf = nova.scheduler.filters.JsonFilter() # filter all hosts that can support 50 ram and 500 disk - name, cooked = hf.instance_type_to_filter(self.instance_type) - self.assertEquals(name.split(".")[-1], 'JsonFilter') - hosts = hf.filter_hosts(self.zone_manager, cooked) + cooked = hf.instance_type_to_filter(self.instance_type) + all_hosts = self._get_all_hosts() + hosts = hf.filter_hosts(all_hosts, cooked) self.assertEquals(6, len(hosts)) just_hosts = [host for host, caps in hosts] just_hosts.sort() @@ -165,7 +173,7 @@ class HostFilterTestCase(test.TestCase): ] ] cooked = json.dumps(raw) - hosts = hf.filter_hosts(self.zone_manager, cooked) + hosts = hf.filter_hosts(all_hosts, cooked) self.assertEquals(5, len(hosts)) just_hosts = [host for host, caps in hosts] @@ -177,7 +185,7 @@ class HostFilterTestCase(test.TestCase): ['=', '$compute.host_memory_free', 30], ] cooked = json.dumps(raw) - hosts = hf.filter_hosts(self.zone_manager, cooked) + hosts = hf.filter_hosts(all_hosts, cooked) self.assertEquals(9, len(hosts)) just_hosts = [host for host, caps in hosts] @@ -187,7 +195,7 @@ class HostFilterTestCase(test.TestCase): raw = ['in', '$compute.host_memory_free', 20, 40, 60, 80, 100] cooked = json.dumps(raw) - hosts = hf.filter_hosts(self.zone_manager, cooked) + hosts = hf.filter_hosts(all_hosts, cooked) self.assertEquals(5, len(hosts)) just_hosts = [host for host, caps in hosts] just_hosts.sort() @@ -198,32 +206,32 @@ class HostFilterTestCase(test.TestCase): raw = ['unknown command', ] cooked = json.dumps(raw) try: - hf.filter_hosts(self.zone_manager, cooked) + hf.filter_hosts(all_hosts, cooked) self.fail("Should give KeyError") except KeyError, e: pass - self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps([]))) - self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps({}))) - self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps( + 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(self.zone_manager, json.dumps( + 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(self.zone_manager, + self.assertFalse(hf.filter_hosts(all_hosts, json.dumps(['=', '$foo', 100]))) - self.assertFalse(hf.filter_hosts(self.zone_manager, + self.assertFalse(hf.filter_hosts(all_hosts, json.dumps(['=', '$.....', 100]))) - self.assertFalse(hf.filter_hosts(self.zone_manager, + self.assertFalse(hf.filter_hosts(all_hosts, json.dumps( ['>', ['and', ['or', ['not', ['<', ['>=', ['<=', ['in', ]]]]]]]]))) - self.assertFalse(hf.filter_hosts(self.zone_manager, + self.assertFalse(hf.filter_hosts(all_hosts, json.dumps(['=', {}, ['>', '$missing....foo']]))) diff --git a/nova/tests/scheduler/test_least_cost_scheduler.py b/nova/tests/scheduler/test_least_cost_scheduler.py index b8847a2bf..589308e38 100644 --- a/nova/tests/scheduler/test_least_cost_scheduler.py +++ b/nova/tests/scheduler/test_least_cost_scheduler.py @@ -82,7 +82,7 @@ class LeastCostSchedulerTestCase(test.TestCase): super(LeastCostSchedulerTestCase, self).tearDown() def assertWeights(self, expected, num, request_spec, hosts): - weighted = self.sched.weigh_hosts("compute", request_spec, hosts) + weighted = self.sched.weigh_hosts(request_spec, hosts) self.assertDictListMatch(weighted, expected, approx_equal=True) def test_no_hosts(self): @@ -97,50 +97,20 @@ class LeastCostSchedulerTestCase(test.TestCase): self.flags(least_cost_scheduler_cost_functions=[ 'nova.scheduler.least_cost.noop_cost_fn'], noop_cost_fn_weight=1) - num = 1 request_spec = {} hosts = self.sched.filter_hosts(num, request_spec) - - expected = [dict(weight=1, hostname=hostname) - for hostname, caps in hosts] + expected = [{"hostname": hostname, "weight": 1, "capabilities": caps} + for hostname, caps in hosts] self.assertWeights(expected, num, request_spec, hosts) def test_cost_fn_weights(self): self.flags(least_cost_scheduler_cost_functions=[ 'nova.scheduler.least_cost.noop_cost_fn'], noop_cost_fn_weight=2) - num = 1 request_spec = {} hosts = self.sched.filter_hosts(num, request_spec) - - expected = [dict(weight=2, hostname=hostname) - for hostname, caps in hosts] - self.assertWeights(expected, num, request_spec, hosts) - - def test_compute_fill_first_cost_fn(self): - self.flags(least_cost_scheduler_cost_functions=[ - 'nova.scheduler.least_cost.compute_fill_first_cost_fn'], - compute_fill_first_cost_fn_weight=1) - num = 1 - instance_type = {'memory_mb': 1024} - request_spec = {'instance_type': instance_type} - svc_states = self.sched.zone_manager.service_states.iteritems() - all_hosts = [(host, services["compute"]) - for host, services in svc_states - if "compute" in services] - hosts = self.sched.filter_hosts('compute', request_spec, all_hosts) - - expected = [] - for idx, (hostname, services) in enumerate(hosts): - caps = copy.deepcopy(services) - # Costs are normalized so over 10 hosts, each host with increasing - # free ram will cost 1/N more. Since the lowest cost host has some - # free ram, we add in the 1/N for the base_cost - weight = 0.1 + (0.1 * idx) - wtd_dict = dict(hostname=hostname, weight=weight, - capabilities=caps) - expected.append(wtd_dict) - + expected = [{"hostname": hostname, "weight": 2, "capabilities": caps} + for hostname, caps in hosts] self.assertWeights(expected, num, request_spec, hosts) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 36c61f78d..f02597afc 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -271,6 +271,7 @@ def cleanup(dct): # avv["major"] = safe_int(dct.get("API-version-major", "")) # avv["minor"] = safe_int(dct.get("API-version-minor", "")) + out["enabled"] = dct.get("enabled", True) out["host_uuid"] = dct.get("uuid", None) out["host_name-label"] = dct.get("name-label", "") out["host_name-description"] = dct.get("name-description", "") -- cgit