HPA Constraint solver plugin 71/36071/6
authorDileep Ranganathan <dileep.ranganathan@intel.com>
Wed, 14 Mar 2018 22:17:06 +0000 (15:17 -0700)
committerDileep Ranganathan <dileep.ranganathan@intel.com>
Sun, 25 Mar 2018 12:53:06 +0000 (05:53 -0700)
Implement hpa constraint plugin and implement solve method
Initialize HPA constraint per solver request
Reordered constraint ranking
Implemented RPC invocation to data plugin for matching hpa

Change-Id: I5d63464b6bff6abc10599e000b2b56f926f146f9
Issue-ID: OPTFRA-178
Signed-off-by: Dileep Ranganathan <dileep.ranganathan@intel.com>
conductor/conductor/solver/optimizer/constraints/hpa.py [new file with mode: 0644]
conductor/conductor/solver/request/parser.py
conductor/conductor/solver/utils/constraint_engine_interface.py
conductor/conductor/tests/unit/solver/__init__.py [new file with mode: 0644]
conductor/conductor/tests/unit/solver/candidate_list.json [new file with mode: 0644]
conductor/conductor/tests/unit/solver/hpa_constraints.json [new file with mode: 0644]
conductor/conductor/tests/unit/solver/test_hpa.py [new file with mode: 0644]

diff --git a/conductor/conductor/solver/optimizer/constraints/hpa.py b/conductor/conductor/solver/optimizer/constraints/hpa.py
new file mode 100644 (file)
index 0000000..a7e8d3c
--- /dev/null
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 Intel Corporation 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.
+#
+# -------------------------------------------------------------------------
+#
+
+'''Solver class for constraint type hpa
+   Hardware Platform Awareness (HPA) constraint plugin'''
+
+# python imports
+
+from conductor.i18n import _LE, _LI
+# Conductor imports
+from conductor.solver.optimizer.constraints import constraint
+# Third-party library imports
+from oslo_log import log
+
+LOG = log.getLogger(__name__)
+
+
+class HPA(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):
+        '''
+        Solver for HPA constraint type.
+        :param _decision_path: decision tree
+        :param _candidate_list: List of candidates
+        :param _request: solver request
+        :return: candidate_list with hpa features and flavor label mapping
+        '''
+        # call conductor engine with request parameters
+        cei = _request.cei
+        demand_name = _decision_path.current_demand.name
+        LOG.info(_LI("Solving constraint type '{}' for demand - [{}]").format(
+            self.constraint_type, demand_name))
+        vm_label_list = self.properties.get('evaluate')
+        for vm_demand in vm_label_list:
+            label_name = vm_demand['label']
+            features = vm_demand['features']
+            response = (cei.get_candidates_with_hpa(label_name,
+                                                    _candidate_list,
+                                                    features))
+            if response:
+                _candidate_list = response
+            else:
+                LOG.error(_LE("Flavor mapping for label name {} already"
+                              "exists").format(label_name))
+
+        return _candidate_list
index 1f966ec..d7f3cec 100755 (executable)
 
 # import json
 import operator
-
-from oslo_log import log
 import random
 
 from conductor.solver.optimizer.constraints \
     import access_distance as access_dist
-from conductor.solver.optimizer.constraints \
-    import attribute as attribute_constraint
 from conductor.solver.optimizer.constraints \
     import aic_distance as aic_dist
+from conductor.solver.optimizer.constraints \
+    import attribute as attribute_constraint
+from conductor.solver.optimizer.constraints import hpa
 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 import objective
 from conductor.solver.request.functions import aic_version
 from conductor.solver.request.functions import cost
 from conductor.solver.request.functions import distance_between
-from conductor.solver.request import objective
+from oslo_log import log
 
 # from conductor.solver.request.functions import distance_between
 # from conductor.solver.request import objective
@@ -100,7 +100,8 @@ class Parser(object):
             loc.loc_type = "coordinates"
             loc.value = (float(location_info["latitude"]),
                          float(location_info["longitude"]))
-            loc.country = location_info['country'] if 'country' in location_info else None
+            loc.country = location_info[
+                'country'] if 'country' in location_info else None
             self.locations[location_id] = loc
 
         # get constraints
@@ -194,7 +195,8 @@ class Parser(object):
                 location = self.locations[location_id] if location_id else None
                 my_zone_constraint = zone.Zone(
                     constraint_id, constraint_type, constraint_demands,
-                    _qualifier=qualifier, _category=category, _location=location)
+                    _qualifier=qualifier, _category=category,
+                    _location=location)
                 self.constraints[my_zone_constraint.name] = my_zone_constraint
             elif constraint_type == "attribute":
                 c_property = constraint_info.get("properties")
