From 492b9f946782fa8d1a348e3c8ffd5ac0e57cc2b1 Mon Sep 17 00:00:00 2001 From: rl001m Date: Sun, 17 Dec 2017 09:26:41 -0500 Subject: Added solver directory to the repository Added the HAS-Solver module in ONAP Change-Id: I5567e3976ad0dc693eb2adc2f479987001b14770 Issue-ID: OPTFRA-14 Signed-off-by: rl001m --- conductor/conductor/solver/__init__.py | 20 ++ conductor/conductor/solver/optimizer/__init__.py | 19 ++ conductor/conductor/solver/optimizer/best_first.py | 163 +++++++++++ .../solver/optimizer/constraints/__init__.py | 19 ++ .../optimizer/constraints/access_distance.py | 111 ++++++++ .../solver/optimizer/constraints/attribute.py | 49 ++++ .../solver/optimizer/constraints/cloud_distance.py | 96 +++++++ .../solver/optimizer/constraints/constraint.py | 50 ++++ .../optimizer/constraints/inventory_group.py | 78 ++++++ .../solver/optimizer/constraints/service.py | 76 +++++ .../conductor/solver/optimizer/constraints/zone.py | 81 ++++++ .../conductor/solver/optimizer/decision_path.py | 55 ++++ conductor/conductor/solver/optimizer/fit_first.py | 160 +++++++++++ conductor/conductor/solver/optimizer/greedy.py | 65 +++++ conductor/conductor/solver/optimizer/optimizer.py | 196 +++++++++++++ .../conductor/solver/optimizer/random_pick.py | 43 +++ conductor/conductor/solver/optimizer/search.py | 90 ++++++ conductor/conductor/solver/request/__init__.py | 19 ++ conductor/conductor/solver/request/demand.py | 49 ++++ .../conductor/solver/request/functions/__init__.py | 19 ++ .../solver/request/functions/cloud_version.py | 27 ++ .../solver/request/functions/distance_between.py | 37 +++ conductor/conductor/solver/request/objective.py | 111 ++++++++ conductor/conductor/solver/request/parser.py | 240 ++++++++++++++++ conductor/conductor/solver/resource/__init__.py | 19 ++ conductor/conductor/solver/resource/region.py | 99 +++++++ conductor/conductor/solver/resource/service.py | 52 ++++ conductor/conductor/solver/service.py | 307 +++++++++++++++++++++ conductor/conductor/solver/simulators/__init__.py | 19 ++ .../solver/simulators/a_and_ai/__init__.py | 19 ++ .../conductor/solver/simulators/valet/__init__.py | 19 ++ conductor/conductor/solver/utils/__init__.py | 19 ++ .../solver/utils/constraint_engine_interface.py | 114 ++++++++ conductor/conductor/solver/utils/utils.py | 56 ++++ 34 files changed, 2596 insertions(+) create mode 100644 conductor/conductor/solver/__init__.py create mode 100755 conductor/conductor/solver/optimizer/__init__.py create mode 100755 conductor/conductor/solver/optimizer/best_first.py create mode 100755 conductor/conductor/solver/optimizer/constraints/__init__.py create mode 100755 conductor/conductor/solver/optimizer/constraints/access_distance.py create mode 100644 conductor/conductor/solver/optimizer/constraints/attribute.py create mode 100755 conductor/conductor/solver/optimizer/constraints/cloud_distance.py create mode 100755 conductor/conductor/solver/optimizer/constraints/constraint.py create mode 100755 conductor/conductor/solver/optimizer/constraints/inventory_group.py create mode 100644 conductor/conductor/solver/optimizer/constraints/service.py create mode 100755 conductor/conductor/solver/optimizer/constraints/zone.py create mode 100755 conductor/conductor/solver/optimizer/decision_path.py create mode 100755 conductor/conductor/solver/optimizer/fit_first.py create mode 100755 conductor/conductor/solver/optimizer/greedy.py create mode 100755 conductor/conductor/solver/optimizer/optimizer.py create mode 100644 conductor/conductor/solver/optimizer/random_pick.py create mode 100755 conductor/conductor/solver/optimizer/search.py create mode 100755 conductor/conductor/solver/request/__init__.py create mode 100755 conductor/conductor/solver/request/demand.py create mode 100755 conductor/conductor/solver/request/functions/__init__.py create mode 100644 conductor/conductor/solver/request/functions/cloud_version.py create mode 100755 conductor/conductor/solver/request/functions/distance_between.py create mode 100755 conductor/conductor/solver/request/objective.py create mode 100755 conductor/conductor/solver/request/parser.py create mode 100755 conductor/conductor/solver/resource/__init__.py create mode 100755 conductor/conductor/solver/resource/region.py create mode 100755 conductor/conductor/solver/resource/service.py create mode 100644 conductor/conductor/solver/service.py create mode 100644 conductor/conductor/solver/simulators/__init__.py create mode 100755 conductor/conductor/solver/simulators/a_and_ai/__init__.py create mode 100755 conductor/conductor/solver/simulators/valet/__init__.py create mode 100755 conductor/conductor/solver/utils/__init__.py create mode 100644 conductor/conductor/solver/utils/constraint_engine_interface.py create mode 100755 conductor/conductor/solver/utils/utils.py (limited to 'conductor') diff --git a/conductor/conductor/solver/__init__.py b/conductor/conductor/solver/__init__.py new file mode 100644 index 0000000..ff501ef --- /dev/null +++ b/conductor/conductor/solver/__init__.py @@ -0,0 +1,20 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 .service import SolverServiceLauncher # noqa: F401 diff --git a/conductor/conductor/solver/optimizer/__init__.py b/conductor/conductor/solver/optimizer/__init__.py new file mode 100755 index 0000000..f2bbdfd --- /dev/null +++ b/conductor/conductor/solver/optimizer/__init__.py @@ -0,0 +1,19 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + diff --git a/conductor/conductor/solver/optimizer/best_first.py b/conductor/conductor/solver/optimizer/best_first.py new file mode 100755 index 0000000..65e435d --- /dev/null +++ b/conductor/conductor/solver/optimizer/best_first.py @@ -0,0 +1,163 @@ +#!/bin/python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 copy +import operator +from oslo_log import log +import sys + +from conductor.solver.optimizer import decision_path as dpath +from conductor.solver.optimizer import search + +LOG = log.getLogger(__name__) + + +class BestFirst(search.Search): + + def __init__(self, conf): + search.Search.__init__(self, conf) + + def search(self, _demand_list, _objective): + dlist = copy.deepcopy(_demand_list) + heuristic_solution = self._search_by_fit_first(dlist, _objective) + if heuristic_solution is None: + LOG.debug("no solution") + return None + + open_list = [] + close_paths = {} + + ''' for the decision length heuristic ''' + # current_decision_length = 0 + + # create root path + decision_path = dpath.DecisionPath() + decision_path.set_decisions({}) + + # insert the root path into open_list + open_list.append(decision_path) + + while len(open_list) > 0: + p = open_list.pop(0) + + ''' for the decision length heuristic ''' + # dl = len(p.decisions) + # if dl >= current_decision_length: + # current_decision_length = dl + # else: + # continue + + # if explored all demands in p, complete the search with p + unexplored_demand = self._get_new_demand(p, _demand_list) + if unexplored_demand is None: + return p + + p.current_demand = unexplored_demand + + msg = "demand = {}, decisions = {}, value = {}" + LOG.debug(msg.format(p.current_demand.name, + p.decision_id, p.total_value)) + + # constraint solving + candidate_list = self._solve_constraints(p) + if len(candidate_list) > 0: + for candidate in candidate_list: + # create path for each candidate for given demand + np = dpath.DecisionPath() + np.set_decisions(p.decisions) + np.decisions[p.current_demand.name] = candidate + _objective.compute(np) + + valid_candidate = True + + # check closeness for this decision + np.set_decision_id(p, candidate.name) + if np.decision_id in close_paths.keys(): + valid_candidate = False + + ''' for base comparison heuristic ''' + # TODO(gjung): how to know this is about min + if _objective.goal == "min": + if np.total_value >= heuristic_solution.total_value: + valid_candidate = False + + if valid_candidate is True: + open_list.append(np) + + # sort open_list by value + open_list.sort(key=operator.attrgetter("total_value")) + else: + LOG.debug("no candidates") + + # insert p into close_paths + close_paths[p.decision_id] = p + + return heuristic_solution + + def _get_new_demand(self, _p, _demand_list): + for demand in _demand_list: + if demand.name not in _p.decisions.keys(): + return demand + + return None + + def _search_by_fit_first(self, _demand_list, _objective): + decision_path = dpath.DecisionPath() + decision_path.set_decisions({}) + + return self._find_current_best(_demand_list, _objective, decision_path) + + def _find_current_best(self, _demand_list, _objective, _decision_path): + if len(_demand_list) == 0: + LOG.debug("search done") + return _decision_path + + demand = _demand_list.pop(0) + LOG.debug("demand = {}".format(demand.name)) + _decision_path.current_demand = demand + candidate_list = self._solve_constraints(_decision_path) + + bound_value = 0.0 + if _objective.goal == "min": + bound_value = sys.float_info.max + + while True: + best_resource = None + for candidate in candidate_list: + _decision_path.decisions[demand.name] = candidate + _objective.compute(_decision_path) + if _objective.goal == "min": + if _decision_path.total_value < bound_value: + bound_value = _decision_path.total_value + best_resource = candidate + + if best_resource is None: + LOG.debug("no resource, rollback") + return None + else: + _decision_path.decisions[demand.name] = best_resource + _decision_path.total_value = bound_value + decision_path = self._find_current_best( + _demand_list, _objective, _decision_path) + if decision_path is None: + candidate_list.remove(best_resource) + else: + return decision_path diff --git a/conductor/conductor/solver/optimizer/constraints/__init__.py b/conductor/conductor/solver/optimizer/constraints/__init__.py new file mode 100755 index 0000000..f2bbdfd --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/__init__.py @@ -0,0 +1,19 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + diff --git a/conductor/conductor/solver/optimizer/constraints/access_distance.py b/conductor/conductor/solver/optimizer/constraints/access_distance.py new file mode 100755 index 0000000..7c400b8 --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/access_distance.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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_log import log + +from conductor.solver.optimizer.constraints import constraint +from conductor.solver.utils import utils + +LOG = log.getLogger(__name__) + + +class AccessDistance(constraint.Constraint): + def __init__(self, _name, _type, _demand_list, _priority=0, + _comparison_operator=operator.le, + _threshold=None, _location=None): + constraint.Constraint.__init__( + self, _name, _type, _demand_list, _priority) + + # The distance threshold for the constraint + self.distance_threshold = _threshold + # The comparison operator from the constraint. + self.comparison_operator = _comparison_operator + # This has to be reference to a function + # from the python operator class + self.location = _location # Location instance + + def solve(self, _decision_path, _candidate_list, _request): + if _candidate_list is None: + LOG.debug("Empty candidate list, need to get " + + "the candidate list for the demand/service") + return _candidate_list + conflict_list = [] + cei = _request.cei + for candidate in _candidate_list: + air_distance = utils.compute_air_distance( + self.location.value, + cei.get_candidate_location(candidate)) + if not self.comparison_operator(air_distance, + self.distance_threshold): + if candidate not in conflict_list: + conflict_list.append(candidate) + + _candidate_list = \ + [c for c in _candidate_list if c not in conflict_list] + # self.distance_threshold + # cei = _request.constraint_engine_interface + # _candidate_list = \ + # [candidate for candidate in _candidate_list if \ + # (self.comparison_operator( + # utils.compute_air_distance(self.location.value, + # cei.get_candidate_location(candidate)), + # self.distance_threshold))] + + # # This section may be relevant ONLY when the candidate list + # # of two demands are identical and we want to optimize the solver + # # to winnow the candidate list of the current demand based on + # # whether this constraint will be met for other demands + # + # # local candidate list + # tmp_candidate_list = copy.deepcopy(_candidate_list) + # for candidate in tmp_candidate_list: + # # TODO(snarayanan): Check if the location type matches + # # the candidate location type + # # if self.location.loc_type != candidate_location.loc_type: + # # LOG.debug("Mismatch in the location types being compared.") + # + # + # satisfies_all_demands = True + # for demand in self.demand_list: + # # Ideally candidate should be in resources for + # # current demand if the candidate list is generated + # # from the demand.resources + # # However, this may not be guaranteed for other demands. + # if candidate not in demand.resources: + # LOG.debug("Candidate not in the demand's resources") + # satisfies_all_demands = False + # break + # + # candidate_location = demand.resources[candidate].location + # + # if not self.comparison_operator(utils.compute_air_distance( + # self.location.value, candidate_location), + # self.distance_threshold): + # # can we assume that the type of candidate_location + # # will be compatible with location.value ? + # satisfies_all_demands = False + # break + # + # if not satisfies_all_demands: + # _candidate_list.remove(candidate) + + return _candidate_list diff --git a/conductor/conductor/solver/optimizer/constraints/attribute.py b/conductor/conductor/solver/optimizer/constraints/attribute.py new file mode 100644 index 0000000..18f9332 --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/attribute.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + + +# python imports + +# Conductor imports +from conductor.solver.optimizer.constraints import constraint + +# Third-party library imports +from oslo_log import log + +LOG = log.getLogger(__name__) + + +class Attribute(constraint.Constraint): + def __init__(self, _name, _type, _demand_list, _priority=0, + _properties=None): + constraint.Constraint.__init__( + self, _name, _type, _demand_list, _priority) + self.properties = _properties + + def solve(self, _decision_path, _candidate_list, _request): + # call conductor engine with request parameters + cei = _request.cei + demand_name = _decision_path.current_demand.name + select_list = cei.get_candidates_by_attributes(demand_name, + _candidate_list, + self.properties) + _candidate_list[:] = \ + [c for c in _candidate_list if c in select_list] + return _candidate_list diff --git a/conductor/conductor/solver/optimizer/constraints/cloud_distance.py b/conductor/conductor/solver/optimizer/constraints/cloud_distance.py new file mode 100755 index 0000000..1e862d4 --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/cloud_distance.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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_log import log + +from conductor.solver.optimizer.constraints import constraint +from conductor.solver.utils import utils + +LOG = log.getLogger(__name__) + + +class CloudDistance(constraint.Constraint): + def __init__(self, _name, _type, _demand_list, _priority=0, + _comparison_operator=operator.le, _threshold=None): + constraint.Constraint.__init__( + self, _name, _type, _demand_list, _priority) + self.distance_threshold = _threshold + self.comparison_operator = _comparison_operator + if len(_demand_list) <= 1: + LOG.debug("Insufficient number of demands.") + raise ValueError + + def solve(self, _decision_path, _candidate_list, _request): + conflict_list = [] + + # get the list of candidates filtered from the previous demand + solved_demands = list() # demands that have been solved in the past + decision_list = list() + future_demands = list() # demands that will be solved in future + + # LOG.debug("initial candidate list {}".format(_candidate_list.name)) + + # find previously made decisions for the constraint's demand list + for demand in self.demand_list: + # decision made for demand + if demand in _decision_path.decisions: + solved_demands.append(demand) + # only one candidate expected per demand in decision path + decision_list.append( + _decision_path.decisions[demand]) + else: # decision will be made in future + future_demands.append(demand) + # placeholder for any optimization we may + # want to do for demands in the constraint's demand + # list that conductor will solve in the future + + # LOG.debug("decisions = {}".format(decision_list)) + + # temp copy to iterate + # temp_candidate_list = copy.deepcopy(_candidate_list) + # for candidate in temp_candidate_list: + for candidate in _candidate_list: + # check if candidate satisfies constraint + # for all relevant decisions thus far + is_candidate = True + for filtered_candidate in decision_list: + cei = _request.cei + if not self.comparison_operator( + utils.compute_air_distance( + cei.get_candidate_location(candidate), + cei.get_candidate_location(filtered_candidate)), + self.distance_threshold): + is_candidate = False + + if not is_candidate: + if candidate not in conflict_list: + conflict_list.append(candidate) + + _candidate_list = \ + [c for c in _candidate_list if c not in conflict_list] + + # msg = "final candidate list for demand {} is " + # LOG.debug(msg.format(_decision_path.current_demand.name)) + # for c in _candidate_list: + # LOG.debug(" " + c.name) + + return _candidate_list diff --git a/conductor/conductor/solver/optimizer/constraints/constraint.py b/conductor/conductor/solver/optimizer/constraints/constraint.py new file mode 100755 index 0000000..03e2c33 --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/constraint.py @@ -0,0 +1,50 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 abc + +from oslo_log import log +import six + +LOG = log.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class Constraint(object): + """Base class for Constraints""" + + def __init__(self, _name, _type, _demand_list, _priority=0): + """Common initializer. + + Be sure to call this superclass when initializing. + """ + self.name = _name + self.constraint_type = _type + self.demand_list = _demand_list + self.check_priority = _priority + + @abc.abstractmethod + def solve(self, _decision_path, _candidate_list, _request): + """Solve. + + Implement the constraint solving in each inherited class, + depending on constraint type. + """ + + return _candidate_list diff --git a/conductor/conductor/solver/optimizer/constraints/inventory_group.py b/conductor/conductor/solver/optimizer/constraints/inventory_group.py new file mode 100755 index 0000000..f0f8089 --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/inventory_group.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 + +from constraint import Constraint + +LOG = log.getLogger(__name__) + + +class InventoryGroup(Constraint): + def __init__(self, _name, _type, _demand_list, _priority=0): + Constraint.__init__(self, _name, _type, _demand_list, _priority) + if not len(self.demand_list) == 2: + LOG.debug("More than two demands in the list") + raise ValueError + + def solve(self, _decision_path, _candidate_list, _request): + + # check if other demand in the demand pair has been already solved + # other demand in pair + other_demand = [d for d in self.demand_list if + d != _decision_path.current_demand.name][0] + if other_demand not in _decision_path.decisions: + LOG.debug("Other demand not yet resolved, " + + "return the current candidates") + return _candidate_list + # expect only one candidate per demand in decision + resolved_candidate = _decision_path.decisions[other_demand] + cei = _request.cei + inventory_group_candidates = cei.get_inventory_group_candidates( + _candidate_list, + _decision_path.current_demand.name, + resolved_candidate) + _candidate_list = [candidate for candidate in _candidate_list if + (candidate in inventory_group_candidates)] + + ''' + # Alternate implementation that *may* be more efficient + # if the decision path has multiple candidates per solved demand + # *and* inventory group is smaller than than the candidate list + + select_list = list() + # get candidates for current demand + current_demand = _decision_path.current_demand + current_candidates = _candidate_list + + # get inventory groups for current demand, + # assuming that group information is tied with demand + inventory_groups = cei.get_inventory_groups(current_demand) + + for group in inventory_groups: + if group[0] in current_candidates and group[1] in other_candidates: + # is the symmetric candidacy valid too ? + if group[0] not in select_list: + select_list.append(group[0]) + _candidate_list[:] = [c for c in _candidate_list if c in select_list] + ''' + + return _candidate_list diff --git a/conductor/conductor/solver/optimizer/constraints/service.py b/conductor/conductor/solver/optimizer/constraints/service.py new file mode 100644 index 0000000..bdbe267 --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/service.py @@ -0,0 +1,76 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 + +from conductor.i18n import _LE +from conductor.solver.optimizer.constraints import constraint + +LOG = log.getLogger(__name__) + + +class Service(constraint.Constraint): + def __init__(self, _name, _type, _demand_list, _priority=0, + _controller=None, _request=None, _cost=None, + _inventory_type=None): + constraint.Constraint.__init__( + self, _name, _type, _demand_list, _priority) + if _controller is None: + LOG.debug("Provider URL not available") + raise ValueError + self.request = _request + self.controller = _controller + self.cost = _cost + self.inventory_type = _inventory_type + + def solve(self, _decision_path, _candidate_list, _request): + select_list = list() + candidates_to_check = list() + demand_name = _decision_path.current_demand.name + # service-check candidates of the same inventory type + # select candidate of all other types + for candidate in _candidate_list: + if self.inventory_type == "cloud": + if candidate["inventory_type"] == "cloud": + candidates_to_check.append(candidate) + else: + select_list.append(candidate) + elif self.inventory_type == "service": + if candidate["inventory_type"] == "service": + candidates_to_check.append(candidate) + else: + select_list.append(candidate) + # call conductor data with request parameters + if len(candidates_to_check) > 0: + cei = _request.cei + filtered_list = cei.get_candidates_from_service( + self.name, self.constraint_type, candidates_to_check, + self.controller, self.inventory_type, self.request, + self.cost, demand_name) + for c in filtered_list: + select_list.append(c) + else: + LOG.error(_LE("Constraint {} ({}) has no candidates of " + "inventory type {} for demand {}").format( + self.name, self.constraint_type, + self.inventory_type, demand_name) + ) + + _candidate_list[:] = [c for c in _candidate_list if c in select_list] + return _candidate_list diff --git a/conductor/conductor/solver/optimizer/constraints/zone.py b/conductor/conductor/solver/optimizer/constraints/zone.py new file mode 100755 index 0000000..c7a968f --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/zone.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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_log import log + +from constraint import Constraint + +LOG = log.getLogger(__name__) + + +class Zone(Constraint): + def __init__(self, _name, _type, _demand_list, _priority=0, + _qualifier=None, _category=None): + Constraint.__init__(self, _name, _type, _demand_list, _priority) + + self.qualifier = _qualifier # different or same + self.category = _category # disaster, region, or update + self.comparison_operator = None + + if self.qualifier == "same": + self.comparison_operator = operator.eq + elif self.qualifier == "different": + self.comparison_operator = operator.ne + + def solve(self, _decision_path, _candidate_list, _request): + conflict_list = [] + + decision_list = list() + # find previously made decisions for the constraint's demand list + for demand in self.demand_list: + # decision made for demand + if demand in _decision_path.decisions: + decision_list.append(_decision_path.decisions[demand]) + # temp copy to iterate + # temp_candidate_list = copy.deepcopy(_candidate_list) + # for candidate in temp_candidate_list: + for candidate in _candidate_list: + # check if candidate satisfies constraint + # for all relevant decisions thus far + is_candidate = True + for filtered_candidate in decision_list: + cei = _request.cei + if not self.comparison_operator( + cei.get_candidate_zone(candidate, self.category), + cei.get_candidate_zone(filtered_candidate, + self.category)): + is_candidate = False + + if not is_candidate: + if candidate not in conflict_list: + conflict_list.append(candidate) + # _candidate_list.remove(candidate) + + _candidate_list[:] =\ + [c for c in _candidate_list if c not in conflict_list] + + # msg = "final candidate list for demand {} is " + # LOG.debug(msg.format(_decision_path.current_demand.name)) + # for c in _candidate_list: + # print " " + c.name + + return _candidate_list diff --git a/conductor/conductor/solver/optimizer/decision_path.py b/conductor/conductor/solver/optimizer/decision_path.py new file mode 100755 index 0000000..0890f52 --- /dev/null +++ b/conductor/conductor/solver/optimizer/decision_path.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 copy + + +class DecisionPath(object): + + def __init__(self): + """local copy of decisions so far + + key = demand.name, value = region or service instance + """ + + self.decisions = None + + ''' to identify this decision path in the search ''' + self.decision_id = "" + + ''' current demand to be dealt with''' + self.current_demand = None + + ''' decision values so far ''' + self.cumulated_value = 0.0 + self.cumulated_cost = 0.0 + self.heuristic_to_go_value = 0.0 + self.heuristic_to_go_cost = 0.0 + # cumulated_value + heuristic_to_go_value (if exist) + self.total_value = 0.0 + # cumulated_cost + heuristic_to_go_cost (if exist) + self.total_cost = 0.0 + + def set_decisions(self, _prior_decisions): + self.decisions = copy.deepcopy(_prior_decisions) + + def set_decision_id(self, _dk, _rk): + self.decision_id += (str(_dk) + ":" + str(_rk) + ">") diff --git a/conductor/conductor/solver/optimizer/fit_first.py b/conductor/conductor/solver/optimizer/fit_first.py new file mode 100755 index 0000000..42d8fed --- /dev/null +++ b/conductor/conductor/solver/optimizer/fit_first.py @@ -0,0 +1,160 @@ +#!/bin/python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 +import sys + +from conductor.solver.optimizer import decision_path as dpath +from conductor.solver.optimizer import search + +LOG = log.getLogger(__name__) + + +class FitFirst(search.Search): + + def __init__(self, conf): + search.Search.__init__(self, conf) + + def search(self, _demand_list, _objective, _request): + decision_path = dpath.DecisionPath() + decision_path.set_decisions({}) + + # Begin the recursive serarch + return self._find_current_best( + _demand_list, _objective, decision_path, _request) + + def _find_current_best(self, _demand_list, _objective, + _decision_path, _request): + # _demand_list is common across all recursions + if len(_demand_list) == 0: + LOG.debug("search done") + return _decision_path + + # get next demand to resolve + demand = _demand_list.pop(0) + LOG.debug("demand = {}".format(demand.name)) + _decision_path.current_demand = demand + + # call constraints to whittle initial candidates + # candidate_list meets all constraints for the demand + candidate_list = self._solve_constraints(_decision_path, _request) + + # find the best candidate among the list + + # bound_value keeps track of the max value discovered + # thus far for the _decision_path. For every demand + # added to the _decision_path bound_value will be set + # to a really large value to begin with + bound_value = 0.0 + version_value = "0.0" + + if "min" in _objective.goal: + bound_value = sys.float_info.max + + # Start recursive search + while True: + best_resource = None + # Find best candidate that optimizes the cost for demand. + # The candidate list can be empty if the constraints + # rule out all candidates + for candidate in candidate_list: + _decision_path.decisions[demand.name] = candidate + _objective.compute(_decision_path, _request) + # this will set the total_value of the _decision_path + # thus far up to the demand + if _objective.goal is None: + best_resource = candidate + + elif _objective.goal == "min_cloud_version": + # convert the unicode to string + candidate_version = candidate \ + .get("cloud_region_version").encode('utf-8') + if _decision_path.total_value < bound_value or \ + (_decision_path.total_value == bound_value and + self._compare_version(candidate_version, + version_value) > 0): + bound_value = _decision_path.total_value + version_value = candidate_version + best_resource = candidate + + elif _objective.goal == "min": + # if the path value is less than bound value + # we have found the better candidate + if _decision_path.total_value < bound_value: + # relax the bound_value to the value of + # the path - this will ensure a future + # candidate will be picked only if it has + # a value lesser than the current best candidate + bound_value = _decision_path.total_value + best_resource = candidate + + # Rollback if we don't have any candidate picked for + # the demand. + if best_resource is None: + LOG.debug("no resource, rollback") + # Put the current demand (which failed to find a + # candidate) back in the list so that it can be picked + # up in the next iteration of the recursion + _demand_list.insert(0, demand) + return None # return None back to the recursion + else: + # best resource is found, add to the decision path + _decision_path.decisions[demand.name] = best_resource + _decision_path.total_value = bound_value + + # Begin the next recursive call to find candidate + # for the next demand in the list + decision_path = self._find_current_best( + _demand_list, _objective, _decision_path, _request) + + # The point of return from the previous recursion. + # If the call returns no candidates, no solution exists + # in that path of the decision tree. Rollback the + # current best_resource and remove it from the list + # of potential candidates. + if decision_path is None: + candidate_list.remove(best_resource) + # reset bound_value to a large value so that + # the next iteration of the current recursion + # will pick the next best candidate, which + # will have a value larger than the current + # bound_value (proof by contradiction: + # it cannot have a smaller value, if it wasn't + # the best_resource. + if _objective.goal == "min": + bound_value = sys.float_info.max + else: + # A candidate was found for the demand, and + # was added to the decision path. Return current + # path back to the recursion. + return decision_path + + def _compare_version(self, version1, version2): + version1 = version1.split('.') + version2 = version2.split('.') + for i in range(max(len(version1), len(version2))): + v1 = int(version1[i]) if i < len(version1) else 0 + v2 = int(version2[i]) if i < len(version2) else 0 + if v1 > v2: + return 1 + elif v1 < v2: + return -1 + return 0 diff --git a/conductor/conductor/solver/optimizer/greedy.py b/conductor/conductor/solver/optimizer/greedy.py new file mode 100755 index 0000000..eae1b12 --- /dev/null +++ b/conductor/conductor/solver/optimizer/greedy.py @@ -0,0 +1,65 @@ +#!/bin/python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 +import sys + +from conductor.solver.optimizer import decision_path as dpath +from conductor.solver.optimizer import search + +LOG = log.getLogger(__name__) + + +class Greedy(search.Search): + + def __init__(self, conf): + search.Search.__init__(self, conf) + + def search(self, _demand_list, _objective): + decision_path = dpath.DecisionPath() + decision_path.set_decisions({}) + + for demand in _demand_list: + LOG.debug("demand = {}".format(demand.name)) + + decision_path.current_demand = demand + candidate_list = self._solve_constraints(decision_path) + + bound_value = 0.0 + if _objective.goal == "min": + bound_value = sys.float_info.max + + best_resource = None + for candidate in candidate_list: + decision_path.decisions[demand.name] = candidate + _objective.compute(decision_path) + if _objective.goal == "min": + if decision_path.total_value < bound_value: + bound_value = decision_path.total_value + best_resource = candidate + + if best_resource is not None: + decision_path.decisions[demand.name] = best_resource + decision_path.total_value = bound_value + else: + return None + + return decision_path diff --git a/conductor/conductor/solver/optimizer/optimizer.py b/conductor/conductor/solver/optimizer/optimizer.py new file mode 100755 index 0000000..c7155c4 --- /dev/null +++ b/conductor/conductor/solver/optimizer/optimizer.py @@ -0,0 +1,196 @@ +#!/bin/python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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_config import cfg +from oslo_log import log +import time + +from conductor import service +# from conductor.solver.optimizer import decision_path as dpath +# from conductor.solver.optimizer import best_first +# from conductor.solver.optimizer import greedy +from conductor.solver.optimizer import fit_first +from conductor.solver.optimizer import random_pick +from conductor.solver.request import demand + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + +SOLVER_OPTS = [ + +] + +CONF.register_opts(SOLVER_OPTS, group='solver') + + +class Optimizer(object): + + # FIXME(gjung): _requests should be request (no underscore, one item) + def __init__(self, conf, _requests=None): + self.conf = conf + + # self.search = greedy.Greedy(self.conf) + self.search = None + # self.search = best_first.BestFirst(self.conf) + + if _requests is not None: + self.requests = _requests + + def get_solution(self): + LOG.debug("search start") + + for rk in self.requests: + request = self.requests[rk] + LOG.debug("--- request = {}".format(rk)) + + LOG.debug("1. sort demands") + demand_list = self._sort_demands(request) + + for d in demand_list: + LOG.debug(" demand = {}".format(d.name)) + + LOG.debug("2. search") + st = time.time() + + if not request.objective.goal: + LOG.debug("No objective function is provided. " + "Random pick algorithm is used") + self.search = random_pick.RandomPick(self.conf) + best_path = self.search.search(demand_list, request) + else: + LOG.debug("Fit first algorithm is used") + self.search = fit_first.FitFirst(self.conf) + best_path = self.search.search(demand_list, + request.objective, request) + + if best_path is not None: + self.search.print_decisions(best_path) + else: + LOG.debug("no solution found") + LOG.debug("search delay = {} sec".format(time.time() - st)) + return best_path + + def _sort_demands(self, _request): + demand_list = [] + + # first, find loc-demand dependencies + # using constraints and objective functions + open_demand_list = [] + for key in _request.constraints: + c = _request.constraints[key] + if c.constraint_type == "distance_to_location": + for dk in c.demand_list: + if _request.demands[dk].sort_base != 1: + _request.demands[dk].sort_base = 1 + open_demand_list.append(_request.demands[dk]) + for op in _request.objective.operand_list: + if op.function.func_type == "distance_between": + if isinstance(op.function.loc_a, demand.Location): + if _request.demands[op.function.loc_z.name].sort_base != 1: + _request.demands[op.function.loc_z.name].sort_base = 1 + open_demand_list.append(op.function.loc_z) + elif isinstance(op.function.loc_z, demand.Location): + if _request.demands[op.function.loc_a.name].sort_base != 1: + _request.demands[op.function.loc_a.name].sort_base = 1 + open_demand_list.append(op.function.loc_a) + + if len(open_demand_list) == 0: + init_demand = self._exist_not_sorted_demand(_request.demands) + open_demand_list.append(init_demand) + + # second, find demand-demand dependencies + while True: + d_list = self._get_depended_demands(open_demand_list, _request) + for d in d_list: + demand_list.append(d) + + init_demand = self._exist_not_sorted_demand(_request.demands) + if init_demand is None: + break + open_demand_list.append(init_demand) + + return demand_list + + def _get_depended_demands(self, _open_demand_list, _request): + demand_list = [] + + while True: + if len(_open_demand_list) == 0: + break + + d = _open_demand_list.pop(0) + if d.sort_base != 1: + d.sort_base = 1 + demand_list.append(d) + + for key in _request.constraints: + c = _request.constraints[key] + if c.constraint_type == "distance_between_demands": + if d.name in c.demand_list: + for dk in c.demand_list: + if dk != d.name and \ + _request.demands[dk].sort_base != 1: + _request.demands[dk].sort_base = 1 + _open_demand_list.append( + _request.demands[dk]) + + for op in _request.objective.operand_list: + if op.function.func_type == "distance_between": + if op.function.loc_a.name == d.name: + if op.function.loc_z.name in \ + _request.demands.keys(): + if _request.demands[ + op.function.loc_z.name].sort_base != 1: + _request.demands[ + op.function.loc_z.name].sort_base = 1 + _open_demand_list.append(op.function.loc_z) + elif op.function.loc_z.name == d.name: + if op.function.loc_a.name in \ + _request.demands.keys(): + if _request.demands[ + op.function.loc_a.name].sort_base != 1: + _request.demands[ + op.function.loc_a.name].sort_base = 1 + _open_demand_list.append(op.function.loc_a) + + return demand_list + + def _exist_not_sorted_demand(self, _demands): + not_sorted_demand = None + for key in _demands: + demand = _demands[key] + if demand.sort_base != 1: + not_sorted_demand = demand + break + return not_sorted_demand + + +# Used for testing. This file is in .gitignore and will NOT be checked in. +CONFIG_FILE = '' + +''' for unit test ''' +if __name__ == "__main__": + # Prepare service-wide components (e.g., config) + conf = service.prepare_service([], config_files=[CONFIG_FILE]) + + opt = Optimizer(conf) + opt.get_solution() diff --git a/conductor/conductor/solver/optimizer/random_pick.py b/conductor/conductor/solver/optimizer/random_pick.py new file mode 100644 index 0000000..2896757 --- /dev/null +++ b/conductor/conductor/solver/optimizer/random_pick.py @@ -0,0 +1,43 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 + +from conductor.solver.optimizer import decision_path as dpath +from conductor.solver.optimizer import search +from random import randint + +LOG = log.getLogger(__name__) + + +class RandomPick(search.Search): + def __init__(self, conf): + search.Search.__init__(self, conf) + + def search(self, _demand_list, _request): + decision_path = dpath.DecisionPath() + decision_path.set_decisions({}) + return self._find_current_best(_demand_list, decision_path, _request) + + def _find_current_best(self, _demand_list, _decision_path, _request): + for demand in _demand_list: + r_index = randint(0, len(demand.resources) - 1) + best_resource = demand.resources[demand.resources.keys()[r_index]] + _decision_path.decisions[demand.name] = best_resource + return _decision_path diff --git a/conductor/conductor/solver/optimizer/search.py b/conductor/conductor/solver/optimizer/search.py new file mode 100755 index 0000000..9d138e4 --- /dev/null +++ b/conductor/conductor/solver/optimizer/search.py @@ -0,0 +1,90 @@ +#!/bin/python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 operator import itemgetter +from oslo_log import log + +from conductor.solver.optimizer import decision_path as dpath + +LOG = log.getLogger(__name__) + + +class Search(object): + + def __init__(self, conf): + self.conf = conf + + def search(self, _demand_list, _objective): + decision_path = dpath.DecisionPath() + decision_path.set_decisions({}) + + ''' implement search algorithm ''' + + return decision_path + + def _solve_constraints(self, _decision_path, _request): + candidate_list = [] + for key in _decision_path.current_demand.resources: + resource = _decision_path.current_demand.resources[key] + candidate_list.append(resource) + + for constraint in _decision_path.current_demand.constraint_list: + LOG.debug("Evaluating constraint = {}".format(constraint.name)) + LOG.debug("Available candidates before solving " + "constraint {}".format(candidate_list)) + + candidate_list =\ + constraint.solve(_decision_path, candidate_list, _request) + LOG.debug("Available candidates after solving " + "constraint {}".format(candidate_list)) + if len(candidate_list) == 0: + LOG.error("No candidates found for demand {} " + "when constraint {} was evaluated " + "".format(_decision_path.current_demand, + constraint.name) + ) + break + + if len(candidate_list) > 0: + self._set_candidate_cost(candidate_list) + + return candidate_list + + def _set_candidate_cost(self, _candidate_list): + for c in _candidate_list: + if c["inventory_type"] == "service": + c["cost"] = "1" + else: + c["cost"] = "2" + _candidate_list[:] = sorted(_candidate_list, key=itemgetter("cost")) + + def print_decisions(self, _best_path): + if _best_path: + msg = "--- demand = {}, chosen resource = {} at {}" + for demand_name in _best_path.decisions: + resource = _best_path.decisions[demand_name] + LOG.debug(msg.format(demand_name, resource["candidate_id"], + resource["location_id"])) + + msg = "--- total value of decision = {}" + LOG.debug(msg.format(_best_path.total_value)) + msg = "--- total cost of decision = {}" + LOG.debug(msg.format(_best_path.total_cost)) diff --git a/conductor/conductor/solver/request/__init__.py b/conductor/conductor/solver/request/__init__.py new file mode 100755 index 0000000..f2bbdfd --- /dev/null +++ b/conductor/conductor/solver/request/__init__.py @@ -0,0 +1,19 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + diff --git a/conductor/conductor/solver/request/demand.py b/conductor/conductor/solver/request/demand.py new file mode 100755 index 0000000..5554cfe --- /dev/null +++ b/conductor/conductor/solver/request/demand.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + + + +class Demand(object): + + def __init__(self, _name=None): + self.name = _name + + # initial candidates (regions or services) for this demand + # key = region_id (or service_id), + # value = region (or service) instance + self.resources = {} + + # applicable constraint checkers + # a list of constraint instances to be applied + self.constraint_list = [] + + # to sort demands in the optimization process + self.sort_base = -1 + + +class Location(object): + + def __init__(self, _name=None): + self.name = _name + # clli, coordinates, or placemark + self.loc_type = None + + # depending on type + self.value = None diff --git a/conductor/conductor/solver/request/functions/__init__.py b/conductor/conductor/solver/request/functions/__init__.py new file mode 100755 index 0000000..f2bbdfd --- /dev/null +++ b/conductor/conductor/solver/request/functions/__init__.py @@ -0,0 +1,19 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + diff --git a/conductor/conductor/solver/request/functions/cloud_version.py b/conductor/conductor/solver/request/functions/cloud_version.py new file mode 100644 index 0000000..564468b --- /dev/null +++ b/conductor/conductor/solver/request/functions/cloud_version.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + + + +class CloudVersion(object): + + def __init__(self, _type): + self.func_type = _type + self.loc = None diff --git a/conductor/conductor/solver/request/functions/distance_between.py b/conductor/conductor/solver/request/functions/distance_between.py new file mode 100755 index 0000000..8cf3f86 --- /dev/null +++ b/conductor/conductor/solver/request/functions/distance_between.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 conductor.solver.utils import utils + + +class DistanceBetween(object): + + def __init__(self, _type): + self.func_type = _type + + self.loc_a = None + self.loc_z = None + + def compute(self, _loc_a, _loc_z): + distance = utils.compute_air_distance(_loc_a, _loc_z) + + return distance diff --git a/conductor/conductor/solver/request/objective.py b/conductor/conductor/solver/request/objective.py new file mode 100755 index 0000000..ca1e614 --- /dev/null +++ b/conductor/conductor/solver/request/objective.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 conductor.solver.request import demand +# from conductor.solver.resource import region +# from conductor.solver.resource import service + + +class Objective(object): + + def __init__(self): + self.goal = None + self.operation = None + self.operand_list = [] + + def compute(self, _decision_path, _request): + value = 0.0 + + for op in self.operand_list: + if self.operation == "sum": + value += op.compute(_decision_path, _request) + + _decision_path.cumulated_value = value + _decision_path.total_value = \ + _decision_path.cumulated_value + \ + _decision_path.heuristic_to_go_value + + +class Operand(object): + + def __init__(self): + self.operation = None + self.weight = 0 + self.function = None + + def compute(self, _decision_path, _request): + value = 0.0 + cei = _request.cei + if self.function.func_type == "distance_between": + if isinstance(self.function.loc_a, demand.Location): + if self.function.loc_z.name in \ + _decision_path.decisions.keys(): + resource = \ + _decision_path.decisions[self.function.loc_z.name] + loc = None + # if isinstance(resource, region.Region): + # loc = resource.location + # elif isinstance(resource, service.Service): + # loc = resource.region.location + loc = cei.get_candidate_location(resource) + value = \ + self.function.compute(self.function.loc_a.value, loc) + elif isinstance(self.function.loc_z, demand.Location): + if self.function.loc_a.name in \ + _decision_path.decisions.keys(): + resource = \ + _decision_path.decisions[self.function.loc_a.name] + loc = None + # if isinstance(resource, region.Region): + # loc = resource.location + # elif isinstance(resource, service.Service): + # loc = resource.region.location + loc = cei.get_candidate_location(resource) + value = \ + self.function.compute(self.function.loc_z.value, loc) + else: + if self.function.loc_a.name in \ + _decision_path.decisions.keys() and \ + self.function.loc_z.name in \ + _decision_path.decisions.keys(): + resource_a = \ + _decision_path.decisions[self.function.loc_a.name] + loc_a = None + # if isinstance(resource_a, region.Region): + # loc_a = resource_a.location + # elif isinstance(resource_a, service.Service): + # loc_a = resource_a.region.location + loc_a = cei.get_candidate_location(resource_a) + resource_z = \ + _decision_path.decisions[self.function.loc_z.name] + loc_z = None + # if isinstance(resource_z, region.Region): + # loc_z = resource_z.location + # elif isinstance(resource_z, service.Service): + # loc_z = resource_z.region.location + loc_z = cei.get_candidate_location(resource_z) + + value = self.function.compute(loc_a, loc_z) + + if self.operation == "product": + value *= self.weight + + return value diff --git a/conductor/conductor/solver/request/parser.py b/conductor/conductor/solver/request/parser.py new file mode 100755 index 0000000..6e30549 --- /dev/null +++ b/conductor/conductor/solver/request/parser.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 json +import operator +from oslo_log import log +import random +# import sys + +from conductor.solver.optimizer.constraints \ + import access_distance as access_dist +from conductor.solver.optimizer.constraints \ + import cloud_distance as cloud_dist +from conductor.solver.optimizer.constraints \ + import attribute as attribute_constraint +# from conductor.solver.optimizer.constraints import constraint +from conductor.solver.optimizer.constraints \ + import inventory_group +from conductor.solver.optimizer.constraints \ + import service as service_constraint +from conductor.solver.optimizer.constraints import zone +from conductor.solver.request import demand +from conductor.solver.request.functions import cloud_version +from conductor.solver.request.functions import distance_between +from conductor.solver.request import objective + +# from conductor.solver.request.functions import distance_between +# from conductor.solver.request import objective +# from conductor.solver.resource import region +# from conductor.solver.resource import service +# from conductor.solver.utils import constraint_engine_interface as cei +# from conductor.solver.utils import utils + +LOG = log.getLogger(__name__) + + +# FIXME(snarayanan): This is really a SolverRequest (or Request) object +class Parser(object): + + def __init__(self, _region_gen=None): + self.demands = {} + self.locations = {} + self.region_gen = _region_gen + self.constraints = {} + self.objective = None + self.cei = None + self.request_id = None + + # def get_data_engine_interface(self): + # self.cei = cei.ConstraintEngineInterface() + + # FIXME(snarayanan): This should just be parse_template + def parse_template(self, json_template=None): + if json_template is None: + LOG.error("No template specified") + return "Error" + + # get demands + demand_list = json_template["conductor_solver"]["demands"] + for demand_id, candidate_list in demand_list.items(): + current_demand = demand.Demand(demand_id) + # candidate should only have minimal information like location_id + for candidate in candidate_list["candidates"]: + candidate_id = candidate["candidate_id"] + current_demand.resources[candidate_id] = candidate + current_demand.sort_base = 0 # this is only for testing + self.demands[demand_id] = current_demand + + # get locations + location_list = json_template["conductor_solver"]["locations"] + for location_id, location_info in location_list.items(): + loc = demand.Location(location_id) + loc.loc_type = "coordinates" + loc.value = (float(location_info["latitude"]), + float(location_info["longitude"])) + self.locations[location_id] = loc + + # get constraints + input_constraints = json_template["conductor_solver"]["constraints"] + for constraint_id, constraint_info in input_constraints.items(): + constraint_type = constraint_info["type"] + constraint_demands = list() + parsed_demands = constraint_info["demands"] + if isinstance(parsed_demands, list): + for d in parsed_demands: + constraint_demands.append(d) + else: + constraint_demands.append(parsed_demands) + if constraint_type == "distance_to_location": + c_property = constraint_info.get("properties") + location_id = c_property.get("location") + op = operator.le # default operator + c_op = c_property.get("distance").get("operator") + if c_op == ">": + op = operator.gt + elif c_op == ">=": + op = operator.ge + elif c_op == "<": + op = operator.lt + elif c_op == "<=": + op = operator.le + elif c_op == "=": + op = operator.eq + dist_value = c_property.get("distance").get("value") + my_access_distance_constraint = access_dist.AccessDistance( + constraint_id, constraint_type, constraint_demands, + _comparison_operator=op, _threshold=dist_value, + _location=self.locations[location_id]) + self.constraints[my_access_distance_constraint.name] = \ + my_access_distance_constraint + elif constraint_type == "distance_between_demands": + c_property = constraint_info.get("properties") + op = operator.le # default operator + c_op = c_property.get("distance").get("operator") + if c_op == ">": + op = operator.gt + elif c_op == ">=": + op = operator.ge + elif c_op == "<": + op = operator.lt + elif c_op == "<=": + op = operator.le + elif c_op == "=": + op = operator.eq + dist_value = c_property.get("distance").get("value") + my_cloud_distance_constraint = cloud_dist.CloudDistance( + constraint_id, constraint_type, constraint_demands, + _comparison_operator=op, _threshold=dist_value) + self.constraints[my_cloud_distance_constraint.name] = \ + my_cloud_distance_constraint + elif constraint_type == "inventory_group": + my_inventory_group_constraint = \ + inventory_group.InventoryGroup( + constraint_id, constraint_type, constraint_demands) + self.constraints[my_inventory_group_constraint.name] = \ + my_inventory_group_constraint + elif constraint_type == "region_fit": + c_property = constraint_info.get("properties") + controller = c_property.get("controller") + request = c_property.get("request") + # inventory type is cloud for region_fit + inventory_type = "cloud" + my_service_constraint = service_constraint.Service( + constraint_id, constraint_type, constraint_demands, + _controller=controller, _request=request, _cost=None, + _inventory_type=inventory_type) + self.constraints[my_service_constraint.name] = \ + my_service_constraint + elif constraint_type == "instance_fit": + c_property = constraint_info.get("properties") + controller = c_property.get("controller") + request = c_property.get("request") + # inventory type is service for instance_fit + inventory_type = "service" + my_service_constraint = service_constraint.Service( + constraint_id, constraint_type, constraint_demands, + _controller=controller, _request=request, _cost=None, + _inventory_type=inventory_type) + self.constraints[my_service_constraint.name] = \ + my_service_constraint + elif constraint_type == "zone": + c_property = constraint_info.get("properties") + qualifier = c_property.get("qualifier") + category = c_property.get("category") + my_zone_constraint = zone.Zone( + constraint_id, constraint_type, constraint_demands, + _qualifier=qualifier, _category=category) + self.constraints[my_zone_constraint.name] = my_zone_constraint + elif constraint_type == "attribute": + c_property = constraint_info.get("properties") + my_attribute_constraint = \ + attribute_constraint.Attribute(constraint_id, + constraint_type, + constraint_demands, + _properties=c_property) + self.constraints[my_attribute_constraint.name] = \ + my_attribute_constraint + else: + LOG.error("unknown constraint type {}".format(constraint_type)) + return + + # get objective function + if "objective" not in json_template["conductor_solver"]\ + or not json_template["conductor_solver"]["objective"]: + self.objective = objective.Objective() + else: + input_objective = json_template["conductor_solver"]["objective"] + self.objective = objective.Objective() + self.objective.goal = input_objective["goal"] + self.objective.operation = input_objective["operation"] + for operand_data in input_objective["operands"]: + operand = objective.Operand() + operand.operation = operand_data["operation"] + operand.weight = float(operand_data["weight"]) + if operand_data["function"] == "distance_between": + func = distance_between.DistanceBetween("distance_between") + param = operand_data["function_param"][0] + if param in self.locations: + func.loc_a = self.locations[param] + elif param in self.demands: + func.loc_a = self.demands[param] + param = operand_data["function_param"][1] + if param in self.locations: + func.loc_z = self.locations[param] + elif param in self.demands: + func.loc_z = self.demands[param] + operand.function = func + elif operand_data["function"] == "cloud_version": + self.objective.goal = "min_cloud_version" + func = cloud_version.CloudVersion("cloud_version") + func.loc = operand_data["function_param"] + operand.function = func + + self.objective.operand_list.append(operand) + + def map_constraints_to_demands(self): + # spread the constraints over the demands + for constraint_name, constraint in self.constraints.items(): + for d in constraint.demand_list: + if d in self.demands.keys(): + self.demands[d].constraint_list.append(constraint) + diff --git a/conductor/conductor/solver/resource/__init__.py b/conductor/conductor/solver/resource/__init__.py new file mode 100755 index 0000000..f2bbdfd --- /dev/null +++ b/conductor/conductor/solver/resource/__init__.py @@ -0,0 +1,19 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + diff --git a/conductor/conductor/solver/resource/region.py b/conductor/conductor/solver/resource/region.py new file mode 100755 index 0000000..fc42bd1 --- /dev/null +++ b/conductor/conductor/solver/resource/region.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + + +"""Cloud region""" + + +class Region(object): + + def __init__(self, _rid=None): + self.name = _rid + + self.status = "active" + + '''general region properties''' + # S (i.e., medium_lite), M (i.e., medium), or L (i.e., large) + self.region_type = None + # (latitude, longitude) + self.location = None + + ''' + placemark: + + country_code (e.g., US), + postal_code (e.g., 07920), + administrative_area (e.g., NJ), + sub_administrative_area (e.g., Somerset), + locality (e.g., Bedminster), + thoroughfare (e.g., AT&T Way), + sub_thoroughfare (e.g., 1) + ''' + self.address = {} + + self.zones = {} # Zone instances (e.g., disaster and/or update) + self.cost = 0.0 + + '''abstracted resource capacity status''' + self.capacity = {} + + self.allocated_demand_list = [] + + '''other opaque metadata such as cloud_version, sriov, etc.''' + self.properties = {} + + '''known neighbor regions to be used for constraint solving''' + self.neighbor_list = [] # a list of Link instances + + self.last_update = 0 + + '''update resource capacity after allocating demand''' + def update_capacity(self): + pass + + '''for logging''' + def get_json_summary(self): + pass + + +class Zone(object): + + def __init__(self, _zid=None): + self.name = _zid + self.zone_type = None # disaster or update + + self.region_list = [] # a list of region names + + def get_json_summary(self): + pass + + +class Link(object): + + def __init__(self, _region_name): + self.destination_region_name = _region_name + + self.distance = 0.0 + self.nw_distance = 0.0 + self.latency = 0.0 + self.bandwidth = 0.0 + + def get_json_summary(self): + pass diff --git a/conductor/conductor/solver/resource/service.py b/conductor/conductor/solver/resource/service.py new file mode 100755 index 0000000..faedb53 --- /dev/null +++ b/conductor/conductor/solver/resource/service.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + + +"""Existing service instance in a region""" + + +class Service(object): + + def __init__(self, _sid=None): + self.name = _sid + + self.region = None + + self.status = "active" + + self.cost = 0.0 + + """abstracted resource capacity status""" + self.capacity = {} + + self.allocated_demand_list = [] + + """other opaque metadata if necessary""" + self.properties = {} + + self.last_update = 0 + + """update resource capacity after allocating demand""" + def update_capacity(self): + pass + + """for logging""" + def get_json_summary(self): + pass diff --git a/conductor/conductor/solver/service.py b/conductor/conductor/solver/service.py new file mode 100644 index 0000000..60aa092 --- /dev/null +++ b/conductor/conductor/solver/service.py @@ -0,0 +1,307 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 cotyledon +from oslo_config import cfg +from oslo_log import log + +from conductor.common.models import plan +from conductor.common.music import api +from conductor.common.music import messaging as music_messaging +from conductor.common.music.model import base +from conductor.i18n import _LE, _LI +from conductor import messaging +from conductor import service +from conductor.solver.optimizer import optimizer +from conductor.solver.request import parser +from conductor.solver.utils import constraint_engine_interface as cei + + +# To use oslo.log in services: +# +# 0. Note that conductor.service.prepare_service() bootstraps this. +# It's set up within conductor.cmd.SERVICENAME. +# 1. Add "from oslo_log import log" +# 2. Also add "LOG = log.getLogger(__name__)" +# 3. For i18n support, import appropriate shortcuts as well: +# "from i18n import _, _LC, _LE, _LI, _LW # noqa" +# (that's for primary, critical, error, info, warning) +# 4. Use LOG.info, LOG.warning, LOG.error, LOG.critical, LOG.debug, e.g.: +# "LOG.info(_LI("Something happened with {}").format(thingie))" +# 5. Do NOT put translation wrappers around any LOG.debug text. +# 6. Be liberal with logging, especially in the absence of unit tests! +# 7. Calls to print() are verboten within the service proper. +# Logging can be redirected! (In a CLI-side script, print() is fine.) +# +# Usage: http://docs.openstack.org/developer/oslo.i18n/usage.html + +LOG = log.getLogger(__name__) + +# To use oslo.config in services: +# +# 0. Note that conductor.service.prepare_service() bootstraps this. +# It's set up within conductor.cmd.SERVICENAME. +# 1. Add "from oslo_config import cfg" +# 2. Also add "CONF = cfg.CONF" +# 3. Set a list of locally used options (SOLVER_OPTS is fine). +# Choose key names thoughtfully. Be technology-agnostic, avoid TLAs, etc. +# 4. Register, e.g. "CONF.register_opts(SOLVER_OPTS, group='solver')" +# 5. Add file reference to opts.py (may need to use itertools.chain()) +# 6. Run tox -e genconfig to build a new config template. +# 7. If you want to load an entire config from a CLI you can do this: +# "conf = service.prepare_service([], config_files=[CONFIG_FILE])" +# 8. You can even use oslo_config from a CLI and override values on the fly, +# e.g., "CONF.set_override('hostnames', ['music2'], 'music_api')" +# (leave the third arg out to use the DEFAULT group). +# 9. Loading a config from a CLI is optional. So long as all the options +# have defaults (or you override them as needed), it should all work. +# +# Docs: http://docs.openstack.org/developer/oslo.config/ + +CONF = cfg.CONF + +SOLVER_OPTS = [ + cfg.IntOpt('workers', + default=1, + min=1, + help='Number of workers for solver service. ' + 'Default value is 1.'), + cfg.BoolOpt('concurrent', + default=False, + help='Set to True when solver will run in active-active ' + 'mode. When set to False, solver will restart any ' + 'orphaned solving requests at startup.'), +] + +CONF.register_opts(SOLVER_OPTS, group='solver') + +# Pull in service opts. We use them here. +OPTS = service.OPTS +CONF.register_opts(OPTS) + + +class SolverServiceLauncher(object): + """Launcher for the solver service.""" + def __init__(self, conf): + self.conf = conf + + # Set up Music access. + self.music = api.API() + self.music.keyspace_create(keyspace=conf.keyspace) + + # Dynamically create a plan class for the specified keyspace + self.Plan = base.create_dynamic_model( + keyspace=conf.keyspace, baseclass=plan.Plan, classname="Plan") + + if not self.Plan: + raise + + def run(self): + kwargs = {'plan_class': self.Plan} + svcmgr = cotyledon.ServiceManager() + svcmgr.add(SolverService, + workers=self.conf.solver.workers, + args=(self.conf,), kwargs=kwargs) + svcmgr.run() + + +class SolverService(cotyledon.Service): + """Solver service.""" + + # This will appear in 'ps xaf' + name = "Conductor Solver" + + def __init__(self, worker_id, conf, **kwargs): + """Initializer""" + LOG.debug("%s" % self.__class__.__name__) + super(SolverService, self).__init__(worker_id) + self._init(conf, **kwargs) + self.running = True + + def _init(self, conf, **kwargs): + """Set up the necessary ingredients.""" + self.conf = conf + self.kwargs = kwargs + + self.Plan = kwargs.get('plan_class') + + # Set up the RPC service(s) we want to talk to. + self.data_service = self.setup_rpc(conf, "data") + + # Set up the cei and optimizer + self.cei = cei.ConstraintEngineInterface(self.data_service) + # self.optimizer = optimizer.Optimizer(conf) + + # Set up Music access. + self.music = api.API() + + if not self.conf.solver.concurrent: + self._reset_solving_status() + + def _gracefully_stop(self): + """Gracefully stop working on things""" + pass + + def _reset_solving_status(self): + """Reset plans being solved so they are solved again. + + Use this only when the solver service is not running concurrently. + """ + plans = self.Plan.query.all() + for the_plan in plans: + if the_plan.status == self.Plan.SOLVING: + the_plan.status = self.Plan.TRANSLATED + the_plan.update() + + def _restart(self): + """Prepare to restart the service""" + pass + + def setup_rpc(self, conf, topic): + """Set up the RPC Client""" + # TODO(jdandrea): Put this pattern inside music_messaging? + transport = messaging.get_transport(conf=conf) + target = music_messaging.Target(topic=topic) + client = music_messaging.RPCClient(conf=conf, + transport=transport, + target=target) + return client + + def run(self): + """Run""" + LOG.debug("%s" % self.__class__.__name__) + # TODO(snarayanan): This is really meant to be a control loop + # As long as self.running is true, we process another request. + while self.running: + # plans = Plan.query().all() + # Find the first plan with a status of TRANSLATED. + # Change its status to SOLVING. + # Then, read the "translated" field as "template". + json_template = None + requests_to_solve = dict() + plans = self.Plan.query.all() + found_translated_template = False + for p in plans: + if p.status == self.Plan.TRANSLATED: + json_template = p.translation + found_translated_template = True + break + if found_translated_template and not json_template: + message = _LE("Plan {} status is translated, yet " + "the translation wasn't found").format(p.id) + LOG.error(message) + p.status = self.Plan.ERROR + p.message = message + p.update() + continue + elif not json_template: + continue + + p.status = self.Plan.SOLVING + p.update() + + request = parser.Parser() + request.cei = self.cei + try: + request.parse_template(json_template) + except Exception as err: + message = _LE("Plan {} status encountered a " + "parsing error: {}").format(p.id, err.message) + LOG.error(message) + p.status = self.Plan.ERROR + p.message = message + p.update() + continue + + request.map_constraints_to_demands() + requests_to_solve[p.id] = request + opt = optimizer.Optimizer(self.conf, _requests=requests_to_solve) + solution = opt.get_solution() + + recommendations = [] + if not solution or not solution.decisions: + message = _LI("Plan {} search failed, no " + "recommendations found").format(p.id) + LOG.info(message) + # Update the plan status + p.status = self.Plan.NOT_FOUND + p.message = message + p.update() + else: + # Assemble recommendation result JSON + for demand_name in solution.decisions: + resource = solution.decisions[demand_name] + + rec = { + # FIXME(shankar) A&AI must not be hardcoded here. + # Also, account for more than one Inventory Provider. + "inventory_provider": "aai", + "service_resource_id": + resource.get("service_resource_id"), + "candidate": { + "candidate_id": resource.get("candidate_id"), + "inventory_type": resource.get("inventory_type"), + "cloud_owner": resource.get("cloud_owner"), + "location_type": resource.get("location_type"), + "location_id": resource.get("location_id")}, + "attributes": { + "physical-location-id": + resource.get("physical_location_id"), + "sriov_automation": + resource.get("sriov_automation"), + "cloud_owner": resource.get("cloud_owner"), + 'cloud_version': resource.get("cloud_region_version")}, + } + if rec["candidate"]["inventory_type"] == "service": + rec["attributes"]["host_id"] = resource.get("host_id") + rec["candidate"]["host_id"] = resource.get("host_id") + + # TODO(snarayanan): Add total value to recommendations? + # msg = "--- total value of decision = {}" + # LOG.debug(msg.format(_best_path.total_value)) + # msg = "--- total cost of decision = {}" + # LOG.debug(msg.format(_best_path.total_cost)) + + recommendations.append({demand_name: rec}) + + # Update the plan with the solution + p.solution = { + "recommendations": recommendations + } + p.status = self.Plan.SOLVED + p.update() + LOG.info(_LI("Plan {} search complete, solution with {} " + "recommendations found"). + format(p.id, len(recommendations))) + LOG.debug("Plan {} detailed solution: {}". + format(p.id, p.solution)) + + # Check status, update plan with response, SOLVED or ERROR + + def terminate(self): + """Terminate""" + LOG.debug("%s" % self.__class__.__name__) + self.running = False + self._gracefully_stop() + super(SolverService, self).terminate() + + def reload(self): + """Reload""" + LOG.debug("%s" % self.__class__.__name__) + self._restart() diff --git a/conductor/conductor/solver/simulators/__init__.py b/conductor/conductor/solver/simulators/__init__.py new file mode 100644 index 0000000..f2bbdfd --- /dev/null +++ b/conductor/conductor/solver/simulators/__init__.py @@ -0,0 +1,19 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + diff --git a/conductor/conductor/solver/simulators/a_and_ai/__init__.py b/conductor/conductor/solver/simulators/a_and_ai/__init__.py new file mode 100755 index 0000000..f2bbdfd --- /dev/null +++ b/conductor/conductor/solver/simulators/a_and_ai/__init__.py @@ -0,0 +1,19 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + diff --git a/conductor/conductor/solver/simulators/valet/__init__.py b/conductor/conductor/solver/simulators/valet/__init__.py new file mode 100755 index 0000000..f2bbdfd --- /dev/null +++ b/conductor/conductor/solver/simulators/valet/__init__.py @@ -0,0 +1,19 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + diff --git a/conductor/conductor/solver/utils/__init__.py b/conductor/conductor/solver/utils/__init__.py new file mode 100755 index 0000000..f2bbdfd --- /dev/null +++ b/conductor/conductor/solver/utils/__init__.py @@ -0,0 +1,19 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + diff --git a/conductor/conductor/solver/utils/constraint_engine_interface.py b/conductor/conductor/solver/utils/constraint_engine_interface.py new file mode 100644 index 0000000..de335d6 --- /dev/null +++ b/conductor/conductor/solver/utils/constraint_engine_interface.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# + + +"""Constraint/Engine Interface + +Utility library that defines the interface between +the constraints and the conductor data engine. + +""" + +from oslo_log import log + +LOG = log.getLogger(__name__) + + +class ConstraintEngineInterface(object): + def __init__(self, client): + self.client = client + + def get_candidate_location(self, candidate): + # Try calling a method (remember, "calls" are synchronous) + # FIXME(jdandrea): Doing this because Music calls are expensive. + lat = candidate.get('latitude') + lon = candidate.get('longitude') + if lat and lon: + response = (float(lat), float(lon)) + else: + ctxt = {} + args = {"candidate": candidate} + response = self.client.call(ctxt=ctxt, + method="get_candidate_location", + args=args) + LOG.debug("get_candidate_location response: {}".format(response)) + return response + + def get_candidate_zone(self, candidate, _category=None): + # FIXME(jdandrea): Doing this because Music calls are expensive. + if _category == 'region': + response = candidate['location_id'] + elif _category == 'complex': + response = candidate['complex_name'] + else: + ctxt = {} + args = {"candidate": candidate, "category": _category} + response = self.client.call(ctxt=ctxt, + method="get_candidate_zone", + args=args) + LOG.debug("get_candidate_zone response: {}".format(response)) + return response + + def get_candidates_from_service(self, constraint_name, + constraint_type, candidate_list, + controller, inventory_type, + request, cost, demand_name): + ctxt = {} + args = {"constraint_name": constraint_name, + "constraint_type": constraint_type, + "candidate_list": candidate_list, + "controller": controller, + "inventory_type": inventory_type, + "request": request, + "cost": cost, + "demand_name": demand_name} + response = self.client.call(ctxt=ctxt, + method="get_candidates_from_service", + args=args) + LOG.debug("get_candidates_from_service response: {}".format(response)) + # response is a list of (candidate, cost) tuples + return response + + def get_inventory_group_candidates(self, candidate_list, + demand_name, resolved_candidate): + # return a list of the "pair" candidates for the given candidate + ctxt = {} + args = {"candidate_list": candidate_list, + "demand_name": demand_name, + "resolved_candidate": resolved_candidate} + response = self.client.call(ctxt=ctxt, + method="get_inventory_group_candidates", + args=args) + LOG.debug("get_inventory_group_candidates \ + response: {}".format(response)) + return response + + def get_candidates_by_attributes(self, demand_name, + candidate_list, properties): + ctxt = {} + args = {"candidate_list": candidate_list, + "properties": properties, + "demand_name": demand_name} + response = self.client.call(ctxt=ctxt, + method="get_candidates_by_attributes", + args=args) + LOG.debug("get_candidates_by_attribute response: {}".format(response)) + # response is a list of (candidate, cost) tuples + return response diff --git a/conductor/conductor/solver/utils/utils.py b/conductor/conductor/solver/utils/utils.py new file mode 100755 index 0000000..5cec51f --- /dev/null +++ b/conductor/conductor/solver/utils/utils.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 math + + +def compute_air_distance(_src, _dst): + """Compute Air Distance + + based on latitude and longitude + input: a pair of (lat, lon)s + output: air distance as km + """ + distance = 0.0 + + if _src == _dst: + return distance + + radius = 6371.0 # km + + dlat = math.radians(_dst[0] - _src[0]) + dlon = math.radians(_dst[1] - _src[1]) + a = math.sin(dlat / 2.0) * math.sin(dlat / 2.0) + \ + math.cos(math.radians(_src[0])) * \ + math.cos(math.radians(_dst[0])) * \ + math.sin(dlon / 2.0) * math.sin(dlon / 2.0) + c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a)) + distance = radius * c + + return distance + + +def convert_km_to_miles(_km): + return _km * 0.621371 + + +def convert_miles_to_km(_miles): + return _miles / 0.621371 -- cgit 1.2.3-korg