Latest code and tests on solver scheduler, and some additional
design specification.
This commit is contained in:
parent
5d25dea794
commit
0ea9a5c657
3748
etc/nova/nova.conf.sample
Normal file
3748
etc/nova/nova.conf.sample
Normal file
File diff suppressed because it is too large
Load Diff
373
nova/scheduler/filter_scheduler.py
Normal file
373
nova/scheduler/filter_scheduler.py
Normal file
@ -0,0 +1,373 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# 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.
|
||||
|
||||
"""
|
||||
The FilterScheduler is for creating instances locally.
|
||||
You can customize this scheduler by specifying your own Host Filters and
|
||||
Weighing Functions.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from nova.compute import rpcapi as compute_rpcapi
|
||||
from nova import exception
|
||||
from nova.objects import instance_group as instance_group_obj
|
||||
from nova.openstack.common.gettextutils import _
|
||||
from nova.openstack.common import log as logging
|
||||
from nova.pci import pci_request
|
||||
from nova import rpc
|
||||
from nova.scheduler import driver
|
||||
from nova.scheduler import scheduler_options
|
||||
from nova.scheduler import utils as scheduler_utils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
filter_scheduler_opts = [
|
||||
cfg.IntOpt('scheduler_host_subset_size',
|
||||
default=1,
|
||||
help='New instances will be scheduled on a host chosen '
|
||||
'randomly from a subset of the N best hosts. This '
|
||||
'property defines the subset size that a host is '
|
||||
'chosen from. A value of 1 chooses the '
|
||||
'first host returned by the weighing functions. '
|
||||
'This value must be at least 1. Any value less than 1 '
|
||||
'will be ignored, and 1 will be used instead')
|
||||
]
|
||||
|
||||
CONF.register_opts(filter_scheduler_opts)
|
||||
|
||||
|
||||
class FilterScheduler(driver.Scheduler):
|
||||
"""Scheduler that can be used for filtering and weighing."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilterScheduler, self).__init__(*args, **kwargs)
|
||||
self.options = scheduler_options.SchedulerOptions()
|
||||
self.compute_rpcapi = compute_rpcapi.ComputeAPI()
|
||||
self.notifier = rpc.get_notifier('scheduler')
|
||||
|
||||
# NOTE(alaski): Remove this method when the scheduler rpc interface is
|
||||
# bumped to 4.x as it is no longer used.
|
||||
def schedule_run_instance(self, context, request_spec,
|
||||
admin_password, injected_files,
|
||||
requested_networks, is_first_time,
|
||||
filter_properties, legacy_bdm_in_spec):
|
||||
"""Provisions instances that needs to be scheduled
|
||||
|
||||
Applies filters and weighters on request properties to get a list of
|
||||
compute hosts and calls them to spawn instance(s).
|
||||
"""
|
||||
payload = dict(request_spec=request_spec)
|
||||
self.notifier.info(context, 'scheduler.run_instance.start', payload)
|
||||
|
||||
instance_uuids = request_spec.get('instance_uuids')
|
||||
LOG.info(_("Attempting to build %(num_instances)d instance(s) "
|
||||
"uuids: %(instance_uuids)s"),
|
||||
{'num_instances': len(instance_uuids),
|
||||
'instance_uuids': instance_uuids})
|
||||
LOG.debug("Request Spec: %s" % request_spec)
|
||||
|
||||
weighed_hosts = self._schedule(context, request_spec,
|
||||
filter_properties, instance_uuids)
|
||||
|
||||
# NOTE: Pop instance_uuids as individual creates do not need the
|
||||
# set of uuids. Do not pop before here as the upper exception
|
||||
# handler fo NoValidHost needs the uuid to set error state
|
||||
instance_uuids = request_spec.pop('instance_uuids')
|
||||
|
||||
# NOTE(comstud): Make sure we do not pass this through. It
|
||||
# contains an instance of RpcContext that cannot be serialized.
|
||||
filter_properties.pop('context', None)
|
||||
|
||||
for num, instance_uuid in enumerate(instance_uuids):
|
||||
request_spec['instance_properties']['launch_index'] = num
|
||||
|
||||
try:
|
||||
try:
|
||||
weighed_host = weighed_hosts.pop(0)
|
||||
LOG.info(_("Choosing host %(weighed_host)s "
|
||||
"for instance %(instance_uuid)s"),
|
||||
{'weighed_host': weighed_host,
|
||||
'instance_uuid': instance_uuid})
|
||||
except IndexError:
|
||||
raise exception.NoValidHost(reason="")
|
||||
|
||||
self._provision_resource(context, weighed_host,
|
||||
request_spec,
|
||||
filter_properties,
|
||||
requested_networks,
|
||||
injected_files, admin_password,
|
||||
is_first_time,
|
||||
instance_uuid=instance_uuid,
|
||||
legacy_bdm_in_spec=legacy_bdm_in_spec)
|
||||
except Exception as ex:
|
||||
# NOTE(vish): we don't reraise the exception here to make sure
|
||||
# that all instances in the request get set to
|
||||
# error properly
|
||||
driver.handle_schedule_error(context, ex, instance_uuid,
|
||||
request_spec)
|
||||
# scrub retry host list in case we're scheduling multiple
|
||||
# instances:
|
||||
retry = filter_properties.get('retry', {})
|
||||
retry['hosts'] = []
|
||||
|
||||
self.notifier.info(context, 'scheduler.run_instance.end', payload)
|
||||
|
||||
def select_destinations(self, context, request_spec, filter_properties):
|
||||
"""Selects a filtered set of hosts and nodes."""
|
||||
num_instances = request_spec['num_instances']
|
||||
instance_uuids = request_spec.get('instance_uuids')
|
||||
selected_hosts = self._schedule(context, request_spec,
|
||||
filter_properties, instance_uuids)
|
||||
|
||||
# Couldn't fulfill the request_spec
|
||||
if len(selected_hosts) < num_instances:
|
||||
raise exception.NoValidHost(reason='')
|
||||
|
||||
dests = [dict(host=host.obj.host, nodename=host.obj.nodename,
|
||||
limits=host.obj.limits) for host in selected_hosts]
|
||||
return dests
|
||||
|
||||
def _provision_resource(self, context, weighed_host, request_spec,
|
||||
filter_properties, requested_networks, injected_files,
|
||||
admin_password, is_first_time, instance_uuid=None,
|
||||
legacy_bdm_in_spec=True):
|
||||
"""Create the requested resource in this Zone."""
|
||||
# NOTE(vish): add our current instance back into the request spec
|
||||
request_spec['instance_uuids'] = [instance_uuid]
|
||||
payload = dict(request_spec=request_spec,
|
||||
weighted_host=weighed_host.to_dict(),
|
||||
instance_id=instance_uuid)
|
||||
self.notifier.info(context,
|
||||
'scheduler.run_instance.scheduled', payload)
|
||||
|
||||
# Update the metadata if necessary
|
||||
try:
|
||||
updated_instance = driver.instance_update_db(context,
|
||||
instance_uuid)
|
||||
except exception.InstanceNotFound:
|
||||
LOG.warning(_("Instance disappeared during scheduling"),
|
||||
context=context, instance_uuid=instance_uuid)
|
||||
|
||||
else:
|
||||
scheduler_utils.populate_filter_properties(filter_properties,
|
||||
weighed_host.obj)
|
||||
|
||||
self.compute_rpcapi.run_instance(context,
|
||||
instance=updated_instance,
|
||||
host=weighed_host.obj.host,
|
||||
request_spec=request_spec,
|
||||
filter_properties=filter_properties,
|
||||
requested_networks=requested_networks,
|
||||
injected_files=injected_files,
|
||||
admin_password=admin_password, is_first_time=is_first_time,
|
||||
node=weighed_host.obj.nodename,
|
||||
legacy_bdm_in_spec=legacy_bdm_in_spec)
|
||||
|
||||
def _get_configuration_options(self):
|
||||
"""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 overridden in a
|
||||
subclass to add more data.
|
||||
"""
|
||||
# Save useful information from the request spec for filter processing:
|
||||
project_id = request_spec['instance_properties']['project_id']
|
||||
os_type = request_spec['instance_properties']['os_type']
|
||||
filter_properties['project_id'] = project_id
|
||||
filter_properties['os_type'] = os_type
|
||||
pci_requests = pci_request.get_pci_requests_from_flavor(
|
||||
request_spec.get('instance_type') or {})
|
||||
if pci_requests:
|
||||
filter_properties['pci_requests'] = pci_requests
|
||||
|
||||
def _max_attempts(self):
|
||||
max_attempts = CONF.scheduler_max_attempts
|
||||
if max_attempts < 1:
|
||||
raise exception.NovaException(_("Invalid value for "
|
||||
"'scheduler_max_attempts', must be >= 1"))
|
||||
return max_attempts
|
||||
|
||||
def _log_compute_error(self, instance_uuid, retry):
|
||||
"""If the request contained an exception from a previous compute
|
||||
build/resize operation, log it to aid debugging
|
||||
"""
|
||||
exc = retry.pop('exc', None) # string-ified exception from compute
|
||||
if not exc:
|
||||
return # no exception info from a previous attempt, skip
|
||||
|
||||
hosts = retry.get('hosts', None)
|
||||
if not hosts:
|
||||
return # no previously attempted hosts, skip
|
||||
|
||||
last_host, last_node = hosts[-1]
|
||||
LOG.error(_('Error from last host: %(last_host)s (node %(last_node)s):'
|
||||
' %(exc)s'),
|
||||
{'last_host': last_host,
|
||||
'last_node': last_node,
|
||||
'exc': exc},
|
||||
instance_uuid=instance_uuid)
|
||||
|
||||
def _populate_retry(self, filter_properties, instance_properties):
|
||||
"""Populate filter properties with history of retries for this
|
||||
request. If maximum retries is exceeded, raise NoValidHost.
|
||||
"""
|
||||
max_attempts = self._max_attempts()
|
||||
force_hosts = filter_properties.get('force_hosts', [])
|
||||
force_nodes = filter_properties.get('force_nodes', [])
|
||||
|
||||
if max_attempts == 1 or force_hosts or force_nodes:
|
||||
# re-scheduling is disabled.
|
||||
return
|
||||
|
||||
retry = filter_properties.pop('retry', {})
|
||||
# retry is enabled, update attempt count:
|
||||
if retry:
|
||||
retry['num_attempts'] += 1
|
||||
else:
|
||||
retry = {
|
||||
'num_attempts': 1,
|
||||
'hosts': [] # list of compute hosts tried
|
||||
}
|
||||
filter_properties['retry'] = retry
|
||||
|
||||
instance_uuid = instance_properties.get('uuid')
|
||||
self._log_compute_error(instance_uuid, retry)
|
||||
|
||||
if retry['num_attempts'] > max_attempts:
|
||||
msg = (_('Exceeded max scheduling attempts %(max_attempts)d for '
|
||||
'instance %(instance_uuid)s')
|
||||
% {'max_attempts': max_attempts,
|
||||
'instance_uuid': instance_uuid})
|
||||
raise exception.NoValidHost(reason=msg)
|
||||
|
||||
@staticmethod
|
||||
def _setup_instance_group(context, filter_properties):
|
||||
update_group_hosts = False
|
||||
scheduler_hints = filter_properties.get('scheduler_hints') or {}
|
||||
group_hint = scheduler_hints.get('group', None)
|
||||
if group_hint:
|
||||
group = instance_group_obj.InstanceGroup.get_by_hint(context,
|
||||
group_hint)
|
||||
policies = set(('anti-affinity', 'affinity'))
|
||||
if any((policy in policies) for policy in group.policies):
|
||||
update_group_hosts = True
|
||||
filter_properties.setdefault('group_hosts', set())
|
||||
user_hosts = set(filter_properties['group_hosts'])
|
||||
group_hosts = set(group.get_hosts(context))
|
||||
filter_properties['group_hosts'] = user_hosts | group_hosts
|
||||
filter_properties['group_policies'] = group.policies
|
||||
return update_group_hosts
|
||||
|
||||
def _schedule(self, context, request_spec, filter_properties,
|
||||
instance_uuids=None):
|
||||
"""Returns a list of hosts that meet the required specs,
|
||||
ordered by their fitness.
|
||||
"""
|
||||
elevated = context.elevated()
|
||||
instance_properties = request_spec['instance_properties']
|
||||
instance_type = request_spec.get("instance_type", None)
|
||||
|
||||
update_group_hosts = self._setup_instance_group(context,
|
||||
filter_properties)
|
||||
|
||||
config_options = self._get_configuration_options()
|
||||
|
||||
# check retry policy. Rather ugly use of instance_uuids[0]...
|
||||
# but if we've exceeded max retries... then we really only
|
||||
# have a single instance.
|
||||
properties = instance_properties.copy()
|
||||
if instance_uuids:
|
||||
properties['uuid'] = instance_uuids[0]
|
||||
self._populate_retry(filter_properties, properties)
|
||||
|
||||
filter_properties.update({'context': context,
|
||||
'request_spec': request_spec,
|
||||
'config_options': config_options,
|
||||
'instance_type': instance_type})
|
||||
|
||||
self.populate_filter_properties(request_spec,
|
||||
filter_properties)
|
||||
|
||||
# Note: Moving the host selection logic to a new method so that
|
||||
# the subclasses can override the behavior.
|
||||
return self._get_final_host_list(elevated, request_spec,
|
||||
filter_properties,
|
||||
instance_properties,
|
||||
update_group_hosts,
|
||||
instance_uuids)
|
||||
|
||||
def _get_final_host_list(self, context, request_spec, filter_properties,
|
||||
instance_properties, update_group_hosts=False,
|
||||
instance_uuids=None):
|
||||
"""Returns the final list of filtered hosts and ordered by their
|
||||
fitness. Subclasses can override this method to change the mechanism
|
||||
of selecting the final host list.
|
||||
"""
|
||||
# Find our local list of acceptable hosts by repeatedly
|
||||
# filtering and weighing our options. Each time we choose a
|
||||
# host, we virtually consume resources on it so subsequent
|
||||
# selections can adjust accordingly.
|
||||
|
||||
# Note: remember, we are using an iterator here. So only
|
||||
# traverse this list once. This can bite you if the hosts
|
||||
# are being scanned in a filter or weighing function.
|
||||
hosts = self._get_all_host_states(context)
|
||||
|
||||
selected_hosts = []
|
||||
if instance_uuids:
|
||||
num_instances = len(instance_uuids)
|
||||
else:
|
||||
num_instances = request_spec.get('num_instances', 1)
|
||||
for num in xrange(num_instances):
|
||||
# Filter local hosts based on requirements ...
|
||||
hosts = self.host_manager.get_filtered_hosts(hosts,
|
||||
filter_properties, index=num)
|
||||
if not hosts:
|
||||
# Can't get any more locally.
|
||||
break
|
||||
|
||||
LOG.debug("Filtered %(hosts)s", {'hosts': hosts})
|
||||
|
||||
weighed_hosts = self.host_manager.get_weighed_hosts(hosts,
|
||||
filter_properties)
|
||||
|
||||
LOG.debug("Weighed %(hosts)s", {'hosts': weighed_hosts})
|
||||
|
||||
scheduler_host_subset_size = CONF.scheduler_host_subset_size
|
||||
if scheduler_host_subset_size > len(weighed_hosts):
|
||||
scheduler_host_subset_size = len(weighed_hosts)
|
||||
if scheduler_host_subset_size < 1:
|
||||
scheduler_host_subset_size = 1
|
||||
|
||||
chosen_host = random.choice(
|
||||
weighed_hosts[0:scheduler_host_subset_size])
|
||||
selected_hosts.append(chosen_host)
|
||||
|
||||
# Now consume the resources so the filter/weights
|
||||
# will change for the next instance.
|
||||
chosen_host.obj.consume_from_instance(instance_properties)
|
||||
if update_group_hosts is True:
|
||||
filter_properties['group_hosts'].add(chosen_host.obj.host)
|
||||
return selected_hosts
|
||||
|
||||
def _get_all_host_states(self, context):
|
||||
"""Template method, so a subclass can implement caching."""
|
||||
return self.host_manager.get_all_host_states(context)
|
@ -49,7 +49,9 @@ host_manager_opts = [
|
||||
'RamFilter',
|
||||
'ComputeFilter',
|
||||
'ComputeCapabilitiesFilter',
|
||||
'ImagePropertiesFilter'
|
||||
'ImagePropertiesFilter',
|
||||
'ServerGroupAntiAffinityFilter',
|
||||
'ServerGroupAffinityFilter',
|
||||
],
|
||||
help='Which filter class names to use for filtering hosts '
|
||||
'when not specified in the request.'),
|
||||
@ -114,6 +116,7 @@ class HostState(object):
|
||||
|
||||
# Mutable available resources.
|
||||
# These will change as resources are virtually "consumed".
|
||||
self.total_usable_ram_mb = 0
|
||||
self.total_usable_disk_gb = 0
|
||||
self.disk_mb_used = 0
|
||||
self.free_ram_mb = 0
|
||||
@ -183,9 +186,16 @@ class HostState(object):
|
||||
all_ram_mb = compute['memory_mb']
|
||||
|
||||
# Assume virtual size is all consumed by instances if use qcow2 disk.
|
||||
least = compute.get('disk_available_least')
|
||||
free_disk_mb = least if least is not None else compute['free_disk_gb']
|
||||
free_disk_mb *= 1024
|
||||
free_gb = compute['free_disk_gb']
|
||||
least_gb = compute.get('disk_available_least')
|
||||
if least_gb is not None:
|
||||
if least_gb > free_gb:
|
||||
# can occur when an instance in database is not on host
|
||||
LOG.warn(_("Host has more disk space than database expected"
|
||||
" (%(physical)sgb > %(database)sgb)") %
|
||||
{'physical': least_gb, 'database': free_gb})
|
||||
free_gb = min(least_gb, free_gb)
|
||||
free_disk_mb = free_gb * 1024
|
||||
|
||||
self.disk_mb_used = compute['local_gb_used'] * 1024
|
||||
|
||||
@ -215,8 +225,8 @@ class HostState(object):
|
||||
# Don't store stats directly in host_state to make sure these don't
|
||||
# overwrite any values, or get overwritten themselves. Store in self so
|
||||
# filters can schedule with them.
|
||||
self.stats = self._statmap(compute.get('stats', []))
|
||||
self.hypervisor_version = compute['hypervisor_version']
|
||||
stats = compute.get('stats', None) or '{}'
|
||||
self.stats = jsonutils.loads(stats)
|
||||
|
||||
# Track number of instances on host
|
||||
self.num_instances = int(self.stats.get('num_instances', 0))
|
||||
@ -295,17 +305,13 @@ class HostState(object):
|
||||
if pci_requests and self.pci_stats:
|
||||
self.pci_stats.apply_requests(pci_requests)
|
||||
|
||||
vm_state = instance.get('vm_state', vm_states.BUILDING)
|
||||
task_state = instance.get('task_state')
|
||||
if vm_state == vm_states.BUILDING or task_state in [
|
||||
task_states.RESIZE_MIGRATING, task_states.REBUILDING,
|
||||
task_states.RESIZE_PREP, task_states.IMAGE_SNAPSHOT,
|
||||
task_states.IMAGE_BACKUP]:
|
||||
task_states.IMAGE_BACKUP, task_states.UNSHELVING,
|
||||
task_states.RESCUING]:
|
||||
self.num_io_ops += 1
|
||||
|
||||
def _statmap(self, stats):
|
||||
return dict((st['key'], st['value']) for st in stats)
|
||||
|
||||
def __repr__(self):
|
||||
return ("(%s, %s) ram:%s disk:%s io_ops:%s instances:%s" %
|
||||
(self.host, self.nodename, self.free_ram_mb, self.free_disk_mb,
|
||||
@ -418,10 +424,6 @@ class HostManager(object):
|
||||
_match_forced_hosts(name_to_cls_map, force_hosts)
|
||||
if force_nodes:
|
||||
_match_forced_nodes(name_to_cls_map, force_nodes)
|
||||
if force_hosts or force_nodes:
|
||||
# NOTE(deva): Skip filters when forcing host or node
|
||||
if name_to_cls_map:
|
||||
return name_to_cls_map.values()
|
||||
hosts = name_to_cls_map.itervalues()
|
||||
|
||||
return hosts
|
||||
@ -429,9 +431,17 @@ class HostManager(object):
|
||||
def get_filtered_hosts(self, hosts, filter_properties,
|
||||
filter_class_names=None, index=0):
|
||||
"""Filter hosts and return only ones passing all filters."""
|
||||
#NOTE(Yathi): Calling the method to apply ignored and forced options
|
||||
# NOTE(Yathi): Calling the method to apply ignored and forced options
|
||||
hosts = self.get_hosts_stripping_ignored_and_forced(hosts,
|
||||
filter_properties)
|
||||
|
||||
force_hosts = filter_properties.get('force_hosts', [])
|
||||
force_nodes = filter_properties.get('force_nodes', [])
|
||||
|
||||
if force_hosts or force_nodes:
|
||||
# NOTE: Skip filters when forcing host or node
|
||||
return list(hosts)
|
||||
|
||||
filter_classes = self._choose_host_filters(filter_class_names)
|
||||
|
||||
return self.filter_handler.get_filtered_objects(filter_classes,
|
||||
@ -442,24 +452,6 @@ class HostManager(object):
|
||||
return self.weight_handler.get_weighed_objects(self.weight_classes,
|
||||
hosts, weight_properties)
|
||||
|
||||
def update_service_capabilities(self, service_name, host, capabilities):
|
||||
"""Update the per-service capabilities based on this notification."""
|
||||
|
||||
if service_name != 'compute':
|
||||
LOG.debug(_('Ignoring %(service_name)s service update '
|
||||
'from %(host)s'), {'service_name': service_name,
|
||||
'host': host})
|
||||
return
|
||||
|
||||
state_key = (host, capabilities.get('hypervisor_hostname'))
|
||||
LOG.debug(_("Received %(service_name)s service update from "
|
||||
"%(state_key)s."), {'service_name': service_name,
|
||||
'state_key': state_key})
|
||||
# Copy the capabilities, so we don't modify the original dict
|
||||
capab_copy = dict(capabilities)
|
||||
capab_copy["timestamp"] = timeutils.utcnow() # Reported time
|
||||
self.service_states[state_key] = capab_copy
|
||||
|
||||
def get_all_host_states(self, context):
|
||||
"""Returns a list of HostStates that represents all the hosts
|
||||
the HostManager knows about. Also, each of the consumable resources
|
||||
|
541
nova/tests/scheduler/test_host_manager.py
Normal file
541
nova/tests/scheduler/test_host_manager.py
Normal file
@ -0,0 +1,541 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# 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
|
||||
"""
|
||||
from nova.compute import task_states
|
||||
from nova.compute import vm_states
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova.openstack.common import jsonutils
|
||||
from nova.openstack.common import timeutils
|
||||
from nova.scheduler import filters
|
||||
from nova.scheduler import host_manager
|
||||
from nova import test
|
||||
from nova.tests.scheduler import fakes
|
||||
from nova import utils
|
||||
|
||||
|
||||
class FakeFilterClass1(filters.BaseHostFilter):
|
||||
def host_passes(self, host_state, filter_properties):
|
||||
pass
|
||||
|
||||
|
||||
class FakeFilterClass2(filters.BaseHostFilter):
|
||||
def host_passes(self, host_state, filter_properties):
|
||||
pass
|
||||
|
||||
|
||||
class HostManagerTestCase(test.NoDBTestCase):
|
||||
"""Test case for HostManager class."""
|
||||
|
||||
def setUp(self):
|
||||
super(HostManagerTestCase, self).setUp()
|
||||
self.host_manager = host_manager.HostManager()
|
||||
self.fake_hosts = [host_manager.HostState('fake_host%s' % x,
|
||||
'fake-node') for x in xrange(1, 5)]
|
||||
self.fake_hosts += [host_manager.HostState('fake_multihost',
|
||||
'fake-node%s' % x) for x in xrange(1, 5)]
|
||||
self.addCleanup(timeutils.clear_time_override)
|
||||
|
||||
def test_choose_host_filters_not_found(self):
|
||||
self.flags(scheduler_default_filters='FakeFilterClass3')
|
||||
self.host_manager.filter_classes = [FakeFilterClass1,
|
||||
FakeFilterClass2]
|
||||
self.assertRaises(exception.SchedulerHostFilterNotFound,
|
||||
self.host_manager._choose_host_filters, None)
|
||||
|
||||
def test_choose_host_filters(self):
|
||||
self.flags(scheduler_default_filters=['FakeFilterClass2'])
|
||||
self.host_manager.filter_classes = [FakeFilterClass1,
|
||||
FakeFilterClass2]
|
||||
|
||||
# Test we returns 1 correct function
|
||||
filter_classes = self.host_manager._choose_host_filters(None)
|
||||
self.assertEqual(len(filter_classes), 1)
|
||||
self.assertEqual(filter_classes[0].__name__, 'FakeFilterClass2')
|
||||
|
||||
def _mock_get_filtered_hosts(self, info, specified_filters=None):
|
||||
self.mox.StubOutWithMock(self.host_manager, '_choose_host_filters')
|
||||
|
||||
info['got_objs'] = []
|
||||
info['got_fprops'] = []
|
||||
|
||||
def fake_filter_one(_self, obj, filter_props):
|
||||
info['got_objs'].append(obj)
|
||||
info['got_fprops'].append(filter_props)
|
||||
return True
|
||||
|
||||
self.stubs.Set(FakeFilterClass1, '_filter_one', fake_filter_one)
|
||||
self.host_manager._choose_host_filters(specified_filters).AndReturn(
|
||||
[FakeFilterClass1])
|
||||
|
||||
def _verify_result(self, info, result, filters=True):
|
||||
for x in info['got_fprops']:
|
||||
self.assertEqual(x, info['expected_fprops'])
|
||||
if filters:
|
||||
self.assertEqual(set(info['expected_objs']), set(info['got_objs']))
|
||||
self.assertEqual(set(info['expected_objs']), set(result))
|
||||
|
||||
def test_get_filtered_hosts(self):
|
||||
fake_properties = {'moo': 1, 'cow': 2}
|
||||
|
||||
info = {'expected_objs': self.fake_hosts,
|
||||
'expected_fprops': fake_properties}
|
||||
|
||||
self._mock_get_filtered_hosts(info)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result)
|
||||
|
||||
def test_get_filtered_hosts_with_specified_filters(self):
|
||||
fake_properties = {'moo': 1, 'cow': 2}
|
||||
|
||||
specified_filters = ['FakeFilterClass1', 'FakeFilterClass2']
|
||||
info = {'expected_objs': self.fake_hosts,
|
||||
'expected_fprops': fake_properties}
|
||||
self._mock_get_filtered_hosts(info, specified_filters)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties, filter_class_names=specified_filters)
|
||||
self._verify_result(info, result)
|
||||
|
||||
def test_get_filtered_hosts_with_ignore(self):
|
||||
fake_properties = {'ignore_hosts': ['fake_host1', 'fake_host3',
|
||||
'fake_host5', 'fake_multihost']}
|
||||
|
||||
# [1] and [3] are host2 and host4
|
||||
info = {'expected_objs': [self.fake_hosts[1], self.fake_hosts[3]],
|
||||
'expected_fprops': fake_properties}
|
||||
self._mock_get_filtered_hosts(info)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result)
|
||||
|
||||
def test_get_filtered_hosts_with_force_hosts(self):
|
||||
fake_properties = {'force_hosts': ['fake_host1', 'fake_host3',
|
||||
'fake_host5']}
|
||||
|
||||
# [0] and [2] are host1 and host3
|
||||
info = {'expected_objs': [self.fake_hosts[0], self.fake_hosts[2]],
|
||||
'expected_fprops': fake_properties,
|
||||
'got_fprops': []}
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result, False)
|
||||
|
||||
def test_get_filtered_hosts_with_no_matching_force_hosts(self):
|
||||
fake_properties = {'force_hosts': ['fake_host5', 'fake_host6']}
|
||||
|
||||
info = {'expected_objs': [],
|
||||
'expected_fprops': fake_properties,
|
||||
'got_fprops': []}
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result, False)
|
||||
|
||||
def test_get_filtered_hosts_with_ignore_and_force_hosts(self):
|
||||
# Ensure ignore_hosts processed before force_hosts in host filters.
|
||||
fake_properties = {'force_hosts': ['fake_host3', 'fake_host1'],
|
||||
'ignore_hosts': ['fake_host1']}
|
||||
|
||||
# only fake_host3 should be left.
|
||||
info = {'expected_objs': [self.fake_hosts[2]],
|
||||
'expected_fprops': fake_properties,
|
||||
'got_fprops': []}
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result, False)
|
||||
|
||||
def test_get_filtered_hosts_with_force_host_and_many_nodes(self):
|
||||
# Ensure all nodes returned for a host with many nodes
|
||||
fake_properties = {'force_hosts': ['fake_multihost']}
|
||||
|
||||
info = {'expected_objs': [self.fake_hosts[4], self.fake_hosts[5],
|
||||
self.fake_hosts[6], self.fake_hosts[7]],
|
||||
'expected_fprops': fake_properties,
|
||||
'got_fprops': []}
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result, False)
|
||||
|
||||
def test_get_filtered_hosts_with_force_nodes(self):
|
||||
fake_properties = {'force_nodes': ['fake-node2', 'fake-node4',
|
||||
'fake-node9']}
|
||||
|
||||
# [5] is fake-node2, [7] is fake-node4
|
||||
info = {'expected_objs': [self.fake_hosts[5], self.fake_hosts[7]],
|
||||
'expected_fprops': fake_properties,
|
||||
'got_fprops': []}
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result, False)
|
||||
|
||||
def test_get_filtered_hosts_with_force_hosts_and_nodes(self):
|
||||
# Ensure only overlapping results if both force host and node
|
||||
fake_properties = {'force_hosts': ['fake_host1', 'fake_multihost'],
|
||||
'force_nodes': ['fake-node2', 'fake-node9']}
|
||||
|
||||
# [5] is fake-node2
|
||||
info = {'expected_objs': [self.fake_hosts[5]],
|
||||
'expected_fprops': fake_properties,
|
||||
'got_fprops': []}
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result, False)
|
||||
|
||||
def test_get_filtered_hosts_with_force_hosts_and_wrong_nodes(self):
|
||||
# Ensure non-overlapping force_node and force_host yield no result
|
||||
fake_properties = {'force_hosts': ['fake_multihost'],
|
||||
'force_nodes': ['fake-node']}
|
||||
|
||||
info = {'expected_objs': [],
|
||||
'expected_fprops': fake_properties,
|
||||
'got_fprops': []}
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result, False)
|
||||
|
||||
def test_get_filtered_hosts_with_ignore_hosts_and_force_nodes(self):
|
||||
# Ensure ignore_hosts can coexist with force_nodes
|
||||
fake_properties = {'force_nodes': ['fake-node4', 'fake-node2'],
|
||||
'ignore_hosts': ['fake_host1', 'fake_host2']}
|
||||
|
||||
info = {'expected_objs': [self.fake_hosts[5], self.fake_hosts[7]],
|
||||
'expected_fprops': fake_properties,
|
||||
'got_fprops': []}
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result, False)
|
||||
|
||||
def test_get_filtered_hosts_with_ignore_hosts_and_force_same_nodes(self):
|
||||
# Ensure ignore_hosts is processed before force_nodes
|
||||
fake_properties = {'force_nodes': ['fake_node4', 'fake_node2'],
|
||||
'ignore_hosts': ['fake_multihost']}
|
||||
|
||||
info = {'expected_objs': [],
|
||||
'expected_fprops': fake_properties,
|
||||
'got_fprops': []}
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
result = self.host_manager.get_filtered_hosts(self.fake_hosts,
|
||||
fake_properties)
|
||||
self._verify_result(info, result, False)
|
||||
|
||||
def test_get_all_host_states(self):
|
||||
|
||||
context = 'fake_context'
|
||||
|
||||
self.mox.StubOutWithMock(db, 'compute_node_get_all')
|
||||
self.mox.StubOutWithMock(host_manager.LOG, 'warn')
|
||||
|
||||
db.compute_node_get_all(context).AndReturn(fakes.COMPUTE_NODES)
|
||||
# node 3 host physical disk space is greater than database
|
||||
host_manager.LOG.warn("Host has more disk space than database expected"
|
||||
" (3333gb > 3072gb)")
|
||||
# Invalid service
|
||||
host_manager.LOG.warn("No service for compute ID 5")
|
||||
|
||||
self.mox.ReplayAll()
|
||||
self.host_manager.get_all_host_states(context)
|
||||
host_states_map = self.host_manager.host_state_map
|
||||
|
||||
self.assertEqual(len(host_states_map), 4)
|
||||
# Check that .service is set properly
|
||||
for i in xrange(4):
|
||||
compute_node = fakes.COMPUTE_NODES[i]
|
||||
host = compute_node['service']['host']
|
||||
node = compute_node['hypervisor_hostname']
|
||||
state_key = (host, node)
|
||||
self.assertEqual(host_states_map[state_key].service,
|
||||
compute_node['service'])
|
||||
self.assertEqual(host_states_map[('host1', 'node1')].free_ram_mb,
|
||||
512)
|
||||
# 511GB
|
||||
self.assertEqual(host_states_map[('host1', 'node1')].free_disk_mb,
|
||||
524288)
|
||||
self.assertEqual(host_states_map[('host2', 'node2')].free_ram_mb,
|
||||
1024)
|
||||
# 1023GB
|
||||
self.assertEqual(host_states_map[('host2', 'node2')].free_disk_mb,
|
||||
1048576)
|
||||
self.assertEqual(host_states_map[('host3', 'node3')].free_ram_mb,
|
||||
3072)
|
||||
# 3071GB
|
||||
self.assertEqual(host_states_map[('host3', 'node3')].free_disk_mb,
|
||||
3145728)
|
||||
self.assertEqual(host_states_map[('host4', 'node4')].free_ram_mb,
|
||||
8192)
|
||||
# 8191GB
|
||||
self.assertEqual(host_states_map[('host4', 'node4')].free_disk_mb,
|
||||
8388608)
|
||||
|
||||
|
||||
class HostManagerChangedNodesTestCase(test.NoDBTestCase):
|
||||
"""Test case for HostManager class."""
|
||||
|
||||
def setUp(self):
|
||||
super(HostManagerChangedNodesTestCase, self).setUp()
|
||||
self.host_manager = host_manager.HostManager()
|
||||
self.fake_hosts = [
|
||||
host_manager.HostState('host1', 'node1'),
|
||||
host_manager.HostState('host2', 'node2'),
|
||||
host_manager.HostState('host3', 'node3'),
|
||||
host_manager.HostState('host4', 'node4')
|
||||
]
|
||||
self.addCleanup(timeutils.clear_time_override)
|
||||
|
||||
def test_get_all_host_states(self):
|
||||
context = 'fake_context'
|
||||
|
||||
self.mox.StubOutWithMock(db, 'compute_node_get_all')
|
||||
db.compute_node_get_all(context).AndReturn(fakes.COMPUTE_NODES)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.host_manager.get_all_host_states(context)
|
||||
host_states_map = self.host_manager.host_state_map
|
||||
self.assertEqual(len(host_states_map), 4)
|
||||
|
||||
def test_get_all_host_states_after_delete_one(self):
|
||||
context = 'fake_context'
|
||||
|
||||
self.mox.StubOutWithMock(db, 'compute_node_get_all')
|
||||
# all nodes active for first call
|
||||
db.compute_node_get_all(context).AndReturn(fakes.COMPUTE_NODES)
|
||||
# remove node4 for second call
|
||||
running_nodes = [n for n in fakes.COMPUTE_NODES
|
||||
if n.get('hypervisor_hostname') != 'node4']
|
||||
db.compute_node_get_all(context).AndReturn(running_nodes)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.host_manager.get_all_host_states(context)
|
||||
self.host_manager.get_all_host_states(context)
|
||||
host_states_map = self.host_manager.host_state_map
|
||||
self.assertEqual(len(host_states_map), 3)
|
||||
|
||||
def test_get_all_host_states_after_delete_all(self):
|
||||
context = 'fake_context'
|
||||
|
||||
self.mox.StubOutWithMock(db, 'compute_node_get_all')
|
||||
# all nodes active for first call
|
||||
db.compute_node_get_all(context).AndReturn(fakes.COMPUTE_NODES)
|
||||
# remove all nodes for second call
|
||||
db.compute_node_get_all(context).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.host_manager.get_all_host_states(context)
|
||||
self.host_manager.get_all_host_states(context)
|
||||
host_states_map = self.host_manager.host_state_map
|
||||
self.assertEqual(len(host_states_map), 0)
|
||||
|
||||
|
||||
class HostStateTestCase(test.NoDBTestCase):
|
||||
"""Test case for HostState class."""
|
||||
|
||||
# update_from_compute_node() and consume_from_instance() are tested
|
||||
# in HostManagerTestCase.test_get_all_host_states()
|
||||
|
||||
def test_stat_consumption_from_compute_node(self):
|
||||
stats = {
|
||||
'num_instances': '5',
|
||||
'num_proj_12345': '3',
|
||||
'num_proj_23456': '1',
|
||||
'num_vm_%s' % vm_states.BUILDING: '2',
|
||||
'num_vm_%s' % vm_states.SUSPENDED: '1',
|
||||
'num_task_%s' % task_states.RESIZE_MIGRATING: '1',
|
||||
'num_task_%s' % task_states.MIGRATING: '2',
|
||||
'num_os_type_linux': '4',
|
||||
'num_os_type_windoze': '1',
|
||||
'io_workload': '42',
|
||||
}
|
||||
stats = jsonutils.dumps(stats)
|
||||
|
||||
hyper_ver_int = utils.convert_version_to_int('6.0.0')
|
||||
compute = dict(stats=stats, memory_mb=1, free_disk_gb=0, local_gb=0,
|
||||
local_gb_used=0, free_ram_mb=0, vcpus=0, vcpus_used=0,
|
||||
updated_at=None, host_ip='127.0.0.1',
|
||||
hypervisor_type='htype',
|
||||
hypervisor_hostname='hostname', cpu_info='cpu_info',
|
||||
supported_instances='{}',
|
||||
hypervisor_version=hyper_ver_int)
|
||||
|
||||
host = host_manager.HostState("fakehost", "fakenode")
|
||||
host.update_from_compute_node(compute)
|
||||
|
||||
self.assertEqual(5, host.num_instances)
|
||||
self.assertEqual(3, host.num_instances_by_project['12345'])
|
||||
self.assertEqual(1, host.num_instances_by_project['23456'])
|
||||
self.assertEqual(2, host.vm_states[vm_states.BUILDING])
|
||||
self.assertEqual(1, host.vm_states[vm_states.SUSPENDED])
|
||||
self.assertEqual(1, host.task_states[task_states.RESIZE_MIGRATING])
|
||||
self.assertEqual(2, host.task_states[task_states.MIGRATING])
|
||||
self.assertEqual(4, host.num_instances_by_os_type['linux'])
|
||||
self.assertEqual(1, host.num_instances_by_os_type['windoze'])
|
||||
self.assertEqual(42, host.num_io_ops)
|
||||
self.assertEqual(10, len(host.stats))
|
||||
|
||||
self.assertEqual('127.0.0.1', host.host_ip)
|
||||
self.assertEqual('htype', host.hypervisor_type)
|
||||
self.assertEqual('hostname', host.hypervisor_hostname)
|
||||
self.assertEqual('cpu_info', host.cpu_info)
|
||||
self.assertEqual({}, host.supported_instances)
|
||||
self.assertEqual(hyper_ver_int, host.hypervisor_version)
|
||||
|
||||
def test_stat_consumption_from_compute_node_non_pci(self):
|
||||
stats = {
|
||||
'num_instances': '5',
|
||||
'num_proj_12345': '3',
|
||||
'num_proj_23456': '1',
|
||||
'num_vm_%s' % vm_states.BUILDING: '2',
|
||||
'num_vm_%s' % vm_states.SUSPENDED: '1',
|
||||
'num_task_%s' % task_states.RESIZE_MIGRATING: '1',
|
||||
'num_task_%s' % task_states.MIGRATING: '2',
|
||||
'num_os_type_linux': '4',
|
||||
'num_os_type_windoze': '1',
|
||||
'io_workload': '42',
|
||||
}
|
||||
stats = jsonutils.dumps(stats)
|
||||
|
||||
hyper_ver_int = utils.convert_version_to_int('6.0.0')
|
||||
compute = dict(stats=stats, memory_mb=0, free_disk_gb=0, local_gb=0,
|
||||
local_gb_used=0, free_ram_mb=0, vcpus=0, vcpus_used=0,
|
||||
updated_at=None, host_ip='127.0.0.1',
|
||||
hypervisor_version=hyper_ver_int)
|
||||
|
||||
host = host_manager.HostState("fakehost", "fakenode")
|
||||
host.update_from_compute_node(compute)
|
||||
self.assertIsNone(host.pci_stats)
|
||||
self.assertEqual(hyper_ver_int, host.hypervisor_version)
|
||||
|
||||
def test_stat_consumption_from_compute_node_rescue_unshelving(self):
|
||||
stats = {
|
||||
'num_instances': '5',
|
||||
'num_proj_12345': '3',
|
||||
'num_proj_23456': '1',
|
||||
'num_vm_%s' % vm_states.BUILDING: '2',
|
||||
'num_vm_%s' % vm_states.SUSPENDED: '1',
|
||||
'num_task_%s' % task_states.UNSHELVING: '1',
|
||||
'num_task_%s' % task_states.RESCUING: '2',
|
||||
'num_os_type_linux': '4',
|
||||
'num_os_type_windoze': '1',
|
||||
'io_workload': '42',
|
||||
}
|
||||
stats = jsonutils.dumps(stats)
|
||||
|
||||
hyper_ver_int = utils.convert_version_to_int('6.0.0')
|
||||
compute = dict(stats=stats, memory_mb=0, free_disk_gb=0, local_gb=0,
|
||||
local_gb_used=0, free_ram_mb=0, vcpus=0, vcpus_used=0,
|
||||
updated_at=None, host_ip='127.0.0.1',
|
||||
hypervisor_version=hyper_ver_int)
|
||||
|
||||
host = host_manager.HostState("fakehost", "fakenode")
|
||||
host.update_from_compute_node(compute)
|
||||
|
||||
self.assertEqual(5, host.num_instances)
|
||||
self.assertEqual(3, host.num_instances_by_project['12345'])
|
||||
self.assertEqual(1, host.num_instances_by_project['23456'])
|
||||
self.assertEqual(2, host.vm_states[vm_states.BUILDING])
|
||||
self.assertEqual(1, host.vm_states[vm_states.SUSPENDED])
|
||||
self.assertEqual(1, host.task_states[task_states.UNSHELVING])
|
||||
self.assertEqual(2, host.task_states[task_states.RESCUING])
|
||||
self.assertEqual(4, host.num_instances_by_os_type['linux'])
|
||||
self.assertEqual(1, host.num_instances_by_os_type['windoze'])
|
||||
self.assertEqual(42, host.num_io_ops)
|
||||
self.assertEqual(10, len(host.stats))
|
||||
|
||||
self.assertIsNone(host.pci_stats)
|
||||
self.assertEqual(hyper_ver_int, host.hypervisor_version)
|
||||
|
||||
def test_stat_consumption_from_instance(self):
|
||||
host = host_manager.HostState("fakehost", "fakenode")
|
||||
|
||||
instance = dict(root_gb=0, ephemeral_gb=0, memory_mb=0, vcpus=0,
|
||||
project_id='12345', vm_state=vm_states.BUILDING,
|
||||
task_state=task_states.SCHEDULING, os_type='Linux')
|
||||
host.consume_from_instance(instance)
|
||||
|
||||
instance = dict(root_gb=0, ephemeral_gb=0, memory_mb=0, vcpus=0,
|
||||
project_id='12345', vm_state=vm_states.PAUSED,
|
||||
task_state=None, os_type='Linux')
|
||||
host.consume_from_instance(instance)
|
||||
|
||||
self.assertEqual(2, host.num_instances)
|
||||
self.assertEqual(2, host.num_instances_by_project['12345'])
|
||||
self.assertEqual(1, host.vm_states[vm_states.BUILDING])
|
||||
self.assertEqual(1, host.vm_states[vm_states.PAUSED])
|
||||
self.assertEqual(1, host.task_states[task_states.SCHEDULING])
|
||||
self.assertEqual(1, host.task_states[None])
|
||||
self.assertEqual(2, host.num_instances_by_os_type['Linux'])
|
||||
self.assertEqual(1, host.num_io_ops)
|
||||
|
||||
def test_resources_consumption_from_compute_node(self):
|
||||
metrics = [
|
||||
dict(name='res1',
|
||||
value=1.0,
|
||||
source='source1',
|
||||
timestamp=None),
|
||||
dict(name='res2',
|
||||
value="string2",
|
||||
source='source2',
|
||||
timestamp=None),
|
||||
]
|
||||
hyper_ver_int = utils.convert_version_to_int('6.0.0')
|
||||
compute = dict(metrics=jsonutils.dumps(metrics),
|
||||
memory_mb=0, free_disk_gb=0, local_gb=0,
|
||||
local_gb_used=0, free_ram_mb=0, vcpus=0, vcpus_used=0,
|
||||
updated_at=None, host_ip='127.0.0.1',
|
||||
hypervisor_version=hyper_ver_int)
|
||||
|
||||
host = host_manager.HostState("fakehost", "fakenode")
|
||||
host.update_from_compute_node(compute)
|
||||
|
||||
self.assertEqual(len(host.metrics), 2)
|
||||
self.assertEqual(set(['res1', 'res2']), set(host.metrics.keys()))
|
||||
self.assertEqual(1.0, host.metrics['res1'].value)
|
||||
self.assertEqual('source1', host.metrics['res1'].source)
|
||||
self.assertEqual('string2', host.metrics['res2'].value)
|
||||
self.assertEqual('source2', host.metrics['res2'].source)
|
36
requirements.txt
Normal file
36
requirements.txt
Normal file
@ -0,0 +1,36 @@
|
||||
pbr>=0.6,!=0.7,<1.0
|
||||
SQLAlchemy>=0.7.8,<=0.9.99
|
||||
anyjson>=0.3.3
|
||||
argparse
|
||||
boto>=2.12.0,!=2.13.0
|
||||
eventlet>=0.13.0
|
||||
Jinja2
|
||||
kombu>=2.4.8
|
||||
lxml>=2.3
|
||||
Routes>=1.12.3
|
||||
WebOb>=1.2.3
|
||||
greenlet>=0.3.2
|
||||
PasteDeploy>=1.5.0
|
||||
Paste
|
||||
sqlalchemy-migrate>=0.9.1
|
||||
netaddr>=0.7.6
|
||||
suds>=0.4
|
||||
paramiko>=1.13.0
|
||||
pyasn1
|
||||
Babel>=1.3
|
||||
iso8601>=0.1.9
|
||||
jsonschema>=2.0.0,<3.0.0
|
||||
python-cinderclient>=1.0.6
|
||||
python-neutronclient>=2.3.4,<3
|
||||
python-glanceclient>=0.9.0
|
||||
python-keystoneclient>=0.9.0
|
||||
six>=1.6.0
|
||||
stevedore>=0.14
|
||||
websockify>=0.5.1,<0.6
|
||||
wsgiref>=0.1.2
|
||||
oslo.config>=1.2.0
|
||||
oslo.rootwrap
|
||||
oslotest
|
||||
pycadf>=0.5.1
|
||||
oslo.messaging>=1.3.0
|
||||
coinor.pulp>=1.0.4
|
152
solver_scheduler.rst
Normal file
152
solver_scheduler.rst
Normal file
@ -0,0 +1,152 @@
|
||||
Solver Scheduler
|
||||
================
|
||||
|
||||
The **Solver Scheduler** provides an extensible mechanism for making smarter,
|
||||
complex constraints optimization based resource scheduling in Nova. This
|
||||
driver supports pluggable Solvers, that can leverage existing complex
|
||||
constraint solving frameworks, available in open source such as PULP_, CVXOPT_,
|
||||
`Google OR-TOOLS`_, etc. This Scheduler is currently supported to work with
|
||||
Compute Nodes in Nova.
|
||||
|
||||
.. _PULP: https://projects.coin-or.org/PuLP
|
||||
|
||||
.. _CVXOPT: http://cvxopt.org/
|
||||
|
||||
.. _`Google OR-TOOLS`: https://code.google.com/p/or-tools/
|
||||
|
||||
The Nova compute resource placement can be described as a problem of placing a
|
||||
set of VMs on a set of physical hosts, where each VM has a set of resource
|
||||
requirements that have to be satisfied by a host with available resource
|
||||
capacity. In addition to the constraints, we optimize the solution for some
|
||||
cost metrics, so that the net cost of placing all VMs to certain hosts is
|
||||
minimized.
|
||||
|
||||
A pluggable Solver used by the Solver Scheduler driver models the Nova compute
|
||||
placement request as a constraint optimization problem using a set of
|
||||
constraints derived from the placement request specification and a net cost
|
||||
value to optimize. A Solver implementation should model the constraints and
|
||||
costs and feed it to a constraint problem specification, which
|
||||
is eventually solved by using any external solvers such as the COIN-OR_ CLP_,
|
||||
CBC_, GLPK_, and so on.
|
||||
|
||||
.. _COIN-OR: http://en.wikipedia.org/wiki/COIN-OR
|
||||
|
||||
.. _CLP: http://en.wikipedia.org/wiki/COIN-OR#CLP
|
||||
|
||||
.. _CBC: http://en.wikipedia.org/wiki/COIN-OR#CBC
|
||||
|
||||
.. _GLPK: http://en.wikipedia.org/wiki/GNU_Linear_Programming_Kit
|
||||
|
||||
The Nova compute resource placement optimization problem when subject to a set
|
||||
of linear constraints, can be formulated and solved as a `linear programming`_
|
||||
problem. A **linear programming (LP)** problem involves maximizing or
|
||||
minimizing a linear function subject to linear constraints.
|
||||
|
||||
.. _linear programming: http://en.wikipedia.org/wiki/Linear_programming
|
||||
|
||||
Solvers
|
||||
-------
|
||||
|
||||
All Solver implementations will be in the module
|
||||
(:mod:`nova.scheduler.solvers`). A solver implementation should be a
|
||||
subclass of ``solvers.BaseHostSolver`` and they implement the ``host_solve``
|
||||
method. This method returns a list of host-instance tuples after solving
|
||||
the constraints optimization problem.
|
||||
|
||||
A Reference Solver Implementation
|
||||
---------------------------------
|
||||
|HostsPulpSolver| is a reference solver implementation that models the Nova
|
||||
scheduling problem as a linear programming (LP) problem using the PULP_
|
||||
modeling framework. This example implementation is a working solver that
|
||||
includes the required disk and memory as constraints, and uses the free ram
|
||||
as a cost metric to maximize (for spreading hosts), or minimize (for stacking)
|
||||
for the LP problem.
|
||||
|
||||
An example LP problem formulation is provided below to describe how this
|
||||
example solver models the problem in LP.
|
||||
|
||||
Consider there are 2 hosts `Host_1` and `Host_2`, with available resources
|
||||
described as a tuple (usable_disk_mb, usable_memory_mb, free_ram_mb):
|
||||
|
||||
* `Host_1`: (2048, 2048, 2048)
|
||||
|
||||
* `Host_2`: (4096, 1536, 1536)
|
||||
|
||||
There are two VM requests with the following disk and memory requirements:
|
||||
|
||||
* `VM_1`: (1024, 512)
|
||||
|
||||
* `VM_2`: (1024, 512)
|
||||
|
||||
To formulate this problem as a LP problem, we use the variables: `X11`, `X12`,
|
||||
`X21`, `X22`. Here, a variable `Xij` takes the value `1` if `VM_i` is placed on
|
||||
`Host_j`, `0` otherwise.
|
||||
|
||||
If the problem objective is to minimize the cost metric of free_ram_mb, the
|
||||
mathematical LP formulation of this example is as follows:
|
||||
|
||||
::
|
||||
|
||||
Minimize (2048*X11 + 2048*X21 + 1536*X12 + 1536*X22)
|
||||
|
||||
subject to constraints:
|
||||
|
||||
X11*1024 + X21*1024 <= 2048 (disk maximum supply for Host_1)
|
||||
X11*512 + X21*512 <= 2048 (memory maximum supply for Host_1)
|
||||
X12*1024 + X22*1024 <= 4096 (disk maximum supply for Host_2)
|
||||
X12*512 + X22*512 <= 1536 (memory maximum supply for Host_2)
|
||||
X11*1024 + X12*1024 >= 1024 (disk minimum demand for VM_1)
|
||||
X11*512 + X12*512 >= 512 (memory minimum demand for VM_1)
|
||||
X21*1024 + X22*1024 >= 1024 (disk minimum demand for VM_2)
|
||||
X21*512 + X22*512 >= 512 (memory minimum demand for VM_2)
|
||||
X11 + X12 == 1 (VM_1 can run in only 1 Host)
|
||||
X21 + X22 == 1 (VM_2 can run in only 1 Host)
|
||||
|
||||
`X11` = 0, `X12` = 1, `X21` = 0, and `X22` = 1 happens to be the optimal
|
||||
solution, implying, both `VM_1` and `VM_2` will be hosted in `Host_2`.
|
||||
|
||||
|HostsPulpSolver| models such LP problems using the PULP_ LP modeler written in
|
||||
Python. This problem is solved for an optimal solution using an external
|
||||
solver supported by PULP such as CLP_, CBC_, GLPK_, etc. By default, PULP_
|
||||
uses the CBC_ solver, which is packaged with the `coinor.pulp`_ distribution.
|
||||
|
||||
.. _`coinor.pulp`: https://pypi.python.org/pypi/coinor.pulp
|
||||
|
||||
Additional Solver implementations are planned in the roadmap, that support
|
||||
pluggable constraints and costs.
|
||||
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
To use Solver Scheduler, the nova.conf should contain the following settings
|
||||
under the ``[solver_scheduler]`` namespace:
|
||||
|
||||
`The Solver Scheduler driver to use (required):`
|
||||
|
||||
``scheduler_driver=nova.scheduler.solver_scheduler.ConstraintSolverScheduler``
|
||||
|
||||
`The Solver implementation to use:`
|
||||
|
||||
``scheduler_host_solver=nova.scheduler.solvers.hosts_pulp_solver.HostsPulpSolver``
|
||||
|
||||
When using the default provided Solver implementation |HostsPulpSolver|, the
|
||||
following default values of these settings can be modified:
|
||||
|
||||
These are under the ``[DEFAULT]`` namespace, as they are also being used by
|
||||
the Filter Scheduler as well.
|
||||
|
||||
`The ram weight multiplier. A negative value indicates stacking as opposed
|
||||
to spreading:`
|
||||
|
||||
``ram_weight_multiplier=1.0``
|
||||
|
||||
`Virtual disk to physical disk allocation ratio:`
|
||||
|
||||
``disk_allocation_ratio=1.0``
|
||||
|
||||
`Virtual ram to physical ram allocation ratio:`
|
||||
|
||||
``ram_allocation_ratio=1.5``
|
||||
|
||||
.. |HostsPulpSolver| replace:: :class:`HostsPulpSolver <nova.scheduler.solvers.hosts_pulp_solver.HostsPulpSolver>`
|
Loading…
Reference in New Issue
Block a user