diff options
Diffstat (limited to 'conductor')
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() |