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