Add functionalities to support NSSI selection
[optf/has.git] / conductor / conductor / controller / translator.py
index fb591e0..9fa5b6b 100644 (file)
@@ -1,6 +1,7 @@
 #
 # -------------------------------------------------------------------------
 #   Copyright (c) 2015-2017 AT&T Intellectual Property
+#   Copyright (C) 2020 Wipro Limited.
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
 #   you may not use this file except in compliance with the License.
@@ -28,8 +29,11 @@ import yaml
 from conductor import __file__ as conductor_root
 from conductor import messaging
 from conductor import service
+
 from conductor.common import threshold
 from conductor.common.music import messaging as music_messaging
+from conductor.data.plugins.triage_translator.triage_translator_data import TraigeTranslatorData
+from conductor.data.plugins.triage_translator.triage_translator import TraigeTranslator
 from oslo_config import cfg
 from oslo_log import log
 
@@ -40,15 +44,16 @@ CONF = cfg.CONF
 VERSIONS = ["2016-11-01", "2017-10-10", "2018-02-01"]
 LOCATION_KEYS = ['latitude', 'longitude', 'host_name', 'clli_code']
 INVENTORY_PROVIDERS = ['aai']
-INVENTORY_TYPES = ['cloud', 'service', 'transport']
+INVENTORY_TYPES = ['cloud', 'service', 'transport', 'vfmodule', 'nssi']
 DEFAULT_INVENTORY_PROVIDER = INVENTORY_PROVIDERS[0]
-CANDIDATE_KEYS = ['inventory_type', 'candidate_id', 'location_id',
-                  'location_type', 'cost']
-DEMAND_KEYS = ['inventory_provider', 'inventory_type', 'service_type',
-               'service_id', 'service_resource_id', 'customer_id',
-               'default_cost', 'candidates', 'region', 'complex',
-               'required_candidates', 'excluded_candidates',
-               'existing_placement', 'subdivision', 'flavor', 'attributes']
+CANDIDATE_KEYS = ['candidate_id', 'cost', 'inventory_type', 'location_id',
+                  'location_type']
+DEMAND_KEYS = ['filtering_attributes', 'passthrough_attributes', 'candidates', 'complex', 'conflict_identifier',
+               'customer_id', 'default_cost', 'excluded_candidates',
+               'existing_placement', 'flavor', 'inventory_provider',
+               'inventory_type', 'port_key', 'region', 'required_candidates',
+               'service_id', 'service_resource_id', 'service_subscription',
+               'service_type', 'subdivision', 'unique', 'vlan_key']
 CONSTRAINT_KEYS = ['type', 'demands', 'properties']
 CONSTRAINTS = {
     # constraint_type: {
@@ -63,6 +68,11 @@ CONSTRAINTS = {
         'split': True,
         'required': ['evaluate'],
     },
+    'threshold': {
+        'split': True,
+        'required': ['attribute', 'threshold', 'operator'],
+        'optional': ['unit']
+    },
     'distance_between_demands': {
         'required': ['distance'],
         'thresholds': {
@@ -105,7 +115,7 @@ CONSTRAINTS = {
     },
 }
 HPA_FEATURES = ['architecture', 'hpa-feature', 'hpa-feature-attributes',
-                'hpa-version', 'mandatory']
+                'hpa-version', 'mandatory', 'directives']
 HPA_OPTIONAL = ['score']
 HPA_ATTRIBUTES = ['hpa-attribute-key', 'hpa-attribute-value', 'operator']
 HPA_ATTRIBUTES_OPTIONAL = ['unit']
@@ -133,7 +143,8 @@ class Translator(object):
         self._translation = None
         self._valid = False
         self._ok = False
-
+        self.triageTranslatorData= TraigeTranslatorData()
+        self.triageTranslator = TraigeTranslator()
         # Set up the RPC service(s) we want to talk to.
         self.data_service = self.setup_rpc(self.conf, "data")
 
@@ -220,8 +231,8 @@ class Translator(object):
                             "{} {} has an invalid key {}".format(
                                 name, content_name, key))
 
