diff options
author | 2018-03-14 15:17:06 -0700 | |
---|---|---|
committer | 2018-03-25 05:53:06 -0700 | |
commit | ac91747b8819d23fd2e56d9e1dd170b83498a2bf (patch) | |
tree | bdd9a64e13fb3b15bc35b0830dbd2dd2ea0937ba | |
parent | 1c4a9f7f0d7ca61df2f0955f60aea9670e399d45 (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>
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() |