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