2 # -------------------------------------------------------------------------
3 # Copyright (c) 2015-2017 AT&T Intellectual Property
4 # Copyright (C) 2020 Wipro Limited.
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
18 # -------------------------------------------------------------------------
27 from oslo_config import cfg
28 from oslo_log import log
31 from conductor.common import rest
32 from conductor.data.plugins import constants
33 from conductor.data.plugins.inventory_provider import base
34 from conductor.data.plugins.inventory_provider.candidates.candidate import Candidate
35 from conductor.data.plugins.inventory_provider.candidates.cloud_candidate import Cloud
36 from conductor.data.plugins.inventory_provider.candidates.nst_candidate import NST
37 from conductor.data.plugins.inventory_provider.candidates.nxi_candidate import NxI
38 from conductor.data.plugins.inventory_provider.candidates.service_candidate import Service
39 from conductor.data.plugins.inventory_provider.candidates.transport_candidate import Transport
40 from conductor.data.plugins.inventory_provider.candidates.vfmodule_candidate import VfModule
41 from conductor.data.plugins.inventory_provider import hpa_utils
42 from conductor.data.plugins.inventory_provider.sdc import SDC
43 from conductor.data.plugins.inventory_provider.utils import aai_utils
44 from conductor.data.plugins.triage_translator.triage_translator import TraigeTranslator
45 from conductor.i18n import _LE
46 from conductor.i18n import _LI
48 LOG = log.getLogger(__name__)
53 cfg.IntOpt('cache_refresh_interval',
55 help='Interval with which to refresh the local cache, '
57 cfg.IntOpt('complex_cache_refresh_interval',
59 help='Interval with which to refresh the local complex cache, '
61 cfg.StrOpt('table_prefix',
63 help='Data Store table prefix.'),
64 cfg.StrOpt('server_url',
65 default='https://controller:8443/aai',
66 help='Base URL for A&AI, up to and not including '
67 'the version, and without a trailing slash.'),
68 cfg.StrOpt('aai_rest_timeout',
70 help='Timeout for A&AI Rest Call'),
71 cfg.StrOpt('aai_retries',
73 help='Number of retry for A&AI Rest Call'),
74 cfg.StrOpt('server_url_version',
76 help='The version of A&AI in v# format.'),
77 cfg.StrOpt('certificate_file',
78 default='certificate.pem',
79 help='SSL/TLS certificate file in pem format. '
80 'This certificate must be registered with the A&AI '
82 cfg.StrOpt('certificate_key_file',
83 default='certificate_key.pem',
84 help='Private Certificate Key file in pem format.'),
85 cfg.StrOpt('certificate_authority_bundle_file',
86 default='certificate_authority_bundle.pem',
87 help='Certificate Authority Bundle file in pem format. '
88 'Must contain the appropriate trust chain for the '
90 # TODO(larry): follow-up with ONAP people on this (AA&I basic auth username and password?)
91 cfg.StrOpt('username',
93 help='Username for AAI.'),
94 cfg.StrOpt('password',
96 help='Password for AAI.'),
99 CONF.register_opts(AAI_OPTS, group='aai')
102 class AAI(base.InventoryProviderBase):
103 """Active and Available Inventory Provider"""
108 # FIXME(jdandrea): Pass this in to init.
111 self.base = self.conf.aai.server_url.rstrip('/')
112 self.version = self.conf.aai.server_url_version.rstrip('/')
113 self.cert = self.conf.aai.certificate_file
114 self.key = self.conf.aai.certificate_key_file
115 self.verify = self.conf.aai.certificate_authority_bundle_file
116 self.cache_refresh_interval = self.conf.aai.cache_refresh_interval
117 self.last_refresh_time = None
118 self.complex_cache_refresh_interval = \
119 self.conf.aai.complex_cache_refresh_interval
120 self.complex_last_refresh_time = None
121 self.timeout = self.conf.aai.aai_rest_timeout
122 self.retries = self.conf.aai.aai_retries
123 self.username = self.conf.aai.username
124 self.password = self.conf.aai.password
125 self.triage_translator = TraigeTranslator()
127 # Cache is initially empty
129 self._aai_complex_cache = {}
131 def initialize(self):
133 """Perform any late initialization."""
134 # Initialize the Python requests
135 self._init_python_request()
137 # Refresh the cache once for now
138 self._refresh_cache()
140 # TODO(jdandrea): Make this periodic, and without a True condition!
141 # executor = futurist.ThreadPoolExecutor()
143 # fut = executor.submit(self.refresh_cache)
146 # # Now wait for the next time.
147 # # FIXME(jdandrea): Put inside refresh_cache()?
148 # refresh_interval = self.conf.aai.cache_refresh_interval
149 # time.sleep(refresh_interval)
150 # executor.shutdown()
153 """Return human-readable name."""
156 def invoke_method(self, arg):
157 if arg.pop('method_name') == "get_candidates_with_hpa":
158 return hpa_utils.get_candidates_with_hpa(arg)
161 def _get_version_from_string(string):
162 """Extract version number from string"""
163 return re.sub("[^0-9.]", "", string)
165 def _aai_versioned_path(self, path):
166 """Return a URL path with the A&AI version prepended"""
167 return '/{}/{}'.format(self.version, path.lstrip('/'))
169 def _request(self, method='get', path='/', data=None,
170 context=None, value=None):
171 """Performs HTTP request."""
173 'X-FromAppId': 'CONDUCTOR',
174 'X-TransactionId': str(uuid.uuid4()),
183 # TODO(jdandrea): Move timing/response logging into the rest helper?
184 start_time = time.time()
185 response = self.rest.request(**kwargs)
186 elapsed = time.time() - start_time
187 LOG.debug("Total time for A&AI request "
188 "({0:}: {1:}): {2:.3f} sec".format(context, value, elapsed))
191 LOG.error(_LE("No response from A&AI ({}: {})").
192 format(context, value))
193 elif response.status_code != 200:
194 LOG.error(_LE("A&AI request ({}: {}) returned HTTP "
195 "status {} {}, link: {}{}").
196 format(context, value,
197 response.status_code, response.reason,
201 def _init_python_request(self):
204 "server_url": self.base,
205 "retries": self.retries,
206 "username": self.username,
207 "password": self.password,
208 "cert_file": self.cert,
209 "cert_key_file": self.key,
210 "ca_bundle_file": self.verify,
211 "log_debug": self.conf.debug,
212 "read_timeout": self.timeout,
214 self.rest = rest.REST(**kwargs)
216 def _refresh_cache(self):
217 """Refresh the A&AI cache."""
218 if not self.last_refresh_time or \
219 (time.time() - self.last_refresh_time) > \
220 self.cache_refresh_interval * 60:
221 # TODO(jdandrea): This is presently brute force.
222 # It does not persist to Music. A general purpose ORM caching
223 # object likely needs to be made, with a key (hopefully we
224 # can use one that is not just a UUID), a value, and a
225 # timestamp. The other alternative is to not use the ORM
226 # layer and call the API directly, but that is
227 # also trading one set of todos for another ...
230 LOG.info(_LI("**** Refreshing A&AI cache *****"))
231 path = self._aai_versioned_path(
232 '/cloud-infrastructure/cloud-regions/?depth=0')
233 response = self._request(
234 path=path, context="cloud regions", value="all")
238 if response.status_code == 200:
239 body = response.json()
240 regions = body.get('cloud-region', {})
242 # Nothing to update the cache with
243 LOG.error(_LE("A&AI returned no regions, link: {}{}").
244 format(self.base, path))
250 for region in regions:
251 cloud_region_id = region.get('cloud-region-id')
253 LOG.debug("Working on region '{}' ".format(cloud_region_id))
255 cloud_region_version = region.get('cloud-region-version')
256 cloud_owner = region.get('cloud-owner')
257 cloud_type = region.get('cloud-type')
258 cloud_zone = region.get('cloud-zone')
260 physical_location_list = self._get_aai_rel_link_data(data=region, related_to='complex',
261 search_key='complex.physical-location-id')
262 if len(physical_location_list) > 0:
263 physical_location_id = physical_location_list[0].get('d_value')
265 if not (cloud_region_version and cloud_region_id):
267 rel_link_data_list = \
268 self._get_aai_rel_link_data(
270 related_to='complex',
271 search_key='complex.physical-location-id')
272 if len(rel_link_data_list) > 1:
273 LOG.error(_LE("Region {} has more than one complex").
274 format(cloud_region_id))
275 LOG.debug("Region {}: {}".format(cloud_region_id, region))
278 rel_link_data = rel_link_data_list[0]
279 complex_id = rel_link_data.get("d_value")
280 complex_link = rel_link_data.get("link")
281 if complex_id and complex_link:
282 complex_info = self._get_complex(
283 complex_link=complex_link,
284 complex_id=complex_id)
285 else: # no complex information
286 LOG.error(_LE("Region {} does not reference a complex").
287 format(cloud_region_id))
290 LOG.error(_LE("Region {}, complex {} info not found, "
291 "link {}").format(cloud_region_id,
292 complex_id, complex_link))
295 latitude = complex_info.get('latitude')
296 longitude = complex_info.get('longitude')
297 city = complex_info.get('city')
298 state = complex_info.get('state')
299 region = complex_info.get('region')
300 country = complex_info.get('country')
301 complex_name = complex_info.get('complex-name')
303 if not (latitude and longitude and city and country and complex_name):
304 keys = ('latitude', 'longitude', 'city', 'country',
307 list(set(keys).difference(
308 list(complex_info.keys()))) # Python 3 Conversion -- dict object to list object
309 LOG.error(_LE("Complex {} is missing {}, link: {}").
310 format(complex_id, missing_keys, complex_link))
311 LOG.debug("Complex {}: {}".
312 format(complex_id, complex_info))
315 cache['cloud_region'][cloud_region_id] = {
316 'cloud_region_version': cloud_region_version,
317 'cloud_owner': cloud_owner,
318 'cloud_type': cloud_type,
319 'cloud_zone': cloud_zone,
320 'complex_name': complex_name,
321 'physical_location_id': physical_location_id,
323 'complex_id': complex_id,
324 'complex_name': complex_name,
325 'latitude': latitude,
326 'longitude': longitude,
334 # Added for HPA support
335 if self.conf.HPA_enabled:
336 flavors = self._get_flavors(cloud_owner, cloud_region_id)
337 cache['cloud_region'][cloud_region_id]['flavors'] = flavors
339 LOG.debug("Candidate with cloud_region_id '{}' selected "
340 "as a potential candidate - ".format(cloud_region_id))
341 LOG.debug("Done with region '{}' ".format(cloud_region_id))
342 self._aai_cache = cache
343 self.last_refresh_time = time.time()
344 LOG.info(_LI("**** A&AI cache refresh complete *****"))
347 def _get_aai_rel_link(data, related_to):
348 """Given an A&AI data structure, return the related-to link"""
349 rel_dict = data.get('relationship-list')
351 for key, rel_list in rel_dict.items():
353 if related_to == rel.get('related-to'):
354 return rel.get('related-link')
357 def _get_aai_rel_link_data(data, related_to, search_key=None,
359 # some strings that we will encounter frequently
360 rel_lst = "relationship-list"
361 rkey = "relationship-key"
362 rval = "relationship-value"
363 rdata = "relationship-data"
366 m_key = match_dict.get('key')
367 m_value = match_dict.get('value')
371 rel_dict = data.get(rel_lst)
372 if rel_dict: # check if data has relationship lists
373 for key, rel_list in rel_dict.items():
375 if rel.get("related-to") == related_to:
378 link = rel.get("related-link")
379 r_data = rel.get(rdata, [])
382 if rd.get(rkey) == search_key:
384 if not match_dict: # return first match
386 {"link": link, "d_value": dval}
388 break # go to next relation
389 if rd.get(rkey) == m_key \
390 and rd.get(rval) == m_value:
392 if match_dict and matched: # if matching required
394 {"link": link, "d_value": dval}
396 # matched, return search value corresponding
397 # to the matched r_data group
398 else: # no search key; just return the link
400 {"link": link, "d_value": dval}
402 if len(response) == 0:
404 {"link": None, "d_value": None}
409 def check_sriov_automation(aic_version, demand_name, candidate_name):
410 """Check if specific candidate has SRIOV automation available or no"""
412 LOG.debug(_LI("Demand {}, candidate {} has an AIC version number {}").format(demand_name, candidate_name,
414 if aic_version == "X.Y":
418 def _get_complex(self, complex_link, complex_id=None):
420 if not self.complex_last_refresh_time or \
421 (time.time() - self.complex_last_refresh_time) > \
422 self.complex_cache_refresh_interval * 60:
423 self._aai_complex_cache.clear()
424 if complex_id and complex_id in self._aai_complex_cache:
425 return self._aai_complex_cache[complex_id]
427 path = self._aai_versioned_path(self._get_aai_path_from_link(complex_link))
428 response = self._request(path=path, context="complex", value=complex_id)
431 if response.status_code == 200:
432 complex_info = response.json()
433 if 'complex' in complex_info:
434 complex_info = complex_info.get('complex')
436 latitude = complex_info.get('latitude')
437 longitude = complex_info.get('longitude')
438 city = complex_info.get('city')
439 country = complex_info.get('country')
440 # removed the state check for countries in Europe that do not always enter states
441 if not (latitude and longitude and city and country):
442 keys = ('latitude', 'longitude', 'city', 'country')
444 list(set(keys).difference(set(complex_info.keys())))
445 LOG.error(_LE("Complex {} is missing {}, link: {}").
446 format(complex_id, missing_keys, complex_link))
447 LOG.debug("Complex {}: {}".format(complex_id, complex_info))
450 if complex_id: # cache only if complex_id is given
451 self._aai_complex_cache[complex_id] = response.json()
452 self.complex_last_refresh_time = time.time()
456 def _get_regions(self):
457 self._refresh_cache()
458 regions = self._aai_cache.get('cloud_region', {})
461 def _get_flavors(self, cloud_owner, cloud_region_id):
462 '''Fetch all flavors of a given cloud regions specified using {cloud-owner}/{cloud-region-id} composite key
464 :return flavors_info json object which list of flavor nodes and its children - HPACapabilities:
467 LOG.debug("Fetch all flavors and its child nodes HPACapabilities")
468 flavor_path = constants.FLAVORS_URI % (cloud_owner, cloud_region_id)
469 path = self._aai_versioned_path(flavor_path)
470 LOG.debug("Flavors path '{}' ".format(path))
472 response = self._request(path=path, context="flavors", value="all")
475 if response.status_code == 200:
476 flavors_info = response.json()
477 if not flavors_info or not flavors_info["flavor"] or \
478 len(flavors_info["flavor"]) == 0:
479 LOG.error(_LE("Flavor is missing in Cloud-Region {}/{}").
480 format(cloud_owner, cloud_region_id))
482 LOG.debug(flavors_info)
483 # Remove extraneous flavor information
486 LOG.error(_LE("Received Error while fetching flavors from Cloud-region {}/{}").format(cloud_owner,
490 def _get_aai_path_from_link(self, link):
491 path = link.split(self.version, 1)
492 if not path or len(path) <= 1:
493 # TODO(shankar): Treat this as a critical error?
494 LOG.error(_LE("A&AI version {} not found in link {}").
495 format(self.version, link))
497 return "{}".format(path[1])
499 def check_candidate_role(self, host_id=None):
501 vnf_name_uri = '/network/generic-vnfs/?vnf-name=' + host_id + '&depth=0'
502 path = self._aai_versioned_path(vnf_name_uri)
504 response = self._request('get', path=path, data=None,
507 if response is None or not response.ok:
509 body = response.json()
511 generic_vnf = body.get("generic-vnf", [])
513 for vnf in generic_vnf:
514 related_to = "service-instance"
515 search_key = "customer.global-customer-id"
516 rl_data_list = self._get_aai_rel_link_data(
518 related_to=related_to,
519 search_key=search_key,
522 if len(rl_data_list) != 1:
525 rl_data = rl_data_list[0]
526 candidate_role_link = rl_data.get("link")
528 if not candidate_role_link:
529 LOG.error(_LE("Unable to get candidate role link for host id {} ").format(host_id))
532 candidate_role_path = self._get_aai_path_from_link(candidate_role_link) + '/allotted-resources?depth=all'
533 path = self._aai_versioned_path(candidate_role_path)
535 response = self._request('get', path=path, data=None,
536 context="candidate role")
538 if response is None or not response.ok:
540 body = response.json()
542 response_items = body.get('allotted-resource')
543 if len(response_items) > 0:
544 role = response_items[0].get('role')
547 def check_network_roles(self, network_role_id=None):
548 # the network role query from A&AI is not using
549 # the version number in the query
551 '/network/l3-networks?network-role=' + network_role_id
552 path = self._aai_versioned_path(network_role_uri)
554 # This UUID is reserved by A&AI for a Conductor-specific named query.
555 named_query_uid = "role-UUID"
558 "query-parameters": {
560 "named-query-uuid": named_query_uid
563 "instance-filters": {
567 "network-role": network_role_id
574 response = self._request('get', path=path, data=data,
575 context="role", value=network_role_id)
578 body = response.json()
580 response_items = body.get('l3-network', [])
582 for item in response_items:
583 cloud_region_instances = self._get_aai_rel_link_data(
585 related_to='cloud-region',
586 search_key='cloud-region.cloud-region-id'
589 if len(cloud_region_instances) > 0:
590 for r_instance in cloud_region_instances:
591 region_id = r_instance.get('d_value')
592 if region_id is not None:
593 region_ids.add(region_id)
595 # return region ids that fit the role
598 def resolve_host_location(self, host_name):
599 path = self._aai_versioned_path('/query?format=id')
600 data = {"start": ["network/pnfs/pnf/" + host_name,
601 "cloud-infrastructure/pservers/pserver/" + host_name],
602 "query": "query/ucpe-instance"
604 response = self._request('put', path=path, data=data,
605 context="host name", value=host_name)
606 if response is None or response.status_code != 200:
608 body = response.json()
609 results = body.get('results', [])
611 for result in results:
612 if "resource-type" in result and \
613 "resource-link" in result and \
614 result["resource-type"] == "complex":
615 complex_link = result["resource-link"]
617 LOG.error(_LE("Unable to get a complex link for hostname {} "
618 " in response {}").format(host_name, response))
620 complex_info = self._get_complex(
621 complex_link=complex_link,
625 lat = complex_info.get('latitude')
626 lon = complex_info.get('longitude')
627 country = complex_info.get('country')
629 location = {"latitude": lat, "longitude": lon}
630 location["country"] = country if country else None
633 LOG.error(_LE("Unable to get a latitude and longitude "
634 "information for hostname {} from complex "
635 " link {}").format(host_name, complex_link))
638 LOG.error(_LE("Unable to get a complex information for "
639 " hostname {} from complex "
640 " link {}").format(host_name, complex_link))
643 def resolve_clli_location(self, clli_name):
644 clli_uri = '/cloud-infrastructure/complexes/complex/' + clli_name
645 path = self._aai_versioned_path(clli_uri)
647 response = self._request('get', path=path, data=None,
648 context="clli name", value=clli_name)
649 if response is None or response.status_code != 200:
652 body = response.json()
655 lat = body.get('latitude')
656 lon = body.get('longitude')
657 country = body.get('country')
659 location = {"latitude": lat, "longitude": lon}
660 location["country"] = country if country else None
663 LOG.error(_LE("Unable to get a latitude and longitude "
664 "information for CLLI code {} from complex").
668 LOG.error(_LE("Unable to get a complex information for "
669 " clli {} from complex "
670 " link {}").format(clli_name, clli_uri))
673 def get_inventory_group_pairs(self, service_description):
675 path = self._aai_versioned_path(
676 '/network/instance-groups/?description={}&depth=0'.format(
677 service_description))
678 response = self._request(path=path, context="inventory group",
679 value=service_description)
680 if response is None or response.status_code != 200:
682 body = response.json()
683 if "instance-group" not in body:
684 LOG.error(_LE("Unable to get instance groups from inventory "
685 " in response {}").format(response))
687 for instance_groups in body["instance-group"]:
688 s_instances = self._get_aai_rel_link_data(
689 data=instance_groups,
690 related_to='service-instance',
691 search_key='service-instance.service-instance-id'
693 if s_instances and len(s_instances) == 2:
695 for s_inst in s_instances:
696 pair.append(s_inst.get('d_value'))
699 LOG.error(_LE("Number of instance pairs not found to "
700 "be two: {}").format(instance_groups))
703 def _log_multiple_item_error(self, name, service_type,
704 related_to, search_key='',
705 context=None, value=None):
706 """Helper method to log multiple-item errors
708 Used by resolve_demands
710 LOG.error(_LE("Demand {}, role {} has more than one {} ({})").
711 format(name, service_type, related_to, search_key))
712 if context and value:
713 LOG.debug("{} details: {}".format(context, value))
715 def match_candidate_attribute(self, candidate, attribute_name,
716 restricted_value, demand_name,
718 """Check if specific candidate attribute matches the restricted value
720 Used by resolve_demands
722 if restricted_value and restricted_value != '' and candidate[attribute_name] != restricted_value:
723 LOG.info(_LI("Demand: {} "
724 "Discarded {} candidate as "
725 "it doesn't match the "
727 "{} ").format(demand_name,
736 def match_vserver_attribute(self, vserver_list):
739 for i in range(0, len(vserver_list)):
741 value != vserver_list[i].get('d_value'):
743 value = vserver_list[i].get('d_value')
746 def match_inventory_attributes(self, template_attributes,
747 inventory_attributes, candidate_id):
749 for attribute_key, attribute_values in template_attributes.items():
751 if attribute_key and \
752 (attribute_key == 'service-type' or attribute_key == 'equipment-role'
753 or attribute_key == 'model-invariant-id' or attribute_key == 'model-version-id'):
757 if type(attribute_values) is dict:
758 if 'any' in attribute_values:
759 attribute_values = attribute_values['any']
760 elif 'not' in attribute_values:
762 attribute_values = attribute_values['not']
764 if match_type == 'any':
765 if attribute_key not in inventory_attributes or \
766 (len(attribute_values) > 0 and inventory_attributes[attribute_key] and
767 inventory_attributes[attribute_key] not in attribute_values):
769 elif match_type == 'not':
770 # drop the candidate when
771 # 1)field exists in AAI and 2)value is not null or empty 3)value is one of those in the 'not' list
772 # Remember, this means if the property is not returned at all from AAI, that still can be a candidate.
773 if attribute_key in inventory_attributes and \
774 inventory_attributes[attribute_key] and \
775 inventory_attributes[attribute_key] in attribute_values:
780 def first_level_service_call(self, path, name, service_type):
782 response = self._request(
783 path=path, context="demand, GENERIC-VNF role",
784 value="{}, {}".format(name, service_type))
785 if response is None or response.status_code != 200:
786 return list() # move ahead with next requirement
787 body = response.json()
788 return body.get("generic-vnf", [])
790 def resolve_v_server_for_candidate(self, candidate_id, location_id, vs_link, add_interfaces, demand_name,
791 triage_translator_data):
793 LOG.error(_LE("{} VSERVER link information not "
794 "available from A&AI").format(demand_name))
795 self.triage_translator.collectDroppedCandiate(candidate_id,
796 location_id, demand_name,
797 triage_translator_data,
798 reason="VSERVER link information not")
799 return None # move ahead with the next vnf
802 vs_link = vs_link + '?depth=2'
803 vs_path = self._get_aai_path_from_link(vs_link)
805 LOG.error(_LE("{} VSERVER path information not "
806 "available from A&AI - {}").
807 format(demand_name, vs_path))
808 self.triage_translator.collectDroppedCandiate(candidate_id,
809 location_id, demand_name,
810 triage_translator_data,
811 reason="VSERVER path information not available from A&AI")
812 return None # move ahead with the next vnf
813 path = self._aai_versioned_path(vs_path)
814 response = self._request(
815 path=path, context="demand, VSERVER",
816 value="{}, {}".format(demand_name, vs_path))
817 if response is None or response.status_code != 200:
818 self.triage_translator.collectDroppedCandiate(candidate_id,
819 location_id, demand_name,
820 triage_translator_data,
821 reason=response.status_code)
823 return response.json()
825 def resolve_vf_modules_for_generic_vnf(self, candidate_id, location_id, vnf, demand_name, triage_translator_data):
826 raw_path = '/network/generic-vnfs/generic-vnf/{}?depth=1'.format(vnf.get("vnf-id"))
827 path = self._aai_versioned_path(raw_path)
829 response = self._request('get', path=path, data=None)
830 if response is None or response.status_code != 200:
831 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
832 triage_translator_data, reason=response)
834 generic_vnf_details = response.json()
836 if generic_vnf_details is None or not generic_vnf_details.get('vf-modules') \
837 or not generic_vnf_details.get('vf-modules').get('vf-module'):
838 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
839 triage_translator_data,
840 reason="Generic-VNF No detailed data for VF-modules")
843 return generic_vnf_details.get('vf-modules').get('vf-module')
845 def resolve_cloud_regions_by_cloud_region_id(self, cloud_region_id):
846 cloud_region_uri = '/cloud-infrastructure/cloud-regions' \
847 '/?cloud-region-id=' \
849 path = self._aai_versioned_path(cloud_region_uri)
851 response = self._request('get',
854 if response is None or response.status_code != 200:
857 body = response.json()
858 return body.get('cloud-region', [])
860 def assign_candidate_existing_placement(self, candidate, existing_placement):
862 """Assign existing_placement and cost parameters to candidate
864 Used by resolve_demands
866 candidate['existing_placement'] = 'false'
867 if existing_placement:
868 if existing_placement.get('candidate_id') == candidate['candidate_id']:
869 candidate['cost'] = self.conf.data.existing_placement_cost
870 candidate['existing_placement'] = 'true'
872 def resovle_conflict_id(self, conflict_identifier, candidate):
874 # Initialize the conflict_id_list
875 conflict_id_list = list()
876 # conflict_id is separated by pipe (|)
879 for conflict_element in conflict_identifier:
880 # if the conflict_element is a dictionary with key = 'get_candidate_attribute',
881 # then add candidate's coressponding value to conflict_id string
882 if isinstance(conflict_element, dict) and 'get_candidate_attribute' in conflict_element:
883 attribute_name = conflict_element.get('get_candidate_attribute')
884 conflict_id_list.append(candidate[attribute_name] + separator)
885 elif isinstance(conflict_element, unicode):
886 conflict_id_list.append(conflict_element + separator)
888 return ''.join(conflict_id_list)
890 def resolve_v_server_links_for_vnf(self, vnf):
891 related_to = "vserver"
892 search_key = "cloud-region.cloud-owner"
893 rl_data_list = self._get_aai_rel_link_data(
894 data=vnf, related_to=related_to,
895 search_key=search_key)
896 vs_link_list = list()
897 for i in range(0, len(rl_data_list)):
898 vs_link_list.append(rl_data_list[i].get('link'))
901 def resolve_complex_info_link_for_v_server(self, candidate_id, v_server, cloud_owner, cloud_region_id,
902 service_type, demand_name, triage_translator_data):
903 related_to = "pserver"
904 rl_data_list = self._get_aai_rel_link_data(
906 related_to=related_to,
909 if len(rl_data_list) > 1:
910 self._log_multiple_item_error(
911 demand_name, service_type, related_to, "item",
913 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
914 triage_translator_data, reason="item VSERVER")
916 rl_data = rl_data_list[0]
917 ps_link = rl_data.get('link')
919 # Third level query to get cloud region from pserver
921 LOG.error(_LE("{} pserver related link "
922 "not found in A&AI: {}").
923 format(demand_name, rl_data))
924 # if HPA_feature is disabled
925 if not self.conf.HPA_enabled:
926 # Triage Tool Feature Changes
927 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
928 triage_translator_data, reason="ps link not found")
931 if not (cloud_owner and cloud_region_id):
932 LOG.error("{} cloud-owner or cloud-region not "
933 "available from A&AI".
935 # Triage Tool Feature Changes
936 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
937 triage_translator_data,
938 reason="Cloud owner and cloud region "
940 return None # move ahead with the next vnf
942 '/cloud-infrastructure/cloud-regions/cloud-region' \
943 '/?cloud-owner=' + cloud_owner \
944 + '&cloud-region-id=' + cloud_region_id
945 path = self._aai_versioned_path(cloud_region_uri)
946 response = self._request('get',
949 if response is None or response.status_code != 200:
950 # Triage Tool Feature Changes
951 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
952 triage_translator_data, reason=response)
954 body = response.json()
956 ps_path = self._get_aai_path_from_link(ps_link)
958 LOG.error(_LE("{} pserver path information "
959 "not found in A&AI: {}").
960 format(demand_name, ps_link))
961 # Triage Tool Feature Changes
962 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
963 triage_translator_data, reason="ps path not found")
964 return None # move ahead with the next vnf
965 path = self._aai_versioned_path(ps_path)
966 response = self._request(
967 path=path, context="PSERVER", value=ps_path)
968 if response is None or response.status_code != 200:
969 # Triage Tool Feature Changes
970 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
971 triage_translator_data, reason=response)
973 body = response.json()
975 related_to = "complex"
976 search_key = "complex.physical-location-id"
977 rl_data_list = self._get_aai_rel_link_data(
979 related_to=related_to,
980 search_key=search_key
982 if len(rl_data_list) > 1:
983 if not self.match_vserver_attribute(rl_data_list):
984 self._log_multiple_item_error(
985 demand_name, service_type, related_to, search_key, "PSERVER", body)
986 self.triage_translator.collectDroppedCandiate(candidate_id, cloud_region_id, demand_name,
987 triage_translator_data, reason="PSERVER error")
989 return rl_data_list[0]
991 def resolve_cloud_for_vnf(self, candidate_id, location_id, vnf, service_type, demand_name, triage_translator_data):
992 related_to = "vserver"
993 search_keys = ["cloud-region.cloud-owner", "cloud-region.cloud-region-id"]
995 for search_key in search_keys:
996 rl_data_list = self._get_aai_rel_link_data(
997 data=vnf, related_to=related_to,
998 search_key=search_key)
1000 if len(rl_data_list) > 1:
1001 if not self.match_vserver_attribute(rl_data_list):
1002 self._log_multiple_item_error(
1003 demand_name, service_type, related_to, search_key,
1005 self.triage_translator.collectDroppedCandiate(candidate_id,
1006 location_id, demand_name,
1007 triage_translator_data,
1010 cloud_info[search_key.split(".")[1].replace('-', '_')] = rl_data_list[0].get('d_value') if rl_data_list[
1012 cloud_info['cloud_region_version'] = self.get_cloud_region_version(cloud_info['cloud_region_id'])
1013 cloud_info['location_type'] = 'att_aic'
1014 cloud_info['location_id'] = cloud_info.pop('cloud_region_id')
1017 def get_cloud_region_version(self, cloud_region_id):
1019 regions = self.resolve_cloud_regions_by_cloud_region_id(cloud_region_id)
1022 for region in regions:
1023 if "cloud-region-version" in region:
1024 return self._get_version_from_string(region["cloud-region-version"])
1026 def resolve_global_customer_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1027 demand_name, triage_translator_data):
1028 related_to = "service-instance"
1029 search_key = "customer.global-customer-id"
1030 match_key = "customer.global-customer-id"
1031 rl_data_list = self._get_aai_rel_link_data(
1033 related_to=related_to,
1034 search_key=search_key,
1035 match_dict={'key': match_key,
1036 'value': customer_id}
1038 if len(rl_data_list) > 1:
1039 if not self.match_vserver_attribute(rl_data_list):
1040 self._log_multiple_item_error(
1041 demand_name, service_type, related_to, search_key, "VNF", vnf)
1042 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1043 demand_name, triage_translator_data,
1044 reason=" match_vserver_attribute generic-vnf")
1046 return rl_data_list[0]
1048 def resolve_service_instance_id_for_vnf(self, candidate_id, location_id, vnf, customer_id, service_type,
1049 demand_name, triage_translator_data):
1050 related_to = "service-instance"
1051 search_key = "service-instance.service-instance-id"
1052 match_key = "customer.global-customer-id"
1053 rl_data_list = self._get_aai_rel_link_data(
1055 related_to=related_to,
1056 search_key=search_key,
1057 match_dict={'key': match_key,
1058 'value': customer_id}
1060 if len(rl_data_list) > 1:
1061 if not self.match_vserver_attribute(rl_data_list):
1062 self._log_multiple_item_error(
1063 demand_name, service_type, related_to, search_key, "VNF", vnf)
1064 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1065 demand_name, triage_translator_data,
1066 reason="multiple_item_error generic-vnf")
1068 return rl_data_list[0]
1070 def build_complex_info_for_candidate(self, candidate_id, location_id, vnf, complex_list, service_type, demand_name,
1071 triage_translator_data):
1072 if not complex_list or \
1073 len(complex_list) < 1:
1074 LOG.error("Complex information not "
1075 "available from A&AI")
1076 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1077 triage_translator_data,
1078 reason="Complex information not available from A&AI")
1081 # In the scenario where no pserver information is available
1082 # assumption here is that cloud-region does not span across
1083 # multiple complexes
1084 if len(complex_list) > 1:
1085 related_to = "complex"
1086 search_key = "complex.physical-location-id"
1087 if not self.match_vserver_attribute(complex_list):
1088 self._log_multiple_item_error(
1089 demand_name, service_type, related_to, search_key,
1091 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1092 triage_translator_data,
1093 reason="Generic-vnf error")
1096 rl_data = complex_list[0]
1097 complex_link = rl_data.get('link')
1098 complex_id = rl_data.get('d_value')
1100 # Final query for the complex information
1101 if not (complex_link and complex_id):
1102 LOG.debug("{} complex information not "
1103 "available from A&AI - {}".
1104 format(demand_name, complex_link))
1105 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1106 triage_translator_data,
1107 reason="Complex information not available from A&AI")
1108 return # move ahead with the next vnf
1110 complex_info = self._get_complex(
1111 complex_link=complex_link,
1112 complex_id=complex_id
1114 if not complex_info:
1115 LOG.debug("{} complex information not "
1116 "available from A&AI - {}".
1117 format(demand_name, complex_link))
1118 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, demand_name,
1119 triage_translator_data,
1120 reason="Complex information not available from A&AI")
1121 return # move ahead with the next vnf
1123 complex_info = self.build_complex_dict(complex_info, '')
1126 def resolve_demands(self, demands, plan_info, triage_translator_data):
1127 """Resolve demands into inventory candidate lists"""
1129 self.triage_translator.getPlanIdNAme(plan_info['plan_name'], plan_info['plan_id'], triage_translator_data)
1131 resolved_demands = {}
1132 for name, requirements in demands.items():
1133 self.triage_translator.addDemandsTriageTranslator(name, triage_translator_data)
1134 resolved_demands[name] = []
1135 for requirement in requirements:
1136 inventory_type = requirement.get('inventory_type').lower()
1137 service_subscription = requirement.get('service_subscription')
1138 candidate_uniqueness = requirement.get('unique', 'true')
1139 filtering_attributes = requirement.get('filtering_attributes')
1140 passthrough_attributes = requirement.get('passthrough_attributes')
1141 default_attributes = requirement.get('default_attributes')
1142 # TODO(XYZ): may need to support multiple service_type and customer_id in the futrue
1144 # TODO(XYZ): make it consistent for dash and underscore
1145 if filtering_attributes:
1146 # catch equipment-role and service-type from template
1147 equipment_role = filtering_attributes.get('equipment-role')
1148 service_type = filtering_attributes.get('service-type')
1150 service_type = equipment_role
1151 # catch global-customer-id and customer-id from template
1152 global_customer_id = filtering_attributes.get('global-customer-id')
1153 customer_id = filtering_attributes.get('customer-id')
1154 if global_customer_id:
1155 customer_id = global_customer_id
1157 model_invariant_id = filtering_attributes.get('model-invariant-id')
1158 model_version_id = filtering_attributes.get('model-version-id')
1159 service_role = filtering_attributes.get('service-role')
1162 service_type = equipment_role = requirement.get('service_type')
1163 customer_id = global_customer_id = requirement.get('customer_id')
1164 # region_id is OPTIONAL. This will restrict the initial
1165 # candidate set to come from the given region id
1166 restricted_region_id = requirement.get('region')
1167 restricted_complex_id = requirement.get('complex')
1168 # Used for order locking feature
1169 # by defaut, conflict id is the combination of candidate id, service type and vnf-e2e-key
1170 conflict_identifier = requirement.get('conflict_identifier')
1172 vlan_key = requirement.get('vlan_key')
1173 port_key = requirement.get('port_key')
1175 # get required candidates from the demand
1176 required_candidates = requirement.get("required_candidates")
1178 # get existing_placement from the demand
1179 existing_placement = requirement.get("existing_placement")
1181 if required_candidates:
1182 resolved_demands['required_candidates'] = \
1185 # get excluded candidate from the demand
1186 excluded_candidates = requirement.get("excluded_candidates")
1188 # service_resource_id is OPTIONAL and is
1189 # transparent to Conductor
1190 service_resource_id = requirement.get('service_resource_id') \
1191 if requirement.get('service_resource_id') else ''
1193 if inventory_type == 'cloud':
1194 # load region candidates from cache
1195 regions = self._get_regions()
1196 if not regions or len(regions) < 1:
1197 LOG.debug("Region information is not available in cache")
1198 for region_id, region in regions.items():
1199 # Pick only candidates from the restricted_region
1200 info = Candidate.build_candidate_info('aai', inventory_type,
1201 self.conf.data.cloud_candidate_cost,
1202 candidate_uniqueness, region_id, service_resource_id)
1203 cloud = self.resolve_cloud_for_region(region, region_id)
1204 complex_info = self.build_complex_dict(region['complex'], inventory_type)
1205 flavors = self.resolve_flavors_for_region(region['flavors'])
1207 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1208 if self.check_sriov_automation(cloud['cloud_region_version'], name, info['candidate_id']):
1209 other['sriov_automation'] = 'true'
1211 other['sriov_automation'] = 'false'
1212 cloud_candidate = Cloud(info=info, cloud_region=cloud, complex=complex_info, flavors=flavors,
1213 additional_fields=other)
1214 candidate = cloud_candidate.convert_nested_dict_to_dict()
1216 cloud_region_attr = dict()
1217 cloud_region_attr['cloud-owner'] = region['cloud_owner']
1218 cloud_region_attr['cloud-region-version'] = region['cloud_region_version']
1219 cloud_region_attr['cloud-type'] = region['cloud_type']
1220 cloud_region_attr['cloud-zone'] = region['cloud_zone']
1221 cloud_region_attr['complex-name'] = region['complex_name']
1222 cloud_region_attr['physical-location-id'] = region['physical_location_id']
1224 if filtering_attributes and (not self.match_inventory_attributes(filtering_attributes,
1226 candidate['candidate_id'])):
1227 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1228 candidate['location_id'], name,
1229 triage_translator_data,
1230 reason='attributes and match invetory '
1234 if conflict_identifier:
1235 candidate['conflict_id'] = self.resovle_conflict_id(conflict_identifier, candidate)
1237 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1238 triage_translator_data):
1241 self.assign_candidate_existing_placement(candidate, existing_placement)
1243 # Pick only candidates not in the excluded list, if excluded candidate list is provided
1244 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1245 name, triage_translator_data):
1248 # Pick only candidates in the required list, if required candidate list is provided
1249 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1251 triage_translator_data):
1254 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1255 # add candidate to demand candidates
1256 resolved_demands[name].append(candidate)
1257 LOG.debug(">>>>>>> Candidate <<<<<<<")
1258 LOG.debug(json.dumps(candidate, indent=4))
1260 elif (inventory_type == 'service') and customer_id:
1261 # First level query to get the list of generic vnfs
1262 vnf_by_model_invariant = list()
1263 if filtering_attributes and model_invariant_id:
1265 raw_path = '/network/generic-vnfs/' \
1266 '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1267 if model_version_id:
1268 raw_path = '/network/generic-vnfs/' \
1269 '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1271 path = self._aai_versioned_path(raw_path)
1272 vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1274 vnf_by_service_type = list()
1275 if service_type or equipment_role:
1276 path = self._aai_versioned_path(
1277 '/network/generic-vnfs/'
1278 '?equipment-role={}&depth=0'.format(service_type))
1279 vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1281 generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1284 for vnf in generic_vnf:
1285 # if this vnf already appears, skip it
1286 vnf_id = vnf.get('vnf-id')
1287 if vnf_id in vnf_dict:
1289 # add vnf (with vnf_id as key) to the dictionary
1290 vnf_dict[vnf_id] = vnf
1292 vnf_info['host_id'] = vnf.get("vnf-name")
1293 vlan_info = self.build_vlan_info(vlan_key, port_key)
1294 cloud = self.resolve_cloud_for_vnf('', '', vnf, service_type, name, triage_translator_data)
1295 if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1296 cloud['cloud_region_version'] is None:
1299 rl_data = self.resolve_global_customer_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1300 service_type, name, triage_translator_data)
1304 vs_cust_id = rl_data.get('d_value')
1305 rl_data = self.resolve_service_instance_id_for_vnf('', cloud['location_id'], vnf, customer_id,
1306 service_type, name, triage_translator_data)
1310 vs_service_instance_id = rl_data.get('d_value')
1313 if vs_cust_id and vs_cust_id == customer_id:
1314 info = Candidate.build_candidate_info('aai', inventory_type,
1315 self.conf.data.service_candidate_cost,
1316 candidate_uniqueness, vs_service_instance_id,
1317 service_resource_id)
1318 else: # vserver is for a different customer
1319 self.triage_translator.collectDroppedCandiate('', cloud['location_id'], name,
1320 triage_translator_data,
1321 reason="vserver is for a different customer")
1323 # Added vim-id for short-term workaround
1325 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1326 other['sriov_automation'] = 'true' if self.check_sriov_automation(
1327 cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1329 # Second level query to get the pserver from vserver
1330 complex_list = list()
1331 for complex_link in self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'], cloud,
1333 triage_translator_data,
1335 complex_list.append(complex_link[1])
1336 complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1337 cloud['location_id'], vnf,
1338 complex_list, service_type, name,
1339 triage_translator_data)
1340 if "complex_name" not in complex_info:
1343 service_candidate = Service(info=info, cloud_region=cloud, complex=complex_info,
1344 generic_vnf=vnf_info, additional_fields=other, vlan=vlan_info)
1345 candidate = service_candidate.convert_nested_dict_to_dict()
1347 # add specifal parameters for comparsion
1348 vnf['global-customer-id'] = customer_id
1349 vnf['customer-id'] = customer_id
1350 vnf['cloud-region-id'] = cloud.get('cloud_region_id')
1351 vnf['physical-location-id'] = complex_info.get('physical_location_id')
1353 if filtering_attributes and not self.match_inventory_attributes(filtering_attributes, vnf,
1354 candidate['candidate_id']):
1355 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1356 candidate['location_id'], name,
1357 triage_translator_data,
1358 reason="attibute check error")
1360 self.assign_candidate_existing_placement(candidate, existing_placement)
1362 # Pick only candidates not in the excluded list
1363 # if excluded candidate list is provided
1364 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
1365 name, triage_translator_data):
1368 # Pick only candidates in the required list
1369 # if required candidate list is provided
1370 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1372 triage_translator_data):
1375 # add the candidate to the demand
1376 # Pick only candidates from the restricted_region
1377 # or restricted_complex
1378 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1379 triage_translator_data):
1382 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1383 resolved_demands[name].append(candidate)
1384 LOG.debug(">>>>>>> Candidate <<<<<<<")
1385 LOG.debug(json.dumps(candidate, indent=4))
1387 elif (inventory_type == 'vfmodule') and customer_id:
1389 # First level query to get the list of generic vnfs
1390 vnf_by_model_invariant = list()
1391 if filtering_attributes and model_invariant_id:
1393 raw_path = '/network/generic-vnfs/' \
1394 '?model-invariant-id={}&depth=0'.format(model_invariant_id)
1395 if model_version_id:
1396 raw_path = '/network/generic-vnfs/' \
1397 '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
1399 path = self._aai_versioned_path(raw_path)
1400 vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
1402 vnf_by_service_type = list()
1403 if service_type or equipment_role:
1404 path = self._aai_versioned_path('/network/generic-vnfs/'
1405 '?equipment-role={}&depth=0'.format(service_type))
1406 vnf_by_service_type = self.first_level_service_call(path, name, service_type)
1408 generic_vnf = vnf_by_model_invariant + vnf_by_service_type
1411 for vnf in generic_vnf:
1412 # if this vnf already appears, skip it
1413 vnf_id = vnf.get('vnf-id')
1414 if vnf_id in vnf_dict:
1416 # add vnf (with vnf_id as key) to the dictionary
1417 vnf_dict[vnf_id] = vnf
1420 info = Candidate.build_candidate_info('aai', inventory_type,
1421 self.conf.data.service_candidate_cost,
1422 candidate_uniqueness, "", service_resource_id)
1424 vlan_info = self.build_vlan_info(vlan_key, port_key)
1426 vnf_info = self.get_vnf_info(vnf)
1428 rl_data = self.resolve_global_customer_id_for_vnf('', '', vnf, customer_id,
1429 service_type, name, triage_translator_data)
1433 vs_cust_id = rl_data.get('d_value')
1435 rl_data = self.resolve_service_instance_id_for_vnf('', '', vnf, customer_id,
1436 service_type, name, triage_translator_data)
1440 vs_service_instance_id = rl_data.get('d_value')
1442 service_info = dict()
1443 if vs_cust_id and vs_cust_id == customer_id:
1444 service_info['service_instance_id'] = vs_service_instance_id
1445 else: # vserver is for a different customer
1446 self.triage_translator.collectDroppedCandiate('', '', name,
1447 triage_translator_data,
1448 reason="candidate is for a different"
1452 vf_modules_list = self.resolve_vf_modules_for_generic_vnf('', '', vnf, name,
1453 triage_translator_data)
1454 if vf_modules_list is None:
1457 for vf_module in vf_modules_list:
1458 # for vfmodule demands we allow to have vfmodules from different cloud regions
1459 info['candidate_id'] = vf_module.get("vf-module-id")
1460 vf_module_info = self.get_vf_module(vf_module)
1461 cloud = self.resolve_cloud_for_vnf(info['candidate_id'], '', vf_module, service_type, name,
1462 triage_translator_data)
1463 if cloud['location_id'] is None or cloud['cloud_owner'] is None or \
1464 cloud['cloud_region_version'] is None:
1467 # OTHER - Added vim-id for short-term workaround
1469 other['vim-id'] = self.get_vim_id(cloud['cloud_owner'], cloud['location_id'])
1470 other['sriov_automation'] = 'true' if self.check_sriov_automation(
1471 cloud['cloud_region_version'], name, info['candidate_id']) else 'false'
1473 # Second level query to get the pserver from vserver
1474 vserver_info = dict()
1475 vserver_info['vservers'] = list()
1476 complex_list = list()
1477 for v_server, complex_link in \
1478 self.resolve_v_server_and_complex_link_for_vnf(info['candidate_id'],
1480 triage_translator_data,
1482 complex_list.append(complex_link)
1483 candidate_vserver = dict()
1484 candidate_vserver['vserver-id'] = v_server.get('vserver-id')
1485 candidate_vserver['vserver-name'] = v_server.get('vserver-name')
1486 l_interfaces = self.get_l_interfaces_from_vserver(info['candidate_id'],
1487 cloud['location_id'],
1489 triage_translator_data)
1491 candidate_vserver['l-interfaces'] = l_interfaces
1494 vserver_info['vservers'].append(candidate_vserver)
1497 complex_info = self.build_complex_info_for_candidate(info['candidate_id'],
1498 cloud['location_id'], vnf,
1499 complex_list, service_type, name,
1500 triage_translator_data)
1501 if complex_info.get("complex_name") is None:
1504 vf_module_candidate = VfModule(complex=complex_info, info=info, generic_vnf=vnf_info,
1505 cloud_region=cloud, service_instance=service_info,
1506 vf_module=vf_module_info, vserver=vserver_info,
1507 additional_fields=other, vlan=vlan_info)
1508 candidate = vf_module_candidate.convert_nested_dict_to_dict()
1510 # add vf-module parameters for filtering
1511 vnf_vf_module_inventory = copy.deepcopy(vnf)
1512 vnf_vf_module_inventory.update(vf_module)
1513 # add specifal parameters for comparsion
1514 vnf_vf_module_inventory['global-customer-id'] = customer_id
1515 vnf_vf_module_inventory['customer-id'] = customer_id
1516 vnf_vf_module_inventory['cloud-region-id'] = cloud.get('location_id')
1517 vnf_vf_module_inventory['physical-location-id'] = complex_info.get('physical_location_id')
1518 vnf_vf_module_inventory['service_instance_id'] = vs_service_instance_id
1520 if filtering_attributes and not self.match_inventory_attributes(filtering_attributes,
1521 vnf_vf_module_inventory,
1522 candidate['candidate_id']):
1523 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
1524 candidate['location_id'], name,
1525 triage_translator_data,
1526 reason="attibute check error")
1528 self.assign_candidate_existing_placement(candidate, existing_placement)
1530 # Pick only candidates not in the excluded list
1531 # if excluded candidate list is provided
1532 if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates,
1534 name, triage_translator_data):
1537 # Pick only candidates in the required list
1538 # if required candidate list is provided
1539 if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
1541 triage_translator_data):
1544 # add the candidate to the demand
1545 # Pick only candidates from the restricted_region
1546 # or restricted_complex
1547 if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
1548 triage_translator_data):
1551 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1552 resolved_demands[name].append(candidate)
1553 LOG.debug(">>>>>>> Candidate <<<<<<<")
1554 with open("vf.log", mode='w') as log_file:
1555 log_file.write(">>>>>>>Vf Candidate <<<<<<<")
1556 log_file.write(json.dumps(candidate, indent=4))
1557 LOG.debug(json.dumps(candidate, indent=4))
1559 elif inventory_type == 'transport' \
1560 and customer_id and service_type and \
1561 service_subscription and service_role:
1563 path = self._aai_versioned_path('business/customers/customer/{}/service-subscriptions/'
1564 'service-subscription/{}/service-instances'
1565 '?service-type={}&service-role={}'.format(customer_id,
1566 service_subscription,
1569 response = self._request('get', path=path, data=None)
1570 if response is None or response.status_code != 200:
1571 self.triage_translator.collectDroppedCandiate("", "", name,
1572 triage_translator_data,
1573 reason=response.status_code)
1575 body = response.json()
1576 transport_vnfs = body.get('service-instance', [])
1578 for vnf in transport_vnfs:
1579 # create a default candidate
1581 other['location_id'] = ''
1582 other['location_type'] = 'att_aic'
1584 vnf_service_instance_id = vnf.get('service-instance-id')
1585 if vnf_service_instance_id:
1586 info = Candidate.build_candidate_info('aai', inventory_type,
1587 self.conf.data.transport_candidate_cost,
1588 candidate_uniqueness, vnf_service_instance_id,
1589 service_resource_id)
1591 self.triage_translator.collectDroppedCandiate('', other['location_id'], name,
1592 triage_translator_data,
1593 reason="service-instance-id error ")
1598 zone = self.resolve_zone_for_vnf(info['candidate_id'], other['location_id'], vnf, name,
1599 triage_translator_data)
1601 zone_info['zone_id'] = zone.get('zone-id')
1602 zone_info['zone_name'] = zone.get('zone-name')
1607 related_to = "complex"
1608 search_key = "complex.physical-location-id"
1609 rel_link_data_list = self._get_aai_rel_link_data(
1611 related_to=related_to,
1612 search_key=search_key
1615 if len(rel_link_data_list) > 1:
1616 self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1617 name, triage_translator_data,
1618 reason="rel_link_data_list error")
1621 rel_link_data = rel_link_data_list[0]
1622 complex_id = rel_link_data.get("d_value")
1623 complex_link = rel_link_data.get('link')
1625 if not (complex_link and complex_id):
1626 LOG.debug("{} complex information not "
1627 "available from A&AI - {}".
1628 format(name, complex_link))
1629 self.triage_translator.collectDroppedCandiate(info['candidate_id'], other['location_id'],
1630 name, triage_translator_data,
1631 reason="complex information not available "
1635 complex_info = self._get_complex(
1636 complex_link=complex_link,
1637 complex_id=complex_id
1639 if not complex_info:
1640 LOG.debug("{} complex information not "
1641 "available from A&AI - {}".
1642 format(name, complex_link))
1643 self.triage_translator.collectDroppedCandiate(info['candidate_id'],
1644 other['location_id'], name,
1645 triage_translator_data,
1646 reason="complex information not "
1647 "available from A&AI")
1648 continue # move ahead with the next vnf
1650 complex_info = self.build_complex_dict(complex_info, inventory_type)
1651 transport_candidate = Transport(info=info, zone=zone_info, complex=complex_info,
1652 additional_fiels=other)
1653 candidate = transport_candidate.convert_nested_dict_to_dict()
1655 self.add_passthrough_attributes(candidate, passthrough_attributes, name)
1656 # add candidate to demand candidates
1657 resolved_demands[name].append(candidate)
1659 elif inventory_type == 'nssi' or inventory_type == 'nsi':
1660 if filtering_attributes and model_invariant_id:
1661 second_level_match = aai_utils.get_first_level_and_second_level_filter(filtering_attributes,
1663 aai_response = self.get_nxi_candidates(filtering_attributes)
1664 resolved_demands[name].extend(self.filter_nxi_candidates(aai_response, second_level_match,
1666 candidate_uniqueness, inventory_type))
1668 elif inventory_type == 'nst':
1669 if filtering_attributes:
1670 second_level_match = aai_utils.get_first_level_and_second_level_filter(filtering_attributes,
1672 aai_response = self.get_nst_response(filtering_attributes)
1674 sdc_candidates_list = self.get_nst_candidates(aai_response, second_level_match,
1675 default_attributes, candidate_uniqueness,
1677 resolved_demands[name].extend(SDC().update_candidates(sdc_candidates_list))
1680 LOG.error("Unknown inventory_type "
1681 " {}".format(inventory_type))
1682 return resolved_demands
1685 def build_complex_dict(aai_complex, inv_type):
1686 complex_info = dict()
1687 valid_keys = ['physical-location-id', 'complex-name', 'latitude', 'longitude', 'state', 'country', 'city',
1689 # for cloud type, complex_id instead of physical-location-id - note
1690 if inv_type == "cloud":
1691 for valid_key in valid_keys:
1692 if '-' in valid_key:
1693 complex_info[valid_key.replace('-', '_')] = aai_complex.get('complex_id') \
1694 if valid_key == 'physical-location-id' else \
1695 aai_complex.get(valid_key.replace('-', '_'))
1697 complex_info[valid_key] = aai_complex.get(valid_key)
1699 for valid_key in valid_keys:
1700 if '-' in valid_key:
1701 complex_info[valid_key.replace('-', '_')] = aai_complex.get(valid_key)
1703 complex_info[valid_key] = aai_complex.get(valid_key)
1707 def build_vlan_info(vlan_key, port_key):
1709 vlan_info['vlan_key'] = vlan_key
1710 vlan_info['port_key'] = port_key
1713 def resolve_flavors_for_region(self, flavors_obj):
1714 if self.conf.HPA_enabled:
1716 flavors['flavors'] = flavors_obj
1719 def resolve_v_server_and_complex_link_for_vnf(self, candidate_id, cloud, vnf, name, triage_translator_data,
1721 vs_link_list = self.resolve_v_server_links_for_vnf(vnf)
1722 for vs_link in vs_link_list:
1723 body = self.resolve_v_server_for_candidate(candidate_id, cloud['location_id'],
1724 vs_link, True, name, triage_translator_data)
1727 rl_data = self.resolve_complex_info_link_for_v_server(candidate_id, body,
1728 cloud['cloud_owner'], cloud['location_id'],
1729 service_type, name, triage_translator_data)
1734 def get_l_interfaces_from_vserver(self, candidate_id, location_id, v_server, name, triage_translator_data):
1735 if not v_server.get('l-interfaces') or not v_server.get('l-interfaces').get('l-interface'):
1736 self.triage_translator.collectDroppedCandiate(candidate_id,
1738 triage_translator_data,
1739 reason="VF-server interfaces error")
1742 l_interfaces = v_server.get('l-interfaces').get('l-interface')
1743 l_interfaces_list = list()
1745 for l_interface in l_interfaces:
1746 vserver_interface = dict()
1747 vserver_interface['interface-id'] = l_interface.get('interface-id')
1748 vserver_interface['interface-name'] = l_interface.get('interface-name')
1749 vserver_interface['macaddr'] = l_interface.get('macaddr')
1750 vserver_interface['network-id'] = l_interface.get('network-name')
1751 vserver_interface['network-name'] = ''
1752 vserver_interface['ipv4-addresses'] = list()
1753 vserver_interface['ipv6-addresses'] = list()
1755 if l_interface.get('l3-interface-ipv4-address-list'):
1756 for ip_address_info in l_interface.get('l3-interface-ipv4-address-list'):
1757 vserver_interface['ipv4-addresses']. \
1758 append(ip_address_info.get('l3-interface-ipv4-address'))
1760 if l_interface.get('l3-interface-ipv6-address-list'):
1761 for ip_address_info in l_interface.get('l3-interface-ipv6-address-list'):
1762 vserver_interface['ipv6-addresses']. \
1763 append(ip_address_info.get('l3-interface-ipv6-address'))
1765 l_interfaces_list.append(vserver_interface)
1766 return l_interfaces_list
1769 def get_vnf_info(vnf):
1770 # some validation should happen
1772 vnf_info['host_id'] = vnf.get("vnf-name")
1773 vnf_info['nf-name'] = vnf.get("vnf-name")
1774 vnf_info['nf-id'] = vnf.get("vnf-id")
1775 vnf_info['nf-type'] = 'vnf'
1776 vnf_info['vnf-type'] = vnf.get("vnf-type")
1777 vnf_info['ipv4-oam-address'] = vnf.get("ipv4-oam-address") if vnf.get("ipv4-oam-address") else ""
1778 vnf_info['ipv6-oam-address'] = vnf.get("ipv6-oam-address") if vnf.get("ipv6-oam-address") else ""
1782 def resolve_cloud_for_region(region, region_id):
1784 valid_keys = ['cloud_owner', 'cloud_region_version', 'location_id']
1785 for valid_key in valid_keys:
1786 cloud[valid_key] = region.get(valid_key) if not valid_key == 'location_id' else region_id
1787 cloud['location_type'] = 'att_aic'
1791 def get_vf_module(vf_module):
1792 vf_module_info = dict()
1793 vf_module_info['vf-module-name'] = vf_module.get("vf-module-name")
1794 vf_module_info['vf-module-id'] = vf_module.get("vf-module-id")
1795 return vf_module_info
1797 def get_vim_id(self, cloud_owner, cloud_region_id):
1798 if self.conf.HPA_enabled:
1799 return cloud_owner + '_' + cloud_region_id
1802 def add_passthrough_attributes(candidate, passthrough_attributes, demand_name):
1803 if passthrough_attributes is None:
1805 if len(passthrough_attributes.items()) > 0:
1806 candidate['passthrough_attributes'] = dict()
1807 for key, value in passthrough_attributes.items():
1808 candidate['passthrough_attributes'][key] = value
1810 def resolve_zone_for_vnf(self, candidate_id, location_id, vnf, name, triage_translator_data):
1812 zone_link = self._get_aai_rel_link(
1813 data=vnf, related_to=related_to)
1815 LOG.error("Zone information not available from A&AI for transport candidates")
1816 self.triage_translator.collectDroppedCandiate(candidate_id, location_id,
1817 name, triage_translator_data,
1818 reason="Zone information not available from A&AI for "
1819 "transport candidates")
1821 zone_aai_path = self._get_aai_path_from_link(zone_link)
1822 response = self._request('get', path=zone_aai_path, data=None)
1823 if response is None or response.status_code != 200:
1824 self.triage_translator.collectDroppedCandiate(candidate_id, location_id, name,
1825 triage_translator_data,
1826 reason=response.status_code)
1828 body = response.json()
1831 def match_region(self, candidate, restricted_region_id, restricted_complex_id, demand_name,
1832 triage_translator_data):
1833 if self.match_candidate_attribute(
1836 restricted_region_id,
1838 candidate.get('inventory_type')) or \
1839 self.match_candidate_attribute(
1841 "physical_location_id",
1842 restricted_complex_id,
1844 candidate.get('inventory_type')):
1845 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1846 demand_name, triage_translator_data,
1847 reason="candidate region does not match")
1852 def match_candidate_by_list(self, candidate, candidates_list, exclude, demand_name, triage_translator_data):
1853 has_candidate = False
1855 for list_candidate in candidates_list:
1857 and list_candidate.get('inventory_type') \
1858 == candidate.get('inventory_type'):
1859 if isinstance(list_candidate.get('candidate_id'), list):
1860 for candidate_id in list_candidate.get('candidate_id'):
1861 if candidate_id == candidate.get('candidate_id'):
1862 has_candidate = True
1865 raise Exception("Invalid candidate id list format")
1870 if not has_candidate:
1871 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1872 demand_name, triage_translator_data,
1873 reason="has_required_candidate candidate")
1875 self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
1876 demand_name, triage_translator_data,
1877 reason="excluded candidate")
1878 return has_candidate
1880 def get_nxi_candidates(self, filtering_attributes):
1881 raw_path = 'nodes/service-instances' + aai_utils.add_query_params_and_depth(filtering_attributes, "2")
1882 path = self._aai_versioned_path(raw_path)
1883 aai_response = self._request('get', path, data=None)
1885 if aai_response is None or aai_response.status_code != 200:
1887 if aai_response.json():
1888 return aai_response.json()
1890 def filter_nxi_candidates(self, response_body, filtering_attributes, default_attributes, candidate_uniqueness,
1893 if response_body is not None:
1894 nxi_instances = response_body.get("service-instance", [])
1896 for nxi_instance in nxi_instances:
1897 inventory_attributes = aai_utils.get_inv_values_for_second_level_filter(filtering_attributes,
1899 nxi_info = aai_utils.get_instance_info(nxi_instance)
1900 if not filtering_attributes or \
1901 self.match_inventory_attributes(filtering_attributes, inventory_attributes,
1902 nxi_instance.get('service-instance-id')):
1903 profile_instances = self.get_profile_instances(nxi_instance)
1905 profiles = aai_utils.get_profiles(profile_instances, "slice-profile")
1906 cost = self.conf.data.nssi_candidate_cost
1908 profiles = aai_utils.get_profiles(profile_instances, "service-profile")
1909 cost = self.conf.data.nsi_candidate_cost
1910 for profile in profiles:
1911 profile_id = profile.get('profile-id')
1912 info = Candidate.build_candidate_info('aai', type, cost, candidate_uniqueness, profile_id)
1913 profile_info = aai_utils.convert_hyphen_to_under_score(profile)
1914 nxi_candidate = NxI(instance_info=nxi_info, profile_info=profile_info, info=info,
1915 default_fields=aai_utils.convert_hyphen_to_under_score(default_attributes))
1916 candidate = nxi_candidate.convert_nested_dict_to_dict()
1917 candidates.append(candidate)
1920 def get_profile_instances(self, nxi_instance):
1921 slice_role = nxi_instance['service-role']
1922 related_key = "allotted-resource" if slice_role == 'nsi' else 'service-instance'
1923 related_nodes = self._get_aai_rel_link_data(nxi_instance, related_key,
1924 "service-instance.service-instance-id")
1925 profile_instances = []
1926 for node in related_nodes:
1927 profile_instance_id = node["d_value"]
1928 raw_path = f'nodes/service-instances/service-instance/{profile_instance_id}?depth=2'
1929 path = self._aai_versioned_path(raw_path)
1930 aai_response = self._request('get', path, data=None)
1931 if aai_response.status_code == 200 and aai_response.json():
1932 profile_instances.append(aai_response.json())
1934 return profile_instances
1936 def get_nst_response(self, filtering_attributes):
1937 raw_path = 'service-design-and-creation/models' + aai_utils.add_query_params_and_depth(filtering_attributes,
1939 path = self._aai_versioned_path(raw_path)
1940 aai_response = self._request('get', path, data=None)
1942 if aai_response is None or aai_response.status_code != 200:
1944 if aai_response.json():
1945 return aai_response.json()
1947 def get_nst_candidates(self, response_body, filtering_attributes, default_attributes, candidate_uniqueness,
1950 if response_body is not None:
1951 nst_metadatas = response_body.get("model", [])
1953 for nst_metadata in nst_metadatas:
1954 nst_info = aai_utils.get_nst_info(nst_metadata)
1955 model_vers = nst_metadata.get('model-vers').get('model-ver')
1956 for model_ver in model_vers:
1957 model_version_id = model_ver.get('model-version-id')
1958 cost = self.conf.data.nst_candidate_cost
1959 info = Candidate.build_candidate_info('aai', type, cost, candidate_uniqueness, model_version_id)
1960 model_version_obj = aai_utils.get_model_ver_info(model_ver)
1961 model_ver_info = aai_utils.convert_hyphen_to_under_score(model_version_obj)
1962 nst_candidate = NST(model_info=nst_info, model_ver=model_ver_info, info=info,
1963 default_fields=aai_utils.convert_hyphen_to_under_score(default_attributes),
1965 candidates.append(nst_candidate)