2 # -------------------------------------------------------------------------
3 # Copyright (c) 2015-2017 AT&T Intellectual Property
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 # -------------------------------------------------------------------------
28 from conductor import __file__ as conductor_root
29 from conductor import messaging
30 from conductor import service
32 from conductor.common import threshold
33 from conductor.common.music import messaging as music_messaging
34 from conductor.data.plugins.triage_translator.triage_translator_data import TraigeTranslatorData
35 from conductor.data.plugins.triage_translator.triage_translator import TraigeTranslator
36 from oslo_config import cfg
37 from oslo_log import log
39 LOG = log.getLogger(__name__)
43 VERSIONS = ["2016-11-01", "2017-10-10", "2018-02-01"]
44 LOCATION_KEYS = ['latitude', 'longitude', 'host_name', 'clli_code']
45 INVENTORY_PROVIDERS = ['aai']
46 INVENTORY_TYPES = ['cloud', 'service', 'transport', 'vfmodule']
47 DEFAULT_INVENTORY_PROVIDER = INVENTORY_PROVIDERS[0]
48 CANDIDATE_KEYS = ['candidate_id', 'cost', 'inventory_type', 'location_id',
50 DEMAND_KEYS = ['filtering_attributes', 'passthrough_attributes', 'candidates', 'complex', 'conflict_identifier',
51 'customer_id', 'default_cost', 'excluded_candidates',
52 'existing_placement', 'flavor', 'inventory_provider',
53 'inventory_type', 'port_key', 'region', 'required_candidates',
54 'service_id', 'service_resource_id', 'service_subscription',
55 'service_type', 'subdivision', 'unique', 'vlan_key']
56 CONSTRAINT_KEYS = ['type', 'demands', 'properties']
59 # split: split into individual constraints, one per demand
60 # required: list of required property names,
61 # optional: list of optional property names,
62 # thresholds: dict of property/base-unit pairs for threshold parsing
63 # allowed: dict of keys and allowed values (if controlled vocab);
64 # only use this for Conductor-controlled values!
68 'required': ['evaluate'],
70 'distance_between_demands': {
71 'required': ['distance'],
73 'distance': 'distance'
76 'distance_to_location': {
78 'required': ['distance', 'location'],
80 'distance': 'distance'
85 'required': ['controller'],
86 'optional': ['request'],
88 'inventory_group': {},
91 'required': ['controller'],
92 'optional': ['request'],
95 'required': ['qualifier', 'category'],
96 'optional': ['location'],
97 'allowed': {'qualifier': ['same', 'different'],
98 'category': ['disaster', 'region', 'complex', 'country',
99 'time', 'maintenance']},
103 'required': ['controller'],
104 'optional': ['request'],
108 'required': ['evaluate'],
111 HPA_FEATURES = ['architecture', 'hpa-feature', 'hpa-feature-attributes',
112 'hpa-version', 'mandatory', 'directives']
113 HPA_OPTIONAL = ['score']
114 HPA_ATTRIBUTES = ['hpa-attribute-key', 'hpa-attribute-value', 'operator']
115 HPA_ATTRIBUTES_OPTIONAL = ['unit']
118 class TranslatorException(Exception):
122 class Translator(object):
123 """Template translator.
125 Takes an input template and translates it into
126 something the solver can use. Calls the data service
127 as needed, giving it the inventory provider as context.
128 Presently the only inventory provider is A&AI. Others
129 may be added in the future.
132 def __init__(self, conf, plan_name, plan_id, template):
134 self._template = copy.deepcopy(template)
135 self._plan_name = plan_name
136 self._plan_id = plan_id
137 self._translation = None
140 self.triageTranslatorData= TraigeTranslatorData()
141 self.triageTranslator = TraigeTranslator()
142 # Set up the RPC service(s) we want to talk to.
143 self.data_service = self.setup_rpc(self.conf, "data")
145 def setup_rpc(self, conf, topic):
146 """Set up the RPC Client"""
147 # TODO(jdandrea): Put this pattern inside music_messaging?
148 transport = messaging.get_transport(conf=conf)
149 target = music_messaging.Target(topic=topic)
150 client = music_messaging.RPCClient(conf=conf,
155 def create_components(self):
156 # TODO(jdandrea): Make deep copies so the template is untouched
157 self._version = self._template.get("homing_template_version")
158 self._parameters = self._template.get("parameters", {})
159 self._locations = self._template.get("locations", {})
160 self._demands = self._template.get("demands", {})
161 self._constraints = self._template.get("constraints", {})
162 self._optmization = self._template.get("optimization", {})
163 self._reservations = self._template.get("reservation", {})
165 if isinstance(self._version, datetime.date):
166 self._version = str(self._version)
168 def validate_components(self):
169 """Cursory validation of template components.
171 More detailed validation happens while parsing each component.
176 if self._version not in VERSIONS:
177 raise TranslatorException(
178 "conductor_template_version must be one "
179 "of: {}".format(', '.join(VERSIONS)))
181 # Check top level structure
185 "content": self._parameters,
189 "keys": LOCATION_KEYS,
190 "content": self._locations,
194 "content": self._demands,
197 "name": "Constraint",
198 "keys": CONSTRAINT_KEYS,
199 "content": self._constraints,
202 "name": "Optimization",
203 "content": self._optmization,
206 "name": "Reservation",
207 "content": self._reservations,
210 for name, component in components.items():
211 name = component.get('name')
212 keys = component.get('keys', None)
213 content = component.get('content')
215 if type(content) is not dict:
216 raise TranslatorException(
217 "{} section must be a dictionary".format(name))
218 for content_name, content_def in content.items():
222 for key in content_def:
224 raise TranslatorException(
225 "{} {} has an invalid key {}".format(
226 name, content_name, key))
228 demand_keys = self._demands.keys()
229 location_keys = self._locations.keys()
230 for constraint_name, constraint in self._constraints.items():
232 # Require a single demand (string), or a list of one or more.
233 demands = constraint.get('demands')
234 if isinstance(demands, six.string_types):
236 if not isinstance(demands, list) or len(demands) < 1:
237 raise TranslatorException(
238 "Demand list for Constraint {} must be "
239 "a list of names or a string with one name".format(
241 if not set(demands).issubset(demand_keys + location_keys):
242 raise TranslatorException(
243 "Undefined Demand(s) {} in Constraint '{}'".format(
244 list(set(demands).difference(demand_keys)),
247 properties = constraint.get('properties', None)
249 location = properties.get('location', None)
251 if location not in location_keys:
252 raise TranslatorException(
253 "Location {} in Constraint {} is undefined".format(
254 location, constraint_name))
258 def _parse_parameters(self, obj, path=[]):
259 """Recursively parse all {get_param: X} occurrences
261 This modifies obj in-place. If you want to keep the original,
264 # Ok to start with a string ...
265 if isinstance(path, six.string_types):
266 # ... but the breadcrumb trail goes in an array.
270 if type(obj) is list:
271 for idx, val in enumerate(obj, start=0):
272 # Add path to the breadcrumb trail
273 new_path = list(path)
274 new_path[-1] += "[{}]".format(idx)
276 # Look at each element.
277 obj[idx] = self._parse_parameters(val, new_path)
280 elif type(obj) is dict:
281 # Did we find a "{get_param: ...}" intrinsic?
282 if obj.keys() == ['get_param']:
283 param_name = obj['get_param']
285 # The parameter name must be a string.
286 if not isinstance(param_name, six.string_types):
287 path_str = ' > '.join(path)
288 raise TranslatorException(
289 "Parameter name '{}' not a string in path {}".format(
290 param_name, path_str))
292 # Parameter name must be defined.
293 if param_name not in self._parameters:
294 path_str = ' > '.join(path)
295 raise TranslatorException(
296 "Parameter '{}' undefined in path {}".format(
297 param_name, path_str))
299 # Return the value in place of the call.
300 return self._parameters.get(param_name)
302 # Not an intrinsic. Traverse as usual.
303 for key in obj.keys():
304 # Add path to the breadcrumb trail.
305 new_path = list(path)
308 # Look at each key/value pair.
309 obj[key] = self._parse_parameters(obj[key], new_path)
311 # Return whatever we have after unwinding.
314 def parse_parameters(self):
315 """Resolve all parameters references."""
316 locations = copy.deepcopy(self._locations)
317 self._locations = self._parse_parameters(locations, 'locations')
319 demands = copy.deepcopy(self._demands)
320 self._demands = self._parse_parameters(demands, 'demands')
322 constraints = copy.deepcopy(self._constraints)
323 self._constraints = self._parse_parameters(constraints, 'constraints')
325 reservations = copy.deepcopy(self._reservations)
326 self._reservations = self._parse_parameters(reservations,
329 def parse_locations(self, locations):
330 """Prepare the locations for use by the solver."""
332 for location, args in locations.items():
334 'plan_id': self._plan_id,
335 'keyspace': self.conf.keyspace
338 latitude = args.get("latitude")
339 longitude = args.get("longitude")
341 if latitude and longitude:
342 resolved_location = {"latitude": latitude, "longitude": longitude}
345 response = self.data_service.call(
347 method="resolve_location",
350 resolved_location = \
351 response and response.get('resolved_location')
352 if not resolved_location:
353 raise TranslatorException(
354 "Unable to resolve location {}".format(location)
356 parsed[location] = resolved_location
359 def parse_demands(self, demands):
360 """Validate/prepare demands for use by the solver."""
361 if type(demands) is not dict:
362 raise TranslatorException("Demands must be provided in "
365 # Look at each demand
366 demands_copy = copy.deepcopy(demands)
368 for name, requirements in demands_copy.items():
369 inventory_candidates = []
370 for requirement in requirements:
371 for key in requirement:
372 if key not in DEMAND_KEYS:
373 raise TranslatorException(
374 "Demand {} has an invalid key {}".format(
377 if 'candidates' in requirement:
378 # Candidates *must* specify an inventory provider
379 provider = requirement.get("inventory_provider")
380 if provider and provider not in INVENTORY_PROVIDERS:
381 raise TranslatorException(
382 "Unsupported inventory provider {} "
383 "in demand {}".format(provider, name))
385 provider = DEFAULT_INVENTORY_PROVIDER
387 # Check each candidate
388 for candidate in requirement.get('candidates'):
389 # Must be a dictionary
390 if type(candidate) is not dict:
391 raise TranslatorException(
392 "Candidate found in demand {} that is "
393 "not a dictionary".format(name))
395 # Must have only supported keys
396 for key in candidate.keys():
397 if key not in CANDIDATE_KEYS:
398 raise TranslatorException(
399 "Candidate with invalid key {} found "
400 "in demand {}".format(key, name)
403 # TODO(jdandrea): Check required/optional keys
405 # Set the inventory provider if not already
406 candidate['inventory_provider'] = \
407 candidate.get('inventory_provider', provider)
409 # Set cost if not already (default cost is 0?)
410 candidate['cost'] = candidate.get('cost', 0)
412 # Add to our list of parsed candidates
413 inventory_candidates.append(candidate)
415 # candidates are specified through inventory providers
416 # Do the basic sanity checks for inputs
418 # inventory provider MUST be specified
419 provider = requirement.get("inventory_provider")
421 raise TranslatorException(
422 "Inventory provider not specified "
423 "in demand {}".format(name)
425 elif provider and provider not in INVENTORY_PROVIDERS:
426 raise TranslatorException(
427 "Unsupported inventory provider {} "
428 "in demand {}".format(provider, name)
431 provider = DEFAULT_INVENTORY_PROVIDER
432 requirement['provider'] = provider
434 # inventory type MUST be specified
435 inventory_type = requirement.get('inventory_type')
436 if not inventory_type or inventory_type == '':
437 raise TranslatorException(
438 "Inventory type not specified for "
439 "demand {}".format(name)
441 if inventory_type and \
442 inventory_type not in INVENTORY_TYPES:
443 raise TranslatorException(
444 "Unknown inventory type {} specified for "
445 "demand {}".format(inventory_type, name)
448 # For service and vfmodule inventories, customer_id and
449 # service_type MUST be specified
450 if inventory_type == 'service' or inventory_type == 'vfmodule':
451 filtering_attributes = requirement.get('filtering_attributes')
453 if filtering_attributes:
454 customer_id = filtering_attributes.get('customer-id')
455 global_customer_id = filtering_attributes.get('global-customer-id')
456 if global_customer_id:
457 customer_id = global_customer_id
459 # for backward compatibility
460 customer_id = requirement.get('customer_id')
461 service_type = requirement.get('service_type')
464 raise TranslatorException(
465 "Customer ID not specified for "
466 "demand {}".format(name)
468 if not filtering_attributes and not service_type:
469 raise TranslatorException(
470 "Service Type not specified for "
471 "demand {}".format(name)
474 # TODO(jdandrea): Check required/optional keys for requirement
475 # elif 'inventory_type' in requirement:
476 # # For now this is just a stand-in candidate
478 # 'inventory_provider':
479 # requirement.get('inventory_provider',
480 # DEFAULT_INVENTORY_PROVIDER),
482 # requirement.get('inventory_type', ''),
483 # 'candidate_id': '',
485 # 'location_type': '',
489 # # Add to our list of parsed candidates
490 # inventory_candidates.append(candidate)
492 # Ask conductor-data for one or more candidates.
494 "plan_id": self._plan_id,
495 "plan_name": self._plan_name,
496 "keyspace": self.conf.keyspace,
503 "plan_id": self._plan_id,
504 "plan_name": self._plan_name
506 "triage_translator_data": self.triageTranslatorData.__dict__
510 # Check if required_candidate and excluded candidate
511 # are mutually exclusive.
512 for requirement in requirements:
513 required_candidates = requirement.get("required_candidates")
514 excluded_candidates = requirement.get("excluded_candidates")
515 if (required_candidates and
516 excluded_candidates and
517 set(map(lambda entry: entry['candidate_id'],
518 required_candidates))
519 & set(map(lambda entry: entry['candidate_id'],
520 excluded_candidates))):
521 raise TranslatorException(
522 "Required candidate list and excluded candidate"
523 " list are not mutually exclusive for demand"
526 response = self.data_service.call(
528 method="resolve_demands",
532 response and response.get('resolved_demands')
533 triage_data_trans = \
534 response and response.get('trans')
536 required_candidates = resolved_demands \
537 .get('required_candidates')
538 if not resolved_demands:
539 self.triageTranslator.thefinalCallTrans(triage_data_trans)
540 raise TranslatorException(
541 "Unable to resolve inventory "
542 "candidates for demand {}"
545 resolved_candidates = resolved_demands.get(name)
546 for candidate in resolved_candidates:
547 inventory_candidates.append(candidate)
548 if len(inventory_candidates) < 1:
549 if not required_candidates:
550 self.triageTranslator.thefinalCallTrans(triage_data_trans)
551 raise TranslatorException(
552 "Unable to find any candidate for "
553 "demand {}".format(name)
556 self.triageTranslator.thefinalCallTrans(triage_data_trans)
557 raise TranslatorException(
558 "Unable to find any required "
559 "candidate for demand {}"
563 "candidates": inventory_candidates,
565 self.triageTranslator.thefinalCallTrans(triage_data_trans)
568 def validate_hpa_constraints(self, req_prop, value):
569 for para in value.get(req_prop):
570 # Make sure there is at least one
571 # set of id, type, directives and flavorProperties
572 if not para.get('id') \
573 or not para.get('type') \
574 or not para.get('directives') \
575 or not para.get('flavorProperties') \
576 or para.get('id') == '' \
577 or para.get('type') == '' \
578 or not isinstance(para.get('directives'), list) \
579 or para.get('flavorProperties') == '':
580 raise TranslatorException(
581 "HPA requirements need at least "
582 "one set of id, type, directives and flavorProperties"
584 for feature in para.get('flavorProperties'):
585 if type(feature) is not dict:
586 raise TranslatorException("HPA feature must be a dict")
587 # process mandatory parameter
588 hpa_mandatory = set(HPA_FEATURES).difference(feature.keys())
589 if bool(hpa_mandatory):
590 raise TranslatorException(
591 "Lack of compulsory elements inside HPA feature")
592 # process optional parameter
593 hpa_optional = set(feature.keys()).difference(HPA_FEATURES)
594 if hpa_optional and not hpa_optional.issubset(HPA_OPTIONAL):
595 raise TranslatorException(
596 "Got unrecognized elements inside HPA feature")
597 if feature.get('mandatory') == 'False' and not feature.get(
599 raise TranslatorException(
600 "Score needs to be present if mandatory is False")
602 for attr in feature.get('hpa-feature-attributes'):
603 if type(attr) is not dict:
604 raise TranslatorException(
605 "HPA feature attributes must be a dict")
607 # process mandatory hpa attribute parameter
608 hpa_attr_mandatory = set(HPA_ATTRIBUTES).difference(
610 if bool(hpa_attr_mandatory):
611 raise TranslatorException(
612 "Lack of compulsory elements inside HPA "
613 "feature attributes")
614 # process optional hpa attribute parameter
615 hpa_attr_optional = set(attr.keys()).difference(
617 if hpa_attr_optional and not hpa_attr_optional.issubset(
618 HPA_ATTRIBUTES_OPTIONAL):
619 raise TranslatorException(
620 "Invalid attributes '{}' found inside HPA "
621 "feature attributes".format(hpa_attr_optional))
623 def parse_constraints(self, constraints):
624 """Validate/prepare constraints for use by the solver."""
625 if not isinstance(constraints, dict):
626 raise TranslatorException("Constraints must be provided in "
629 # Look at each constraint. Properties must exist, even if empty.
630 constraints_copy = copy.deepcopy(constraints)
633 for name, constraint in constraints_copy.items():
635 if not constraint.get('properties'):
636 constraint['properties'] = {}
638 constraint_type = constraint.get('type')
639 constraint_def = CONSTRAINTS.get(constraint_type)
641 # Is it a supported type?
642 if constraint_type not in CONSTRAINTS:
643 raise TranslatorException(
644 "Unsupported type '{}' found in constraint "
645 "named '{}'".format(constraint_type, name))
647 # Now walk through the constraint's content
648 for key, value in constraint.items():
649 # Must be a supported key
650 if key not in CONSTRAINT_KEYS:
651 raise TranslatorException(
652 "Invalid key '{}' found in constraint "
653 "named '{}'".format(key, name))
656 if key == 'properties':
657 # Make sure all required properties are present
658 required = constraint_def.get('required', [])
659 for req_prop in required:
660 if req_prop not in value.keys():
661 raise TranslatorException(
662 "Required property '{}' not found in "
663 "constraint named '{}'".format(
665 if not value.get(req_prop) \
666 or value.get(req_prop) == '':
667 raise TranslatorException(
668 "No value specified for property '{}' in "
669 "constraint named '{}'".format(
671 # For HPA constraints
672 if constraint_type == 'hpa':
673 self.validate_hpa_constraints(req_prop, value)
675 # Make sure there are no unknown properties
676 optional = constraint_def.get('optional', [])
677 for prop_name in value.keys():
678 if prop_name not in required + optional:
679 raise TranslatorException(
680 "Unknown property '{}' in "
681 "constraint named '{}'".format(
684 # If a property has a controlled vocabulary, make
685 # sure its value is one of the allowed ones.
686 allowed = constraint_def.get('allowed', {})
687 for prop_name, allowed_values in allowed.items():
688 if prop_name in value.keys():
689 prop_value = value.get(prop_name, '')
690 if prop_value not in allowed_values:
691 raise TranslatorException(
692 "Property '{}' value '{}' unsupported in "
693 "constraint named '{}' (must be one of "
694 "{})".format(prop_name, prop_value,
695 name, allowed_values))
697 # Break all threshold-formatted values into parts
698 thresholds = constraint_def.get('thresholds', {})
699 for thr_prop, base_units in thresholds.items():
700 if thr_prop in value.keys():
701 expression = value.get(thr_prop)
702 thr = threshold.Threshold(expression, base_units)
703 value[thr_prop] = thr.parts
705 # We already know we have one or more demands due to
706 # validate_components(). We still need to coerce the demands
707 # into a list in case only one demand was provided.
708 constraint_demands = constraint.get('demands')
709 if isinstance(constraint_demands, six.string_types):
710 constraint['demands'] = [constraint_demands]
712 # Either split the constraint into parts, one per demand,
714 if constraint_def.get('split'):
715 for demand in constraint.get('demands', []):
716 constraint_demand = name + '_' + demand
717 parsed[constraint_demand] = copy.deepcopy(constraint)
718 parsed[constraint_demand]['name'] = name
719 parsed[constraint_demand]['demands'] = demand
721 parsed[name] = copy.deepcopy(constraint)
722 parsed[name]['name'] = name
726 def parse_optimization(self, optimization):
727 """Validate/prepare optimization for use by the solver."""
729 # WARNING: The template format for optimization is generalized,
730 # however the solver is very particular about the expected
731 # goal, functions, and operands. Therefore, for the time being,
732 # we are choosing to be highly conservative in what we accept
733 # at the template level. Once the solver can handle the more
734 # general form, we can make the translation pass in this
735 # essentially pre-parsed formula unchanged, or we may allow
736 # optimizations to be written in algebraic form and pre-parsed
737 # with antlr4-python2-runtime. (jdandrea 1 Dec 2016)
740 LOG.debug("No objective function or "
741 "optimzation provided in the template")
744 optimization_copy = copy.deepcopy(optimization)
751 if type(optimization_copy) is not dict:
752 raise TranslatorException("Optimization must be a dictionary.")
754 goals = optimization_copy.keys()
755 if goals != ['minimize']:
756 raise TranslatorException(
757 "Optimization must contain a single goal of 'minimize'.")
759 funcs = optimization_copy['minimize'].keys()
761 raise TranslatorException(
762 "Optimization goal 'minimize' must "
763 "contain a single function of 'sum'.")
765 operands = optimization_copy['minimize']['sum']
766 if type(operands) is not list:
767 # or len(operands) != 2:
768 raise TranslatorException(
769 "Optimization goal 'minimize', function 'sum' "
770 "must be a list of exactly two operands.")
772 def get_latency_between_args(operand):
773 args = operand.get('latency_between')
774 if type(args) is not list and len(args) != 2:
775 raise TranslatorException(
776 "Optimization 'latency_between' arguments must "
777 "be a list of length two.")
782 if not got_demand and arg in self._demands.keys():
784 if not got_location and arg in self._locations.keys():
786 if not got_demand or not got_location:
787 raise TranslatorException(
788 "Optimization 'latency_between' arguments {} must "
789 "include one valid demand name and one valid "
790 "location name.".format(args))
794 def get_distance_between_args(operand):
795 args = operand.get('distance_between')
796 if type(args) is not list and len(args) != 2:
797 raise TranslatorException(
798 "Optimization 'distance_between' arguments must "
799 "be a list of length two.")
804 if not got_demand and arg in self._demands.keys():
806 if not got_location and arg in self._locations.keys():
808 if not got_demand or not got_location:
809 raise TranslatorException(
810 "Optimization 'distance_between' arguments {} must "
811 "include one valid demand name and one valid "
812 "location name.".format(args))
816 for operand in operands:
821 if operand.keys() == ['distance_between']:
822 # Value must be a list of length 2 with one
823 # location and one demand
824 function = 'distance_between'
825 args = get_distance_between_args(operand)
827 elif operand.keys() == ['product']:
828 for product_op in operand['product']:
829 if threshold.is_number(product_op):
831 elif isinstance(product_op, dict):
832 if product_op.keys() == ['latency_between']:
833 function = 'latency_between'
834 args = get_latency_between_args(product_op)
835 elif product_op.keys() == ['distance_between']:
836 function = 'distance_between'
837 args = get_distance_between_args(product_op)
838 elif product_op.keys() == ['aic_version']:
839 function = 'aic_version'
840 args = product_op.get('aic_version')
841 elif product_op.keys() == ['hpa_score']:
842 function = 'hpa_score'
843 args = product_op.get('hpa_score')
844 if not self.is_hpa_policy_exists(args):
845 raise TranslatorException(
846 "HPA Score Optimization must include a "
847 "HPA Policy constraint ")
848 elif product_op.keys() == ['sum']:
850 nested_operands = product_op.get('sum')
851 for nested_operand in nested_operands:
852 if nested_operand.keys() == ['product']:
853 nested_weight = weight
854 for nested_product_op in nested_operand['product']:
855 if threshold.is_number(nested_product_op):
856 nested_weight = nested_weight * int(nested_product_op)
857 elif isinstance(nested_product_op, dict):
858 if nested_product_op.keys() == ['latency_between']:
859 function = 'latency_between'
860 args = get_latency_between_args(nested_product_op)
861 elif nested_product_op.keys() == ['distance_between']:
862 function = 'distance_between'
863 args = get_distance_between_args(nested_product_op)
864 parsed['operands'].append(
866 "operation": "product",
867 "weight": nested_weight,
868 "function": function,
869 "function_param": args,
874 raise TranslatorException(
875 "Optimization products must include at least "
876 "one 'distance_between' function call and "
877 "one optional number to be used as a weight.")
879 # We now have our weight/function_param.
881 parsed['operands'].append(
883 "operation": "product",
885 "function": function,
886 "function_param": args,
891 def is_hpa_policy_exists(self, demand_list):
892 # Check if a HPA constraint exist for the demands in the demand list.
893 constraints_copy = copy.deepcopy(self._constraints)
894 for demand in demand_list:
895 for name, constraint in constraints_copy.items():
896 constraint_type = constraint.get('type')
897 if constraint_type == 'hpa':
898 hpa_demands = constraint.get('demands')
899 if demand in hpa_demands:
903 def parse_reservations(self, reservations):
904 demands = self._demands
905 if not isinstance(reservations, dict):
906 raise TranslatorException("Reservations must be provided in "
910 parsed['counter'] = 0
911 parsed['demands'] = {}
913 for key, value in reservations.items():
915 if key == "service_model":
916 parsed['service_model'] = value
918 elif key == "service_candidates":
919 for name, reservation_details in value.items():
920 if not reservation_details.get('properties'):
921 reservation_details['properties'] = {}
922 for demand in reservation_details.get('demands', []):
923 if demand in demands.keys():
924 reservation_demand = name + '_' + demand
925 parsed['demands'][reservation_demand] = copy.deepcopy(reservation_details)
926 parsed['demands'][reservation_demand]['name'] = name
927 parsed['demands'][reservation_demand]['demands'] = demand
929 raise TranslatorException("Demand {} must be provided in demands section".format(demand))
933 def do_translation(self):
934 """Perform the translation."""
936 raise TranslatorException("Can't translate an invalid template.")
938 request_type = self._parameters.get("request_type") \
939 or self._parameters.get("REQUEST_TYPE") \
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 "objective": self.parse_optimization(self._optmization),
952 "reservations": self.parse_reservations(self._reservations),
957 """Translate the template for the solver."""
960 self.create_components()
961 self.validate_components()
962 self.parse_parameters()
963 self.do_translation()
965 except Exception as exc:
966 self._error_message = exc.message
970 """Returns True if the template has been validated."""
975 """Returns True if the translation was successful."""
979 def translation(self):
980 """Returns the translation if it was successful."""
981 return self._translation
984 def error_message(self):
985 """Returns the last known error message."""
986 return self._error_message
990 template_name = 'some_template'
992 path = os.path.abspath(conductor_root)
993 dir_path = os.path.dirname(path)
995 # Prepare service-wide components (e.g., config)
996 conf = service.prepare_service(
997 [], config_files=[dir_path + '/../etc/conductor/conductor.conf'])
998 # conf.set_override('mock', True, 'music_api')
1000 t1 = threshold.Threshold("< 500 ms", "time")
1001 t2 = threshold.Threshold("= 120 mi", "distance")
1002 t3 = threshold.Threshold("160", "currency")
1003 t4 = threshold.Threshold("60-80 Gbps", "throughput")
1004 print('t1: {}\nt2: {}\nt3: {}\nt4: {}\n'.format(t1, t2, t3, t4))
1006 template_file = dir_path + '/tests/data/' + template_name + '.yaml'
1007 fd = open(template_file, "r")
1008 template = yaml.load(fd)
1010 trns = Translator(conf, template_name, str(uuid.uuid4()), template)
1013 print(json.dumps(trns.translation, indent=2))
1015 print("TESTING - Translator Error: {}".format(trns.error_message))
1018 if __name__ == '__main__':