Merge "Refactor the scheduler to use placement service"
This commit is contained in:
commit
97d9024420
|
@ -15,6 +15,7 @@ from oslo_concurrency import processutils
|
|||
from oslo_context import context
|
||||
from oslo_log import log
|
||||
import oslo_messaging as messaging
|
||||
from oslo_service import periodic_task
|
||||
from oslo_service import service
|
||||
from oslo_service import wsgi
|
||||
from oslo_utils import importutils
|
||||
|
@ -53,10 +54,11 @@ class RPCService(service.Service):
|
|||
self.rpcserver.start()
|
||||
|
||||
self.manager.init_host()
|
||||
self.tg.add_dynamic_timer(
|
||||
self.manager.periodic_tasks,
|
||||
periodic_interval_max=CONF.periodic_interval,
|
||||
context=admin_context)
|
||||
if isinstance(self.manager, periodic_task.PeriodicTasks):
|
||||
self.tg.add_dynamic_timer(
|
||||
self.manager.periodic_tasks,
|
||||
periodic_interval_max=CONF.periodic_interval,
|
||||
context=admin_context)
|
||||
|
||||
LOG.info('Created RPC server for service %(service)s on host '
|
||||
'%(host)s.',
|
||||
|
|
|
@ -286,7 +286,6 @@ class API(object):
|
|||
servers = self._provision_servers(context, base_options,
|
||||
min_count, max_count)
|
||||
request_spec = {
|
||||
'server_id': servers[0].uuid,
|
||||
'server_properties': {
|
||||
'flavor_uuid': servers[0].flavor_uuid,
|
||||
'networks': requested_networks,
|
||||
|
|
|
@ -24,7 +24,6 @@ from mogan.db import api as dbapi
|
|||
from mogan.engine.baremetal import driver
|
||||
from mogan.engine import rpcapi
|
||||
from mogan import network
|
||||
from mogan.scheduler import rpcapi as scheduler_rpcapi
|
||||
|
||||
|
||||
class BaseEngineManager(periodic_task.PeriodicTasks):
|
||||
|
@ -36,7 +35,6 @@ class BaseEngineManager(periodic_task.PeriodicTasks):
|
|||
self.host = host
|
||||
self.topic = topic
|
||||
self.network_api = network.API()
|
||||
self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
|
||||
self.driver = driver.load_engine_driver(CONF.engine.engine_driver)
|
||||
self.engine_rpcapi = rpcapi.EngineAPI()
|
||||
self._sync_power_pool = greenpool.GreenPool(
|
||||
|
|
|
@ -34,7 +34,7 @@ from mogan.common import utils
|
|||
from mogan.engine import configdrive
|
||||
from mogan.engine import metadata as server_metadata
|
||||
from mogan import objects
|
||||
|
||||
from mogan.scheduler import client as sched_client
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -64,6 +64,7 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
|
|||
exception.ServerDeployAborted,
|
||||
exception.NetworkError,
|
||||
]
|
||||
self.reportclient = sched_client.SchedulerClient().reportclient
|
||||
|
||||
def execute(self, **kwargs):
|
||||
pass
|
||||
|
@ -137,15 +138,16 @@ class BuildNetworkTask(flow_utils.MoganTask):
|
|||
"""Build network for the server."""
|
||||
|
||||
def __init__(self, manager):
|
||||
requires = ['server', 'requested_networks', 'ports', 'context']
|
||||
requires = ['server', 'requested_networks', 'context']
|
||||
super(BuildNetworkTask, self).__init__(addons=[ACTION],
|
||||
requires=requires)
|
||||
self.manager = manager
|
||||
|
||||
def _build_networks(self, context, server, requested_networks, ports):
|
||||
def _build_networks(self, context, server, requested_networks):
|
||||
|
||||
# TODO(zhenguo): This seems not needed as our scheduler has already
|
||||
# guaranteed this.
|
||||
ports = self.manager.driver.get_ports_from_node(server.node_uuid)
|
||||
if len(requested_networks) > len(ports):
|
||||
raise exception.InterfacePlugException(_(
|
||||
"Ironic node: %(id)s virtual to physical interface count"
|
||||
|
@ -196,12 +198,11 @@ class BuildNetworkTask(flow_utils.MoganTask):
|
|||
|
||||
return nics_obj
|
||||
|
||||
def execute(self, context, server, requested_networks, ports):
|
||||
def execute(self, context, server, requested_networks):
|
||||
server_nics = self._build_networks(
|
||||
context,
|
||||
server,
|
||||
requested_networks,
|
||||
ports)
|
||||
requested_networks)
|
||||
|
||||
server.nics = server_nics
|
||||
server.save()
|
||||
|
@ -298,7 +299,7 @@ class CreateServerTask(flow_utils.MoganTask):
|
|||
|
||||
|
||||
def get_flow(context, manager, server, requested_networks, user_data,
|
||||
injected_files, key_pair, ports, request_spec,
|
||||
injected_files, key_pair, request_spec,
|
||||
filter_properties):
|
||||
|
||||
"""Constructs and returns the manager entrypoint flow
|
||||
|
@ -326,7 +327,6 @@ def get_flow(context, manager, server, requested_networks, user_data,
|
|||
'user_data': user_data,
|
||||
'injected_files': injected_files,
|
||||
'key_pair': key_pair,
|
||||
'ports': ports,
|
||||
'configdrive': {}
|
||||
}
|
||||
|
||||
|
|
|
@ -195,6 +195,9 @@ class EngineManager(base_manager.BaseEngineManager):
|
|||
self.scheduler_client.set_inventory_for_provider(
|
||||
node.uuid, node.name, inventory_data,
|
||||
resource_class)
|
||||
if node.provision_state == 'available':
|
||||
self.scheduler_client.reportclient \
|
||||
.delete_allocations_for_resource_provider(node.uuid)
|
||||
|
||||
@periodic_task.periodic_task(spacing=CONF.engine.sync_power_state_interval,
|
||||
run_immediately=True)
|
||||
|
@ -392,9 +395,10 @@ class EngineManager(base_manager.BaseEngineManager):
|
|||
}
|
||||
filter_properties['retry'] = retry
|
||||
request_spec['num_servers'] = len(servers)
|
||||
request_spec['server_ids'] = [s.uuid for s in servers]
|
||||
|
||||
try:
|
||||
nodes = self.scheduler_rpcapi.select_destinations(
|
||||
nodes = self.scheduler_client.select_destinations(
|
||||
context, request_spec, filter_properties)
|
||||
except exception.NoValidNode as e:
|
||||
# Here should reset the state of building servers to Error
|
||||
|
@ -413,11 +417,11 @@ class EngineManager(base_manager.BaseEngineManager):
|
|||
{"nodes": nodes})
|
||||
|
||||
for (server, node) in six.moves.zip(servers, nodes):
|
||||
server.node_uuid = node['node_uuid']
|
||||
server.node_uuid = node
|
||||
server.save()
|
||||
# Add a retry entry for the selected node
|
||||
retry_nodes = retry['nodes']
|
||||
retry_nodes.append(node['node_uuid'])
|
||||
retry_nodes.append(node)
|
||||
|
||||
for server in servers:
|
||||
utils.spawn_n(self._create_server,
|
||||
|
@ -444,7 +448,6 @@ class EngineManager(base_manager.BaseEngineManager):
|
|||
target_state=states.ACTIVE)
|
||||
|
||||
try:
|
||||
node = objects.ComputeNode.get(context, server.node_uuid)
|
||||
flow_engine = create_server.get_flow(
|
||||
context,
|
||||
self,
|
||||
|
@ -453,7 +456,6 @@ class EngineManager(base_manager.BaseEngineManager):
|
|||
user_data,
|
||||
injected_files,
|
||||
key_pair,
|
||||
node['ports'],
|
||||
request_spec,
|
||||
filter_properties,
|
||||
)
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
# Copyright (c) 2011-2012 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.
|
||||
|
||||
"""
|
||||
Filter support
|
||||
"""
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
|
||||
from mogan.scheduler import base_handler
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseFilter(object):
|
||||
"""Base class for all filter classes."""
|
||||
def _filter_one(self, obj, filter_properties):
|
||||
"""Return True if it passes the filter, False otherwise.
|
||||
|
||||
Override this in a subclass.
|
||||
"""
|
||||
return True
|
||||
|
||||
def filter_all(self, filter_obj_list, filter_properties):
|
||||
"""Yield objects that pass the filter.
|
||||
|
||||
Can be overridden in a subclass, if you need to base filtering
|
||||
decisions on all objects. Otherwise, one can just override
|
||||
_filter_one() to filter a single object.
|
||||
"""
|
||||
for obj in filter_obj_list:
|
||||
if self._filter_one(obj, filter_properties):
|
||||
yield obj
|
||||
|
||||
# Set to true in a subclass if a filter only needs to be run once
|
||||
# for each request rather than for each server
|
||||
run_filter_once_per_request = False
|
||||
|
||||
def run_filter_for_index(self, index):
|
||||
"""Return True if the filter needs to be run for n-th servers.
|
||||
|
||||
Only need to override this if a filter needs anything other than
|
||||
"first only" or "all" behaviour.
|
||||
"""
|
||||
return not (self.run_filter_once_per_request and index > 0)
|
||||
|
||||
|
||||
class BaseFilterHandler(base_handler.BaseHandler):
|
||||
"""Base class to handle loading filter classes.
|
||||
|
||||
This class should be subclassed where one needs to use filters.
|
||||
"""
|
||||
|
||||
def _log_filtration(self, full_filter_results,
|
||||
part_filter_results, filter_properties):
|
||||
# Log the filtration history
|
||||
rspec = filter_properties.get("request_spec", {})
|
||||
msg_dict = {"server_id": rspec.get("server_id", ""),
|
||||
"str_results": six.text_type(full_filter_results),
|
||||
}
|
||||
full_msg = ("Filtering removed all nodes for the request with "
|
||||
"server ID "
|
||||
"'%(server_id)s'. Filter results: %(str_results)s"
|
||||
) % msg_dict
|
||||
msg_dict["str_results"] = ', '.join(
|
||||
("%(cls_name)s: (start: %(start)s, end: %(end)s)") %
|
||||
{"cls_name": value[0], "start": value[1], "end": value[2]}
|
||||
for value in part_filter_results)
|
||||
part_msg = ("Filtering removed all nodes for the request with "
|
||||
"server ID '%(server_id)s'. "
|
||||
"Filter results: %(str_results)s") % msg_dict
|
||||
LOG.debug(full_msg)
|
||||
LOG.info(part_msg)
|
||||
|
||||
def get_filtered_objects(self, filter_classes, objs,
|
||||
filter_properties, index=0):
|
||||
"""Get objects after filter
|
||||
|
||||
:param filter_classes: filters that will be used to filter the
|
||||
objects
|
||||
:param objs: objects that will be filtered
|
||||
:param filter_properties: client filter properties
|
||||
:param index: This value needs to be increased in the caller
|
||||
function of get_filtered_objects when handling
|
||||
each resource.
|
||||
"""
|
||||
list_objs = list(objs)
|
||||
LOG.debug("Starting with %d node(s)", len(list_objs))
|
||||
# The 'part_filter_results' list just tracks the number of hosts
|
||||
# before and after the filter, unless the filter returns zero
|
||||
# hosts, in which it records the host/nodename for the last batch
|
||||
# that was removed. Since the full_filter_results can be very large,
|
||||
# it is only recorded if the LOG level is set to debug.
|
||||
part_filter_results = []
|
||||
full_filter_results = []
|
||||
for filter_cls in filter_classes:
|
||||
cls_name = filter_cls.__name__
|
||||
start_count = len(list_objs)
|
||||
filter_class = filter_cls()
|
||||
|
||||
if filter_class.run_filter_for_index(index):
|
||||
objs = filter_class.filter_all(list_objs, filter_properties)
|
||||
if objs is None:
|
||||
LOG.info("Filter %s returned 0 nodes", cls_name)
|
||||
full_filter_results.append((cls_name, None))
|
||||
list_objs = None
|
||||
break
|
||||
|
||||
list_objs = list(objs)
|
||||
end_count = len(list_objs)
|
||||
part_filter_results.append((cls_name, start_count, end_count))
|
||||
remaining = [getattr(obj, "node", obj)
|
||||
for obj in list_objs]
|
||||
full_filter_results.append((cls_name, remaining))
|
||||
|
||||
LOG.debug("Filter %(cls_name)s returned "
|
||||
"%(obj_len)d node(s)",
|
||||
{'cls_name': cls_name, 'obj_len': len(list_objs)})
|
||||
if not list_objs:
|
||||
self._log_filtration(full_filter_results,
|
||||
part_filter_results, filter_properties)
|
||||
return list_objs
|
|
@ -1,47 +0,0 @@
|
|||
# Copyright (c) 2011-2013 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.
|
||||
|
||||
"""
|
||||
A common base for handling extension classes.
|
||||
|
||||
Used by BaseFilterHandler and BaseWeightHandler
|
||||
"""
|
||||
|
||||
import inspect
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
|
||||
class BaseHandler(object):
|
||||
"""Base class to handle loading filter and weight classes."""
|
||||
def __init__(self, modifier_class_type, modifier_namespace):
|
||||
self.namespace = modifier_namespace
|
||||
self.modifier_class_type = modifier_class_type
|
||||
self.extension_manager = extension.ExtensionManager(modifier_namespace)
|
||||
|
||||
def _is_correct_class(self, cls):
|
||||
"""Return whether an object is a class of the correct type.
|
||||
|
||||
(or is not prefixed with an underscore)
|
||||
"""
|
||||
return (inspect.isclass(cls) and
|
||||
not cls.__name__.startswith('_') and
|
||||
issubclass(cls, self.modifier_class_type))
|
||||
|
||||
def get_all_classes(self):
|
||||
# We use a set, as some classes may have an entrypoint of their own,
|
||||
# and also be returned by a function such as 'all_filters' for example
|
||||
return [ext.plugin for ext in self.extension_manager if
|
||||
self._is_correct_class(ext.plugin)]
|
|
@ -1,145 +0,0 @@
|
|||
# Copyright (c) 2011-2012 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.
|
||||
|
||||
"""
|
||||
Pluggable Weighing support
|
||||
"""
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
from mogan.scheduler import base_handler
|
||||
|
||||
|
||||
def normalize(weight_list, minval=None, maxval=None):
|
||||
"""Normalize the values in a list between 0 and 1.0.
|
||||
|
||||
The normalization is made regarding the lower and upper values present in
|
||||
weight_list. If the minval and/or maxval parameters are set, these values
|
||||
will be used instead of the minimum and maximum from the list.
|
||||
|
||||
If all the values are equal, they are normalized to 0.
|
||||
"""
|
||||
|
||||
if not weight_list:
|
||||
return ()
|
||||
|
||||
if maxval is None:
|
||||
maxval = max(weight_list)
|
||||
|
||||
if minval is None:
|
||||
minval = min(weight_list)
|
||||
|
||||
maxval = float(maxval)
|
||||
minval = float(minval)
|
||||
|
||||
if minval == maxval:
|
||||
return [0] * len(weight_list)
|
||||
|
||||
range_ = maxval - minval
|
||||
return ((i - minval) / range_ for i in weight_list)
|
||||
|
||||
|
||||
class WeighedObject(object):
|
||||
"""Object with weight information."""
|
||||
def __init__(self, obj, weight):
|
||||
self.obj = obj
|
||||
self.weight = weight
|
||||
|
||||
def __repr__(self):
|
||||
return "<WeighedObject '%s': %s>" % (self.obj, self.weight)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseWeigher(object):
|
||||
"""Base class for pluggable weighers.
|
||||
|
||||
The attributes maxval and minval can be specified to set up the maximum
|
||||
and minimum values for the weighed objects. These values will then be
|
||||
taken into account in the normalization step, instead of taking the values
|
||||
from the calculated weights.
|
||||
"""
|
||||
|
||||
minval = None
|
||||
maxval = None
|
||||
|
||||
def weight_multiplier(self):
|
||||
"""How weighted this weigher should be.
|
||||
|
||||
Override this method in a subclass, so that the returned value is
|
||||
read from a configuration option to permit operators specify a
|
||||
multiplier for the weigher.
|
||||
"""
|
||||
return 1.0
|
||||
|
||||
@abc.abstractmethod
|
||||
def _weigh_object(self, obj, weight_properties):
|
||||
"""Override in a subclass to specify a weight for a specific object."""
|
||||
|
||||
def weigh_objects(self, weighed_obj_list, weight_properties):
|
||||
"""Weigh multiple objects.
|
||||
|
||||
Override in a subclass if you need access to all objects in order
|
||||
to calculate weights. Do not modify the weight of an object here,
|
||||
just return a list of weights.
|
||||
"""
|
||||
# Calculate the weights
|
||||
weights = []
|
||||
for obj in weighed_obj_list:
|
||||
weight = self._weigh_object(obj.obj, weight_properties)
|
||||
|
||||
# Record the min and max values if they are None. If they anything
|
||||
# but none we assume that the weigher has set them
|
||||
if self.minval is None:
|
||||
self.minval = weight
|
||||
if self.maxval is None:
|
||||
self.maxval = weight
|
||||
|
||||
if weight < self.minval:
|
||||
self.minval = weight
|
||||
elif weight > self.maxval:
|
||||
self.maxval = weight
|
||||
|
||||
weights.append(weight)
|
||||
|
||||
return weights
|
||||
|
||||
|
||||
class BaseWeightHandler(base_handler.BaseHandler):
|
||||
object_class = WeighedObject
|
||||
|
||||
def get_weighed_objects(self, weigher_classes, obj_list,
|
||||
weighing_properties):
|
||||
"""Return a sorted (descending), normalized list of WeighedObjects."""
|
||||
|
||||
if not obj_list:
|
||||
return []
|
||||
|
||||
weighed_objs = [self.object_class(obj, 0.0) for obj in obj_list]
|
||||
for weigher_cls in weigher_classes:
|
||||
weigher = weigher_cls()
|
||||
weights = weigher.weigh_objects(weighed_objs, weighing_properties)
|
||||
|
||||
# Normalize the weights
|
||||
weights = normalize(weights,
|
||||
minval=weigher.minval,
|
||||
maxval=weigher.maxval)
|
||||
|
||||
for i, weight in enumerate(weights):
|
||||
obj = weighed_objs[i]
|
||||
obj.weight += weigher.weight_multiplier() * weight
|
||||
|
||||
return sorted(weighed_objs, key=lambda x: x.weight, reverse=True)
|
|
@ -17,6 +17,8 @@ import functools
|
|||
|
||||
from oslo_utils import importutils
|
||||
|
||||
from mogan.scheduler import utils
|
||||
|
||||
|
||||
class LazyLoader(object):
|
||||
|
||||
|
@ -39,9 +41,16 @@ class SchedulerClient(object):
|
|||
"""Client library for placing calls to the scheduler."""
|
||||
|
||||
def __init__(self):
|
||||
self.queryclient = LazyLoader(importutils.import_class(
|
||||
'mogan.scheduler.client.query.SchedulerQueryClient'))
|
||||
self.reportclient = LazyLoader(importutils.import_class(
|
||||
'mogan.scheduler.client.report.SchedulerReportClient'))
|
||||
|
||||
@utils.retry_select_destinations
|
||||
def select_destinations(self, context, spec_obj, filter_properties):
|
||||
return self.queryclient.select_destinations(context, spec_obj,
|
||||
filter_properties)
|
||||
|
||||
def set_inventory_for_provider(self, rp_uuid, rp_name, inv_data,
|
||||
res_class):
|
||||
self.reportclient.set_inventory_for_provider(
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (c) 2014 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from mogan.scheduler import rpcapi as scheduler_rpcapi
|
||||
|
||||
|
||||
class SchedulerQueryClient(object):
|
||||
"""Client class for querying to the scheduler."""
|
||||
|
||||
def __init__(self):
|
||||
self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
|
||||
|
||||
def select_destinations(self, context, spec_obj, filter_properties):
|
||||
"""Returns destinations(s) best suited for this request_spec and
|
||||
filter_properties.
|
||||
|
||||
The result should be a list of dicts with 'host', 'nodename' and
|
||||
'limits' as keys.
|
||||
"""
|
||||
return self.scheduler_rpcapi.select_destinations(context, spec_obj,
|
||||
filter_properties)
|
|
@ -98,7 +98,7 @@ class SchedulerReportClient(object):
|
|||
def __init__(self):
|
||||
# A dict, keyed by the resource provider UUID, of ResourceProvider
|
||||
# objects that will have their inventories and allocations tracked by
|
||||
# the placement API for the compute host
|
||||
# the placement API for the node
|
||||
self._resource_providers = {}
|
||||
# A dict, keyed by resource provider UUID, of sets of aggregate UUIDs
|
||||
# the provider is associated with
|
||||
|
@ -167,8 +167,6 @@ class SchedulerReportClient(object):
|
|||
url,
|
||||
endpoint_filter=self.ks_filter, raise_exc=False)
|
||||
|
||||
# TODO(sbauza): Change that poor interface into passing a rich versioned
|
||||
# object that would provide the ResourceProvider requirements.
|
||||
@safe_connect
|
||||
def get_filtered_resource_providers(self, filters):
|
||||
"""Returns a list of ResourceProviders matching the requirements
|
||||
|
@ -421,34 +419,6 @@ class SchedulerReportClient(object):
|
|||
{'placement_req_id': get_placement_request_id(result),
|
||||
'resource_provider_uuid': rp_uuid,
|
||||
'generation_id': cur_rp_gen})
|
||||
# NOTE(jaypipes): There may be cases when we try to set a
|
||||
# provider's inventory that results in attempting to delete an
|
||||
# inventory record for a resource class that has an active
|
||||
# allocation. We need to catch this particular case and raise an
|
||||
# exception here instead of returning False, since we should not
|
||||
# re-try the operation in this case.
|
||||
#
|
||||
# A use case for where this can occur is the following:
|
||||
#
|
||||
# 1) Provider created for each Ironic baremetal node in Newton
|
||||
# 2) Inventory records for baremetal node created for VCPU,
|
||||
# MEMORY_MB and DISK_GB
|
||||
# 3) A Nova instance consumes the baremetal node and allocation
|
||||
# records are created for VCPU, MEMORY_MB and DISK_GB matching
|
||||
# the total amount of those resource on the baremetal node.
|
||||
# 3) Upgrade to Ocata and now resource tracker wants to set the
|
||||
# provider's inventory to a single record of resource class
|
||||
# CUSTOM_IRON_SILVER (or whatever the Ironic node's
|
||||
# "resource_class" attribute is)
|
||||
# 4) Scheduler report client sends the inventory list containing a
|
||||
# single CUSTOM_IRON_SILVER record and placement service
|
||||
# attempts to delete the inventory records for VCPU, MEMORY_MB
|
||||
# and DISK_GB. An exception is raised from the placement service
|
||||
# because allocation records exist for those resource classes,
|
||||
# and a 409 Conflict is returned to the compute node. We need to
|
||||
# trigger a delete of the old allocation records and then set
|
||||
# the new inventory, and then set the allocation record to the
|
||||
# new CUSTOM_IRON_SILVER record.
|
||||
match = _RE_INV_IN_USE.search(result.text)
|
||||
if match:
|
||||
rc = match.group(1)
|
||||
|
@ -517,71 +487,6 @@ class SchedulerReportClient(object):
|
|||
time.sleep(1)
|
||||
return False
|
||||
|
||||
@safe_connect
|
||||
def _delete_inventory(self, rp_uuid):
|
||||
"""Deletes all inventory records for a resource provider with the
|
||||
supplied UUID.
|
||||
"""
|
||||
curr = self._get_inventory_and_update_provider_generation(rp_uuid)
|
||||
|
||||
# Check to see if we need to update placement's view
|
||||
if not curr.get('inventories', {}):
|
||||
msg = "No inventory to delete from resource provider %s."
|
||||
LOG.debug(msg, rp_uuid)
|
||||
return
|
||||
|
||||
msg = ("Compute node %s reported no inventory but previous "
|
||||
"inventory was detected. Deleting existing inventory "
|
||||
"records.")
|
||||
LOG.info(msg, rp_uuid)
|
||||
|
||||
url = '/resource_providers/%s/inventories' % rp_uuid
|
||||
cur_rp_gen = self._resource_providers[rp_uuid]['generation']
|
||||
payload = {
|
||||
'resource_provider_generation': cur_rp_gen,
|
||||
'inventories': {},
|
||||
}
|
||||
r = self.put(url, payload)
|
||||
placement_req_id = get_placement_request_id(r)
|
||||
if r.status_code == 200:
|
||||
# Update our view of the generation for next time
|
||||
updated_inv = r.json()
|
||||
new_gen = updated_inv['resource_provider_generation']
|
||||
|
||||
self._resource_providers[rp_uuid]['generation'] = new_gen
|
||||
msg_args = {
|
||||
'rp_uuid': rp_uuid,
|
||||
'generation': new_gen,
|
||||
'placement_req_id': placement_req_id,
|
||||
}
|
||||
LOG.info(('[%(placement_req_id)s] Deleted all inventory for '
|
||||
'resource provider %(rp_uuid)s at generation '
|
||||
'%(generation)i'),
|
||||
msg_args)
|
||||
return
|
||||
elif r.status_code == 409:
|
||||
rc_str = _extract_inventory_in_use(r.text)
|
||||
if rc_str is not None:
|
||||
msg = ("[%(placement_req_id)s] We cannot delete inventory "
|
||||
"%(rc_str)s for resource provider %(rp_uuid)s "
|
||||
"because the inventory is in use.")
|
||||
msg_args = {
|
||||
'rp_uuid': rp_uuid,
|
||||
'rc_str': rc_str,
|
||||
'placement_req_id': placement_req_id,
|
||||
}
|
||||
LOG.warning(msg, msg_args)
|
||||
return
|
||||
|
||||
msg = ("[%(placement_req_id)s] Failed to delete inventory for "
|
||||
"resource provider %(rp_uuid)s. Got error response: %(err)s")
|
||||
msg_args = {
|
||||
'rp_uuid': rp_uuid,
|
||||
'err': r.text,
|
||||
'placement_req_id': placement_req_id,
|
||||
}
|
||||
LOG.error(msg, msg_args)
|
||||
|
||||
def set_inventory_for_provider(self, rp_uuid, rp_name, inv_data,
|
||||
resource_class):
|
||||
"""Given the UUID of a provider, set the inventory records for the
|
||||
|
@ -601,10 +506,7 @@ class SchedulerReportClient(object):
|
|||
# Auto-create custom resource classes coming from a virt driver
|
||||
self._ensure_resource_class(resource_class)
|
||||
|
||||
if inv_data:
|
||||
self._update_inventory(rp_uuid, inv_data)
|
||||
else:
|
||||
self._delete_inventory(rp_uuid)
|
||||
self._update_inventory(rp_uuid, inv_data)
|
||||
|
||||
@safe_connect
|
||||
def _ensure_resource_class(self, name):
|
||||
|
@ -704,8 +606,26 @@ class SchedulerReportClient(object):
|
|||
raise exception.InvalidResourceClass(resource_class=name)
|
||||
|
||||
@safe_connect
|
||||
def put_allocations(self, rp_uuid, consumer_uuid, alloc_data):
|
||||
"""Creates allocation records for the supplied instance UUID against
|
||||
def delete_allocation_for_server(self, uuid):
|
||||
url = '/allocations/%s' % uuid
|
||||
r = self.delete(url)
|
||||
if r:
|
||||
LOG.info('Deleted allocation for server %s', uuid)
|
||||
else:
|
||||
# Check for 404 since we don't need to log a warning if we tried to
|
||||
# delete something which doesn't actually exist.
|
||||
if r.status_code != 404:
|
||||
LOG.warning(
|
||||
'Unable to delete allocation for server '
|
||||
'%(uuid)s: (%(code)i %(text)s)',
|
||||
{'uuid': uuid,
|
||||
'code': r.status_code,
|
||||
'text': r.text})
|
||||
|
||||
@safe_connect
|
||||
def put_allocations(self, rp_uuid, consumer_uuid, alloc_data, project_id,
|
||||
user_id):
|
||||
"""Creates allocation records for the supplied server UUID against
|
||||
the supplied resource provider.
|
||||
|
||||
:note Currently we only allocate against a single resource provider.
|
||||
|
@ -713,9 +633,11 @@ class SchedulerReportClient(object):
|
|||
reality, this will change to allocate against multiple providers.
|
||||
|
||||
:param rp_uuid: The UUID of the resource provider to allocate against.
|
||||
:param consumer_uuid: The instance's UUID.
|
||||
:param consumer_uuid: The server's UUID.
|
||||
:param alloc_data: Dict, keyed by resource class, of amounts to
|
||||
consume.
|
||||
:param project_id: The project_id associated with the allocations.
|
||||
:param user_id: The user_id associated with the allocations.
|
||||
:returns: True if the allocations were created, False otherwise.
|
||||
"""
|
||||
payload = {
|
||||
|
@ -727,18 +649,51 @@ class SchedulerReportClient(object):
|
|||
'resources': alloc_data,
|
||||
},
|
||||
],
|
||||
'project_id': project_id,
|
||||
'user_id': user_id,
|
||||
}
|
||||
url = '/allocations/%s' % consumer_uuid
|
||||
r = self.put(url, payload)
|
||||
r = self.put(url, payload, version='1.8')
|
||||
if r.status_code == 406:
|
||||
# microversion 1.8 not available so try the earlier way
|
||||
# TODO(melwitt): Remove this when we can be sure all placement
|
||||
# servers support version 1.8.
|
||||
payload.pop('project_id')
|
||||
payload.pop('user_id')
|
||||
r = self.put(url, payload)
|
||||
if r.status_code != 204:
|
||||
LOG.warning(
|
||||
'Unable to submit allocation for instance '
|
||||
'Unable to submit allocation for server '
|
||||
'%(uuid)s (%(code)i %(text)s)',
|
||||
{'uuid': consumer_uuid,
|
||||
'code': r.status_code,
|
||||
'text': r.text})
|
||||
return r.status_code == 204
|
||||
|
||||
@safe_connect
|
||||
def delete_resource_provider(self, rp_uuid):
|
||||
"""Deletes the ResourceProvider record for the compute_node.
|
||||
|
||||
:param rp_uuid: The uuid of resource provider being deleted.
|
||||
"""
|
||||
url = "/resource_providers/%s" % rp_uuid
|
||||
resp = self.delete(url)
|
||||
if resp:
|
||||
LOG.info("Deleted resource provider %s", rp_uuid)
|
||||
# clean the caches
|
||||
self._resource_providers.pop(rp_uuid, None)
|
||||
self._provider_aggregate_map.pop(rp_uuid, None)
|
||||
else:
|
||||
# Check for 404 since we don't need to log a warning if we tried to
|
||||
# delete something which doesn"t actually exist.
|
||||
if resp.status_code != 404:
|
||||
LOG.warning(
|
||||
"Unable to delete resource provider "
|
||||
"%(uuid)s: (%(code)i %(text)s)",
|
||||
{"uuid": rp_uuid,
|
||||
"code": resp.status_code,
|
||||
"text": resp.text})
|
||||
|
||||
@safe_connect
|
||||
def get_allocations_for_resource_provider(self, rp_uuid):
|
||||
url = '/resource_providers/%s/allocations' % rp_uuid
|
||||
|
@ -747,3 +702,10 @@ class SchedulerReportClient(object):
|
|||
return {}
|
||||
else:
|
||||
return resp.json()['allocations']
|
||||
|
||||
def delete_allocations_for_resource_provider(self, rp_uuid):
|
||||
allocations = self.get_allocations_for_resource_provider(rp_uuid)
|
||||
if allocations:
|
||||
LOG.info('Deleted allocation for resource provider %s', rp_uuid)
|
||||
for consumer_id in allocations:
|
||||
self.delete_allocation_for_server(consumer_id)
|
||||
|
|
|
@ -20,7 +20,6 @@ Scheduler base class that all Schedulers should inherit from
|
|||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import importutils
|
||||
|
||||
from mogan.common.i18n import _
|
||||
|
||||
|
@ -31,10 +30,6 @@ CONF = cfg.CONF
|
|||
class Scheduler(object):
|
||||
"""The base class that all Scheduler classes should inherit from."""
|
||||
|
||||
def __init__(self):
|
||||
self.node_manager = importutils.import_object(
|
||||
CONF.scheduler.scheduler_node_manager)
|
||||
|
||||
def schedule(self, context, request_spec, filter_properties):
|
||||
"""Must override schedule method for scheduler to work."""
|
||||
raise NotImplementedError(_("Must implement schedule"))
|
||||
|
|
|
@ -15,16 +15,16 @@
|
|||
You can customize this scheduler by specifying your own node Filters and
|
||||
Weighing Functions.
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from mogan.common import exception
|
||||
from mogan.common.i18n import _
|
||||
from mogan.common import utils
|
||||
from mogan import objects
|
||||
from mogan.scheduler import client
|
||||
from mogan.scheduler import driver
|
||||
from mogan.scheduler import scheduler_options
|
||||
from mogan.scheduler import utils as sched_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -34,21 +34,8 @@ 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.max_attempts = self._max_attempts()
|
||||
|
||||
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.
|
||||
"""
|
||||
server = request_spec['server_properties']
|
||||
filter_properties['availability_zone'] = \
|
||||
server.get('availability_zone')
|
||||
self.reportclient = client.SchedulerClient().reportclient
|
||||
|
||||
def _max_attempts(self):
|
||||
max_attempts = CONF.scheduler.scheduler_max_attempts
|
||||
|
@ -87,7 +74,7 @@ class FilterScheduler(driver.Scheduler):
|
|||
# re-scheduling is disabled.
|
||||
return
|
||||
|
||||
server_id = request_spec.get('server_id')
|
||||
server_id = request_spec.get('server_ids')[0]
|
||||
self._log_server_error(server_id, retry)
|
||||
|
||||
if retry['num_attempts'] > max_attempts:
|
||||
|
@ -97,54 +84,21 @@ class FilterScheduler(driver.Scheduler):
|
|||
{'max_attempts': max_attempts,
|
||||
'server_id': server_id})
|
||||
|
||||
def _get_weighted_candidates(self, context, request_spec,
|
||||
filter_properties=None):
|
||||
"""Return a list of nodes that meet required specs.
|
||||
@staticmethod
|
||||
def _get_res_cls_filters(request_spec):
|
||||
flavor_dict = request_spec['flavor']
|
||||
resources = dict([(sched_utils.ensure_resource_class_name(res[0]),
|
||||
int(res[1]))
|
||||
for res in flavor_dict['resources'].items()])
|
||||
return resources
|
||||
|
||||
Returned list is ordered by their fitness.
|
||||
"""
|
||||
# Since Mogan is using mixed filters from Oslo and it's own, which
|
||||
# takes 'resource_XX' and 'server_XX' as input respectively, copying
|
||||
# 'flavor' to 'resource_type' will make both filters happy.
|
||||
flavor = resource_type = request_spec.get("flavor")
|
||||
|
||||
config_options = self._get_configuration_options()
|
||||
|
||||
if filter_properties is None:
|
||||
filter_properties = {}
|
||||
self._populate_retry(filter_properties, request_spec)
|
||||
|
||||
request_spec_dict = jsonutils.to_primitive(request_spec)
|
||||
|
||||
filter_properties.update({'request_spec': request_spec_dict,
|
||||
'config_options': config_options,
|
||||
'flavor': flavor,
|
||||
'resource_type': resource_type})
|
||||
|
||||
self.populate_filter_properties(request_spec,
|
||||
filter_properties)
|
||||
|
||||
# Find our local list of acceptable nodes by filtering and
|
||||
# weighing our options. 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.
|
||||
nodes = self.node_manager.get_all_node_states(context)
|
||||
|
||||
# Filter local nodes based on requirements ...
|
||||
nodes = self.node_manager.get_filtered_nodes(nodes,
|
||||
filter_properties)
|
||||
if not nodes:
|
||||
def _get_filtered_nodes(self, request_spec):
|
||||
query_filters = {'resources': self._get_res_cls_filters(request_spec)}
|
||||
filtered_nodes = self.reportclient.get_filtered_resource_providers(
|
||||
query_filters)
|
||||
if not filtered_nodes:
|
||||
return []
|
||||
|
||||
LOG.debug("Filtered %(nodes)s", {'nodes': nodes})
|
||||
# weighted_node = WeightedNode() ... the best
|
||||
# node for the job.
|
||||
weighed_nodes = self.node_manager.get_weighed_nodes(nodes,
|
||||
filter_properties)
|
||||
LOG.debug("Weighed %(nodes)s", {'nodes': weighed_nodes})
|
||||
return weighed_nodes
|
||||
return [node['uuid'] for node in filtered_nodes]
|
||||
|
||||
def schedule(self, context, request_spec, filter_properties=None):
|
||||
|
||||
|
@ -155,20 +109,21 @@ class FilterScheduler(driver.Scheduler):
|
|||
# we need to improve this.
|
||||
@utils.synchronized('schedule')
|
||||
def _schedule(self, context, request_spec, filter_properties):
|
||||
weighed_nodes = self._get_weighted_candidates(
|
||||
context, request_spec, filter_properties)
|
||||
if not weighed_nodes:
|
||||
LOG.warning('No weighed nodes found for server '
|
||||
self._populate_retry(filter_properties, request_spec)
|
||||
filtered_nodes = self._get_filtered_nodes(request_spec)
|
||||
if not filtered_nodes:
|
||||
LOG.warning('No filtered nodes found for server '
|
||||
'with properties: %s',
|
||||
request_spec.get('flavor'))
|
||||
raise exception.NoValidNode(_("No weighed nodes available"))
|
||||
|
||||
dest_nodes = []
|
||||
nodes = self._choose_nodes(weighed_nodes, request_spec)
|
||||
for node in nodes:
|
||||
node.obj.consume_from_request(context)
|
||||
dest_nodes.append(
|
||||
dict(node_uuid=node.obj.node_uuid, ports=node.obj.ports))
|
||||
raise exception.NoValidNode(_("No filtered nodes available"))
|
||||
dest_nodes = self._choose_nodes(filtered_nodes, request_spec)
|
||||
for server_id, node in zip(request_spec['server_ids'], dest_nodes):
|
||||
server_obj = objects.Server.get(
|
||||
context, server_id)
|
||||
alloc_data = self._get_res_cls_filters(request_spec)
|
||||
self.reportclient.put_allocations(
|
||||
node, server_obj.uuid, alloc_data,
|
||||
server_obj.project_id, server_obj.user_id)
|
||||
return dest_nodes
|
||||
|
||||
return _schedule(self, context, request_spec, filter_properties)
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
Scheduler node filters
|
||||
"""
|
||||
|
||||
from mogan.scheduler import base_filter
|
||||
|
||||
|
||||
class BaseNodeFilter(base_filter.BaseFilter):
|
||||
"""Base class for node filters."""
|
||||
def _filter_one(self, obj, filter_properties):
|
||||
"""Return True if the object passes the filter, otherwise False."""
|
||||
return self.node_passes(obj, filter_properties)
|
||||
|
||||
def node_passes(self, node_state, filter_properties):
|
||||
"""Return True if the NodeState passes the filter, otherwise False.
|
||||
|
||||
Override this in a subclass.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class NodeFilterHandler(base_filter.BaseFilterHandler):
|
||||
def __init__(self, namespace):
|
||||
super(NodeFilterHandler, self).__init__(BaseNodeFilter, namespace)
|
|
@ -1,31 +0,0 @@
|
|||
# Copyright (c) 2011-2012 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.
|
||||
|
||||
from mogan.scheduler import filters
|
||||
|
||||
|
||||
class AvailabilityZoneFilter(filters.BaseNodeFilter):
|
||||
"""Filters Nodes by availability zone."""
|
||||
|
||||
# Availability zones do not change within a request
|
||||
run_filter_once_per_request = True
|
||||
|
||||
def node_passes(self, node_state, filter_properties):
|
||||
spec = filter_properties.get('request_spec', {})
|
||||
availability_zone = spec.get('availability_zone')
|
||||
|
||||
if availability_zone:
|
||||
return availability_zone == node_state.availability_zone
|
||||
return True
|
|
@ -1,86 +0,0 @@
|
|||
# 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.
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from mogan.scheduler import filters
|
||||
from mogan.scheduler.filters import extra_specs_ops
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CapabilitiesFilter(filters.BaseNodeFilter):
|
||||
"""NodeFilter to work with resource server type records."""
|
||||
|
||||
def _satisfies_extra_specs(self, capabilities, resource_type):
|
||||
"""Check if capabilities satisfy resource type requirements.
|
||||
|
||||
Check that the capabilities provided by the services satisfy
|
||||
the extra specs associated with the resource type.
|
||||
"""
|
||||
|
||||
if not resource_type:
|
||||
return True
|
||||
|
||||
extra_specs = resource_type.get('extra_specs', [])
|
||||
if not extra_specs:
|
||||
return True
|
||||
|
||||
for key, req in extra_specs.items():
|
||||
|
||||
# Either not scoped format, or in capabilities scope
|
||||
scope = key.split(':')
|
||||
|
||||
# Ignore scoped (such as vendor-specific) capabilities
|
||||
if len(scope) > 1 and scope[0] != "capabilities":
|
||||
continue
|
||||
# Strip off prefix if spec started with 'capabilities:'
|
||||
elif scope[0] == "capabilities":
|
||||
del scope[0]
|
||||
|
||||
cap = capabilities
|
||||
for index in range(len(scope)):
|
||||
try:
|
||||
cap = cap[scope[index]]
|
||||
except (TypeError, KeyError):
|
||||
LOG.debug("Node doesn't provide capability '%(cap)s' " %
|
||||
{'cap': scope[index]})
|
||||
return False
|
||||
|
||||
# Make all capability values a list so we can handle lists
|
||||
cap_list = [cap] if not isinstance(cap, list) else cap
|
||||
|
||||
# Loop through capability values looking for any match
|
||||
for cap_value in cap_list:
|
||||
if extra_specs_ops.match(cap_value, req):
|
||||
break
|
||||
else:
|
||||
# Nothing matched, so bail out
|
||||
LOG.debug('Flavor extra spec requirement '
|
||||
'"%(key)s=%(req)s" does not match reported '
|
||||
'capability "%(cap)s"',
|
||||
{'key': key, 'req': req, 'cap': cap})
|
||||
return False
|
||||
return True
|
||||
|
||||
def node_passes(self, node_state, filter_properties):
|
||||
"""Return a list of nodes that can create resource_type."""
|
||||
resource_type = filter_properties.get('resource_type')
|
||||
if not self._satisfies_extra_specs(node_state.capabilities,
|
||||
resource_type):
|
||||
LOG.debug("%(node_state)s fails resource_type extra_specs "
|
||||
"requirements", {'node_state': node_state})
|
||||
return False
|
||||
return True
|
|
@ -1,77 +0,0 @@
|
|||
# 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.
|
||||
|
||||
import operator
|
||||
|
||||
from oslo_utils import strutils
|
||||
|
||||
# 1. The following operations are supported:
|
||||
# =, s==, s!=, s>=, s>, s<=, s<, <in>, <is>, <or>, ==, !=, >=, <=
|
||||
# 2. Note that <or> is handled in a different way below.
|
||||
# 3. If the first word in the extra_specs is not one of the operators,
|
||||
# it is ignored.
|
||||
_op_methods = {'=': lambda x, y: float(x) >= float(y),
|
||||
'<in>': lambda x, y: y in x,
|
||||
'<is>': lambda x, y: (strutils.bool_from_string(x) is
|
||||
strutils.bool_from_string(y)),
|
||||
'==': lambda x, y: float(x) == float(y),
|
||||
'!=': lambda x, y: float(x) != float(y),
|
||||
'>=': lambda x, y: float(x) >= float(y),
|
||||
'<=': lambda x, y: float(x) <= float(y),
|
||||
's==': operator.eq,
|
||||
's!=': operator.ne,
|
||||
's<': operator.lt,
|
||||
's<=': operator.le,
|
||||
's>': operator.gt,
|
||||
's>=': operator.ge}
|
||||
|
||||
|
||||
def match(value, req):
|
||||
if req is None:
|
||||
if value is None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
words = req.split()
|
||||
|
||||
op = method = None
|
||||
if words:
|
||||
op = words.pop(0)
|
||||
method = _op_methods.get(op)
|
||||
|
||||
if op != '<or>' and not method:
|
||||
return value == req
|
||||
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if op == '<or>': # Ex: <or> v1 <or> v2 <or> v3
|
||||
while True:
|
||||
if words.pop(0) == value:
|
||||
return True
|
||||
if not words:
|
||||
break
|
||||
op = words.pop(0) # remove a keyword <or>
|
||||
if not words:
|
||||
break
|
||||
return False
|
||||
|
||||
try:
|
||||
if words and method(value, words[0]):
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return False
|
|
@ -1,32 +0,0 @@
|
|||
# Copyright 2016 Huawei Technologies Co.,LTD.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from mogan.scheduler import filters
|
||||
|
||||
|
||||
class FlavorFilter(filters.BaseNodeFilter):
|
||||
"""Filters Nodes by server type."""
|
||||
|
||||
# Flavors do not change within a request
|
||||
run_filter_once_per_request = True
|
||||
|
||||
def node_passes(self, node_state, filter_properties):
|
||||
spec = filter_properties.get('request_spec', {})
|
||||
flavor = spec.get('flavor', {})
|
||||
type_name = flavor.get('name')
|
||||
|
||||
if type_name:
|
||||
return type_name == node_state.flavor
|
||||
return True
|
|
@ -1,149 +0,0 @@
|
|||
# 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.
|
||||
|
||||
import operator
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
import six
|
||||
|
||||
from mogan.scheduler import filters
|
||||
|
||||
|
||||
class JsonFilter(filters.BaseNodeFilter):
|
||||
"""Node Filter to allow simple JSON-based grammar for selecting nodes."""
|
||||
def _op_compare(self, args, op):
|
||||
"""Compare first item of args with the rest using specified operator.
|
||||
|
||||
Returns True if the specified operator can successfully
|
||||
compare the first item in the args with all the rest. Will
|
||||
return False if only one item is in the list.
|
||||
"""
|
||||
if len(args) < 2:
|
||||
return False
|
||||
if op is operator.contains:
|
||||
bad = args[0] not in args[1:]
|
||||
else:
|
||||
bad = [arg for arg in args[1:]
|
||||
if not op(args[0], arg)]
|
||||
return not bool(bad)
|
||||
|
||||
def _equals(self, args):
|
||||
"""First term is == all the other terms."""
|
||||
return self._op_compare(args, operator.eq)
|
||||
|
||||
def _less_than(self, args):
|
||||
"""First term is < all the other terms."""
|
||||
return self._op_compare(args, operator.lt)
|
||||
|
||||
def _greater_than(self, args):
|
||||
"""First term is > all the other terms."""
|
||||
return self._op_compare(args, operator.gt)
|
||||
|
||||
def _in(self, args):
|
||||
"""First term is in set of remaining terms."""
|
||||
return self._op_compare(args, operator.contains)
|
||||
|
||||
def _less_than_equal(self, args):
|
||||
"""First term is <= all the other terms."""
|
||||
return self._op_compare(args, operator.le)
|
||||
|
||||
def _greater_than_equal(self, args):
|
||||
"""First term is >= all the other terms."""
|
||||
return self._op_compare(args, operator.ge)
|
||||
|
||||
def _not(self, args):
|
||||
"""Flip each of the arguments."""
|
||||
return [not arg for arg in args]
|
||||
|
||||
def _or(self, args):
|
||||
"""True if any arg is True."""
|
||||
return any(args)
|
||||
|
||||
def _and(self, args):
|
||||
"""True if all args are True."""
|
||||
return all(args)
|
||||
|
||||
commands = {
|
||||
'=': _equals,
|
||||
'<': _less_than,
|
||||
'>': _greater_than,
|
||||
'in': _in,
|
||||
'<=': _less_than_equal,
|
||||
'>=': _greater_than_equal,
|
||||
'not': _not,
|
||||
'or': _or,
|
||||
'and': _and,
|
||||
}
|
||||
|
||||
def _parse_string(self, string, node_state):
|
||||
"""Parse capability lookup strings.
|
||||
|
||||
Strings prefixed with $ are capability lookups in the
|
||||
form '$variable' where 'variable' is an attribute in the
|
||||
NodeState class. If $variable is a dictionary, you may
|
||||
use: $variable.dictkey
|
||||
"""
|
||||
if not string:
|
||||
return None
|
||||
if not string.startswith("$"):
|
||||
return string
|
||||
|
||||
path = string[1:].split(".")
|
||||
obj = getattr(node_state, path[0], None)
|
||||
if obj is None:
|
||||
return None
|
||||
for item in path[1:]:
|
||||
obj = obj.get(item)
|
||||
if obj is None:
|
||||
return None
|
||||
return obj
|
||||
|
||||
def _process_filter(self, query, node_state):
|
||||
"""Recursively parse the query structure."""
|
||||
if not query:
|
||||
return True
|
||||
cmd = query[0]
|
||||
method = self.commands[cmd]
|
||||
cooked_args = []
|
||||
for arg in query[1:]:
|
||||
if isinstance(arg, list):
|
||||
arg = self._process_filter(arg, node_state)
|
||||
elif isinstance(arg, six.string_types):
|
||||
arg = self._parse_string(arg, node_state)
|
||||
if arg is not None:
|
||||
cooked_args.append(arg)
|
||||
result = method(self, cooked_args)
|
||||
return result
|
||||
|
||||
def node_passes(self, node_state, filter_properties):
|
||||
"""Return a list of nodes that can fulfill query requirements."""
|
||||
try:
|
||||
query = filter_properties['scheduler_hints']['query']
|
||||
except KeyError:
|
||||
query = None
|
||||
if not query:
|
||||
return True
|
||||
|
||||
# NOTE(comstud): Not checking capabilities or service for
|
||||
# enabled/disabled so that a provided json filter can decide
|
||||
|
||||
result = self._process_filter(jsonutils.loads(query), node_state)
|
||||
if isinstance(result, list):
|
||||
# If any succeeded, include the node
|
||||
result = any(result)
|
||||
if result:
|
||||
# Filter it out.
|
||||
return True
|
||||
return False
|
|
@ -1,50 +0,0 @@
|
|||
# Copyright 2016 Huawei Technologies Co.,LTD.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from mogan.scheduler import filters
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortsFilter(filters.BaseNodeFilter):
|
||||
"""NodeFilter to work with resource server type records."""
|
||||
|
||||
def _satisfies_networks(self, ports, networks):
|
||||
"""Check if ports satisfy networks requirements.
|
||||
|
||||
Check that the ports provided by the nodes satisfy
|
||||
the networks associated with the request spec.
|
||||
"""
|
||||
|
||||
if not networks:
|
||||
return True
|
||||
|
||||
if len(ports) < len(networks):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def node_passes(self, node_state, filter_properties):
|
||||
"""Return a list of nodes that can create resource_type."""
|
||||
spec = filter_properties.get('request_spec', {})
|
||||
props = spec.get('server_properties', {})
|
||||
networks = props.get('networks')
|
||||
if not self._satisfies_networks(node_state.ports, networks):
|
||||
LOG.debug("%(node_state)s fails network ports "
|
||||
"requirements", {'node_state': node_state})
|
||||
return False
|
||||
return True
|
|
@ -15,14 +15,13 @@
|
|||
|
||||
import eventlet
|
||||
import oslo_messaging as messaging
|
||||
from oslo_service import periodic_task
|
||||
from oslo_utils import importutils
|
||||
|
||||
from mogan.common import exception
|
||||
from mogan.conf import CONF
|
||||
|
||||
|
||||
class SchedulerManager(periodic_task.PeriodicTasks):
|
||||
class SchedulerManager(object):
|
||||
"""Mogan Scheduler manager main class."""
|
||||
|
||||
RPC_API_VERSION = '1.0'
|
||||
|
@ -30,7 +29,7 @@ class SchedulerManager(periodic_task.PeriodicTasks):
|
|||
target = messaging.Target(version=RPC_API_VERSION)
|
||||
|
||||
def __init__(self, topic, host=None):
|
||||
super(SchedulerManager, self).__init__(CONF)
|
||||
super(SchedulerManager, self).__init__()
|
||||
self.host = host or CONF.host
|
||||
self.topic = topic
|
||||
scheduler_driver = CONF.scheduler.scheduler_driver
|
||||
|
@ -53,6 +52,3 @@ class SchedulerManager(periodic_task.PeriodicTasks):
|
|||
|
||||
def del_host(self):
|
||||
pass
|
||||
|
||||
def periodic_tasks(self, context, raise_on_error=False):
|
||||
return self.run_periodic_tasks(context, raise_on_error=raise_on_error)
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
Manage nodes.
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import importutils
|
||||
|
||||
from mogan.common import exception
|
||||
from mogan import objects
|
||||
from mogan.scheduler import filters
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NodeState(object):
|
||||
"""Mutable and immutable information tracked for a Ironic node."""
|
||||
|
||||
def __init__(self, node):
|
||||
self.node_uuid = node.node_uuid
|
||||
self.capabilities = node.extra_specs
|
||||
self.availability_zone = node.availability_zone \
|
||||
or CONF.engine.default_availability_zone
|
||||
self.flavor = node.resource_class
|
||||
self.ports = node.ports
|
||||
|
||||
def consume_from_request(self, context):
|
||||
"""Consume the compute node."""
|
||||
objects.ComputeNode.consume_node(context, self.node_uuid)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Node:%s resource_class:%s>" % (self.node_uuid, self.flavor)
|
||||
|
||||
|
||||
class NodeManager(object):
|
||||
"""Base NodeManager class."""
|
||||
|
||||
node_state_cls = NodeState
|
||||
|
||||
def __init__(self):
|
||||
self.filter_handler = filters.NodeFilterHandler(
|
||||
'mogan.scheduler.filters')
|
||||
self.filter_classes = self.filter_handler.get_all_classes()
|
||||
self.weight_handler = importutils.import_object(
|
||||
CONF.scheduler.scheduler_weight_handler,
|
||||
'mogan.scheduler.weights')
|
||||
self.weight_classes = self.weight_handler.get_all_classes()
|
||||
|
||||
def _choose_node_filters(self, filter_cls_names):
|
||||
"""Return a list of available filter names.
|
||||
|
||||
This function checks input filter names against a predefined set
|
||||
of acceptable filterss (all loaded filters). If input is None,
|
||||
it uses CONF.scheduler_default_filters instead.
|
||||
"""
|
||||
if filter_cls_names is None:
|
||||
filter_cls_names = CONF.scheduler.scheduler_default_filters
|
||||
if not isinstance(filter_cls_names, (list, tuple)):
|
||||
filter_cls_names = [filter_cls_names]
|
||||
good_filters = []
|
||||
bad_filters = []
|
||||
for filter_name in filter_cls_names:
|
||||
found_class = False
|
||||
for cls in self.filter_classes:
|
||||
if cls.__name__ == filter_name:
|
||||
found_class = True
|
||||
good_filters.append(cls)
|
||||
break
|
||||
if not found_class:
|
||||
bad_filters.append(filter_name)
|
||||
if bad_filters:
|
||||
raise exception.SchedulerNodeFilterNotFound(
|
||||
filter_name=", ".join(bad_filters))
|
||||
return good_filters
|
||||
|
||||
def _choose_node_weighers(self, weight_cls_names):
|
||||
"""Return a list of available weigher names.
|
||||
|
||||
This function checks input weigher names against a predefined set
|
||||
of acceptable weighers (all loaded weighers). If input is None,
|
||||
it uses CONF.scheduler_default_weighers instead.
|
||||
"""
|
||||
if weight_cls_names is None:
|
||||
weight_cls_names = CONF.scheduler.scheduler_default_weighers
|
||||
if not isinstance(weight_cls_names, (list, tuple)):
|
||||
weight_cls_names = [weight_cls_names]
|
||||
|
||||
good_weighers = []
|
||||
bad_weighers = []
|
||||
for weigher_name in weight_cls_names:
|
||||
found_class = False
|
||||
for cls in self.weight_classes:
|
||||
if cls.__name__ == weigher_name:
|
||||
good_weighers.append(cls)
|
||||
found_class = True
|
||||
break
|
||||
if not found_class:
|
||||
bad_weighers.append(weigher_name)
|
||||
if bad_weighers:
|
||||
raise exception.SchedulerNodeWeigherNotFound(
|
||||
weigher_name=", ".join(bad_weighers))
|
||||
return good_weighers
|
||||
|
||||
def get_filtered_nodes(self, nodes, filter_properties,
|
||||
filter_class_names=None):
|
||||
"""Filter nodes and return only ones passing all filters."""
|
||||
filter_classes = self._choose_node_filters(filter_class_names)
|
||||
return self.filter_handler.get_filtered_objects(filter_classes,
|
||||
nodes,
|
||||
filter_properties)
|
||||
|
||||
def get_weighed_nodes(self, nodes, weight_properties,
|
||||
weigher_class_names=None):
|
||||
"""Weigh the nodes."""
|
||||
weigher_classes = self._choose_node_weighers(weigher_class_names)
|
||||
return self.weight_handler.get_weighed_objects(weigher_classes,
|
||||
nodes,
|
||||
weight_properties)
|
||||
|
||||
def get_all_node_states(self, context):
|
||||
"""Returns a list of all the nodes the NodeManager knows about."""
|
||||
|
||||
nodes = objects.ComputeNodeList.get_all_available(context)
|
||||
node_states = []
|
||||
for node in nodes:
|
||||
node_state = self.node_state_cls(node)
|
||||
node_states.append(node_state)
|
||||
|
||||
return node_states
|
|
@ -1,95 +0,0 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
SchedulerOptions monitors a local .json file for changes and loads
|
||||
it if needed. This file is converted to a data structure and passed
|
||||
into the filtering and weighing functions which can use it for
|
||||
dynamic configuration.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import timeutils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SchedulerOptions(object):
|
||||
"""SchedulerOptions monitors a local .json file for changes.
|
||||
|
||||
The file is reloaded if needed and converted to a data structure and
|
||||
passed into the filtering and weighing functions which can use it
|
||||
for dynamic configuration.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(SchedulerOptions, self).__init__()
|
||||
self.data = {}
|
||||
self.last_modified = None
|
||||
self.last_checked = None
|
||||
|
||||
def _get_file_handle(self, filename):
|
||||
"""Get file handle. Broken out for testing."""
|
||||
return open(filename)
|
||||
|
||||
def _get_file_timestamp(self, filename):
|
||||
"""Get the last modified datetime. Broken out for testing."""
|
||||
try:
|
||||
return os.path.getmtime(filename)
|
||||
except os.error:
|
||||
LOG.exception("Could not stat scheduler options file "
|
||||
"%(filename)s.",
|
||||
{'filename': filename})
|
||||
raise
|
||||
|
||||
def _load_file(self, handle):
|
||||
"""Decode the JSON file. Broken out for testing."""
|
||||
try:
|
||||
return json.load(handle)
|
||||
except ValueError:
|
||||
LOG.exception("Could not decode scheduler options.")
|
||||
return {}
|
||||
|
||||
def _get_time_now(self):
|
||||
"""Get current UTC. Broken out for testing."""
|
||||
return timeutils.utcnow()
|
||||
|
||||
def get_configuration(self, filename=None):
|
||||
"""Check the json file for changes and load it if needed."""
|
||||
if not filename:
|
||||
filename = CONF.scheduler.scheduler_json_config_location
|
||||
if not filename:
|
||||
return self.data
|
||||
if self.last_checked:
|
||||
now = self._get_time_now()
|
||||
if now - self.last_checked < datetime.timedelta(minutes=5):
|
||||
return self.data
|
||||
|
||||
last_modified = self._get_file_timestamp(filename)
|
||||
if (not last_modified or not self.last_modified or
|
||||
last_modified > self.last_modified):
|
||||
self.data = self._load_file(self._get_file_handle(filename))
|
||||
self.last_modified = last_modified
|
||||
if not self.data:
|
||||
self.data = {}
|
||||
|
||||
return self.data
|
|
@ -1,45 +0,0 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
Scheduler node weights
|
||||
"""
|
||||
|
||||
from mogan.scheduler import base_weight
|
||||
|
||||
|
||||
class WeighedNode(base_weight.WeighedObject):
|
||||
def to_dict(self):
|
||||
return {
|
||||
'weight': self.weight,
|
||||
'node': self.obj.node_uuid,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return ("WeighedNode [node: %s, weight: %s]" %
|
||||
(self.obj.node_uuid, self.weight))
|
||||
|
||||
|
||||
class BaseNodeWeigher(base_weight.BaseWeigher):
|
||||
"""Base class for node weights."""
|
||||
pass
|
||||
|
||||
|
||||
class OrderedNodeWeightHandler(base_weight.BaseWeightHandler):
|
||||
object_class = WeighedNode
|
||||
|
||||
def __init__(self, namespace):
|
||||
super(OrderedNodeWeightHandler, self).__init__(BaseNodeWeigher,
|
||||
namespace)
|
|
@ -1,41 +0,0 @@
|
|||
# Copyright 2017 Huawei Technologies Co.,LTD.
|
||||
# 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.
|
||||
"""
|
||||
Port Weigher. Weigh nodes by their ports quantity.
|
||||
|
||||
The default is to preferably choose nodes with less ports. If you prefer
|
||||
choosing more ports nodes, you can set the 'port_weight_multiplier' option
|
||||
to a positive number and the weighing has the opposite effect of the default.
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from mogan.scheduler import weights
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class PortWeigher(weights.BaseNodeWeigher):
|
||||
minval = 0
|
||||
|
||||
def weight_multiplier(self):
|
||||
"""Override the weight multiplier."""
|
||||
return CONF.scheduler.port_weight_multiplier
|
||||
|
||||
def _weigh_object(self, node_state, weight_properties):
|
||||
"""Higher weights win. We want to choose less ports node to be the
|
||||
default.
|
||||
"""
|
||||
return len(node_state.ports)
|
|
@ -36,18 +36,16 @@ class CreateServerFlowTestCase(base.TestCase):
|
|||
def test_create_network_task_execute(self, mock_build_networks, mock_save):
|
||||
fake_engine_manager = mock.MagicMock()
|
||||
fake_requested_networks = mock.MagicMock()
|
||||
fake_ports = mock.MagicMock()
|
||||
task = create_server.BuildNetworkTask(fake_engine_manager)
|
||||
server_obj = obj_utils.get_test_server(self.ctxt)
|
||||
mock_build_networks.return_value = None
|
||||
mock_save.return_value = None
|
||||
|
||||
task.execute(
|
||||
self.ctxt, server_obj, fake_requested_networks, fake_ports)
|
||||
self.ctxt, server_obj, fake_requested_networks)
|
||||
mock_build_networks.assert_called_once_with(self.ctxt,
|
||||
server_obj,
|
||||
fake_requested_networks,
|
||||
fake_ports)
|
||||
fake_requested_networks)
|
||||
|
||||
@mock.patch.object(IronicDriver, 'spawn')
|
||||
def test_create_server_task_execute(self, mock_spawn):
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
# Copyright (c) 2016 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.
|
||||
#
|
||||
"""
|
||||
Fakes For Scheduler tests.
|
||||
"""
|
||||
|
||||
from mogan.scheduler import filter_scheduler
|
||||
from mogan.scheduler import node_manager
|
||||
|
||||
|
||||
class FakeFilterScheduler(filter_scheduler.FilterScheduler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FakeFilterScheduler, self).__init__(*args, **kwargs)
|
||||
self.node_manager = node_manager.NodeManager()
|
||||
|
||||
|
||||
class FakeNodeState(node_manager.NodeState):
|
||||
def __init__(self, node, attribute_dict):
|
||||
super(FakeNodeState, self).__init__(node)
|
||||
for (key, val) in attribute_dict.items():
|
||||
setattr(self, key, val)
|
|
@ -1,174 +0,0 @@
|
|||
# Copyright (c) 2016 OpenStack Foundation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import mock
|
||||
|
||||
from mogan.scheduler import base_filter
|
||||
from mogan.tests import base as test
|
||||
|
||||
|
||||
class TestBaseFilter(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBaseFilter, self).setUp()
|
||||
self.filter = base_filter.BaseFilter()
|
||||
|
||||
def test_filter_one_is_called(self):
|
||||
filters = [1, 2, 3, 4]
|
||||
filter_properties = {'x': 'y'}
|
||||
|
||||
self.filter._filter_one = mock.Mock()
|
||||
self.filter._filter_one.side_effect = [False, True, True, False]
|
||||
calls = [mock.call(i, filter_properties) for i in filters]
|
||||
|
||||
result = list(self.filter.filter_all(filters, filter_properties))
|
||||
self.assertEqual([2, 3], result)
|
||||
self.filter._filter_one.assert_has_calls(calls)
|
||||
|
||||
|
||||
class FakeExtension(object):
|
||||
|
||||
def __init__(self, plugin):
|
||||
self.plugin = plugin
|
||||
|
||||
|
||||
class BaseFakeFilter(base_filter.BaseFilter):
|
||||
pass
|
||||
|
||||
|
||||
class FakeFilter1(BaseFakeFilter):
|
||||
"""Derives from BaseFakeFilter and has a fake entry point defined.
|
||||
|
||||
Entry point is returned by fake ExtensionManager.
|
||||
Should be included in the output of all_classes.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FakeFilter2(BaseFakeFilter):
|
||||
"""Derives from BaseFakeFilter but has no entry point.
|
||||
|
||||
Should be not included in all_classes.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FakeFilter3(base_filter.BaseFilter):
|
||||
"""Does not derive from BaseFakeFilter.
|
||||
|
||||
Should not be included.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FakeFilter4(BaseFakeFilter):
|
||||
"""Derives from BaseFakeFilter and has an entry point.
|
||||
|
||||
Should be included.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FakeFilter5(BaseFakeFilter):
|
||||
"""Derives from BaseFakeFilter but has no entry point.
|
||||
|
||||
Should not be included.
|
||||
"""
|
||||
run_filter_once_per_request = True
|
||||
pass
|
||||
|
||||
|
||||
class FilterA(base_filter.BaseFilter):
|
||||
def filter_all(self, list_objs, filter_properties):
|
||||
# return all but the first object
|
||||
return list_objs[1:]
|
||||
|
||||
|
||||
class FilterB(base_filter.BaseFilter):
|
||||
def filter_all(self, list_objs, filter_properties):
|
||||
# return an empty list
|
||||
return None
|
||||
|
||||
|
||||
class FakeExtensionManager(list):
|
||||
|
||||
def __init__(self, namespace):
|
||||
classes = [FakeFilter1, FakeFilter3, FakeFilter4]
|
||||
exts = map(FakeExtension, classes)
|
||||
super(FakeExtensionManager, self).__init__(exts)
|
||||
self.namespace = namespace
|
||||
|
||||
|
||||
class TestBaseFilterHandler(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBaseFilterHandler, self).setUp()
|
||||
self.mock_object(base_filter.base_handler.extension,
|
||||
'ExtensionManager', FakeExtensionManager)
|
||||
self.handler = base_filter.BaseFilterHandler(BaseFakeFilter,
|
||||
'fake_filters')
|
||||
|
||||
def test_get_all_classes(self):
|
||||
# In order for a FakeFilter to be returned by get_all_classes, it has
|
||||
# to comply with these rules:
|
||||
# * It must be derived from BaseFakeFilter
|
||||
# AND
|
||||
# * It must have a python entrypoint assigned (returned by
|
||||
# FakeExtensionManager)
|
||||
expected = [FakeFilter1, FakeFilter4]
|
||||
result = self.handler.get_all_classes()
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def _get_filtered_objects(self, filter_classes, index=0):
|
||||
filter_objs_initial = [1, 2, 3, 4]
|
||||
filter_properties = {'x': 'y'}
|
||||
return self.handler.get_filtered_objects(filter_classes,
|
||||
filter_objs_initial,
|
||||
filter_properties,
|
||||
index)
|
||||
|
||||
@mock.patch.object(FakeFilter4, 'filter_all')
|
||||
@mock.patch.object(FakeFilter3, 'filter_all', return_value=None)
|
||||
def test_get_filtered_objects_return_none(self, fake3_filter_all,
|
||||
fake4_filter_all):
|
||||
filter_classes = [FakeFilter1, FakeFilter2, FakeFilter3, FakeFilter4]
|
||||
result = self._get_filtered_objects(filter_classes)
|
||||
self.assertIsNone(result)
|
||||
fake4_filter_all.assert_not_called()
|
||||
|
||||
def test_get_filtered_objects(self):
|
||||
filter_objs_expected = [1, 2, 3, 4]
|
||||
filter_classes = [FakeFilter1, FakeFilter2, FakeFilter3, FakeFilter4]
|
||||
result = self._get_filtered_objects(filter_classes)
|
||||
self.assertEqual(filter_objs_expected, result)
|
||||
|
||||
def test_get_filtered_objects_with_filter_run_once(self):
|
||||
filter_objs_expected = [1, 2, 3, 4]
|
||||
filter_classes = [FakeFilter5]
|
||||
|
||||
with mock.patch.object(FakeFilter5, 'filter_all',
|
||||
return_value=filter_objs_expected
|
||||
) as fake5_filter_all:
|
||||
result = self._get_filtered_objects(filter_classes)
|
||||
self.assertEqual(filter_objs_expected, result)
|
||||
self.assertEqual(1, fake5_filter_all.call_count)
|
||||
|
||||
result = self._get_filtered_objects(filter_classes, index=1)
|
||||
self.assertEqual(filter_objs_expected, result)
|
||||
self.assertEqual(1, fake5_filter_all.call_count)
|
||||
|
||||
result = self._get_filtered_objects(filter_classes, index=2)
|
||||
self.assertEqual(filter_objs_expected, result)
|
||||
self.assertEqual(1, fake5_filter_all.call_count)
|
|
@ -1,93 +0,0 @@
|
|||
# Copyright (c) 2016 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 NodeManager
|
||||
"""
|
||||
|
||||
import mock
|
||||
from oslo_context import context
|
||||
from oslo_versionedobjects import base as object_base
|
||||
|
||||
from mogan.common import exception
|
||||
from mogan.objects import compute_port
|
||||
from mogan.scheduler import filters
|
||||
from mogan.scheduler import node_manager
|
||||
from mogan.scheduler.node_manager import NodeState
|
||||
from mogan.tests import base as test
|
||||
from mogan.tests.unit.objects import utils as obj_utils
|
||||
|
||||
|
||||
class FakeFilterClass1(filters.BaseNodeFilter):
|
||||
def node_passes(self, node_state, filter_properties):
|
||||
pass
|
||||
|
||||
|
||||
class FakeFilterClass2(filters.BaseNodeFilter):
|
||||
def node_passes(self, node_state, filter_properties):
|
||||
pass
|
||||
|
||||
|
||||
class NodeManagerTestCase(test.TestCase):
|
||||
"""Test case for NodeManager class."""
|
||||
|
||||
def setUp(self):
|
||||
super(NodeManagerTestCase, self).setUp()
|
||||
self.ctxt = context.get_admin_context()
|
||||
self.node_manager = node_manager.NodeManager()
|
||||
|
||||
fake_node = obj_utils.get_test_compute_node(self.ctxt)
|
||||
fake_ports = object_base.obj_make_list(
|
||||
self.ctxt, compute_port.ComputePortList(self.ctxt),
|
||||
compute_port.ComputePort, [])
|
||||
fake_node.ports = fake_ports
|
||||
self.fake_nodes = [NodeState(fake_node)]
|
||||
|
||||
def test_choose_node_filters_not_found(self):
|
||||
self.override_config('scheduler_default_filters', 'FakeFilterClass3',
|
||||
'scheduler')
|
||||
self.node_manager.filter_classes = [FakeFilterClass1,
|
||||
FakeFilterClass2]
|
||||
self.assertRaises(exception.SchedulerNodeFilterNotFound,
|
||||
self.node_manager._choose_node_filters, None)
|
||||
|
||||
def test_choose_node_filters(self):
|
||||
self.override_config('scheduler_default_filters', 'FakeFilterClass2',
|
||||
group='scheduler')
|
||||
self.node_manager.filter_classes = [FakeFilterClass1,
|
||||
FakeFilterClass2]
|
||||
|
||||
# Test returns 1 correct filter class
|
||||
filter_classes = self.node_manager._choose_node_filters(None)
|
||||
self.assertEqual(1, len(filter_classes))
|
||||
self.assertEqual('FakeFilterClass2', filter_classes[0].__name__)
|
||||
|
||||
@mock.patch('mogan.scheduler.node_manager.NodeManager.'
|
||||
'_choose_node_filters')
|
||||
def test_get_filtered_nodes(self, _mock_choose_node_filters):
|
||||
filter_class = FakeFilterClass1
|
||||
mock_func = mock.Mock()
|
||||
mock_func.return_value = True
|
||||
filter_class._filter_one = mock_func
|
||||
_mock_choose_node_filters.return_value = [filter_class]
|
||||
|
||||
fake_properties = {'moo': 1, 'cow': 2}
|
||||
expected = []
|
||||
for fake_node in self.fake_nodes:
|
||||
expected.append(mock.call(fake_node, fake_properties))
|
||||
|
||||
result = self.node_manager.get_filtered_nodes(self.fake_nodes,
|
||||
fake_properties)
|
||||
self.assertEqual(expected, mock_func.call_args_list)
|
||||
self.assertEqual(set(self.fake_nodes), set(result))
|
|
@ -1,94 +0,0 @@
|
|||
#
|
||||
# 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.
|
||||
"""
|
||||
Unit Tests for :py:class:`mogan.scheduler.rpcapi.SchedulerAPI`.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_messaging import _utils as messaging_utils
|
||||
|
||||
from mogan.scheduler import manager as scheduler_manager
|
||||
from mogan.scheduler import rpcapi as scheduler_rpcapi
|
||||
from mogan.tests import base as tests_base
|
||||
from mogan.tests.unit.db import base
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class SchedulerRPCAPITestCase(tests_base.TestCase):
|
||||
|
||||
def test_versions_in_sync(self):
|
||||
self.assertEqual(
|
||||
scheduler_manager.SchedulerManager.RPC_API_VERSION,
|
||||
scheduler_rpcapi.SchedulerAPI.RPC_API_VERSION)
|
||||
|
||||
|
||||
class RPCAPITestCase(base.DbTestCase):
|
||||
|
||||
def _test_rpcapi(self, method, rpc_method, **kwargs):
|
||||
rpcapi = scheduler_rpcapi.SchedulerAPI(topic='fake-topic')
|
||||
|
||||
expected_retval = 'hello world' if rpc_method == 'call' else None
|
||||
|
||||
expected_topic = 'fake-topic'
|
||||
|
||||
target = {
|
||||
"topic": expected_topic,
|
||||
"server": CONF.host,
|
||||
"version": kwargs.pop('version', rpcapi.RPC_API_VERSION)
|
||||
}
|
||||
expected_msg = copy.deepcopy(kwargs)
|
||||
|
||||
self.fake_args = None
|
||||
self.fake_kwargs = None
|
||||
|
||||
def _fake_can_send_version_method(version):
|
||||
return messaging_utils.version_is_compatible(
|
||||
rpcapi.RPC_API_VERSION, version)
|
||||
|
||||
def _fake_prepare_method(*args, **kwargs):
|
||||
for kwd in kwargs:
|
||||
self.assertEqual(kwargs[kwd], target[kwd])
|
||||
return rpcapi.client
|
||||
|
||||
def _fake_rpc_method(*args, **kwargs):
|
||||
self.fake_args = args
|
||||
self.fake_kwargs = kwargs
|
||||
if expected_retval:
|
||||
return expected_retval
|
||||
|
||||
with mock.patch.object(rpcapi.client,
|
||||
"can_send_version") as mock_can_send_version:
|
||||
mock_can_send_version.side_effect = _fake_can_send_version_method
|
||||
with mock.patch.object(rpcapi.client, "prepare") as mock_prepared:
|
||||
mock_prepared.side_effect = _fake_prepare_method
|
||||
|
||||
with mock.patch.object(rpcapi.client,
|
||||
rpc_method) as mock_method:
|
||||
mock_method.side_effect = _fake_rpc_method
|
||||
retval = getattr(rpcapi, method)(self.context, **kwargs)
|
||||
self.assertEqual(retval, expected_retval)
|
||||
expected_args = [self.context, method, expected_msg]
|
||||
for arg, expected_arg in zip(self.fake_args,
|
||||
expected_args):
|
||||
self.assertEqual(arg, expected_arg)
|
||||
|
||||
def test_select_destinations(self):
|
||||
self._test_rpcapi('select_destinations',
|
||||
'call',
|
||||
version='1.0',
|
||||
request_spec=None,
|
||||
filter_properties=None)
|
|
@ -1,138 +0,0 @@
|
|||
# Copyright 2016 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 PickledScheduler.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
import six
|
||||
|
||||
from mogan.scheduler import scheduler_options
|
||||
from mogan.tests import base as test
|
||||
|
||||
|
||||
class FakeSchedulerOptions(scheduler_options.SchedulerOptions):
|
||||
def __init__(self, last_checked, now, file_old, file_now, data, filedata):
|
||||
super(FakeSchedulerOptions, self).__init__()
|
||||
# Change internals ...
|
||||
self.last_modified = file_old
|
||||
self.last_checked = last_checked
|
||||
self.data = data
|
||||
|
||||
# For overrides ...
|
||||
self._time_now = now
|
||||
self._file_now = file_now
|
||||
self._file_data = filedata
|
||||
|
||||
self.file_was_loaded = False
|
||||
|
||||
def _get_file_timestamp(self, filename):
|
||||
return self._file_now
|
||||
|
||||
def _get_file_handle(self, filename):
|
||||
self.file_was_loaded = True
|
||||
return six.StringIO(self._file_data)
|
||||
|
||||
def _get_time_now(self):
|
||||
return self._time_now
|
||||
|
||||
|
||||
class SchedulerOptionsTestCase(test.TestCase):
|
||||
def test_get_configuration_first_time_no_flag(self):
|
||||
last_checked = None
|
||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
file_old = None
|
||||
file_now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
|
||||
data = dict(a=1, b=2, c=3)
|
||||
jdata = jsonutils.dumps(data)
|
||||
|
||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
||||
{}, jdata)
|
||||
self.assertEqual({}, fake.get_configuration())
|
||||
self.assertFalse(fake.file_was_loaded)
|
||||
|
||||
def test_get_configuration_first_time_empty_file(self):
|
||||
last_checked = None
|
||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
file_old = None
|
||||
file_now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
|
||||
jdata = ""
|
||||
|
||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
||||
{}, jdata)
|
||||
self.assertEqual({}, fake.get_configuration('foo.json'))
|
||||
self.assertTrue(fake.file_was_loaded)
|
||||
|
||||
def test_get_configuration_first_time_happy_day(self):
|
||||
last_checked = None
|
||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
file_old = None
|
||||
file_now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
|
||||
data = dict(a=1, b=2, c=3)
|
||||
jdata = jsonutils.dumps(data)
|
||||
|
||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
||||
{}, jdata)
|
||||
self.assertEqual(data, fake.get_configuration('foo.json'))
|
||||
self.assertTrue(fake.file_was_loaded)
|
||||
|
||||
def test_get_configuration_second_time_no_change(self):
|
||||
last_checked = datetime.datetime(2011, 1, 1, 1, 1, 1)
|
||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
file_old = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
file_now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
|
||||
data = dict(a=1, b=2, c=3)
|
||||
jdata = jsonutils.dumps(data)
|
||||
|
||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
||||
data, jdata)
|
||||
self.assertEqual(data, fake.get_configuration('foo.json'))
|
||||
self.assertFalse(fake.file_was_loaded)
|
||||
|
||||
def test_get_configuration_second_time_too_fast(self):
|
||||
last_checked = datetime.datetime(2011, 1, 1, 1, 1, 1)
|
||||
now = datetime.datetime(2011, 1, 1, 1, 1, 2)
|
||||
file_old = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
file_now = datetime.datetime(2013, 1, 1, 1, 1, 1)
|
||||
|
||||
old_data = dict(a=1, b=2, c=3)
|
||||
data = dict(a=11, b=12, c=13)
|
||||
jdata = jsonutils.dumps(data)
|
||||
|
||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
||||
old_data, jdata)
|
||||
self.assertEqual(old_data, fake.get_configuration('foo.json'))
|
||||
self.assertFalse(fake.file_was_loaded)
|
||||
|
||||
def test_get_configuration_second_time_change(self):
|
||||
last_checked = datetime.datetime(2011, 1, 1, 1, 1, 1)
|
||||
now = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
file_old = datetime.datetime(2012, 1, 1, 1, 1, 1)
|
||||
file_now = datetime.datetime(2013, 1, 1, 1, 1, 1)
|
||||
|
||||
old_data = dict(a=1, b=2, c=3)
|
||||
data = dict(a=11, b=12, c=13)
|
||||
jdata = jsonutils.dumps(data)
|
||||
|
||||
fake = FakeSchedulerOptions(last_checked, now, file_old, file_now,
|
||||
old_data, jdata)
|
||||
self.assertEqual(data, fake.get_configuration('foo.json'))
|
||||
self.assertTrue(fake.file_was_loaded)
|
|
@ -1,54 +0,0 @@
|
|||
# Copyright 2016 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 Scheduler weights.
|
||||
"""
|
||||
|
||||
from mogan.scheduler import base_weight
|
||||
from mogan.tests import base as test
|
||||
|
||||
|
||||
class TestWeightHandler(test.TestCase):
|
||||
def test_no_multiplier(self):
|
||||
class FakeWeigher(base_weight.BaseWeigher):
|
||||
def _weigh_object(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
self.assertEqual(1.0,
|
||||
FakeWeigher().weight_multiplier())
|
||||
|
||||
def test_no_weight_object(self):
|
||||
class FakeWeigher(base_weight.BaseWeigher):
|
||||
def weight_multiplier(self, *args, **kwargs):
|
||||
pass
|
||||
self.assertRaises(TypeError,
|
||||
FakeWeigher)
|
||||
|
||||
def test_normalization(self):
|
||||
# weight_list, expected_result, minval, maxval
|
||||
map_ = (
|
||||
((), (), None, None),
|
||||
((0.0, 0.0), (0.0, 0.0), None, None),
|
||||
((1.0, 1.0), (0.0, 0.0), None, None),
|
||||
|
||||
((20.0, 50.0), (0.0, 1.0), None, None),
|
||||
((20.0, 50.0), (0.0, 0.375), None, 100.0),
|
||||
((20.0, 50.0), (0.4, 1.0), 0.0, None),
|
||||
((20.0, 50.0), (0.2, 0.5), 0.0, 100.0),
|
||||
)
|
||||
for seq, result, minval, maxval in map_:
|
||||
ret = base_weight.normalize(seq, minval=minval, maxval=maxval)
|
||||
self.assertEqual(result, tuple(ret))
|
|
@ -24,15 +24,6 @@ packages =
|
|||
mogan
|
||||
|
||||
[entry_points]
|
||||
mogan.scheduler.filters =
|
||||
AvailabilityZoneFilter = mogan.scheduler.filters.availability_zone_filter:AvailabilityZoneFilter
|
||||
FlavorFilter = mogan.scheduler.filters.flavor_filter:FlavorFilter
|
||||
CapabilitiesFilter = mogan.scheduler.filters.capabilities_filter:CapabilitiesFilter
|
||||
PortsFilter = mogan.scheduler.filters.ports_filter:PortsFilter
|
||||
JsonFilter = mogan.scheduler.filters.json_filter:JsonFilter
|
||||
mogan.scheduler.weights =
|
||||
PortWeigher = mogan.scheduler.weights.port:PortWeigher
|
||||
|
||||
oslo.config.opts =
|
||||
mogan = mogan.conf.opts:list_opts
|
||||
|
||||
|
|
Loading…
Reference in New Issue