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 # -------------------------------------------------------------------------
27 from oslo_config import cfg
28 from oslo_log import log
31 from conductor.common import rest
32 from conductor.data.plugins import constants
33 from conductor.data.plugins.inventory_provider import base
34 from conductor.data.plugins.inventory_provider.candidates.candidate import Candidate
35 from conductor.data.plugins.inventory_provider.candidates.cloud_candidate import Cloud
36 from conductor.data.plugins.inventory_provider.candidates.nst_candidate import NST
37 from conductor.data.plugins.inventory_provider.candidates.nxi_candidate import NxI
38 from conductor.data.plugins.inventory_provider.candidates.service_candidate import Service
39 from conductor.data.plugins.inventory_provider.candidates.transport_candidate import Transport
40 from conductor.data.plugins.inventory_provider.candidates.vfmodule_candidate import VfModule
41 from conductor.data.plugins.inventory_provider import hpa_utils
42 from conductor.data.plugins.inventory_provider.sdc import SDC
43 from conductor.data.plugins.inventory_provider.utils import aai_utils
44 from conductor.data.plugins.triage_translator.triage_translator import TraigeTranslator
45 from conductor.i18n import _LE
46 from conductor.i18n import _LI
48 LOG = log.getLogger(__name__)
53 cfg.IntOpt('cache_refresh_interval',
55 help='Interval with which to refresh the local cache, '
57 cfg.IntOpt('complex_cache_refresh_interval',
59 help='Interval with which to refresh the local complex cache, '
61 cfg.StrOpt('table_prefix',
63 help='Data Store table prefix.'),
64 cfg.StrOpt('server_url',
65 default='https://controller:8443/aai',
66 help='Base URL for A&AI, up to and not including '
67 'the version, and without a trailing slash.'),
68 cfg.StrOpt('aai_rest_timeout',
70 help='Timeout for A&AI Rest Call'),
71 cfg.StrOpt('aai_retries',
73 help='Number of retry for A&AI Rest Call'),
74 cfg.StrOpt('server_url_version',
76 help='The version of A&AI in v# format.'),
77 cfg.StrOpt('certificate_file',
78 default='certificate.pem',
79 help='SSL/TLS certificate file in pem format. '
80 'This certificate must be registered with the A&AI '
82 cfg.StrOpt('certificate_key_file',
83 default='certificate_key.pem',
84 help='Private Certificate Key file in pem format.'),
85 cfg.StrOpt('certificate_authority_bundle_file',
86 default='certificate_authority_bundle.pem',
87 help='Certificate Authority Bundle file in pem format. '
88 'Must contain the appropriate trust chain for the '
90 # TODO(larry): follow-up with ONAP people on this (AA&I basic auth username and password?)
91 cfg.StrOpt('username',
93 help='Username for AAI.'),
94 cfg.StrOpt('password',
96 help='Password for AAI.'),
99 CONF.register_opts(AAI_OPTS, group='aai')
102 class AAI(base.InventoryProviderBase):
103 """Active and Available Inventory Provider"""
108 # FIXME(jdandrea): Pass this in to init.
111 self.base = self.conf.aai.server_url.rstrip('/')
112 self.version = self.conf.aai.server_url_version.rstrip('/')
113 self.cert = self.conf.aai.certificate_file
114 self.key = self.conf.aai.certificate_key_file
115 self.verify = self.conf.aai.certificate_authority_bundle_file
116 self.cache_refresh_interval = self.conf.aai.cache_refresh_interval
117 self.last_refresh_time = None
118 self.complex_cache_refresh_interval = \
119 self.conf.aai.complex_cache_refresh_interval
120 self.complex_last_refresh_time = None
121 self.timeout = self.conf.aai.aai_rest_timeout
122 self.retries = self.conf.aai.aai_retries
123 self.username = self.conf.aai.username
124 self.password = self.conf.aai.password
125 self.triage_translator = TraigeTranslator()
127 # Cache is initially empty
129 self._aai_complex_cache = {}
131 def initialize(self):
133 """Perform any late initialization."""
134 # Initialize the Python requests
135 self._init_python_request()
137 # Refresh the cache once for now
138 self._refresh_cache()
140 # TODO(jdandrea): Make this periodic, and without a True condition!
141 # executor = futurist.ThreadPoolExecutor()
143 # fut = executor.submit(self.refresh_cache)
146 # # Now wait for the next time.
147 # # FIXME(jdandrea): Put inside refresh_cache()?
148 # refresh_interval = self.conf.aai.cache_refresh_interval
149 # time.sleep(refresh_interval)
150 # executor.shutdown()
153 """Return human-readable name."""
156 def invoke_method(self, arg):
157 if arg.pop('method_name') == "get_candidates_with_hpa":
158 return hpa_utils.get_candidates_with_hpa(arg)
161 def _get_version_from_string(string):
162 """Extract version number from string"""
163 return re.sub("[^0-9.]", "", string)
165 def _aai_versioned_path(self, path):
166 """Return a URL path with the A&AI version prepended"""
167 return '/{}/{}'.format(self.version, path.lstrip('/'))
169 def _request(self, method='get', path='/', data=None,
170 context=None, value=None):
171 """Performs HTTP request."""
173 'X-FromAppId': 'CONDUCTOR',
174 'X-TransactionId': str(uuid.uuid4()),
183 # TODO(jdandrea): Move timing/response logging into the rest helper?
184 start_time = time.time()
185 response = self.rest.request(**kwargs)
186 elapsed = time.time() - start_time
187 LOG.debug("Total time for A&AI request "
188 "({0:}: {1:}): {2:.3f} sec".format(context, value, elapsed))
191 LOG.error(_LE("No response from A&AI ({}: {})").
192 format(context, value))
193 elif response.status_code != 200:
194 LOG.error(_LE("A&AI request ({}: {}) returned HTTP "
195 "status {} {}, link: {}{}").
196 format(context, value,
197 response.status_code, response.reason,
201 def _init_python_request(self):
204 "server_url": self.base,
205 "retries": self.retries,
206 "username": self.username,
207 "password": self.password,
208 "cert_file": self.cert,
209 "cert_key_file": self.key,
210 "ca_bundle_file": self.verify,
211 "log_debug": self.conf.debug,
212 "read_timeout": self.timeout,
214 self.rest = rest.REST(**kwargs)
216 def _refresh_cache(self):
217 """Refresh the A&AI cache."""
218 if not self.last_refresh_time or \
219 (time.time() - self.last_refresh_time) > \
220 self.cache_refresh_interval * 60:
221 # TODO(jdandrea): This is presently brute force.
222 # It does not persist to Music. A general purpose ORM caching
223 # object likely needs to be made, with a key (hopefully we
224 # can use one that is not just a UUID), a value, and a
225 # timestamp. The other alternative is to not use the ORM
226 # layer and call the API directly, but that is
227 # also trading one set of todos for another ...
230 LOG.info(_LI("**** Refreshing A&AI cache *****"))
231 path = self._aai_versioned_path(
232 '/cloud-infrastructure/cloud-regions/?depth=0')
233 response = self._request(
234 path=path, context="cloud regions", value="all")
238 if response.status_code == 200:
239 body = response.json()
240 regions = body.get('cloud-region', {})
242 # Nothing to update the cache with
243 LOG.error(_LE("A&AI returned no regions, link: {}{}").
244 format(self.base, path))
250 for region in regions:
251 cloud_region_id = region.get('cloud-region-id')
253 LOG.debug("Working on region '{}' ".format(cloud_region_id))
255 cloud_region_version = region.get('cloud-region-version')
256 cloud_owner = region.get('cloud-owner')
257 cloud_type = region.get('cloud-type')
258 cloud_zone = region.get('cloud-zone')
260 physical_location_list = self._get_aai_rel_link_data(data=region, related_to='complex',
261 search_key='complex.physical-location-id')
262 if len(physical_location_list) > 0:
263 physical_location_id = physical_location_list[0].get('d_value')
265 if not (cloud_region_version and cloud_region_id):
267 rel_link_data_list = \
268 self._get_aai_rel_link_data(
270 related_to='complex',
271 search_key='complex.physical-location-id')
272 if len(rel_link_data_list) > 1:
273 LOG.error(_LE("Region {} has more than one complex").
274 format(cloud_region_id))
275 LOG.debug("Region {}: {}".format(cloud_region_id, region))
278 rel_link_data = rel_link_data_list[0]
279 complex_id = rel_link_data.get("d_value")
280 complex_link = rel_link_data.get("link")
281 if complex_id and complex_link:
282 complex_info = self._get_complex(
283 complex_link=complex_link,
284 complex_id=complex_id)
285 else: # no complex information
286 LOG.error(_LE("Region {} does not reference a complex").
287 format(cloud_region_id))
290 LOG.error(_LE("Region {}, complex {} info not found, "
291 "link {}").format(cloud_region_id,
292 complex_id, complex_link))
295 latitude = complex_info.get('latitude')
296 longitude = complex_info.get('longitude')
297 city = complex_info.get('city')
298 state = complex_info.get('state')
299 region = complex_info.get('region')
300 country = complex_info.get('country')
301 complex_name = complex_info.get('complex-name')
303 if not (latitude and longitude and city and country and complex_name):
304 keys = ('latitude', 'longitude', 'city', 'country',
307 list(set(keys).difference(
308 list(complex_info.keys()))) # Python 3 Conversion -- dict object to list object
309 LOG.error(_LE("Complex {} is missing {}, link: {}").
310 format(complex_id, missing_keys, complex_link))
311 LOG.debug("Complex {}: {}".
312 format(complex_id, complex_info))
315 cache['cloud_region'][cloud_region_id] = {
316 'cloud_region_version': cloud_region_version,
317 'cloud_owner': cloud_owner,
318 'cloud_type': cloud_type,
319 'cloud_zone': cloud_zone,
320 'complex_name': complex_name,
321 'physical_location_id': physical_location_id,
323 'complex_id': complex_id,
324 'complex_name': complex_name,
325 'latitude': latitude,
326 'longitude': longitude,
334 # Added for HPA support
335 if self.conf.HPA_enabled:
336 flavors = self._get_flavors(cloud_owner, cloud_region_id)
337 cache['cloud_region'][cloud_region_id]['flavors'] = flavors
339 LOG.debug("Candidate with cloud_region_id '{}' selected "
340 "as a potential candidate - ".format(cloud_region_id))
341 LOG.debug("Done with region '{}' ".format(cloud_region_id))
342 self._aai_cache = cache
343 self.last_refresh_time = time.time()
344 LOG.info(_LI("**** A&AI cache refresh complete *****"))
347 def _get_aai_rel_link(data, related_to):
348 """Given an A&AI data structure, return the related-to link"""
349 rel_dict = data.get('relationship-list')
351 for key, rel_list in rel_dict.items():
353 if related_to == rel.get('related-to'):
354 return rel.get('related-link')
357 def _get_aai_rel_link_data(data, related_to, search_key=None,
359 # some strings that we will encounter frequently
360 rel_lst = "relationship-list"
361 rkey = "relationship-key"
362 rval = "relationship-value"
363 rdata = "relationship-data"
366 m_key = match_dict.get('key')
367 m_value = match_dict.get('value')
371 rel_dict = data.get(rel_lst)
372 if rel_dict: # check if data has relationship lists
373 for key, rel_list in rel_dict.items():
375 if rel.get("related-to") == related_to:
378 link = rel.get("related-link")
379 r_data = rel.get(rdata, [])
382 if rd.get(rkey) == search_key:
384 if not match_dict: # return first match
386 {"link": link, "d_value": dval}
388 break # go to next relation
389 if rd.get(rkey) == m_key \
390 and rd.get(rval) == m_value:
392 if match_dict and matched: # if matching required
394 {"link": link, "d_value": dval}
396 # matched, return search value corresponding
397 # to the matched r_data group
398 else: # no search key; just return the link
400 {"link": link, "d_value": dval}
402 if len(response) == 0:
404 {"link": None, "d_value": None}
409 def check_sriov_automation(aic_version, demand_name, candidate_name):
410 """Check if specific candidate has SRIOV automation available or no"""
412 LOG.debug(_LI("Demand {}, candidate {} has an AIC version number {}").format(demand_name, candidate_name,
414 if aic_version == "X.Y":
418 def _get_complex(self, complex_link, complex_id=None):
420 if not self.complex_last_refresh_time or \
421 (time.time() - self.complex_last_refresh_time) > \
422 self.complex_cache_refresh_interval * 60:
423 self._aai_complex_cache.clear()
424 if complex_id and complex_id in self._aai_complex_cache:
425 return self._aai_complex_cache[complex_id]
427 path = self._aai_versioned_path(self._get_aai_path_from_link(complex_link))
428 response = self._request(path=path, context="complex", value=complex_id)
431 if response.status_code == 200:
432 complex_info = response.json()
433 if 'complex' in complex_info:
434 complex_info = complex_info.get('complex')
436 latitude = complex_info.get('latitude')
437 longitude = complex_info.get('longitude')
438 city = complex_info.get('city')
439 country = complex_info.get('country')
440 # removed the state check for countries in Europe that do not always enter states
441 if not (latitude and longitude and city and country):
442 keys = ('latitude', 'longitude', 'city', 'country')
444 list(set(keys).difference(set(complex_info.keys())))
445 LOG.error(_LE("Complex {} is missing {}, link: {}").
446 format(complex_id, missing_keys, complex_link))
447 LOG.debug("Complex {}: {}".format(complex_id, complex_info))
450 if complex_id: # cache only if complex_id is given
451 self._aai_complex_cache[complex_id] = response.json()
452 self.complex_last_refresh_time = time.time()
456 def _get_regions(self):
457 self._refresh_cache()
458 regions = self._aai_cache.get('cloud_region', {})
461 def _get_flavors(self, cloud_owner, cloud_region_id):
462 '''Fetch all flavors of a given cloud regions specified using {cloud-owner}/{cloud-region-id} composite key
464 :return flavors_info json object which list of flavor nodes and its children - HPACapabilities:
467 LOG.debug("Fetch all flavors and its child nodes HPACapabilities")
468 flavor_path = constants.FLAVORS_URI % (cloud_owner, cloud_region_id)
469 path = self._aai_versioned_path(flavor_path)
470 LOG.debug("Flavors path '{}' ".format(path))
472 response = self._request(path=path, context="flavors", value="all")
475 if response.status_code == 200:
476 flavors_info = response.json()
477 if not flavors_info or not flavors_info["flavor"] or \
478 len(flavors_info["flavor"]) == 0:
479 LOG.error(_LE("Flavor is missing in Cloud-Region {}/{}").
480 format(cloud_owner, cloud_region_id))
482 LOG.debug(flavors_info)
483 # Remove extraneous flavor information
486 LOG.error(_LE("Received Error while fetching flavors from Cloud-region {}/{}").format(cloud_owner,
490 def _get_aai_path_from_link(self, link):
491 path = link.split(self.version, 1)
492 if not path or len(path) <= 1:
493 # TODO(shankar): Treat this as a critical error?
494 LOG.error(_LE("A&AI version {} not found in link {}").
495 format(self.version, link))
497 return "{}".format(path[1])
499 def check_candidate_role(self, host_id=None):
501 vnf_name_uri = '/network/generic-vnfs/?vnf-name=' + host_id + '&depth=0'
502 path = self._aai_versioned_path(vnf_name_uri)
504 response = self._request('get', path=path, data=None,
507 if response is None or not response.ok:
509 body = response.json()
511 generic_vnf = body.get("generic-vnf", [])
513 for vnf in generic_vnf:
514 related_to = "service-instance"
515 search_key = "customer.global-customer-id"
516 rl_data_list = self._get_aai_rel_link_data(
518 related_to=related_to,
519 search_key=search_key,
522 if len(rl_data_list) != 1:
525 rl_data = rl_data_list[0]
526 candidate_role_link = rl_data.get("link")
528 if not candidate_role_link:
529 LOG.error(_LE("Unable to get candidate role link for host id {} ").format(host_id))
532 candidate_role_path = self._get_aai_path_from_link(candidate_role_link) + '/allotted-resources?depth=all'
533 path = self._aai_versioned_path(candidate_role_path)
535 response = self._request('get', path=path, data=None,
536 context="candidate role")
538 if response is None or not response.ok:
540 body = response.json()
542 response_items = body.get('allotted-resource')
543 if len(response_items) > 0:
544 role = response_items[0].get('role')
547 def check_network_roles(self, network_role_id=None):
548 # the network role query from A&AI is not using
549 # the version number in the query
551 '/network/l3-networks?network-role=' + network_role_id
552 path = self._aai_versioned_path(network_role_uri)
554 # This UUID is reserved by A&AI for a Conductor-specific named query.
555 named_query_uid = "role-UUID"
558 "query-parameters": {
560 "named-query-uuid": named_query_uid
563 "instance-filters": {
567 "network-role": network_role_id
574 response = self._request('get', path=path, data=data,
575 context="role", value=network_role_id)
578 body = response.json()
580 response_items = body.get('l3-network', [])
582 for item in response_items:
583 cloud_region_instances = self._get_aai_rel_link_data(
585 related_to='cloud-region',
586 search_key='cloud-region.cloud-region-id'
589 if len(cloud_region_instances) > 0:
590 for r_instance in cloud_region_instances:
591 region_id = r_instance.get('d_value')
592 if region_id is not None:
593 region_ids.add(region_id)
595 # return region ids that fit the role
598 def resolve_host_location(self, host_name):
599 path = self._aai_versioned_path('/query?format=id')
600 data = {"start": ["network/pnfs/pnf/" + host_name,
601 "cloud-infrastructure/pservers/pserver/" + host_name],
602 "query": "query/ucpe-instance"
604 response = self._request('put', path=path, data=data,
605 context="host name", value=host_name)
606 if response is None or response.status_code != 200:
608 body = response.json()
609 results = body.get('results', [])
611 for result in results:
612 if "resource-type" in result and \
613 "resource-link" in result and \
614 result["resource-type"] == "complex":
615 complex_link = result["resource-link"]
617 LOG.error(_LE("Unable to get a complex link for hostname {} "
618 " in response {}").format(host_name, response))
620 complex_info = self._get_complex(
621 complex_link=complex_link,
625 lat = complex_info.get('latitude')
626 lon = complex_info.get('longitude')
627 country = complex_info.get('country')
629 location = {"latitude": lat, "longitude": lon}
630 location["country"] = country if country else None
633 LOG.error(_LE("Unable to get a latitude and longitude "
634 "information for hostname {} from complex "
635 " link {}").format(host_name, complex_link))
638 LOG.error(_LE("Unable to get a complex information for "
639 " hostname {} from complex "
640 " link {}").format(host_name, complex_link))
643 def resolve_clli_location(self, clli_name):
644 clli_uri = '/cloud-infrastructure/complexes/complex/' + clli_name
645 path = self._aai_versioned_path(clli_uri)
647 response = self._request('get', path=path, data=None,
648 context="clli name", value=clli_name)
649 if response is None or response.status_code != 200:
652 body = response.json()
655 lat = body.get('latitude')
656 lon = body.get('longitude')
657 country = body.get('country')
659 location = {"latitude": lat, "longitude": lon}
660 location["country"] = country if country else None
663 LOG.error(_LE("Unable to get a latitude and longitude "
664 "information for CLLI code {} from complex").
668 LOG.error(_LE("Unable to get a complex information for "
669 " clli {} from complex "
670 " link {}").format(clli_name, clli_uri))
673 def get_inventory_group_pairs(self, service_description):
675 path = self._aai_versioned_path(
676 '/network/instance-groups/?description={}&depth=0'.format(
677 service_description))
678 response = self._request(path=path, context="inventory group",
679 value=service_description)
680 if response is None or response.status_code != 200:
682 body = response.json()
683 if "instance-group" not in body:
684 LOG.error(_LE("Unable to get instance groups from inventory "
685 " in response {}").format(response))
687 for instance_groups in body["instance-group"]:
688 s_instances = self._get_aai_rel_link_data(
689 data=instance_groups,
690 related_to='service-instance',
691 search_key='service-instance.service-instance-id'
693 if s_instances and len(s_instances) == 2:
695 for s_inst in s_instances:
696 pair.append(s_inst.get('d_value'))
699 LOG.error(_LE("Number of instance pairs not found to "
700 "be two: {}").format(instance_groups))
703 def _log_multiple_item_error(self, name, service_type,
704 related_to, search_key='',
705 context=None, value=None):
706 """Helper method to log multiple-item errors
708 Used by resolve_demands
710 LOG.error(_LE("Demand {}, role {} has more than one {} ({})").
711 format(name, service_type, related_to, search_key))
712 if context and value:
713 LOG.debug("{} details: {}".format(context, value))
715 def match_candidate_attribute(self, candidate, attribute_name,
716 restricted_value, demand_name,
718 """Check if specific candidate attribute matches the restricted value
720 Used by resolve_demands
722 if restricted_value and restricted_value != '' and candidate[attribute_name] != restricted_value:
723 LOG.info(_LI("Demand: {} "
724 "Discarded {} candidate as "
725 "it doesn't match the "
727 "{} ").format(demand_name,
736 def match_vserver_attribute(self, vserver_list):
739 for i in range(0, len(vserver_list)):
741 value != vserver_list[i].get('d_value'):
743 value = vserver_list[i].get('d_value')
746 def match_inventory_attributes(self, template_attributes,
747 inventory_attributes, candidate_id):
749 for attribute_key, attribute_values in template_attributes.items():
751 if attribute_key and \
752 (attribute_key == 'service-type' or attribute_key == 'equipment-role'
753 or attribute_key == 'model-invariant-id' or attribute_key == 'model-version-id'):
757 if type(attribute_values) is dict:
758 if 'any' in attribute_values:
759 attribute_values = attribute_values['any']
760 elif 'not' in attribute_values:
762 attribute_values = attribute_values['not']
764 if match_type == 'any':
765 if attribute_key not in inventory_attributes or \
766 (len(attribute_values) > 0 and inventory_attributes[attribute_key] not in attribute_values):
768 elif match_type == 'not':
769 # drop the candidate when
770 # 1)field exists in AAI and 2)value is not null or empty 3)value is one of those in the 'not' list
771 # Remember, this means if the property is not returned at all from AAI, that still can be a candidate.
772 if attribute_key in inventory_attributes and \
773 inventory_attributes[attribute_key] and \
774 inventory_attributes[attribute_key] in attribute_values:
779 def first_level_service_call(self, path, name, service_type):
781 response = self._request(
782 path=path, context="demand, GENERIC-VNF role",
783 value="{}, {}".format(name, service_type))
784 if response is None or response.status_code != 200:
785 return list() # move ahead with next requirement
786 body = response.json()
787 return body.get("generic-vnf", [])
789 def resolve_v_server_for_candidate(self, candidate_id, location_id, vs_link, add_interfaces, demand_name,
790 triage_translator_data):
792 LOG.error(_LE("{} VSERVER link information not "
793 "available from A&AI").format(demand_name))
794 self.triage_translator.collectDroppedCandiate(candidate_id,
795 location_id, demand_name,
796 triage_translator_data,
797 reason="VSERVER link information not")
798 return None # move ahead with the next vnf
801 vs_link = vs_link + '?depth=2'
802 vs_path = self._get_aai_path_from_link(vs_link)
804 LOG.error(_LE("{} VSERVER path information not "
805 "available from A&AI - {}").
806 format(demand_name, vs_path))
807 self.triage_translator.collectDroppedCandiate(candidate_id,
808 location_id, demand_name,
809 triage_translator_data,
810 reason="VSERVER path information not available from A&AI")
811 return None # move ahead with the next vnf
812 path = self._aai_versioned_path(vs_path)
813 response = self._request(
814 path=path, context="demand, VSERVER",
815 value="{}, {}".format(demand_name, vs_path))
816 if response is None or response.status_code != 200:
817 self.triage_translator.collectDroppedCandiate(candidate_id,
818 location_id, demand_name,
819 triage_translator_data,
820 reason=response.status_code)
822 return response.json()
824 def resolve_vf_modules_for_generic_vnf(self, candidate_id, location_id, vnf, demand_name, triage_translator_data):
825 raw_path = '/network/generic-vnfs/generic-vnf/{}?depth=1'.format(vnf.get("vnf-id"))
826 path = self._aai_versioned_path(raw_path)
828 response = self._request('get', path=path, data=None)
829 if response is None or response.status_code != 200:
830 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
831 triage_translator_data, reason=response)
833 generic_vnf_details = response.json()
835 if generic_vnf_details is None or not generic_vnf_details.get('vf-modules') \
836 or not generic_vnf_details.get('vf-modules').get('vf-module'):
837 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
838 triage_translator_data,
839 reason="Generic-VNF No detailed data for VF-modules")
842 return generic_vnf_details.get('vf-modules').get('vf-module')
844 def resolve_cloud_regions_by_cloud_region_id(self, cloud_region_id):
845 cloud_region_uri = '/cloud-infrastructure/cloud-regions' \
846 '/?cloud-region-id=' \
848 path = self._aai_versioned_path(cloud_region_uri)
850 response = self._request('get',
853 if response is None or response.status_code != 200:
856 body = response.json()
857 return body.get('cloud-region', [])
859 def assign_candidate_existing_placement(self, candidate, existing_placement):
861 """Assign existing_placement and cost parameters to candidate
863 Used by resolve_demands
865 candidate['existing_placement'] = 'false'
866 if existing_placement:
867 if existing_placement.get('candidate_id') == candidate['candidate_id']:
868 candidate['cost'] = self.conf.data.existing_placement_cost
869 candidate['existing_placement'] = 'true'
871 def resovle_conflict_id(self, conflict_identifier, candidate):
873 # Initialize the conflict_id_list
874 conflict_id_list = list()
875 # conflict_id is separated by pipe (|)
878 for conflict_element in conflict_identifier:
879 # if the conflict_element is a dictionary with key = 'get_candidate_attribute',
880 # then add candidate's coressponding value to conflict_id string
881 if isinstance(conflict_element, dict) and 'get_candidate_attribute' in conflict_element:
882 attribute_name = conflict_element.get('get_candidate_attribute')
883 conflict_id_list.append(candidate[attribute_name] + separator)
884 elif isinstance(conflict_element, unicode):
885 conflict_id_list.append(conflict_element + separator)
887 return ''.join(conflict_id_list)
889 def resolve_v_server_links_for_vnf(self, vnf):
890 related_to = "vserver"
891 search_key = "cloud-region.cloud-owner"
892 rl_data_list = self._get_aai_rel_link_data(
893 data=vnf, related_to=related_to,
894 search_key=search_key)
895 vs_link_list = list()
896 for i in range(0, len(rl_data_list)):
897 vs_link_list.append(rl_data_list[i].get('link'))
900 def resolve_complex_info_link_for_v_server(self, candidate_id, v_server, cloud_owner, cloud_region_id,
901 service_type, demand_name, triage_translator_data):
902 related_to = "pserver"
903 rl_data_list = self._get_aai_rel_link_data(
905 related_to=related_to,
908 if len(rl_data_list) > 1:
909 self._log_multiple_item_error(
910 demand_name, service_type, related_to, "item",
912 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
913 triage_translator_data, reason="item VSERVER")
915 rl_data = rl_data_list[0]
916 ps_link = rl_data.get('link')
918 # Third level query to get cloud region from pserver
920 LOG.error(_LE("{} pserver related link "
921 "not found in A&AI: {}").
922 format(demand_name, rl_data))
923 # if HPA_feature is disabled
924 if not self.conf.HPA_enabled:
925 # Triage Tool Feature Changes
926 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
927 triage_translator_data, reason="ps link not found")
930 if not (cloud_owner and cloud_region_id):
931 LOG.error("{} cloud-owner or cloud-region not "
932 "available from A&AI".
934 # Triage Tool Feature Changes
935 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
936 triage_translator_data,
937 reason="Cloud owner and cloud region "
939 return None # move ahead with the next vnf
941 '/cloud-infrastructure/cloud-regions/cloud-region' \
942 '/?cloud-owner=' + cloud_owner \
943 + '&cloud-region-id=' + cloud_region_id
944 path = self._aai_versioned_path(cloud_region_uri)
945 response = self._request('get',
948 if response is None or response.status_code != 200:
949 # Triage Tool Feature Changes
950 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
951 triage_translator_data, reason=response)
953 body = response.json()
955 ps_path = self._get_aai_path_from_link(ps_link)
957 LOG.error(_LE("{} pserver path information "
958 "not found in A&AI: {}").
959 format(demand_name, ps_link))
960 # Triage Tool Feature Changes
961 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
962 triage_translator_data, reason="ps path not found")
963 return None # move ahead with the next vnf
964 path = self._aai_versioned_path(ps_path)
965 response = self._request(
966 path=path, context="PSERVER", value=ps_path)
967 if response is None or response.status_code != 200:
968 # Triage Tool Feature Changes
969 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
970 triage_translator_data, reason=response)
972 body = response.json()
974 related_to = "complex"
975 search_key = "complex.physical-location-id"
976 rl_data_list = self._get_aai_rel_link_data(
978 related_to=related_to,
979 search_key=search_key
981 if len(rl_data_list) > 1:
982 if not self.match_vserver_attribute(rl_data_list):
983 self._log_multiple_item_error(
984 demand_name, service_type, related_to, search_key, "PSERVER", body)
985 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
986 triage_translator_data, reason="PSERVER error")
988 return rl_data_list[0]
990 def resolve_cloud_for_vnf(self, candidate_id, location_id, vnf, service_type, demand_name, triage_translator_data):
991 related_to = "vserver"
992 search_keys = ["cloud-region.cloud-owner", "cloud-region.cloud-region-id"]
994 for search_key in search_keys:
995 rl_data_list = self._get_aai_rel_link_data(
996 data=vnf, related_to=related_to,
997 search_key=search_key)
999 if len(rl_data_list) > 1:
1000 if not self.match_vserver_attribute(rl_data_list):
1001 self._log_multiple_item_error(
1002 demand_name, service_type, related_to, search_key,
1004 self.triage_translator.collectDroppedCandiate(candidate_id,
1005 location_id, demand_name,
1006 triage_translator_data,
1009 cloud_info[search_key.split(".")[1].replace('-', '_')] = rl_data_list[0].get('d_value') if rl_data_list[
1011 cloud_info['cloud_region_version'] = self.get_cloud_region_version(cloud_info['cloud_region_id'])
1012 cloud_info['location_type'] = 'att_aic'
1013 cloud_info['location_id'] = cloud_info.pop('cloud_region_id')
1016 def get_cloud_region_version(self, cloud_region_id):
1018 regions = self.resolve_cloud_regions_by_cloud_region_id(cloud_region_id)
1021 for region in regions:
1022 if "cloud-region-version" in region:
1023 return self._get_version_from_string(region["cloud-region-version"])
1025 def resolve_global_customer_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1026 demand_name, triage_translator_data):
1027 related_to = "service-instance"
1028 search_key = "customer.global-customer-id"
1029 match_key = "customer.global-customer-id"
1030 rl_data_list = self._get_aai_rel_link_data(
1032 related_to=related_to,
1033 search_key=search_key,
1034 match_dict={'key': match_key,
1035 'value': customer_id}
1037 if len(rl_data_list) > 1:
1038 if not self.match_vserver_attribute(rl_data_list):
1039 self._log_multiple_item_error(
1040 demand_name, service_type, related_to, search_key, "VNF", vnf)
1041 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1042 demand_name, triage_translator_data,
1043 reason=" match_vserver_attribute generic-vnf")
1045 return rl_data_list[0]
1047 def resolve_service_instance_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1048 demand_name, triage_translator_data):
1049 related_to = "service-instance"
1050 search_key = "service-instance.service-instance-id"
1051 match_key = "customer.global-customer-id"
1052 rl_data_list = self._get_aai_rel_link_data(
1054 related_to=related_to,
1055 search_key=search_key,
1056 match_dict={'key': match_key,
1057 'value': customer_id}
1059 if len(rl_data_list) > 1:
1060 if not self.match_vserver_attribute(rl_data_list):
1061 self._log_multiple_item_error(
1062 demand_name, service_type, related_to, search_key, "VNF", vnf)
1063 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1064 demand_name, triage_translator_data,
1065 reason="multiple_item_error generic-vnf")
1067 return rl_data_list[0]
1069 def build_complex_info_for_candidate(self, candidate_id, location_id, vnf, complex_list, service_type, demand_name,
1070 triage_translator_data):
1071 if not complex_list or \
1072 len(complex_list) < 1:
1073 LOG.error("Complex information not "
1074 "available from A&AI")
1075 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1076 triage_translator_data,
1077 reason="Complex information not available from A&AI")
1080 # In the scenario where no pserver information is available
1081 # assumption here is that cloud-region does not span across
1082 # multiple complexes
1083 if len(complex_list) > 1:
1084 related_to = "complex"
1085 search_key = "complex.physical-location-id"
1086 if not self.match_vserver_attribute(complex_list):
1087 self._log_multiple_item_error(
1088 demand_name, service_type, related_to, search_key,
1090 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1091 triage_translator_data,
1092 reason="Generic-vnf error")
1095 rl_data = complex_list[0]
1096 complex_link = rl_data.get('link')
1097 complex_id = rl_data.get('d_value')
1099 # Final query for the complex information
1100 if not (complex_link and complex_id):
1101 LOG.debug("{} complex information not "
1102 "available from A&AI - {}".
1103 format(demand_name, complex_link))
1104 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1105 triage_translator_data,
1106 reason="Complex information not available from A&AI")
1107 return # move ahead with the next vnf
1109 complex_info = self._get_complex(
1110 complex_link=complex_link,
1111 complex_id=complex_id
1113 if not complex_info:
1114 LOG.debug("{} complex information not "
1115 "available from A&AI - {}".
1116 format(demand_name, complex_link))
1117 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1118 triage_translator_data,
1119 reason="Complex information not available from A&AI")
1120 return # move ahead with the next vnf
1122 complex_info = self.build_complex_dict(complex_info, '')
1125 def resolve_demands(self, demands, plan_info, triage_translator_data):
1126 """Resolve demands into inventory candidate lists"""
1128 self.triage_translator.getPlanIdNAme(plan_info['plan_name'], plan_info['plan_id'], triage_translator_data)
1130 resolved_demands = {}
1131 for name, requirements in demands.items():
1132 self.triage_translator.addDemandsTriageTranslator(name, triage_translator_data)
1133 resolved_demands[name] = []
1134 for requirement in requirements:
1135 inventory_type = requirement.get('inventory_type').lower()
1136 service_subscription = requirement.get('service_subscription')
1137 candidate_uniqueness = requirement.get('unique', 'true')
1138 filtering_attributes = requirement.get('filtering_attributes')
1139 passthrough_attributes = requirement.get('passthrough_attributes')
1140 default_attributes = requirement.get('default_attributes')
1141 # TODO(XYZ): may need to support multiple service_type and customer_id in the futrue
1143 # TODO(XYZ): make it consistent for dash and underscore
1144 if filtering_attributes:
1145 # catch equipment-role and service-type from template
1146 equipment_role = filtering_attributes.get('equipment-role')
1147 service_type = filtering_attributes.get('service-type')
1149 service_type = equipment_role
1150 # catch global-customer-id and customer-id from template
1151 global_customer_id = filtering_attributes.get('global-customer-id')
1152 customer_id = filtering_attributes.get('customer-id')
1153 if global_customer_id:
1154 customer_id = global_customer_id
1156 model_invariant_id = filtering_attributes.get('model-invariant-id')
1157 model_version_id = filtering_attributes.get('model-version-id')
1158 service_role = filtering_attributes.get('service-role')
1161 service_type = equipment_role = requirement.get('service_type')
1162 customer_id = global_customer_id = requirement.get('customer_id')
1163 # region_id is OPTIONAL. This will restrict the initial
1164 # candidate set to come from the given region id
1165 restricted_region_id = requirement.get('region')
1166 restricted_complex_id = requirement.get('complex')
1167 # Used for order locking feature
1168 # by defaut, conflict id is the combination of candidate id, service type and vnf-e2e-key
1169 conflict_identifier = requirement.get('conflict_identifier')
1171 vlan_key = requirement.get('vlan_key')
1172 port_key = requirement.get('port_key')
1174 # get required candidates from the demand
1175 required_candidates = requirement.get("required_candidates")
1177 # get existing_placement from the demand
1178 existing_placement = requirement.get("existing_placement")
1180 if required_candidates:
1181 resolved_demands['required_candidates'] = \
1184 # get excluded candidate from the demand
1185 excluded_candidates = requirement.get("excluded_candidates")
1187 # service_resource_id is OPTIONAL and is
1188 # transparent to Conductor
1189 service_resource_id = requirement.get('service_resource_id') \
1190 if requirement.get('service_resource_id') else ''
1192 if inventory_type == 'cloud':
1193 # load region candidates from cache
1194 regions = self._get_regions()
1195 if not regions or len(regions) < 1:
1196 LOG.debug("Region information is not available in cache")
1197 for region_id, region in regions.items():
1198 # Pick only candidates from the restricted_region
1199 info = Candidate.build_candidate_info('aai', inventory_type,
1200 self.conf.data.cloud_candidate_cost,
1201 candidate_uniqueness, region_id, service_resource_id)
1202 cloud = self.resolve_cloud_for_region(region, region_id)
1203 complex_info = self.build_complex_dict(region['complex'], inventory_type)
1204 flavors = self.resolve_flavors_for_region(region['flavors'])
1206 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1207 if self.check_sriov_automation(cloud['cloud_region_version'], name, info['candidate_id']):
1208 other['sriov_automation'] = 'true'
1210 other['sriov_automation'] = 'false'
1211 cloud_candidate = Cloud(info=info, cloud_region=cloud, complex=complex_info, flavors=flavors,
1212 additional_fields=other)
1213 candidate = cloud_candidate.convert_nested_dict_to_dict()
1215 cloud_region_attr = dict()
1216 cloud_region_attr['cloud-owner'] = region['cloud_owner']
1217 cloud_region_attr['cloud-region-version'] = region['cloud_region_version']
1218 cloud_region_attr['cloud-type'] = region['cloud_type']
1219 cloud_region_attr['cloud-zone'] = region['cloud_zone']
1220 cloud_region_attr['complex-name'] = region['complex_name']
1221 cloud_region_attr['physical-location-id'] = region['physical_location_id']
1223 if filtering_attributes and (not self.match_inventory_attributes(filtering_attributes,
1225 candidate['candidate_id'])):
1226 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1227 candidate['location_id'], name,
1228 triage_translator_data,
1229 reason='attributes and match invetory '
1233 if conflict_identifier:
1234 candidate['conflict_id'] = self.resovle_conflict_id(conflict_identifier, candidate)
1236 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1237 triage_translator_data):
1240 self.assign_candidate_existing_placement(candidate, existing_placement)
1242 # Pick only candidates not in the excluded list, if excluded candidate list is provided
1243 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1244 name, triage_translator_data):
1247 # Pick only candidates in the required list, if required candidate list is provided
1248 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1250 triage_translator_data):
1253 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1254 # add candidate to demand candidates
1255 resolved_demands[name].append(candidate)
1256 LOG.debug(">>>>>>> Candidate <<<<<<<")
1257 LOG.debug(json.dumps(candidate, indent=4))
1259 elif (inventory_type == 'service') and customer_id:
1260 # First level query to get the list of generic vnfs
1261 vnf_by_model_invariant = list()
1262 if filtering_attributes and model_invariant_id:
1264 raw_path = '/network/generic-vnfs/' \
1265 '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1266 if model_version_id:
1267 raw_path = '/network/generic-vnfs/' \
1268 '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1270 path = self._aai_versioned_path(raw_path)
1271 vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1273 vnf_by_service_type = list()
1274 if service_type or equipment_role:
1275 path = self._aai_versioned_path(
1276 '/network/generic-vnfs/'
1277 '?equipment-role={}&depth=0'.format(service_type))
1278 vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1280 generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1283 for vnf in generic_vnf:
1284 # if this vnf already appears, skip it
1285 vnf_id = vnf.get('vnf-id')
1286 if vnf_id in vnf_dict:
1288 # add vnf (with vnf_id as key) to the dictionary
1289 vnf_dict[vnf_id] = vnf
1291 vnf_info['host_id'] = vnf.get("vnf-name")
1292 vlan_info = self.build_vlan_info(vlan_key, port_key)
1293 cloud = self.resolve_cloud_for_vnf('', '', vnf, service_type, name, triage_translator_data)
1294 if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1295 cloud['cloud_region_version'] is None:
1298 rl_data = self.resolve_global_customer_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1299 service_type, name, triage_translator_data)
1303 vs_cust_id = rl_data.get('d_value')
1304 rl_data = self.resolve_service_instance_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1305 service_type, name, triage_translator_data)
1309 vs_service_instance_id = rl_data.get('d_value')
1312 if vs_cust_id and vs_cust_id == customer_id:
1313 info = Candidate.build_candidate_info('aai', inventory_type,
1314 self.conf.data.service_candidate_cost,
1315 candidate_uniqueness, vs_service_instance_id,
1316 service_resource_id)
1317 else: # vserver is for a different customer
1318 self.triage_translator.collectDroppedCandiate('', cloud['location_id'], name,
1319 triage_translator_data,
1320 reason="vserver is for a different customer")
1322 # Added vim-id for short-term workaround
1324 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1325 other['sriov_automation'] = 'true' if self.check_sriov_automation(
1326 cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1328 # Second level query to get the pserver from vserver
1329 complex_list = list()
1330 for complex_link in self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'], cloud,
1332 triage_translator_data,
1334 complex_list.append(complex_link[1])
1335 complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1336 cloud['location_id'], vnf,
1337 complex_list, service_type, name,
1338 triage_translator_data)
1339 if "complex_name" not in complex_info:
1342 service_candidate = Service(info=info, cloud_region=cloud, complex=complex_info,
1343 generic_vnf=vnf_info, additional_fields=other, vlan=vlan_info)
1344 candidate = service_candidate.convert_nested_dict_to_dict()
1346 # add specifal parameters for comparsion
1347 vnf['global-customer-id'] = customer_id
1348 vnf['customer-id'] = customer_id
1349 vnf['cloud-region-id'] = cloud.get('cloud_region_id')
1350 vnf['physical-location-id'] = complex_info.get('physical_location_id')
1352 if filtering_attributes and not self.match_inventory_attributes(filtering_attributes, vnf,
1353 candidate['candidate_id']):
1354 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1355 candidate['location_id'], name,
1356 triage_translator_data,
1357 reason="attibute check error")
1359 self.assign_candidate_existing_placement(candidate, existing_placement)
1361 # Pick only candidates not in the excluded list
1362 # if excluded candidate list is provided
1363 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1364 name, triage_translator_data):
1367 # Pick only candidates in the required list
1368 # if required candidate list is provided
1369 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1371 triage_translator_data):
1374 # add the candidate to the demand
1375 # Pick only candidates from the restricted_region
1376 # or restricted_complex
1377 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1378 triage_translator_data):
1381 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1382 resolved_demands[name].append(candidate)
1383 LOG.debug(">>>>>>> Candidate <<<<<<<")
1384 LOG.debug(json.dumps(candidate, indent=4))
1386 elif (inventory_type == 'vfmodule') and customer_id:
1388 # First level query to get the list of generic vnfs
1389 vnf_by_model_invariant = list()
1390 if filtering_attributes and model_invariant_id:
1392 raw_path = '/network/generic-vnfs/' \
1393 '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1394 if model_version_id:
1395 raw_path = '/network/generic-vnfs/' \
1396 '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1398 path = self._aai_versioned_path(raw_path)
1399 vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1401 vnf_by_service_type = list()
1402 if service_type or equipment_role:
1403 path = self._aai_versioned_path('/network/generic-vnfs/'
1404 '?equipment-role={}&depth=0'.format(service_type))
1405 vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1407 generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1410 for vnf in generic_vnf:
1411 # if this vnf already appears, skip it
1412 vnf_id = vnf.get('vnf-id')
1413 if vnf_id in vnf_dict:
1415 # add vnf (with vnf_id as key) to the dictionary
1416 vnf_dict[vnf_id] = vnf
1419 info = Candidate.build_candidate_info('aai', inventory_type,
1420 self.conf.data.service_candidate_cost,
1421 candidate_uniqueness, "", service_resource_id)
1423 vlan_info = self.build_vlan_info(vlan_key, port_key)
1425 vnf_info = self.get_vnf_info(vnf)
1427 rl_data = self.resolve_global_customer_id_for_vnf('', '', vnf, customer_id,
1428 service_type, name, triage_translator_data)
1432 vs_cust_id = rl_data.get('d_value')
1434 rl_data = self.resolve_service_instance_id_for_vnf('', '', vnf, customer_id,
1435 service_type, name, triage_translator_data)
1439 vs_service_instance_id = rl_data.get('d_value')
1441 service_info = dict()
1442 if vs_cust_id and vs_cust_id == customer_id:
1443 service_info['service_instance_id'] = vs_service_instance_id
1444 else: # vserver is for a different customer
1445 self.triage_translator.collectDroppedCandiate('', '', name,
1446 triage_translator_data,
1447 reason="candidate is for a different"
1451 vf_modules_list = self.resolve_vf_modules_for_generic_vnf('', '', vnf, name,
1452 triage_translator_data)
1453 if vf_modules_list is None:
1456 for vf_module in vf_modules_list:
1457 # for vfmodule demands we allow to have vfmodules from different cloud regions
1458 info['candidate_id'] = vf_module.get("vf-module-id")
1459 vf_module_info = self.get_vf_module(vf_module)
1460 cloud = self.resolve_cloud_for_vnf(info['candidate_id'], '', vf_module, service_type, name,
1461 triage_translator_data)
1462 if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1463 cloud['cloud_region_version'] is None:
1466 # OTHER - Added vim-id for short-term workaround
1468 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1469 other['sriov_automation'] = 'true' if self.check_sriov_automation(
1470 cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1472 # Second level query to get the pserver from vserver
1473 vserver_info = dict()
1474 vserver_info['vservers'] = list()
1475 complex_list = list()
1476 for v_server, complex_link in \
1477 self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'],
1479 triage_translator_data,
1481 complex_list.append(complex_link)
1482 candidate_vserver = dict()
1483 candidate_vserver['vserver-id'] = v_server.get('vserver-id')
1484 candidate_vserver['vserver-name'] = v_server.get('vserver-name')
1485 l_interfaces = self.get_l_interfaces_from_vserver(info['candidate_id'],
1486 cloud['location_id'],
1488 triage_translator_data)
1490 candidate_vserver['l-interfaces'] = l_interfaces
1493 vserver_info['vservers'].append(candidate_vserver)
1496 complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1497 cloud['location_id'], vnf,
1498 complex_list, service_type, name,
1499 triage_translator_data)
1500 if complex_info.get("complex_name") is None:
1503 vf_module_candidate = VfModule(complex=complex_info, info=info, generic_vnf=vnf_info,
1504 cloud_region=cloud, service_instance=service_info,
1505 vf_module=vf_module_info, vserver=vserver_info,
1506 additional_fields=other, vlan=vlan_info)
1507 candidate = vf_module_candidate.convert_nested_dict_to_dict()
1509 # add vf-module parameters for filtering
1510 vnf_vf_module_inventory = copy.deepcopy(vnf)
1511 vnf_vf_module_inventory.update(vf_module)
1512 # add specifal parameters for comparsion
1513 vnf_vf_module_inventory['global-customer-id'] = customer_id
1514 vnf_vf_module_inventory['customer-id'] = customer_id
1515 vnf_vf_module_inventory['cloud-region-id'] = cloud.get('location_id')
1516 vnf_vf_module_inventory['physical-location-id'] = complex_info.get('physical_location_id')
1517 vnf_vf_module_inventory['service_instance_id'] = vs_service_instance_id
1519 if filtering_attributes and not self.match_inventory_attributes(filtering_attributes,
1520 vnf_vf_module_inventory,
1521 candidate['candidate_id']):
1522 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1523 candidate['location_id'], name,
1524 triage_translator_data,
1525 reason="attibute check error")
1527 self.assign_candidate_existing_placement(candidate, existing_placement)
1529 # Pick only candidates not in the excluded list
1530 # if excluded candidate list is provided
1531 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates,
1533 name, triage_translator_data):
1536 # Pick only candidates in the required list
1537 # if required candidate list is provided
1538 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1540 triage_translator_data):
1543 # add the candidate to the demand
1544 # Pick only candidates from the restricted_region
1545 # or restricted_complex
1546 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1547 triage_translator_data):
1550 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1551 resolved_demands[name].append(candidate)
1552 LOG.debug(">>>>>>> Candidate <<<<<<<")
1553 with open("vf.log", mode='w') as log_file:
1554 log_file.write(">>>>>>>Vf Candidate <<<<<<<")
1555 log_file.write(json.dumps(candidate, indent=4))
1556 LOG.debug(json.dumps(candidate, indent=4))
1558 elif inventory_type == 'transport' \
1559 and customer_id and service_type and \
1560 service_subscription and service_role:
1562 path = self._aai_versioned_path('business/customers/customer/{}/service-subscriptions/'
1563 'service-subscription/{}/service-instances'
1564 '?service-type={}&service-role={}'.format(customer_id,
1565 service_subscription,
1568 response = self._request('get', path=path, data=None)
1569 if response is None or response.status_code != 200:
1570 self.triage_translator.collectDroppedCandiate("", "", name,
1571 triage_translator_data,
1572 reason=response.status_code)
1574 body = response.json()
1575 transport_vnfs = body.get('service-instance', [])
1577 for vnf in transport_vnfs:
1578 # create a default candidate
1580 other['location_id'] = ''
1581 other['location_type'] = 'att_aic'
1583 vnf_service_instance_id = vnf.get('service-instance-id')
1584 if vnf_service_instance_id:
1585 info = Candidate.build_candidate_info('aai', inventory_type,
1586 self.conf.data.transport_candidate_cost,
1587 candidate_uniqueness, vnf_service_instance_id,
1588 service_resource_id)
1590 self.triage_translator.collectDroppedCandiate('', other['location_id'], name,
1591 triage_translator_data,
1592 reason="service-instance-id error ")
1597 zone = self.resolve_zone_for_vnf(info['candidate_id'], other['location_id'], vnf, name,
1598 triage_translator_data)
1600 zone_info['zone_id'] = zone.get('zone-id')
1601 zone_info['zone_name'] = zone.get('zone-name')
1606 related_to = "complex"
1607 search_key = "complex.physical-location-id"
1608 rel_link_data_list = self._get_aai_rel_link_data(
1610 related_to=related_to,
1611 search_key=search_key
1614 if len(rel_link_data_list) > 1:
1615 self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1616 name, triage_translator_data,
1617 reason="rel_link_data_list error")
1620 rel_link_data = rel_link_data_list[0]
1621 complex_id = rel_link_data.get("d_value")
1622 complex_link = rel_link_data.get('link')
1624 if not (complex_link and complex_id):
1625 LOG.debug("{} complex information not "
1626 "available from A&AI - {}".
1627 format(name, complex_link))
1628 self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1629 name, triage_translator_data,
1630 reason="complex information not available "
1634 complex_info = self._get_complex(
1635 complex_link=complex_link,
1636 complex_id=complex_id
1638 if not complex_info:
1639 LOG.debug("{} complex information not "
1640 "available from A&AI - {}".
1641 format(name, complex_link))
1642 self.triage_translator.collectDroppedCandiate(info['candidate_id'],
1643 other['location_id'], name,
1644 triage_translator_data,
1645 reason="complex information not "
1646 "available from A&AI")
1647 continue # move ahead with the next vnf
1649 complex_info = self.build_complex_dict(complex_info, inventory_type)
1650 transport_candidate = Transport(info=info, zone=zone_info, complex=complex_info,
1651 additional_fiels=other)
1652 candidate = transport_candidate.convert_nested_dict_to_dict()
1654 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1655 # add candidate to demand candidates
1656 resolved_demands[name].append(candidate)
1658 elif inventory_type == 'nssi' or inventory_type == 'nsi':
1659 if filtering_attributes and model_invariant_id:
1660 second_level_match = aai_utils.get_first_level_and_second_level_filter(filtering_attributes,
1662 aai_response = self.get_nxi_candidates(filtering_attributes)
1663 resolved_demands[name].extend(self.filter_nxi_candidates(aai_response, second_level_match,
1665 candidate_uniqueness, inventory_type))
1667 elif inventory_type == 'nst':
1668 if filtering_attributes:
1669 second_level_match = aai_utils.get_first_level_and_second_level_filter(filtering_attributes,
1671 aai_response = self.get_nst_response(filtering_attributes)
1673 sdc_candidates_list = self.get_nst_candidates(aai_response, second_level_match,
1674 default_attributes, candidate_uniqueness,
1676 resolved_demands[name].extend(SDC().update_candidates(sdc_candidates_list))
1679 LOG.error("Unknown inventory_type "
1680 " {}".format(inventory_type))
1681 return resolved_demands
1684 def build_complex_dict(aai_complex, inv_type):
1685 complex_info = dict()
1686 valid_keys = ['physical-location-id', 'complex-name', 'latitude', 'longitude', 'state', 'country', 'city',
1688 # for cloud type, complex_id instead of physical-location-id - note
1689 if inv_type == "cloud":
1690 for valid_key in valid_keys:
1691 if '-' in valid_key:
1692 complex_info[valid_key.replace('-', '_')] = aai_complex.get('complex_id') \
1693 if valid_key == 'physical-location-id' else \
1694 aai_complex.get(valid_key.replace('-', '_'))
1696 complex_info[valid_key] = aai_complex.get(valid_key)
1698 for valid_key in valid_keys:
1699 if '-' in valid_key:
1700 complex_info[valid_key.replace('-', '_')] = aai_complex.get(valid_key)
1702 complex_info[valid_key] = aai_complex.get(valid_key)
1706 def build_vlan_info(vlan_key, port_key):
1708 vlan_info['vlan_key'] = vlan_key
1709 vlan_info['port_key'] = port_key
1712 def resolve_flavors_for_region(self, flavors_obj):
1713 if self.conf.HPA_enabled:
1715 flavors['flavors'] = flavors_obj
1718 def resolve_v_server_and_complex_link_for_vnf(self, candidate_id, cloud, vnf, name, triage_translator_data,
1720 vs_link_list = self.resolve_v_server_links_for_vnf(vnf)
1721 for vs_link in vs_link_list:
1722 body = self.resolve_v_server_for_candidate(candidate_id, cloud['location_id'],
1723 vs_link, True, name, triage_translator_data)
1726 rl_data = self.resolve_complex_info_link_for_v_server(candidate_id, body,
1727 cloud['cloud_owner'], cloud['location_id'],
1728 service_type, name, triage_translator_data)
1733 def get_l_interfaces_from_vserver(self, candidate_id, location_id, v_server, name, triage_translator_data):
1734 if not v_server.get('l-interfaces') or not v_server.get('l-interfaces').get('l-interface'):
1735 self.triage_translator.collectDroppedCandiate(candidate_id,
1737 triage_translator_data,
1738 reason="VF-server interfaces error")
1741 l_interfaces = v_server.get('l-interfaces').get('l-interface')
1742 l_interfaces_list = list()
1744 for l_interface in l_interfaces:
1745 vserver_interface = dict()
1746 vserver_interface['interface-id'] = l_interface.get('interface-id')
1747 vserver_interface['interface-name'] = l_interface.get('interface-name')
1748 vserver_interface['macaddr'] = l_interface.get('macaddr')
1749 vserver_interface['network-id'] = l_interface.get('network-name')
1750 vserver_interface['network-name'] = ''
1751 vserver_interface['ipv4-addresses'] = list()
1752 vserver_interface['ipv6-addresses'] = list()
1754 if l_interface.get('l3-interface-ipv4-address-list'):
1755 for ip_address_info in l_interface.get('l3-interface-ipv4-address-list'):
1756 vserver_interface['ipv4-addresses']. \
1757 append(ip_address_info.get('l3-interface-ipv4-address'))
1759 if l_interface.get('l3-interface-ipv6-address-list'):
1760 for ip_address_info in l_interface.get('l3-interface-ipv6-address-list'):
1761 vserver_interface['ipv6-addresses']. \
1762 append(ip_address_info.get('l3-interface-ipv6-address'))
1764 l_interfaces_list.append(vserver_interface)
1765 return l_interfaces_list
1768 def get_vnf_info(vnf):
1769 # some validation should happen
1771 vnf_info['host_id'] = vnf.get("vnf-name")
1772 vnf_info['nf-name'] = vnf.get("vnf-name")
1773 vnf_info['nf-id'] = vnf.get("vnf-id")
1774 vnf_info['nf-type'] = 'vnf'
1775 vnf_info['vnf-type'] = vnf.get("vnf-type")
1776 vnf_info['ipv4-oam-address'] = vnf.get("ipv4-oam-address") if vnf.get("ipv4-oam-address") else ""
1777 vnf_info['ipv6-oam-address'] = vnf.get("ipv6-oam-address") if vnf.get("ipv6-oam-address") else ""
1781 def resolve_cloud_for_region(region, region_id):
1783 valid_keys = ['cloud_owner', 'cloud_region_version', 'location_id']
1784 for valid_key in valid_keys:
1785 cloud[valid_key] = region.get(valid_key) if not valid_key == 'location_id' else region_id
1786 cloud['location_type'] = 'att_aic'
1790 def get_vf_module(vf_module):
1791 vf_module_info = dict()
1792 vf_module_info['vf-module-name'] = vf_module.get("vf-module-name")
1793 vf_module_info['vf-module-id'] = vf_module.get("vf-module-id")
1794 return vf_module_info
1796 def get_vim_id(self, cloud_owner, cloud_region_id):
1797 if self.conf.HPA_enabled:
1798 return cloud_owner + '_' + cloud_region_id
1801 def add_passthrough_attributes(candidate, passthrough_attributes, demand_name):
1802 if passthrough_attributes is None:
1804 if len(passthrough_attributes.items()) > 0:
1805 candidate['passthrough_attributes'] = dict()
1806 for key, value in passthrough_attributes.items():
1807 candidate['passthrough_attributes'][key] = value
1809 def resolve_zone_for_vnf(self, candidate_id, location_id, vnf, name, triage_translator_data):
1811 zone_link = self._get_aai_rel_link(
1812 data=vnf, related_to=related_to)
1814 LOG.error("Zone information not available from A&AI for transport candidates")
1815 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1816 name, triage_translator_data,
1817 reason="Zone information not available from A&AI for "
1818 "transport candidates")
1820 zone_aai_path = self._get_aai_path_from_link(zone_link)
1821 response = self._request('get', path=zone_aai_path, data=None)
1822 if response is None or response.status_code != 200:
1823 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, name,
1824 triage_translator_data,
1825 reason=response.status_code)
1827 body = response.json()
1830 def match_region(self, candidate, restricted_region_id, restricted_complex_id, demand_name,
1831 triage_translator_data):
1832 if self.match_candidate_attribute(
1835 restricted_region_id,
1837 candidate.get('inventory_type')) or \
1838 self.match_candidate_attribute(
1840 "physical_location_id",
1841 restricted_complex_id,
1843 candidate.get('inventory_type')):
1844 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1845 demand_name, triage_translator_data,
1846 reason="candidate region does not match")
1851 def match_candidate_by_list(self, candidate, candidates_list, exclude, demand_name, triage_translator_data):
1852 has_candidate = False
1854 for list_candidate in candidates_list:
1856 and list_candidate.get('inventory_type') \
1857 == candidate.get('inventory_type'):
1858 if isinstance(list_candidate.get('candidate_id'), list):
1859 for candidate_id in list_candidate.get('candidate_id'):
1860 if candidate_id == candidate.get('candidate_id'):
1861 has_candidate = True
1864 raise Exception("Invalid candidate id list format")
1869 if not has_candidate:
1870 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1871 demand_name, triage_translator_data,
1872 reason="has_required_candidate candidate")
1874 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1875 demand_name, triage_translator_data,
1876 reason="excluded candidate")
1877 return has_candidate
1879 def get_nxi_candidates(self, filtering_attributes):
1880 raw_path = 'nodes/service-instances' + aai_utils.add_query_params_and_depth(filtering_attributes, "2")
1881 path = self._aai_versioned_path(raw_path)
1882 aai_response = self._request('get', path, data=None)
1884 if aai_response is None or aai_response.status_code != 200:
1886 if aai_response.json():
1887 return aai_response.json()
1889 def filter_nxi_candidates(self, response_body, filtering_attributes, default_attributes, candidate_uniqueness,
1892 if response_body is not None:
1893 nxi_instances = response_body.get("service-instance", [])
1895 for nxi_instance in nxi_instances:
1896 inventory_attributes = aai_utils.get_inv_values_for_second_level_filter(filtering_attributes,
1898 nxi_info = aai_utils.get_instance_info(nxi_instance)
1899 if not filtering_attributes or \
1900 self.match_inventory_attributes(filtering_attributes, inventory_attributes,
1901 nxi_instance.get('service-instance-id')):
1903 profiles = nxi_instance.get('slice-profiles').get('slice-profile')
1904 cost = self.conf.data.nssi_candidate_cost
1906 profiles = nxi_instance.get('service-profiles').get('service-profile')
1907 cost = self.conf.data.nsi_candidate_cost
1908 for profile in profiles:
1909 profile_id = profile.get('profile-id')
1910 info = Candidate.build_candidate_info('aai', type, cost, candidate_uniqueness, profile_id)
1911 profile_info = aai_utils.convert_hyphen_to_under_score(profile)
1912 nxi_candidate = NxI(instance_info=nxi_info, profile_info=profile_info, info=info,
1913 default_fields=aai_utils.convert_hyphen_to_under_score(default_attributes))
1914 candidate = nxi_candidate.convert_nested_dict_to_dict()
1915 candidates.append(candidate)
1918 def get_nst_response(self, filtering_attributes):
1919 raw_path = 'service-design-and-creation/models' + aai_utils.add_query_params_and_depth(filtering_attributes,
1921 path = self._aai_versioned_path(raw_path)
1922 aai_response = self._request('get', path, data=None)
1924 if aai_response is None or aai_response.status_code != 200:
1926 if aai_response.json():
1927 return aai_response.json()
1929 def get_nst_candidates(self, response_body, filtering_attributes, default_attributes, candidate_uniqueness,
1932 if response_body is not None:
1933 nst_metadatas = response_body.get("model", [])
1935 for nst_metadata in nst_metadatas:
1936 nst_info = aai_utils.get_nst_info(nst_metadata)
1937 model_vers = nst_metadata.get('model-vers').get('model-ver')
1938 for model_ver in model_vers:
1939 model_version_id = model_ver.get('model-version-id')
1940 cost = self.conf.data.nst_candidate_cost
1941 info = Candidate.build_candidate_info('aai', type, cost, candidate_uniqueness, model_version_id)
1942 model_version_obj = aai_utils.get_model_ver_info(model_ver)
1943 model_ver_info = aai_utils.convert_hyphen_to_under_score(model_version_obj)
1944 nst_candidate = NST(model_info=nst_info, model_ver=model_ver_info, info=info,
1945 default_fields=aai_utils.convert_hyphen_to_under_score(default_attributes),
1947 candidates.append(nst_candidate)