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.dcae import DCAE
42 from conductor.data.plugins.inventory_provider import hpa_utils
43 from conductor.data.plugins.inventory_provider.sdc import SDC
44 from conductor.data.plugins.inventory_provider.utils import aai_utils
45 from conductor.data.plugins.triage_translator.triage_translator import TraigeTranslator
46 from conductor.i18n import _LE
47 from conductor.i18n import _LI
49 LOG = log.getLogger(__name__)
54 cfg.IntOpt('cache_refresh_interval',
56 help='Interval with which to refresh the local cache, '
58 cfg.IntOpt('complex_cache_refresh_interval',
60 help='Interval with which to refresh the local complex cache, '
62 cfg.StrOpt('table_prefix',
64 help='Data Store table prefix.'),
65 cfg.StrOpt('server_url',
66 default='https://controller:8443/aai',
67 help='Base URL for A&AI, up to and not including '
68 'the version, and without a trailing slash.'),
69 cfg.StrOpt('aai_rest_timeout',
71 help='Timeout for A&AI Rest Call'),
72 cfg.StrOpt('aai_retries',
74 help='Number of retry for A&AI Rest Call'),
75 cfg.StrOpt('server_url_version',
77 help='The version of A&AI in v# format.'),
78 cfg.StrOpt('certificate_file',
79 default='certificate.pem',
80 help='SSL/TLS certificate file in pem format. '
81 'This certificate must be registered with the A&AI '
83 cfg.StrOpt('certificate_key_file',
84 default='certificate_key.pem',
85 help='Private Certificate Key file in pem format.'),
86 cfg.StrOpt('certificate_authority_bundle_file',
87 default='certificate_authority_bundle.pem',
88 help='Certificate Authority Bundle file in pem format. '
89 'Must contain the appropriate trust chain for the '
91 # TODO(larry): follow-up with ONAP people on this (AA&I basic auth username and password?)
92 cfg.StrOpt('username',
94 help='Username for AAI.'),
95 cfg.StrOpt('password',
97 help='Password for AAI.'),
100 CONF.register_opts(AAI_OPTS, group='aai')
103 class AAI(base.InventoryProviderBase):
104 """Active and Available Inventory Provider"""
109 # FIXME(jdandrea): Pass this in to init.
112 self.base = self.conf.aai.server_url.rstrip('/')
113 self.version = self.conf.aai.server_url_version.rstrip('/')
114 self.cert = self.conf.aai.certificate_file
115 self.key = self.conf.aai.certificate_key_file
116 self.verify = self.conf.aai.certificate_authority_bundle_file
117 self.cache_refresh_interval = self.conf.aai.cache_refresh_interval
118 self.last_refresh_time = None
119 self.complex_cache_refresh_interval = \
120 self.conf.aai.complex_cache_refresh_interval
121 self.complex_last_refresh_time = None
122 self.timeout = self.conf.aai.aai_rest_timeout
123 self.retries = self.conf.aai.aai_retries
124 self.username = self.conf.aai.username
125 self.password = self.conf.aai.password
126 self.triage_translator = TraigeTranslator()
128 # Cache is initially empty
130 self._aai_complex_cache = {}
132 def initialize(self):
134 """Perform any late initialization."""
135 # Initialize the Python requests
136 self._init_python_request()
138 # Refresh the cache once for now
139 self._refresh_cache()
141 # TODO(jdandrea): Make this periodic, and without a True condition!
142 # executor = futurist.ThreadPoolExecutor()
144 # fut = executor.submit(self.refresh_cache)
147 # # Now wait for the next time.
148 # # FIXME(jdandrea): Put inside refresh_cache()?
149 # refresh_interval = self.conf.aai.cache_refresh_interval
150 # time.sleep(refresh_interval)
151 # executor.shutdown()
154 """Return human-readable name."""
157 def invoke_method(self, arg):
158 if arg.pop('method_name') == "get_candidates_with_hpa":
159 return hpa_utils.get_candidates_with_hpa(arg)
162 def _get_version_from_string(string):
163 """Extract version number from string"""
164 return re.sub("[^0-9.]", "", string)
166 def _aai_versioned_path(self, path):
167 """Return a URL path with the A&AI version prepended"""
168 return '/{}/{}'.format(self.version, path.lstrip('/'))
170 def _request(self, method='get', path='/', data=None,
171 context=None, value=None):
172 """Performs HTTP request."""
174 'X-FromAppId': 'CONDUCTOR',
175 'X-TransactionId': str(uuid.uuid4()),
184 # TODO(jdandrea): Move timing/response logging into the rest helper?
185 start_time = time.time()
186 response = self.rest.request(**kwargs)
187 elapsed = time.time() - start_time
188 LOG.debug("Total time for A&AI request "
189 "({0:}: {1:}): {2:.3f} sec".format(context, value, elapsed))
192 LOG.error(_LE("No response from A&AI ({}: {})").
193 format(context, value))
194 elif response.status_code != 200:
195 LOG.error(_LE("A&AI request ({}: {}) returned HTTP "
196 "status {} {}, link: {}{}").
197 format(context, value,
198 response.status_code, response.reason,
202 def _init_python_request(self):
205 "server_url": self.base,
206 "retries": self.retries,
207 "username": self.username,
208 "password": self.password,
209 "cert_file": self.cert,
210 "cert_key_file": self.key,
211 "ca_bundle_file": self.verify,
212 "log_debug": self.conf.debug,
213 "read_timeout": self.timeout,
215 self.rest = rest.REST(**kwargs)
217 def _refresh_cache(self):
218 """Refresh the A&AI cache."""
219 if not self.last_refresh_time or \
220 (time.time() - self.last_refresh_time) > \
221 self.cache_refresh_interval * 60:
222 # TODO(jdandrea): This is presently brute force.
223 # It does not persist to Music. A general purpose ORM caching
224 # object likely needs to be made, with a key (hopefully we
225 # can use one that is not just a UUID), a value, and a
226 # timestamp. The other alternative is to not use the ORM
227 # layer and call the API directly, but that is
228 # also trading one set of todos for another ...
231 LOG.info(_LI("**** Refreshing A&AI cache *****"))
232 path = self._aai_versioned_path(
233 '/cloud-infrastructure/cloud-regions/?depth=0')
234 response = self._request(
235 path=path, context="cloud regions", value="all")
239 if response.status_code == 200:
240 body = response.json()
241 regions = body.get('cloud-region', {})
243 # Nothing to update the cache with
244 LOG.error(_LE("A&AI returned no regions, link: {}{}").
245 format(self.base, path))
251 for region in regions:
252 cloud_region_id = region.get('cloud-region-id')
254 LOG.debug("Working on region '{}' ".format(cloud_region_id))
256 cloud_region_version = region.get('cloud-region-version')
257 cloud_owner = region.get('cloud-owner')
258 cloud_type = region.get('cloud-type')
259 cloud_zone = region.get('cloud-zone')
261 physical_location_list = self._get_aai_rel_link_data(data=region, related_to='complex',
262 search_key='complex.physical-location-id')
263 if len(physical_location_list) > 0:
264 physical_location_id = physical_location_list[0].get('d_value')
266 if not (cloud_region_version and cloud_region_id):
268 rel_link_data_list = \
269 self._get_aai_rel_link_data(
271 related_to='complex',
272 search_key='complex.physical-location-id')
273 if len(rel_link_data_list) > 1:
274 LOG.error(_LE("Region {} has more than one complex").
275 format(cloud_region_id))
276 LOG.debug("Region {}: {}".format(cloud_region_id, region))
279 rel_link_data = rel_link_data_list[0]
280 complex_id = rel_link_data.get("d_value")
281 complex_link = rel_link_data.get("link")
282 if complex_id and complex_link:
283 complex_info = self._get_complex(
284 complex_link=complex_link,
285 complex_id=complex_id)
286 else: # no complex information
287 LOG.error(_LE("Region {} does not reference a complex").
288 format(cloud_region_id))
291 LOG.error(_LE("Region {}, complex {} info not found, "
292 "link {}").format(cloud_region_id,
293 complex_id, complex_link))
296 latitude = complex_info.get('latitude')
297 longitude = complex_info.get('longitude')
298 city = complex_info.get('city')
299 state = complex_info.get('state')
300 region = complex_info.get('region')
301 country = complex_info.get('country')
302 complex_name = complex_info.get('complex-name')
304 if not (latitude and longitude and city and country and complex_name):
305 keys = ('latitude', 'longitude', 'city', 'country',
308 list(set(keys).difference(
309 list(complex_info.keys()))) # Python 3 Conversion -- dict object to list object
310 LOG.error(_LE("Complex {} is missing {}, link: {}").
311 format(complex_id, missing_keys, complex_link))
312 LOG.debug("Complex {}: {}".
313 format(complex_id, complex_info))
316 cache['cloud_region'][cloud_region_id] = {
317 'cloud_region_version': cloud_region_version,
318 'cloud_owner': cloud_owner,
319 'cloud_type': cloud_type,
320 'cloud_zone': cloud_zone,
321 'complex_name': complex_name,
322 'physical_location_id': physical_location_id,
324 'complex_id': complex_id,
325 'complex_name': complex_name,
326 'latitude': latitude,
327 'longitude': longitude,
335 # Added for HPA support
336 if self.conf.HPA_enabled:
337 flavors = self._get_flavors(cloud_owner, cloud_region_id)
338 cache['cloud_region'][cloud_region_id]['flavors'] = flavors
340 LOG.debug("Candidate with cloud_region_id '{}' selected "
341 "as a potential candidate - ".format(cloud_region_id))
342 LOG.debug("Done with region '{}' ".format(cloud_region_id))
343 self._aai_cache = cache
344 self.last_refresh_time = time.time()
345 LOG.info(_LI("**** A&AI cache refresh complete *****"))
348 def _get_aai_rel_link(data, related_to):
349 """Given an A&AI data structure, return the related-to link"""
350 rel_dict = data.get('relationship-list')
352 for key, rel_list in rel_dict.items():
354 if related_to == rel.get('related-to'):
355 return rel.get('related-link')
358 def _get_aai_rel_link_data(data, related_to, search_key=None,
360 # some strings that we will encounter frequently
361 rel_lst = "relationship-list"
362 rkey = "relationship-key"
363 rval = "relationship-value"
364 rdata = "relationship-data"
367 m_key = match_dict.get('key')
368 m_value = match_dict.get('value')
372 rel_dict = data.get(rel_lst)
373 if rel_dict: # check if data has relationship lists
374 for key, rel_list in rel_dict.items():
376 if rel.get("related-to") == related_to:
379 link = rel.get("related-link")
380 r_data = rel.get(rdata, [])
383 if rd.get(rkey) == search_key:
385 if not match_dict: # return first match
387 {"link": link, "d_value": dval}
389 break # go to next relation
390 if rd.get(rkey) == m_key \
391 and rd.get(rval) == m_value:
393 if match_dict and matched: # if matching required
395 {"link": link, "d_value": dval}
397 # matched, return search value corresponding
398 # to the matched r_data group
399 else: # no search key; just return the link
401 {"link": link, "d_value": dval}
403 if len(response) == 0:
405 {"link": None, "d_value": None}
410 def check_sriov_automation(aic_version, demand_name, candidate_name):
411 """Check if specific candidate has SRIOV automation available or no"""
413 LOG.debug(_LI("Demand {}, candidate {} has an AIC version number {}").format(demand_name, candidate_name,
415 if aic_version == "X.Y":
419 def _get_complex(self, complex_link, complex_id=None):
421 if not self.complex_last_refresh_time or \
422 (time.time() - self.complex_last_refresh_time) > \
423 self.complex_cache_refresh_interval * 60:
424 self._aai_complex_cache.clear()
425 if complex_id and complex_id in self._aai_complex_cache:
426 return self._aai_complex_cache[complex_id]
428 path = self._aai_versioned_path(self._get_aai_path_from_link(complex_link))
429 response = self._request(path=path, context="complex", value=complex_id)
432 if response.status_code == 200:
433 complex_info = response.json()
434 if 'complex' in complex_info:
435 complex_info = complex_info.get('complex')
437 latitude = complex_info.get('latitude')
438 longitude = complex_info.get('longitude')
439 city = complex_info.get('city')
440 country = complex_info.get('country')
441 # removed the state check for countries in Europe that do not always enter states
442 if not (latitude and longitude and city and country):
443 keys = ('latitude', 'longitude', 'city', 'country')
445 list(set(keys).difference(set(complex_info.keys())))
446 LOG.error(_LE("Complex {} is missing {}, link: {}").
447 format(complex_id, missing_keys, complex_link))
448 LOG.debug("Complex {}: {}".format(complex_id, complex_info))
451 if complex_id: # cache only if complex_id is given
452 self._aai_complex_cache[complex_id] = response.json()
453 self.complex_last_refresh_time = time.time()
457 def _get_regions(self):
458 self._refresh_cache()
459 regions = self._aai_cache.get('cloud_region', {})
462 def _get_flavors(self, cloud_owner, cloud_region_id):
463 '''Fetch all flavors of a given cloud regions specified using {cloud-owner}/{cloud-region-id} composite key
465 :return flavors_info json object which list of flavor nodes and its children - HPACapabilities:
468 LOG.debug("Fetch all flavors and its child nodes HPACapabilities")
469 flavor_path = constants.FLAVORS_URI % (cloud_owner, cloud_region_id)
470 path = self._aai_versioned_path(flavor_path)
471 LOG.debug("Flavors path '{}' ".format(path))
473 response = self._request(path=path, context="flavors", value="all")
476 if response.status_code == 200:
477 flavors_info = response.json()
478 if not flavors_info or not flavors_info["flavor"] or \
479 len(flavors_info["flavor"]) == 0:
480 LOG.error(_LE("Flavor is missing in Cloud-Region {}/{}").
481 format(cloud_owner, cloud_region_id))
483 LOG.debug(flavors_info)
484 # Remove extraneous flavor information
487 LOG.error(_LE("Received Error while fetching flavors from Cloud-region {}/{}").format(cloud_owner,
491 def _get_aai_path_from_link(self, link):
492 path = link.split(self.version, 1)
493 if not path or len(path) <= 1:
494 # TODO(shankar): Treat this as a critical error?
495 LOG.error(_LE("A&AI version {} not found in link {}").
496 format(self.version, link))
498 return "{}".format(path[1])
500 def check_candidate_role(self, host_id=None):
502 vnf_name_uri = '/network/generic-vnfs/?vnf-name=' + host_id + '&depth=0'
503 path = self._aai_versioned_path(vnf_name_uri)
505 response = self._request('get', path=path, data=None,
508 if response is None or not response.ok:
510 body = response.json()
512 generic_vnf = body.get("generic-vnf", [])
514 for vnf in generic_vnf:
515 related_to = "service-instance"
516 search_key = "customer.global-customer-id"
517 rl_data_list = self._get_aai_rel_link_data(
519 related_to=related_to,
520 search_key=search_key,
523 if len(rl_data_list) != 1:
526 rl_data = rl_data_list[0]
527 candidate_role_link = rl_data.get("link")
529 if not candidate_role_link:
530 LOG.error(_LE("Unable to get candidate role link for host id {} ").format(host_id))
533 candidate_role_path = self._get_aai_path_from_link(candidate_role_link) + '/allotted-resources?depth=all'
534 path = self._aai_versioned_path(candidate_role_path)
536 response = self._request('get', path=path, data=None,
537 context="candidate role")
539 if response is None or not response.ok:
541 body = response.json()
543 response_items = body.get('allotted-resource')
544 if len(response_items) > 0:
545 role = response_items[0].get('role')
548 def check_network_roles(self, network_role_id=None):
549 # the network role query from A&AI is not using
550 # the version number in the query
552 '/network/l3-networks?network-role=' + network_role_id
553 path = self._aai_versioned_path(network_role_uri)
555 # This UUID is reserved by A&AI for a Conductor-specific named query.
556 named_query_uid = "role-UUID"
559 "query-parameters": {
561 "named-query-uuid": named_query_uid
564 "instance-filters": {
568 "network-role": network_role_id
575 response = self._request('get', path=path, data=data,
576 context="role", value=network_role_id)
579 body = response.json()
581 response_items = body.get('l3-network', [])
583 for item in response_items:
584 cloud_region_instances = self._get_aai_rel_link_data(
586 related_to='cloud-region',
587 search_key='cloud-region.cloud-region-id'
590 if len(cloud_region_instances) > 0:
591 for r_instance in cloud_region_instances:
592 region_id = r_instance.get('d_value')
593 if region_id is not None:
594 region_ids.add(region_id)
596 # return region ids that fit the role
599 def resolve_host_location(self, host_name):
600 path = self._aai_versioned_path('/query?format=id')
601 data = {"start": ["network/pnfs/pnf/" + host_name,
602 "cloud-infrastructure/pservers/pserver/" + host_name],
603 "query": "query/ucpe-instance"
605 response = self._request('put', path=path, data=data,
606 context="host name", value=host_name)
607 if response is None or response.status_code != 200:
609 body = response.json()
610 results = body.get('results', [])
612 for result in results:
613 if "resource-type" in result and \
614 "resource-link" in result and \
615 result["resource-type"] == "complex":
616 complex_link = result["resource-link"]
618 LOG.error(_LE("Unable to get a complex link for hostname {} "
619 " in response {}").format(host_name, response))
621 complex_info = self._get_complex(
622 complex_link=complex_link,
626 lat = complex_info.get('latitude')
627 lon = complex_info.get('longitude')
628 country = complex_info.get('country')
630 location = {"latitude": lat, "longitude": lon}
631 location["country"] = country if country else None
634 LOG.error(_LE("Unable to get a latitude and longitude "
635 "information for hostname {} from complex "
636 " link {}").format(host_name, complex_link))
639 LOG.error(_LE("Unable to get a complex information for "
640 " hostname {} from complex "
641 " link {}").format(host_name, complex_link))
644 def resolve_clli_location(self, clli_name):
645 clli_uri = '/cloud-infrastructure/complexes/complex/' + clli_name
646 path = self._aai_versioned_path(clli_uri)
648 response = self._request('get', path=path, data=None,
649 context="clli name", value=clli_name)
650 if response is None or response.status_code != 200:
653 body = response.json()
656 lat = body.get('latitude')
657 lon = body.get('longitude')
658 country = body.get('country')
660 location = {"latitude": lat, "longitude": lon}
661 location["country"] = country if country else None
664 LOG.error(_LE("Unable to get a latitude and longitude "
665 "information for CLLI code {} from complex").
669 LOG.error(_LE("Unable to get a complex information for "
670 " clli {} from complex "
671 " link {}").format(clli_name, clli_uri))
674 def get_inventory_group_pairs(self, service_description):
676 path = self._aai_versioned_path(
677 '/network/instance-groups/?description={}&depth=0'.format(
678 service_description))
679 response = self._request(path=path, context="inventory group",
680 value=service_description)
681 if response is None or response.status_code != 200:
683 body = response.json()
684 if "instance-group" not in body:
685 LOG.error(_LE("Unable to get instance groups from inventory "
686 " in response {}").format(response))
688 for instance_groups in body["instance-group"]:
689 s_instances = self._get_aai_rel_link_data(
690 data=instance_groups,
691 related_to='service-instance',
692 search_key='service-instance.service-instance-id'
694 if s_instances and len(s_instances) == 2:
696 for s_inst in s_instances:
697 pair.append(s_inst.get('d_value'))
700 LOG.error(_LE("Number of instance pairs not found to "
701 "be two: {}").format(instance_groups))
704 def _log_multiple_item_error(self, name, service_type,
705 related_to, search_key='',
706 context=None, value=None):
707 """Helper method to log multiple-item errors
709 Used by resolve_demands
711 LOG.error(_LE("Demand {}, role {} has more than one {} ({})").
712 format(name, service_type, related_to, search_key))
713 if context and value:
714 LOG.debug("{} details: {}".format(context, value))
716 def match_candidate_attribute(self, candidate, attribute_name,
717 restricted_value, demand_name,
719 """Check if specific candidate attribute matches the restricted value
721 Used by resolve_demands
723 if restricted_value and restricted_value != '' and candidate[attribute_name] != restricted_value:
724 LOG.info(_LI("Demand: {} "
725 "Discarded {} candidate as "
726 "it doesn't match the "
728 "{} ").format(demand_name,
737 def match_vserver_attribute(self, vserver_list):
740 for i in range(0, len(vserver_list)):
742 value != vserver_list[i].get('d_value'):
744 value = vserver_list[i].get('d_value')
747 def match_inventory_attributes(self, template_attributes,
748 inventory_attributes, candidate_id):
750 for attribute_key, attribute_values in template_attributes.items():
752 if attribute_key and \
753 (attribute_key == 'service-type' or attribute_key == 'equipment-role'
754 or attribute_key == 'model-invariant-id' or attribute_key == 'model-version-id'):
758 if type(attribute_values) is dict:
759 if 'any' in attribute_values:
760 attribute_values = attribute_values['any']
761 elif 'not' in attribute_values:
763 attribute_values = attribute_values['not']
765 if match_type == 'any':
766 if attribute_key not in inventory_attributes or \
767 (len(attribute_values) > 0 and inventory_attributes[attribute_key] and
768 inventory_attributes[attribute_key] not in attribute_values):
770 elif match_type == 'not':
771 # drop the candidate when
772 # 1)field exists in AAI and 2)value is not null or empty 3)value is one of those in the 'not' list
773 # Remember, this means if the property is not returned at all from AAI, that still can be a candidate.
774 if attribute_key in inventory_attributes and \
775 inventory_attributes[attribute_key] and \
776 inventory_attributes[attribute_key] in attribute_values:
781 def first_level_service_call(self, path, name, service_type):
783 response = self._request(
784 path=path, context="demand, GENERIC-VNF role",
785 value="{}, {}".format(name, service_type))
786 if response is None or response.status_code != 200:
787 return list() # move ahead with next requirement
788 body = response.json()
789 return body.get("generic-vnf", [])
791 def resolve_v_server_for_candidate(self, candidate_id, location_id, vs_link, add_interfaces, demand_name,
792 triage_translator_data):
794 LOG.error(_LE("{} VSERVER link information not "
795 "available from A&AI").format(demand_name))
796 self.triage_translator.collectDroppedCandiate(candidate_id,
797 location_id, demand_name,
798 triage_translator_data,
799 reason="VSERVER link information not")
800 return None # move ahead with the next vnf
803 vs_link = vs_link + '?depth=2'
804 vs_path = self._get_aai_path_from_link(vs_link)
806 LOG.error(_LE("{} VSERVER path information not "
807 "available from A&AI - {}").
808 format(demand_name, vs_path))
809 self.triage_translator.collectDroppedCandiate(candidate_id,
810 location_id, demand_name,
811 triage_translator_data,
812 reason="VSERVER path information not available from A&AI")
813 return None # move ahead with the next vnf
814 path = self._aai_versioned_path(vs_path)
815 response = self._request(
816 path=path, context="demand, VSERVER",
817 value="{}, {}".format(demand_name, vs_path))
818 if response is None or response.status_code != 200:
819 self.triage_translator.collectDroppedCandiate(candidate_id,
820 location_id, demand_name,
821 triage_translator_data,
822 reason=response.status_code)
824 return response.json()
826 def resolve_vf_modules_for_generic_vnf(self, candidate_id, location_id, vnf, demand_name, triage_translator_data):
827 raw_path = '/network/generic-vnfs/generic-vnf/{}?depth=1'.format(vnf.get("vnf-id"))
828 path = self._aai_versioned_path(raw_path)
830 response = self._request('get', path=path, data=None)
831 if response is None or response.status_code != 200:
832 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
833 triage_translator_data, reason=response)
835 generic_vnf_details = response.json()
837 if generic_vnf_details is None or not generic_vnf_details.get('vf-modules') \
838 or not generic_vnf_details.get('vf-modules').get('vf-module'):
839 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
840 triage_translator_data,
841 reason="Generic-VNF No detailed data for VF-modules")
844 return generic_vnf_details.get('vf-modules').get('vf-module')
846 def resolve_cloud_regions_by_cloud_region_id(self, cloud_region_id):
847 cloud_region_uri = '/cloud-infrastructure/cloud-regions' \
848 '/?cloud-region-id=' \
850 path = self._aai_versioned_path(cloud_region_uri)
852 response = self._request('get',
855 if response is None or response.status_code != 200:
858 body = response.json()
859 return body.get('cloud-region', [])
861 def assign_candidate_existing_placement(self, candidate, existing_placement):
863 """Assign existing_placement and cost parameters to candidate
865 Used by resolve_demands
867 candidate['existing_placement'] = 'false'
868 if existing_placement:
869 if existing_placement.get('candidate_id') == candidate['candidate_id']:
870 candidate['cost'] = self.conf.data.existing_placement_cost
871 candidate['existing_placement'] = 'true'
873 def resovle_conflict_id(self, conflict_identifier, candidate):
875 # Initialize the conflict_id_list
876 conflict_id_list = list()
877 # conflict_id is separated by pipe (|)
880 for conflict_element in conflict_identifier:
881 # if the conflict_element is a dictionary with key = 'get_candidate_attribute',
882 # then add candidate's coressponding value to conflict_id string
883 if isinstance(conflict_element, dict) and 'get_candidate_attribute' in conflict_element:
884 attribute_name = conflict_element.get('get_candidate_attribute')
885 conflict_id_list.append(candidate[attribute_name] + separator)
886 elif isinstance(conflict_element, unicode):
887 conflict_id_list.append(conflict_element + separator)
889 return ''.join(conflict_id_list)
891 def resolve_v_server_links_for_vnf(self, vnf):
892 related_to = "vserver"
893 search_key = "cloud-region.cloud-owner"
894 rl_data_list = self._get_aai_rel_link_data(
895 data=vnf, related_to=related_to,
896 search_key=search_key)
897 vs_link_list = list()
898 for i in range(0, len(rl_data_list)):
899 vs_link_list.append(rl_data_list[i].get('link'))
902 def resolve_complex_info_link_for_v_server(self, candidate_id, v_server, cloud_owner, cloud_region_id,
903 service_type, demand_name, triage_translator_data):
904 related_to = "pserver"
905 rl_data_list = self._get_aai_rel_link_data(
907 related_to=related_to,
910 if len(rl_data_list) > 1:
911 self._log_multiple_item_error(
912 demand_name, service_type, related_to, "item",
914 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
915 triage_translator_data, reason="item VSERVER")
917 rl_data = rl_data_list[0]
918 ps_link = rl_data.get('link')
920 # Third level query to get cloud region from pserver
922 LOG.error(_LE("{} pserver related link "
923 "not found in A&AI: {}").
924 format(demand_name, rl_data))
925 # if HPA_feature is disabled
926 if not self.conf.HPA_enabled:
927 # Triage Tool Feature Changes
928 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
929 triage_translator_data, reason="ps link not found")
932 if not (cloud_owner and cloud_region_id):
933 LOG.error("{} cloud-owner or cloud-region not "
934 "available from A&AI".
936 # Triage Tool Feature Changes
937 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
938 triage_translator_data,
939 reason="Cloud owner and cloud region "
941 return None # move ahead with the next vnf
943 '/cloud-infrastructure/cloud-regions/cloud-region' \
944 '/?cloud-owner=' + cloud_owner \
945 + '&cloud-region-id=' + cloud_region_id
946 path = self._aai_versioned_path(cloud_region_uri)
947 response = self._request('get',
950 if response is None or response.status_code != 200:
951 # Triage Tool Feature Changes
952 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
953 triage_translator_data, reason=response)
955 body = response.json()
957 ps_path = self._get_aai_path_from_link(ps_link)
959 LOG.error(_LE("{} pserver path information "
960 "not found in A&AI: {}").
961 format(demand_name, ps_link))
962 # Triage Tool Feature Changes
963 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
964 triage_translator_data, reason="ps path not found")
965 return None # move ahead with the next vnf
966 path = self._aai_versioned_path(ps_path)
967 response = self._request(
968 path=path, context="PSERVER", value=ps_path)
969 if response is None or response.status_code != 200:
970 # Triage Tool Feature Changes
971 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
972 triage_translator_data, reason=response)
974 body = response.json()
976 related_to = "complex"
977 search_key = "complex.physical-location-id"
978 rl_data_list = self._get_aai_rel_link_data(
980 related_to=related_to,
981 search_key=search_key
983 if len(rl_data_list) > 1:
984 if not self.match_vserver_attribute(rl_data_list):
985 self._log_multiple_item_error(
986 demand_name, service_type, related_to, search_key, "PSERVER", body)
987 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
988 triage_translator_data, reason="PSERVER error")
990 return rl_data_list[0]
992 def resolve_cloud_for_vnf(self, candidate_id, location_id, vnf, service_type, demand_name, triage_translator_data):
993 related_to = "vserver"
994 search_keys = ["cloud-region.cloud-owner", "cloud-region.cloud-region-id"]
996 for search_key in search_keys:
997 rl_data_list = self._get_aai_rel_link_data(
998 data=vnf, related_to=related_to,
999 search_key=search_key)
1001 if len(rl_data_list) > 1:
1002 if not self.match_vserver_attribute(rl_data_list):
1003 self._log_multiple_item_error(
1004 demand_name, service_type, related_to, search_key,
1006 self.triage_translator.collectDroppedCandiate(candidate_id,
1007 location_id, demand_name,
1008 triage_translator_data,
1011 cloud_info[search_key.split(".")[1].replace('-', '_')] = rl_data_list[0].get('d_value') if rl_data_list[
1013 cloud_info['cloud_region_version'] = self.get_cloud_region_version(cloud_info['cloud_region_id'])
1014 cloud_info['location_type'] = 'att_aic'
1015 cloud_info['location_id'] = cloud_info.pop('cloud_region_id')
1018 def get_cloud_region_version(self, cloud_region_id):
1020 regions = self.resolve_cloud_regions_by_cloud_region_id(cloud_region_id)
1023 for region in regions:
1024 if "cloud-region-version" in region:
1025 return self._get_version_from_string(region["cloud-region-version"])
1027 def resolve_global_customer_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1028 demand_name, triage_translator_data):
1029 related_to = "service-instance"
1030 search_key = "customer.global-customer-id"
1031 match_key = "customer.global-customer-id"
1032 rl_data_list = self._get_aai_rel_link_data(
1034 related_to=related_to,
1035 search_key=search_key,
1036 match_dict={'key': match_key,
1037 'value': customer_id}
1039 if len(rl_data_list) > 1:
1040 if not self.match_vserver_attribute(rl_data_list):
1041 self._log_multiple_item_error(
1042 demand_name, service_type, related_to, search_key, "VNF", vnf)
1043 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1044 demand_name, triage_translator_data,
1045 reason=" match_vserver_attribute generic-vnf")
1047 return rl_data_list[0]
1049 def resolve_service_instance_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1050 demand_name, triage_translator_data):
1051 related_to = "service-instance"
1052 search_key = "service-instance.service-instance-id"
1053 match_key = "customer.global-customer-id"
1054 rl_data_list = self._get_aai_rel_link_data(
1056 related_to=related_to,
1057 search_key=search_key,
1058 match_dict={'key': match_key,
1059 'value': customer_id}
1061 if len(rl_data_list) > 1:
1062 if not self.match_vserver_attribute(rl_data_list):
1063 self._log_multiple_item_error(
1064 demand_name, service_type, related_to, search_key, "VNF", vnf)
1065 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1066 demand_name, triage_translator_data,
1067 reason="multiple_item_error generic-vnf")
1069 return rl_data_list[0]
1071 def build_complex_info_for_candidate(self, candidate_id, location_id, vnf, complex_list, service_type, demand_name,
1072 triage_translator_data):
1073 if not complex_list or \
1074 len(complex_list) < 1:
1075 LOG.error("Complex information not "
1076 "available from A&AI")
1077 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1078 triage_translator_data,
1079 reason="Complex information not available from A&AI")
1082 # In the scenario where no pserver information is available
1083 # assumption here is that cloud-region does not span across
1084 # multiple complexes
1085 if len(complex_list) > 1:
1086 related_to = "complex"
1087 search_key = "complex.physical-location-id"
1088 if not self.match_vserver_attribute(complex_list):
1089 self._log_multiple_item_error(
1090 demand_name, service_type, related_to, search_key,
1092 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1093 triage_translator_data,
1094 reason="Generic-vnf error")
1097 rl_data = complex_list[0]
1098 complex_link = rl_data.get('link')
1099 complex_id = rl_data.get('d_value')
1101 # Final query for the complex information
1102 if not (complex_link and complex_id):
1103 LOG.debug("{} complex information not "
1104 "available from A&AI - {}".
1105 format(demand_name, complex_link))
1106 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1107 triage_translator_data,
1108 reason="Complex information not available from A&AI")
1109 return # move ahead with the next vnf
1111 complex_info = self._get_complex(
1112 complex_link=complex_link,
1113 complex_id=complex_id
1115 if not complex_info:
1116 LOG.debug("{} complex information not "
1117 "available from A&AI - {}".
1118 format(demand_name, complex_link))
1119 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1120 triage_translator_data,
1121 reason="Complex information not available from A&AI")
1122 return # move ahead with the next vnf
1124 complex_info = self.build_complex_dict(complex_info, '')
1127 def resolve_demands(self, demands, plan_info, triage_translator_data):
1128 """Resolve demands into inventory candidate lists"""
1130 self.triage_translator.getPlanIdNAme(plan_info['plan_name'], plan_info['plan_id'], triage_translator_data)
1132 resolved_demands = {}
1133 for name, requirements in demands.items():
1134 self.triage_translator.addDemandsTriageTranslator(name, triage_translator_data)
1135 resolved_demands[name] = []
1136 for requirement in requirements:
1137 inventory_type = requirement.get('inventory_type').lower()
1138 service_subscription = requirement.get('service_subscription')
1139 candidate_uniqueness = requirement.get('unique', 'true')
1140 filtering_attributes = requirement.get('filtering_attributes')
1141 passthrough_attributes = requirement.get('passthrough_attributes')
1142 default_attributes = requirement.get('default_attributes')
1143 # TODO(XYZ): may need to support multiple service_type and customer_id in the futrue
1145 # TODO(XYZ): make it consistent for dash and underscore
1146 if filtering_attributes:
1147 # catch equipment-role and service-type from template
1148 equipment_role = filtering_attributes.get('equipment-role')
1149 service_type = filtering_attributes.get('service-type')
1151 service_type = equipment_role
1152 # catch global-customer-id and customer-id from template
1153 global_customer_id = filtering_attributes.get('global-customer-id')
1154 customer_id = filtering_attributes.get('customer-id')
1155 if global_customer_id:
1156 customer_id = global_customer_id
1158 model_invariant_id = filtering_attributes.get('model-invariant-id')
1159 model_version_id = filtering_attributes.get('model-version-id')
1160 service_role = filtering_attributes.get('service-role')
1163 service_type = equipment_role = requirement.get('service_type')
1164 customer_id = global_customer_id = requirement.get('customer_id')
1165 # region_id is OPTIONAL. This will restrict the initial
1166 # candidate set to come from the given region id
1167 restricted_region_id = requirement.get('region')
1168 restricted_complex_id = requirement.get('complex')
1169 # Used for order locking feature
1170 # by defaut, conflict id is the combination of candidate id, service type and vnf-e2e-key
1171 conflict_identifier = requirement.get('conflict_identifier')
1173 vlan_key = requirement.get('vlan_key')
1174 port_key = requirement.get('port_key')
1176 # get required candidates from the demand
1177 required_candidates = requirement.get("required_candidates")
1179 # get existing_placement from the demand
1180 existing_placement = requirement.get("existing_placement")
1182 if required_candidates:
1183 resolved_demands['required_candidates'] = \
1186 # get excluded candidate from the demand
1187 excluded_candidates = requirement.get("excluded_candidates")
1189 # service_resource_id is OPTIONAL and is
1190 # transparent to Conductor
1191 service_resource_id = requirement.get('service_resource_id') \
1192 if requirement.get('service_resource_id') else ''
1194 if inventory_type == 'cloud':
1195 # load region candidates from cache
1196 regions = self._get_regions()
1197 if not regions or len(regions) < 1:
1198 LOG.debug("Region information is not available in cache")
1199 for region_id, region in regions.items():
1200 # Pick only candidates from the restricted_region
1201 info = Candidate.build_candidate_info('aai', inventory_type,
1202 self.conf.data.cloud_candidate_cost,
1203 candidate_uniqueness, region_id, service_resource_id)
1204 cloud = self.resolve_cloud_for_region(region, region_id)
1205 complex_info = self.build_complex_dict(region['complex'], inventory_type)
1206 flavors = self.resolve_flavors_for_region(region['flavors'])
1208 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1209 if self.check_sriov_automation(cloud['cloud_region_version'], name, info['candidate_id']):
1210 other['sriov_automation'] = 'true'
1212 other['sriov_automation'] = 'false'
1213 cloud_candidate = Cloud(info=info, cloud_region=cloud, complex=complex_info, flavors=flavors,
1214 additional_fields=other)
1215 candidate = cloud_candidate.convert_nested_dict_to_dict()
1217 cloud_region_attr = dict()
1218 cloud_region_attr['cloud-owner'] = region['cloud_owner']
1219 cloud_region_attr['cloud-region-version'] = region['cloud_region_version']
1220 cloud_region_attr['cloud-type'] = region['cloud_type']
1221 cloud_region_attr['cloud-zone'] = region['cloud_zone']
1222 cloud_region_attr['complex-name'] = region['complex_name']
1223 cloud_region_attr['physical-location-id'] = region['physical_location_id']
1225 if filtering_attributes and (not self.match_inventory_attributes(filtering_attributes,
1227 candidate['candidate_id'])):
1228 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1229 candidate['location_id'], name,
1230 triage_translator_data,
1231 reason='attributes and match invetory '
1235 if conflict_identifier:
1236 candidate['conflict_id'] = self.resovle_conflict_id(conflict_identifier, candidate)
1238 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1239 triage_translator_data):
1242 self.assign_candidate_existing_placement(candidate, existing_placement)
1244 # Pick only candidates not in the excluded list, if excluded candidate list is provided
1245 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1246 name, triage_translator_data):
1249 # Pick only candidates in the required list, if required candidate list is provided
1250 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1252 triage_translator_data):
1255 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1256 # add candidate to demand candidates
1257 resolved_demands[name].append(candidate)
1258 LOG.debug(">>>>>>> Candidate <<<<<<<")
1259 LOG.debug(json.dumps(candidate, indent=4))
1261 elif (inventory_type == 'service') and customer_id:
1262 # First level query to get the list of generic vnfs
1263 vnf_by_model_invariant = list()
1264 if filtering_attributes and model_invariant_id:
1266 raw_path = '/network/generic-vnfs/' \
1267 '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1268 if model_version_id:
1269 raw_path = '/network/generic-vnfs/' \
1270 '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1272 path = self._aai_versioned_path(raw_path)
1273 vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1275 vnf_by_service_type = list()
1276 if service_type or equipment_role:
1277 path = self._aai_versioned_path(
1278 '/network/generic-vnfs/'
1279 '?equipment-role={}&depth=0'.format(service_type))
1280 vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1282 generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1285 for vnf in generic_vnf:
1286 # if this vnf already appears, skip it
1287 vnf_id = vnf.get('vnf-id')
1288 if vnf_id in vnf_dict:
1290 # add vnf (with vnf_id as key) to the dictionary
1291 vnf_dict[vnf_id] = vnf
1293 vnf_info['host_id'] = vnf.get("vnf-name")
1294 vlan_info = self.build_vlan_info(vlan_key, port_key)
1295 cloud = self.resolve_cloud_for_vnf('', '', vnf, service_type, name, triage_translator_data)
1296 if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1297 cloud['cloud_region_version'] is None:
1300 rl_data = self.resolve_global_customer_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1301 service_type, name, triage_translator_data)
1305 vs_cust_id = rl_data.get('d_value')
1306 rl_data = self.resolve_service_instance_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1307 service_type, name, triage_translator_data)
1311 vs_service_instance_id = rl_data.get('d_value')
1314 if vs_cust_id and vs_cust_id == customer_id:
1315 info = Candidate.build_candidate_info('aai', inventory_type,
1316 self.conf.data.service_candidate_cost,
1317 candidate_uniqueness, vs_service_instance_id,
1318 service_resource_id)
1319 else: # vserver is for a different customer
1320 self.triage_translator.collectDroppedCandiate('', cloud['location_id'], name,
1321 triage_translator_data,
1322 reason="vserver is for a different customer")
1324 # Added vim-id for short-term workaround
1326 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1327 other['sriov_automation'] = 'true' if self.check_sriov_automation(
1328 cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1330 # Second level query to get the pserver from vserver
1331 complex_list = list()
1332 for complex_link in self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'], cloud,
1334 triage_translator_data,
1336 complex_list.append(complex_link[1])
1337 complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1338 cloud['location_id'], vnf,
1339 complex_list, service_type, name,
1340 triage_translator_data)
1341 if "complex_name" not in complex_info:
1344 service_candidate = Service(info=info, cloud_region=cloud, complex=complex_info,
1345 generic_vnf=vnf_info, additional_fields=other, vlan=vlan_info)
1346 candidate = service_candidate.convert_nested_dict_to_dict()
1348 # add specifal parameters for comparsion
1349 vnf['global-customer-id'] = customer_id
1350 vnf['customer-id'] = customer_id
1351 vnf['cloud-region-id'] = cloud.get('cloud_region_id')
1352 vnf['physical-location-id'] = complex_info.get('physical_location_id')
1354 if filtering_attributes and not self.match_inventory_attributes(filtering_attributes, vnf,
1355 candidate['candidate_id']):
1356 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1357 candidate['location_id'], name,
1358 triage_translator_data,
1359 reason="attibute check error")
1361 self.assign_candidate_existing_placement(candidate, existing_placement)
1363 # Pick only candidates not in the excluded list
1364 # if excluded candidate list is provided
1365 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1366 name, triage_translator_data):
1369 # Pick only candidates in the required list
1370 # if required candidate list is provided
1371 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1373 triage_translator_data):
1376 # add the candidate to the demand
1377 # Pick only candidates from the restricted_region
1378 # or restricted_complex
1379 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1380 triage_translator_data):
1383 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1384 resolved_demands[name].append(candidate)
1385 LOG.debug(">>>>>>> Candidate <<<<<<<")
1386 LOG.debug(json.dumps(candidate, indent=4))
1388 elif (inventory_type == 'vfmodule') and customer_id:
1390 # First level query to get the list of generic vnfs
1391 vnf_by_model_invariant = list()
1392 if filtering_attributes and model_invariant_id:
1394 raw_path = '/network/generic-vnfs/' \
1395 '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1396 if model_version_id:
1397 raw_path = '/network/generic-vnfs/' \
1398 '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1400 path = self._aai_versioned_path(raw_path)
1401 vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1403 vnf_by_service_type = list()
1404 if service_type or equipment_role:
1405 path = self._aai_versioned_path('/network/generic-vnfs/'
1406 '?equipment-role={}&depth=0'.format(service_type))
1407 vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1409 generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1412 for vnf in generic_vnf:
1413 # if this vnf already appears, skip it
1414 vnf_id = vnf.get('vnf-id')
1415 if vnf_id in vnf_dict:
1417 # add vnf (with vnf_id as key) to the dictionary
1418 vnf_dict[vnf_id] = vnf
1421 info = Candidate.build_candidate_info('aai', inventory_type,
1422 self.conf.data.service_candidate_cost,
1423 candidate_uniqueness, "", service_resource_id)
1425 vlan_info = self.build_vlan_info(vlan_key, port_key)
1427 vnf_info = self.get_vnf_info(vnf)
1429 rl_data = self.resolve_global_customer_id_for_vnf('', '', vnf, customer_id,
1430 service_type, name, triage_translator_data)
1434 vs_cust_id = rl_data.get('d_value')
1436 rl_data = self.resolve_service_instance_id_for_vnf('', '', vnf, customer_id,
1437 service_type, name, triage_translator_data)
1441 vs_service_instance_id = rl_data.get('d_value')
1443 service_info = dict()
1444 if vs_cust_id and vs_cust_id == customer_id:
1445 service_info['service_instance_id'] = vs_service_instance_id
1446 else: # vserver is for a different customer
1447 self.triage_translator.collectDroppedCandiate('', '', name,
1448 triage_translator_data,
1449 reason="candidate is for a different"
1453 vf_modules_list = self.resolve_vf_modules_for_generic_vnf('', '', vnf, name,
1454 triage_translator_data)
1455 if vf_modules_list is None:
1458 for vf_module in vf_modules_list:
1459 # for vfmodule demands we allow to have vfmodules from different cloud regions
1460 info['candidate_id'] = vf_module.get("vf-module-id")
1461 vf_module_info = self.get_vf_module(vf_module)
1462 cloud = self.resolve_cloud_for_vnf(info['candidate_id'], '', vf_module, service_type, name,
1463 triage_translator_data)
1464 if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1465 cloud['cloud_region_version'] is None:
1468 # OTHER - Added vim-id for short-term workaround
1470 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1471 other['sriov_automation'] = 'true' if self.check_sriov_automation(
1472 cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1474 # Second level query to get the pserver from vserver
1475 vserver_info = dict()
1476 vserver_info['vservers'] = list()
1477 complex_list = list()
1478 for v_server, complex_link in \
1479 self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'],
1481 triage_translator_data,
1483 complex_list.append(complex_link)
1484 candidate_vserver = dict()
1485 candidate_vserver['vserver-id'] = v_server.get('vserver-id')
1486 candidate_vserver['vserver-name'] = v_server.get('vserver-name')
1487 l_interfaces = self.get_l_interfaces_from_vserver(info['candidate_id'],
1488 cloud['location_id'],
1490 triage_translator_data)
1492 candidate_vserver['l-interfaces'] = l_interfaces
1495 vserver_info['vservers'].append(candidate_vserver)
1498 complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1499 cloud['location_id'], vnf,
1500 complex_list, service_type, name,
1501 triage_translator_data)
1502 if complex_info.get("complex_name") is None:
1505 vf_module_candidate = VfModule(complex=complex_info, info=info, generic_vnf=vnf_info,
1506 cloud_region=cloud, service_instance=service_info,
1507 vf_module=vf_module_info, vserver=vserver_info,
1508 additional_fields=other, vlan=vlan_info)
1509 candidate = vf_module_candidate.convert_nested_dict_to_dict()
1511 # add vf-module parameters for filtering
1512 vnf_vf_module_inventory = copy.deepcopy(vnf)
1513 vnf_vf_module_inventory.update(vf_module)
1514 # add specifal parameters for comparsion
1515 vnf_vf_module_inventory['global-customer-id'] = customer_id
1516 vnf_vf_module_inventory['customer-id'] = customer_id
1517 vnf_vf_module_inventory['cloud-region-id'] = cloud.get('location_id')
1518 vnf_vf_module_inventory['physical-location-id'] = complex_info.get('physical_location_id')
1519 vnf_vf_module_inventory['service_instance_id'] = vs_service_instance_id
1521 if filtering_attributes and not self.match_inventory_attributes(filtering_attributes,
1522 vnf_vf_module_inventory,
1523 candidate['candidate_id']):
1524 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1525 candidate['location_id'], name,
1526 triage_translator_data,
1527 reason="attibute check error")
1529 self.assign_candidate_existing_placement(candidate, existing_placement)
1531 # Pick only candidates not in the excluded list
1532 # if excluded candidate list is provided
1533 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates,
1535 name, triage_translator_data):
1538 # Pick only candidates in the required list
1539 # if required candidate list is provided
1540 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1542 triage_translator_data):
1545 # add the candidate to the demand
1546 # Pick only candidates from the restricted_region
1547 # or restricted_complex
1548 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1549 triage_translator_data):
1552 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1553 resolved_demands[name].append(candidate)
1554 LOG.debug(">>>>>>> Candidate <<<<<<<")
1555 with open("vf.log", mode='w') as log_file:
1556 log_file.write(">>>>>>>Vf Candidate <<<<<<<")
1557 log_file.write(json.dumps(candidate, indent=4))
1558 LOG.debug(json.dumps(candidate, indent=4))
1560 elif inventory_type == 'transport' \
1561 and customer_id and service_type and \
1562 service_subscription and service_role:
1564 path = self._aai_versioned_path('business/customers/customer/{}/service-subscriptions/'
1565 'service-subscription/{}/service-instances'
1566 '?service-type={}&service-role={}'.format(customer_id,
1567 service_subscription,
1570 response = self._request('get', path=path, data=None)
1571 if response is None or response.status_code != 200:
1572 self.triage_translator.collectDroppedCandiate("", "", name,
1573 triage_translator_data,
1574 reason=response.status_code)
1576 body = response.json()
1577 transport_vnfs = body.get('service-instance', [])
1579 for vnf in transport_vnfs:
1580 # create a default candidate
1582 other['location_id'] = ''
1583 other['location_type'] = 'att_aic'
1585 vnf_service_instance_id = vnf.get('service-instance-id')
1586 if vnf_service_instance_id:
1587 info = Candidate.build_candidate_info('aai', inventory_type,
1588 self.conf.data.transport_candidate_cost,
1589 candidate_uniqueness, vnf_service_instance_id,
1590 service_resource_id)
1592 self.triage_translator.collectDroppedCandiate('', other['location_id'], name,
1593 triage_translator_data,
1594 reason="service-instance-id error ")
1599 zone = self.resolve_zone_for_vnf(info['candidate_id'], other['location_id'], vnf, name,
1600 triage_translator_data)
1602 zone_info['zone_id'] = zone.get('zone-id')
1603 zone_info['zone_name'] = zone.get('zone-name')
1608 related_to = "complex"
1609 search_key = "complex.physical-location-id"
1610 rel_link_data_list = self._get_aai_rel_link_data(
1612 related_to=related_to,
1613 search_key=search_key
1616 if len(rel_link_data_list) > 1:
1617 self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1618 name, triage_translator_data,
1619 reason="rel_link_data_list error")
1622 rel_link_data = rel_link_data_list[0]
1623 complex_id = rel_link_data.get("d_value")
1624 complex_link = rel_link_data.get('link')
1626 if not (complex_link and complex_id):
1627 LOG.debug("{} complex information not "
1628 "available from A&AI - {}".
1629 format(name, complex_link))
1630 self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1631 name, triage_translator_data,
1632 reason="complex information not available "
1636 complex_info = self._get_complex(
1637 complex_link=complex_link,
1638 complex_id=complex_id
1640 if not complex_info:
1641 LOG.debug("{} complex information not "
1642 "available from A&AI - {}".
1643 format(name, complex_link))
1644 self.triage_translator.collectDroppedCandiate(info['candidate_id'],
1645 other['location_id'], name,
1646 triage_translator_data,
1647 reason="complex information not "
1648 "available from A&AI")
1649 continue # move ahead with the next vnf
1651 complex_info = self.build_complex_dict(complex_info, inventory_type)
1652 transport_candidate = Transport(info=info, zone=zone_info, complex=complex_info,
1653 additional_fiels=other)
1654 candidate = transport_candidate.convert_nested_dict_to_dict()
1656 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1657 # add candidate to demand candidates
1658 resolved_demands[name].append(candidate)
1660 elif inventory_type == 'nssi' or inventory_type == 'nsi':
1661 if filtering_attributes and model_invariant_id:
1662 second_level_match = aai_utils.get_first_level_and_second_level_filter(filtering_attributes,
1664 aai_response = self.get_nxi_candidates(filtering_attributes)
1665 resolved_demands[name].extend(self.filter_nxi_candidates(aai_response, second_level_match,
1667 candidate_uniqueness, inventory_type))
1669 elif inventory_type == 'nst':
1670 if filtering_attributes:
1671 second_level_match = aai_utils.get_first_level_and_second_level_filter(filtering_attributes,
1673 aai_response = self.get_nst_response(filtering_attributes)
1675 sdc_candidates_list = self.get_nst_candidates(aai_response, second_level_match,
1676 default_attributes, candidate_uniqueness,
1678 resolved_demands[name].extend(SDC().update_candidates(sdc_candidates_list))
1681 LOG.error("Unknown inventory_type "
1682 " {}".format(inventory_type))
1683 return resolved_demands
1686 def build_complex_dict(aai_complex, inv_type):
1687 complex_info = dict()
1688 valid_keys = ['physical-location-id', 'complex-name', 'latitude', 'longitude', 'state', 'country', 'city',
1690 # for cloud type, complex_id instead of physical-location-id - note
1691 if inv_type == "cloud":
1692 for valid_key in valid_keys:
1693 if '-' in valid_key:
1694 complex_info[valid_key.replace('-', '_')] = aai_complex.get('complex_id') \
1695 if valid_key == 'physical-location-id' else \
1696 aai_complex.get(valid_key.replace('-', '_'))
1698 complex_info[valid_key] = aai_complex.get(valid_key)
1700 for valid_key in valid_keys:
1701 if '-' in valid_key:
1702 complex_info[valid_key.replace('-', '_')] = aai_complex.get(valid_key)
1704 complex_info[valid_key] = aai_complex.get(valid_key)
1708 def build_vlan_info(vlan_key, port_key):
1710 vlan_info['vlan_key'] = vlan_key
1711 vlan_info['port_key'] = port_key
1714 def resolve_flavors_for_region(self, flavors_obj):
1715 if self.conf.HPA_enabled:
1717 flavors['flavors'] = flavors_obj
1720 def resolve_v_server_and_complex_link_for_vnf(self, candidate_id, cloud, vnf, name, triage_translator_data,
1722 vs_link_list = self.resolve_v_server_links_for_vnf(vnf)
1723 for vs_link in vs_link_list:
1724 body = self.resolve_v_server_for_candidate(candidate_id, cloud['location_id'],
1725 vs_link, True, name, triage_translator_data)
1728 rl_data = self.resolve_complex_info_link_for_v_server(candidate_id, body,
1729 cloud['cloud_owner'], cloud['location_id'],
1730 service_type, name, triage_translator_data)
1735 def get_l_interfaces_from_vserver(self, candidate_id, location_id, v_server, name, triage_translator_data):
1736 if not v_server.get('l-interfaces') or not v_server.get('l-interfaces').get('l-interface'):
1737 self.triage_translator.collectDroppedCandiate(candidate_id,
1739 triage_translator_data,
1740 reason="VF-server interfaces error")
1743 l_interfaces = v_server.get('l-interfaces').get('l-interface')
1744 l_interfaces_list = list()
1746 for l_interface in l_interfaces:
1747 vserver_interface = dict()
1748 vserver_interface['interface-id'] = l_interface.get('interface-id')
1749 vserver_interface['interface-name'] = l_interface.get('interface-name')
1750 vserver_interface['macaddr'] = l_interface.get('macaddr')
1751 vserver_interface['network-id'] = l_interface.get('network-name')
1752 vserver_interface['network-name'] = ''
1753 vserver_interface['ipv4-addresses'] = list()
1754 vserver_interface['ipv6-addresses'] = list()
1756 if l_interface.get('l3-interface-ipv4-address-list'):
1757 for ip_address_info in l_interface.get('l3-interface-ipv4-address-list'):
1758 vserver_interface['ipv4-addresses']. \
1759 append(ip_address_info.get('l3-interface-ipv4-address'))
1761 if l_interface.get('l3-interface-ipv6-address-list'):
1762 for ip_address_info in l_interface.get('l3-interface-ipv6-address-list'):
1763 vserver_interface['ipv6-addresses']. \
1764 append(ip_address_info.get('l3-interface-ipv6-address'))
1766 l_interfaces_list.append(vserver_interface)
1767 return l_interfaces_list
1770 def get_vnf_info(vnf):
1771 # some validation should happen
1773 vnf_info['host_id'] = vnf.get("vnf-name")
1774 vnf_info['nf-name'] = vnf.get("vnf-name")
1775 vnf_info['nf-id'] = vnf.get("vnf-id")
1776 vnf_info['nf-type'] = 'vnf'
1777 vnf_info['vnf-type'] = vnf.get("vnf-type")
1778 vnf_info['ipv4-oam-address'] = vnf.get("ipv4-oam-address") if vnf.get("ipv4-oam-address") else ""
1779 vnf_info['ipv6-oam-address'] = vnf.get("ipv6-oam-address") if vnf.get("ipv6-oam-address") else ""
1783 def resolve_cloud_for_region(region, region_id):
1785 valid_keys = ['cloud_owner', 'cloud_region_version', 'location_id']
1786 for valid_key in valid_keys:
1787 cloud[valid_key] = region.get(valid_key) if not valid_key == 'location_id' else region_id
1788 cloud['location_type'] = 'att_aic'
1792 def get_vf_module(vf_module):
1793 vf_module_info = dict()
1794 vf_module_info['vf-module-name'] = vf_module.get("vf-module-name")
1795 vf_module_info['vf-module-id'] = vf_module.get("vf-module-id")
1796 return vf_module_info
1798 def get_vim_id(self, cloud_owner, cloud_region_id):
1799 if self.conf.HPA_enabled:
1800 return cloud_owner + '_' + cloud_region_id
1803 def add_passthrough_attributes(candidate, passthrough_attributes, demand_name):
1804 if passthrough_attributes is None:
1806 if len(passthrough_attributes.items()) > 0:
1807 candidate['passthrough_attributes'] = dict()
1808 for key, value in passthrough_attributes.items():
1809 candidate['passthrough_attributes'][key] = value
1811 def resolve_zone_for_vnf(self, candidate_id, location_id, vnf, name, triage_translator_data):
1813 zone_link = self._get_aai_rel_link(
1814 data=vnf, related_to=related_to)
1816 LOG.error("Zone information not available from A&AI for transport candidates")
1817 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1818 name, triage_translator_data,
1819 reason="Zone information not available from A&AI for "
1820 "transport candidates")
1822 zone_aai_path = self._get_aai_path_from_link(zone_link)
1823 response = self._request('get', path=zone_aai_path, data=None)
1824 if response is None or response.status_code != 200:
1825 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, name,
1826 triage_translator_data,
1827 reason=response.status_code)
1829 body = response.json()
1832 def match_region(self, candidate, restricted_region_id, restricted_complex_id, demand_name,
1833 triage_translator_data):
1834 if self.match_candidate_attribute(
1837 restricted_region_id,
1839 candidate.get('inventory_type')) or \
1840 self.match_candidate_attribute(
1842 "physical_location_id",
1843 restricted_complex_id,
1845 candidate.get('inventory_type')):
1846 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1847 demand_name, triage_translator_data,
1848 reason="candidate region does not match")
1853 def match_candidate_by_list(self, candidate, candidates_list, exclude, demand_name, triage_translator_data):
1854 has_candidate = False
1856 for list_candidate in candidates_list:
1858 and list_candidate.get('inventory_type') \
1859 == candidate.get('inventory_type'):
1860 if isinstance(list_candidate.get('candidate_id'), list):
1861 for candidate_id in list_candidate.get('candidate_id'):
1862 if candidate_id == candidate.get('candidate_id'):
1863 has_candidate = True
1866 raise Exception("Invalid candidate id list format")
1871 if not has_candidate:
1872 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1873 demand_name, triage_translator_data,
1874 reason="has_required_candidate candidate")
1876 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1877 demand_name, triage_translator_data,
1878 reason="excluded candidate")
1879 return has_candidate
1881 def get_nxi_candidates(self, filtering_attributes):
1882 raw_path = 'nodes/service-instances' + aai_utils.add_query_params_and_depth(filtering_attributes, "2")
1883 path = self._aai_versioned_path(raw_path)
1884 aai_response = self._request('get', path, data=None)
1886 if aai_response is None or aai_response.status_code != 200:
1888 if aai_response.json():
1889 return aai_response.json()
1891 def filter_nxi_candidates(self, response_body, filtering_attributes, default_attributes, candidate_uniqueness,
1893 required_candidates = list()
1895 if response_body is not None:
1896 nxi_instances = response_body.get("service-instance", [])
1898 for nxi_instance in nxi_instances:
1899 inventory_attributes = aai_utils.get_inv_values_for_second_level_filter(filtering_attributes,
1901 nxi_info = aai_utils.get_instance_info(nxi_instance)
1902 if not filtering_attributes or \
1903 self.match_inventory_attributes(filtering_attributes, inventory_attributes,
1904 nxi_instance.get('service-instance-id')):
1905 profile_instances = self.get_profile_instances(nxi_instance)
1907 profiles = aai_utils.get_profiles(profile_instances, "slice-profile")
1908 cost = self.conf.data.nssi_candidate_cost
1910 profiles = aai_utils.get_profiles(profile_instances, "service-profile")
1911 cost = self.conf.data.nsi_candidate_cost
1912 for profile in profiles:
1913 profile_id = profile.get('profile-id')
1914 info = Candidate.build_candidate_info('aai', type, cost, candidate_uniqueness, profile_id)
1915 profile_info = aai_utils.convert_hyphen_to_under_score(profile)
1916 nxi_candidate = NxI(instance_info=nxi_info, profile_info=profile_info, info=info,
1917 default_fields=aai_utils.convert_hyphen_to_under_score(default_attributes))
1918 candidate = nxi_candidate.convert_nested_dict_to_dict()
1919 candidates.append(candidate)
1920 LOG.debug("AAI candidates before adding capacity attributes ", candidates)
1921 capacity_filtered_candidates = DCAE().capacity_filter(candidates)
1922 required_candidates = capacity_filtered_candidates
1923 LOG.debug("updated candidate from DCAE class ", required_candidates)
1924 return required_candidates
1926 def get_profile_instances(self, nxi_instance):
1927 slice_role = nxi_instance['service-role']
1928 related_key = "allotted-resource" if slice_role == 'nsi' else 'service-instance'
1929 related_nodes = self._get_aai_rel_link_data(nxi_instance, related_key,
1930 "service-instance.service-instance-id")
1931 profile_instances = []
1932 for node in related_nodes:
1933 profile_instance_id = node["d_value"]
1934 raw_path = f'nodes/service-instances/service-instance/{profile_instance_id}?depth=2'
1935 path = self._aai_versioned_path(raw_path)
1936 aai_response = self._request('get', path, data=None)
1937 if aai_response.status_code == 200 and aai_response.json():
1938 profile_instances.append(aai_response.json())
1940 return profile_instances
1942 def get_nst_response(self, filtering_attributes):
1943 raw_path = 'service-design-and-creation/models' + aai_utils.add_query_params_and_depth(filtering_attributes,
1945 path = self._aai_versioned_path(raw_path)
1946 aai_response = self._request('get', path, data=None)
1948 if aai_response is None or aai_response.status_code != 200:
1950 if aai_response.json():
1951 return aai_response.json()
1953 def get_nst_candidates(self, response_body, filtering_attributes, default_attributes, candidate_uniqueness,
1956 if response_body is not None:
1957 nst_metadatas = response_body.get("model", [])
1959 for nst_metadata in nst_metadatas:
1960 nst_info = aai_utils.get_nst_info(nst_metadata)
1961 model_vers = nst_metadata.get('model-vers').get('model-ver')
1962 for model_ver in model_vers:
1963 model_version_id = model_ver.get('model-version-id')
1964 cost = self.conf.data.nst_candidate_cost
1965 info = Candidate.build_candidate_info('aai', type, cost, candidate_uniqueness, model_version_id)
1966 model_version_obj = aai_utils.get_model_ver_info(model_ver)
1967 model_ver_info = aai_utils.convert_hyphen_to_under_score(model_version_obj)
1968 nst_candidate = NST(model_info=nst_info, model_ver=model_ver_info, info=info,
1969 default_fields=aai_utils.convert_hyphen_to_under_score(default_attributes),
1971 candidates.append(nst_candidate)