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.nxi_candidate import NxI
37 from conductor.data.plugins.inventory_provider.candidates.service_candidate import Service
38 from conductor.data.plugins.inventory_provider.candidates.transport_candidate import Transport
39 from conductor.data.plugins.inventory_provider.candidates.vfmodule_candidate import VfModule
40 from conductor.data.plugins.inventory_provider import hpa_utils
41 from conductor.data.plugins.inventory_provider.utils import aai_utils
42 from conductor.data.plugins.triage_translator.triage_translator import TraigeTranslator
43 from conductor.i18n import _LE
44 from conductor.i18n import _LI
46 LOG = log.getLogger(__name__)
51 cfg.IntOpt('cache_refresh_interval',
53 help='Interval with which to refresh the local cache, '
55 cfg.IntOpt('complex_cache_refresh_interval',
57 help='Interval with which to refresh the local complex cache, '
59 cfg.StrOpt('table_prefix',
61 help='Data Store table prefix.'),
62 cfg.StrOpt('server_url',
63 default='https://controller:8443/aai',
64 help='Base URL for A&AI, up to and not including '
65 'the version, and without a trailing slash.'),
66 cfg.StrOpt('aai_rest_timeout',
68 help='Timeout for A&AI Rest Call'),
69 cfg.StrOpt('aai_retries',
71 help='Number of retry for A&AI Rest Call'),
72 cfg.StrOpt('server_url_version',
74 help='The version of A&AI in v# format.'),
75 cfg.StrOpt('certificate_file',
76 default='certificate.pem',
77 help='SSL/TLS certificate file in pem format. '
78 'This certificate must be registered with the A&AI '
80 cfg.StrOpt('certificate_key_file',
81 default='certificate_key.pem',
82 help='Private Certificate Key file in pem format.'),
83 cfg.StrOpt('certificate_authority_bundle_file',
84 default='certificate_authority_bundle.pem',
85 help='Certificate Authority Bundle file in pem format. '
86 'Must contain the appropriate trust chain for the '
88 # TODO(larry): follow-up with ONAP people on this (AA&I basic auth username and password?)
89 cfg.StrOpt('username',
91 help='Username for AAI.'),
92 cfg.StrOpt('password',
94 help='Password for AAI.'),
97 CONF.register_opts(AAI_OPTS, group='aai')
100 class AAI(base.InventoryProviderBase):
101 """Active and Available Inventory Provider"""
106 # FIXME(jdandrea): Pass this in to init.
109 self.base = self.conf.aai.server_url.rstrip('/')
110 self.version = self.conf.aai.server_url_version.rstrip('/')
111 self.cert = self.conf.aai.certificate_file
112 self.key = self.conf.aai.certificate_key_file
113 self.verify = self.conf.aai.certificate_authority_bundle_file
114 self.cache_refresh_interval = self.conf.aai.cache_refresh_interval
115 self.last_refresh_time = None
116 self.complex_cache_refresh_interval = \
117 self.conf.aai.complex_cache_refresh_interval
118 self.complex_last_refresh_time = None
119 self.timeout = self.conf.aai.aai_rest_timeout
120 self.retries = self.conf.aai.aai_retries
121 self.username = self.conf.aai.username
122 self.password = self.conf.aai.password
123 self.triage_translator = TraigeTranslator()
125 # Cache is initially empty
127 self._aai_complex_cache = {}
129 def initialize(self):
131 """Perform any late initialization."""
132 # Initialize the Python requests
133 self._init_python_request()
135 # Refresh the cache once for now
136 self._refresh_cache()
138 # TODO(jdandrea): Make this periodic, and without a True condition!
139 # executor = futurist.ThreadPoolExecutor()
141 # fut = executor.submit(self.refresh_cache)
144 # # Now wait for the next time.
145 # # FIXME(jdandrea): Put inside refresh_cache()?
146 # refresh_interval = self.conf.aai.cache_refresh_interval
147 # time.sleep(refresh_interval)
148 # executor.shutdown()
151 """Return human-readable name."""
155 def _get_version_from_string(string):
156 """Extract version number from string"""
157 return re.sub("[^0-9.]", "", string)
159 def _aai_versioned_path(self, path):
160 """Return a URL path with the A&AI version prepended"""
161 return '/{}/{}'.format(self.version, path.lstrip('/'))
163 def _request(self, method='get', path='/', data=None,
164 context=None, value=None):
165 """Performs HTTP request."""
167 'X-FromAppId': 'CONDUCTOR',
168 'X-TransactionId': str(uuid.uuid4()),
177 # TODO(jdandrea): Move timing/response logging into the rest helper?
178 start_time = time.time()
179 response = self.rest.request(**kwargs)
180 elapsed = time.time() - start_time
181 LOG.debug("Total time for A&AI request "
182 "({0:}: {1:}): {2:.3f} sec".format(context, value, elapsed))
185 LOG.error(_LE("No response from A&AI ({}: {})").
186 format(context, value))
187 elif response.status_code != 200:
188 LOG.error(_LE("A&AI request ({}: {}) returned HTTP "
189 "status {} {}, link: {}{}").
190 format(context, value,
191 response.status_code, response.reason,
195 def _init_python_request(self):
198 "server_url": self.base,
199 "retries": self.retries,
200 "username": self.username,
201 "password": self.password,
202 "cert_file": self.cert,
203 "cert_key_file": self.key,
204 "ca_bundle_file": self.verify,
205 "log_debug": self.conf.debug,
206 "read_timeout": self.timeout,
208 self.rest = rest.REST(**kwargs)
210 def _refresh_cache(self):
211 """Refresh the A&AI cache."""
212 if not self.last_refresh_time or \
213 (time.time() - self.last_refresh_time) > \
214 self.cache_refresh_interval * 60:
215 # TODO(jdandrea): This is presently brute force.
216 # It does not persist to Music. A general purpose ORM caching
217 # object likely needs to be made, with a key (hopefully we
218 # can use one that is not just a UUID), a value, and a
219 # timestamp. The other alternative is to not use the ORM
220 # layer and call the API directly, but that is
221 # also trading one set of todos for another ...
224 LOG.info(_LI("**** Refreshing A&AI cache *****"))
225 path = self._aai_versioned_path(
226 '/cloud-infrastructure/cloud-regions/?depth=0')
227 response = self._request(
228 path=path, context="cloud regions", value="all")
232 if response.status_code == 200:
233 body = response.json()
234 regions = body.get('cloud-region', {})
236 # Nothing to update the cache with
237 LOG.error(_LE("A&AI returned no regions, link: {}{}").
238 format(self.base, path))
244 for region in regions:
245 cloud_region_id = region.get('cloud-region-id')
247 LOG.debug("Working on region '{}' ".format(cloud_region_id))
249 cloud_region_version = region.get('cloud-region-version')
250 cloud_owner = region.get('cloud-owner')
251 cloud_type = region.get('cloud-type')
252 cloud_zone = region.get('cloud-zone')
254 physical_location_list = self._get_aai_rel_link_data(data=region, related_to='complex',
255 search_key='complex.physical-location-id')
256 if len(physical_location_list) > 0:
257 physical_location_id = physical_location_list[0].get('d_value')
259 if not (cloud_region_version and cloud_region_id):
261 rel_link_data_list = \
262 self._get_aai_rel_link_data(
264 related_to='complex',
265 search_key='complex.physical-location-id')
266 if len(rel_link_data_list) > 1:
267 LOG.error(_LE("Region {} has more than one complex").
268 format(cloud_region_id))
269 LOG.debug("Region {}: {}".format(cloud_region_id, region))
272 rel_link_data = rel_link_data_list[0]
273 complex_id = rel_link_data.get("d_value")
274 complex_link = rel_link_data.get("link")
275 if complex_id and complex_link:
276 complex_info = self._get_complex(
277 complex_link=complex_link,
278 complex_id=complex_id)
279 else: # no complex information
280 LOG.error(_LE("Region {} does not reference a complex").
281 format(cloud_region_id))
284 LOG.error(_LE("Region {}, complex {} info not found, "
285 "link {}").format(cloud_region_id,
286 complex_id, complex_link))
289 latitude = complex_info.get('latitude')
290 longitude = complex_info.get('longitude')
291 city = complex_info.get('city')
292 state = complex_info.get('state')
293 region = complex_info.get('region')
294 country = complex_info.get('country')
295 complex_name = complex_info.get('complex-name')
297 if not (latitude and longitude and city and country and complex_name):
298 keys = ('latitude', 'longitude', 'city', 'country',
301 list(set(keys).difference(
302 list(complex_info.keys()))) # Python 3 Conversion -- dict object to list object
303 LOG.error(_LE("Complex {} is missing {}, link: {}").
304 format(complex_id, missing_keys, complex_link))
305 LOG.debug("Complex {}: {}".
306 format(complex_id, complex_info))
309 cache['cloud_region'][cloud_region_id] = {
310 'cloud_region_version': cloud_region_version,
311 'cloud_owner': cloud_owner,
312 'cloud_type': cloud_type,
313 'cloud_zone': cloud_zone,
314 'complex_name': complex_name,
315 'physical_location_id': physical_location_id,
317 'complex_id': complex_id,
318 'complex_name': complex_name,
319 'latitude': latitude,
320 'longitude': longitude,
328 # Added for HPA support
329 if self.conf.HPA_enabled:
330 flavors = self._get_flavors(cloud_owner, cloud_region_id)
331 cache['cloud_region'][cloud_region_id]['flavors'] = flavors
333 LOG.debug("Candidate with cloud_region_id '{}' selected "
334 "as a potential candidate - ".format(cloud_region_id))
335 LOG.debug("Done with region '{}' ".format(cloud_region_id))
336 self._aai_cache = cache
337 self.last_refresh_time = time.time()
338 LOG.info(_LI("**** A&AI cache refresh complete *****"))
341 def _get_aai_rel_link(data, related_to):
342 """Given an A&AI data structure, return the related-to link"""
343 rel_dict = data.get('relationship-list')
345 for key, rel_list in rel_dict.items():
347 if related_to == rel.get('related-to'):
348 return rel.get('related-link')
351 def _get_aai_rel_link_data(data, related_to, search_key=None,
353 # some strings that we will encounter frequently
354 rel_lst = "relationship-list"
355 rkey = "relationship-key"
356 rval = "relationship-value"
357 rdata = "relationship-data"
360 m_key = match_dict.get('key')
361 m_value = match_dict.get('value')
365 rel_dict = data.get(rel_lst)
366 if rel_dict: # check if data has relationship lists
367 for key, rel_list in rel_dict.items():
369 if rel.get("related-to") == related_to:
372 link = rel.get("related-link")
373 r_data = rel.get(rdata, [])
376 if rd.get(rkey) == search_key:
378 if not match_dict: # return first match
380 {"link": link, "d_value": dval}
382 break # go to next relation
383 if rd.get(rkey) == m_key \
384 and rd.get(rval) == m_value:
386 if match_dict and matched: # if matching required
388 {"link": link, "d_value": dval}
390 # matched, return search value corresponding
391 # to the matched r_data group
392 else: # no search key; just return the link
394 {"link": link, "d_value": dval}
396 if len(response) == 0:
398 {"link": None, "d_value": None}
403 def check_sriov_automation(aic_version, demand_name, candidate_name):
404 """Check if specific candidate has SRIOV automation available or no"""
406 LOG.debug(_LI("Demand {}, candidate {} has an AIC version number {}").format(demand_name, candidate_name,
408 if aic_version == "X.Y":
412 def _get_complex(self, complex_link, complex_id=None):
414 if not self.complex_last_refresh_time or \
415 (time.time() - self.complex_last_refresh_time) > \
416 self.complex_cache_refresh_interval * 60:
417 self._aai_complex_cache.clear()
418 if complex_id and complex_id in self._aai_complex_cache:
419 return self._aai_complex_cache[complex_id]
421 path = self._aai_versioned_path(self._get_aai_path_from_link(complex_link))
422 response = self._request(path=path, context="complex", value=complex_id)
425 if response.status_code == 200:
426 complex_info = response.json()
427 if 'complex' in complex_info:
428 complex_info = complex_info.get('complex')
430 latitude = complex_info.get('latitude')
431 longitude = complex_info.get('longitude')
432 city = complex_info.get('city')
433 country = complex_info.get('country')
434 # removed the state check for countries in Europe that do not always enter states
435 if not (latitude and longitude and city and country):
436 keys = ('latitude', 'longitude', 'city', 'country')
438 list(set(keys).difference(set(complex_info.keys())))
439 LOG.error(_LE("Complex {} is missing {}, link: {}").
440 format(complex_id, missing_keys, complex_link))
441 LOG.debug("Complex {}: {}".format(complex_id, complex_info))
444 if complex_id: # cache only if complex_id is given
445 self._aai_complex_cache[complex_id] = response.json()
446 self.complex_last_refresh_time = time.time()
450 def _get_regions(self):
451 self._refresh_cache()
452 regions = self._aai_cache.get('cloud_region', {})
455 def _get_flavors(self, cloud_owner, cloud_region_id):
456 '''Fetch all flavors of a given cloud regions specified using {cloud-owner}/{cloud-region-id} composite key
458 :return flavors_info json object which list of flavor nodes and its children - HPACapabilities:
461 LOG.debug("Fetch all flavors and its child nodes HPACapabilities")
462 flavor_path = constants.FLAVORS_URI % (cloud_owner, cloud_region_id)
463 path = self._aai_versioned_path(flavor_path)
464 LOG.debug("Flavors path '{}' ".format(path))
466 response = self._request(path=path, context="flavors", value="all")
469 if response.status_code == 200:
470 flavors_info = response.json()
471 if not flavors_info or not flavors_info["flavor"] or \
472 len(flavors_info["flavor"]) == 0:
473 LOG.error(_LE("Flavor is missing in Cloud-Region {}/{}").
474 format(cloud_owner, cloud_region_id))
476 LOG.debug(flavors_info)
477 # Remove extraneous flavor information
480 LOG.error(_LE("Received Error while fetching flavors from Cloud-region {}/{}").format(cloud_owner,
484 def _get_aai_path_from_link(self, link):
485 path = link.split(self.version, 1)
486 if not path or len(path) <= 1:
487 # TODO(shankar): Treat this as a critical error?
488 LOG.error(_LE("A&AI version {} not found in link {}").
489 format(self.version, link))
491 return "{}".format(path[1])
493 def check_candidate_role(self, host_id=None):
495 vnf_name_uri = '/network/generic-vnfs/?vnf-name=' + host_id + '&depth=0'
496 path = self._aai_versioned_path(vnf_name_uri)
498 response = self._request('get', path=path, data=None,
501 if response is None or not response.ok:
503 body = response.json()
505 generic_vnf = body.get("generic-vnf", [])
507 for vnf in generic_vnf:
508 related_to = "service-instance"
509 search_key = "customer.global-customer-id"
510 rl_data_list = self._get_aai_rel_link_data(
512 related_to=related_to,
513 search_key=search_key,
516 if len(rl_data_list) != 1:
519 rl_data = rl_data_list[0]
520 candidate_role_link = rl_data.get("link")
522 if not candidate_role_link:
523 LOG.error(_LE("Unable to get candidate role link for host id {} ").format(host_id))
526 candidate_role_path = self._get_aai_path_from_link(candidate_role_link) + '/allotted-resources?depth=all'
527 path = self._aai_versioned_path(candidate_role_path)
529 response = self._request('get', path=path, data=None,
530 context="candidate role")
532 if response is None or not response.ok:
534 body = response.json()
536 response_items = body.get('allotted-resource')
537 if len(response_items) > 0:
538 role = response_items[0].get('role')
541 def check_network_roles(self, network_role_id=None):
542 # the network role query from A&AI is not using
543 # the version number in the query
545 '/network/l3-networks?network-role=' + network_role_id
546 path = self._aai_versioned_path(network_role_uri)
548 # This UUID is reserved by A&AI for a Conductor-specific named query.
549 named_query_uid = "role-UUID"
552 "query-parameters": {
554 "named-query-uuid": named_query_uid
557 "instance-filters": {
561 "network-role": network_role_id
568 response = self._request('get', path=path, data=data,
569 context="role", value=network_role_id)
572 body = response.json()
574 response_items = body.get('l3-network', [])
576 for item in response_items:
577 cloud_region_instances = self._get_aai_rel_link_data(
579 related_to='cloud-region',
580 search_key='cloud-region.cloud-region-id'
583 if len(cloud_region_instances) > 0:
584 for r_instance in cloud_region_instances:
585 region_id = r_instance.get('d_value')
586 if region_id is not None:
587 region_ids.add(region_id)
589 # return region ids that fit the role
592 def resolve_host_location(self, host_name):
593 path = self._aai_versioned_path('/query?format=id')
594 data = {"start": ["network/pnfs/pnf/" + host_name,
595 "cloud-infrastructure/pservers/pserver/" + host_name],
596 "query": "query/ucpe-instance"
598 response = self._request('put', path=path, data=data,
599 context="host name", value=host_name)
600 if response is None or response.status_code != 200:
602 body = response.json()
603 results = body.get('results', [])
605 for result in results:
606 if "resource-type" in result and \
607 "resource-link" in result and \
608 result["resource-type"] == "complex":
609 complex_link = result["resource-link"]
611 LOG.error(_LE("Unable to get a complex link for hostname {} "
612 " in response {}").format(host_name, response))
614 complex_info = self._get_complex(
615 complex_link=complex_link,
619 lat = complex_info.get('latitude')
620 lon = complex_info.get('longitude')
621 country = complex_info.get('country')
623 location = {"latitude": lat, "longitude": lon}
624 location["country"] = country if country else None
627 LOG.error(_LE("Unable to get a latitude and longitude "
628 "information for hostname {} from complex "
629 " link {}").format(host_name, complex_link))
632 LOG.error(_LE("Unable to get a complex information for "
633 " hostname {} from complex "
634 " link {}").format(host_name, complex_link))
637 def resolve_clli_location(self, clli_name):
638 clli_uri = '/cloud-infrastructure/complexes/complex/' + clli_name
639 path = self._aai_versioned_path(clli_uri)
641 response = self._request('get', path=path, data=None,
642 context="clli name", value=clli_name)
643 if response is None or response.status_code != 200:
646 body = response.json()
649 lat = body.get('latitude')
650 lon = body.get('longitude')
651 country = body.get('country')
653 location = {"latitude": lat, "longitude": lon}
654 location["country"] = country if country else None
657 LOG.error(_LE("Unable to get a latitude and longitude "
658 "information for CLLI code {} from complex").
662 LOG.error(_LE("Unable to get a complex information for "
663 " clli {} from complex "
664 " link {}").format(clli_name, clli_uri))
667 def get_inventory_group_pairs(self, service_description):
669 path = self._aai_versioned_path(
670 '/network/instance-groups/?description={}&depth=0'.format(
671 service_description))
672 response = self._request(path=path, context="inventory group",
673 value=service_description)
674 if response is None or response.status_code != 200:
676 body = response.json()
677 if "instance-group" not in body:
678 LOG.error(_LE("Unable to get instance groups from inventory "
679 " in response {}").format(response))
681 for instance_groups in body["instance-group"]:
682 s_instances = self._get_aai_rel_link_data(
683 data=instance_groups,
684 related_to='service-instance',
685 search_key='service-instance.service-instance-id'
687 if s_instances and len(s_instances) == 2:
689 for s_inst in s_instances:
690 pair.append(s_inst.get('d_value'))
693 LOG.error(_LE("Number of instance pairs not found to "
694 "be two: {}").format(instance_groups))
697 def _log_multiple_item_error(self, name, service_type,
698 related_to, search_key='',
699 context=None, value=None):
700 """Helper method to log multiple-item errors
702 Used by resolve_demands
704 LOG.error(_LE("Demand {}, role {} has more than one {} ({})").
705 format(name, service_type, related_to, search_key))
706 if context and value:
707 LOG.debug("{} details: {}".format(context, value))
709 def match_candidate_attribute(self, candidate, attribute_name,
710 restricted_value, demand_name,
712 """Check if specific candidate attribute matches the restricted value
714 Used by resolve_demands
716 if restricted_value and restricted_value != '' and candidate[attribute_name] != restricted_value:
717 LOG.info(_LI("Demand: {} "
718 "Discarded {} candidate as "
719 "it doesn't match the "
721 "{} ").format(demand_name,
730 def match_vserver_attribute(self, vserver_list):
733 for i in range(0, len(vserver_list)):
735 value != vserver_list[i].get('d_value'):
737 value = vserver_list[i].get('d_value')
740 def match_inventory_attributes(self, template_attributes,
741 inventory_attributes, candidate_id):
743 for attribute_key, attribute_values in template_attributes.items():
745 if attribute_key and \
746 (attribute_key == 'service-type' or attribute_key == 'equipment-role'
747 or attribute_key == 'model-invariant-id' or attribute_key == 'model-version-id'):
751 if type(attribute_values) is dict:
752 if 'any' in attribute_values:
753 attribute_values = attribute_values['any']
754 elif 'not' in attribute_values:
756 attribute_values = attribute_values['not']
758 if match_type == 'any':
759 if attribute_key not in inventory_attributes or \
760 (len(attribute_values) > 0 and inventory_attributes[attribute_key] not in attribute_values):
762 elif match_type == 'not':
763 # drop the candidate when
764 # 1)field exists in AAI and 2)value is not null or empty 3)value is one of those in the 'not' list
765 # Remember, this means if the property is not returned at all from AAI, that still can be a candidate.
766 if attribute_key in inventory_attributes and \
767 inventory_attributes[attribute_key] and \
768 inventory_attributes[attribute_key] in attribute_values:
773 def first_level_service_call(self, path, name, service_type):
775 response = self._request(
776 path=path, context="demand, GENERIC-VNF role",
777 value="{}, {}".format(name, service_type))
778 if response is None or response.status_code != 200:
779 return list() # move ahead with next requirement
780 body = response.json()
781 return body.get("generic-vnf", [])
783 def resolve_v_server_for_candidate(self, candidate_id, location_id, vs_link, add_interfaces, demand_name,
784 triage_translator_data):
786 LOG.error(_LE("{} VSERVER link information not "
787 "available from A&AI").format(demand_name))
788 self.triage_translator.collectDroppedCandiate(candidate_id,
789 location_id, demand_name,
790 triage_translator_data,
791 reason="VSERVER link information not")
792 return None # move ahead with the next vnf
795 vs_link = vs_link + '?depth=2'
796 vs_path = self._get_aai_path_from_link(vs_link)
798 LOG.error(_LE("{} VSERVER path information not "
799 "available from A&AI - {}").
800 format(demand_name, vs_path))
801 self.triage_translator.collectDroppedCandiate(candidate_id,
802 location_id, demand_name,
803 triage_translator_data,
804 reason="VSERVER path information not available from A&AI")
805 return None # move ahead with the next vnf
806 path = self._aai_versioned_path(vs_path)
807 response = self._request(
808 path=path, context="demand, VSERVER",
809 value="{}, {}".format(demand_name, vs_path))
810 if response is None or response.status_code != 200:
811 self.triage_translator.collectDroppedCandiate(candidate_id,
812 location_id, demand_name,
813 triage_translator_data,
814 reason=response.status_code)
816 return response.json()
818 def resolve_vf_modules_for_generic_vnf(self, candidate_id, location_id, vnf, demand_name, triage_translator_data):
819 raw_path = '/network/generic-vnfs/generic-vnf/{}?depth=1'.format(vnf.get("vnf-id"))
820 path = self._aai_versioned_path(raw_path)
822 response = self._request('get', path=path, data=None)
823 if response is None or response.status_code != 200:
824 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
825 triage_translator_data, reason=response)
827 generic_vnf_details = response.json()
829 if generic_vnf_details is None or not generic_vnf_details.get('vf-modules') \
830 or not generic_vnf_details.get('vf-modules').get('vf-module'):
831 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
832 triage_translator_data,
833 reason="Generic-VNF No detailed data for VF-modules")
836 return generic_vnf_details.get('vf-modules').get('vf-module')
838 def resolve_cloud_regions_by_cloud_region_id(self, cloud_region_id):
839 cloud_region_uri = '/cloud-infrastructure/cloud-regions' \
840 '/?cloud-region-id=' \
842 path = self._aai_versioned_path(cloud_region_uri)
844 response = self._request('get',
847 if response is None or response.status_code != 200:
850 body = response.json()
851 return body.get('cloud-region', [])
853 def assign_candidate_existing_placement(self, candidate, existing_placement):
855 """Assign existing_placement and cost parameters to candidate
857 Used by resolve_demands
859 candidate['existing_placement'] = 'false'
860 if existing_placement:
861 if existing_placement.get('candidate_id') == candidate['candidate_id']:
862 candidate['cost'] = self.conf.data.existing_placement_cost
863 candidate['existing_placement'] = 'true'
865 def resovle_conflict_id(self, conflict_identifier, candidate):
867 # Initialize the conflict_id_list
868 conflict_id_list = list()
869 # conflict_id is separated by pipe (|)
872 for conflict_element in conflict_identifier:
873 # if the conflict_element is a dictionary with key = 'get_candidate_attribute',
874 # then add candidate's coressponding value to conflict_id string
875 if isinstance(conflict_element, dict) and 'get_candidate_attribute' in conflict_element:
876 attribute_name = conflict_element.get('get_candidate_attribute')
877 conflict_id_list.append(candidate[attribute_name] + separator)
878 elif isinstance(conflict_element, unicode):
879 conflict_id_list.append(conflict_element + separator)
881 return ''.join(conflict_id_list)
883 def resolve_v_server_links_for_vnf(self, vnf):
884 related_to = "vserver"
885 search_key = "cloud-region.cloud-owner"
886 rl_data_list = self._get_aai_rel_link_data(
887 data=vnf, related_to=related_to,
888 search_key=search_key)
889 vs_link_list = list()
890 for i in range(0, len(rl_data_list)):
891 vs_link_list.append(rl_data_list[i].get('link'))
894 def resolve_complex_info_link_for_v_server(self, candidate_id, v_server, cloud_owner, cloud_region_id,
895 service_type, demand_name, triage_translator_data):
896 related_to = "pserver"
897 rl_data_list = self._get_aai_rel_link_data(
899 related_to=related_to,
902 if len(rl_data_list) > 1:
903 self._log_multiple_item_error(
904 demand_name, service_type, related_to, "item",
906 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
907 triage_translator_data, reason="item VSERVER")
909 rl_data = rl_data_list[0]
910 ps_link = rl_data.get('link')
912 # Third level query to get cloud region from pserver
914 LOG.error(_LE("{} pserver related link "
915 "not found in A&AI: {}").
916 format(demand_name, rl_data))
917 # if HPA_feature is disabled
918 if not self.conf.HPA_enabled:
919 # Triage Tool Feature Changes
920 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
921 triage_translator_data, reason="ps link not found")
924 if not (cloud_owner and cloud_region_id):
925 LOG.error("{} cloud-owner or cloud-region not "
926 "available from A&AI".
928 # Triage Tool Feature Changes
929 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
930 triage_translator_data,
931 reason="Cloud owner and cloud region "
933 return None # move ahead with the next vnf
935 '/cloud-infrastructure/cloud-regions/cloud-region' \
936 '/?cloud-owner=' + cloud_owner \
937 + '&cloud-region-id=' + cloud_region_id
938 path = self._aai_versioned_path(cloud_region_uri)
939 response = self._request('get',
942 if response is None or response.status_code != 200:
943 # Triage Tool Feature Changes
944 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
945 triage_translator_data, reason=response)
947 body = response.json()
949 ps_path = self._get_aai_path_from_link(ps_link)
951 LOG.error(_LE("{} pserver path information "
952 "not found in A&AI: {}").
953 format(demand_name, ps_link))
954 # Triage Tool Feature Changes
955 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
956 triage_translator_data, reason="ps path not found")
957 return None # move ahead with the next vnf
958 path = self._aai_versioned_path(ps_path)
959 response = self._request(
960 path=path, context="PSERVER", value=ps_path)
961 if response is None or response.status_code != 200:
962 # Triage Tool Feature Changes
963 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
964 triage_translator_data, reason=response)
966 body = response.json()
968 related_to = "complex"
969 search_key = "complex.physical-location-id"
970 rl_data_list = self._get_aai_rel_link_data(
972 related_to=related_to,
973 search_key=search_key
975 if len(rl_data_list) > 1:
976 if not self.match_vserver_attribute(rl_data_list):
977 self._log_multiple_item_error(
978 demand_name, service_type, related_to, search_key, "PSERVER", body)
979 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
980 triage_translator_data, reason="PSERVER error")
982 return rl_data_list[0]
984 def resolve_cloud_for_vnf(self, candidate_id, location_id, vnf, service_type, demand_name, triage_translator_data):
985 related_to = "vserver"
986 search_keys = ["cloud-region.cloud-owner", "cloud-region.cloud-region-id"]
988 for search_key in search_keys:
989 rl_data_list = self._get_aai_rel_link_data(
990 data=vnf, related_to=related_to,
991 search_key=search_key)
993 if len(rl_data_list) > 1:
994 if not self.match_vserver_attribute(rl_data_list):
995 self._log_multiple_item_error(
996 demand_name, service_type, related_to, search_key,
998 self.triage_translator.collectDroppedCandiate(candidate_id,
999 location_id, demand_name,
1000 triage_translator_data,
1003 cloud_info[search_key.split(".")[1].replace('-', '_')] = rl_data_list[0].get('d_value') if rl_data_list[
1005 cloud_info['cloud_region_version'] = self.get_cloud_region_version(cloud_info['cloud_region_id'])
1006 cloud_info['location_type'] = 'att_aic'
1007 cloud_info['location_id'] = cloud_info.pop('cloud_region_id')
1010 def get_cloud_region_version(self, cloud_region_id):
1012 regions = self.resolve_cloud_regions_by_cloud_region_id(cloud_region_id)
1015 for region in regions:
1016 if "cloud-region-version" in region:
1017 return self._get_version_from_string(region["cloud-region-version"])
1019 def resolve_global_customer_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1020 demand_name, triage_translator_data):
1021 related_to = "service-instance"
1022 search_key = "customer.global-customer-id"
1023 match_key = "customer.global-customer-id"
1024 rl_data_list = self._get_aai_rel_link_data(
1026 related_to=related_to,
1027 search_key=search_key,
1028 match_dict={'key': match_key,
1029 'value': customer_id}
1031 if len(rl_data_list) > 1:
1032 if not self.match_vserver_attribute(rl_data_list):
1033 self._log_multiple_item_error(
1034 demand_name, service_type, related_to, search_key, "VNF", vnf)
1035 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1036 demand_name, triage_translator_data,
1037 reason=" match_vserver_attribute generic-vnf")
1039 return rl_data_list[0]
1041 def resolve_service_instance_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1042 demand_name, triage_translator_data):
1043 related_to = "service-instance"
1044 search_key = "service-instance.service-instance-id"
1045 match_key = "customer.global-customer-id"
1046 rl_data_list = self._get_aai_rel_link_data(
1048 related_to=related_to,
1049 search_key=search_key,
1050 match_dict={'key': match_key,
1051 'value': customer_id}
1053 if len(rl_data_list) > 1:
1054 if not self.match_vserver_attribute(rl_data_list):
1055 self._log_multiple_item_error(
1056 demand_name, service_type, related_to, search_key, "VNF", vnf)
1057 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1058 demand_name, triage_translator_data,
1059 reason="multiple_item_error generic-vnf")
1061 return rl_data_list[0]
1063 def build_complex_info_for_candidate(self, candidate_id, location_id, vnf, complex_list, service_type, demand_name,
1064 triage_translator_data):
1065 if not complex_list or \
1066 len(complex_list) < 1:
1067 LOG.error("Complex information not "
1068 "available from A&AI")
1069 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1070 triage_translator_data,
1071 reason="Complex information not available from A&AI")
1074 # In the scenario where no pserver information is available
1075 # assumption here is that cloud-region does not span across
1076 # multiple complexes
1077 if len(complex_list) > 1:
1078 related_to = "complex"
1079 search_key = "complex.physical-location-id"
1080 if not self.match_vserver_attribute(complex_list):
1081 self._log_multiple_item_error(
1082 demand_name, service_type, related_to, search_key,
1084 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1085 triage_translator_data,
1086 reason="Generic-vnf error")
1089 rl_data = complex_list[0]
1090 complex_link = rl_data.get('link')
1091 complex_id = rl_data.get('d_value')
1093 # Final query for the complex information
1094 if not (complex_link and complex_id):
1095 LOG.debug("{} complex information not "
1096 "available from A&AI - {}".
1097 format(demand_name, complex_link))
1098 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1099 triage_translator_data,
1100 reason="Complex information not available from A&AI")
1101 return # move ahead with the next vnf
1103 complex_info = self._get_complex(
1104 complex_link=complex_link,
1105 complex_id=complex_id
1107 if not complex_info:
1108 LOG.debug("{} complex information not "
1109 "available from A&AI - {}".
1110 format(demand_name, complex_link))
1111 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1112 triage_translator_data,
1113 reason="Complex information not available from A&AI")
1114 return # move ahead with the next vnf
1116 complex_info = self.build_complex_dict(complex_info, '')
1119 def resolve_demands(self, demands, plan_info, triage_translator_data):
1120 """Resolve demands into inventory candidate lists"""
1122 self.triage_translator.getPlanIdNAme(plan_info['plan_name'], plan_info['plan_id'], triage_translator_data)
1124 resolved_demands = {}
1125 for name, requirements in demands.items():
1126 self.triage_translator.addDemandsTriageTranslator(name, triage_translator_data)
1127 resolved_demands[name] = []
1128 for requirement in requirements:
1129 inventory_type = requirement.get('inventory_type').lower()
1130 service_subscription = requirement.get('service_subscription')
1131 candidate_uniqueness = requirement.get('unique', 'true')
1132 filtering_attributes = requirement.get('filtering_attributes')
1133 passthrough_attributes = requirement.get('passthrough_attributes')
1134 default_attributes = requirement.get('default_attributes')
1135 # TODO(XYZ): may need to support multiple service_type and customer_id in the futrue
1137 # TODO(XYZ): make it consistent for dash and underscore
1138 if filtering_attributes:
1139 # catch equipment-role and service-type from template
1140 equipment_role = filtering_attributes.get('equipment-role')
1141 service_type = filtering_attributes.get('service-type')
1143 service_type = equipment_role
1144 # catch global-customer-id and customer-id from template
1145 global_customer_id = filtering_attributes.get('global-customer-id')
1146 customer_id = filtering_attributes.get('customer-id')
1147 if global_customer_id:
1148 customer_id = global_customer_id
1150 model_invariant_id = filtering_attributes.get('model-invariant-id')
1151 model_version_id = filtering_attributes.get('model-version-id')
1152 service_role = filtering_attributes.get('service-role')
1155 service_type = equipment_role = requirement.get('service_type')
1156 customer_id = global_customer_id = requirement.get('customer_id')
1157 # region_id is OPTIONAL. This will restrict the initial
1158 # candidate set to come from the given region id
1159 restricted_region_id = requirement.get('region')
1160 restricted_complex_id = requirement.get('complex')
1161 # Used for order locking feature
1162 # by defaut, conflict id is the combination of candidate id, service type and vnf-e2e-key
1163 conflict_identifier = requirement.get('conflict_identifier')
1165 vlan_key = requirement.get('vlan_key')
1166 port_key = requirement.get('port_key')
1168 # get required candidates from the demand
1169 required_candidates = requirement.get("required_candidates")
1171 # get existing_placement from the demand
1172 existing_placement = requirement.get("existing_placement")
1174 if required_candidates:
1175 resolved_demands['required_candidates'] = \
1178 # get excluded candidate from the demand
1179 excluded_candidates = requirement.get("excluded_candidates")
1181 # service_resource_id is OPTIONAL and is
1182 # transparent to Conductor
1183 service_resource_id = requirement.get('service_resource_id') \
1184 if requirement.get('service_resource_id') else ''
1186 if inventory_type == 'cloud':
1187 # load region candidates from cache
1188 regions = self._get_regions()
1189 if not regions or len(regions) < 1:
1190 LOG.debug("Region information is not available in cache")
1191 for region_id, region in regions.items():
1192 # Pick only candidates from the restricted_region
1193 info = Candidate.build_candidate_info('aai', inventory_type,
1194 self.conf.data.cloud_candidate_cost,
1195 candidate_uniqueness, region_id, service_resource_id)
1196 cloud = self.resolve_cloud_for_region(region, region_id)
1197 complex_info = self.build_complex_dict(region['complex'], inventory_type)
1198 flavors = self.resolve_flavors_for_region(region['flavors'])
1200 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1201 if self.check_sriov_automation(cloud['cloud_region_version'], name, info['candidate_id']):
1202 other['sriov_automation'] = 'true'
1204 other['sriov_automation'] = 'false'
1205 cloud_candidate = Cloud(info=info, cloud_region=cloud, complex=complex_info, flavors=flavors,
1206 additional_fields=other)
1207 candidate = cloud_candidate.convert_nested_dict_to_dict()
1209 cloud_region_attr = dict()
1210 cloud_region_attr['cloud-owner'] = region['cloud_owner']
1211 cloud_region_attr['cloud-region-version'] = region['cloud_region_version']
1212 cloud_region_attr['cloud-type'] = region['cloud_type']
1213 cloud_region_attr['cloud-zone'] = region['cloud_zone']
1214 cloud_region_attr['complex-name'] = region['complex_name']
1215 cloud_region_attr['physical-location-id'] = region['physical_location_id']
1217 if filtering_attributes and (not self.match_inventory_attributes(filtering_attributes,
1219 candidate['candidate_id'])):
1220 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1221 candidate['location_id'], name,
1222 triage_translator_data,
1223 reason='attributes and match invetory '
1227 if conflict_identifier:
1228 candidate['conflict_id'] = self.resovle_conflict_id(conflict_identifier, candidate)
1230 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1231 triage_translator_data):
1234 self.assign_candidate_existing_placement(candidate, existing_placement)
1236 # Pick only candidates not in the excluded list, if excluded candidate list is provided
1237 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1238 name, triage_translator_data):
1241 # Pick only candidates in the required list, if required candidate list is provided
1242 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1244 triage_translator_data):
1247 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1248 # add candidate to demand candidates
1249 resolved_demands[name].append(candidate)
1250 LOG.debug(">>>>>>> Candidate <<<<<<<")
1251 LOG.debug(json.dumps(candidate, indent=4))
1253 elif (inventory_type == 'service') and customer_id:
1254 # First level query to get the list of generic vnfs
1255 vnf_by_model_invariant = list()
1256 if filtering_attributes and model_invariant_id:
1258 raw_path = '/network/generic-vnfs/' \
1259 '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1260 if model_version_id:
1261 raw_path = '/network/generic-vnfs/' \
1262 '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1264 path = self._aai_versioned_path(raw_path)
1265 vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1267 vnf_by_service_type = list()
1268 if service_type or equipment_role:
1269 path = self._aai_versioned_path(
1270 '/network/generic-vnfs/'
1271 '?equipment-role={}&depth=0'.format(service_type))
1272 vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1274 generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1277 for vnf in generic_vnf:
1278 # if this vnf already appears, skip it
1279 vnf_id = vnf.get('vnf-id')
1280 if vnf_id in vnf_dict:
1282 # add vnf (with vnf_id as key) to the dictionary
1283 vnf_dict[vnf_id] = vnf
1285 vnf_info['host_id'] = vnf.get("vnf-name")
1286 vlan_info = self.build_vlan_info(vlan_key, port_key)
1287 cloud = self.resolve_cloud_for_vnf('', '', vnf, service_type, name, triage_translator_data)
1288 if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1289 cloud['cloud_region_version'] is None:
1292 rl_data = self.resolve_global_customer_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1293 service_type, name, triage_translator_data)
1297 vs_cust_id = rl_data.get('d_value')
1298 rl_data = self.resolve_service_instance_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1299 service_type, name, triage_translator_data)
1303 vs_service_instance_id = rl_data.get('d_value')
1306 if vs_cust_id and vs_cust_id == customer_id:
1307 info = Candidate.build_candidate_info('aai', inventory_type,
1308 self.conf.data.service_candidate_cost,
1309 candidate_uniqueness, vs_service_instance_id,
1310 service_resource_id)
1311 else: # vserver is for a different customer
1312 self.triage_translator.collectDroppedCandiate('', cloud['location_id'], name,
1313 triage_translator_data,
1314 reason="vserver is for a different customer")
1316 # Added vim-id for short-term workaround
1318 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1319 other['sriov_automation'] = 'true' if self.check_sriov_automation(
1320 cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1322 # Second level query to get the pserver from vserver
1323 complex_list = list()
1324 for complex_link in self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'], cloud,
1326 triage_translator_data,
1328 complex_list.append(complex_link[1])
1329 complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1330 cloud['location_id'], vnf,
1331 complex_list, service_type, name,
1332 triage_translator_data)
1333 if "complex_name" not in complex_info:
1336 service_candidate = Service(info=info, cloud_region=cloud, complex=complex_info,
1337 generic_vnf=vnf_info, additional_fields=other, vlan=vlan_info)
1338 candidate = service_candidate.convert_nested_dict_to_dict()
1340 # add specifal parameters for comparsion
1341 vnf['global-customer-id'] = customer_id
1342 vnf['customer-id'] = customer_id
1343 vnf['cloud-region-id'] = cloud.get('cloud_region_id')
1344 vnf['physical-location-id'] = complex_info.get('physical_location_id')
1346 if filtering_attributes and not self.match_inventory_attributes(filtering_attributes, vnf,
1347 candidate['candidate_id']):
1348 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1349 candidate['location_id'], name,
1350 triage_translator_data,
1351 reason="attibute check error")
1353 self.assign_candidate_existing_placement(candidate, existing_placement)
1355 # Pick only candidates not in the excluded list
1356 # if excluded candidate list is provided
1357 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1358 name, triage_translator_data):
1361 # Pick only candidates in the required list
1362 # if required candidate list is provided
1363 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1365 triage_translator_data):
1368 # add the candidate to the demand
1369 # Pick only candidates from the restricted_region
1370 # or restricted_complex
1371 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1372 triage_translator_data):
1375 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1376 resolved_demands[name].append(candidate)
1377 LOG.debug(">>>>>>> Candidate <<<<<<<")
1378 LOG.debug(json.dumps(candidate, indent=4))
1380 elif (inventory_type == 'vfmodule') and customer_id:
1382 # First level query to get the list of generic vnfs
1383 vnf_by_model_invariant = list()
1384 if filtering_attributes and model_invariant_id:
1386 raw_path = '/network/generic-vnfs/' \
1387 '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1388 if model_version_id:
1389 raw_path = '/network/generic-vnfs/' \
1390 '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1392 path = self._aai_versioned_path(raw_path)
1393 vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1395 vnf_by_service_type = list()
1396 if service_type or equipment_role:
1397 path = self._aai_versioned_path('/network/generic-vnfs/'
1398 '?equipment-role={}&depth=0'.format(service_type))
1399 vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1401 generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1404 for vnf in generic_vnf:
1405 # if this vnf already appears, skip it
1406 vnf_id = vnf.get('vnf-id')
1407 if vnf_id in vnf_dict:
1409 # add vnf (with vnf_id as key) to the dictionary
1410 vnf_dict[vnf_id] = vnf
1413 info = Candidate.build_candidate_info('aai', inventory_type,
1414 self.conf.data.service_candidate_cost,
1415 candidate_uniqueness, "", service_resource_id)
1417 vlan_info = self.build_vlan_info(vlan_key, port_key)
1419 vnf_info = self.get_vnf_info(vnf)
1421 rl_data = self.resolve_global_customer_id_for_vnf('', '', vnf, customer_id,
1422 service_type, name, triage_translator_data)
1426 vs_cust_id = rl_data.get('d_value')
1428 rl_data = self.resolve_service_instance_id_for_vnf('', '', vnf, customer_id,
1429 service_type, name, triage_translator_data)
1433 vs_service_instance_id = rl_data.get('d_value')
1435 service_info = dict()
1436 if vs_cust_id and vs_cust_id == customer_id:
1437 service_info['service_instance_id'] = vs_service_instance_id
1438 else: # vserver is for a different customer
1439 self.triage_translator.collectDroppedCandiate('', '', name,
1440 triage_translator_data,
1441 reason="candidate is for a different"
1445 vf_modules_list = self.resolve_vf_modules_for_generic_vnf('', '', vnf, name,
1446 triage_translator_data)
1447 if vf_modules_list is None:
1450 for vf_module in vf_modules_list:
1451 # for vfmodule demands we allow to have vfmodules from different cloud regions
1452 info['candidate_id'] = vf_module.get("vf-module-id")
1453 vf_module_info = self.get_vf_module(vf_module)
1454 cloud = self.resolve_cloud_for_vnf(info['candidate_id'], '', vf_module, service_type, name,
1455 triage_translator_data)
1456 if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1457 cloud['cloud_region_version'] is None:
1460 # OTHER - Added vim-id for short-term workaround
1462 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1463 other['sriov_automation'] = 'true' if self.check_sriov_automation(
1464 cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1466 # Second level query to get the pserver from vserver
1467 vserver_info = dict()
1468 vserver_info['vservers'] = list()
1469 complex_list = list()
1470 for v_server, complex_link in \
1471 self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'],
1473 triage_translator_data,
1475 complex_list.append(complex_link)
1476 candidate_vserver = dict()
1477 candidate_vserver['vserver-id'] = v_server.get('vserver-id')
1478 candidate_vserver['vserver-name'] = v_server.get('vserver-name')
1479 l_interfaces = self.get_l_interfaces_from_vserver(info['candidate_id'],
1480 cloud['location_id'],
1482 triage_translator_data)
1484 candidate_vserver['l-interfaces'] = l_interfaces
1487 vserver_info['vservers'].append(candidate_vserver)
1490 complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1491 cloud['location_id'], vnf,
1492 complex_list, service_type, name,
1493 triage_translator_data)
1494 if complex_info.get("complex_name") is None:
1497 vf_module_candidate = VfModule(complex=complex_info, info=info, generic_vnf=vnf_info,
1498 cloud_region=cloud, service_instance=service_info,
1499 vf_module=vf_module_info, vserver=vserver_info,
1500 additional_fields=other, vlan=vlan_info)
1501 candidate = vf_module_candidate.convert_nested_dict_to_dict()
1503 # add vf-module parameters for filtering
1504 vnf_vf_module_inventory = copy.deepcopy(vnf)
1505 vnf_vf_module_inventory.update(vf_module)
1506 # add specifal parameters for comparsion
1507 vnf_vf_module_inventory['global-customer-id'] = customer_id
1508 vnf_vf_module_inventory['customer-id'] = customer_id
1509 vnf_vf_module_inventory['cloud-region-id'] = cloud.get('location_id')
1510 vnf_vf_module_inventory['physical-location-id'] = complex_info.get('physical_location_id')
1511 vnf_vf_module_inventory['service_instance_id'] = vs_service_instance_id
1513 if filtering_attributes and not self.match_inventory_attributes(filtering_attributes,
1514 vnf_vf_module_inventory,
1515 candidate['candidate_id']):
1516 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1517 candidate['location_id'], name,
1518 triage_translator_data,
1519 reason="attibute check error")
1521 self.assign_candidate_existing_placement(candidate, existing_placement)
1523 # Pick only candidates not in the excluded list
1524 # if excluded candidate list is provided
1525 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates,
1527 name, triage_translator_data):
1530 # Pick only candidates in the required list
1531 # if required candidate list is provided
1532 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1534 triage_translator_data):
1537 # add the candidate to the demand
1538 # Pick only candidates from the restricted_region
1539 # or restricted_complex
1540 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1541 triage_translator_data):
1544 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1545 resolved_demands[name].append(candidate)
1546 LOG.debug(">>>>>>> Candidate <<<<<<<")
1547 with open("vf.log", mode='w') as log_file:
1548 log_file.write(">>>>>>>Vf Candidate <<<<<<<")
1549 log_file.write(json.dumps(candidate, indent=4))
1550 LOG.debug(json.dumps(candidate, indent=4))
1552 elif inventory_type == 'transport' \
1553 and customer_id and service_type and \
1554 service_subscription and service_role:
1556 path = self._aai_versioned_path('business/customers/customer/{}/service-subscriptions/'
1557 'service-subscription/{}/service-instances'
1558 '?service-type={}&service-role={}'.format(customer_id,
1559 service_subscription,
1562 response = self._request('get', path=path, data=None)
1563 if response is None or response.status_code != 200:
1564 self.triage_translator.collectDroppedCandiate("", "", name,
1565 triage_translator_data,
1566 reason=response.status_code)
1568 body = response.json()
1569 transport_vnfs = body.get('service-instance', [])
1571 for vnf in transport_vnfs:
1572 # create a default candidate
1574 other['location_id'] = ''
1575 other['location_type'] = 'att_aic'
1577 vnf_service_instance_id = vnf.get('service-instance-id')
1578 if vnf_service_instance_id:
1579 info = Candidate.build_candidate_info('aai', inventory_type,
1580 self.conf.data.transport_candidate_cost,
1581 candidate_uniqueness, vnf_service_instance_id,
1582 service_resource_id)
1584 self.triage_translator.collectDroppedCandiate('', other['location_id'], name,
1585 triage_translator_data,
1586 reason="service-instance-id error ")
1591 zone = self.resolve_zone_for_vnf(info['candidate_id'], other['location_id'], vnf, name,
1592 triage_translator_data)
1594 zone_info['zone_id'] = zone.get('zone-id')
1595 zone_info['zone_name'] = zone.get('zone-name')
1600 related_to = "complex"
1601 search_key = "complex.physical-location-id"
1602 rel_link_data_list = self._get_aai_rel_link_data(
1604 related_to=related_to,
1605 search_key=search_key
1608 if len(rel_link_data_list) > 1:
1609 self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1610 name, triage_translator_data,
1611 reason="rel_link_data_list error")
1614 rel_link_data = rel_link_data_list[0]
1615 complex_id = rel_link_data.get("d_value")
1616 complex_link = rel_link_data.get('link')
1618 if not (complex_link and complex_id):
1619 LOG.debug("{} complex information not "
1620 "available from A&AI - {}".
1621 format(name, complex_link))
1622 self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1623 name, triage_translator_data,
1624 reason="complex information not available "
1628 complex_info = self._get_complex(
1629 complex_link=complex_link,
1630 complex_id=complex_id
1632 if not complex_info:
1633 LOG.debug("{} complex information not "
1634 "available from A&AI - {}".
1635 format(name, complex_link))
1636 self.triage_translator.collectDroppedCandiate(info['candidate_id'],
1637 other['location_id'], name,
1638 triage_translator_data,
1639 reason="complex information not "
1640 "available from A&AI")
1641 continue # move ahead with the next vnf
1643 complex_info = self.build_complex_dict(complex_info, inventory_type)
1644 transport_candidate = Transport(info=info, zone=zone_info, complex=complex_info,
1645 additional_fiels=other)
1646 candidate = transport_candidate.convert_nested_dict_to_dict()
1648 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1649 # add candidate to demand candidates
1650 resolved_demands[name].append(candidate)
1652 elif inventory_type == 'nssi':
1653 if filtering_attributes and model_invariant_id:
1654 second_level_match = aai_utils.get_first_level_and_second_level_filter(filtering_attributes,
1656 aai_response = self.get_nxi_candidates(filtering_attributes)
1657 resolved_demands[name].extend(self.filter_nxi_candidates(aai_response, second_level_match,
1659 candidate_uniqueness, inventory_type))
1662 LOG.error("Unknown inventory_type "
1663 " {}".format(inventory_type))
1664 return resolved_demands
1667 def build_complex_dict(aai_complex, inv_type):
1668 complex_info = dict()
1669 valid_keys = ['physical-location-id', 'complex-name', 'latitude', 'longitude', 'state', 'country', 'city',
1671 # for cloud type, complex_id instead of physical-location-id - note
1672 if inv_type == "cloud":
1673 for valid_key in valid_keys:
1674 if '-' in valid_key:
1675 complex_info[valid_key.replace('-', '_')] = aai_complex.get('complex_id') \
1676 if valid_key == 'physical-location-id' else \
1677 aai_complex.get(valid_key.replace('-', '_'))
1679 complex_info[valid_key] = aai_complex.get(valid_key)
1681 for valid_key in valid_keys:
1682 if '-' in valid_key:
1683 complex_info[valid_key.replace('-', '_')] = aai_complex.get(valid_key)
1685 complex_info[valid_key] = aai_complex.get(valid_key)
1689 def build_vlan_info(vlan_key, port_key):
1691 vlan_info['vlan_key'] = vlan_key
1692 vlan_info['port_key'] = port_key
1695 def resolve_flavors_for_region(self, flavors_obj):
1696 if self.conf.HPA_enabled:
1698 flavors['flavors'] = flavors_obj
1701 def resolve_v_server_and_complex_link_for_vnf(self, candidate_id, cloud, vnf, name, triage_translator_data,
1703 vs_link_list = self.resolve_v_server_links_for_vnf(vnf)
1704 for vs_link in vs_link_list:
1705 body = self.resolve_v_server_for_candidate(candidate_id, cloud['location_id'],
1706 vs_link, True, name, triage_translator_data)
1709 rl_data = self.resolve_complex_info_link_for_v_server(candidate_id, body,
1710 cloud['cloud_owner'], cloud['location_id'],
1711 service_type, name, triage_translator_data)
1716 def get_l_interfaces_from_vserver(self, candidate_id, location_id, v_server, name, triage_translator_data):
1717 if not v_server.get('l-interfaces') or not v_server.get('l-interfaces').get('l-interface'):
1718 self.triage_translator.collectDroppedCandiate(candidate_id,
1720 triage_translator_data,
1721 reason="VF-server interfaces error")
1724 l_interfaces = v_server.get('l-interfaces').get('l-interface')
1725 l_interfaces_list = list()
1727 for l_interface in l_interfaces:
1728 vserver_interface = dict()
1729 vserver_interface['interface-id'] = l_interface.get('interface-id')
1730 vserver_interface['interface-name'] = l_interface.get('interface-name')
1731 vserver_interface['macaddr'] = l_interface.get('macaddr')
1732 vserver_interface['network-id'] = l_interface.get('network-name')
1733 vserver_interface['network-name'] = ''
1734 vserver_interface['ipv4-addresses'] = list()
1735 vserver_interface['ipv6-addresses'] = list()
1737 if l_interface.get('l3-interface-ipv4-address-list'):
1738 for ip_address_info in l_interface.get('l3-interface-ipv4-address-list'):
1739 vserver_interface['ipv4-addresses']. \
1740 append(ip_address_info.get('l3-interface-ipv4-address'))
1742 if l_interface.get('l3-interface-ipv6-address-list'):
1743 for ip_address_info in l_interface.get('l3-interface-ipv6-address-list'):
1744 vserver_interface['ipv6-addresses']. \
1745 append(ip_address_info.get('l3-interface-ipv6-address'))
1747 l_interfaces_list.append(vserver_interface)
1748 return l_interfaces_list
1751 def get_vnf_info(vnf):
1752 # some validation should happen
1754 vnf_info['host_id'] = vnf.get("vnf-name")
1755 vnf_info['nf-name'] = vnf.get("vnf-name")
1756 vnf_info['nf-id'] = vnf.get("vnf-id")
1757 vnf_info['nf-type'] = 'vnf'
1758 vnf_info['vnf-type'] = vnf.get("vnf-type")
1759 vnf_info['ipv4-oam-address'] = vnf.get("ipv4-oam-address") if vnf.get("ipv4-oam-address") else ""
1760 vnf_info['ipv6-oam-address'] = vnf.get("ipv6-oam-address") if vnf.get("ipv6-oam-address") else ""
1764 def resolve_cloud_for_region(region, region_id):
1766 valid_keys = ['cloud_owner', 'cloud_region_version', 'location_id']
1767 for valid_key in valid_keys:
1768 cloud[valid_key] = region.get(valid_key) if not valid_key == 'location_id' else region_id
1769 cloud['location_type'] = 'att_aic'
1773 def get_vf_module(vf_module):
1774 vf_module_info = dict()
1775 vf_module_info['vf-module-name'] = vf_module.get("vf-module-name")
1776 vf_module_info['vf-module-id'] = vf_module.get("vf-module-id")
1777 return vf_module_info
1779 def get_vim_id(self, cloud_owner, cloud_region_id):
1780 if self.conf.HPA_enabled:
1781 return cloud_owner + '_' + cloud_region_id
1784 def add_passthrough_attributes(candidate, passthrough_attributes, demand_name):
1785 if passthrough_attributes is None:
1787 if len(passthrough_attributes.items()) > 0:
1788 candidate['passthrough_attributes'] = dict()
1789 for key, value in passthrough_attributes.items():
1790 candidate['passthrough_attributes'][key] = value
1792 def resolve_zone_for_vnf(self, candidate_id, location_id, vnf, name, triage_translator_data):
1794 zone_link = self._get_aai_rel_link(
1795 data=vnf, related_to=related_to)
1797 LOG.error("Zone information not available from A&AI for transport candidates")
1798 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1799 name, triage_translator_data,
1800 reason="Zone information not available from A&AI for "
1801 "transport candidates")
1803 zone_aai_path = self._get_aai_path_from_link(zone_link)
1804 response = self._request('get', path=zone_aai_path, data=None)
1805 if response is None or response.status_code != 200:
1806 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, name,
1807 triage_translator_data,
1808 reason=response.status_code)
1810 body = response.json()
1813 def match_region(self, candidate, restricted_region_id, restricted_complex_id, demand_name,
1814 triage_translator_data):
1815 if self.match_candidate_attribute(
1818 restricted_region_id,
1820 candidate.get('inventory_type')) or \
1821 self.match_candidate_attribute(
1823 "physical_location_id",
1824 restricted_complex_id,
1826 candidate.get('inventory_type')):
1827 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1828 demand_name, triage_translator_data,
1829 reason="candidate region does not match")
1834 def match_candidate_by_list(self, candidate, candidates_list, exclude, demand_name, triage_translator_data):
1835 has_candidate = False
1837 for list_candidate in candidates_list:
1839 and list_candidate.get('inventory_type') \
1840 == candidate.get('inventory_type'):
1841 if isinstance(list_candidate.get('candidate_id'), list):
1842 for candidate_id in list_candidate.get('candidate_id'):
1843 if candidate_id == candidate.get('candidate_id'):
1844 has_candidate = True
1847 raise Exception("Invalid candidate id list format")
1852 if not has_candidate:
1853 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1854 demand_name, triage_translator_data,
1855 reason="has_required_candidate candidate")
1857 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1858 demand_name, triage_translator_data,
1859 reason="excluded candidate")
1860 return has_candidate
1862 def match_hpa(self, candidate, features):
1863 """Match HPA features requirement with the candidate flavors """
1864 hpa_provider = hpa_utils.HpaMatchProvider(candidate, features)
1865 if hpa_provider.init_verify():
1866 directives = hpa_provider.match_flavor()
1871 def get_nxi_candidates(self, filtering_attributes):
1872 raw_path = 'nodes/service-instances' + aai_utils.add_query_params_and_depth(filtering_attributes, "2")
1873 path = self._aai_versioned_path(raw_path)
1874 aai_response = self._request('get', path, data=None)
1876 if aai_response is None or aai_response.status_code != 200:
1878 if aai_response.json():
1879 return aai_response.json()
1881 def filter_nxi_candidates(self, response_body, filtering_attributes, default_attributes, candidate_uniqueness,
1884 if response_body is not None:
1885 nxi_instances = response_body.get("service-instance", [])
1887 for nxi_instance in nxi_instances:
1888 inventory_attributes = aai_utils.get_inv_values_for_second_level_filter(filtering_attributes,
1890 nxi_info = aai_utils.get_instance_info(nxi_instance)
1891 if not filtering_attributes or \
1892 self.match_inventory_attributes(filtering_attributes, inventory_attributes,
1893 nxi_instance.get('service-instance-id')):
1895 profiles = nxi_instance.get('slice-profiles').get('slice-profile')
1896 cost = self.conf.data.nssi_candidate_cost
1898 profiles = nxi_instance.get('service-profiles').get('service-profile')
1899 cost = self.conf.data.nsi_candidate_cost
1900 for profile in profiles:
1901 profile_id = profile.get('profile-id')
1902 info = Candidate.build_candidate_info('aai', type, cost, candidate_uniqueness, profile_id)
1903 profile_info = aai_utils.convert_hyphen_to_under_score(profile)
1904 nxi_candidate = NxI(instance_info=nxi_info, profile_info=profile_info, info=info,
1905 default_fields=aai_utils.convert_hyphen_to_under_score(default_attributes))
1906 candidate = nxi_candidate.convert_nested_dict_to_dict()
1907 candidates.append(candidate)