summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDileep Ranganathan <dileep.ranganathan@intel.com>2018-03-14 15:17:06 -0700
committerDileep Ranganathan <dileep.ranganathan@intel.com>2018-03-25 05:53:06 -0700
commitac91747b8819d23fd2e56d9e1dd170b83498a2bf (patch)
treebdd9a64e13fb3b15bc35b0830dbd2dd2ea0937ba
parent1c4a9f7f0d7ca61df2f0955f60aea9670e399d45 (diff)
HPA Constraint solver plugin
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>
-rw-r--r--conductor/conductor/solver/optimizer/constraints/hpa.py68
-rwxr-xr-xconductor/conductor/solver/request/parser.py42
-rw-r--r--conductor/conductor/solver/utils/constraint_engine_interface.py19
-rw-r--r--conductor/conductor/tests/unit/solver/__init__.py0
-rw-r--r--conductor/conductor/tests/unit/solver/candidate_list.json43
-rw-r--r--conductor/conductor/tests/unit/solver/hpa_constraints.json171
-rw-r--r--conductor/conductor/tests/unit/solver/test_hpa.py92
7 files changed, 420 insertions, 15 deletions
diff --git a/conductor/conductor/solver/optimizer/constraints/hpa.py b/conductor/conductor/solver/optimizer/constraints/hpa.py
new file mode 100644
index 0000000..a7e8d3c
--- /dev/null
+++ b/conductor/conductor/solver/optimizer/constraints/hpa.py
@@ -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
diff --git a/conductor/conductor/solver/request/parser.py b/conductor/conductor/solver/request/parser.py
index 1f966ec..d7f3cec 100755
--- a/conductor/conductor/solver/request/parser.py
+++ b/conductor/conductor/solver/request/parser.py
@@ -21,26 +21,26 @@
# 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
diff --git a/conductor/conductor/solver/utils/constraint_engine_interface.py b/conductor/conductor/solver/utils/constraint_engine_interface.py
index 99526f7..256e4bb 100644
--- a/conductor/conductor/solver/utils/constraint_engine_interface.py
+++ b/conductor/conductor/solver/utils/constraint_engine_interface.py
@@ -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
index 0000000..e69de29
--- /dev/null
+++ b/conductor/conductor/tests/unit/solver/__init__.py
diff --git a/conductor/conductor/tests/unit/solver/candidate_list.json b/conductor/conductor/tests/unit/solver/candidate_list.json
new file mode 100644
index 0000000..e29782c
--- /dev/null
+++ b/conductor/conductor/tests/unit/solver/candidate_list.json
@@ -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
index 0000000..3954cd5
--- /dev/null
+++ b/conductor/conductor/tests/unit/solver/hpa_constraints.json
@@ -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
index 0000000..c9bbbbc
--- /dev/null
+++ b/conductor/conductor/tests/unit/solver/test_hpa.py
@@ -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()