-        demand_keys = self._demands.keys()
-        location_keys = self._locations.keys()
+        demand_keys = list(self._demands.keys())     # Python 3 Conversion -- dict object to list object
+        location_keys = list(self._locations.keys())     # Python 3 Conversion -- dict object to list object
         for constraint_name, constraint in self._constraints.items():
 
             # Require a single demand (string), or a list of one or more.
@@ -274,7 +285,7 @@ class Translator(object):
         # Traverse a dict
         elif type(obj) is dict:
             # Did we find a "{get_param: ...}" intrinsic?
-            if obj.keys() == ['get_param']:
+            if list(obj.keys()) == ['get_param']:
                 param_name = obj['get_param']
 
                 # The parameter name must be a string.
@@ -295,7 +306,7 @@ class Translator(object):
                 return self._parameters.get(param_name)
 
             # Not an intrinsic. Traverse as usual.
-            for key in obj.keys():
+            for key in list(obj.keys()):
                 # Add path to the breadcrumb trail.
                 new_path = list(path)
                 new_path.append(key)
@@ -388,7 +399,7 @@ class Translator(object):
                                 "not a dictionary".format(name))
 
                         # Must have only supported keys
-                        for key in candidate.keys():
+                        for key in list(candidate.keys()):
                             if key not in CANDIDATE_KEYS:
                                 raise TranslatorException(
                                     "Candidate with invalid key {} found "
@@ -440,14 +451,14 @@ class Translator(object):
                             "demand {}".format(inventory_type, name)
                         )
 
-                    # For service inventories, customer_id and
+                    # For service and vfmodule inventories, customer_id and
                     # service_type MUST be specified
-                    if inventory_type == 'service':
-                        attributes = requirement.get('attributes')
+                    if inventory_type == 'service' or inventory_type == 'vfmodule':
+                        filtering_attributes = requirement.get('filtering_attributes')
 
-                        if attributes:
-                            customer_id = attributes.get('customer-id')
-                            global_customer_id = attributes.get('global-customer-id')
+                        if filtering_attributes:
+                            customer_id = filtering_attributes.get('customer-id')
+                            global_customer_id = filtering_attributes.get('global-customer-id')
                             if global_customer_id:
                                 customer_id = global_customer_id
                         else:
@@ -460,7 +471,7 @@ class Translator(object):
                                 "Customer ID not specified for "
                                 "demand {}".format(name)
                             )
-                        if not attributes and not service_type:
+                        if not filtering_attributes and not service_type:
                             raise TranslatorException(
                                 "Service Type not specified for "
                                 "demand {}".format(name)
@@ -493,7 +504,13 @@ class Translator(object):
             args = {
                 "demands": {
                     name: requirements,
-                }
+                },
+                "plan_info":{
+                    "plan_id": self._plan_id,
+                    "plan_name": self._plan_name
+                },
+                "triage_translator_data": self.triageTranslatorData.__dict__
+
             }
 
             # Check if required_candidate and excluded candidate
@@ -512,7 +529,6 @@ class Translator(object):
                         " list are not mutually exclusive for demand"
                         " {}".format(name)
                     )
