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 # -------------------------------------------------------------------------
30 from conductor import __file__ as conductor_root
31 from conductor.common.music import messaging as music_messaging
32 from conductor.common import threshold
33 from conductor.data.plugins.triage_translator.triage_translator import TraigeTranslator
34 from conductor.data.plugins.triage_translator.triage_translator_data import TraigeTranslatorData
35 from conductor import messaging
36 from conductor import service
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', 'generator']
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', 'default_attributes', 'candidates', 'complex',
52 'conflict_identifier', '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': ['evaluate'],
75 'distance_between_demands': {
76 'required': ['distance'],
78 'distance': 'distance'
81 'distance_to_location': {
83 'required': ['distance', 'location'],
85 'distance': 'distance'
90 'required': ['controller'],
91 'optional': ['request'],
93 'inventory_group': {},
96 'required': ['controller'],
97 'optional': ['request'],
100 'required': ['qualifier', 'category'],
101 'optional': ['location'],
102 'allowed': {'qualifier': ['same', 'different'],
103 'category': ['disaster', 'region', 'complex', 'country',
104 'time', 'maintenance']},
108 'required': ['controller'],
109 'optional': ['request'],
113 'required': ['evaluate'],
116 HPA_FEATURES = ['architecture', 'hpa-feature', 'hpa-feature-attributes',
117 'hpa-version', 'mandatory', 'directives']
118 HPA_OPTIONAL = ['score']
119 HPA_ATTRIBUTES = ['hpa-attribute-key', 'hpa-attribute-value', 'operator']
120 HPA_ATTRIBUTES_OPTIONAL = ['unit']
123 class TranslatorException(Exception):
127 class Translator(object):
128 """Template translator.
130 Takes an input template and translates it into
131 something the solver can use. Calls the data service
132 as needed, giving it the inventory provider as context.
133 Presently the only inventory provider is A&AI. Others
134 may be added in the future.
137 def __init__(self, conf, plan_name, plan_id, template):
139 self._template = copy.deepcopy(template)
140 self._plan_name = plan_name
141 self._plan_id = plan_id
142 self._translation = None
145 self.triageTranslatorData = TraigeTranslatorData()
146 self.triageTranslator = TraigeTranslator()
147 # Set up the RPC service(s) we want to talk to.
148 self.data_service = self.setup_rpc(self.conf, "data")
150 def setup_rpc(self, conf, topic):
151 """Set up the RPC Client"""
152 # TODO(jdandrea): Put this pattern inside music_messaging?
153 transport = messaging.get_transport(conf=conf)
154 target = music_messaging.Target(topic=topic)
155 client = music_messaging.RPCClient(conf=conf,
160 def create_components(self):
161 # TODO(jdandrea): Make deep copies so the template is untouched
162 self._version = self._template.get("homing_template_version")
163 self._parameters = self._template.get("parameters", {})
164 self._locations = self._template.get("locations", {})
165 self._demands = self._template.get("demands", {})
166 self._constraints = self._template.get("constraints", {})
167 self._optmization = self._template.get("optimization", {})
168 self._reservations = self._template.get("reservation", {})
170 if isinstance(self._version, datetime.date):
171 self._version = str(self._version)
173 def validate_components(self):
174 """Cursory validation of template components.
176 More detailed validation happens while parsing each component.
181 if self._version not in VERSIONS:
182 raise TranslatorException(
183 "conductor_template_version must be one "
184 "of: {}".format(', '.join(VERSIONS)))
186 # Check top level structure
190 "content": self._parameters,
194 "keys": LOCATION_KEYS,
195 "content": self._locations,
199 "content": self._demands,
202 "name": "Constraint",
203 "keys": CONSTRAINT_KEYS,
204 "content": self._constraints,
207 "name": "Optimization",
208 "content": self._optmization,
211 "name": "Reservation",
212 "content": self._reservations,
215 for name, component in components.items():
216 name = component.get('name')
217 keys = component.get('keys', None)
218 content = component.get('content')
220 if type(content) is not dict:
221 raise TranslatorException(
222 "{} section must be a dictionary".format(name))
223 for content_name, content_def in content.items():
227 for key in content_def:
229 raise TranslatorException(
230 "{} {} has an invalid key {}".format(
231 name, content_name, key))
233 demand_keys = list(self._demands.keys()) # Python 3 Conversion -- dict object to list object
234 location_keys = list(self._locations.keys()) # Python 3 Conversion -- dict object to list object
235 for constraint_name, constraint in self._constraints.items():
237 # Require a single demand (string), or a list of one or more.
238 demands = constraint.get('demands')
239 if isinstance(demands, six.string_types):
241 if not isinstance(demands, list) or len(demands) < 1:
242 raise TranslatorException(
243 "Demand list for Constraint {} must be "
244 "a list of names or a string with one name".format(
246 if not set(demands).issubset(demand_keys + location_keys):
247 raise TranslatorException(
248 "Undefined Demand(s) {} in Constraint '{}'".format(
249 list(set(demands).difference(demand_keys)),
252 properties = constraint.get('properties', None)
254 location = properties.get('location', None)
256 if location not in location_keys:
257 raise TranslatorException(
258 "Location {} in Constraint {} is undefined".format(
259 location, constraint_name))
263 def _parse_parameters(self, obj, path=[]):
264 """Recursively parse all {get_param: X} occurrences
266 This modifies obj in-place. If you want to keep the original,
269 # Ok to start with a string ...
270 if isinstance(path, six.string_types):
271 # ... but the breadcrumb trail goes in an array.
275 if type(obj) is list:
276 for idx, val in enumerate(obj, start=0):
277 # Add path to the breadcrumb trail
278 new_path = list(path)
279 new_path[-1] += "[{}]".format(idx)
281 # Look at each element.
282 obj[idx] = self._parse_parameters(val, new_path)
285 elif type(obj) is dict:
286 # Did we find a "{get_param: ...}" intrinsic?
287 if list(obj.keys()) == ['get_param']:
288 param_name = obj['get_param']
290 # The parameter name must be a string.
291 if not isinstance(param_name, six.string_types):
292 path_str = ' > '.join(path)
293 raise TranslatorException(
294 "Parameter name '{}' not a string in path {}".format(
295 param_name, path_str))
297 # Parameter name must be defined.
298 if param_name not in self._parameters:
299 path_str = ' > '.join(path)
300 raise TranslatorException(
301 "Parameter '{}' undefined in path {}".format(
302 param_name, path_str))
304 # Return the value in place of the call.
305 return self._parameters.get(param_name)
307 # Not an intrinsic. Traverse as usual.
308 for key in list(obj.keys()):
309 # Add path to the breadcrumb trail.
310 new_path = list(path)
313 # Look at each key/value pair.
314 obj[key] = self._parse_parameters(obj[key], new_path)
316 # Return whatever we have after unwinding.
319 def parse_parameters(self):
320 """Resolve all parameters references."""
321 locations = copy.deepcopy(self._locations)
322 self._locations = self._parse_parameters(locations, 'locations')
324 demands = copy.deepcopy(self._demands)
325 self._demands = self._parse_parameters(demands, 'demands')
327 constraints = copy.deepcopy(self._constraints)
328 self._constraints = self._parse_parameters(constraints, 'constraints')
330 reservations = copy.deepcopy(self._reservations)
331 self._reservations = self._parse_parameters(reservations,
334 def parse_locations(self, locations):
335 """Prepare the locations for use by the solver."""
337 for location, args in locations.items():
339 'plan_id': self._plan_id,
340 'keyspace': self.conf.keyspace
343 latitude = args.get("latitude")
344 longitude = args.get("longitude")
346 if latitude and longitude:
347 resolved_location = {"latitude": latitude, "longitude": longitude}
350 response = self.data_service.call(
352 method="resolve_location",
355 resolved_location = \
356 response and response.get('resolved_location')
357 if not resolved_location:
358 raise TranslatorException(
359 "Unable to resolve location {}".format(location)
361 parsed[location] = resolved_location
364 def parse_demands(self, demands):
365 """Validate/prepare demands for use by the solver."""
366 if type(demands) is not dict:
367 raise TranslatorException("Demands must be provided in "
370 # Look at each demand
371 demands_copy = copy.deepcopy(demands)
373 for name, requirements in demands_copy.items():
374 inventory_candidates = []
375 for requirement in requirements:
376 for key in requirement:
377 if key not in DEMAND_KEYS:
378 raise TranslatorException(
379 "Demand {} has an invalid key {}".format(
382 if 'candidates' in requirement:
383 # Candidates *must* specify an inventory provider
384 provider = requirement.get("inventory_provider")
385 if provider and provider not in INVENTORY_PROVIDERS:
386 raise TranslatorException(
387 "Unsupported inventory provider {} "
388 "in demand {}".format(provider, name))
390 provider = DEFAULT_INVENTORY_PROVIDER
392 # Check each candidate
393 for candidate in requirement.get('candidates'):
394 # Must be a dictionary
395 if type(candidate) is not dict:
396 raise TranslatorException(
397 "Candidate found in demand {} that is "
398 "not a dictionary".format(name))
400 # Must have only supported keys
401 for key in list(candidate.keys()):
402 if key not in CANDIDATE_KEYS:
403 raise TranslatorException(
404 "Candidate with invalid key {} found "
405 "in demand {}".format(key, name)
408 # TODO(jdandrea): Check required/optional keys
410 # Set the inventory provider if not already
411 candidate['inventory_provider'] = \
412 candidate.get('inventory_provider', provider)
414 # Set cost if not already (default cost is 0?)
415 candidate['cost'] = candidate.get('cost', 0)
417 # Add to our list of parsed candidates
418 inventory_candidates.append(candidate)
420 # candidates are specified through inventory providers
421 # Do the basic sanity checks for inputs
423 # inventory provider MUST be specified
424 provider = requirement.get("inventory_provider")
426 raise TranslatorException(
427 "Inventory provider not specified "
428 "in demand {}".format(name)
430 elif provider and provider not in INVENTORY_PROVIDERS:
431 raise TranslatorException(
432 "Unsupported inventory provider {} "
433 "in demand {}".format(provider, name)
436 provider = DEFAULT_INVENTORY_PROVIDER
437 requirement['provider'] = provider
439 # inventory type MUST be specified
440 inventory_type = requirement.get('inventory_type')
441 if not inventory_type or inventory_type == '':
442 raise TranslatorException(
443 "Inventory type not specified for "
444 "demand {}".format(name)
446 if inventory_type and \
447 inventory_type not in INVENTORY_TYPES:
448 raise TranslatorException(
449 "Unknown inventory type {} specified for "
450 "demand {}".format(inventory_type, name)
453 # For service and vfmodule inventories, customer_id and
454 # service_type MUST be specified
455 if inventory_type == 'service' or inventory_type == 'vfmodule':
456 filtering_attributes = requirement.get('filtering_attributes')
458 if filtering_attributes:
459 customer_id = filtering_attributes.get('customer-id')
460 global_customer_id = filtering_attributes.get('global-customer-id')
461 if global_customer_id:
462 customer_id = global_customer_id
464 # for backward compatibility
465 customer_id = requirement.get('customer_id')
466 service_type = requirement.get('service_type')
469 raise TranslatorException(
470 "Customer ID not specified for "
471 "demand {}".format(name)
473 if not filtering_attributes and not service_type:
474 raise TranslatorException(
475 "Service Type not specified for "
476 "demand {}".format(name)
479 # TODO(jdandrea): Check required/optional keys for requirement
480 # elif 'inventory_type' in requirement:
481 # # For now this is just a stand-in candidate
483 # 'inventory_provider':
484 # requirement.get('inventory_provider',
485 # DEFAULT_INVENTORY_PROVIDER),
487 # requirement.get('inventory_type', ''),
488 # 'candidate_id': '',
490 # 'location_type': '',
494 # # Add to our list of parsed candidates
495 # inventory_candidates.append(candidate)
497 # Ask conductor-data for one or more candidates.
499 "plan_id": self._plan_id,
500 "plan_name": self._plan_name,
501 "keyspace": self.conf.keyspace,
508 "plan_id": self._plan_id,
509 "plan_name": self._plan_name
511 "triage_translator_data": self.triageTranslatorData.__dict__
515 # Check if required_candidate and excluded candidate
516 # are mutually exclusive.
517 for requirement in requirements:
518 required_candidates = requirement.get("required_candidates")
519 excluded_candidates = requirement.get("excluded_candidates")
520 if (required_candidates and excluded_candidates and set(map(lambda entry: entry['candidate_id'],
521 required_candidates))
522 & set(map(lambda entry: entry['candidate_id'],
523 excluded_candidates))):
524 raise TranslatorException(
525 "Required candidate list and excluded candidate"
526 " list are not mutually exclusive for demand"
529 response = self.data_service.call(
531 method="resolve_demands",
535 response and response.get('resolved_demands')
536 triage_data_trans = \
537 response and response.get('trans')
539 required_candidates = resolved_demands \
540 .get('required_candidates')
541 if not resolved_demands:
542 self.triageTranslator.thefinalCallTrans(triage_data_trans)
543 raise TranslatorException(
544 "Unable to resolve inventory "
545 "candidates for demand {}"
548 resolved_candidates = resolved_demands.get(name)
549 for candidate in resolved_candidates:
550 inventory_candidates.append(candidate)
551 if len(inventory_candidates) < 1:
552 if not required_candidates:
553 self.triageTranslator.thefinalCallTrans(triage_data_trans)
554 raise TranslatorException(
555 "Unable to find any candidate for "
556 "demand {}".format(name)
559 self.triageTranslator.thefinalCallTrans(triage_data_trans)
560 raise TranslatorException(
561 "Unable to find any required "
562 "candidate for demand {}"
566 "candidates": inventory_candidates,
568 self.triageTranslator.thefinalCallTrans(triage_data_trans)
571 def validate_hpa_constraints(self, req_prop, value):
572 for para in value.get(req_prop):
573 # Make sure there is at least one
574 # set of id, type, directives and flavorProperties
575 if not para.get('id') \
576 or not para.get('type') \
577 or not para.get('directives') \
578 or not para.get('flavorProperties') \
579 or para.get('id') == '' \
580 or para.get('type') == '' \
581 or not isinstance(para.get('directives'), list) \
582 or para.get('flavorProperties') == '':
583 raise TranslatorException(
584 "HPA requirements need at least "
585 "one set of id, type, directives and flavorProperties"
587 for feature in para.get('flavorProperties'):
588 if type(feature) is not dict:
589 raise TranslatorException("HPA feature must be a dict")
590 # process mandatory parameter
591 hpa_mandatory = set(HPA_FEATURES).difference(feature.keys())
592 if bool(hpa_mandatory):
593 raise TranslatorException(
594 "Lack of compulsory elements inside HPA feature")
595 # process optional parameter
596 hpa_optional = set(feature.keys()).difference(HPA_FEATURES)
597 if hpa_optional and not hpa_optional.issubset(HPA_OPTIONAL):
598 raise TranslatorException(
599 "Got unrecognized elements inside HPA feature")
600 if feature.get('mandatory') == 'False' and not feature.get(
602 raise TranslatorException(
603 "Score needs to be present if mandatory is False")
605 for attr in feature.get('hpa-feature-attributes'):
606 if type(attr) is not dict:
607 raise TranslatorException(
608 "HPA feature attributes must be a dict")
610 # process mandatory hpa attribute parameter
611 hpa_attr_mandatory = set(HPA_ATTRIBUTES).difference(
613 if bool(hpa_attr_mandatory):
614 raise TranslatorException(
615 "Lack of compulsory elements inside HPA "
616 "feature attributes")
617 # process optional hpa attribute parameter
618 hpa_attr_optional = set(attr.keys()).difference(
620 if hpa_attr_optional and not hpa_attr_optional.issubset(
621 HPA_ATTRIBUTES_OPTIONAL):
622 raise TranslatorException(
623 "Invalid attributes '{}' found inside HPA "
624 "feature attributes".format(hpa_attr_optional))
626 def parse_constraints(self, constraints):
627 """Validate/prepare constraints for use by the solver."""
628 if not isinstance(constraints, dict):
629 raise TranslatorException("Constraints must be provided in "
632 # Look at each constraint. Properties must exist, even if empty.
633 constraints_copy = copy.deepcopy(constraints)
636 for name, constraint in constraints_copy.items():
638 if not constraint.get('properties'):
639 constraint['properties'] = {}
641 constraint_type = constraint.get('type')
642 constraint_def = CONSTRAINTS.get(constraint_type)
644 # Is it a supported type?
645 if constraint_type not in CONSTRAINTS:
646 raise TranslatorException(
647 "Unsupported type '{}' found in constraint "
648 "named '{}'".format(constraint_type, name))
650 # Now walk through the constraint's content
651 for key, value in constraint.items():
652 # Must be a supported key
653 if key not in CONSTRAINT_KEYS:
654 raise TranslatorException(
655 "Invalid key '{}' found in constraint "
656 "named '{}'".format(key, name))
659 if key == 'properties':
660 # Make sure all required properties are present
661 required = constraint_def.get('required', [])
662 for req_prop in required:
663 if req_prop not in list(value.keys()):
664 raise TranslatorException(
665 "Required property '{}' not found in "
666 "constraint named '{}'".format(
668 if not value.get(req_prop) \
669 or value.get(req_prop) == '':
670 raise TranslatorException(
671 "No value specified for property '{}' in "
672 "constraint named '{}'".format(
674 # For HPA constraints
675 if constraint_type == 'hpa':
676 self.validate_hpa_constraints(req_prop, value)
678 # Make sure there are no unknown properties
679 optional = constraint_def.get('optional', [])
680 for prop_name in list(value.keys()):
681 if prop_name not in required + optional:
682 raise TranslatorException(
683 "Unknown property '{}' in "
684 "constraint named '{}'".format(
687 # If a property has a controlled vocabulary, make
688 # sure its value is one of the allowed ones.
689 allowed = constraint_def.get('allowed', {})
690 for prop_name, allowed_values in allowed.items():
691 if prop_name in list(value.keys()):
692 prop_value = value.get(prop_name, '')
693 if prop_value not in allowed_values:
694 raise TranslatorException(
695 "Property '{}' value '{}' unsupported in "
696 "constraint named '{}' (must be one of "
697 "{})".format(prop_name, prop_value,
698 name, allowed_values))
700 # Break all threshold-formatted values into parts
701 thresholds = constraint_def.get('thresholds', {})
702 for thr_prop, base_units in thresholds.items():
703 if thr_prop in list(value.keys()):
704 expression = value.get(thr_prop)
705 thr = threshold.Threshold(expression, base_units)
706 value[thr_prop] = thr.parts
708 # We already know we have one or more demands due to
709 # validate_components(). We still need to coerce the demands
710 # into a list in case only one demand was provided.
711 constraint_demands = constraint.get('demands')
712 if isinstance(constraint_demands, six.string_types):
713 constraint['demands'] = [constraint_demands]
715 # Either split the constraint into parts, one per demand,
717 if constraint_def.get('split'):
718 for demand in constraint.get('demands', []):
719 constraint_demand = name + '_' + demand
720 parsed[constraint_demand] = copy.deepcopy(constraint)
721 parsed[constraint_demand]['name'] = name
722 parsed[constraint_demand]['demands'] = demand
724 parsed[name] = copy.deepcopy(constraint)
725 parsed[name]['name'] = name
729 def parse_optimization(self, optimization):
730 """Validate/prepare optimization for use by the solver."""
732 # WARNING: The template format for optimization is generalized,
733 # however the solver is very particular about the expected
734 # goal, functions, and operands. Therefore, for the time being,
735 # we are choosing to be highly conservative in what we accept
736 # at the template level. Once the solver can handle the more
737 # general form, we can make the translation pass in this
738 # essentially pre-parsed formula unchanged, or we may allow
739 # optimizations to be written in algebraic form and pre-parsed
740 # with antlr4-python2-runtime. (jdandrea 1 Dec 2016)
743 LOG.debug("No objective function or "
744 "optimzation provided in the template")
747 optimization_copy = copy.deepcopy(optimization)
754 if type(optimization_copy) is not dict:
755 raise TranslatorException("Optimization must be a dictionary.")
757 goals = list(optimization_copy.keys())
758 if goals != ['minimize']:
759 raise TranslatorException(
760 "Optimization must contain a single goal of 'minimize'.")
762 funcs = list(optimization_copy['minimize'].keys())
764 raise TranslatorException(
765 "Optimization goal 'minimize' must "
766 "contain a single function of 'sum'.")
767 operands = optimization_copy['minimize']['sum']
768 if type(operands) is not list:
769 # or len(operands) != 2:
770 raise TranslatorException(
771 "Optimization goal 'minimize', function 'sum' "
772 "must be a list of exactly two operands.")
774 def get_latency_between_args(operand):
775 args = operand.get('latency_between')
776 if type(args) is not list and len(args) != 2:
777 raise TranslatorException(
778 "Optimization 'latency_between' arguments must "
779 "be a list of length two.")
784 if not got_demand and arg in list(self._demands.keys()):
786 if not got_location and arg in list(self._locations.keys()):
788 if not got_demand or not got_location:
789 raise TranslatorException(
790 "Optimization 'latency_between' arguments {} must "
791 "include one valid demand name and one valid "
792 "location name.".format(args))
796 def get_distance_between_args(operand):
797 args = operand.get('distance_between')
798 if type(args) is not list and len(args) != 2:
799 raise TranslatorException(
800 "Optimization 'distance_between' arguments must "
801 "be a list of length two.")
806 if not got_demand and arg in list(self._demands.keys()):
808 if not got_location and arg in list(self._locations.keys()):
810 if not got_demand or not got_location:
811 raise TranslatorException(
812 "Optimization 'distance_between' arguments {} must "
813 "include one valid demand name and one valid "
814 "location name.".format(args))
818 for operand in operands:
823 if list(operand.keys()) == ['distance_between']:
824 # Value must be a list of length 2 with one
825 # location and one demand
826 function = 'distance_between'
827 args = get_distance_between_args(operand)
829 elif list(operand.keys()) == ['product']:
830 for product_op in operand['product']:
831 if threshold.is_number(product_op):
833 elif isinstance(product_op, dict):
834 if list(product_op.keys()) == ['latency_between']:
835 function = 'latency_between'
836 args = get_latency_between_args(product_op)
837 elif list(product_op.keys()) == ['distance_between']:
838 function = 'distance_between'
839 args = get_distance_between_args(product_op)
840 elif list(product_op.keys()) == ['aic_version']:
841 function = 'aic_version'
842 args = product_op.get('aic_version')
843 elif list(product_op.keys()) == ['hpa_score']:
844 function = 'hpa_score'
845 args = product_op.get('hpa_score')
846 if not self.is_hpa_policy_exists(args):
847 raise TranslatorException(
848 "HPA Score Optimization must include a "
849 "HPA Policy constraint ")
850 elif list(product_op.keys()) == ['sum']:
852 nested_operands = product_op.get('sum')
853 for nested_operand in nested_operands:
854 if list(nested_operand.keys()) == ['product']:
855 nested_weight = weight
856 for nested_product_op in nested_operand['product']:
857 if threshold.is_number(nested_product_op):
858 nested_weight = nested_weight * int(nested_product_op)
859 elif isinstance(nested_product_op, dict):
860 if list(nested_product_op.keys()) == ['latency_between']:
861 function = 'latency_between'
862 args = get_latency_between_args(nested_product_op)
863 elif list(nested_product_op.keys()) == ['distance_between']:
864 function = 'distance_between'
865 args = get_distance_between_args(nested_product_op)
866 parsed['operands'].append(
868 "operation": "product",
869 "weight": nested_weight,
870 "function": function,
871 "function_param": args,
876 raise TranslatorException(
877 "Optimization products must include at least "
878 "one 'distance_between' function call and "
879 "one optional number to be used as a weight.")
881 # We now have our weight/function_param.
883 parsed['operands'].append(
885 "operation": "product",
887 "function": function,
888 "function_param": args,
893 def is_hpa_policy_exists(self, demand_list):
894 # Check if a HPA constraint exist for the demands in the demand list.
895 constraints_copy = copy.deepcopy(self._constraints)
896 for demand in demand_list:
897 for name, constraint in constraints_copy.items():
898 constraint_type = constraint.get('type')
899 if constraint_type == 'hpa':
900 hpa_demands = constraint.get('demands')
901 if demand in hpa_demands:
905 def parse_reservations(self, reservations):
906 demands = self._demands
907 if not isinstance(reservations, dict):
908 raise TranslatorException("Reservations must be provided in "
912 parsed['counter'] = 0
913 parsed['demands'] = {}
915 for key, value in reservations.items():
917 if key == "service_model":
918 parsed['service_model'] = value
920 elif key == "service_candidates":
921 for name, reservation_details in value.items():
922 if not reservation_details.get('properties'):
923 reservation_details['properties'] = {}
924 for demand in reservation_details.get('demands', []):
925 if demand in list(demands.keys()):
926 reservation_demand = name + '_' + demand
927 parsed['demands'][reservation_demand] = copy.deepcopy(reservation_details)
928 parsed['demands'][reservation_demand]['name'] = name
929 parsed['demands'][reservation_demand]['demands'] = demand
931 raise TranslatorException("Demand {} must be provided in demands section".format(demand))
935 def do_translation(self):
936 """Perform the translation."""
938 raise TranslatorException("Can't translate an invalid template.")
940 request_type = self._parameters.get("request_type") or self._parameters.get("REQUEST_TYPE") or ""
942 self._translation = {
943 "conductor_solver": {
944 "version": self._version,
945 "plan_id": self._plan_id,
946 "request_type": request_type,
947 "locations": self.parse_locations(self._locations),
948 "demands": self.parse_demands(self._demands),
949 "objective": self.parse_optimization(self._optmization),
950 "constraints": self.parse_constraints(self._constraints),
951 "reservations": self.parse_reservations(self._reservations),
956 """Translate the template for the solver."""
959 self.create_components()
960 self.validate_components()
961 self.parse_parameters()
962 self.do_translation()
964 except Exception as exc:
965 self._error_message = exc.args
969 """Returns True if the template has been validated."""
974 """Returns True if the translation was successful."""
978 def translation(self):
979 """Returns the translation if it was successful."""
980 return self._translation
983 def error_message(self):
984 """Returns the last known error message."""
985 return self._error_message
989 template_name = 'some_template'
991 path = os.path.abspath(conductor_root)
992 dir_path = os.path.dirname(path)
994 # Prepare service-wide components (e.g., config)
995 conf = service.prepare_service(
996 [], config_files=[dir_path + '/../etc/conductor/conductor.conf'])
997 # conf.set_override('mock', True, 'music_api')
999 t1 = threshold.Threshold("< 500 ms", "time")
1000 t2 = threshold.Threshold("= 120 mi", "distance")
1001 t3 = threshold.Threshold("160", "currency")
1002 t4 = threshold.Threshold("60-80 Gbps", "throughput")
1003 print('t1: {}\nt2: {}\nt3: {}\nt4: {}\n'.format(t1, t2, t3, t4))
1005 template_file = dir_path + '/tests/data/' + template_name + '.yaml'
1006 fd = open(template_file, "r")
1007 template = yaml.load(fd)
1009 trns = Translator(conf, template_name, str(uuid.uuid4()), template)
1012 print(json.dumps(trns.translation, indent=2))
1014 print("TESTING - Translator Error: {}".format(trns.error_message))
1017 if __name__ == '__main__':