Added solver directory to the repository 13/26413/1
authorrl001m <ruilu@research.att.com>
Sun, 17 Dec 2017 14:26:41 +0000 (09:26 -0500)
committerrl001m <ruilu@research.att.com>
Sun, 17 Dec 2017 14:26:52 +0000 (09:26 -0500)
Added the HAS-Solver module in ONAP

Change-Id: I5567e3976ad0dc693eb2adc2f479987001b14770
Issue-ID: OPTFRA-14
Signed-off-by: rl001m <ruilu@research.att.com>
34 files changed:
conductor/conductor/solver/__init__.py [new file with mode: 0644]
conductor/conductor/solver/optimizer/__init__.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/best_first.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/constraints/__init__.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/constraints/access_distance.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/constraints/attribute.py [new file with mode: 0644]
conductor/conductor/solver/optimizer/constraints/cloud_distance.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/constraints/constraint.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/constraints/inventory_group.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/constraints/service.py [new file with mode: 0644]
conductor/conductor/solver/optimizer/constraints/zone.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/decision_path.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/fit_first.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/greedy.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/optimizer.py [new file with mode: 0755]
conductor/conductor/solver/optimizer/random_pick.py [new file with mode: 0644]
conductor/conductor/solver/optimizer/search.py [new file with mode: 0755]
conductor/conductor/solver/request/__init__.py [new file with mode: 0755]
conductor/conductor/solver/request/demand.py [new file with mode: 0755]
conductor/conductor/solver/request/functions/__init__.py [new file with mode: 0755]
conductor/conductor/solver/request/functions/cloud_version.py [new file with mode: 0644]
conductor/conductor/solver/request/functions/distance_between.py [new file with mode: 0755]
conductor/conductor/solver/request/objective.py [new file with mode: 0755]
conductor/conductor/solver/request/parser.py [new file with mode: 0755]
conductor/conductor/solver/resource/__init__.py [new file with mode: 0755]
conductor/conductor/solver/resource/region.py [new file with mode: 0755]
conductor/conductor/solver/resource/service.py [new file with mode: 0755]
conductor/conductor/solver/service.py [new file with mode: 0644]
conductor/conductor/solver/simulators/__init__.py [new file with mode: 0644]
conductor/conductor/solver/simulators/a_and_ai/__init__.py [new file with mode: 0755]
conductor/conductor/solver/simulators/valet/__init__.py [new file with mode: 0755]
conductor/conductor/solver/utils/__init__.py [new file with mode: 0755]
conductor/conductor/solver/utils/constraint_engine_interface.py [new file with mode: 0644]
conductor/conductor/solver/utils/utils.py [new file with mode: 0755]

diff --git a/conductor/conductor/solver/__init__.py b/conductor/conductor/solver/__init__.py
new file mode 100644 (file)
index 0000000..ff501ef
--- /dev/null
@@ -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 (executable)
index 0000000..f2bbdfd
--- /dev/null
@@ -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 (executable)
index 0000000..65e435d
--- /dev/null
@@ -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 (executable)
index 0000000..f2bbdfd
--- /dev/null
@@ -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 (executable)
index 0000000..7c400b8
--- /dev/null
@@ -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 (file)
index 0000000..18f9332
--- /dev/null
@@ -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 (executable)
index 0000000..1e862d4
--- /dev/null
@@ -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 (executable)
index 0000000..03e2c33
--- /dev/null
@@ -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 (executable)
index 0000000..f0f8089
--- /dev/null
@@ -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 (file)
index 0000000..bdbe267
--- /dev/null
@@ -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 (executable)
index 0000000..c7a968f
--- /dev/null
@@ -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 (executable)
index 0000000..0890f52
--- /dev/null
@@ -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 (executable)
index 0000000..42d8fed
--- /dev/null
@@ -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 (executable)
index 0000000..eae1b12
--- /dev/null
@@ -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 (executable)
index 0000000..c7155c4
--- /dev/null
@@ -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 (file)
index 0000000..2896757
--- /dev/null
@@ -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 (executable)
index 0000000..9d138e4
--- /dev/null
@@ -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 (executable)
index 0000000..f2bbdfd
--- /dev/null
@@ -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 (executable)
index 0000000..5554cfe
--- /dev/null
@@ -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 (executable)
index 0000000..f2bbdfd
--- /dev/null
@@ -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 (file)
index 0000000..564468b
--- /dev/null
@@ -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 (executable)
index 0000000..8cf3f86
--- /dev/null
@@ -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 (executable)
index 0000000..ca1e614
--- /dev/null
@@ -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 (executable)
index 0000000..6e30549
--- /dev/null
@@ -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 (executable)
index 0000000..f2bbdfd
--- /dev/null
@@ -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 (executable)
index 0000000..fc42bd1
--- /dev/null
@@ -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 (executable)
index 0000000..faedb53
--- /dev/null
@@ -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 (file)
index 0000000..60aa092
--- /dev/null
@@ -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 (file)
index 0000000..f2bbdfd
--- /dev/null
@@ -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 (executable)
index 0000000..f2bbdfd
--- /dev/null
@@ -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 (executable)
index 0000000..f2bbdfd
--- /dev/null
@@ -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 (executable)
index 0000000..f2bbdfd
--- /dev/null
@@ -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 (file)
index 0000000..de335d6
--- /dev/null
@@ -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 (executable)
index 0000000..5cec51f
--- /dev/null
@@ -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