summaryrefslogtreecommitdiffstats
path: root/conductor
diff options
context:
space:
mode:
authorShankaranarayanan Puzhavakath Narayanan <snarayanan@research.att.com>2017-12-17 15:38:42 +0000
committerGerrit Code Review <gerrit@onap.org>2017-12-17 15:38:42 +0000
commit3c0b5b6d33e98a3d43171b1c58b5bbe5a98e6cef (patch)
tree5e6f0a4cb6a63dca28ed971595f05a987980ef7a /conductor
parent3212850cfc6d88d4d186b6805e7774cb70fbd137 (diff)
parent492b9f946782fa8d1a348e3c8ffd5ac0e57cc2b1 (diff)
Merge "Added solver directory to the repository"
Diffstat (limited to 'conductor')
-rw-r--r--conductor/conductor/solver/__init__.py20
-rwxr-xr-xconductor/conductor/solver/optimizer/__init__.py19
-rwxr-xr-xconductor/conductor/solver/optimizer/best_first.py163
-rwxr-xr-xconductor/conductor/solver/optimizer/constraints/__init__.py19
-rwxr-xr-xconductor/conductor/solver/optimizer/constraints/access_distance.py111
-rw-r--r--conductor/conductor/solver/optimizer/constraints/attribute.py49
-rwxr-xr-xconductor/conductor/solver/optimizer/constraints/cloud_distance.py96
-rwxr-xr-xconductor/conductor/solver/optimizer/constraints/constraint.py50
-rwxr-xr-xconductor/conductor/solver/optimizer/constraints/inventory_group.py78
-rw-r--r--conductor/conductor/solver/optimizer/constraints/service.py76
-rwxr-xr-xconductor/conductor/solver/optimizer/constraints/zone.py81
-rwxr-xr-xconductor/conductor/solver/optimizer/decision_path.py55
-rwxr-xr-xconductor/conductor/solver/optimizer/fit_first.py160
-rwxr-xr-xconductor/conductor/solver/optimizer/greedy.py65
-rwxr-xr-xconductor/conductor/solver/optimizer/optimizer.py196
-rw-r--r--conductor/conductor/solver/optimizer/random_pick.py43
-rwxr-xr-xconductor/conductor/solver/optimizer/search.py90
-rwxr-xr-xconductor/conductor/solver/request/__init__.py19
-rwxr-xr-xconductor/conductor/solver/request/demand.py49
-rwxr-xr-xconductor/conductor/solver/request/functions/__init__.py19
-rw-r--r--conductor/conductor/solver/request/functions/cloud_version.py27
-rwxr-xr-xconductor/conductor/solver/request/functions/distance_between.py37
-rwxr-xr-xconductor/conductor/solver/request/objective.py111
-rwxr-xr-xconductor/conductor/solver/request/parser.py240
-rwxr-xr-xconductor/conductor/solver/resource/__init__.py19
-rwxr-xr-xconductor/conductor/solver/resource/region.py99
-rwxr-xr-xconductor/conductor/solver/resource/service.py52
-rw-r--r--conductor/conductor/solver/service.py307
-rw-r--r--conductor/conductor/solver/simulators/__init__.py19
-rwxr-xr-xconductor/conductor/solver/simulators/a_and_ai/__init__.py19
-rwxr-xr-xconductor/conductor/solver/simulators/valet/__init__.py19
-rwxr-xr-xconductor/conductor/solver/utils/__init__.py19
-rw-r--r--conductor/conductor/solver/utils/constraint_engine_interface.py114
-rwxr-xr-xconductor/conductor/solver/utils/utils.py56
34 files changed, 2596 insertions, 0 deletions
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