-
             response = self.data_service.call(
                 ctxt=ctxt,
                 method="resolve_demands",
@@ -520,10 +536,13 @@ class Translator(object):
 
             resolved_demands = \
                 response and response.get('resolved_demands')
+            triage_data_trans = \
+                response and response.get('trans')
 
             required_candidates = resolved_demands \
                 .get('required_candidates')
             if not resolved_demands:
+                self.triageTranslator.thefinalCallTrans(triage_data_trans)
                 raise TranslatorException(
                     "Unable to resolve inventory "
                     "candidates for demand {}"
@@ -534,11 +553,13 @@ class Translator(object):
                 inventory_candidates.append(candidate)
             if len(inventory_candidates) < 1:
                 if not required_candidates:
+                    self.triageTranslator.thefinalCallTrans(triage_data_trans)
                     raise TranslatorException(
                         "Unable to find any candidate for "
                         "demand {}".format(name)
                     )
                 else:
+                    self.triageTranslator.thefinalCallTrans(triage_data_trans)
                     raise TranslatorException(
                         "Unable to find any required "
                         "candidate for demand {}"
@@ -547,20 +568,24 @@ class Translator(object):
             parsed[name] = {
                 "candidates": inventory_candidates,
             }
-
+        self.triageTranslator.thefinalCallTrans(triage_data_trans)
         return parsed
 
     def validate_hpa_constraints(self, req_prop, value):
         for para in value.get(req_prop):
             # Make sure there is at least one
-            # set of flavorLabel and flavorProperties
-            if not para.get('flavorLabel') \
+            # set of id, type, directives and flavorProperties
+            if not para.get('id') \
+                    or not para.get('type') \
+                    or not para.get('directives') \
                     or not para.get('flavorProperties') \
-                    or para.get('flavorLabel') == '' \
+                    or para.get('id') == '' \
+                    or para.get('type') == '' \
+                    or not isinstance(para.get('directives'), list) \
                     or para.get('flavorProperties') == '':
                 raise TranslatorException(
                     "HPA requirements need at least "
-                    "one set of flavorLabel and flavorProperties"
+                    "one set of id, type, directives and flavorProperties"
                 )
             for feature in para.get('flavorProperties'):
                 if type(feature) is not dict:
@@ -574,7 +599,7 @@ class Translator(object):
                 hpa_optional = set(feature.keys()).difference(HPA_FEATURES)
                 if hpa_optional and not hpa_optional.issubset(HPA_OPTIONAL):
                     raise TranslatorException(
-                        "Lack of compulsory elements inside HPA feature")
+                        "Got unrecognized elements inside HPA feature")
                 if feature.get('mandatory') == 'False' and not feature.get(
                         'score'):
                     raise TranslatorException(
@@ -591,7 +616,7 @@ class Translator(object):
                     if bool(hpa_attr_mandatory):
                         raise TranslatorException(
                             "Lack of compulsory elements inside HPA "
-                            "feature atrributes")
+                            "feature attributes")
                     # process optional hpa attribute parameter
                     hpa_attr_optional = set(attr.keys()).difference(
                         HPA_ATTRIBUTES)
@@ -638,7 +663,7 @@ class Translator(object):
                     # Make sure all required properties are present
                     required = constraint_def.get('required', [])
                     for req_prop in required:
-                        if req_prop not in value.keys():
+                        if req_prop not in list(value.keys()):
                             raise TranslatorException(
                                 "Required property '{}' not found in "
                                 "constraint named '{}'".format(
@@ -649,13 +674,13 @@ class Translator(object):
                                 "No value specified for property '{}' in "
                                 "constraint named '{}'".format(
                                     req_prop, name))
-                        # For HPA constraints
+                            # For HPA constraints
                         if constraint_type == 'hpa':
                             self.validate_hpa_constraints(req_prop, value)
 
                     # Make sure there are no unknown properties
                     optional = constraint_def.get('optional', [])
-                    for prop_name in value.keys():
+                    for prop_name in list(value.keys()):
                         if prop_name not in required + optional:
                             raise TranslatorException(
                                 "Unknown property '{}' in "
@@ -666,7 +691,7 @@ class Translator(object):
                     # sure its value is one of the allowed ones.
                     allowed = constraint_def.get('allowed', {})
                     for prop_name, allowed_values in allowed.items():
-                        if prop_name in value.keys():
+                        if prop_name in list(value.keys()):
                             prop_value = value.get(prop_name, '')
                             if prop_value not in allowed_values:
                                 raise TranslatorException(
@@ -678,7 +703,7 @@ class Translator(object):
                     # Break all threshold-formatted values into parts
                     thresholds = constraint_def.get('thresholds', {})
                     for thr_prop, base_units in thresholds.items():
-                        if thr_prop in value.keys():
+                        if thr_prop in list(value.keys()):
                             expression = value.get(thr_prop)
                             thr = threshold.Threshold(expression, base_units)
                             value[thr_prop] = thr.parts
@@ -732,17 +757,16 @@ class Translator(object):
         if type(optimization_copy) is not dict:
             raise TranslatorException("Optimization must be a dictionary.")
 
-        goals = optimization_copy.keys()
+        goals = list(optimization_copy.keys())
         if goals != ['minimize']:
             raise TranslatorException(
                 "Optimization must contain a single goal of 'minimize'.")
 
-        funcs = optimization_copy['minimize'].keys()
+        funcs = list(optimization_copy['minimize'].keys())
         if funcs != ['sum']:
             raise TranslatorException(
                 "Optimization goal 'minimize' must "
                 "contain a single function of 'sum'.")
-
         operands = optimization_copy['minimize']['sum']
         if type(operands) is not list:
             # or len(operands) != 2:
@@ -750,6 +774,28 @@ class Translator(object):
                 "Optimization goal 'minimize', function 'sum' "
                 "must be a list of exactly two operands.")
 
+        def get_latency_between_args(operand):
+            args = operand.get('latency_between')
+            if type(args) is not list and len(args) != 2:
+                raise TranslatorException(
+                    "Optimization 'latency_between' arguments must "
+                    "be a list of length two.")
+
+            got_demand = False
+            got_location = False
+            for arg in args:
+                if not got_demand and arg in list(self._demands.keys()):
+                    got_demand = True
+                if not got_location and arg in list(self._locations.keys()):
+                    got_location = True
+            if not got_demand or not got_location:
+                raise TranslatorException(
+                    "Optimization 'latency_between' arguments {} must "
+                    "include one valid demand name and one valid "
+                    "location name.".format(args))
+
+            return args
+
         def get_distance_between_args(operand):
             args = operand.get('distance_between')
             if type(args) is not list and len(args) != 2:
@@ -760,9 +806,9 @@ class Translator(object):
             got_demand = False
             got_location = False
             for arg in args:
-                if not got_demand and arg in self._demands.keys():
+                if not got_demand and arg in list(self._demands.keys()):
                     got_demand = True
-                if not got_location and arg in self._locations.keys():
+                if not got_location and arg in list(self._locations.keys()):
                     got_location = True
             if not got_demand or not got_location:
                 raise TranslatorException(
@@ -777,41 +823,47 @@ class Translator(object):
             args = None
             nested = False
 
-            if operand.keys() == ['distance_between']:
+            if list(operand.keys()) == ['distance_between']:
                 # Value must be a list of length 2 with one
                 # location and one demand
                 function = 'distance_between'
                 args = get_distance_between_args(operand)
 
-            elif operand.keys() == ['product']:
+            elif list(operand.keys()) == ['product']:
                 for product_op in operand['product']:
                     if threshold.is_number(product_op):
                         weight = product_op
-                    elif type(product_op) is dict:
-                        if product_op.keys() == ['distance_between']:
+                    elif isinstance(product_op, dict):
+                        if list(product_op.keys()) == ['latency_between']:
+                            function = 'latency_between'
+                            args = get_latency_between_args(product_op)
+                        elif list(product_op.keys()) == ['distance_between']:
                             function = 'distance_between'
                             args = get_distance_between_args(product_op)
-                        elif product_op.keys() == ['aic_version']:
+                        elif list(product_op.keys()) == ['aic_version']:
                             function = 'aic_version'
                             args = product_op.get('aic_version')
-                        elif product_op.keys() == ['hpa_score']:
+                        elif list(product_op.keys()) == ['hpa_score']:
                             function = 'hpa_score'
                             args = product_op.get('hpa_score')
                             if not self.is_hpa_policy_exists(args):
                                 raise TranslatorException(
                                     "HPA Score Optimization must include a "
                                     "HPA Policy constraint ")
-                        elif product_op.keys() == ['sum']:
+                        elif list(product_op.keys()) == ['sum']:
                             nested = True
                             nested_operands = product_op.get('sum')
                             for nested_operand in nested_operands:
-                                if nested_operand.keys() == ['product']:
+                                if list(nested_operand.keys()) == ['product']:
                                     nested_weight = weight
                                     for nested_product_op in nested_operand['product']:
                                         if threshold.is_number(nested_product_op):
                                             nested_weight = nested_weight * int(nested_product_op)
-                                        elif type(nested_product_op) is dict:
-                                            if nested_product_op.keys() == ['distance_between']:
+                                        elif isinstance(nested_product_op, dict):
+                                            if list(nested_product_op.keys()) == ['latency_between']:
+                                                function = 'latency_between'
+                                                args = get_latency_between_args(nested_product_op)
+                                            elif list(nested_product_op.keys()) == ['distance_between']:
                                                 function = 'distance_between'
                                                 args = get_distance_between_args(nested_product_op)
                                     parsed['operands'].append(
@@ -823,16 +875,6 @@ class Translator(object):
                                         }
                                     )
 
-                    elif type(product_op) is unicode:
-                        if product_op == 'W1':
-                            # get this weight from configuration file
-                            weight = self.conf.controller.weight1
-                        elif product_op == 'W2':
-                            # get this weight from configuration file
-                            weight = self.conf.controller.weight2
-                        elif product_op == 'cost':
-                            function = 'cost'
-
                 if not args:
                     raise TranslatorException(
                         "Optimization products must include at least "
@@ -865,23 +907,31 @@ class Translator(object):
 
     def parse_reservations(self, reservations):
         demands = self._demands
-        if type(reservations) is not dict:
+        if not isinstance(reservations, dict):
             raise TranslatorException("Reservations must be provided in "
                                       "dictionary form")
-
         parsed = {}
         if reservations:
             parsed['counter'] = 0
-        for name, reservation in reservations.items():
-            if not reservation.get('properties'):
-                reservation['properties'] = {}
-            for demand in reservation.get('demands', []):
-                if demand in demands.keys():
-                    constraint_demand = name + '_' + demand
-                    parsed['demands'] = {}
-                    parsed['demands'][constraint_demand] = copy.deepcopy(reservation)
-                    parsed['demands'][constraint_demand]['name'] = name
-                    parsed['demands'][constraint_demand]['demand'] = demand
+            parsed['demands'] = {}
+
+        for key, value in reservations.items():
+
+            if key == "service_model":
+                parsed['service_model'] = value
+
+            elif key == "service_candidates":
+                for name, reservation_details in value.items():
+                    if not reservation_details.get('properties'):
+                        reservation_details['properties'] = {}
+                    for demand in reservation_details.get('demands', []):
+                        if demand in list(demands.keys()):
+                            reservation_demand = name + '_' + demand
+                            parsed['demands'][reservation_demand] = copy.deepcopy(reservation_details)
+                            parsed['demands'][reservation_demand]['name'] = name
+                            parsed['demands'][reservation_demand]['demands'] = demand
+                        else:
+                            raise TranslatorException("Demand {} must be provided in demands section".format(demand))
 
         return parsed
 
@@ -890,7 +940,9 @@ class Translator(object):
         if not self.valid:
             raise TranslatorException("Can't translate an invalid template.")
 
-        request_type = self._parameters.get("request_type") or ""
+        request_type = self._parameters.get("request_type") \
+                       or self._parameters.get("REQUEST_TYPE") \
+                       or ""
 
         self._translation = {
             "conductor_solver": {
@@ -899,6 +951,7 @@ class Translator(object):
                 "request_type": request_type,
                 "locations": self.parse_locations(self._locations),
                 "demands": self.parse_demands(self._demands),
+                "objective": self.parse_optimization(self._optmization),
                 "constraints": self.parse_constraints(self._constraints),
                 "objective": self.parse_optimization(self._optmization),
                 "reservations": self.parse_reservations(self._reservations),
@@ -915,7 +968,7 @@ class Translator(object):
             self.do_translation()
             self._ok = True
         except Exception as exc:
-            self._error_message = exc.message
+            self._error_message = exc.args
 
     @property
     def valid(self):