Fix UnboundlocalError in translator.py
[optf/has.git] / conductor / conductor / controller / translator.py
1 #
2 # -------------------------------------------------------------------------
3 #   Copyright (c) 2015-2017 AT&T Intellectual Property
4 #
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
8 #
9 #       http://www.apache.org/licenses/LICENSE-2.0
10 #
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.
16 #
17 # -------------------------------------------------------------------------
18 #
19
20 import copy
21 import datetime
22 import json
23 import os
24 import uuid
25 import yaml
26
27 from oslo_config import cfg
28 from oslo_log import log
29 import six
30
31 from conductor import __file__ as conductor_root
32 from conductor.common.music import messaging as music_messaging
33 from conductor.common import threshold
34 from conductor import messaging
35 from conductor import service
36
37 LOG = log.getLogger(__name__)
38
39 CONF = cfg.CONF
40
41 VERSIONS = ["2016-11-01", "2017-10-10"]
42 LOCATION_KEYS = ['latitude', 'longitude', 'host_name', 'clli_code']
43 INVENTORY_PROVIDERS = ['aai']
44 INVENTORY_TYPES = ['cloud', 'service', 'transport']
45 DEFAULT_INVENTORY_PROVIDER = INVENTORY_PROVIDERS[0]
46 CANDIDATE_KEYS = ['inventory_type', 'candidate_id', 'location_id',
47                   'location_type', 'cost']
48 DEMAND_KEYS = ['inventory_provider', 'inventory_type', 'service_type',
49                'service_id', 'service_resource_id', 'customer_id',
50                'default_cost', 'candidates', 'region', 'complex',
51                'required_candidates', 'excluded_candidates',
52                'existing_placement', 'subdivision', 'flavor', 'attributes']
53 CONSTRAINT_KEYS = ['type', 'demands', 'properties']
54 CONSTRAINTS = {
55     # constraint_type: {
56     #   split: split into individual constraints, one per demand
57     #   required: list of required property names,
58     #   optional: list of optional property names,
59     #   thresholds: dict of property/base-unit pairs for threshold parsing
60     #   allowed: dict of keys and allowed values (if controlled vocab);
61     #            only use this for Conductor-controlled values!
62     # }
63     'attribute': {
64         'split': True,
65         'required': ['evaluate'],
66     },
67     'distance_between_demands': {
68         'required': ['distance'],
69         'thresholds': {
70             'distance': 'distance'
71         },
72     },
73     'distance_to_location': {
74         'split': True,
75         'required': ['distance', 'location'],
76         'thresholds': {
77             'distance': 'distance'
78         },
79     },
80     'instance_fit': {
81         'split': True,
82         'required': ['controller'],
83         'optional': ['request'],
84     },
85     'inventory_group': {},
86     'region_fit': {
87         'split': True,
88         'required': ['controller'],
89         'optional': ['request'],
90     },
91     'zone': {
92         'required': ['qualifier', 'category'],
93         'optional': ['location'],
94         'allowed': {'qualifier': ['same', 'different'],
95                     'category': ['disaster', 'region', 'complex', 'country',
96                                  'time', 'maintenance']},
97     },
98 }
99
100
101 class TranslatorException(Exception):
102     pass
103
104
105 class Translator(object):
106     """Template translator.
107
108     Takes an input template and translates it into
109     something the solver can use. Calls the data service
110     as needed, giving it the inventory provider as context.
111     Presently the only inventory provider is A&AI. Others
112     may be added in the future.
113     """
114
115     def __init__(self, conf, plan_name, plan_id, template):
116         self.conf = conf
117         self._template = copy.deepcopy(template)
118         self._plan_name = plan_name
119         self._plan_id = plan_id
120         self._translation = None
121         self._valid = False
122         self._ok = False
123
124         # Set up the RPC service(s) we want to talk to.
125         self.data_service = self.setup_rpc(self.conf, "data")
126
127     def setup_rpc(self, conf, topic):
128         """Set up the RPC Client"""
129         # TODO(jdandrea): Put this pattern inside music_messaging?
130         transport = messaging.get_transport(conf=conf)
131         target = music_messaging.Target(topic=topic)
132         client = music_messaging.RPCClient(conf=conf,
133                                            transport=transport,
134                                            target=target)
135         return client
136
137     def create_components(self):
138         # TODO(jdandrea): Make deep copies so the template is untouched
139         self._version = self._template.get("homing_template_version")
140         self._parameters = self._template.get("parameters", {})
141         self._locations = self._template.get("locations", {})
142         self._demands = self._template.get("demands", {})
143         self._constraints = self._template.get("constraints", {})
144         self._optmization = self._template.get("optimization", {})
145         self._reservations = self._template.get("reservation", {})
146
147         if isinstance(self._version, datetime.date):
148             self._version = str(self._version)
149
150     def validate_components(self):
151         """Cursory validation of template components.
152
153         More detailed validation happens while parsing each component.
154         """
155         self._valid = False
156
157         # Check version
158         if self._version not in VERSIONS:
159             raise TranslatorException(
160                 "conductor_template_version must be one "
161                 "of: {}".format(', '.join(VERSIONS)))
162
163         # Check top level structure
164         components = {
165             "parameters": {
166                 "name": "Parameter",
167                 "content": self._parameters,
168             },
169             "locations": {
170                 "name": "Location",
171                 "keys": LOCATION_KEYS,
172                 "content": self._locations,
173             },
174             "demands": {
175                 "name": "Demand",
176                 "content": self._demands,
177             },
178             "constraints": {
179                 "name": "Constraint",
180                 "keys": CONSTRAINT_KEYS,
181                 "content": self._constraints,
182             },
183             "optimization": {
184                 "name": "Optimization",
185                 "content": self._optmization,
186             },
187             "reservations": {
188                 "name": "Reservation",
189                 "content": self._reservations,
190             }
191         }
192         for name, component in components.items():
193             name = component.get('name')
194             keys = component.get('keys', None)
195             content = component.get('content')
196
197             if type(content) is not dict:
198                 raise TranslatorException(
199                     "{} section must be a dictionary".format(name))
200             for content_name, content_def in content.items():
201                 if not keys:
202                     continue
203
204                 for key in content_def:
205                     if key not in keys:
206                         raise TranslatorException(
207                             "{} {} has an invalid key {}".format(
208                                 name, content_name, key))
209
210         demand_keys = self._demands.keys()
211         location_keys = self._locations.keys()
212         for constraint_name, constraint in self._constraints.items():
213
214             # Require a single demand (string), or a list of one or more.
215             demands = constraint.get('demands')
216             if isinstance(demands, six.string_types):
217                 demands = [demands]
218             if not isinstance(demands, list) or len(demands) < 1:
219                 raise TranslatorException(
220                     "Demand list for Constraint {} must be "
221                     "a list of names or a string with one name".format(
222                         constraint_name))
223             if not set(demands).issubset(demand_keys + location_keys):
224                 raise TranslatorException(
225                     "Undefined Demand(s) {} in Constraint '{}'".format(
226                         list(set(demands).difference(demand_keys)),
227                         constraint_name))
228
229             properties = constraint.get('properties', None)
230             if properties:
231                 location = properties.get('location', None)
232                 if location:
233                     if location not in location_keys:
234                         raise TranslatorException(
235                             "Location {} in Constraint {} is undefined".format(
236                                 location, constraint_name))
237
238         self._valid = True
239
240     def _parse_parameters(self, obj, path=[]):
241         """Recursively parse all {get_param: X} occurrences
242
243         This modifies obj in-place. If you want to keep the original,
244         pass in a deep copy.
245         """
246         # Ok to start with a string ...
247         if isinstance(path, six.string_types):
248             # ... but the breadcrumb trail goes in an array.
249             path = [path]
250
251         # Traverse a list
252         if type(obj) is list:
253             for idx, val in enumerate(obj, start=0):
254                 # Add path to the breadcrumb trail
255                 new_path = list(path)
256                 new_path[-1] += "[{}]".format(idx)
257
258                 # Look at each element.
259                 obj[idx] = self._parse_parameters(val, new_path)
260
261         # Traverse a dict
262         elif type(obj) is dict:
263             # Did we find a "{get_param: ...}" intrinsic?
264             if obj.keys() == ['get_param']:
265                 param_name = obj['get_param']
266
267                 # The parameter name must be a string.
268                 if not isinstance(param_name, six.string_types):
269                     path_str = ' > '.join(path)
270                     raise TranslatorException(
271                         "Parameter name '{}' not a string in path {}".format(
272                             param_name, path_str))
273
274                 # Parameter name must be defined.
275                 if param_name not in self._parameters:
276                     path_str = ' > '.join(path)
277                     raise TranslatorException(
278                         "Parameter '{}' undefined in path {}".format(
279                             param_name, path_str))
280
281                 # Return the value in place of the call.
282                 return self._parameters.get(param_name)
283
284             # Not an intrinsic. Traverse as usual.
285             for key in obj.keys():
286                 # Add path to the breadcrumb trail.
287                 new_path = list(path)
288                 new_path.append(key)
289
290                 # Look at each key/value pair.
291                 obj[key] = self._parse_parameters(obj[key], new_path)
292
293         # Return whatever we have after unwinding.
294         return obj
295
296     def parse_parameters(self):
297         """Resolve all parameters references."""
298         locations = copy.deepcopy(self._locations)
299         self._locations = self._parse_parameters(locations, 'locations')
300
301         demands = copy.deepcopy(self._demands)
302         self._demands = self._parse_parameters(demands, 'demands')
303
304         constraints = copy.deepcopy(self._constraints)
305         self._constraints = self._parse_parameters(constraints, 'constraints')
306
307         reservations = copy.deepcopy(self._reservations)
308         self._reservations = self._parse_parameters(reservations,
309                                                     'reservations')
310
311     def parse_locations(self, locations):
312         """Prepare the locations for use by the solver."""
313         parsed = {}
314         for location, args in locations.items():
315             ctxt = {
316                 'plan_id': self._plan_id,
317                 'keyspace': self.conf.keyspace
318             }
319
320             latitude = args.get("latitude")
321             longitude = args.get("longitude")
322
323             if latitude and longitude:
324                 resolved_location = {"latitude": latitude, "longitude": longitude}
325             else:
326                 # ctxt = {}
327                 response = self.data_service.call(
328                     ctxt=ctxt,
329                     method="resolve_location",
330                     args=args)
331
332                 resolved_location = \
333                     response and response.get('resolved_location')
334             if not resolved_location:
335                 raise TranslatorException(
336                     "Unable to resolve location {}".format(location)
337                 )
338             parsed[location] = resolved_location
339         return parsed
340
341     def parse_demands(self, demands):
342         """Validate/prepare demands for use by the solver."""
343         if type(demands) is not dict:
344             raise TranslatorException("Demands must be provided in "
345                                       "dictionary form")
346
347         # Look at each demand
348         demands_copy = copy.deepcopy(demands)
349         parsed = {}
350         for name, requirements in demands_copy.items():
351             inventory_candidates = []
352             for requirement in requirements:
353                 for key in requirement:
354                     if key not in DEMAND_KEYS:
355                         raise TranslatorException(
356                             "Demand {} has an invalid key {}".format(
357                                 requirement, key))
358
359                 if 'candidates' in requirement:
360                     # Candidates *must* specify an inventory provider
361                     provider = requirement.get("inventory_provider")
362                     if provider and provider not in INVENTORY_PROVIDERS:
363                         raise TranslatorException(
364                             "Unsupported inventory provider {} "
365                             "in demand {}".format(provider, name))
366                     else:
367                         provider = DEFAULT_INVENTORY_PROVIDER
368
369                     # Check each candidate
370                     for candidate in requirement.get('candidates'):
371                         # Must be a dictionary
372                         if type(candidate) is not dict:
373                             raise TranslatorException(
374                                 "Candidate found in demand {} that is "
375                                 "not a dictionary".format(name))
376
377                         # Must have only supported keys
378                         for key in candidate.keys():
379                             if key not in CANDIDATE_KEYS:
380                                 raise TranslatorException(
381                                     "Candidate with invalid key {} found "
382                                     "in demand {}".format(key, name)
383                                 )
384
385                         # TODO(jdandrea): Check required/optional keys
386
387                         # Set the inventory provider if not already
388                         candidate['inventory_provider'] = \
389                             candidate.get('inventory_provider', provider)
390
391                         # Set cost if not already (default cost is 0?)
392                         candidate['cost'] = candidate.get('cost', 0)
393
394                         # Add to our list of parsed candidates
395                         inventory_candidates.append(candidate)
396
397                 # candidates are specified through inventory providers
398                 # Do the basic sanity checks for inputs
399                 else:
400                     # inventory provider MUST be specified
401                     provider = requirement.get("inventory_provider")
402                     if not provider:
403                         raise TranslatorException(
404                             "Inventory provider not specified "
405                             "in demand {}".format(name)
406                         )
407                     elif provider and provider not in INVENTORY_PROVIDERS:
408                         raise TranslatorException(
409                             "Unsupported inventory provider {} "
410                             "in demand {}".format(provider, name)
411                         )
412                     else:
413                         provider = DEFAULT_INVENTORY_PROVIDER
414                         requirement['provider'] = provider
415
416                     # inventory type MUST be specified
417                     inventory_type = requirement.get('inventory_type')
418                     if not inventory_type or inventory_type == '':
419                         raise TranslatorException(
420                             "Inventory type not specified for "
421                             "demand {}".format(name)
422                         )
423                     if inventory_type and \
424                             inventory_type not in INVENTORY_TYPES:
425                         raise TranslatorException(
426                             "Unknown inventory type {} specified for "
427                             "demand {}".format(inventory_type, name)
428                         )
429
430                     # For service inventories, customer_id and
431                     # service_type MUST be specified
432                     if inventory_type == 'service':
433                         attributes = requirement.get('attributes')
434
435                         if attributes:
436                             customer_id = attributes.get('customer-id')
437                             global_customer_id = attributes.get('global-customer-id')
438                             if global_customer_id:
439                                 customer_id = global_customer_id
440                         else:
441                             # for backward compatibility
442                             customer_id = requirement.get('customer_id')
443                             service_type = requirement.get('service_type')
444
445                         if not customer_id:
446                             raise TranslatorException(
447                                 "Customer ID not specified for "
448                                 "demand {}".format(name)
449                             )
450                         if not attributes and not service_type:
451                             raise TranslatorException(
452                                 "Service Type not specified for "
453                                 "demand {}".format(name)
454                             )
455
456                 # TODO(jdandrea): Check required/optional keys for requirement
457                 # elif 'inventory_type' in requirement:
458                 #     # For now this is just a stand-in candidate
459                 #     candidate = {
460                 #         'inventory_provider':
461                 #             requirement.get('inventory_provider',
462                 #                             DEFAULT_INVENTORY_PROVIDER),
463                 #         'inventory_type':
464                 #             requirement.get('inventory_type', ''),
465                 #         'candidate_id': '',
466                 #         'location_id': '',
467                 #         'location_type': '',
468                 #         'cost': 0,
469                 #     }
470                 #
471                 #     # Add to our list of parsed candidates
472                 #     inventory_candidates.append(candidate)
473
474             # Ask conductor-data for one or more candidates.
475             ctxt = {
476                 "plan_id": self._plan_id,
477                 "plan_name": self._plan_name,
478             }
479             args = {
480                 "demands": {
481                     name: requirements,
482                 }
483             }
484
485             # Check if required_candidate and excluded candidate
486             # are mutually exclusive.
487             for requirement in requirements:
488                 required_candidates = requirement.get("required_candidates")
489                 excluded_candidates = requirement.get("excluded_candidates")
490                 if (required_candidates and
491                     excluded_candidates and
492                     set(map(lambda entry: entry['candidate_id'],
493                             required_candidates))
494                     & set(map(lambda entry: entry['candidate_id'],
495                               excluded_candidates))):
496                     raise TranslatorException(
497                         "Required candidate list and excluded candidate"
498                         " list are not mutually exclusive for demand"
499                         " {}".format(name)
500                     )
501
502             response = self.data_service.call(
503                 ctxt=ctxt,
504                 method="resolve_demands",
505                 args=args)
506
507             resolved_demands = \
508                 response and response.get('resolved_demands')
509
510             required_candidates = resolved_demands\
511                 .get('required_candidates')
512             if not resolved_demands:
513                 raise TranslatorException(
514                     "Unable to resolve inventory "
515                     "candidates for demand {}"
516                     .format(name)
517                 )
518             resolved_candidates = resolved_demands.get(name)
519             for candidate in resolved_candidates:
520                 inventory_candidates.append(candidate)
521             if len(inventory_candidates) < 1:
522                 if not required_candidates:
523                     raise TranslatorException(
524                         "Unable to find any candidate for "
525                         "demand {}".format(name)
526                     )
527                 else:
528                     raise TranslatorException(
529                         "Unable to find any required "
530                         "candidate for demand {}"
531                         .format(name)
532                     )
533             parsed[name] = {
534                 "candidates": inventory_candidates,
535             }
536
537         return parsed
538
539     def parse_constraints(self, constraints):
540         """Validate/prepare constraints for use by the solver."""
541         if not isinstance(constraints, dict):
542             raise TranslatorException("Constraints must be provided in "
543                                       "dictionary form")
544
545         # Look at each constraint. Properties must exist, even if empty.
546         constraints_copy = copy.deepcopy(constraints)
547
548         parsed = {}
549         for name, constraint in constraints_copy.items():
550
551             if not constraint.get('properties'):
552                 constraint['properties'] = {}
553
554             constraint_type = constraint.get('type')
555             constraint_def = CONSTRAINTS.get(constraint_type)
556
557             # Is it a supported type?
558             if constraint_type not in CONSTRAINTS:
559                 raise TranslatorException(
560                     "Unsupported type '{}' found in constraint "
561                     "named '{}'".format(constraint_type, name))
562
563             # Now walk through the constraint's content
564             for key, value in constraint.items():
565                 # Must be a supported key
566                 if key not in CONSTRAINT_KEYS:
567                     raise TranslatorException(
568                         "Invalid key '{}' found in constraint "
569                         "named '{}'".format(key, name))
570
571                 # For properties ...
572                 if key == 'properties':
573                     # Make sure all required properties are present
574                     required = constraint_def.get('required', [])
575                     for req_prop in required:
576                         if req_prop not in value.keys():
577                             raise TranslatorException(
578                                 "Required property '{}' not found in "
579                                 "constraint named '{}'".format(
580                                     req_prop, name))
581                         if not value.get(req_prop) \
582                                 or value.get(req_prop) == '':
583                             raise TranslatorException(
584                                 "No value specified for property '{}' in "
585                                 "constraint named '{}'".format(
586                                     req_prop, name))
587
588                     # Make sure there are no unknown properties
589                     optional = constraint_def.get('optional', [])
590                     for prop_name in value.keys():
591                         if prop_name not in required + optional:
592                             raise TranslatorException(
593                                 "Unknown property '{}' in "
594                                 "constraint named '{}'".format(
595                                     prop_name, name))
596
597                     # If a property has a controlled vocabulary, make
598                     # sure its value is one of the allowed ones.
599                     allowed = constraint_def.get('allowed', {})
600                     for prop_name, allowed_values in allowed.items():
601                         if prop_name in value.keys():
602                             prop_value = value.get(prop_name, '')
603                             if prop_value not in allowed_values:
604                                 raise TranslatorException(
605                                     "Property '{}' value '{}' unsupported in "
606                                     "constraint named '{}' (must be one of "
607                                     "{})".format(prop_name, prop_value,
608                                                  name, allowed_values))
609
610                     # Break all threshold-formatted values into parts
611                     thresholds = constraint_def.get('thresholds', {})
612                     for thr_prop, base_units in thresholds.items():
613                         if thr_prop in value.keys():
614                             expression = value.get(thr_prop)
615                             thr = threshold.Threshold(expression, base_units)
616                             value[thr_prop] = thr.parts
617
618             # We already know we have one or more demands due to
619             # validate_components(). We still need to coerce the demands
620             # into a list in case only one demand was provided.
621             constraint_demands = constraint.get('demands')
622             if isinstance(constraint_demands, six.string_types):
623                 constraint['demands'] = [constraint_demands]
624
625             # Either split the constraint into parts, one per demand,
626             # or use it as-is
627             if constraint_def.get('split'):
628                 for demand in constraint.get('demands', []):
629                     constraint_demand = name + '_' + demand
630                     parsed[constraint_demand] = copy.deepcopy(constraint)
631                     parsed[constraint_demand]['name'] = name
632                     parsed[constraint_demand]['demands'] = demand
633             else:
634                 parsed[name] = copy.deepcopy(constraint)
635                 parsed[name]['name'] = name
636
637         return parsed
638
639     def parse_optimization(self, optimization):
640         """Validate/prepare optimization for use by the solver."""
641
642         # WARNING: The template format for optimization is generalized,
643         # however the solver is very particular about the expected
644         # goal, functions, and operands. Therefore, for the time being,
645         # we are choosing to be highly conservative in what we accept
646         # at the template level. Once the solver can handle the more
647         # general form, we can make the translation pass in this
648         # essentially pre-parsed formula unchanged, or we may allow
649         # optimizations to be written in algebraic form and pre-parsed
650         # with antlr4-python2-runtime. (jdandrea 1 Dec 2016)
651
652         if not optimization:
653             LOG.debug("No objective function or "
654                       "optimzation provided in the template")
655             return
656
657         optimization_copy = copy.deepcopy(optimization)
658         parsed = {
659             "goal": "min",
660             "operation": "sum",
661             "operands": [],
662         }
663
664         if type(optimization_copy) is not dict:
665             raise TranslatorException("Optimization must be a dictionary.")
666
667         goals = optimization_copy.keys()
668         if goals != ['minimize']:
669             raise TranslatorException(
670                 "Optimization must contain a single goal of 'minimize'.")
671
672         funcs = optimization_copy['minimize'].keys()
673         if funcs != ['sum']:
674             raise TranslatorException(
675                 "Optimization goal 'minimize' must "
676                 "contain a single function of 'sum'.")
677
678         operands = optimization_copy['minimize']['sum']
679         if type(operands) is not list:
680             # or len(operands) != 2:
681             raise TranslatorException(
682                 "Optimization goal 'minimize', function 'sum' "
683                 "must be a list of exactly two operands.")
684
685         def get_distance_between_args(operand):
686             args = operand.get('distance_between')
687             if type(args) is not list and len(args) != 2:
688                 raise TranslatorException(
689                     "Optimization 'distance_between' arguments must "
690                     "be a list of length two.")
691
692             got_demand = False
693             got_location = False
694             for arg in args:
695                 if not got_demand and arg in self._demands.keys():
696                     got_demand = True
697                 if not got_location and arg in self._locations.keys():
698                     got_location = True
699             if not got_demand or not got_location:
700                 raise TranslatorException(
701                     "Optimization 'distance_between' arguments {} must "
702                     "include one valid demand name and one valid "
703                     "location name.".format(args))
704
705             return args
706
707         for operand in operands:
708             weight = 1.0
709             args = None
710             nested = False
711
712             if operand.keys() == ['distance_between']:
713                 # Value must be a list of length 2 with one
714                 # location and one demand
715                 args = get_distance_between_args(operand)
716
717             elif operand.keys() == ['product']:
718                 for product_op in operand['product']:
719                     if threshold.is_number(product_op):
720                         weight = product_op
721                     elif type(product_op) is dict:
722                         if product_op.keys() == ['distance_between']:
723                             function = 'distance_between'
724                             args = get_distance_between_args(product_op)
725                         elif product_op.keys() == ['aic_version']:
726                             function = 'aic_version'
727                             args = product_op.get('aic_version')
728                         elif product_op.keys() == ['sum']:
729                             nested = True
730                             nested_operands = product_op.get('sum')
731                             for nested_operand in nested_operands:
732                                 if nested_operand.keys() == ['product']:
733                                     nested_weight = weight
734                                     for nested_product_op in nested_operand['product']:
735                                         if threshold.is_number(nested_product_op):
736                                             nested_weight = nested_weight * int(nested_product_op)
737                                         elif type(nested_product_op) is dict:
738                                             if nested_product_op.keys() == ['distance_between']:
739                                                 function = 'distance_between'
740                                                 args = get_distance_between_args(nested_product_op)
741                                     parsed['operands'].append(
742                                         {
743                                             "operation": "product",
744                                             "weight": nested_weight,
745                                             "function": function,
746                                             "function_param": args,
747                                         }
748                                     )
749
750                     elif type(product_op) is unicode:
751                         if product_op == 'W1':
752                             # get this weight from configuration file
753                             weight = self.conf.controller.weight1
754                         elif product_op == 'W2':
755                             # get this weight from configuration file
756                             weight = self.conf.controller.weight2
757                         elif product_op == 'cost':
758                             function = 'cost'
759
760                 if not args:
761                     raise TranslatorException(
762                         "Optimization products must include at least "
763                         "one 'distance_between' function call and "
764                         "one optional number to be used as a weight.")
765
766             # We now have our weight/function_param.
767             if not nested:
768                 parsed['operands'].append(
769                     {
770                         "operation": "product",
771                         "weight": weight,
772                         "function": function,
773                         "function_param": args,
774                     }
775                 )
776         return parsed
777
778     def parse_reservations(self, reservations):
779         demands = self._demands
780         if type(reservations) is not dict:
781             raise TranslatorException("Reservations must be provided in "
782                                       "dictionary form")
783
784         parsed = {}
785         if reservations:
786             parsed['counter'] = 0
787         for name, reservation in reservations.items():
788             if not reservation.get('properties'):
789                 reservation['properties'] = {}
790             for demand in reservation.get('demands', []):
791                 if demand in demands.keys():
792                     constraint_demand = name + '_' + demand
793                     parsed['demands'] = {}
794                     parsed['demands'][constraint_demand] = copy.deepcopy(reservation)
795                     parsed['demands'][constraint_demand]['name'] = name
796                     parsed['demands'][constraint_demand]['demand'] = demand
797
798         return parsed
799
800     def do_translation(self):
801         """Perform the translation."""
802         if not self.valid:
803             raise TranslatorException("Can't translate an invalid template.")
804
805         request_type = self._parameters.get("request_type") or ""
806
807         self._translation = {
808             "conductor_solver": {
809                 "version": self._version,
810                 "plan_id": self._plan_id,
811                 "request_type": request_type,
812                 "locations": self.parse_locations(self._locations),
813                 "demands": self.parse_demands(self._demands),
814                 "objective": self.parse_optimization(self._optmization),
815                 "constraints": self.parse_constraints(self._constraints),
816                 "objective": self.parse_optimization(self._optmization),
817                 "reservations": self.parse_reservations(self._reservations),
818             }
819         }
820
821     def translate(self):
822         """Translate the template for the solver."""
823         self._ok = False
824         try:
825             self.create_components()
826             self.validate_components()
827             self.parse_parameters()
828             self.do_translation()
829             self._ok = True
830         except Exception as exc:
831             self._error_message = exc.message
832
833     @property
834     def valid(self):
835         """Returns True if the template has been validated."""
836         return self._valid
837
838     @property
839     def ok(self):
840         """Returns True if the translation was successful."""
841         return self._ok
842
843     @property
844     def translation(self):
845         """Returns the translation if it was successful."""
846         return self._translation
847
848     @property
849     def error_message(self):
850         """Returns the last known error message."""
851         return self._error_message
852
853
854 def main():
855     template_name = 'some_template'
856
857     path = os.path.abspath(conductor_root)
858     dir_path = os.path.dirname(path)
859
860     # Prepare service-wide components (e.g., config)
861     conf = service.prepare_service(
862         [], config_files=[dir_path + '/../etc/conductor/conductor.conf'])
863     # conf.set_override('mock', True, 'music_api')
864
865     t1 = threshold.Threshold("< 500 ms", "time")
866     t2 = threshold.Threshold("= 120 mi", "distance")
867     t3 = threshold.Threshold("160", "currency")
868     t4 = threshold.Threshold("60-80 Gbps", "throughput")
869     print('t1: {}\nt2: {}\nt3: {}\nt4: {}\n'.format(t1, t2, t3, t4))
870
871     template_file = dir_path + '/tests/data/' + template_name + '.yaml'
872     fd = open(template_file, "r")
873     template = yaml.load(fd)
874
875     trns = Translator(conf, template_name, str(uuid.uuid4()), template)
876     trns.translate()
877     if trns.ok:
878         print(json.dumps(trns.translation, indent=2))
879     else:
880         print("TESTING - Translator Error: {}".format(trns.error_message))
881
882
883 if __name__ == '__main__':
884     main()