@@ -205,13 +207,21 @@ class Parser(object):
                                                    _properties=c_property)
                 self.constraints[my_attribute_constraint.name] = \
                     my_attribute_constraint
+            elif constraint_type == "hpa":
+                LOG.debug("Creating constraint - {}".format(constraint_type))
+                c_property = constraint_info.get("properties")
+                my_hpa_constraint = hpa.HPA(constraint_id,
+                                            constraint_type,
+                                            constraint_demands,
+                                            _properties=c_property)
+                self.constraints[my_hpa_constraint.name] = my_hpa_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"]:
+        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"]
@@ -302,6 +312,7 @@ class Parser(object):
             _qualifier="same", _category="zone1")
         constraint_list.append(same_zone)
         '''
+
     def reorder_constraint(self):
         # added manual ranking to the constraint type for optimizing purpose the last 2 are costly interaction
         for constraint_name, constraint in self.constraints.items():
@@ -311,17 +322,19 @@ class Parser(object):
                 constraint.rank = 2
             elif constraint.constraint_type == "attribute":
                 constraint.rank = 3
-            elif constraint.constraint_type == "inventory_group":
+            elif constraint.constraint_type == "hpa":
                 constraint.rank = 4
-            elif constraint.constraint_type == "instance_fit":
+            elif constraint.constraint_type == "inventory_group":
                 constraint.rank = 5
-            elif constraint.constraint_type == "region_fit":
+            elif constraint.constraint_type == "instance_fit":
                 constraint.rank = 6
-            else:
+            elif constraint.constraint_type == "region_fit":
                 constraint.rank = 7
+            else:
+                constraint.rank = 8
 
     def attr_sort(self, attrs=['rank']):
-        #this helper for sorting the rank
+        # this helper for sorting the rank
         return lambda k: [getattr(k, attr) for attr in attrs]
 
     def sort_constraint_by_rank(self):
@@ -330,7 +343,6 @@ class Parser(object):
             cl_list = cl.constraint_list
             cl_list.sort(key=self.attr_sort(attrs=['rank']))
 
-
     def assgin_constraints_to_demands(self):
         # self.parse_dhv_template() # get data from DHV template
         # self.get_data_from_aai_simulator() # get data from aai simulation
index 99526f7..256e4bb 100644 (file)
@@ -116,3 +116,22 @@ class ConstraintEngineInterface(object):
         LOG.debug("get_candidates_by_attribute response: {}".format(response))
         # response is a list of (candidate, cost) tuples
         return response
+
+    def get_candidates_with_hpa(self, label_name, candidate_list, features):
+        '''
+        Returns the candidate_list with an addition of flavor_mapping for
+        matching cloud candidates with hpa constraints.
+        :param label_name: vm_label_name passed from the SO/Policy
+        :param candidate_list: list of candidates to process
+        :param features: hpa features for this vm_label_name
+        :return: candidate_list with hpa features and flavor mapping
+        '''
+        ctxt = {}
+        args = {"candidate_list": candidate_list,
+                "features": features,
+                "label_name": label_name}
+        response = self.client.call(ctxt=ctxt,
+                                    method="get_candidates_with_hpa",
+                                    args=args)
+        LOG.debug("get_candidates_with_hpa response: {}".format(response))
+        return response
diff --git a/conductor/conductor/tests/unit/solver/__init__.py b/conductor/conductor/tests/unit/solver/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/conductor/conductor/tests/unit/solver/candidate_list.json b/conductor/conductor/tests/unit/solver/candidate_list.json
new file mode 100644 (file)
index 0000000..e29782c
--- /dev/null
@@ -0,0 +1,43 @@
+{
+  "candidate_list": [
+    {
+      "candidate_id": "1ac71fb8-ad43-4e16-9459-c3f372b8236d",
+      "candidate_type": "service",
+      "inventory_type": "service",
+      "inventory_provider": "aai",
+      "host_id": "vnf_123456",
+      "cost": "100",
+      "location_id": "DLLSTX55",
+      "location_type": "azure",
+      "latitude": "32.897480",
+      "longitude": "-97.040443",
+      "city": "Dallas",
+      "state": "TX",
+      "country": "USA",
+      "region": "US",
+      "complex_name": "dalls_one",
+      "cloud_owner": "att-aic",
+      "cloud_region_version": "1.1",
+      "physical_location_id": "DLLSTX55"
+    },
+    {
+      "candidate_id": "NYCNY55",
+      "candidate_type": "cloud",
+      "inventory_type": "cloud",
+      "inventory_provider": "aai",
+      "cost": "100",
+      "location_id": "NYCNY55",
+      "location_type": "azure",
+      "latitude": "40.7128",
+      "longitude": "-74.0060",
+      "city": "New York",
+      "state": "NY",
+      "country": "USA",
+      "region": "US",
+      "complex_name": "ny_one",
+      "cloud_owner": "att-aic",
+      "cloud_region_version": "1.1",
+      "physical_location_id": "NYCNY55"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/solver/hpa_constraints.json b/conductor/conductor/tests/unit/solver/hpa_constraints.json
new file mode 100644 (file)
index 0000000..3954cd5
--- /dev/null
@@ -0,0 +1,171 @@
+{
+  "conductor_solver": {
+    "constraints": [
+      {
+        "hpa_constraint_vG": {
+          "demands": ["vG"],
+          "name": "hpa_constraint_vG",
+          "type": "hpa",
+          "properties": {
+            "evaluate": [
+              {
+                "features": [
+                  {
+                    "architecture": "generic",
+                    "hpa-feature": "basicCapabilities",
+                    "hpa-feature-attributes": [
+                      {
+                        "hpa-attribute-key": "numVirtualCpu",
+                        "hpa-attribute-value": "4",
+                        "operator": "="
+                      },
+                      {
+                        "hpa-attribute-key": "virtualMemSize",
+                        "hpa-attribute-value": "4",
+                        "operator": "=",
+                        "unit": "GB"
+                      }
+                    ],
+                    "hpa-version": "v1"
+                  },
+                  {
+                    "architecture": "generic",
+                    "hpa-feature": "numa",
+                    "hpa-feature-attributes": [
+                      {
+                        "hpa-attribute-key": "numaNodes",
+                        "hpa-attribute-value": "2",
+                        "operator": "="
+                      },
+                      {
+                        "hpa-attribute-key": "numaCpu-0",
+                        "hpa-attribute-value": "2",
+                        "operator": "="
+                      },
+                      {
+                        "hpa-attribute-key": "numaCpu-1",
+                        "hpa-attribute-value": "4",
+                        "operator": "="
+                      },
+                      {
+                        "hpa-attribute-key": "numaMem-0",
+                        "hpa-attribute-value": "2",
+                        "operator": "=",
+                        "unit": "GB"
+                      },
+                      {
+                        "hpa-attribute-key": "numaMem-1",
+                        "hpa-attribute-value": "4",
+                        "operator": "=",
+                        "unit": "GB"
+                      }
+                    ],
+                    "hpa-version": "v1"
+                  },
+                  {
+                    "architecture": "generic",
+                    "hpa-feature": "cpuPinning",
+                    "hpa-feature-attributes": [
+                      {
+                        "hpa-attribute-key": "logicalCpuThreadPinningPolicy",
+                        "hpa-attribute-value": "prefer",
+                        "operator": "="
+                      },
+                      {
+                        "hpa-attribute-key": "logicalCpuPinningPolicy",
+                        "hpa-attribute-value": "dedicated",
+                        "operator": "="
+                      }
+                    ],
+                    "hpa-version": "v1"
+                  }
+                ],
+                "label": "flavor_label_1"
+              },
+              {
+                "features": [
+                  {
+                    "architecture": "generic",
+                    "hpa-feature": "basicCapabilities",
+                    "hpa-feature-attributes": [
+                      {
+                        "hpa-attribute-key": "numVirtualCpu",
+                        "hpa-attribute-value": "8",
+                        "operator": "="
+                      },
+                      {
+                        "hpa-attribute-key": "virtualMemSize",
+                        "hpa-attribute-value": "16",
+                        "operator": "=",
+                        "unit": "GB"
+                      }
+                    ],
+                    "hpa-version": "v1"
+                  },
+                  {
+                    "architecture": "generic",
+                    "hpa-feature": "numa",
+                    "hpa-feature-attributes": [
+                      {
+                        "hpa-attribute-key": "numaNodes",
+                        "hpa-attribute-value": "2",
+                        "operator": "="
+                      },
+                      {
+                        "hpa-attribute-key": "numaCpu-0",
+                        "hpa-attribute-value": "2",
+                        "operator": "="
+                      },
+                      {
+                        "hpa-attribute-key": "numaCpu-1",
+                        "hpa-attribute-value": "4",
+                        "operator": "="
+                      },
+                      {
+                        "hpa-attribute-key": "numaMem-0",
+                        "hpa-attribute-value": "2",
+                        "operator": "=",
+                        "unit": "GB"
+                      },
+                      {
+                        "hpa-attribute-key": "numaMem-1",
+                        "hpa-attribute-value": "4",
+                        "operator": "=",
+                        "unit": "GB"
+                      }
+                    ],
+                    "hpa-version": "v1"
+                  },
+                  {
+                    "architecture": "generic",
+                    "hpa-feature": "memoryPageSize",
+                    "hpa-feature-attributes": [
+                      {
+                        "hpa-attribute-key": "memoryPageSize",
+                        "hpa-attribute-value": "2",
+                        "operator": "=",
+                        "unit": "GB"
+                      }
+                    ],
+                    "hpa-version": "v1"
+                  }
+                ],
+                "label": "flavor_label_2"
+              }
+            ]
+          }
+        }
+      },
+      {
+        "constraint_vgmux_customer": {
+          "type": "distance_to_location",
+          "demands": ["vGMuxInfra"],
+          "properties": {
+            "distance": "< 100 km",
+            "location": "customer_loc"
+          }
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/solver/test_hpa.py b/conductor/conductor/tests/unit/solver/test_hpa.py
new file mode 100644 (file)
index 0000000..c9bbbbc
--- /dev/null
@@ -0,0 +1,92 @@
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 Intel Corporation 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 unittest
+
+import mock
+import yaml
+from conductor.solver.optimizer.constraints import hpa
+from conductor.solver.utils import constraint_engine_interface as cei
+
+
+class TestHPA(unittest.TestCase):
+
+    def setUp(self):
+        req_json_file = './conductor/tests/unit/solver/candidate_list.json'
+        hpa_json_file = './conductor/tests/unit/solver/hpa_constraints.json'
+        hpa_json = yaml.safe_load(open(hpa_json_file).read())
+        req_json = yaml.safe_load(open(req_json_file).read())
+
+        (constraint_id, constraint_info) = \
+            hpa_json["conductor_solver"]["constraints"][0].items()[0]
+        c_property = constraint_info['properties']
+        constraint_type = constraint_info['properties']
+        constraint_demands = list()
+        parsed_demands = constraint_info['demands']
+        if isinstance(parsed_demands, list):
+            for d in parsed_demands:
+                constraint_demands.append(d)
+        self.hpa = hpa.HPA(constraint_id,
+                           constraint_type,
+                           constraint_demands,
+                           _properties=c_property)
+
+        self.candidate_list = req_json['candidate_list']
+
+    def tearDown(self):
+        pass
+
+    @mock.patch.object(hpa.LOG, 'error')
+    @mock.patch.object(hpa.LOG, 'info')
+    @mock.patch.object(cei.LOG, 'debug')
+    def test_solve(self, debug_mock, info_mock, error_mock):
+
+        flavor_infos = [{"flavor_label_1": {"flavor-id": "vim-flavor-id1",
+                                            "flavor-name": "vim-flavor-1"}},
+                        {"flavor_label_2": {"flavor-id": "vim-flavor-id2",
+                                            "flavor-name": "vim-flavor-2"}}]
+        self.maxDiff = None
+        hpa_candidate_list_1 = copy.deepcopy(self.candidate_list)
+        hpa_candidate_list_1[1]['flavor_map'] = {}
+        hpa_candidate_list_1[1]['flavor_map'].update(flavor_infos[0])
+        hpa_candidate_list_2 = copy.deepcopy(hpa_candidate_list_1)
+        hpa_candidate_list_2[1]['flavor_map'].update(flavor_infos[1])
+
+        mock_decision_path = mock.MagicMock()
+        mock_decision_path.current_demand.name = 'vG'
+        request_mock = mock.MagicMock()
+        client_mock = mock.MagicMock()
+        client_mock.call.return_value = None
+        request_mock.cei = cei.ConstraintEngineInterface(client_mock)
+
+        self.assertEqual(self.candidate_list,
+                         self.hpa.solve(mock_decision_path,
+                                        self.candidate_list, request_mock))
+
+        client_mock.call.side_effect = [hpa_candidate_list_1,
+                                        hpa_candidate_list_2]
+        self.assertEqual(hpa_candidate_list_2,
+                         self.hpa.solve(mock_decision_path,
+                                        self.candidate_list, request_mock))
+
+
+if __name__ == "__main__":
+    unittest.main()