2 # -------------------------------------------------------------------------
3 # Copyright (c) 2015-2017 AT&T Intellectual Property
4 # Copyright (C) 2020 Wipro Limited.
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
18 # -------------------------------------------------------------------------
29 from conductor import __file__ as conductor_root
30 from conductor import messaging
31 from conductor import service
33 from conductor.common import threshold
34 from conductor.common.music import messaging as music_messaging
35 from conductor.data.plugins.triage_translator.triage_translator_data import TraigeTranslatorData
36 from conductor.data.plugins.triage_translator.triage_translator import TraigeTranslator
37 from oslo_config import cfg
38 from oslo_log import log
40 LOG = log.getLogger(__name__)
44 VERSIONS = ["2016-11-01", "2017-10-10", "2018-02-01"]
45 LOCATION_KEYS = ['latitude', 'longitude', 'host_name', 'clli_code']
46 INVENTORY_PROVIDERS = ['aai']
47 INVENTORY_TYPES = ['cloud', 'service', 'transport', 'vfmodule', 'nssi']
48 DEFAULT_INVENTORY_PROVIDER = INVENTORY_PROVIDERS[0]
49 CANDIDATE_KEYS = ['candidate_id', 'cost', 'inventory_type', 'location_id',
51 DEMAND_KEYS = ['filtering_attributes', 'passthrough_attributes', 'candidates', 'complex', 'conflict_identifier',
52 'customer_id', 'default_cost', 'excluded_candidates',
53 'existing_placement', 'flavor', 'inventory_provider',
54 'inventory_type', 'port_key', 'region', 'required_candidates',
55 'service_id', 'service_resource_id', 'service_subscription',
56 'service_type', 'subdivision', 'unique', 'vlan_key']
57 CONSTRAINT_KEYS = ['type', 'demands', 'properties']
60 # split: split into individual constraints, one per demand
61 # required: list of required property names,
62 # optional: list of optional property names,
63 # thresholds: dict of property/base-unit pairs for threshold parsing
64 # allowed: dict of keys and allowed values (if controlled vocab);
65 # only use this for Conductor-controlled values!
69 'required': ['evaluate'],
73 'required': ['attribute', 'threshold', 'operator'],
76 'distance_between_demands': {
77 'required': ['distance'],
79 'distance': 'distance'
82 'distance_to_location': {
84 'required': ['distance', 'location'],
86 'distance': 'distance'
91 'required': ['controller'],
92 'optional': ['request'],
94 'inventory_group': {},
97 'required': ['controller'],
98 'optional': ['request'],
101 'required': ['qualifier', 'category'],
102 'optional': ['location'],
103 'allowed': {'qualifier': ['same', 'different'],
104 'category': ['disaster', 'region', 'complex', 'country',
105 'time', 'maintenance']},
109 'required': ['controller'],
110 'optional': ['request'],
114 'required': ['evaluate'],
117 HPA_FEATURES = ['architecture', 'hpa-feature', 'hpa-feature-attributes',
118 'hpa-version', 'mandatory', 'directives']
119 HPA_OPTIONAL = ['score']
120 HPA_ATTRIBUTES = ['hpa-attribute-key', 'hpa-attribute-value', 'operator']
121 HPA_ATTRIBUTES_OPTIONAL = ['unit']
124 class TranslatorException(Exception):
128 class Translator(object):
129 """Template translator.
131 Takes an input template and translates it into
132 something the solver can use. Calls the data service
133 as needed, giving it the inventory provider as context.
134 Presently the only inventory provider is A&AI. Others
135 may be added in the future.
138 def __init__(self, conf, plan_name, plan_id, template):
140 self._template = copy.deepcopy(template)
141 self._plan_name = plan_name
142 self._plan_id = plan_id
143 self._translation = None
146 self.triageTranslatorData= TraigeTranslatorData()
147 self.triageTranslator = TraigeTranslator()
148 # Set up the RPC service(s) we want to talk to.
149 self.data_service = self.setup_rpc(self.conf, "data")
151 def setup_rpc(self, conf, topic):
152 """Set up the RPC Client"""
153 # TODO(jdandrea): Put this pattern inside music_messaging?
154 transport = messaging.get_transport(conf=conf)
155 target = music_messaging.Target(topic=topic)
156 client = music_messaging.RPCClient(conf=conf,
161 def create_components(self):
162 # TODO(jdandrea): Make deep copies so the template is untouched
163 self._version = self._template.get("homing_template_version")
164 self._parameters = self._template.get("parameters", {})
165 self._locations = self._template.get("locations", {})
166 self._demands = self._template.get("demands", {})
167 self._constraints = self._template.get("constraints", {})
168 self._optmization = self._template.get("optimization", {})
169 self._reservations = self._template.get("reservation", {})
171 if isinstance(self._version, datetime.date):
172 self._version = str(self._version)
174 def validate_components(self):
175 """Cursory validation of template components.
177 More detailed validation happens while parsing each component.
182 if self._version not in VERSIONS:
183 raise TranslatorException(
184 "conductor_template_version must be one "
185 "of: {}".format(', '.join(VERSIONS)))
187 # Check top level structure
191 "content": self._parameters,
195 "keys": LOCATION_KEYS,
196 "content": self._locations,
200 "content": self._demands,
203 "name": "Constraint",
204 "keys": CONSTRAINT_KEYS,
205 "content": self._constraints,
208 "name": "Optimization",
209 "content": self._optmization,
212 "name": "Reservation",
213 "content": self._reservations,
216 for name, component in components.items():
217 name = component.get('name')
218 keys = component.get('keys', None)
219 content = component.get('content')
221 if type(content) is not dict:
222 raise TranslatorException(
223 "{} section must be a dictionary".format(name))
224 for content_name, content_def in content.items():
228 for key in content_def:
230 raise TranslatorException(
231 "{} {} has an invalid key {}".format(
232 name, content_name, key))
234 demand_keys = list(self._demands.keys()) # Python 3 Conversion -- dict object to list object
235 location_keys = list(self._locations.keys()) # Python 3 Conversion -- dict object to list object
236 for constraint_name, constraint in self._constraints.items():
238 # Require a single demand (string), or a list of one or more.
239 demands = constraint.get('demands')
240 if isinstance(demands, six.string_types):
242 if not isinstance(demands, list) or len(demands) < 1:
243 raise TranslatorException(
244 "Demand list for Constraint {} must be "
245 "a list of names or a string with one name".format(
247 if not set(demands).issubset(demand_keys + location_keys):
248 raise TranslatorException(
249 "Undefined Demand(s) {} in Constraint '{}'".format(
250 list(set(demands).difference(demand_keys)),
253 properties = constraint.get('properties', None)
255 location = properties.get('location', None)
257 if location not in location_keys:
258 raise TranslatorException(
259 "Location {} in Constraint {} is undefined".format(
260 location, constraint_name))
264 def _parse_parameters(self, obj, path=[]):
265 """Recursively parse all {get_param: X} occurrences
267 This modifies obj in-place. If you want to keep the original,
270 # Ok to start with a string ...
271 if isinstance(path, six.string_types):
272 # ... but the breadcrumb trail goes in an array.
276 if type(obj) is list:
277 for idx, val in enumerate(obj, start=0):
278 # Add path to the breadcrumb trail
279 new_path = list(path)
280 new_path[-1] += "[{}]".format(idx)
282 # Look at each element.
283 obj[idx] = self._parse_parameters(val, new_path)
286 elif type(obj) is dict:
287 # Did we find a "{get_param: ...}" intrinsic?
288 if list(obj.keys()) == ['get_param']:
289 param_name = obj['get_param']
291 # The parameter name must be a string.
292 if not isinstance(param_name, six.string_types):
293 path_str = ' > '.join(path)
294 raise TranslatorException(
295 "Parameter name '{}' not a string in path {}".format(
296 param_name, path_str))
298 # Parameter name must be defined.
299 if param_name not in self._parameters:
300 path_str = ' > '.join(path)
301 raise TranslatorException(
302 "Parameter '{}' undefined in path {}".format(
303 param_name, path_str))
305 # Return the value in place of the call.
306 return self._parameters.get(param_name)
308 # Not an intrinsic. Traverse as usual.
309 for key in list(obj.keys()):
310 # Add path to the breadcrumb trail.
311 new_path = list(path)
314 # Look at each key/value pair.
315 obj[key] = self._parse_parameters(obj[key], new_path)
317 # Return whatever we have after unwinding.
320 def parse_parameters(self):
321 """Resolve all parameters references."""
322 locations = copy.deepcopy(self._locations)
323 self._locations = self._parse_parameters(locations, 'locations')
325 demands = copy.deepcopy(self._demands)
326 self._demands = self._parse_parameters(demands, 'demands')
328 constraints = copy.deepcopy(self._constraints)
329 self._constraints = self._parse_parameters(constraints, 'constraints')
331 reservations = copy.deepcopy(self._reservations)
332 self._reservations = self._parse_parameters(reservations,
335 def parse_locations(self, locations):
336 """Prepare the locations for use by the solver."""
338 for location, args in locations.items():
340 'plan_id': self._plan_id,
341 'keyspace': self.conf.keyspace
344 latitude = args.get("latitude")
345 longitude = args.get("longitude")
347 if latitude and longitude:
348 resolved_location = {"latitude": latitude, "longitude": longitude}
351 response = self.data_service.call(
353 method="resolve_location",
356 resolved_location = \
357 response and response.get('resolved_location')
358 if not resolved_location:
359 raise TranslatorException(
360 "Unable to resolve location {}".format(location)
362 parsed[location] = resolved_location
365 def parse_demands(self, demands):
366 """Validate/prepare demands for use by the solver."""
367 if type(demands) is not dict:
368 raise TranslatorException("Demands must be provided in "
371 # Look at each demand
372 demands_copy = copy.deepcopy(demands)
374 for name, requirements in demands_copy.items():
375 inventory_candidates = []
376 for requirement in requirements:
377 for key in requirement:
378 if key not in DEMAND_KEYS:
379 raise TranslatorException(
380 "Demand {} has an invalid key {}".format(
383 if 'candidates' in requirement:
384 # Candidates *must* specify an inventory provider
385 provider = requirement.get("inventory_provider")
386 if provider and provider not in INVENTORY_PROVIDERS:
387 raise TranslatorException(
388 "Unsupported inventory provider {} "
389 "in demand {}".format(provider, name))
391 provider = DEFAULT_INVENTORY_PROVIDER
393 # Check each candidate
394 for candidate in requirement.get('candidates'):
395 # Must be a dictionary
396 if type(candidate) is not dict:
397 raise TranslatorException(
398 "Candidate found in demand {} that is "
399 "not a dictionary".format(name))
401 # Must have only supported keys
402 for key in list(candidate.keys()):
403 if key not in CANDIDATE_KEYS:
404 raise TranslatorException(
405 "Candidate with invalid key {} found "
406 "in demand {}".format(key, name)
409 # TODO(jdandrea): Check required/optional keys
411 # Set the inventory provider if not already
412 candidate['inventory_provider'] = \
413 candidate.get('inventory_provider', provider)
415 # Set cost if not already (default cost is 0?)
416 candidate['cost'] = candidate.get('cost', 0)
418 # Add to our list of parsed candidates
419 inventory_candidates.append(candidate)
421 # candidates are specified through inventory providers
422 # Do the basic sanity checks for inputs
424 # inventory provider MUST be specified
425 provider = requirement.get("inventory_provider")
427 raise TranslatorException(
428 "Inventory provider not specified "
429 "in demand {}".format(name)
431 elif provider and provider not in INVENTORY_PROVIDERS:
432 raise TranslatorException(
433 "Unsupported inventory provider {} "
434 "in demand {}".format(provider, name)
437 provider = DEFAULT_INVENTORY_PROVIDER
438 requirement['provider'] = provider
440 # inventory type MUST be specified
441 inventory_type = requirement.get('inventory_type')
442 if not inventory_type or inventory_type == '':
443 raise TranslatorException(
444 "Inventory type not specified for "
445 "demand {}".format(name)
447 if inventory_type and \
448 inventory_type not in INVENTORY_TYPES:
449 raise TranslatorException(
450 "Unknown inventory type {} specified for "
451 "demand {}".format(inventory_type, name)
454 # For service and vfmodule inventories, customer_id and
455 # service_type MUST be specified
456 if inventory_type == 'service' or inventory_type == 'vfmodule':
457 filtering_attributes = requirement.get('filtering_attributes')
459 if filtering_attributes:
460 customer_id = filtering_attributes.get('customer-id')
461 global_customer_id = filtering_attributes.get('global-customer-id')
462 if global_customer_id:
463 customer_id = global_customer_id
465 # for backward compatibility
466 customer_id = requirement.get('customer_id')
467 service_type = requirement.get('service_type')
470 raise TranslatorException(
471 "Customer ID not specified for "
472 "demand {}".format(name)
474 if not filtering_attributes and not service_type:
475 raise TranslatorException(
476 "Service Type not specified for "
477 "demand {}".format(name)
480 # TODO(jdandrea): Check required/optional keys for requirement
481 # elif 'inventory_type' in requirement:
482 # # For now this is just a stand-in candidate
484 # 'inventory_provider':
485 # requirement.get('inventory_provider',
486 # DEFAULT_INVENTORY_PROVIDER),
488 # requirement.get('inventory_type', ''),
489 # 'candidate_id': '',
491 # 'location_type': '',
495 # # Add to our list of parsed candidates
496 # inventory_candidates.append(candidate)
498 # Ask conductor-data for one or more candidates.
500 "plan_id": self._plan_id,
501 "plan_name": self._plan_name,
502 "keyspace": self.conf.keyspace,
509 "plan_id": self._plan_id,
510 "plan_name": self._plan_name
512 "triage_translator_data": self.triageTranslatorData.__dict__
516 # Check if required_candidate and excluded candidate
517 # are mutually exclusive.
518 for requirement in requirements:
519 required_candidates = requirement.get("required_candidates")
520 excluded_candidates = requirement.get("excluded_candidates")
521 if (required_candidates and
522 excluded_candidates and
523 set(map(lambda entry: entry['candidate_id'],
524 required_candidates))
525 & set(map(lambda entry: entry['candidate_id'],
526 excluded_candidates))):
527 raise TranslatorException(
528 "Required candidate list and excluded candidate"
529 " list are not mutually exclusive for demand"
532 response = self.data_service.call(
534 method="resolve_demands",
538 response and response.get('resolved_demands')
539 triage_data_trans = \
540 response and response.get('trans')
542 required_candidates = resolved_demands \
543 .get('required_candidates')
544 if not resolved_demands:
545 self.triageTranslator.thefinalCallTrans(triage_data_trans)
546 raise TranslatorException(
547 "Unable to resolve inventory "
548 "candidates for demand {}"
551 resolved_candidates = resolved_demands.get(name)
552 for candidate in resolved_candidates:
553 inventory_candidates.append(candidate)
554 if len(inventory_candidates) < 1:
555 if not required_candidates:
556 self.triageTranslator.thefinalCallTrans(triage_data_trans)
557 raise TranslatorException(
558 "Unable to find any candidate for "
559 "demand {}".format(name)
562 self.triageTranslator.thefinalCallTrans(triage_data_trans)
563 raise TranslatorException(
564 "Unable to find any required "
565 "candidate for demand {}"
569 "candidates": inventory_candidates,
571 self.triageTranslator.thefinalCallTrans(triage_data_trans)
574 def validate_hpa_constraints(self, req_prop, value):
575 for para in value.get(req_prop):
576 # Make sure there is at least one
577 # set of id, type, directives and flavorProperties
578 if not para.get('id') \
579 or not para.get('type') \
580 or not para.get('directives') \
581 or not para.get('flavorProperties') \
582 or para.get('id') == '' \
583 or para.get('type') == '' \
584 or not isinstance(para.get('directives'), list) \
585 or para.get('flavorProperties') == '':
586 raise TranslatorException(
587 "HPA requirements need at least "
588 "one set of id, type, directives and flavorProperties"
590 for feature in para.get('flavorProperties'):
591 if type(feature) is not dict:
592 raise TranslatorException("HPA feature must be a dict")
593 # process mandatory parameter
594 hpa_mandatory = set(HPA_FEATURES).difference(feature.keys())
595 if bool(hpa_mandatory):
596 raise TranslatorException(
597 "Lack of compulsory elements inside HPA feature")
598 # process optional parameter
599 hpa_optional = set(feature.keys()).difference(HPA_FEATURES)
600 if hpa_optional and not hpa_optional.issubset(HPA_OPTIONAL):
601 raise TranslatorException(
602 "Got unrecognized elements inside HPA feature")
603 if feature.get('mandatory') == 'False' and not feature.get(
605 raise TranslatorException(
606 "Score needs to be present if mandatory is False")
608 for attr in feature.get('hpa-feature-attributes'):
609 if type(attr) is not dict:
610 raise TranslatorException(
611 "HPA feature attributes must be a dict")
613 # process mandatory hpa attribute parameter
614 hpa_attr_mandatory = set(HPA_ATTRIBUTES).difference(
616 if bool(hpa_attr_mandatory):
617 raise TranslatorException(
618 "Lack of compulsory elements inside HPA "
619 "feature attributes")
620 # process optional hpa attribute parameter
621 hpa_attr_optional = set(attr.keys()).difference(
623 if hpa_attr_optional and not hpa_attr_optional.issubset(
624 HPA_ATTRIBUTES_OPTIONAL):
625 raise TranslatorException(
626 "Invalid attributes '{}' found inside HPA "
627 "feature attributes".format(hpa_attr_optional))
629 def parse_constraints(self, constraints):
630 """Validate/prepare constraints for use by the solver."""
631 if not isinstance(constraints, dict):
632 raise TranslatorException("Constraints must be provided in "
635 # Look at each constraint. Properties must exist, even if empty.
636 constraints_copy = copy.deepcopy(constraints)
639 for name, constraint in constraints_copy.items():
641 if not constraint.get('properties'):
642 constraint['properties'] = {}
644 constraint_type = constraint.get('type')
645 constraint_def = CONSTRAINTS.get(constraint_type)
647 # Is it a supported type?
648 if constraint_type not in CONSTRAINTS:
649 raise TranslatorException(
650 "Unsupported type '{}' found in constraint "
651 "named '{}'".format(constraint_type, name))
653 # Now walk through the constraint's content
654 for key, value in constraint.items():
655 # Must be a supported key
656 if key not in CONSTRAINT_KEYS:
657 raise TranslatorException(
658 "Invalid key '{}' found in constraint "
659 "named '{}'".format(key, name))
662 if key == 'properties':
663 # Make sure all required properties are present
664 required = constraint_def.get('required', [])
665 for req_prop in required:
666 if req_prop not in list(value.keys()):
667 raise TranslatorException(
668 "Required property '{}' not found in "
669 "constraint named '{}'".format(
671 if not value.get(req_prop) \
672 or value.get(req_prop) == '':
673 raise TranslatorException(
674 "No value specified for property '{}' in "
675 "constraint named '{}'".format(
677 # For HPA constraints
678 if constraint_type == 'hpa':
679 self.validate_hpa_constraints(req_prop, value)
681 # Make sure there are no unknown properties
682 optional = constraint_def.get('optional', [])
683 for prop_name in list(value.keys()):
684 if prop_name not in required + optional:
685 raise TranslatorException(
686 "Unknown property '{}' in "
687 "constraint named '{}'".format(
690 # If a property has a controlled vocabulary, make
691 # sure its value is one of the allowed ones.
692 allowed = constraint_def.get('allowed', {})
693 for prop_name, allowed_values in allowed.items():
694 if prop_name in list(value.keys()):
695 prop_value = value.get(prop_name, '')
696 if prop_value not in allowed_values:
697 raise TranslatorException(
698 "Property '{}' value '{}' unsupported in "
699 "constraint named '{}' (must be one of "
700 "{})".format(prop_name, prop_value,
701 name, allowed_values))
703 # Break all threshold-formatted values into parts
704 thresholds = constraint_def.get('thresholds', {})
705 for thr_prop, base_units in thresholds.items():
706 if thr_prop in list(value.keys()):
707 expression = value.get(thr_prop)
708 thr = threshold.Threshold(expression, base_units)
709 value[thr_prop] = thr.parts
711 # We already know we have one or more demands due to
712 # validate_components(). We still need to coerce the demands
713 # into a list in case only one demand was provided.
714 constraint_demands = constraint.get('demands')
715 if isinstance(constraint_demands, six.string_types):
716 constraint['demands'] = [constraint_demands]
718 # Either split the constraint into parts, one per demand,
720 if constraint_def.get('split'):
721 for demand in constraint.get('demands', []):
722 constraint_demand = name + '_' + demand
723 parsed[constraint_demand] = copy.deepcopy(constraint)
724 parsed[constraint_demand]['name'] = name
725 parsed[constraint_demand]['demands'] = demand
727 parsed[name] = copy.deepcopy(constraint)
728 parsed[name]['name'] = name
732 def parse_optimization(self, optimization):
733 """Validate/prepare optimization for use by the solver."""
735 # WARNING: The template format for optimization is generalized,
736 # however the solver is very particular about the expected
737 # goal, functions, and operands. Therefore, for the time being,
738 # we are choosing to be highly conservative in what we accept
739 # at the template level. Once the solver can handle the more
740 # general form, we can make the translation pass in this
741 # essentially pre-parsed formula unchanged, or we may allow
742 # optimizations to be written in algebraic form and pre-parsed
743 # with antlr4-python2-runtime. (jdandrea 1 Dec 2016)
746 LOG.debug("No objective function or "
747 "optimzation provided in the template")
750 optimization_copy = copy.deepcopy(optimization)
757 if type(optimization_copy) is not dict:
758 raise TranslatorException("Optimization must be a dictionary.")
760 goals = list(optimization_copy.keys())
761 if goals != ['minimize']:
762 raise TranslatorException(
763 "Optimization must contain a single goal of 'minimize'.")
765 funcs = list(optimization_copy['minimize'].keys())
767 raise TranslatorException(
768 "Optimization goal 'minimize' must "
769 "contain a single function of 'sum'.")
770 operands = optimization_copy['minimize']['sum']
771 if type(operands) is not list:
772 # or len(operands) != 2:
773 raise TranslatorException(
774 "Optimization goal 'minimize', function 'sum' "
775 "must be a list of exactly two operands.")
777 def get_latency_between_args(operand):
778 args = operand.get('latency_between')
779 if type(args) is not list and len(args) != 2:
780 raise TranslatorException(
781 "Optimization 'latency_between' arguments must "
782 "be a list of length two.")
787 if not got_demand and arg in list(self._demands.keys()):
789 if not got_location and arg in list(self._locations.keys()):
791 if not got_demand or not got_location:
792 raise TranslatorException(
793 "Optimization 'latency_between' arguments {} must "
794 "include one valid demand name and one valid "
795 "location name.".format(args))
799 def get_distance_between_args(operand):
800 args = operand.get('distance_between')
801 if type(args) is not list and len(args) != 2:
802 raise TranslatorException(
803 "Optimization 'distance_between' arguments must "
804 "be a list of length two.")
809 if not got_demand and arg in list(self._demands.keys()):
811 if not got_location and arg in list(self._locations.keys()):
813 if not got_demand or not got_location:
814 raise TranslatorException(
815 "Optimization 'distance_between' arguments {} must "
816 "include one valid demand name and one valid "
817 "location name.".format(args))
821 for operand in operands:
826 if list(operand.keys()) == ['distance_between']:
827 # Value must be a list of length 2 with one
828 # location and one demand
829 function = 'distance_between'
830 args = get_distance_between_args(operand)
832 elif list(operand.keys()) == ['product']:
833 for product_op in operand['product']:
834 if threshold.is_number(product_op):
836 elif isinstance(product_op, dict):
837 if list(product_op.keys()) == ['latency_between']:
838 function = 'latency_between'
839 args = get_latency_between_args(product_op)
840 elif list(product_op.keys()) == ['distance_between']:
841 function = 'distance_between'
842 args = get_distance_between_args(product_op)
843 elif list(product_op.keys()) == ['aic_version']:
844 function = 'aic_version'
845 args = product_op.get('aic_version')
846 elif list(product_op.keys()) == ['hpa_score']:
847 function = 'hpa_score'
848 args = product_op.get('hpa_score')
849 if not self.is_hpa_policy_exists(args):
850 raise TranslatorException(
851 "HPA Score Optimization must include a "
852 "HPA Policy constraint ")
853 elif list(product_op.keys()) == ['sum']:
855 nested_operands = product_op.get('sum')
856 for nested_operand in nested_operands:
857 if list(nested_operand.keys()) == ['product']:
858 nested_weight = weight
859 for nested_product_op in nested_operand['product']:
860 if threshold.is_number(nested_product_op):
861 nested_weight = nested_weight * int(nested_product_op)
862 elif isinstance(nested_product_op, dict):
863 if list(nested_product_op.keys()) == ['latency_between']:
864 function = 'latency_between'
865 args = get_latency_between_args(nested_product_op)
866 elif list(nested_product_op.keys()) == ['distance_between']:
867 function = 'distance_between'
868 args = get_distance_between_args(nested_product_op)
869 parsed['operands'].append(
871 "operation": "product",
872 "weight": nested_weight,
873 "function": function,
874 "function_param": args,
879 raise TranslatorException(
880 "Optimization products must include at least "
881 "one 'distance_between' function call and "
882 "one optional number to be used as a weight.")
884 # We now have our weight/function_param.
886 parsed['operands'].append(
888 "operation": "product",
890 "function": function,
891 "function_param": args,
896 def is_hpa_policy_exists(self, demand_list):
897 # Check if a HPA constraint exist for the demands in the demand list.
898 constraints_copy = copy.deepcopy(self._constraints)
899 for demand in demand_list:
900 for name, constraint in constraints_copy.items():
901 constraint_type = constraint.get('type')
902 if constraint_type == 'hpa':
903 hpa_demands = constraint.get('demands')
904 if demand in hpa_demands:
908 def parse_reservations(self, reservations):
909 demands = self._demands
910 if not isinstance(reservations, dict):
911 raise TranslatorException("Reservations must be provided in "
915 parsed['counter'] = 0
916 parsed['demands'] = {}
918 for key, value in reservations.items():
920 if key == "service_model":
921 parsed['service_model'] = value
923 elif key == "service_candidates":
924 for name, reservation_details in value.items():
925 if not reservation_details.get('properties'):
926 reservation_details['properties'] = {}
927 for demand in reservation_details.get('demands', []):
928 if demand in list(demands.keys()):
929 reservation_demand = name + '_' + demand
930 parsed['demands'][reservation_demand] = copy.deepcopy(reservation_details)
931 parsed['demands'][reservation_demand]['name'] = name
932 parsed['demands'][reservation_demand]['demands'] = demand
934 raise TranslatorException("Demand {} must be provided in demands section".format(demand))
938 def do_translation(self):
939 """Perform the translation."""
941 raise TranslatorException("Can't translate an invalid template.")
943 request_type = self._parameters.get("request_type") \
944 or self._parameters.get("REQUEST_TYPE") \
947 self._translation = {
948 "conductor_solver": {
949 "version": self._version,
950 "plan_id": self._plan_id,
951 "request_type": request_type,
952 "locations": self.parse_locations(self._locations),
953 "demands": self.parse_demands(self._demands),
954 "objective": self.parse_optimization(self._optmization),
955 "constraints": self.parse_constraints(self._constraints),
956 "objective": self.parse_optimization(self._optmization),
957 "reservations": self.parse_reservations(self._reservations),
962 """Translate the template for the solver."""
965 self.create_components()
966 self.validate_components()
967 self.parse_parameters()
968 self.do_translation()
970 except Exception as exc:
971 self._error_message = exc.args
975 """Returns True if the template has been validated."""
980 """Returns True if the translation was successful."""
984 def translation(self):
985 """Returns the translation if it was successful."""
986 return self._translation
989 def error_message(self):
990 """Returns the last known error message."""
991 return self._error_message
995 template_name = 'some_template'
997 path = os.path.abspath(conductor_root)
998 dir_path = os.path.dirname(path)
1000 # Prepare service-wide components (e.g., config)
1001 conf = service.prepare_service(
1002 [], config_files=[dir_path + '/../etc/conductor/conductor.conf'])
1003 # conf.set_override('mock', True, 'music_api')
1005 t1 = threshold.Threshold("< 500 ms", "time")
1006 t2 = threshold.Threshold("= 120 mi", "distance")
1007 t3 = threshold.Threshold("160", "currency")
1008 t4 = threshold.Threshold("60-80 Gbps", "throughput")
1009 print('t1: {}\nt2: {}\nt3: {}\nt4: {}\n'.format(t1, t2, t3, t4))
1011 template_file = dir_path + '/tests/data/' + template_name + '.yaml'
1012 fd = open(template_file, "r")
1013 template = yaml.load(fd)
1015 trns = Translator(conf, template_name, str(uuid.uuid4()), template)
1018 print(json.dumps(trns.translation, indent=2))
1020 print("TESTING - Translator Error: {}".format(trns.error_message))
1023 if __name__ == '__main__':