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