bdc74bcfa25d3ab1702b456c134eab3b2ae14f80
[optf/has.git] / conductor / conductor / data / plugins / inventory_provider / aai.py
1 #
2 # -------------------------------------------------------------------------
3 #   Copyright (c) 2015-2017 AT&T Intellectual Property
4 #   Copyright (C) 2020 Wipro Limited.
5 #
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
9 #
10 #       http://www.apache.org/licenses/LICENSE-2.0
11 #
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.
17 #
18 # -------------------------------------------------------------------------
19 #
20
21 import copy
22 import json
23 import re
24 import time
25 import uuid
26
27 from oslo_config import cfg
28 from oslo_log import log
29
30
31 from conductor.common import rest
32 from conductor.data.plugins import constants
33 from conductor.data.plugins.inventory_provider import base
34 from conductor.data.plugins.inventory_provider.candidates.candidate import Candidate
35 from conductor.data.plugins.inventory_provider.candidates.cloud_candidate import Cloud
36 from conductor.data.plugins.inventory_provider.candidates.nst_candidate import NST
37 from conductor.data.plugins.inventory_provider.candidates.nxi_candidate import NxI
38 from conductor.data.plugins.inventory_provider.candidates.service_candidate import Service
39 from conductor.data.plugins.inventory_provider.candidates.transport_candidate import Transport
40 from conductor.data.plugins.inventory_provider.candidates.vfmodule_candidate import VfModule
41 from conductor.data.plugins.inventory_provider import hpa_utils
42 from conductor.data.plugins.inventory_provider.sdc import SDC
43 from conductor.data.plugins.inventory_provider.utils import aai_utils
44 from conductor.data.plugins.triage_translator.triage_translator import TraigeTranslator
45 from conductor.i18n import _LE
46 from conductor.i18n import _LI
47
48 LOG = log.getLogger(__name__)
49
50 CONF = cfg.CONF
51
52 AAI_OPTS = [
53     cfg.IntOpt('cache_refresh_interval',
54                default=1440,
55                help='Interval with which to refresh the local cache, '
56                     'in minutes.'),
57     cfg.IntOpt('complex_cache_refresh_interval',
58                default=1440,
59                help='Interval with which to refresh the local complex cache, '
60                     'in minutes.'),
61     cfg.StrOpt('table_prefix',
62                default='aai',
63                help='Data Store table prefix.'),
64     cfg.StrOpt('server_url',
65                default='https://controller:8443/aai',
66                help='Base URL for A&AI, up to and not including '
67                     'the version, and without a trailing slash.'),
68     cfg.StrOpt('aai_rest_timeout',
69                default='30',
70                help='Timeout for A&AI Rest Call'),
71     cfg.StrOpt('aai_retries',
72                default='3',
73                help='Number of retry for A&AI Rest Call'),
74     cfg.StrOpt('server_url_version',
75                default='v10',
76                help='The version of A&AI in v# format.'),
77     cfg.StrOpt('certificate_file',
78                default='certificate.pem',
79                help='SSL/TLS certificate file in pem format. '
80                     'This certificate must be registered with the A&AI '
81                     'endpoint.'),
82     cfg.StrOpt('certificate_key_file',
83                default='certificate_key.pem',
84                help='Private Certificate Key file in pem format.'),
85     cfg.StrOpt('certificate_authority_bundle_file',
86                default='certificate_authority_bundle.pem',
87                help='Certificate Authority Bundle file in pem format. '
88                     'Must contain the appropriate trust chain for the '
89                     'Certificate file.'),
90     # TODO(larry): follow-up with ONAP people on this (AA&I basic auth username and password?)
91     cfg.StrOpt('username',
92                default='',
93                help='Username for AAI.'),
94     cfg.StrOpt('password',
95                default='',
96                help='Password for AAI.'),
97 ]
98
99 CONF.register_opts(AAI_OPTS, group='aai')
100
101
102 class AAI(base.InventoryProviderBase):
103     """Active and Available Inventory Provider"""
104
105     def __init__(self):
106         """Initializer"""
107
108         # FIXME(jdandrea): Pass this in to init.
109         self.conf = CONF
110
111         self.base = self.conf.aai.server_url.rstrip('/')
112         self.version = self.conf.aai.server_url_version.rstrip('/')
113         self.cert = self.conf.aai.certificate_file
114         self.key = self.conf.aai.certificate_key_file
115         self.verify = self.conf.aai.certificate_authority_bundle_file
116         self.cache_refresh_interval = self.conf.aai.cache_refresh_interval
117         self.last_refresh_time = None
118         self.complex_cache_refresh_interval = \
119             self.conf.aai.complex_cache_refresh_interval
120         self.complex_last_refresh_time = None
121         self.timeout = self.conf.aai.aai_rest_timeout
122         self.retries = self.conf.aai.aai_retries
123         self.username = self.conf.aai.username
124         self.password = self.conf.aai.password
125         self.triage_translator = TraigeTranslator()
126
127         # Cache is initially empty
128         self._aai_cache = {}
129         self._aai_complex_cache = {}
130
131     def initialize(self):
132
133         """Perform any late initialization."""
134         # Initialize the Python requests
135         self._init_python_request()
136
137         # Refresh the cache once for now
138         self._refresh_cache()
139
140         # TODO(jdandrea): Make this periodic, and without a True condition!
141         # executor = futurist.ThreadPoolExecutor()
142         # while True:
143         #     fut = executor.submit(self.refresh_cache)
144         #     fut.result()
145         #
146         #     # Now wait for the next time.
147         #     # FIXME(jdandrea): Put inside refresh_cache()?
148         #     refresh_interval = self.conf.aai.cache_refresh_interval
149         #     time.sleep(refresh_interval)
150         # executor.shutdown()
151
152     def name(self):
153         """Return human-readable name."""
154         return "A&AI"
155
156     def invoke_method(self, arg):
157         if arg.pop('method_name') == "get_candidates_with_hpa":
158             return hpa_utils.get_candidates_with_hpa(arg)
159
160     @staticmethod
161     def _get_version_from_string(string):
162         """Extract version number from string"""
163         return re.sub("[^0-9.]", "", string)
164
165     def _aai_versioned_path(self, path):
166         """Return a URL path with the A&AI version prepended"""
167         return '/{}/{}'.format(self.version, path.lstrip('/'))
168
169     def _request(self, method='get', path='/', data=None,
170                  context=None, value=None):
171         """Performs HTTP request."""
172         headers = {
173             'X-FromAppId': 'CONDUCTOR',
174             'X-TransactionId': str(uuid.uuid4()),
175         }
176         kwargs = {
177             "method": method,
178             "path": path,
179             "headers": headers,
180             "data": data,
181         }
182
183         # TODO(jdandrea): Move timing/response logging into the rest helper?
184         start_time = time.time()
185         response = self.rest.request(**kwargs)
186         elapsed = time.time() - start_time
187         LOG.debug("Total time for A&AI request "
188                   "({0:}: {1:}): {2:.3f} sec".format(context, value, elapsed))
189
190         if response is None:
191             LOG.error(_LE("No response from A&AI ({}: {})").
192                       format(context, value))
193         elif response.status_code != 200:
194             LOG.error(_LE("A&AI request ({}: {}) returned HTTP "
195                           "status {} {}, link: {}{}").
196                       format(context, value,
197                              response.status_code, response.reason,
198                              self.base, path))
199         return response
200
201     def _init_python_request(self):
202
203         kwargs = {
204             "server_url": self.base,
205             "retries": self.retries,
206             "username": self.username,
207             "password": self.password,
208             "cert_file": self.cert,
209             "cert_key_file": self.key,
210             "ca_bundle_file": self.verify,
211             "log_debug": self.conf.debug,
212             "read_timeout": self.timeout,
213         }
214         self.rest = rest.REST(**kwargs)
215
216     def _refresh_cache(self):
217         """Refresh the A&AI cache."""
218         if not self.last_refresh_time or \
219                 (time.time() - self.last_refresh_time) > \
220                 self.cache_refresh_interval * 60:
221             # TODO(jdandrea): This is presently brute force.
222             # It does not persist to Music. A general purpose ORM caching
223             # object likely needs to be made, with a key (hopefully we
224             # can use one that is not just a UUID), a value, and a
225             # timestamp. The other alternative is to not use the ORM
226             # layer and call the API directly, but that is
227             # also trading one set of todos for another ...
228
229             # Get all A&AI sites
230             LOG.info(_LI("**** Refreshing A&AI cache *****"))
231             path = self._aai_versioned_path(
232                 '/cloud-infrastructure/cloud-regions/?depth=0')
233             response = self._request(
234                 path=path, context="cloud regions", value="all")
235             if response is None:
236                 return
237             regions = {}
238             if response.status_code == 200:
239                 body = response.json()
240                 regions = body.get('cloud-region', {})
241             if not regions:
242                 # Nothing to update the cache with
243                 LOG.error(_LE("A&AI returned no regions, link: {}{}").
244                           format(self.base, path))
245                 return
246             cache = {
247                 'cloud_region': {},
248                 'service': {},
249             }
250             for region in regions:
251                 cloud_region_id = region.get('cloud-region-id')
252
253                 LOG.debug("Working on region '{}' ".format(cloud_region_id))
254
255                 cloud_region_version = region.get('cloud-region-version')
256                 cloud_owner = region.get('cloud-owner')
257                 cloud_type = region.get('cloud-type')
258                 cloud_zone = region.get('cloud-zone')
259
260                 physical_location_list = self._get_aai_rel_link_data(data=region, related_to='complex',
261                                                                      search_key='complex.physical-location-id')
262                 if len(physical_location_list) > 0:
263                     physical_location_id = physical_location_list[0].get('d_value')
264
265                 if not (cloud_region_version and cloud_region_id):
266                     continue
267                 rel_link_data_list = \
268                     self._get_aai_rel_link_data(
269                         data=region,
270                         related_to='complex',
271                         search_key='complex.physical-location-id')
272                 if len(rel_link_data_list) > 1:
273                     LOG.error(_LE("Region {} has more than one complex").
274                               format(cloud_region_id))
275                     LOG.debug("Region {}: {}".format(cloud_region_id, region))
276
277                     continue
278                 rel_link_data = rel_link_data_list[0]
279                 complex_id = rel_link_data.get("d_value")
280                 complex_link = rel_link_data.get("link")
281                 if complex_id and complex_link:
282                     complex_info = self._get_complex(
283                         complex_link=complex_link,
284                         complex_id=complex_id)
285                 else:  # no complex information
286                     LOG.error(_LE("Region {} does not reference a complex").
287                               format(cloud_region_id))
288                     continue
289                 if not complex_info:
290                     LOG.error(_LE("Region {}, complex {} info not found, "
291                                   "link {}").format(cloud_region_id,
292                                                     complex_id, complex_link))
293                     continue
294
295                 latitude = complex_info.get('latitude')
296                 longitude = complex_info.get('longitude')
297                 city = complex_info.get('city')
298                 state = complex_info.get('state')
299                 region = complex_info.get('region')
300                 country = complex_info.get('country')
301                 complex_name = complex_info.get('complex-name')
302
303                 if not (latitude and longitude and city and country and complex_name):
304                     keys = ('latitude', 'longitude', 'city', 'country',
305                             'complex_name')
306                     missing_keys = \
307                         list(set(keys).difference(
308                             list(complex_info.keys())))  # Python 3 Conversion -- dict object to list object
309                     LOG.error(_LE("Complex {} is missing {}, link: {}").
310                               format(complex_id, missing_keys, complex_link))
311                     LOG.debug("Complex {}: {}".
312                               format(complex_id, complex_info))
313
314                     continue
315                 cache['cloud_region'][cloud_region_id] = {
316                     'cloud_region_version': cloud_region_version,
317                     'cloud_owner': cloud_owner,
318                     'cloud_type': cloud_type,
319                     'cloud_zone': cloud_zone,
320                     'complex_name': complex_name,
321                     'physical_location_id': physical_location_id,
322                     'complex': {
323                         'complex_id': complex_id,
324                         'complex_name': complex_name,
325                         'latitude': latitude,
326                         'longitude': longitude,
327                         'city': city,
328                         'state': state,
329                         'region': region,
330                         'country': country,
331                     }
332                 }
333
334                 # Added for HPA support
335                 if self.conf.HPA_enabled:
336                     flavors = self._get_flavors(cloud_owner, cloud_region_id)
337                     cache['cloud_region'][cloud_region_id]['flavors'] = flavors
338
339                 LOG.debug("Candidate with cloud_region_id '{}' selected "
340                           "as a potential candidate - ".format(cloud_region_id))
341             LOG.debug("Done with region '{}' ".format(cloud_region_id))
342             self._aai_cache = cache
343             self.last_refresh_time = time.time()
344             LOG.info(_LI("**** A&AI cache refresh complete *****"))
345
346     @staticmethod
347     def _get_aai_rel_link(data, related_to):
348         """Given an A&AI data structure, return the related-to link"""
349         rel_dict = data.get('relationship-list')
350         if rel_dict:
351             for key, rel_list in rel_dict.items():
352                 for rel in rel_list:
353                     if related_to == rel.get('related-to'):
354                         return rel.get('related-link')
355
356     @staticmethod
357     def _get_aai_rel_link_data(data, related_to, search_key=None,
358                                match_dict=None):
359         # some strings that we will encounter frequently
360         rel_lst = "relationship-list"
361         rkey = "relationship-key"
362         rval = "relationship-value"
363         rdata = "relationship-data"
364         response = list()
365         if match_dict:
366             m_key = match_dict.get('key')
367             m_value = match_dict.get('value')
368         else:
369             m_key = None
370             m_value = None
371         rel_dict = data.get(rel_lst)
372         if rel_dict:  # check if data has relationship lists
373             for key, rel_list in rel_dict.items():
374                 for rel in rel_list:
375                     if rel.get("related-to") == related_to:
376                         dval = None
377                         matched = False
378                         link = rel.get("related-link")
379                         r_data = rel.get(rdata, [])
380                         if search_key:
381                             for rd in r_data:
382                                 if rd.get(rkey) == search_key:
383                                     dval = rd.get(rval)
384                                     if not match_dict:  # return first match
385                                         response.append(
386                                             {"link": link, "d_value": dval}
387                                         )
388                                         break  # go to next relation
389                                 if rd.get(rkey) == m_key \
390                                         and rd.get(rval) == m_value:
391                                     matched = True
392                             if match_dict and matched:  # if matching required
393                                 response.append(
394                                     {"link": link, "d_value": dval}
395                                 )
396                                 # matched, return search value corresponding
397                                 # to the matched r_data group
398                         else:  # no search key; just return the link
399                             response.append(
400                                 {"link": link, "d_value": dval}
401                             )
402         if len(response) == 0:
403             response.append(
404                 {"link": None, "d_value": None}
405             )
406         return response
407
408     @staticmethod
409     def check_sriov_automation(aic_version, demand_name, candidate_name):
410         """Check if specific candidate has SRIOV automation available or no"""
411         if aic_version:
412             LOG.debug(_LI("Demand {}, candidate {} has an AIC version number {}").format(demand_name, candidate_name,
413                                                                                          aic_version))
414             if aic_version == "X.Y":
415                 return True
416         return False
417
418     def _get_complex(self, complex_link, complex_id=None):
419
420         if not self.complex_last_refresh_time or \
421                 (time.time() - self.complex_last_refresh_time) > \
422                 self.complex_cache_refresh_interval * 60:
423             self._aai_complex_cache.clear()
424         if complex_id and complex_id in self._aai_complex_cache:
425             return self._aai_complex_cache[complex_id]
426         else:
427             path = self._aai_versioned_path(self._get_aai_path_from_link(complex_link))
428             response = self._request(path=path, context="complex", value=complex_id)
429             if response is None:
430                 return
431             if response.status_code == 200:
432                 complex_info = response.json()
433                 if 'complex' in complex_info:
434                     complex_info = complex_info.get('complex')
435
436                 latitude = complex_info.get('latitude')
437                 longitude = complex_info.get('longitude')
438                 city = complex_info.get('city')
439                 country = complex_info.get('country')
440                 # removed the state check for countries in Europe that do not always enter states
441                 if not (latitude and longitude and city and country):
442                     keys = ('latitude', 'longitude', 'city', 'country')
443                     missing_keys = \
444                         list(set(keys).difference(set(complex_info.keys())))
445                     LOG.error(_LE("Complex {} is missing {}, link: {}").
446                               format(complex_id, missing_keys, complex_link))
447                     LOG.debug("Complex {}: {}".format(complex_id, complex_info))
448                     return
449
450                 if complex_id:  # cache only if complex_id is given
451                     self._aai_complex_cache[complex_id] = response.json()
452                     self.complex_last_refresh_time = time.time()
453
454                 return complex_info
455
456     def _get_regions(self):
457         self._refresh_cache()
458         regions = self._aai_cache.get('cloud_region', {})
459         return regions
460
461     def _get_flavors(self, cloud_owner, cloud_region_id):
462         '''Fetch all flavors of a given cloud regions specified using {cloud-owner}/{cloud-region-id} composite key
463
464         :return flavors_info json object which list of flavor nodes and its children - HPACapabilities:
465         '''
466
467         LOG.debug("Fetch all flavors and its child nodes HPACapabilities")
468         flavor_path = constants.FLAVORS_URI % (cloud_owner, cloud_region_id)
469         path = self._aai_versioned_path(flavor_path)
470         LOG.debug("Flavors path '{}' ".format(path))
471
472         response = self._request(path=path, context="flavors", value="all")
473         if response is None:
474             return
475         if response.status_code == 200:
476             flavors_info = response.json()
477             if not flavors_info or not flavors_info["flavor"] or \
478                     len(flavors_info["flavor"]) == 0:
479                 LOG.error(_LE("Flavor is missing in Cloud-Region {}/{}").
480                           format(cloud_owner, cloud_region_id))
481                 return
482             LOG.debug(flavors_info)
483             # Remove extraneous flavor information
484             return flavors_info
485         else:
486             LOG.error(_LE("Received Error while fetching flavors from Cloud-region {}/{}").format(cloud_owner,
487                                                                                                   cloud_region_id))
488             return
489
490     def _get_aai_path_from_link(self, link):
491         path = link.split(self.version, 1)
492         if not path or len(path) <= 1:
493             # TODO(shankar): Treat this as a critical error?
494             LOG.error(_LE("A&AI version {} not found in link {}").
495                       format(self.version, link))
496         else:
497             return "{}".format(path[1])
498
499     def check_candidate_role(self, host_id=None):
500
501         vnf_name_uri = '/network/generic-vnfs/?vnf-name=' + host_id + '&depth=0'
502         path = self._aai_versioned_path(vnf_name_uri)
503
504         response = self._request('get', path=path, data=None,
505                                  context="vnf name")
506
507         if response is None or not response.ok:
508             return None
509         body = response.json()
510
511         generic_vnf = body.get("generic-vnf", [])
512
513         for vnf in generic_vnf:
514             related_to = "service-instance"
515             search_key = "customer.global-customer-id"
516             rl_data_list = self._get_aai_rel_link_data(
517                 data=vnf,
518                 related_to=related_to,
519                 search_key=search_key,
520             )
521
522             if len(rl_data_list) != 1:
523                 return None
524
525             rl_data = rl_data_list[0]
526             candidate_role_link = rl_data.get("link")
527
528             if not candidate_role_link:
529                 LOG.error(_LE("Unable to get candidate role link for host id {} ").format(host_id))
530                 return None
531
532             candidate_role_path = self._get_aai_path_from_link(candidate_role_link) + '/allotted-resources?depth=all'
533             path = self._aai_versioned_path(candidate_role_path)
534
535             response = self._request('get', path=path, data=None,
536                                      context="candidate role")
537
538             if response is None or not response.ok:
539                 return None
540             body = response.json()
541
542             response_items = body.get('allotted-resource')
543             if len(response_items) > 0:
544                 role = response_items[0].get('role')
545             return role
546
547     def check_network_roles(self, network_role_id=None):
548         # the network role query from A&AI is not using
549         # the version number in the query
550         network_role_uri = \
551             '/network/l3-networks?network-role=' + network_role_id
552         path = self._aai_versioned_path(network_role_uri)
553
554         # This UUID is reserved by A&AI for a Conductor-specific named query.
555         named_query_uid = "role-UUID"
556
557         data = {
558             "query-parameters": {
559                 "named-query": {
560                     "named-query-uuid": named_query_uid
561                 }
562             },
563             "instance-filters": {
564                 "instance-filter": [
565                     {
566                         "l3-network": {
567                             "network-role": network_role_id
568                         }
569                     }
570                 ]
571             }
572         }
573         region_ids = set()
574         response = self._request('get', path=path, data=data,
575                                  context="role", value=network_role_id)
576         if response is None:
577             return None
578         body = response.json()
579
580         response_items = body.get('l3-network', [])
581
582         for item in response_items:
583             cloud_region_instances = self._get_aai_rel_link_data(
584                 data=item,
585                 related_to='cloud-region',
586                 search_key='cloud-region.cloud-region-id'
587             )
588
589             if len(cloud_region_instances) > 0:
590                 for r_instance in cloud_region_instances:
591                     region_id = r_instance.get('d_value')
592                     if region_id is not None:
593                         region_ids.add(region_id)
594
595         # return region ids that fit the role
596         return region_ids
597
598     def resolve_host_location(self, host_name):
599         path = self._aai_versioned_path('/query?format=id')
600         data = {"start": ["network/pnfs/pnf/" + host_name,
601                           "cloud-infrastructure/pservers/pserver/" + host_name],
602                 "query": "query/ucpe-instance"
603                 }
604         response = self._request('put', path=path, data=data,
605                                  context="host name", value=host_name)
606         if response is None or response.status_code != 200:
607             return None
608         body = response.json()
609         results = body.get('results', [])
610         complex_link = None
611         for result in results:
612             if "resource-type" in result and \
613                     "resource-link" in result and \
614                     result["resource-type"] == "complex":
615                 complex_link = result["resource-link"]
616         if not complex_link:
617             LOG.error(_LE("Unable to get a complex link for hostname {} "
618                           " in response {}").format(host_name, response))
619             return None
620         complex_info = self._get_complex(
621             complex_link=complex_link,
622             complex_id=None
623         )
624         if complex_info:
625             lat = complex_info.get('latitude')
626             lon = complex_info.get('longitude')
627             country = complex_info.get('country')
628             if lat and lon:
629                 location = {"latitude": lat, "longitude": lon}
630                 location["country"] = country if country else None
631                 return location
632             else:
633                 LOG.error(_LE("Unable to get a latitude and longitude "
634                               "information for hostname {} from complex "
635                               " link {}").format(host_name, complex_link))
636                 return None
637         else:
638             LOG.error(_LE("Unable to get a complex information for "
639                           " hostname {} from complex "
640                           " link {}").format(host_name, complex_link))
641             return None
642
643     def resolve_clli_location(self, clli_name):
644         clli_uri = '/cloud-infrastructure/complexes/complex/' + clli_name
645         path = self._aai_versioned_path(clli_uri)
646
647         response = self._request('get', path=path, data=None,
648                                  context="clli name", value=clli_name)
649         if response is None or response.status_code != 200:
650             return None
651
652         body = response.json()
653
654         if body:
655             lat = body.get('latitude')
656             lon = body.get('longitude')
657             country = body.get('country')
658             if lat and lon:
659                 location = {"latitude": lat, "longitude": lon}
660                 location["country"] = country if country else None
661                 return location
662             else:
663                 LOG.error(_LE("Unable to get a latitude and longitude "
664                               "information for CLLI code {} from complex").
665                           format(clli_name))
666                 return None
667         else:
668             LOG.error(_LE("Unable to get a complex information for "
669                           " clli {} from complex "
670                           " link {}").format(clli_name, clli_uri))
671             return None
672
673     def get_inventory_group_pairs(self, service_description):
674         pairs = list()
675         path = self._aai_versioned_path(
676             '/network/instance-groups/?description={}&depth=0'.format(
677                 service_description))
678         response = self._request(path=path, context="inventory group",
679                                  value=service_description)
680         if response is None or response.status_code != 200:
681             return
682         body = response.json()
683         if "instance-group" not in body:
684             LOG.error(_LE("Unable to get instance groups from inventory "
685                           " in response {}").format(response))
686             return
687         for instance_groups in body["instance-group"]:
688             s_instances = self._get_aai_rel_link_data(
689                 data=instance_groups,
690                 related_to='service-instance',
691                 search_key='service-instance.service-instance-id'
692             )
693             if s_instances and len(s_instances) == 2:
694                 pair = list()
695                 for s_inst in s_instances:
696                     pair.append(s_inst.get('d_value'))
697                 pairs.append(pair)
698             else:
699                 LOG.error(_LE("Number of instance pairs not found to "
700                               "be two: {}").format(instance_groups))
701         return pairs
702
703     def _log_multiple_item_error(self, name, service_type,
704                                  related_to, search_key='',
705                                  context=None, value=None):
706         """Helper method to log multiple-item errors
707
708         Used by resolve_demands
709         """
710         LOG.error(_LE("Demand {}, role {} has more than one {} ({})").
711                   format(name, service_type, related_to, search_key))
712         if context and value:
713             LOG.debug("{} details: {}".format(context, value))
714
715     def match_candidate_attribute(self, candidate, attribute_name,
716                                   restricted_value, demand_name,
717                                   inventory_type):
718         """Check if specific candidate attribute matches the restricted value
719
720         Used by resolve_demands
721         """
722         if restricted_value and restricted_value != '' and candidate[attribute_name] != restricted_value:
723             LOG.info(_LI("Demand: {} "
724                          "Discarded {} candidate as "
725                          "it doesn't match the "
726                          "{} attribute "
727                          "{} ").format(demand_name,
728                                        inventory_type,
729                                        attribute_name,
730                                        restricted_value
731                                        )
732                      )
733             return True
734         return False
735
736     def match_vserver_attribute(self, vserver_list):
737
738         value = None
739         for i in range(0, len(vserver_list)):
740             if value and \
741                     value != vserver_list[i].get('d_value'):
742                 return False
743             value = vserver_list[i].get('d_value')
744         return True
745
746     def match_inventory_attributes(self, template_attributes,
747                                    inventory_attributes, candidate_id):
748
749         for attribute_key, attribute_values in template_attributes.items():
750
751             if attribute_key and \
752                     (attribute_key == 'service-type' or attribute_key == 'equipment-role'
753                      or attribute_key == 'model-invariant-id' or attribute_key == 'model-version-id'):
754                 continue
755
756             match_type = 'any'
757             if type(attribute_values) is dict:
758                 if 'any' in attribute_values:
759                     attribute_values = attribute_values['any']
760                 elif 'not' in attribute_values:
761                     match_type = 'not'
762                     attribute_values = attribute_values['not']
763
764             if match_type == 'any':
765                 if attribute_key not in inventory_attributes or \
766                         (len(attribute_values) > 0 and inventory_attributes[attribute_key] and
767                             inventory_attributes[attribute_key] not in attribute_values):
768                     return False
769             elif match_type == 'not':
770                 # drop the candidate when
771                 # 1)field exists in AAI and 2)value is not null or empty 3)value is one of those in the 'not' list
772                 # Remember, this means if the property is not returned at all from AAI, that still can be a candidate.
773                 if attribute_key in inventory_attributes and \
774                         inventory_attributes[attribute_key] and \
775                         inventory_attributes[attribute_key] in attribute_values:
776                     return False
777
778         return True
779
780     def first_level_service_call(self, path, name, service_type):
781
782         response = self._request(
783             path=path, context="demand, GENERIC-VNF role",
784             value="{}, {}".format(name, service_type))
785         if response is None or response.status_code != 200:
786             return list()  # move ahead with next requirement
787         body = response.json()
788         return body.get("generic-vnf", [])
789
790     def resolve_v_server_for_candidate(self, candidate_id, location_id, vs_link, add_interfaces, demand_name,
791                                        triage_translator_data):
792         if not vs_link:
793             LOG.error(_LE("{} VSERVER link information not "
794                           "available from A&AI").format(demand_name))
795             self.triage_translator.collectDroppedCandiate(candidate_id,
796                                                           location_id, demand_name,
797                                                           triage_translator_data,
798                                                           reason="VSERVER link information not")
799             return None  # move ahead with the next vnf
800
801         if add_interfaces:
802             vs_link = vs_link + '?depth=2'
803         vs_path = self._get_aai_path_from_link(vs_link)
804         if not vs_path:
805             LOG.error(_LE("{} VSERVER path information not "
806                           "available from A&AI - {}").
807                       format(demand_name, vs_path))
808             self.triage_translator.collectDroppedCandiate(candidate_id,
809                                                           location_id, demand_name,
810                                                           triage_translator_data,
811                                                           reason="VSERVER path information not available from A&AI")
812             return None  # move ahead with the next vnf
813         path = self._aai_versioned_path(vs_path)
814         response = self._request(
815             path=path, context="demand, VSERVER",
816             value="{}, {}".format(demand_name, vs_path))
817         if response is None or response.status_code != 200:
818             self.triage_translator.collectDroppedCandiate(candidate_id,
819                                                           location_id, demand_name,
820                                                           triage_translator_data,
821                                                           reason=response.status_code)
822             return None
823         return response.json()
824
825     def resolve_vf_modules_for_generic_vnf(self, candidate_id, location_id, vnf, demand_name, triage_translator_data):
826         raw_path = '/network/generic-vnfs/generic-vnf/{}?depth=1'.format(vnf.get("vnf-id"))
827         path = self._aai_versioned_path(raw_path)
828
829         response = self._request('get', path=path, data=None)
830         if response is None or response.status_code != 200:
831             self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
832                                                           triage_translator_data, reason=response)
833             return None
834         generic_vnf_details = response.json()
835
836         if generic_vnf_details is None or not generic_vnf_details.get('vf-modules') \
837                 or not generic_vnf_details.get('vf-modules').get('vf-module'):
838             self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
839                                                           triage_translator_data,
840                                                           reason="Generic-VNF No detailed data for VF-modules")
841             return None
842         else:
843             return generic_vnf_details.get('vf-modules').get('vf-module')
844
845     def resolve_cloud_regions_by_cloud_region_id(self, cloud_region_id):
846         cloud_region_uri = '/cloud-infrastructure/cloud-regions' \
847                            '/?cloud-region-id=' \
848                            + cloud_region_id
849         path = self._aai_versioned_path(cloud_region_uri)
850
851         response = self._request('get',
852                                  path=path,
853                                  data=None)
854         if response is None or response.status_code != 200:
855             return None
856
857         body = response.json()
858         return body.get('cloud-region', [])
859
860     def assign_candidate_existing_placement(self, candidate, existing_placement):
861
862         """Assign existing_placement and cost parameters to candidate
863
864         Used by resolve_demands
865         """
866         candidate['existing_placement'] = 'false'
867         if existing_placement:
868             if existing_placement.get('candidate_id') == candidate['candidate_id']:
869                 candidate['cost'] = self.conf.data.existing_placement_cost
870                 candidate['existing_placement'] = 'true'
871
872     def resovle_conflict_id(self, conflict_identifier, candidate):
873
874         # Initialize the conflict_id_list
875         conflict_id_list = list()
876         # conflict_id is separated by pipe (|)
877         separator = '|'
878
879         for conflict_element in conflict_identifier:
880             # if the conflict_element is a dictionary with key = 'get_candidate_attribute',
881             # then add candidate's coressponding value to conflict_id string
882             if isinstance(conflict_element, dict) and 'get_candidate_attribute' in conflict_element:
883                 attribute_name = conflict_element.get('get_candidate_attribute')
884                 conflict_id_list.append(candidate[attribute_name] + separator)
885             elif isinstance(conflict_element, unicode):
886                 conflict_id_list.append(conflict_element + separator)
887
888         return ''.join(conflict_id_list)
889
890     def resolve_v_server_links_for_vnf(self, vnf):
891         related_to = "vserver"
892         search_key = "cloud-region.cloud-owner"
893         rl_data_list = self._get_aai_rel_link_data(
894             data=vnf, related_to=related_to,
895             search_key=search_key)
896         vs_link_list = list()
897         for i in range(0, len(rl_data_list)):
898             vs_link_list.append(rl_data_list[i].get('link'))
899         return vs_link_list
900
901     def resolve_complex_info_link_for_v_server(self, candidate_id, v_server, cloud_owner, cloud_region_id,
902                                                service_type, demand_name, triage_translator_data):
903         related_to = "pserver"
904         rl_data_list = self._get_aai_rel_link_data(
905             data=v_server,
906             related_to=related_to,
907             search_key=None
908         )
909         if len(rl_data_list) > 1:
910             self._log_multiple_item_error(
911                 demand_name, service_type, related_to, "item",
912                 "VSERVER", v_server)
913             self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
914                                                           triage_translator_data, reason="item VSERVER")
915             return None
916         rl_data = rl_data_list[0]
917         ps_link = rl_data.get('link')
918
919         # Third level query to get cloud region from pserver
920         if not ps_link:
921             LOG.error(_LE("{} pserver related link "
922                           "not found in A&AI: {}").
923                       format(demand_name, rl_data))
924             # if HPA_feature is disabled
925             if not self.conf.HPA_enabled:
926                 # Triage Tool Feature Changes
927                 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
928                                                               triage_translator_data, reason="ps link not found")
929                 return None
930             else:
931                 if not (cloud_owner and cloud_region_id):
932                     LOG.error("{} cloud-owner or cloud-region not "
933                               "available from A&AI".
934                               format(demand_name))
935                     # Triage Tool Feature Changes
936                     self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
937                                                                   triage_translator_data,
938                                                                   reason="Cloud owner and cloud region "
939                                                                          "id not found")
940                     return None  # move ahead with the next vnf
941                 cloud_region_uri = \
942                     '/cloud-infrastructure/cloud-regions/cloud-region' \
943                     '/?cloud-owner=' + cloud_owner \
944                     + '&cloud-region-id=' + cloud_region_id
945                 path = self._aai_versioned_path(cloud_region_uri)
946                 response = self._request('get',
947                                          path=path,
948                                          data=None)
949                 if response is None or response.status_code != 200:
950                     # Triage Tool Feature Changes
951                     self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
952                                                                   triage_translator_data, reason=response)
953                     return None
954                 body = response.json()
955         else:
956             ps_path = self._get_aai_path_from_link(ps_link)
957             if not ps_path:
958                 LOG.error(_LE("{} pserver path information "
959                               "not found in A&AI: {}").
960                           format(demand_name, ps_link))
961                 # Triage Tool Feature Changes
962                 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
963                                                               triage_translator_data, reason="ps path not found")
964                 return None  # move ahead with the next vnf
965             path = self._aai_versioned_path(ps_path)
966             response = self._request(
967                 path=path, context="PSERVER", value=ps_path)
968             if response is None or response.status_code != 200:
969                 # Triage Tool Feature Changes
970                 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
971                                                               triage_translator_data, reason=response)
972                 return None
973             body = response.json()
974
975         related_to = "complex"
976         search_key = "complex.physical-location-id"
977         rl_data_list = self._get_aai_rel_link_data(
978             data=body,
979             related_to=related_to,
980             search_key=search_key
981         )
982         if len(rl_data_list) > 1:
983             if not self.match_vserver_attribute(rl_data_list):
984                 self._log_multiple_item_error(
985                     demand_name, service_type, related_to, search_key, "PSERVER", body)
986                 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
987                                                               triage_translator_data, reason="PSERVER error")
988                 return None
989         return rl_data_list[0]
990
991     def resolve_cloud_for_vnf(self, candidate_id, location_id, vnf, service_type, demand_name, triage_translator_data):
992         related_to = "vserver"
993         search_keys = ["cloud-region.cloud-owner", "cloud-region.cloud-region-id"]
994         cloud_info = dict()
995         for search_key in search_keys:
996             rl_data_list = self._get_aai_rel_link_data(
997                 data=vnf, related_to=related_to,
998                 search_key=search_key)
999
1000             if len(rl_data_list) > 1:
1001                 if not self.match_vserver_attribute(rl_data_list):
1002                     self._log_multiple_item_error(
1003                         demand_name, service_type, related_to, search_key,
1004                         "VNF", vnf)
1005                     self.triage_translator.collectDroppedCandiate(candidate_id,
1006                                                                   location_id, demand_name,
1007                                                                   triage_translator_data,
1008                                                                   reason="VNF error")
1009                     return None
1010             cloud_info[search_key.split(".")[1].replace('-', '_')] = rl_data_list[0].get('d_value') if rl_data_list[
1011                 0] else None
1012         cloud_info['cloud_region_version'] = self.get_cloud_region_version(cloud_info['cloud_region_id'])
1013         cloud_info['location_type'] = 'att_aic'
1014         cloud_info['location_id'] = cloud_info.pop('cloud_region_id')
1015         return cloud_info
1016
1017     def get_cloud_region_version(self, cloud_region_id):
1018         if cloud_region_id:
1019             regions = self.resolve_cloud_regions_by_cloud_region_id(cloud_region_id)
1020             if regions is None:
1021                 return None
1022             for region in regions:
1023                 if "cloud-region-version" in region:
1024                     return self._get_version_from_string(region["cloud-region-version"])
1025
1026     def resolve_global_customer_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1027                                            demand_name, triage_translator_data):
1028         related_to = "service-instance"
1029         search_key = "customer.global-customer-id"
1030         match_key = "customer.global-customer-id"
1031         rl_data_list = self._get_aai_rel_link_data(
1032             data=vnf,
1033             related_to=related_to,
1034             search_key=search_key,
1035             match_dict={'key': match_key,
1036                         'value': customer_id}
1037         )
1038         if len(rl_data_list) > 1:
1039             if not self.match_vserver_attribute(rl_data_list):
1040                 self._log_multiple_item_error(
1041                     demand_name, service_type, related_to, search_key, "VNF", vnf)
1042                 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1043                                                               demand_name, triage_translator_data,
1044                                                               reason=" match_vserver_attribute generic-vnf")
1045                 return None
1046         return rl_data_list[0]
1047
1048     def resolve_service_instance_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1049                                             demand_name, triage_translator_data):
1050         related_to = "service-instance"
1051         search_key = "service-instance.service-instance-id"
1052         match_key = "customer.global-customer-id"
1053         rl_data_list = self._get_aai_rel_link_data(
1054             data=vnf,
1055             related_to=related_to,
1056             search_key=search_key,
1057             match_dict={'key': match_key,
1058                         'value': customer_id}
1059         )
1060         if len(rl_data_list) > 1:
1061             if not self.match_vserver_attribute(rl_data_list):
1062                 self._log_multiple_item_error(
1063                     demand_name, service_type, related_to, search_key, "VNF", vnf)
1064                 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1065                                                               demand_name, triage_translator_data,
1066                                                               reason="multiple_item_error generic-vnf")
1067                 return None
1068         return rl_data_list[0]
1069
1070     def build_complex_info_for_candidate(self, candidate_id, location_id, vnf, complex_list, service_type, demand_name,
1071                                          triage_translator_data):
1072         if not complex_list or \
1073                 len(complex_list) < 1:
1074             LOG.error("Complex information not "
1075                       "available from A&AI")
1076             self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1077                                                           triage_translator_data,
1078                                                           reason="Complex information not available from A&AI")
1079             return
1080
1081         # In the scenario where no pserver information is available
1082         # assumption here is that cloud-region does not span across
1083         # multiple complexes
1084         if len(complex_list) > 1:
1085             related_to = "complex"
1086             search_key = "complex.physical-location-id"
1087             if not self.match_vserver_attribute(complex_list):
1088                 self._log_multiple_item_error(
1089                     demand_name, service_type, related_to, search_key,
1090                     "VNF", vnf)
1091                 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1092                                                               triage_translator_data,
1093                                                               reason="Generic-vnf error")
1094                 return
1095
1096         rl_data = complex_list[0]
1097         complex_link = rl_data.get('link')
1098         complex_id = rl_data.get('d_value')
1099
1100         # Final query for the complex information
1101         if not (complex_link and complex_id):
1102             LOG.debug("{} complex information not "
1103                       "available from A&AI - {}".
1104                       format(demand_name, complex_link))
1105             self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1106                                                           triage_translator_data,
1107                                                           reason="Complex information not available from A&AI")
1108             return  # move ahead with the next vnf
1109         else:
1110             complex_info = self._get_complex(
1111                 complex_link=complex_link,
1112                 complex_id=complex_id
1113             )
1114             if not complex_info:
1115                 LOG.debug("{} complex information not "
1116                           "available from A&AI - {}".
1117                           format(demand_name, complex_link))
1118                 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1119                                                               triage_translator_data,
1120                                                               reason="Complex information not available from A&AI")
1121                 return  # move ahead with the next vnf
1122
1123             complex_info = self.build_complex_dict(complex_info, '')
1124             return complex_info
1125
1126     def resolve_demands(self, demands, plan_info, triage_translator_data):
1127         """Resolve demands into inventory candidate lists"""
1128
1129         self.triage_translator.getPlanIdNAme(plan_info['plan_name'], plan_info['plan_id'], triage_translator_data)
1130
1131         resolved_demands = {}
1132         for name, requirements in demands.items():
1133             self.triage_translator.addDemandsTriageTranslator(name, triage_translator_data)
1134             resolved_demands[name] = []
1135             for requirement in requirements:
1136                 inventory_type = requirement.get('inventory_type').lower()
1137                 service_subscription = requirement.get('service_subscription')
1138                 candidate_uniqueness = requirement.get('unique', 'true')
1139                 filtering_attributes = requirement.get('filtering_attributes')
1140                 passthrough_attributes = requirement.get('passthrough_attributes')
1141                 default_attributes = requirement.get('default_attributes')
1142                 # TODO(XYZ): may need to support multiple service_type and customer_id in the futrue
1143
1144                 # TODO(XYZ): make it consistent for dash and underscore
1145                 if filtering_attributes:
1146                     # catch equipment-role and service-type from template
1147                     equipment_role = filtering_attributes.get('equipment-role')
1148                     service_type = filtering_attributes.get('service-type')
1149                     if equipment_role:
1150                         service_type = equipment_role
1151                     # catch global-customer-id and customer-id from template
1152                     global_customer_id = filtering_attributes.get('global-customer-id')
1153                     customer_id = filtering_attributes.get('customer-id')
1154                     if global_customer_id:
1155                         customer_id = global_customer_id
1156
1157                     model_invariant_id = filtering_attributes.get('model-invariant-id')
1158                     model_version_id = filtering_attributes.get('model-version-id')
1159                     service_role = filtering_attributes.get('service-role')
1160                 # For earlier
1161                 else:
1162                     service_type = equipment_role = requirement.get('service_type')
1163                     customer_id = global_customer_id = requirement.get('customer_id')
1164                 # region_id is OPTIONAL. This will restrict the initial
1165                 # candidate set to come from the given region id
1166                 restricted_region_id = requirement.get('region')
1167                 restricted_complex_id = requirement.get('complex')
1168                 # Used for order locking feature
1169                 # by defaut, conflict id is the combination of candidate id, service type and vnf-e2e-key
1170                 conflict_identifier = requirement.get('conflict_identifier')
1171                 # VLAN fields
1172                 vlan_key = requirement.get('vlan_key')
1173                 port_key = requirement.get('port_key')
1174
1175                 # get required candidates from the demand
1176                 required_candidates = requirement.get("required_candidates")
1177
1178                 # get existing_placement from the demand
1179                 existing_placement = requirement.get("existing_placement")
1180
1181                 if required_candidates:
1182                     resolved_demands['required_candidates'] = \
1183                         required_candidates
1184
1185                 # get excluded candidate from the demand
1186                 excluded_candidates = requirement.get("excluded_candidates")
1187
1188                 # service_resource_id is OPTIONAL and is
1189                 # transparent to Conductor
1190                 service_resource_id = requirement.get('service_resource_id') \
1191                     if requirement.get('service_resource_id') else ''
1192
1193                 if inventory_type == 'cloud':
1194                     # load region candidates from cache
1195                     regions = self._get_regions()
1196                     if not regions or len(regions) < 1:
1197                         LOG.debug("Region information is not available in cache")
1198                     for region_id, region in regions.items():
1199                         # Pick only candidates from the restricted_region
1200                         info = Candidate.build_candidate_info('aai', inventory_type,
1201                                                               self.conf.data.cloud_candidate_cost,
1202                                                               candidate_uniqueness, region_id, service_resource_id)
1203                         cloud = self.resolve_cloud_for_region(region, region_id)
1204                         complex_info = self.build_complex_dict(region['complex'], inventory_type)
1205                         flavors = self.resolve_flavors_for_region(region['flavors'])
1206                         other = dict()
1207                         other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1208                         if self.check_sriov_automation(cloud['cloud_region_version'], name, info['candidate_id']):
1209                             other['sriov_automation'] = 'true'
1210                         else:
1211                             other['sriov_automation'] = 'false'
1212                         cloud_candidate = Cloud(info=info, cloud_region=cloud, complex=complex_info, flavors=flavors,
1213                                                 additional_fields=other)
1214                         candidate = cloud_candidate.convert_nested_dict_to_dict()
1215
1216                         cloud_region_attr = dict()
1217                         cloud_region_attr['cloud-owner'] = region['cloud_owner']
1218                         cloud_region_attr['cloud-region-version'] = region['cloud_region_version']
1219                         cloud_region_attr['cloud-type'] = region['cloud_type']
1220                         cloud_region_attr['cloud-zone'] = region['cloud_zone']
1221                         cloud_region_attr['complex-name'] = region['complex_name']
1222                         cloud_region_attr['physical-location-id'] = region['physical_location_id']
1223
1224                         if filtering_attributes and (not self.match_inventory_attributes(filtering_attributes,
1225                                                                                          cloud_region_attr,
1226                                                                                          candidate['candidate_id'])):
1227                             self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1228                                                                           candidate['location_id'], name,
1229                                                                           triage_translator_data,
1230                                                                           reason='attributes and match invetory '
1231                                                                                  'attributes')
1232                             continue
1233
1234                         if conflict_identifier:
1235                             candidate['conflict_id'] = self.resovle_conflict_id(conflict_identifier, candidate)
1236
1237                         if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1238                                                  triage_translator_data):
1239                             continue
1240
1241                         self.assign_candidate_existing_placement(candidate, existing_placement)
1242
1243                         # Pick only candidates not in the excluded list, if excluded candidate list is provided
1244                         if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1245                                                                                 name, triage_translator_data):
1246                             continue
1247
1248                         # Pick only candidates in the required list, if required candidate list is provided
1249                         if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1250                                                                                     False, name,
1251                                                                                     triage_translator_data):
1252                             continue
1253
1254                         self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1255                         # add candidate to demand candidates
1256                         resolved_demands[name].append(candidate)
1257                         LOG.debug(">>>>>>> Candidate <<<<<<<")
1258                         LOG.debug(json.dumps(candidate, indent=4))
1259
1260                 elif (inventory_type == 'service') and customer_id:
1261                     # First level query to get the list of generic vnfs
1262                     vnf_by_model_invariant = list()
1263                     if filtering_attributes and model_invariant_id:
1264
1265                         raw_path = '/network/generic-vnfs/' \
1266                                    '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1267                         if model_version_id:
1268                             raw_path = '/network/generic-vnfs/' \
1269                                        '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1270                                                                                                    model_version_id)
1271                         path = self._aai_versioned_path(raw_path)
1272                         vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1273
1274                     vnf_by_service_type = list()
1275                     if service_type or equipment_role:
1276                         path = self._aai_versioned_path(
1277                             '/network/generic-vnfs/'
1278                             '?equipment-role={}&depth=0'.format(service_type))
1279                         vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1280
1281                     generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1282                     vnf_dict = dict()
1283
1284                     for vnf in generic_vnf:
1285                         # if this vnf already appears, skip it
1286                         vnf_id = vnf.get('vnf-id')
1287                         if vnf_id in vnf_dict:
1288                             continue
1289                         # add vnf (with vnf_id as key) to the dictionary
1290                         vnf_dict[vnf_id] = vnf
1291                         vnf_info = dict()
1292                         vnf_info['host_id'] = vnf.get("vnf-name")
1293                         vlan_info = self.build_vlan_info(vlan_key, port_key)
1294                         cloud = self.resolve_cloud_for_vnf('', '', vnf, service_type, name, triage_translator_data)
1295                         if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1296                                 cloud['cloud_region_version'] is None:
1297                             continue
1298
1299                         rl_data = self.resolve_global_customer_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1300                                                                           service_type, name, triage_translator_data)
1301                         if rl_data is None:
1302                             continue
1303                         else:
1304                             vs_cust_id = rl_data.get('d_value')
1305                         rl_data = self.resolve_service_instance_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1306                                                                            service_type, name, triage_translator_data)
1307                         if rl_data is None:
1308                             continue
1309                         else:
1310                             vs_service_instance_id = rl_data.get('d_value')
1311
1312                         # INFO
1313                         if vs_cust_id and vs_cust_id == customer_id:
1314                             info = Candidate.build_candidate_info('aai', inventory_type,
1315                                                                   self.conf.data.service_candidate_cost,
1316                                                                   candidate_uniqueness, vs_service_instance_id,
1317                                                                   service_resource_id)
1318                         else:  # vserver is for a different customer
1319                             self.triage_translator.collectDroppedCandiate('', cloud['location_id'], name,
1320                                                                           triage_translator_data,
1321                                                                           reason="vserver is for a different customer")
1322                             continue
1323                         # Added vim-id for short-term workaround
1324                         other = dict()
1325                         other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1326                         other['sriov_automation'] = 'true' if self.check_sriov_automation(
1327                             cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1328
1329                         # Second level query to get the pserver from vserver
1330                         complex_list = list()
1331                         for complex_link in self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'], cloud,
1332                                                                                            vnf, name,
1333                                                                                            triage_translator_data,
1334                                                                                            service_type):
1335                             complex_list.append(complex_link[1])
1336                         complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1337                                                                              cloud['location_id'], vnf,
1338                                                                              complex_list, service_type, name,
1339                                                                              triage_translator_data)
1340                         if "complex_name" not in complex_info:
1341                             continue
1342
1343                         service_candidate = Service(info=info, cloud_region=cloud, complex=complex_info,
1344                                                     generic_vnf=vnf_info, additional_fields=other, vlan=vlan_info)
1345                         candidate = service_candidate.convert_nested_dict_to_dict()
1346
1347                         # add specifal parameters for comparsion
1348                         vnf['global-customer-id'] = customer_id
1349                         vnf['customer-id'] = customer_id
1350                         vnf['cloud-region-id'] = cloud.get('cloud_region_id')
1351                         vnf['physical-location-id'] = complex_info.get('physical_location_id')
1352
1353                         if filtering_attributes and not self.match_inventory_attributes(filtering_attributes, vnf,
1354                                                                                         candidate['candidate_id']):
1355                             self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1356                                                                           candidate['location_id'], name,
1357                                                                           triage_translator_data,
1358                                                                           reason="attibute check error")
1359                             continue
1360                         self.assign_candidate_existing_placement(candidate, existing_placement)
1361
1362                         # Pick only candidates not in the excluded list
1363                         # if excluded candidate list is provided
1364                         if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1365                                                                                 name, triage_translator_data):
1366                             continue
1367
1368                         # Pick only candidates in the required list
1369                         # if required candidate list is provided
1370                         if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1371                                                                                     False, name,
1372                                                                                     triage_translator_data):
1373                             continue
1374
1375                         # add the candidate to the demand
1376                         # Pick only candidates from the restricted_region
1377                         # or restricted_complex
1378                         if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1379                                                  triage_translator_data):
1380                             continue
1381                         else:
1382                             self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1383                             resolved_demands[name].append(candidate)
1384                             LOG.debug(">>>>>>> Candidate <<<<<<<")
1385                             LOG.debug(json.dumps(candidate, indent=4))
1386
1387                 elif (inventory_type == 'vfmodule') and customer_id:
1388
1389                     # First level query to get the list of generic vnfs
1390                     vnf_by_model_invariant = list()
1391                     if filtering_attributes and model_invariant_id:
1392
1393                         raw_path = '/network/generic-vnfs/' \
1394                                    '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1395                         if model_version_id:
1396                             raw_path = '/network/generic-vnfs/' \
1397                                        '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1398                                                                                                    model_version_id)
1399                         path = self._aai_versioned_path(raw_path)
1400                         vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1401
1402                     vnf_by_service_type = list()
1403                     if service_type or equipment_role:
1404                         path = self._aai_versioned_path('/network/generic-vnfs/'
1405                                                         '?equipment-role={}&depth=0'.format(service_type))
1406                         vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1407
1408                     generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1409                     vnf_dict = dict()
1410
1411                     for vnf in generic_vnf:
1412                         # if this vnf already appears, skip it
1413                         vnf_id = vnf.get('vnf-id')
1414                         if vnf_id in vnf_dict:
1415                             continue
1416                         # add vnf (with vnf_id as key) to the dictionary
1417                         vnf_dict[vnf_id] = vnf
1418
1419                         # INFO
1420                         info = Candidate.build_candidate_info('aai', inventory_type,
1421                                                               self.conf.data.service_candidate_cost,
1422                                                               candidate_uniqueness, "", service_resource_id)
1423                         # VLAN INFO
1424                         vlan_info = self.build_vlan_info(vlan_key, port_key)
1425                         # Generic VNF Info
1426                         vnf_info = self.get_vnf_info(vnf)
1427
1428                         rl_data = self.resolve_global_customer_id_for_vnf('', '', vnf, customer_id,
1429                                                                           service_type, name, triage_translator_data)
1430                         if rl_data is None:
1431                             continue
1432                         else:
1433                             vs_cust_id = rl_data.get('d_value')
1434
1435                         rl_data = self.resolve_service_instance_id_for_vnf('', '', vnf, customer_id,
1436                                                                            service_type, name, triage_translator_data)
1437                         if rl_data is None:
1438                             continue
1439                         else:
1440                             vs_service_instance_id = rl_data.get('d_value')
1441
1442                         service_info = dict()
1443                         if vs_cust_id and vs_cust_id == customer_id:
1444                             service_info['service_instance_id'] = vs_service_instance_id
1445                         else:  # vserver is for a different customer
1446                             self.triage_translator.collectDroppedCandiate('', '', name,
1447                                                                           triage_translator_data,
1448                                                                           reason="candidate is for a different"
1449                                                                                  " customer")
1450                             continue
1451
1452                         vf_modules_list = self.resolve_vf_modules_for_generic_vnf('', '', vnf, name,
1453                                                                                   triage_translator_data)
1454                         if vf_modules_list is None:
1455                             continue
1456
1457                         for vf_module in vf_modules_list:
1458                             # for vfmodule demands we allow to have vfmodules from different cloud regions
1459                             info['candidate_id'] = vf_module.get("vf-module-id")
1460                             vf_module_info = self.get_vf_module(vf_module)
1461                             cloud = self.resolve_cloud_for_vnf(info['candidate_id'], '', vf_module, service_type, name,
1462                                                                triage_translator_data)
1463                             if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1464                                     cloud['cloud_region_version'] is None:
1465                                 continue
1466
1467                             # OTHER - Added vim-id for short-term workaround
1468                             other = dict()
1469                             other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1470                             other['sriov_automation'] = 'true' if self.check_sriov_automation(
1471                                 cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1472
1473                             # Second level query to get the pserver from vserver
1474                             vserver_info = dict()
1475                             vserver_info['vservers'] = list()
1476                             complex_list = list()
1477                             for v_server, complex_link in \
1478                                     self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'],
1479                                                                                    cloud, vnf, name,
1480                                                                                    triage_translator_data,
1481                                                                                    service_type):
1482                                 complex_list.append(complex_link)
1483                                 candidate_vserver = dict()
1484                                 candidate_vserver['vserver-id'] = v_server.get('vserver-id')
1485                                 candidate_vserver['vserver-name'] = v_server.get('vserver-name')
1486                                 l_interfaces = self.get_l_interfaces_from_vserver(info['candidate_id'],
1487                                                                                   cloud['location_id'],
1488                                                                                   v_server, name,
1489                                                                                   triage_translator_data)
1490                                 if l_interfaces:
1491                                     candidate_vserver['l-interfaces'] = l_interfaces
1492                                 else:
1493                                     continue
1494                                 vserver_info['vservers'].append(candidate_vserver)
1495
1496                             # COMPLEX
1497                             complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1498                                                                                  cloud['location_id'], vnf,
1499                                                                                  complex_list, service_type, name,
1500                                                                                  triage_translator_data)
1501                             if complex_info.get("complex_name") is None:
1502                                 continue
1503
1504                             vf_module_candidate = VfModule(complex=complex_info, info=info, generic_vnf=vnf_info,
1505                                                            cloud_region=cloud, service_instance=service_info,
1506                                                            vf_module=vf_module_info, vserver=vserver_info,
1507                                                            additional_fields=other, vlan=vlan_info)
1508                             candidate = vf_module_candidate.convert_nested_dict_to_dict()
1509
1510                             # add vf-module parameters for filtering
1511                             vnf_vf_module_inventory = copy.deepcopy(vnf)
1512                             vnf_vf_module_inventory.update(vf_module)
1513                             # add specifal parameters for comparsion
1514                             vnf_vf_module_inventory['global-customer-id'] = customer_id
1515                             vnf_vf_module_inventory['customer-id'] = customer_id
1516                             vnf_vf_module_inventory['cloud-region-id'] = cloud.get('location_id')
1517                             vnf_vf_module_inventory['physical-location-id'] = complex_info.get('physical_location_id')
1518                             vnf_vf_module_inventory['service_instance_id'] = vs_service_instance_id
1519
1520                             if filtering_attributes and not self.match_inventory_attributes(filtering_attributes,
1521                                                                                             vnf_vf_module_inventory,
1522                                                                                             candidate['candidate_id']):
1523                                 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1524                                                                               candidate['location_id'], name,
1525                                                                               triage_translator_data,
1526                                                                               reason="attibute check error")
1527                                 continue
1528                             self.assign_candidate_existing_placement(candidate, existing_placement)
1529
1530                             # Pick only candidates not in the excluded list
1531                             # if excluded candidate list is provided
1532                             if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates,
1533                                                                                     True,
1534                                                                                     name, triage_translator_data):
1535                                 continue
1536
1537                             # Pick only candidates in the required list
1538                             # if required candidate list is provided
1539                             if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1540                                                                                         False, name,
1541                                                                                         triage_translator_data):
1542                                 continue
1543
1544                             # add the candidate to the demand
1545                             # Pick only candidates from the restricted_region
1546                             # or restricted_complex
1547                             if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1548                                                      triage_translator_data):
1549                                 continue
1550                             else:
1551                                 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1552                                 resolved_demands[name].append(candidate)
1553                                 LOG.debug(">>>>>>> Candidate <<<<<<<")
1554                                 with open("vf.log", mode='w') as log_file:
1555                                     log_file.write(">>>>>>>Vf Candidate <<<<<<<")
1556                                     log_file.write(json.dumps(candidate, indent=4))
1557                                 LOG.debug(json.dumps(candidate, indent=4))
1558
1559                 elif inventory_type == 'transport' \
1560                         and customer_id and service_type and \
1561                         service_subscription and service_role:
1562
1563                     path = self._aai_versioned_path('business/customers/customer/{}/service-subscriptions/'
1564                                                     'service-subscription/{}/service-instances'
1565                                                     '?service-type={}&service-role={}'.format(customer_id,
1566                                                                                               service_subscription,
1567                                                                                               service_type,
1568                                                                                               service_role))
1569                     response = self._request('get', path=path, data=None)
1570                     if response is None or response.status_code != 200:
1571                         self.triage_translator.collectDroppedCandiate("", "", name,
1572                                                                       triage_translator_data,
1573                                                                       reason=response.status_code)
1574                         continue
1575                     body = response.json()
1576                     transport_vnfs = body.get('service-instance', [])
1577
1578                     for vnf in transport_vnfs:
1579                         # create a default candidate
1580                         other = dict()
1581                         other['location_id'] = ''
1582                         other['location_type'] = 'att_aic'
1583                         # INFO
1584                         vnf_service_instance_id = vnf.get('service-instance-id')
1585                         if vnf_service_instance_id:
1586                             info = Candidate.build_candidate_info('aai', inventory_type,
1587                                                                   self.conf.data.transport_candidate_cost,
1588                                                                   candidate_uniqueness, vnf_service_instance_id,
1589                                                                   service_resource_id)
1590                         else:
1591                             self.triage_translator.collectDroppedCandiate('', other['location_id'], name,
1592                                                                           triage_translator_data,
1593                                                                           reason="service-instance-id error ")
1594                             continue
1595
1596                         # ZONE
1597                         zone_info = dict()
1598                         zone = self.resolve_zone_for_vnf(info['candidate_id'], other['location_id'], vnf, name,
1599                                                          triage_translator_data)
1600                         if zone:
1601                             zone_info['zone_id'] = zone.get('zone-id')
1602                             zone_info['zone_name'] = zone.get('zone-name')
1603                         else:
1604                             continue
1605
1606                         # COMPLEX
1607                         related_to = "complex"
1608                         search_key = "complex.physical-location-id"
1609                         rel_link_data_list = self._get_aai_rel_link_data(
1610                             data=zone,
1611                             related_to=related_to,
1612                             search_key=search_key
1613                         )
1614
1615                         if len(rel_link_data_list) > 1:
1616                             self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1617                                                                           name, triage_translator_data,
1618                                                                           reason="rel_link_data_list error")
1619
1620                             continue
1621                         rel_link_data = rel_link_data_list[0]
1622                         complex_id = rel_link_data.get("d_value")
1623                         complex_link = rel_link_data.get('link')
1624
1625                         if not (complex_link and complex_id):
1626                             LOG.debug("{} complex information not "
1627                                       "available from A&AI - {}".
1628                                       format(name, complex_link))
1629                             self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1630                                                                           name, triage_translator_data,
1631                                                                           reason="complex information not available "
1632                                                                                  "from A&AI")
1633                             continue
1634                         else:
1635                             complex_info = self._get_complex(
1636                                 complex_link=complex_link,
1637                                 complex_id=complex_id
1638                             )
1639                             if not complex_info:
1640                                 LOG.debug("{} complex information not "
1641                                           "available from A&AI - {}".
1642                                           format(name, complex_link))
1643                                 self.triage_translator.collectDroppedCandiate(info['candidate_id'],
1644                                                                               other['location_id'], name,
1645                                                                               triage_translator_data,
1646                                                                               reason="complex information not "
1647                                                                                      "available from A&AI")
1648                                 continue  # move ahead with the next vnf
1649
1650                             complex_info = self.build_complex_dict(complex_info, inventory_type)
1651                             transport_candidate = Transport(info=info, zone=zone_info, complex=complex_info,
1652                                                             additional_fiels=other)
1653                             candidate = transport_candidate.convert_nested_dict_to_dict()
1654
1655                             self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1656                             # add candidate to demand candidates
1657                             resolved_demands[name].append(candidate)
1658
1659                 elif inventory_type == 'nssi' or inventory_type == 'nsi':
1660                     if filtering_attributes and model_invariant_id:
1661                         second_level_match = aai_utils.get_first_level_and_second_level_filter(filtering_attributes,
1662                                                                                                "service_instance")
1663                         aai_response = self.get_nxi_candidates(filtering_attributes)
1664                         resolved_demands[name].extend(self.filter_nxi_candidates(aai_response, second_level_match,
1665                                                                                  default_attributes,
1666                                                                                  candidate_uniqueness, inventory_type))
1667
1668                 elif inventory_type == 'nst':
1669                     if filtering_attributes:
1670                         second_level_match = aai_utils.get_first_level_and_second_level_filter(filtering_attributes,
1671                                                                                                "nst")
1672                         aai_response = self.get_nst_response(filtering_attributes)
1673
1674                         sdc_candidates_list = self.get_nst_candidates(aai_response, second_level_match,
1675                                                                       default_attributes, candidate_uniqueness,
1676                                                                       inventory_type)
1677                         resolved_demands[name].extend(SDC().update_candidates(sdc_candidates_list))
1678
1679                 else:
1680                     LOG.error("Unknown inventory_type "
1681                               " {}".format(inventory_type))
1682         return resolved_demands
1683
1684     @staticmethod
1685     def build_complex_dict(aai_complex, inv_type):
1686         complex_info = dict()
1687         valid_keys = ['physical-location-id', 'complex-name', 'latitude', 'longitude', 'state', 'country', 'city',
1688                       'region']
1689         # for cloud type, complex_id instead of physical-location-id - note
1690         if inv_type == "cloud":
1691             for valid_key in valid_keys:
1692                 if '-' in valid_key:
1693                     complex_info[valid_key.replace('-', '_')] = aai_complex.get('complex_id') \
1694                         if valid_key == 'physical-location-id' else \
1695                         aai_complex.get(valid_key.replace('-', '_'))
1696                 else:
1697                     complex_info[valid_key] = aai_complex.get(valid_key)
1698         else:
1699             for valid_key in valid_keys:
1700                 if '-' in valid_key:
1701                     complex_info[valid_key.replace('-', '_')] = aai_complex.get(valid_key)
1702                 else:
1703                     complex_info[valid_key] = aai_complex.get(valid_key)
1704         return complex_info
1705
1706     @staticmethod
1707     def build_vlan_info(vlan_key, port_key):
1708         vlan_info = dict()
1709         vlan_info['vlan_key'] = vlan_key
1710         vlan_info['port_key'] = port_key
1711         return vlan_info
1712
1713     def resolve_flavors_for_region(self, flavors_obj):
1714         if self.conf.HPA_enabled:
1715             flavors = dict()
1716             flavors['flavors'] = flavors_obj
1717         return flavors
1718
1719     def resolve_v_server_and_complex_link_for_vnf(self, candidate_id, cloud, vnf, name, triage_translator_data,
1720                                                   service_type):
1721         vs_link_list = self.resolve_v_server_links_for_vnf(vnf)
1722         for vs_link in vs_link_list:
1723             body = self.resolve_v_server_for_candidate(candidate_id, cloud['location_id'],
1724                                                        vs_link, True, name, triage_translator_data)
1725             if body is None:
1726                 continue
1727             rl_data = self.resolve_complex_info_link_for_v_server(candidate_id, body,
1728                                                                   cloud['cloud_owner'], cloud['location_id'],
1729                                                                   service_type, name, triage_translator_data)
1730             if rl_data is None:
1731                 continue
1732             yield body, rl_data
1733
1734     def get_l_interfaces_from_vserver(self, candidate_id, location_id, v_server, name, triage_translator_data):
1735         if not v_server.get('l-interfaces') or not v_server.get('l-interfaces').get('l-interface'):
1736             self.triage_translator.collectDroppedCandiate(candidate_id,
1737                                                           location_id, name,
1738                                                           triage_translator_data,
1739                                                           reason="VF-server interfaces error")
1740             return None
1741         else:
1742             l_interfaces = v_server.get('l-interfaces').get('l-interface')
1743             l_interfaces_list = list()
1744
1745             for l_interface in l_interfaces:
1746                 vserver_interface = dict()
1747                 vserver_interface['interface-id'] = l_interface.get('interface-id')
1748                 vserver_interface['interface-name'] = l_interface.get('interface-name')
1749                 vserver_interface['macaddr'] = l_interface.get('macaddr')
1750                 vserver_interface['network-id'] = l_interface.get('network-name')
1751                 vserver_interface['network-name'] = ''
1752                 vserver_interface['ipv4-addresses'] = list()
1753                 vserver_interface['ipv6-addresses'] = list()
1754
1755                 if l_interface.get('l3-interface-ipv4-address-list'):
1756                     for ip_address_info in l_interface.get('l3-interface-ipv4-address-list'):
1757                         vserver_interface['ipv4-addresses']. \
1758                             append(ip_address_info.get('l3-interface-ipv4-address'))
1759
1760                 if l_interface.get('l3-interface-ipv6-address-list'):
1761                     for ip_address_info in l_interface.get('l3-interface-ipv6-address-list'):
1762                         vserver_interface['ipv6-addresses']. \
1763                             append(ip_address_info.get('l3-interface-ipv6-address'))
1764
1765                 l_interfaces_list.append(vserver_interface)
1766             return l_interfaces_list
1767
1768     @staticmethod
1769     def get_vnf_info(vnf):
1770         # some validation should happen
1771         vnf_info = dict()
1772         vnf_info['host_id'] = vnf.get("vnf-name")
1773         vnf_info['nf-name'] = vnf.get("vnf-name")
1774         vnf_info['nf-id'] = vnf.get("vnf-id")
1775         vnf_info['nf-type'] = 'vnf'
1776         vnf_info['vnf-type'] = vnf.get("vnf-type")
1777         vnf_info['ipv4-oam-address'] = vnf.get("ipv4-oam-address") if vnf.get("ipv4-oam-address") else ""
1778         vnf_info['ipv6-oam-address'] = vnf.get("ipv6-oam-address") if vnf.get("ipv6-oam-address") else ""
1779         return vnf_info
1780
1781     @staticmethod
1782     def resolve_cloud_for_region(region, region_id):
1783         cloud = dict()
1784         valid_keys = ['cloud_owner', 'cloud_region_version', 'location_id']
1785         for valid_key in valid_keys:
1786             cloud[valid_key] = region.get(valid_key) if not valid_key == 'location_id' else region_id
1787         cloud['location_type'] = 'att_aic'
1788         return cloud
1789
1790     @staticmethod
1791     def get_vf_module(vf_module):
1792         vf_module_info = dict()
1793         vf_module_info['vf-module-name'] = vf_module.get("vf-module-name")
1794         vf_module_info['vf-module-id'] = vf_module.get("vf-module-id")
1795         return vf_module_info
1796
1797     def get_vim_id(self, cloud_owner, cloud_region_id):
1798         if self.conf.HPA_enabled:
1799             return cloud_owner + '_' + cloud_region_id
1800
1801     @staticmethod
1802     def add_passthrough_attributes(candidate, passthrough_attributes, demand_name):
1803         if passthrough_attributes is None:
1804             return
1805         if len(passthrough_attributes.items()) > 0:
1806             candidate['passthrough_attributes'] = dict()
1807             for key, value in passthrough_attributes.items():
1808                 candidate['passthrough_attributes'][key] = value
1809
1810     def resolve_zone_for_vnf(self, candidate_id, location_id, vnf, name, triage_translator_data):
1811         related_to = "zone"
1812         zone_link = self._get_aai_rel_link(
1813             data=vnf, related_to=related_to)
1814         if not zone_link:
1815             LOG.error("Zone information not available from A&AI for transport candidates")
1816             self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1817                                                           name, triage_translator_data,
1818                                                           reason="Zone information not available from A&AI for "
1819                                                                  "transport candidates")
1820             return None
1821         zone_aai_path = self._get_aai_path_from_link(zone_link)
1822         response = self._request('get', path=zone_aai_path, data=None)
1823         if response is None or response.status_code != 200:
1824             self.triage_translator.collectDroppedCandiate(candidate_id, location_id, name,
1825                                                           triage_translator_data,
1826                                                           reason=response.status_code)
1827             return None
1828         body = response.json()
1829         return body
1830
1831     def match_region(self, candidate, restricted_region_id, restricted_complex_id, demand_name,
1832                      triage_translator_data):
1833         if self.match_candidate_attribute(
1834                 candidate,
1835                 "location_id",
1836                 restricted_region_id,
1837                 demand_name,
1838                 candidate.get('inventory_type')) or \
1839                 self.match_candidate_attribute(
1840                     candidate,
1841                     "physical_location_id",
1842                     restricted_complex_id,
1843                     demand_name,
1844                     candidate.get('inventory_type')):
1845             self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1846                                                           demand_name, triage_translator_data,
1847                                                           reason="candidate region does not match")
1848             return False
1849         else:
1850             return True
1851
1852     def match_candidate_by_list(self, candidate, candidates_list, exclude, demand_name, triage_translator_data):
1853         has_candidate = False
1854         if candidates_list:
1855             for list_candidate in candidates_list:
1856                 if list_candidate \
1857                         and list_candidate.get('inventory_type') \
1858                         == candidate.get('inventory_type'):
1859                     if isinstance(list_candidate.get('candidate_id'), list):
1860                         for candidate_id in list_candidate.get('candidate_id'):
1861                             if candidate_id == candidate.get('candidate_id'):
1862                                 has_candidate = True
1863                                 break
1864                     else:
1865                         raise Exception("Invalid candidate id list format")
1866                     if has_candidate:
1867                         break
1868
1869         if not exclude:
1870             if not has_candidate:
1871                 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1872                                                               demand_name, triage_translator_data,
1873                                                               reason="has_required_candidate candidate")
1874         elif has_candidate:
1875             self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1876                                                           demand_name, triage_translator_data,
1877                                                           reason="excluded candidate")
1878         return has_candidate
1879
1880     def get_nxi_candidates(self, filtering_attributes):
1881         raw_path = 'nodes/service-instances' + aai_utils.add_query_params_and_depth(filtering_attributes, "2")
1882         path = self._aai_versioned_path(raw_path)
1883         aai_response = self._request('get', path, data=None)
1884
1885         if aai_response is None or aai_response.status_code != 200:
1886             return None
1887         if aai_response.json():
1888             return aai_response.json()
1889
1890     def filter_nxi_candidates(self, response_body, filtering_attributes, default_attributes, candidate_uniqueness,
1891                               type):
1892         candidates = list()
1893         if response_body is not None:
1894             nxi_instances = response_body.get("service-instance", [])
1895
1896             for nxi_instance in nxi_instances:
1897                 inventory_attributes = aai_utils.get_inv_values_for_second_level_filter(filtering_attributes,
1898                                                                                         nxi_instance)
1899                 nxi_info = aai_utils.get_instance_info(nxi_instance)
1900                 if not filtering_attributes or \
1901                         self.match_inventory_attributes(filtering_attributes, inventory_attributes,
1902                                                         nxi_instance.get('service-instance-id')):
1903                     profile_instances = self.get_profile_instances(nxi_instance)
1904                     if type == 'nssi':
1905                         profiles = aai_utils.get_profiles(profile_instances, "slice-profile")
1906                         cost = self.conf.data.nssi_candidate_cost
1907                     elif type == 'nsi':
1908                         profiles = aai_utils.get_profiles(profile_instances, "service-profile")
1909                         cost = self.conf.data.nsi_candidate_cost
1910                     for profile in profiles:
1911                         profile_id = profile.get('profile-id')
1912                         info = Candidate.build_candidate_info('aai', type, cost, candidate_uniqueness, profile_id)
1913                         profile_info = aai_utils.convert_hyphen_to_under_score(profile)
1914                         nxi_candidate = NxI(instance_info=nxi_info, profile_info=profile_info, info=info,
1915                                             default_fields=aai_utils.convert_hyphen_to_under_score(default_attributes))
1916                         candidate = nxi_candidate.convert_nested_dict_to_dict()
1917                         candidates.append(candidate)
1918         return candidates
1919
1920     def get_profile_instances(self, nxi_instance):
1921         slice_role = nxi_instance['service-role']
1922         related_key = "allotted-resource" if slice_role == 'nsi' else 'service-instance'
1923         related_nodes = self._get_aai_rel_link_data(nxi_instance, related_key,
1924                                                     "service-instance.service-instance-id")
1925         profile_instances = []
1926         for node in related_nodes:
1927             profile_instance_id = node["d_value"]
1928             raw_path = f'nodes/service-instances/service-instance/{profile_instance_id}?depth=2'
1929             path = self._aai_versioned_path(raw_path)
1930             aai_response = self._request('get', path, data=None)
1931             if aai_response.status_code == 200 and aai_response.json():
1932                 profile_instances.append(aai_response.json())
1933
1934         return profile_instances
1935
1936     def get_nst_response(self, filtering_attributes):
1937         raw_path = 'service-design-and-creation/models' + aai_utils.add_query_params_and_depth(filtering_attributes,
1938                                                                                                "2")
1939         path = self._aai_versioned_path(raw_path)
1940         aai_response = self._request('get', path, data=None)
1941
1942         if aai_response is None or aai_response.status_code != 200:
1943             return None
1944         if aai_response.json():
1945             return aai_response.json()
1946
1947     def get_nst_candidates(self, response_body, filtering_attributes, default_attributes, candidate_uniqueness,
1948                            type):
1949         candidates = list()
1950         if response_body is not None:
1951             nst_metadatas = response_body.get("model", [])
1952
1953             for nst_metadata in nst_metadatas:
1954                 nst_info = aai_utils.get_nst_info(nst_metadata)
1955                 model_vers = nst_metadata.get('model-vers').get('model-ver')
1956                 for model_ver in model_vers:
1957                     model_version_id = model_ver.get('model-version-id')
1958                     cost = self.conf.data.nst_candidate_cost
1959                     info = Candidate.build_candidate_info('aai', type, cost, candidate_uniqueness, model_version_id)
1960                     model_version_obj = aai_utils.get_model_ver_info(model_ver)
1961                     model_ver_info = aai_utils.convert_hyphen_to_under_score(model_version_obj)
1962                     nst_candidate = NST(model_info=nst_info, model_ver=model_ver_info, info=info,
1963                                         default_fields=aai_utils.convert_hyphen_to_under_score(default_attributes),
1964                                         profile_info=None)
1965                     candidates.append(nst_candidate)
1966         return candidates