New Inventory type for TD 37/84537/5
authorLukasz Rajewski <lukasz.rajewski@orange.com>
Tue, 2 Apr 2019 10:08:19 +0000 (12:08 +0200)
committerLukasz Rajewski <lukasz.rajewski@orange.com>
Thu, 18 Apr 2019 14:21:04 +0000 (16:21 +0200)
* New inventory type 'vfmodule' for ditribute traffic and
  refactored 'service' type that has many functions in
  common with 'vfmodule' type
* Improved optimizer for random pick algorithm when no
  optimization constraint is specified
* Unit tests added
* resolve_demands() refactored to make it testable and to remove
  code repetitions for different inventory types

Change-Id: If5c7e7d2d4e7c1b449d0b37e5c33c697aa4e653f
Issue-ID: OPTFRA-445
Signed-off-by: Lukasz Rajewski <lukasz.rajewski@orange.com>
21 files changed:
conductor/conductor/controller/translator.py
conductor/conductor/data/plugins/inventory_provider/aai.py
conductor/conductor/data/service.py
conductor/conductor/solver/optimizer/constraints/service.py
conductor/conductor/solver/optimizer/optimizer.py
conductor/conductor/solver/optimizer/random_pick.py
conductor/conductor/solver/service.py
conductor/conductor/tests/unit/controller/test_translator.py
conductor/conductor/tests/unit/data/demands_vfmodule.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/bad_generic_vnf_list.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/service_candidates.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/service_demand_list.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/test_aai.py
conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_candidates.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_complex.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_demand_list.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_list.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_region.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_service_generic_vnf_list.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_vserver.json [new file with mode: 0644]
conductor/conductor/tests/unit/data/test_service.py

index a13c273..85f63af 100644 (file)
@@ -43,7 +43,7 @@ CONF = cfg.CONF
 VERSIONS = ["2016-11-01", "2017-10-10", "2018-02-01"]
 LOCATION_KEYS = ['latitude', 'longitude', 'host_name', 'clli_code']
 INVENTORY_PROVIDERS = ['aai']
-INVENTORY_TYPES = ['cloud', 'service', 'transport']
+INVENTORY_TYPES = ['cloud', 'service', 'transport', 'vfmodule']
 DEFAULT_INVENTORY_PROVIDER = INVENTORY_PROVIDERS[0]
 CANDIDATE_KEYS = ['candidate_id', 'cost', 'inventory_type', 'location_id',
                   'location_type']
@@ -445,9 +445,9 @@ class Translator(object):
                             "demand {}".format(inventory_type, name)
                         )
 
-                    # For service inventories, customer_id and
+                    # For service and vfmodule inventories, customer_id and
                     # service_type MUST be specified
-                    if inventory_type == 'service':
+                    if inventory_type == 'service' or inventory_type == 'vfmodule':
                         attributes = requirement.get('attributes')
 
                         if attributes:
index 31064a6..d6fb724 100644 (file)
@@ -20,6 +20,7 @@
 import re
 import time
 import uuid
+import copy
 
 import json
 from oslo_config import cfg
@@ -787,6 +788,78 @@ class AAI(base.InventoryProviderBase):
         body = response.json()
         return body.get("generic-vnf", [])
 
+    def resolove_v_server_for_candidate(self, candidate, vs_link, add_interfaces, demand_name, triage_translator_data):
+        if not vs_link:
+            LOG.error(_LE("{} VSERVER link information not "
+                          "available from A&AI").format(demand_name))
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                          candidate['location_id'], demand_name,
+                                                          triage_translator_data,
+                                                          reason="VSERVER link information not")
+            return None  # move ahead with the next vnf
+
+        if add_interfaces:
+            vs_link = vs_link + '?depth=2'
+        vs_path = self._get_aai_path_from_link(vs_link)
+        if not vs_path:
+            LOG.error(_LE("{} VSERVER path information not "
+                          "available from A&AI - {}").
+                      format(demand_name, vs_path))
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                          candidate['location_id'], demand_name,
+                                                          triage_translator_data,
+                                                          reason="VSERVER path information not available from A&AI")
+            return None  # move ahead with the next vnf
+        path = self._aai_versioned_path(vs_path)
+        response = self._request(
+            path=path, context="demand, VSERVER",
+            value="{}, {}".format(demand_name, vs_path))
+        if response is None or response.status_code != 200:
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                          candidate['location_id'], demand_name,
+                                                          triage_translator_data,
+                                                          reason=response.status_code)
+            return None
+        return response.json()
+
+    def resolve_vf_modules_for_generic_vnf(self, candidate, vnf, demand_name, triage_translator_data):
+        raw_path = '/network/generic-vnfs/generic-vnf/{}?depth=1'.format(vnf.get("vnf-id"))
+        path = self._aai_versioned_path(raw_path)
+
+        response = self._request('get', path=path, data=None)
+        if response is None or response.status_code != 200:
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                          candidate['location_id'], demand_name,
+                                                          triage_translator_data,
+                                                          reason=response)
+            return None
+        generic_vnf_details = response.json()
+
+        if generic_vnf_details is None or not generic_vnf_details.get('vf-modules') \
+                or not generic_vnf_details.get('vf-modules').get('vf-module'):
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                          candidate['location_id'], demand_name,
+                                                          triage_translator_data,
+                                                          reason="Generic-VNF No detailed data for VF-modules")
+            return None
+        else:
+            return generic_vnf_details.get('vf-modules').get('vf-module')
+
+    def resolve_cloud_regions_by_cloud_region_id(self, cloud_region_id):
+        cloud_region_uri = '/cloud-infrastructure/cloud-regions' \
+                           '/?cloud-region-id=' \
+                           + cloud_region_id
+        path = self._aai_versioned_path(cloud_region_uri)
+
+        response = self._request('get',
+                                 path=path,
+                                 data=None)
+        if response is None or response.status_code != 200:
+            return None
+
+        body = response.json()
+        return body.get('cloud-region', [])
+
     def assign_candidate_existing_placement(self, candidate, existing_placement):
 
         """Assign existing_placement and cost parameters to candidate
@@ -817,6 +890,289 @@ class AAI(base.InventoryProviderBase):
 
         return ''.join(conflict_id_list)
 
+    def resolve_v_server_links_for_vnf(self, vnf):
+        related_to = "vserver"
+        search_key = "cloud-region.cloud-owner"
+        rl_data_list = self._get_aai_rel_link_data(
+            data=vnf, related_to=related_to,
+            search_key=search_key)
+        vs_link_list = list()
+        for i in range(0, len(rl_data_list)):
+            vs_link_list.append(rl_data_list[i].get('link'))
+        return vs_link_list
+
+    def resolve_complex_info_link_for_v_server(self, candidate, v_server, cloud_owner, cloud_region_id, service_type,
+                                               demand_name, triage_translator_data):
+        related_to = "pserver"
+        rl_data_list = self._get_aai_rel_link_data(
+            data=v_server,
+            related_to=related_to,
+            search_key=None
+        )
+        if len(rl_data_list) > 1:
+            self._log_multiple_item_error(
+                demand_name, service_type, related_to, "item",
+                "VSERVER", v_server)
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                          candidate['location_id'], demand_name,
+                                                          triage_translator_data, reason="item VSERVER")
+            return None
+        rl_data = rl_data_list[0]
+        ps_link = rl_data.get('link')
+
+        # Third level query to get cloud region from pserver
+        if not ps_link:
+            LOG.error(_LE("{} pserver related link "
+                          "not found in A&AI: {}").
+                      format(demand_name, rl_data))
+            # if HPA_feature is disabled
+            if not self.conf.HPA_enabled:
+                # Triage Tool Feature Changes
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                              candidate['location_id'], demand_name,
+                                                              triage_translator_data,
+                                                              reason="ps link not found")
+                return None
+            else:
+                if not (cloud_owner and cloud_region_id):
+                    LOG.error("{} cloud-owner or cloud-region not "
+                              "available from A&AI".
+                              format(demand_name))
+                    # Triage Tool Feature Changes
+                    self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                                  candidate['location_id'], demand_name,
+                                                                  triage_translator_data,
+                                                                  reason="Cloud owner and cloud region "
+                                                                         "id not found")
+                    return None  # move ahead with the next vnf
+                cloud_region_uri = \
+                    '/cloud-infrastructure/cloud-regions/cloud-region' \
+                    '/?cloud-owner=' + cloud_owner \
+                    + '&cloud-region-id=' + cloud_region_id
+                path = self._aai_versioned_path(cloud_region_uri)
+                response = self._request('get',
+                                         path=path,
+                                         data=None)
+                if response is None or response.status_code != 200:
+                    # Triage Tool Feature Changes
+                    self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                                  candidate['location_id'], demand_name,
+                                                                  triage_translator_data,
+                                                                  reason=response)
+                    return None
+                body = response.json()
+        else:
+            ps_path = self._get_aai_path_from_link(ps_link)
+            if not ps_path:
+                LOG.error(_LE("{} pserver path information "
+                              "not found in A&AI: {}").
+                          format(demand_name, ps_link))
+                # Triage Tool Feature Changes
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                              candidate['location_id'], demand_name,
+                                                              triage_translator_data,
+                                                              reason="ps path not found")
+                return None  # move ahead with the next vnf
+            path = self._aai_versioned_path(ps_path)
+            response = self._request(
+                path=path, context="PSERVER", value=ps_path)
+            if response is None or response.status_code != 200:
+                # Triage Tool Feature Changes
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                              candidate['location_id'], demand_name,
+                                                              triage_translator_data,
+                                                              reason=response)
+                return None
+            body = response.json()
+
+        related_to = "complex"
+        search_key = "complex.physical-location-id"
+        rl_data_list = self._get_aai_rel_link_data(
+            data=body,
+            related_to=related_to,
+            search_key=search_key
+        )
+        if len(rl_data_list) > 1:
+            if not self.match_vserver_attribute(rl_data_list):
+                self._log_multiple_item_error(
+                    demand_name, service_type, related_to, search_key, "PSERVER", body)
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
+                                                              demand_name, triage_translator_data,
+                                                              reason="PSERVER error")
+                return None
+        return rl_data_list[0]
+
+    def resolve_cloud_region_id_and_version_for_vnf(self, candidate, vnf, service_type, demand_name,
+                                                    triage_translator_data):
+        related_to = "vserver"
+        search_key = "cloud-region.cloud-region-id"
+
+        rl_data_list = self._get_aai_rel_link_data(
+            data=vnf,
+            related_to=related_to,
+            search_key=search_key
+        )
+        if len(rl_data_list) > 1:
+            if not self.match_vserver_attribute(rl_data_list):
+                self._log_multiple_item_error(
+                    demand_name, service_type, related_to, search_key,
+                    "VNF", vnf)
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                              candidate['location_id'], demand_name,
+                                                              triage_translator_data,
+                                                              reason="VNF error")
+                return None, None
+        cloud_region_rl_data = rl_data_list[0]
+        cloud_region_id = cloud_region_rl_data.get('d_value')
+
+        # get version for service candidate
+        cloud_region_version_rl_data = {'d_value': ''}
+        if cloud_region_id:
+            regions = self.resolve_cloud_regions_by_cloud_region_id(cloud_region_id)
+            if regions is None:
+                return cloud_region_rl_data, None
+
+            for region in regions:
+                if "cloud-region-version" in region:
+                    cloud_region_version_rl_data['d_value'] = self._get_version_from_string(region["cloud-region-version"])
+
+        return cloud_region_rl_data, cloud_region_version_rl_data
+
+    def resolve_cloud_owner_for_vnf(self, candidate, vnf, service_type, demand_name, triage_translator_data):
+        related_to = "vserver"
+        search_key = "cloud-region.cloud-owner"
+        rl_data_list = self._get_aai_rel_link_data(
+            data=vnf, related_to=related_to,
+            search_key=search_key)
+
+        if len(rl_data_list) > 1:
+            if not self.match_vserver_attribute(rl_data_list):
+                self._log_multiple_item_error(
+                    demand_name, service_type, related_to, search_key,
+                    "VNF", vnf)
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                              candidate['location_id'], demand_name,
+                                                              triage_translator_data,
+                                                              reason="VNF error")
+                return None
+        return rl_data_list[0]
+
+    def resolve_global_customer_id_for_vnf(self, candidate, vnf, customer_id, service_type, demand_name,
+                                           triage_translator_data):
+        related_to = "service-instance"
+        search_key = "customer.global-customer-id"
+        match_key = "customer.global-customer-id"
+        rl_data_list = self._get_aai_rel_link_data(
+            data=vnf,
+            related_to=related_to,
+            search_key=search_key,
+            match_dict={'key': match_key,
+                        'value': customer_id}
+        )
+        if len(rl_data_list) > 1:
+            if not self.match_vserver_attribute(rl_data_list):
+                self._log_multiple_item_error(
+                    demand_name, service_type, related_to, search_key, "VNF", vnf)
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
+                                                              demand_name, triage_translator_data,
+                                                              reason=" match_vserver_attribute generic-vnf")
+                return None
+        return rl_data_list[0]
+
+    def resolve_service_instance_id_for_vnf(self, candidate, vnf, customer_id, service_type, demand_name,
+                                            triage_translator_data):
+        related_to = "service-instance"
+        search_key = "service-instance.service-instance-id"
+        match_key = "customer.global-customer-id"
+        rl_data_list = self._get_aai_rel_link_data(
+            data=vnf,
+            related_to=related_to,
+            search_key=search_key,
+            match_dict={'key': match_key,
+                        'value': customer_id}
+        )
+        if len(rl_data_list) > 1:
+            if not self.match_vserver_attribute(rl_data_list):
+                self._log_multiple_item_error(
+                    demand_name, service_type, related_to, search_key, "VNF", vnf)
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
+                                                              demand_name, triage_translator_data,
+                                                              reason="multiple_item_error generic-vnf")
+                return None
+        return rl_data_list[0]
+
+    def build_complex_info_for_candidate(self, candidate, vnf, complex_list, service_type, demand_name,
+                                         triage_translator_data):
+        if not complex_list or \
+                len(complex_list) < 1:
+            LOG.error("Complex information not "
+                      "available from A&AI")
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                          candidate['location_id'], demand_name,
+                                                          triage_translator_data,
+                                                          reason="Complex information not available from A&AI")
+            return
+
+        # In the scenario where no pserver information is available
+        # assumption here is that cloud-region does not span across
+        # multiple complexes
+        if len(complex_list) > 1:
+            related_to = "complex"
+            search_key = "complex.physical-location-id"
+            if not self.match_vserver_attribute(complex_list):
+                self._log_multiple_item_error(
+                    demand_name, service_type, related_to, search_key,
+                    "VNF", vnf)
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                              candidate['location_id'], demand_name,
+                                                              triage_translator_data,
+                                                              reason="Generic-vnf error")
+                return
+
+        rl_data = complex_list[0]
+        complex_link = rl_data.get('link')
+        complex_id = rl_data.get('d_value')
+
+        # Final query for the complex information
+        if not (complex_link and complex_id):
+            LOG.debug("{} complex information not "
+                      "available from A&AI - {}".
+                      format(demand_name, complex_link))
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                          candidate['location_id'], demand_name,
+                                                          triage_translator_data,
+                                                          reason="Complex information not available from A&AI")
+            return  # move ahead with the next vnf
+        else:
+            complex_info = self._get_complex(
+                complex_link=complex_link,
+                complex_id=complex_id
+            )
+            if not complex_info:
+                LOG.debug("{} complex information not "
+                          "available from A&AI - {}".
+                          format(demand_name, complex_link))
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                              candidate['location_id'], demand_name,
+                                                              triage_translator_data,
+                                                              reason="Complex information not available from A&AI")
+                return  # move ahead with the next vnf
+            candidate['physical_location_id'] = \
+                complex_id
+            candidate['complex_name'] = \
+                complex_info.get('complex-name')
+            candidate['latitude'] = \
+                complex_info.get('latitude')
+            candidate['longitude'] = \
+                complex_info.get('longitude')
+            candidate['state'] = \
+                complex_info.get('state')
+            candidate['country'] = \
+                complex_info.get('country')
+            candidate['city'] = \
+                complex_info.get('city')
+            candidate['region'] = \
+                complex_info.get('region')
 
     def resolve_demands(self, demands, plan_info, triage_translator_data):
         """Resolve demands into inventory candidate lists"""
@@ -958,56 +1314,28 @@ class AAI(base.InventoryProviderBase):
                         if conflict_identifier:
                             candidate['conflict_id'] = self.resovle_conflict_id(conflict_identifier, candidate)
 
-                        if self.match_candidate_attribute(
-                                candidate, "candidate_id",
-                                restricted_region_id, name,
-                                inventory_type) or \
-                           self.match_candidate_attribute(
-                               candidate, "physical_location_id",
-                               restricted_complex_id, name,
-                               inventory_type):
+                        if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
+                                                 triage_translator_data):
                             continue
 
                         self.assign_candidate_existing_placement(candidate, existing_placement)
 
                         # Pick only candidates not in the excluded list
                         # if excluded candidate list is provided
-                        if excluded_candidates:
-                            has_excluded_candidate = False
-                            for excluded_candidate in excluded_candidates:
-                                if excluded_candidate \
-                                   and excluded_candidate.get('inventory_type') == \
-                                   candidate.get('inventory_type') \
-                                   and excluded_candidate.get('candidate_id') == \
-                                   candidate.get('candidate_id'):
-                                    has_excluded_candidate = True
-                                    break
-
-                            if has_excluded_candidate:
-                                continue
+                        if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True, name, triage_translator_data):
+                            continue
 
                         # Pick only candidates in the required list
                         # if required candidate list is provided
-                        if required_candidates:
-                            has_required_candidate = False
-                            for required_candidate in required_candidates:
-                                if required_candidate \
-                                   and required_candidate.get('inventory_type') \
-                                   == candidate.get('inventory_type') \
-                                   and required_candidate.get('candidate_id') \
-                                   == candidate.get('candidate_id'):
-                                    has_required_candidate = True
-                                    break
-
-                            if not has_required_candidate:
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason="has_required_candidate")
-                                continue
+                        if required_candidates and not self.match_candidate_by_list(candidate, required_candidates, False, name, triage_translator_data):
+                            continue
 
                         # add candidate to demand candidates
                         resolved_demands[name].append(candidate)
+                        LOG.debug(">>>>>>> Candidate <<<<<<<")
+                        LOG.debug(json.dumps(candidate, indent=4))
 
-                elif inventory_type == 'service' \
+                elif (inventory_type == 'service') \
                         and customer_id:
 
                     # First level query to get the list of generic vnfs
@@ -1045,7 +1373,7 @@ class AAI(base.InventoryProviderBase):
                         candidate = dict()
                         candidate['inventory_provider'] = 'aai'
                         candidate['service_resource_id'] = service_resource_id
-                        candidate['inventory_type'] = 'service'
+                        candidate['inventory_type'] = inventory_type
                         candidate['candidate_id'] = ''
                         candidate['location_id'] = ''
                         candidate['location_type'] = 'att_aic'
@@ -1060,48 +1388,25 @@ class AAI(base.InventoryProviderBase):
                         # start populating the candidate
                         candidate['host_id'] = vnf.get("vnf-name")
 
-                        related_to = "vserver"
-                        search_key = "cloud-region.cloud-owner"
-                        rl_data_list = self._get_aai_rel_link_data(
-                            data=vnf, related_to=related_to,
-                            search_key=search_key)
-
-                        if len(rl_data_list) > 1:
-                            if not self.match_vserver_attribute(rl_data_list):
-                                self._log_multiple_item_error(
-                                    name, service_type, related_to, search_key,
-                                    "GENERIC-VNF", vnf)
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason="Generic -vnf error")
-                                continue
-                        rl_data = rl_data_list[0]
-
-                        vs_link_list = list()
-                        for i in range(0, len(rl_data_list)):
-                            vs_link_list.append(rl_data_list[i].get('link'))
+                        rl_data = self.resolve_cloud_owner_for_vnf(candidate, vnf, service_type, name,
+                                                                   triage_translator_data)
+                        if rl_data is None:
+                            continue
+                        else:
+                            cloud_owner = rl_data.get('d_value')
 
-                        cloud_owner = rl_data.get('d_value')
                         candidate['cloud_owner'] = cloud_owner
 
+                        cloud_region_id_rl_data, cloud_region_version_rl_data = self.resolve_cloud_region_id_and_version_for_vnf(
+                            candidate, vnf, service_type, name, triage_translator_data)
 
-                        search_key = "cloud-region.cloud-region-id"
+                        if cloud_region_id_rl_data is None or cloud_region_version_rl_data is None:
+                            continue
 
-                        rl_data_list = self._get_aai_rel_link_data(
-                            data=vnf,
-                            related_to=related_to,
-                            search_key=search_key
-                        )
-                        if len(rl_data_list) > 1:
-                            if not self.match_vserver_attribute(rl_data_list):
-                                self._log_multiple_item_error(
-                                    name, service_type, related_to, search_key,
-                                    "GENERIC-VNF", vnf)
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason=" generic-vnf error")
-                                continue
-                        rl_data = rl_data_list[0]
-                        cloud_region_id = rl_data.get('d_value')
+                        cloud_region_id = cloud_region_id_rl_data.get('d_value')
+                        cloud_region_version = cloud_region_version_rl_data.get('d_value')
                         candidate['location_id'] = cloud_region_id
+                        candidate['cloud_region_version'] = cloud_region_version
 
                         # Added vim-id for short-term workaround
                         if self.conf.HPA_enabled:
@@ -1110,28 +1415,6 @@ class AAI(base.InventoryProviderBase):
                             candidate['vim-id'] = \
                                 candidate['cloud_owner'] + '_' + cloud_region_id
 
-                        # get version for service candidate
-                        if cloud_region_id:
-                            cloud_region_uri = '/cloud-infrastructure/cloud-regions' \
-                                               '/?cloud-region-id=' \
-                                               + cloud_region_id
-                            path = self._aai_versioned_path(cloud_region_uri)
-
-                            response = self._request('get',
-                                                     path=path,
-                                                     data=None)
-                            if response is None or response.status_code != 200:
-                                return None
-
-                            body = response.json()
-                            regions = body.get('cloud-region', [])
-
-                            for region in regions:
-                                if "cloud-region-version" in region:
-                                    candidate['cloud_region_version'] = \
-                                        self._get_version_from_string(
-                                            region["cloud-region-version"])
-
                         if self.check_sriov_automation(
                                 candidate['cloud_region_version'], name,
                                 candidate['host_id']):
@@ -1139,250 +1422,60 @@ class AAI(base.InventoryProviderBase):
                         else:
                             candidate['sriov_automation'] = 'false'
 
-                        related_to = "service-instance"
-                        search_key = "customer.global-customer-id"
-                        match_key = "customer.global-customer-id"
-                        rl_data_list = self._get_aai_rel_link_data(
-                            data=vnf,
-                            related_to=related_to,
-                            search_key=search_key,
-                            match_dict={'key': match_key,
-                                        'value': customer_id}
-                        )
-                        if len(rl_data_list) > 1:
-                            if not self.match_vserver_attribute(rl_data_list):
-                                self._log_multiple_item_error(
-                                    name, service_type, related_to, search_key,
-                                    "GENERIC-VNF", vnf)
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name,triage_translator_data,
-                                                                             reason=" match_vserver_attribute generic-vnf")
-                                continue
-                        rl_data = rl_data_list[0]
-                        vs_cust_id = rl_data.get('d_value')
+                        rl_data = self.resolve_global_customer_id_for_vnf(candidate, vnf, customer_id, service_type,
+                                                                          name, triage_translator_data)
+                        if rl_data is None:
+                            continue
+                        else:
+                            vs_cust_id = rl_data.get('d_value')
 
-                        search_key = "service-instance.service-instance-id"
-                        match_key = "customer.global-customer-id"
-                        rl_data_list = self._get_aai_rel_link_data(
-                            data=vnf,
-                            related_to=related_to,
-                            search_key=search_key,
-                            match_dict={'key': match_key,
-                                        'value': customer_id}
-                        )
-                        if len(rl_data_list) > 1:
-                            if not self.match_vserver_attribute(rl_data_list):
-                                self._log_multiple_item_error(
-                                    name, service_type, related_to, search_key,
-                                    "GENERIC-VNF", vnf)
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name,triage_translator_data,
-                                                                             reason="multiple_item_error generic-vnf")
-                                continue
-                        rl_data = rl_data_list[0]
-                        vs_service_instance_id = rl_data.get('d_value')
+                        rl_data = self.resolve_service_instance_id_for_vnf(candidate, vnf, customer_id,
+                                                                           service_type, name, triage_translator_data)
+                        if rl_data is None:
+                            continue
+                        else:
+                            vs_service_instance_id = rl_data.get('d_value')
 
                         if vs_cust_id and vs_cust_id == customer_id:
                             candidate['candidate_id'] = \
                                 vs_service_instance_id
                         else:  # vserver is for a different customer
-                            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data, reason= "vserver is for a different customer")
+                            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                                          candidate['location_id'], name,
+                                                                          triage_translator_data,
+                                                                          reason= "vserver is for a different customer")
                             continue
 
                         # Second level query to get the pserver from vserver
                         complex_list = list()
+                        vs_link_list = self.resolve_v_server_links_for_vnf(vnf)
 
                         for vs_link in vs_link_list:
 
-                            if not vs_link:
-                                LOG.error(_LE("{} VSERVER link information not "
-                                              "available from A&AI").format(name))
-                                LOG.debug("Related link data: {}".format(rl_data))
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason="VSERVER link information not")
-                                continue  # move ahead with the next vnf
-
-                            vs_path = self._get_aai_path_from_link(vs_link)
-                            if not vs_path:
-                                LOG.error(_LE("{} VSERVER path information not "
-                                              "available from A&AI - {}").
-                                          format(name, vs_path))
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason="VSERVER path information not available from A&AI")
-                                continue  # move ahead with the next vnf
-                            path = self._aai_versioned_path(vs_path)
-                            response = self._request(
-                                path=path, context="demand, VSERVER",
-                                value="{}, {}".format(name, vs_path))
-                            if response is None or response.status_code != 200:
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason=response.status_code)
+                            body = self.resolove_v_server_for_candidate(candidate, vs_link, True, name,
+                                                                        triage_translator_data)
+                            if body is None:
                                 continue
-                            body = response.json()
 
-                            related_to = "pserver"
-                            rl_data_list = self._get_aai_rel_link_data(
-                                data=body,
-                                related_to=related_to,
-                                search_key=None
-                            )
-                            if len(rl_data_list) > 1:
-                                self._log_multiple_item_error(
-                                    name, service_type, related_to, "item",
-                                    "VSERVER", body)
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason="item VSERVER")
+                            rl_data = self.resolve_complex_info_link_for_v_server(candidate, body, cloud_owner,
+                                                                                  cloud_region_id, service_type,
+                                                                                  name, triage_translator_data)
+                            if rl_data is None:
                                 continue
-                            rl_data = rl_data_list[0]
-                            ps_link = rl_data.get('link')
-
-                            # Third level query to get cloud region from pserver
-                            if not ps_link:
-                                LOG.error(_LE("{} pserver related link "
-                                              "not found in A&AI: {}").
-                                          format(name, rl_data))
-                                # if HPA_feature is disabled
-                                if not self.conf.HPA_enabled:
-                                    # Triage Tool Feature Changes
-                                    self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
-                                                                                  candidate['location_id'], name,
-                                                                                  triage_translator_data,
-                                                                                  reason="ps link not found")
-                                    continue
-                                else:
-                                    if not (cloud_owner and cloud_region_id):
-                                        LOG.error("{} cloud-owner or cloud-region not "
-                                                  "available from A&AI".
-                                                  format(name))
-                                        # Triage Tool Feature Changes
-                                        self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
-                                                                                      candidate['location_id'], name,
-                                                                                      triage_translator_data,
-                                                                                      reason="Cloud owner and cloud region "
-                                                                                             "id not found")
-                                        continue  # move ahead with the next vnf
-                                    cloud_region_uri = \
-                                        '/cloud-infrastructure/cloud-regions/cloud-region' \
-                                        '/?cloud-owner=' + cloud_owner \
-                                        + '&cloud-region-id=' + cloud_region_id
-                                    path = self._aai_versioned_path(cloud_region_uri)
-                                    response = self._request('get',
-                                                             path=path,
-                                                             data=None)
-                                    if response is None or response.status_code != 200:
-                                        # Triage Tool Feature Changes
-                                        self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
-                                                                                      candidate['location_id'], name,
-                                                                                      triage_translator_data,
-                                                                                      reason=response)
-                                        continue
-                                    body = response.json()
-                            else:
-                                ps_path = self._get_aai_path_from_link(ps_link)
-                                if not ps_path:
-                                    LOG.error(_LE("{} pserver path information "
-                                                  "not found in A&AI: {}").
-                                              format(name, ps_link))
-                                    # Triage Tool Feature Changes
-                                    self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
-                                                                                  candidate['location_id'], name,
-                                                                                  triage_translator_data,
-                                                                                  reason="ps path not found")
-                                    continue  # move ahead with the next vnf
-                                path = self._aai_versioned_path(ps_path)
-                                response = self._request(
-                                    path=path, context="PSERVER", value=ps_path)
-                                if response is None or response.status_code != 200:
-                                    # Triage Tool Feature Changes
-                                    self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
-                                                                                  candidate['location_id'], name,
-                                                                                  triage_translator_data,
-                                                                                  reason=response)
-                                    continue
-                                body = response.json()
-
-                            related_to = "complex"
-                            search_key = "complex.physical-location-id"
-                            rl_data_list = self._get_aai_rel_link_data(
-                                data=body,
-                                related_to=related_to,
-                                search_key=search_key
-                            )
-                            if len(rl_data_list) > 1:
-                                if not self.match_vserver_attribute(rl_data_list):
-                                    self._log_multiple_item_error(
-                                        name, service_type, related_to, search_key,
-                                        "PSERVER", body)
-                                    self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                                 reason="PSERVER error")
-                                    continue
-                            rl_data = rl_data_list[0]
-                            complex_list.append(rl_data)
-
-                        if not complex_list or \
-                            len(complex_list) < 1:
-                            LOG.error("Complex information not "
-                                          "available from A&AI")
-                            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                         reason="Complex information not available from A&AI")
-                            continue
 
-                        # In the scenario where no pserver information is available
-                        # assumption here is that cloud-region does not span across
-                        # multiple complexes
-                        if len(complex_list) > 1:
-                            if not self.match_vserver_attribute(complex_list):
-                                self._log_multiple_item_error(
-                                    name, service_type, related_to, search_key,
-                                    "GENERIC-VNF", vnf)
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason="Generic-vnf error")
-                                continue
+                            complex_list.append(rl_data)
 
-                        rl_data = complex_list[0]
-                        complex_link = rl_data.get('link')
-                        complex_id = rl_data.get('d_value')
+                        self.build_complex_info_for_candidate(candidate, vnf, complex_list, service_type, name,
+                                                              triage_translator_data)
 
-                        # Final query for the complex information
-                        if not (complex_link and complex_id):
-                            LOG.debug("{} complex information not "
-                                      "available from A&AI - {}".
-                                      format(name, complex_link))
-                            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                         reason="Complex information not available from A&AI")
-                            continue  # move ahead with the next vnf
-                        else:
-                            complex_info = self._get_complex(
-                                complex_link=complex_link,
-                                complex_id=complex_id
-                            )
-                            if not complex_info:
-                                LOG.debug("{} complex information not "
-                                          "available from A&AI - {}".
-                                          format(name, complex_link))
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason="Complex information not available from A&AI")
-                                continue  # move ahead with the next vnf
-                            candidate['physical_location_id'] = \
-                                complex_id
-                            candidate['complex_name'] = \
-                                complex_info.get('complex-name')
-                            candidate['latitude'] = \
-                                complex_info.get('latitude')
-                            candidate['longitude'] = \
-                                complex_info.get('longitude')
-                            candidate['state'] = \
-                                complex_info.get('state')
-                            candidate['country'] = \
-                                complex_info.get('country')
-                            candidate['city'] = \
-                                complex_info.get('city')
-                            candidate['region'] = \
-                                complex_info.get('region')
+                        if "complex_name" not in candidate:
+                            continue
 
                         # add specifal parameters for comparsion
                         vnf['global-customer-id'] = customer_id
                         vnf['customer-id'] = customer_id
                         vnf['cloud-region-id'] = cloud_region_id
-                        vnf['physical-location-id'] = complex_id
+                        vnf['physical-location-id'] = candidate.get('physical_location_id')
 
                         if attributes and not self.match_inventory_attributes(attributes, vnf, candidate['candidate_id']):
                             self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
@@ -1392,61 +1485,268 @@ class AAI(base.InventoryProviderBase):
 
                         # Pick only candidates not in the excluded list
                         # if excluded candidate list is provided
-                        if excluded_candidates:
-                            has_excluded_candidate = False
-                            for excluded_candidate in excluded_candidates:
-                                if excluded_candidate \
-                                        and excluded_candidate.get('inventory_type') == \
-                                        candidate.get('inventory_type') \
-                                        and excluded_candidate.get('candidate_id') == \
-                                        candidate.get('candidate_id'):
-                                    has_excluded_candidate = True
-                                    break
-
-                            if has_excluded_candidate:
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason="excluded candidate")
-                                continue
+                        if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
+                                                                                name, triage_translator_data):
+                            continue
 
                         # Pick only candidates in the required list
                         # if required candidate list is provided
-                        if required_candidates:
-                            has_required_candidate = False
-                            for required_candidate in required_candidates:
-                                if required_candidate \
-                                        and required_candidate.get('inventory_type') \
-                                        == candidate.get('inventory_type') \
-                                        and required_candidate.get('candidate_id') \
-                                        == candidate.get('candidate_id'):
-                                    has_required_candidate = True
-                                    break
-
-                            if not has_required_candidate:
-                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                             reason="has_required_candidate candidate")
-                                continue
+                        if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
+                                                                                    False, name, triage_translator_data):
+                            continue
 
                         # add the candidate to the demand
                         # Pick only candidates from the restricted_region
                         # or restricted_complex
-                        if self.match_candidate_attribute(
-                                candidate,
-                                "location_id",
-                                restricted_region_id,
-                                name,
-                                inventory_type) or \
-                           self.match_candidate_attribute(
-                               candidate,
-                               "physical_location_id",
-                               restricted_complex_id,
-                               name,
-                               inventory_type):
-                            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'], name, triage_translator_data,
-                                                                         reason="match candidate attribute")
-
+                        if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
+                                                 triage_translator_data):
                             continue
                         else:
                             resolved_demands[name].append(candidate)
+                            LOG.debug(">>>>>>> Candidate <<<<<<<")
+                            LOG.debug(json.dumps(candidate, indent=4))
+
+                elif (inventory_type == 'vfmodule') \
+                        and customer_id:
+
+                    # First level query to get the list of generic vnfs
+                    vnf_by_model_invariant = list()
+                    if attributes and model_invariant_id:
+
+                        raw_path = '/network/generic-vnfs/' \
+                                   '?model-invariant-id={}&depth=0'.format(model_invariant_id)
+                        if model_version_id:
+                            raw_path = '/network/generic-vnfs/' \
+                                       '?model-invariant-id={}&model-version-id={}&depth=0'.format(model_invariant_id,
+                                                                                                   model_version_id)
+                        path = self._aai_versioned_path(raw_path)
+                        vnf_by_model_invariant = self.first_level_service_call(path, name, service_type)
+
+                    vnf_by_service_type = list()
+                    if service_type or equipment_role:
+                        path = self._aai_versioned_path(
+                            '/network/generic-vnfs/'
+                            '?equipment-role={}&depth=0'.format(service_type))
+                        vnf_by_service_type = self.first_level_service_call(path, name, service_type)
+
+                    generic_vnf = vnf_by_model_invariant + vnf_by_service_type
+                    vnf_dict = dict()
+
+                    for vnf in generic_vnf:
+                        # if this vnf already appears, skip it
+                        vnf_id = vnf.get('vnf-id')
+                        if vnf_id in vnf_dict:
+                            continue
+
+                        # add vnf (with vnf_id as key) to the dictionary
+                        vnf_dict[vnf_id] = vnf
+
+                        # create a default candidate
+                        candidate = dict()
+                        candidate['inventory_provider'] = 'aai'
+                        candidate['service_resource_id'] = service_resource_id
+                        candidate['inventory_type'] = inventory_type
+                        candidate['candidate_id'] = ''
+                        candidate['location_id'] = ''
+                        candidate['location_type'] = 'att_aic'
+                        candidate['host_id'] = ''
+                        candidate['cost'] = self.conf.data.service_candidate_cost
+                        candidate['cloud_owner'] = ''
+                        candidate['cloud_region_version'] = ''
+                        candidate['vlan_key'] = vlan_key
+                        candidate['port_key'] = port_key
+                        candidate['uniqueness'] = candidate_uniqueness
+
+                        # start populating the candidate
+                        candidate['host_id'] = vnf.get("vnf-name")
+
+                        candidate['nf-name'] = vnf.get("vnf-name")
+                        candidate['nf-id'] = vnf.get("vnf-id")
+                        candidate['nf-type'] = 'vnf'
+                        candidate['vnf-type'] = vnf.get("vnf-type")
+                        candidate['ipv4-oam-address'] = ''
+                        candidate['ipv6-oam-address'] = ''
+
+                        if vnf.get("ipv4-oam-address"):
+                            candidate['ipv4-oam-address'] = vnf.get("ipv4-oam-address")
+                        if vnf.get("ipv6-oam-address"):
+                            candidate['ipv6-oam-address'] = vnf.get("ipv6-oam-address")
+
+                        rl_data = self.resolve_global_customer_id_for_vnf(candidate, vnf, customer_id, service_type,
+                                                                          name, triage_translator_data)
+                        if rl_data is None:
+                            continue
+                        else:
+                            vs_cust_id = rl_data.get('d_value')
+
+                        rl_data = self.resolve_service_instance_id_for_vnf(candidate, vnf, customer_id,
+                                                                           service_type, name, triage_translator_data)
+                        if rl_data is None:
+                            continue
+                        else:
+                            vs_service_instance_id = rl_data.get('d_value')
+
+                        if vs_cust_id and vs_cust_id == customer_id:
+                            candidate['service_instance_id'] = vs_service_instance_id
+                        else:  # vserver is for a different customer
+                            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                                          candidate['location_id'], name,
+                                                                          triage_translator_data,
+                                                                          reason="candidate is for a different customer")
+                            continue
+
+                        vf_modules_list = self.resolve_vf_modules_for_generic_vnf(candidate, vnf, name,
+                                                                                  triage_translator_data)
+                        if vf_modules_list is None:
+                            continue
+
+                        candidate_base = candidate
+                        for vf_module in vf_modules_list:
+                            # for vfmodule demands we allow to have vfmodules from different cloud regions
+                            candidate = copy.deepcopy(candidate_base)
+                            candidate['candidate_id'] = vf_module.get("vf-module-id")
+                            candidate['vf-module-name'] = vf_module.get("vf-module-name")
+                            candidate['vf-module-id'] = vf_module.get("vf-module-id")
+
+                            rl_data = self.resolve_cloud_owner_for_vnf(candidate, vf_module, service_type, name,
+                                                                       triage_translator_data)
+                            if rl_data is None:
+                                continue
+                            else:
+                                cloud_owner = rl_data.get('d_value')
+                            candidate['cloud_owner'] = cloud_owner
+
+                            cloud_region_id_rl_data, cloud_region_version_rl_data = self.resolve_cloud_region_id_and_version_for_vnf(
+                                candidate, vf_module, service_type, name, triage_translator_data)
+
+                            if cloud_region_id_rl_data is None or cloud_region_version_rl_data is None:
+                                continue
+
+                            cloud_region_id = cloud_region_id_rl_data.get('d_value')
+                            cloud_region_version = cloud_region_version_rl_data.get('d_value')
+                            candidate['location_id'] = cloud_region_id
+                            candidate['cloud_region_version'] = cloud_region_version
+
+                            # Added vim-id for short-term workaround
+                            if self.conf.HPA_enabled:
+                                if not cloud_owner:
+                                    continue
+                                candidate['vim-id'] = \
+                                    candidate['cloud_owner'] + '_' + cloud_region_id
+
+                            if self.check_sriov_automation(
+                                    candidate['cloud_region_version'], name,
+                                    candidate['host_id']):
+                                candidate['sriov_automation'] = 'true'
+                            else:
+                                candidate['sriov_automation'] = 'false'
+
+                            # Second level query to get the pserver from vserver
+                            candidate['vservers'] = list()
+                            complex_list = list()
+                            vs_link_list = self.resolve_v_server_links_for_vnf(vf_module)
+
+                            for vs_link in vs_link_list:
+
+                                body = self.resolove_v_server_for_candidate(candidate, vs_link, True, name,
+                                                                            triage_translator_data)
+                                if body is None:
+                                    continue
+
+                                candidate_vserver = dict()
+                                candidate_vserver['vserver-id'] = body.get('vserver-id')
+                                candidate_vserver['vserver-name'] = body.get('vserver-name')
+
+                                rl_data = self.resolve_complex_info_link_for_v_server(candidate, body, cloud_owner,
+                                                                                      cloud_region_id, service_type,
+                                                                                      name, triage_translator_data)
+                                if rl_data is None:
+                                    continue
+
+                                #Interfaces info
+                                if not body.get('l-interfaces') or not body.get('l-interfaces').get('l-interface'):
+                                    self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                                                  candidate['location_id'], name,
+                                                                                  triage_translator_data,
+                                                                                  reason="VF-server interfaces error")
+                                    continue
+                                else:
+                                    l_interfaces = body.get('l-interfaces').get('l-interface')
+                                    candidate_vserver['l-interfaces'] = list()
+
+                                    for l_interface in l_interfaces:
+                                        vserver_interface = dict()
+                                        vserver_interface['interface-id'] = l_interface.get('interface-id')
+                                        vserver_interface['interface-name'] = l_interface.get('interface-name')
+                                        vserver_interface['macaddr'] = l_interface.get('macaddr')
+                                        vserver_interface['network-id'] = l_interface.get('network-name')
+                                        vserver_interface['network-name'] = ''
+                                        vserver_interface['ipv4-addresses'] = list()
+                                        vserver_interface['ipv6-addresses'] = list()
+
+                                        if l_interface.get('l3-interface-ipv4-address-list'):
+                                            for ip_address_info in l_interface.get('l3-interface-ipv4-address-list'):
+                                                vserver_interface['ipv4-addresses'].\
+                                                    append(ip_address_info.get('l3-interface-ipv4-address'))
+
+                                        if l_interface.get('l3-interface-ipv6-address-list'):
+                                            for ip_address_info in l_interface.get('l3-interface-ipv6-address-list'):
+                                                vserver_interface['ipv6-addresses'].\
+                                                    append(ip_address_info.get('l3-interface-ipv6-address'))
+
+                                        candidate_vserver['l-interfaces'].append(vserver_interface)
+
+                                complex_list.append(rl_data)
+                                candidate['vservers'].append(candidate_vserver)
+
+                            self.build_complex_info_for_candidate(candidate, vnf, complex_list, service_type, name,
+                                                                  triage_translator_data)
+
+                            if candidate.get("complex_name") is None:
+                                continue
+
+                            ##add vf-module parameters for filtering
+                            vnf_vf_module_inventory = copy.deepcopy(vnf)
+                            vnf_vf_module_inventory.update(vf_module)
+                            # add specifal parameters for comparsion
+                            vnf_vf_module_inventory['global-customer-id'] = customer_id
+                            vnf_vf_module_inventory['customer-id'] = customer_id
+                            vnf_vf_module_inventory['cloud-region-id'] = cloud_region_id
+                            vnf_vf_module_inventory['physical-location-id'] = candidate.get('physical_location_id')
+                            vnf_vf_module_inventory['service_instance_id'] = vs_service_instance_id
+
+                            if attributes and not self.match_inventory_attributes(attributes, vnf_vf_module_inventory,
+                                                                                  candidate['candidate_id']):
+                                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'],
+                                                                              candidate['location_id'], name,
+                                                                              triage_translator_data,
+                                                                              reason="attibute check error")
+                                continue
+                            self.assign_candidate_existing_placement(candidate, existing_placement)
+
+                            # Pick only candidates not in the excluded list
+                            # if excluded candidate list is provided
+                            if excluded_candidates and self.match_candidate_by_list(candidate, excluded_candidates, True,
+                                                                                    name, triage_translator_data):
+                                continue
+
+                            # Pick only candidates in the required list
+                            # if required candidate list is provided
+                            if required_candidates and not self.match_candidate_by_list(candidate, required_candidates,
+                                                                                        False, name,
+                                                                                        triage_translator_data):
+                                continue
+
+                            # add the candidate to the demand
+                            # Pick only candidates from the restricted_region
+                            # or restricted_complex
+                            if not self.match_region(candidate, restricted_region_id, restricted_complex_id, name,
+                                                     triage_translator_data):
+                                continue
+                            else:
+                                resolved_demands[name].append(candidate)
+                                LOG.debug(">>>>>>> Candidate <<<<<<<")
+                                LOG.debug(json.dumps(candidate, indent=4))
 
                 elif inventory_type == 'transport' \
                      and customer_id and service_type and \
@@ -1578,6 +1878,49 @@ class AAI(base.InventoryProviderBase):
                               " {}".format(inventory_type))
         return resolved_demands
 
+    def match_region(self, candidate, restricted_region_id, restricted_complex_id, demand_name, triage_translator_data):
+        if self.match_candidate_attribute(
+                candidate,
+                "location_id",
+                restricted_region_id,
+                demand_name,
+                candidate.get('inventory_type')) or \
+                self.match_candidate_attribute(
+                    candidate,
+                    "physical_location_id",
+                    restricted_complex_id,
+                    demand_name,
+                    candidate.get('inventory_type')):
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
+                                                          demand_name, triage_translator_data,
+                                                          reason="candidate region does not match")
+            return False
+        else:
+            return True
+
+    def match_candidate_by_list(self, candidate, candidates_list, exclude, demand_name, triage_translator_data):
+        has_candidate = False
+        if candidates_list:
+            for list_candidate in candidates_list:
+                if list_candidate \
+                        and list_candidate.get('inventory_type') \
+                        == candidate.get('inventory_type') \
+                        and list_candidate.get('candidate_id') \
+                        == candidate.get('candidate_id'):
+                    has_candidate = True
+                    break
+
+        if not exclude:
+            if not has_candidate:
+                self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
+                                                              demand_name, triage_translator_data,
+                                                              reason="has_required_candidate candidate")
+        elif has_candidate:
+            self.triage_translator.collectDroppedCandiate(candidate['candidate_id'], candidate['location_id'],
+                                                          demand_name, triage_translator_data,
+                                                          reason="excluded candidate")
+        return has_candidate
+
     def match_hpa(self, candidate, features):
         """Match HPA features requirement with the candidate flavors """
         hpa_provider = hpa_utils.HpaMatchProvider(candidate, features)
index 832b4f8..09756d2 100644 (file)
@@ -240,6 +240,7 @@ class DataEndpoint(object):
 
         cloud_requests = value.get("cloud-requests")
         service_requests = value.get("service-requests")
+        vfmodule_requests = value.get("vfmodule-requests")
 
         for candidate in candidate_list:
             if candidate.get("inventory_type") == "cloud" and \
@@ -250,6 +251,9 @@ class DataEndpoint(object):
                     (candidate.get(value_attrib) not in service_requests):
                 discard_set.add(candidate.get("candidate_id"))
 
+            elif candidate.get("inventory_type") == "vfmodule" and \
+                    (candidate.get(value_attrib) not in vfmodule_requests):
+                discard_set.add(candidate.get("candidate_id"))
 
         return discard_set
 
@@ -671,6 +675,7 @@ class DataEndpoint(object):
                 clli_code
             )
         else:
+            results = None
             # unknown location response
             LOG.error(_LE("Unknown location type from the input template."
                           "Expected location types are host_name"
index ee16482..c2a5206 100644 (file)
@@ -56,6 +56,11 @@ class Service(constraint.Constraint):
                     candidates_to_check.append(candidate)
                 else:
                     select_list.append(candidate)
+            elif self.inventory_type == "vfmodule":
+                if candidate["inventory_type"] == "vfmodule":
+                    candidates_to_check.append(candidate)
+                else:
+                    select_list.append(candidate)
         # call conductor data with request parameters
         if len(candidates_to_check) > 0:
             cei = _request.cei
index a36124c..7909c15 100755 (executable)
@@ -22,7 +22,6 @@ from oslo_log import log
 import copy
 import time
 
-
 from conductor import service
 # from conductor.solver.optimizer import decision_path as dpath
 # from conductor.solver.optimizer import best_first
@@ -74,7 +73,7 @@ class Optimizer(object):
 
     def get_solution(self, num_solutions):
 
-        LOG.debug("search start")
+        LOG.debug("search start for max {} solutions".format(num_solutions))
         for rk in self.requests:
             request = self.requests[rk]
             LOG.debug("--- request = {}".format(rk))
@@ -88,7 +87,8 @@ class Optimizer(object):
 
             LOG.debug("2. search")
 
-            while (num_solutions == 'all' or num_solutions > 0):
+            rand_counter = 10
+            while num_solutions == 'all' or num_solutions > 0:
 
                 LOG.debug("searching for the solution {}".format(len(decision_list) + 1))
 
@@ -106,19 +106,28 @@ class Optimizer(object):
                     best_path = self.search.search(demand_list,
                                                    request.objective, request)
 
+                LOG.debug("search delay = {} sec".format(time.time() - st))
+
+                demand_list = copy.deepcopy(_copy_demand_list)
+
                 if best_path is not None:
                     self.search.print_decisions(best_path)
+                    rand_counter = 10
+                elif not request.objective.goal and rand_counter > 0 and self._has_candidates(request):
+                    # RandomPick gave no candidates after applying constraints. If there are any candidates left
+                    # lets' try again several times until some solution is found. When one of the demands is not unique
+                    # it persists in the list all the time. In order to prevent infinite loop we need to have counter
+                    rand_counter -= 1
+                    LOG.debug("Incomplete random solution - repeat {}".format(rand_counter))
+                    continue
                 else:
                     LOG.debug("no solution found")
                     break
 
-                LOG.debug("search delay = {} sec".format(time.time() - st))
-
                 # add the current solution to decision_list
                 decision_list.append(best_path.decisions)
 
                 #remove the candidate with "uniqueness = true"
-                demand_list = copy.deepcopy(_copy_demand_list)
                 self._remove_unique_candidate(request, best_path, demand_list)
 
                 if num_solutions != 'all':
@@ -126,6 +135,15 @@ class Optimizer(object):
             self.search.triageSolver.getSolution(decision_list)
             return decision_list
 
+    def _has_candidates(self, request):
+        for demand_name, demand in request.demands.items():
+            LOG.debug("Req Available resources: {} {}".format(demand_name, len(request.demands[demand_name].resources)))
+            if len(demand.resources) == 0:
+                LOG.debug("No more candidates for demand {}".format(demand_name))
+                return False
+
+        return True
+
     def _remove_unique_candidate(self, _request, current_decision, demand_list):
 
         # This method is to remove previous solved/used candidate from consideration
index 79750a6..7fb2ced 100644 (file)
@@ -45,9 +45,15 @@ class RandomPick(search.Search):
             _decision_path.current_demand = demand
             candidate_list = self._solve_constraints(_decision_path, _request)
 
-            # random pick one candidate
-            r_index = randint(0, len(candidate_list) - 1)
-            best_resource = candidate_list[r_index]
-            _decision_path.decisions[demand.name] = best_resource
+            # When you have two demands and for one there are no candidates left after filtering by constraints
+            # the code tries to randomly choose index randint(0, -1) what raises an exception. None is returned
+            # in order to prevent that and this case is considered in the Optimizer respectively
+            if len(candidate_list) > 0:
+                # random pick one candidate
+                r_index = randint(0, len(candidate_list) - 1)
+                best_resource = candidate_list[r_index]
+                _decision_path.decisions[demand.name] = best_resource
+            else:
+                return None
 
         return _decision_path
index 8647e4b..978f735 100644 (file)
@@ -509,6 +509,27 @@ class SolverService(cotyledon.Service):
                             if resource.get('port_key'):
                                 rec["attributes"]['port_key'] = resource.get('port_key')
 
+                        if rec["candidate"]["inventory_type"] == "vfmodule":
+                            rec["attributes"]["host_id"] = resource.get("host_id")
+                            rec["attributes"]["service_instance_id"] = resource.get("service_instance_id")
+                            rec["candidate"]["host_id"] = resource.get("host_id")
+
+                            if resource.get('vlan_key'):
+                                rec["attributes"]['vlan_key'] = resource.get('vlan_key')
+                            if resource.get('port_key'):
+                                rec["attributes"]['port_key'] = resource.get('port_key')
+
+                            vf_module_data = rec["attributes"]
+                            vf_module_data['nf-name'] = resource.get("nf-name")
+                            vf_module_data['nf-id'] = resource.get("nf-id")
+                            vf_module_data['nf-type'] = resource.get("nf-type")
+                            vf_module_data['vnf-type'] = resource.get("vnf-type")
+                            vf_module_data['vf-module-id'] = resource.get("vf-module-id")
+                            vf_module_data['vf-module-name'] = resource.get("vf-module-name")
+                            vf_module_data['ipv4-oam-address'] = resource.get("ipv4-oam-address")
+                            vf_module_data['ipv6-oam-address'] = resource.get("ipv6-oam-address")
+                            vf_module_data['vservers'] = resource.get("vservers")
+
                         elif rec["candidate"]["inventory_type"] == "cloud":
                             if resource.get("all_directives") and resource.get("flavor_map"):
                                 rec["attributes"]["directives"] = \
index 9682c7d..2eea9b5 100644 (file)
@@ -165,9 +165,100 @@ class TestNoExceptionTranslator(unittest.TestCase):
 
         self.assertEquals(self.Translator.parse_demands(demands), rtn)
 
+    @patch('conductor.common.music.messaging.component.RPCClient.call')
+    def test_parse_demands_inventory_type_vfmodule(self, mock_call):
+        TraigeTranslator.thefinalCallTrans = mock.MagicMock(return_value=None)
+        demands = {
+            "vFW-SINK": [{
+                "service_resource_id": "vFW-SINK-XX",
+                "inventory_provider": "aai",
+                "inventory_type": "vfmodule",
+                "vlan_key": "vlan_key",
+                "port_key": "vlan_port",
+                "excluded_candidates": [{
+                    "candidate_id": ["e765d576-8755-4145-8536-0bb6d9b1dc9a"],
+                    "inventory_type": "vfmodule"
+                }],
+                "attributes": {
+                    "prov-status": "ACTIVE",
+                    "global-customer-id": "Demonstration",
+                    "model-version-id": "763731df-84fd-494b-b824-01fc59a5ff2d",
+                    "model-invariant-id": "e7227847-dea6-4374-abca-4561b070fe7d",
+                    "orchestration-status": ["active"],
+                    "cloud-region-id": {
+                        "get_param": "chosen_region"
+                    },
+                    "service_instance_id": {
+                        "get_param": "service_id"
+                    }
+                },
+                "service_type": "vFW-SINK-XX"
+            }]
+        }
+        self.Translator._plan_id = ""
+        self.Translator._plan_name = ""
+        mock_call.return_value = {'resolved_demands': {
+            "vFW-SINK": [{
+                "service_resource_id": "vFW-SINK-XX",
+                "inventory_provider": "aai",
+                "inventory_type": "vfmodule",
+                "vlan_key": "vlan_key",
+                "port_key": "vlan_port",
+                "excluded_candidates": [{
+                    "candidate_id": ["e765d576-8755-4145-8536-0bb6d9b1dc9a"],
+                    "inventory_type": "vfmodule"
+                }],
+                "attributes": {
+                    "prov-status": "ACTIVE",
+                    "global-customer-id": "Demonstration",
+                    "model-version-id": "763731df-84fd-494b-b824-01fc59a5ff2d",
+                    "model-invariant-id": "e7227847-dea6-4374-abca-4561b070fe7d",
+                    "orchestration-status": ["active"],
+                    "cloud-region-id": {
+                        "get_param": "chosen_region"
+                    },
+                    "service_instance_id": {
+                        "get_param": "service_id"
+                    }
+                },
+                "service_type": "vFW-SINK-XX"
+            }]
+        }}
+        rtn = {
+            "vFW-SINK": {
+                "candidates": [{
+                    "excluded_candidates": [{
+                        "candidate_id": ["e765d576-8755-4145-8536-0bb6d9b1dc9a"],
+                        "inventory_type": "vfmodule"
+                    }],
+                    "port_key": "vlan_port",
+                    "service_resource_id": "vFW-SINK-XX",
+                    "vlan_key": "vlan_key",
+                    "service_type": "vFW-SINK-XX",
+                    "attributes": {
+                        "cloud-region-id": {
+                            "get_param": "chosen_region"
+                        },
+                        "model-version-id": "763731df-84fd-494b-b824-01fc59a5ff2d",
+                        "service_instance_id": {
+                            "get_param": "service_id"
+                        },
+                        "orchestration-status": ["active"],
+                        "global-customer-id": "Demonstration",
+                        "prov-status": "ACTIVE",
+                        "model-invariant-id": "e7227847-dea6-4374-abca-4561b070fe7d"
+                    },
+                    "inventory_provider": "aai",
+                    "inventory_type": "vfmodule"
+                }]
+            }
+        }
+
+        self.assertEquals(self.Translator.parse_demands(demands), rtn)
+
     @patch('conductor.common.music.messaging.component.RPCClient.call')
     def test_parse_demands_without_candidate(self, mock_call):
-        TraigeTranslator.thefinalCallTrans = mock.MagicMock(return_value=None)  
+        TraigeTranslator.thefinalCallTrans = mock.MagicMock(return_value=None)
         demands = {
             "vGMuxInfra": [{
                 "inventory_provider": "aai",
diff --git a/conductor/conductor/tests/unit/data/demands_vfmodule.json b/conductor/conductor/tests/unit/data/demands_vfmodule.json
new file mode 100644 (file)
index 0000000..78cb7d4
--- /dev/null
@@ -0,0 +1,35 @@
+{
+    "demands": {
+        "vFW-SINK": [{
+            "service_resource_id": "vFW-SINK-XX",
+            "inventory_provider": "aai",
+            "inventory_type": "vfmodule",
+            "vlan_key": "vlan_key",
+            "port_key": "vlan_port",
+            "excluded_candidates": [{
+                "candidate_id": "e765d576-8755-4145-8536-0bb6d9b1dc9a",
+                "inventory_type": "vfmodule"
+            }],
+            "attributes": {
+                "prov-status": "ACTIVE",
+                "global-customer-id": "Demonstration",
+                "model-version-id": "763731df-84fd-494b-b824-01fc59a5ff2d",
+                "model-invariant-id": "e7227847-dea6-4374-abca-4561b070fe7d",
+                "orchestration-status": ["active"],
+                "cloud-region-id": {
+                    "get_param": "chosen_region"
+                },
+                "service_instance_id": {
+                    "get_param": "service_id"
+                }
+            },
+            "service_type": "vFW-SINK-XX"
+        }]
+    },
+    "triage_translator_data": {
+        "plan_id": "plan_abc",
+        "plan_name": "plan_name",
+        "translator_triage": [],
+        "dropped_candidates": []
+    }
+}
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/bad_generic_vnf_list.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/bad_generic_vnf_list.json
new file mode 100644 (file)
index 0000000..ed86a1b
--- /dev/null
@@ -0,0 +1,127 @@
+[{
+    "vnf-id": "fcbff633-47cc-4f38-a98d-4ba8285bd8b6",
+    "vnf-name": "vFW-PKG-MC",
+    "vnf-type": "5G_EVE_Demo/5G_EVE_PKG 0",
+    "service-id": "f5728144-f4a2-4bf8-9f0e-4ee924235c42",
+    "prov-status": "ACTIVE",
+    "orchestration-status": "Active",
+    "ipv4-oam-address": "oam_network_zb4J",
+    "in-maint": false,
+    "is-closed-loop-disabled": false,
+    "resource-version": "1554713856131",
+    "model-invariant-id": "762472ef-5284-4daa-ab32-3e7bee2ec355",
+    "model-version-id": "e02a7e5c-9d27-4360-ab7c-73bb83b07e3b",
+    "model-customization-id": "83f1f72e-26ea-4e32-8dae-a7b3a833c7f6",
+    "nf-type": "",
+    "nf-function": "",
+    "nf-role": "",
+    "nf-naming-code": "",
+    "relationship-list": {
+        "relationship": [{
+                "related-to": "service-instance",
+                "relationship-label": "org.onap.relationships.inventory.ComposedOf",
+                "related-link": "/aai/v14/business/customers/customer/Demonstration/service-subscriptions/service-subscription/vFW/service-instances/service-instance/3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c",
+                "relationship-data": [{
+                        "relationship-key": "customer.global-customer-id",
+                        "relationship-value": "Demonstration"
+                    },
+                    {
+                        "relationship-key": "service-subscription.service-type",
+                        "relationship-value": "vFW"
+                    },
+                    {
+                        "relationship-key": "service-instance.service-instance-id",
+                        "relationship-value": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c"
+                    }
+                ],
+                "related-to-property": [{
+                    "property-key": "service-instance.service-instance-name",
+                    "property-value": "vFW-R1-Tenant1-LR"
+                }]
+            },
+            {
+                "related-to": "service-instance",
+                "relationship-label": "org.onap.relationships.inventory.ComposedOf",
+                "related-link": "/aai/v14/business/customers/customer/Demonstration/service-subscriptions/service-subscription/vFW/service-instances/service-instance/3e8d118c-10ca-4b4b-b3db-089b5e9e6a1b",
+                "relationship-data": [{
+                        "relationship-key": "customer.global-customer-id",
+                        "relationship-value": "Demonstration"
+                    },
+                    {
+                        "relationship-key": "service-subscription.service-type",
+                        "relationship-value": "vFW"
+                    },
+                    {
+                        "relationship-key": "service-instance.service-instance-id",
+                        "relationship-value": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1b"
+                    }
+                ],
+                "related-to-property": [{
+                    "property-key": "service-instance.service-instance-name",
+                    "property-value": "vFW-R1-Tenant1-LK"
+                }]
+            },
+            {
+                "related-to": "platform",
+                "relationship-label": "org.onap.relationships.inventory.Uses",
+                "related-link": "/aai/v14/business/platforms/platform/Platform-Demonstration",
+                "relationship-data": [{
+                    "relationship-key": "platform.platform-name",
+                    "relationship-value": "Platform-Demonstration"
+                }]
+            },
+            {
+                "related-to": "vserver",
+                "relationship-label": "tosca.relationships.HostedOn",
+                "related-link": "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner/RegionOne/tenants/tenant/3c6c471ada7747fe8ff7f28e100b61e8/vservers/vserver/00bddefc-126e-4e4f-a18d-99b94d8d9a30",
+                "relationship-data": [{
+                        "relationship-key": "cloud-region.cloud-owner",
+                        "relationship-value": "CloudOwner"
+                    },
+                    {
+                        "relationship-key": "cloud-region.cloud-region-id",
+                        "relationship-value": "RegionOne"
+                    },
+                    {
+                        "relationship-key": "tenant.tenant-id",
+                        "relationship-value": "3c6c471ada7747fe8ff7f28e100b61e8"
+                    },
+                    {
+                        "relationship-key": "vserver.vserver-id",
+                        "relationship-value": "00bddefc-126e-4e4f-a18d-99b94d8d9a30"
+                    }
+                ],
+                "related-to-property": [{
+                    "property-key": "vserver.vserver-name",
+                    "property-value": "zdfw1fwl01pgn01"
+                }]
+            },
+            {
+                "related-to": "vserver",
+                "relationship-label": "tosca.relationships.HostedOn",
+                "related-link": "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner2/RegionOne2/tenants/tenant/3c6c471ada7747fe8ff7f28e100b61e8/vservers/vserver/00bddefc-126e-4e4f-a18d-99b94d8d9a31",
+                "relationship-data": [{
+                        "relationship-key": "cloud-region.cloud-owner",
+                        "relationship-value": "CloudOwner2"
+                    },
+                    {
+                        "relationship-key": "cloud-region.cloud-region-id",
+                        "relationship-value": "RegionOne2"
+                    },
+                    {
+                        "relationship-key": "tenant.tenant-id",
+                        "relationship-value": "3c6c471ada7747fe8ff7f28e100b61e8"
+                    },
+                    {
+                        "relationship-key": "vserver.vserver-id",
+                        "relationship-value": "00bddefc-126e-4e4f-a18d-99b94d8d9a31"
+                    }
+                ],
+                "related-to-property": [{
+                    "property-key": "vserver.vserver-name",
+                    "property-value": "zdfw1fwl01pgn02"
+                }]
+            }
+        ]
+    }
+}]
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/service_candidates.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/service_candidates.json
new file mode 100644 (file)
index 0000000..8d39aaa
--- /dev/null
@@ -0,0 +1,28 @@
+{
+    "vPGN": [{
+        "existing_placement": "false",
+        "service_resource_id": "vPGN-XX",
+        "inventory_provider": "aai",
+        "cost": 1.0,
+        "cloud_owner": "CloudOwner",
+        "city": "example-city-val-27150",
+        "uniqueness": "true",
+        "location_id": "RegionOne",
+        "inventory_type": "service",
+        "sriov_automation": "false",
+        "state": "example-state-val-59487",
+        "candidate_id": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c",
+        "latitude": "example-latitude-val-89101",
+        "physical_location_id": "clli1",
+        "port_key": "vlan_port",
+        "vlan_key": "vlan_key",
+        "cloud_region_version": "1",
+        "host_id": "vFW-PKG-MC",
+        "complex_name": "clli1",
+        "location_type": "att_aic",
+        "country": "example-country-val-94173",
+        "region": "example-region-val-13893",
+        "longitude": "32.89948",
+        "vim-id": "CloudOwner_RegionOne"
+    }]
+}
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/service_demand_list.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/service_demand_list.json
new file mode 100644 (file)
index 0000000..fb1059a
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "vPGN": [{
+        "service_resource_id": "vPGN-XX",
+        "inventory_provider": "aai",
+        "inventory_type": "service",
+        "port_key": "vlan_port",
+        "vlan_key": "vlan_key",
+        "attributes": {
+            "global-customer-id": "Demonstration",
+            "model-version-id": "e02a7e5c-9d27-4360-ab7c-73bb83b07e3b",
+            "model-invariant-id": "762472ef-5284-4daa-ab32-3e7bee2ec355"
+        },
+        "service_type": "vPGN-XX"
+    }]
+}
\ No newline at end of file
index a586bc3..906897c 100644 (file)
@@ -18,6 +18,7 @@
 #
 import json
 import unittest
+import copy
 
 import conductor.data.plugins.inventory_provider.aai as aai
 import mock
@@ -103,7 +104,7 @@ class TestAAI(unittest.TestCase):
         self.assertEqual({'country': u'USA', 'latitude': u'28.543251', 'longitude': u'-81.377112'} ,
                          self.aai_ep.resolve_host_location("host_name"))
 
-    def test_resolve_demands(self):
+    def test_resolve_demands_inventory_type_cloud(self):
 
         self.aai_ep.conf.HPA_enabled = True
         TraigeTranslator.getPlanIdNAme = mock.MagicMock(return_value=None)
@@ -141,8 +142,22 @@ class TestAAI(unittest.TestCase):
         self.mock_get_regions = mock.patch.object(AAI, '_get_regions', return_value=regions_response)
         self.mock_get_regions.start()
 
-        self.mock_get_regions = mock.patch.object(AAI, '_request', return_value=req_response)
-        self.mock_get_regions.start()
+        regions_list = list()
+        regions_list.append(regions_response.get('region-name'))
+        self.mock_resolve_cloud_regions_by_cloud_region_id = mock.patch.object(AAI,
+                                                                               'resolve_cloud_regions_by_cloud_region_id',
+                                                                               return_value=regions_list)
+        self.mock_resolve_cloud_regions_by_cloud_region_id.start()
+
+        self.mock_resolove_v_server_for_candidate = mock.patch.object(AAI, 'resolove_v_server_for_candidate',
+                                                                      return_value=demand_service_response)
+        self.mock_resolove_v_server_for_candidate.start()
+
+        complex_link = {"link": "/aai/v10/complex-id", "d_value": 'test-id'}
+        self.mock_resolve_complex_info_link_for_v_server = mock.patch.object(AAI,
+                                                                             'resolve_complex_info_link_for_v_server',
+                                                                             return_value=complex_link)
+        self.mock_resolve_complex_info_link_for_v_server.start()
 
         self.mock_get_complex = mock.patch.object(AAI, '_get_complex', return_value=complex_json)
         self.mock_get_complex.start()
@@ -179,6 +194,154 @@ class TestAAI(unittest.TestCase):
             self.aai_ep.resolve_demands(demands_list, plan_info=plan_info,
                                         triage_translator_data=triage_translator_data))
 
+    def test_resolve_demands_inventory_type_service(self):
+        self.aai_ep.conf.HPA_enabled = True
+        TraigeTranslator.getPlanIdNAme = mock.MagicMock(return_value=None)
+        TraigeTranslator.addDemandsTriageTranslator = mock.MagicMock(return_value=None)
+
+        plan_info = {
+            'plan_name': 'name',
+            'plan_id': 'id'
+        }
+        triage_translator_data = None
+
+        demands_list_file = './conductor/tests/unit/data/plugins/inventory_provider/service_demand_list.json'
+        demands_list = json.loads(open(demands_list_file).read())
+
+        generic_vnf_list_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_service_generic_vnf_list.json'
+        generic_vnf_list = json.loads(open(generic_vnf_list_file).read())
+
+        v_server_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_vserver.json'
+        v_server = json.loads(open(v_server_file).read())
+
+        demand_service_response_file = './conductor/tests/unit/data/plugins/inventory_provider/resolve_demand_service_response.json'
+        demand_service_response = json.loads(open(demand_service_response_file).read())
+
+        complex_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_complex.json'
+        complex_response = json.loads(open(complex_file).read())
+
+        region_response_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_region.json'
+        region_response = json.loads(open(region_response_file).read())
+
+        results_file = './conductor/tests/unit/data/plugins/inventory_provider/service_candidates.json'
+        results_json = json.loads(open(results_file).read())
+
+        req_response = mock.MagicMock()
+        req_response.status_code = 200
+        req_response.ok = True
+        req_response.json.return_value = demand_service_response
+
+        def mock_first_level_service_call_response(path, name, service_type):
+            if "equipment-role" in path:
+                return list()
+            else:
+                return generic_vnf_list
+
+        self.mock_first_level_service_call = mock.patch.object(AAI, 'first_level_service_call',
+                                                               side_effect=mock_first_level_service_call_response)
+        self.mock_first_level_service_call.start()
+
+        regions = list()
+        regions.append(region_response)
+        self.mock_resolve_cloud_regions_by_cloud_region_id = mock.patch.object(AAI,
+                                                                               'resolve_cloud_regions_by_cloud_region_id',
+                                                                               return_value=regions)
+        self.mock_resolve_cloud_regions_by_cloud_region_id.start()
+
+        self.mock_resolove_v_server_for_candidate = mock.patch.object(AAI, 'resolove_v_server_for_candidate',
+                                                                      return_value=v_server)
+        self.mock_resolove_v_server_for_candidate.start()
+
+        complex_link = {"link": "/aai/v14/cloud-infrastructure/complexes/complex/clli1", "d_value": 'clli1'}
+        self.mock_resolve_complex_info_link_for_v_server = mock.patch.object(AAI,
+                                                                             'resolve_complex_info_link_for_v_server',
+                                                                             return_value=complex_link)
+        self.mock_resolve_complex_info_link_for_v_server.start()
+
+        self.mock_get_complex = mock.patch.object(AAI, '_get_complex', return_value=complex_response)
+        self.mock_get_complex.start()
+
+        self.maxDiff = None
+        self.assertEqual(results_json, self.aai_ep.resolve_demands(demands_list, plan_info=plan_info,
+                                         triage_translator_data=triage_translator_data))
+
+    def test_resolve_demands_inventory_type_vfmodule(self):
+        self.aai_ep.conf.HPA_enabled = True
+        TraigeTranslator.getPlanIdNAme = mock.MagicMock(return_value=None)
+        TraigeTranslator.addDemandsTriageTranslator = mock.MagicMock(return_value=None)
+
+        plan_info = {
+            'plan_name': 'name',
+            'plan_id': 'id'
+        }
+        triage_translator_data = None
+
+        demands_list_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_demand_list.json'
+        demands_list = json.loads(open(demands_list_file).read())
+
+        generic_vnf_list_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_service_generic_vnf_list.json'
+        generic_vnf_list = json.loads(open(generic_vnf_list_file).read())
+
+        vfmodules_list_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_list.json'
+        vfmodules_list = json.loads(open(vfmodules_list_file).read())
+
+        v_server_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_vserver.json'
+        v_server = json.loads(open(v_server_file).read())
+
+        demand_service_response_file = './conductor/tests/unit/data/plugins/inventory_provider/resolve_demand_service_response.json'
+        demand_service_response = json.loads(open(demand_service_response_file).read())
+
+        complex_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_complex.json'
+        complex_response = json.loads(open(complex_file).read())
+
+        region_response_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_region.json'
+        region_response = json.loads(open(region_response_file).read())
+
+        results_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_candidates.json'
+        results_json = json.loads(open(results_file).read())
+
+        req_response = mock.MagicMock()
+        req_response.status_code = 200
+        req_response.ok = True
+        req_response.json.return_value = demand_service_response
+
+        def mock_first_level_service_call_response(path, name, service_type):
+            if "equipment-role" in path:
+                return list()
+            else:
+                return generic_vnf_list
+
+        self.mock_first_level_service_call = mock.patch.object(AAI, 'first_level_service_call',
+                                                               side_effect=mock_first_level_service_call_response)
+        self.mock_first_level_service_call.start()
+
+        self.mock_resolve_vf_modules_for_generic_vnf = mock.patch.object(AAI, 'resolve_vf_modules_for_generic_vnf',
+                                                                         return_value=vfmodules_list)
+        self.mock_resolve_vf_modules_for_generic_vnf.start()
+
+        regions = list()
+        regions.append(region_response)
+        self.mock_resolve_cloud_regions_by_cloud_region_id = mock.patch.object(AAI,
+                                                                               'resolve_cloud_regions_by_cloud_region_id',
+                                                                               return_value=regions)
+        self.mock_resolve_cloud_regions_by_cloud_region_id.start()
+
+        self.mock_resolove_v_server_for_candidate = mock.patch.object(AAI, 'resolove_v_server_for_candidate',
+                                                                       return_value=v_server)
+        self.mock_resolove_v_server_for_candidate.start()
+
+        complex_link = {"link": "/aai/v14/cloud-infrastructure/complexes/complex/clli1", "d_value": 'clli1'}
+        self.mock_resolve_complex_info_link_for_v_server = mock.patch.object(AAI, 'resolve_complex_info_link_for_v_server',
+                                                                             return_value=complex_link)
+        self.mock_resolve_complex_info_link_for_v_server.start()
+
+        self.mock_get_complex = mock.patch.object(AAI, '_get_complex', return_value=complex_response)
+        self.mock_get_complex.start()
+
+        self.maxDiff = None
+        self.assertEqual(results_json, self.aai_ep.resolve_demands(demands_list, plan_info=plan_info,
+                                         triage_translator_data=triage_translator_data))
+
     def test_get_complex(self):
 
         complex_json_file = './conductor/tests/unit/data/plugins/inventory_provider/_request_get_complex.json'
@@ -312,6 +475,167 @@ class TestAAI(unittest.TestCase):
                                                 "mock-cloud-region-id")
         self.assertEqual(2, len(flavors_info['flavor']))
 
+    def test_resolve_complex_info_link_for_v_server(self):
+        TraigeTranslator.collectDroppedCandiate = mock.MagicMock(return_value=None)
+        triage_translator_data = None
+        demand_name = 'vPGN'
+        service_type = 'vFW'
+        cloud_owner = 'CloudOwner'
+        cloud_region_id = 'RegionOne'
+        v_server_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_vserver.json'
+        v_server = json.loads(open(v_server_file).read())
+        region_response_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_region.json'
+        region_response = json.loads(open(region_response_file).read())
+
+        candidate = dict()
+        candidate['candidate_id'] = 'some_id'
+        candidate['location_id'] = 'some_location_id'
+        candidate['inventory_type'] = 'service'
+
+        response = mock.MagicMock()
+        response.status_code = 200
+        response.ok = True
+        response.json.return_value = region_response
+
+        self.mock_get_request = mock.patch.object(AAI, '_request', return_value=response)
+        self.mock_get_request.start()
+
+        link_rl_data = self.aai_ep.resolve_complex_info_link_for_v_server(candidate, v_server, None,
+                                                                          cloud_region_id, service_type,
+                                                                          demand_name, triage_translator_data)
+        self.assertEqual(None, link_rl_data)
+
+        complex_link = {"link": "/aai/v14/cloud-infrastructure/complexes/complex/clli1", "d_value": 'clli1'}
+        link_rl_data = self.aai_ep.resolve_complex_info_link_for_v_server(candidate, v_server, cloud_owner,
+                                                                          cloud_region_id, service_type,
+                                                                          demand_name, triage_translator_data)
+        self.assertEqual(complex_link, link_rl_data)
+
+    def test_build_complex_info_for_candidate(self):
+        TraigeTranslator.collectDroppedCandiate = mock.MagicMock(return_value=None)
+        triage_translator_data = None
+        demand_name = 'vPGN'
+        service_type = 'vFW'
+        complex_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_complex.json'
+        complex_response = json.loads(open(complex_file).read())
+
+        candidate = dict()
+        candidate['candidate_id'] = 'some_id'
+        candidate['location_id'] = 'some_location_id'
+        candidate['inventory_type'] = 'service'
+        initial_candidate = copy.deepcopy(candidate)
+        complex_list_empty = dict()
+        complex_list = list()
+        complex_list.append({"link": "/aai/v14/cloud-infrastructure/complexes/complex/clli1", "d_value": 'clli1'})
+        complex_list.append({"link": "/aai/v14/cloud-infrastructure/complexes/complex/clli2", "d_value": 'clli2'})
+
+        self.mock_get_complex = mock.patch.object(AAI, '_get_complex', return_value=complex_response)
+        self.mock_get_complex.start()
+
+        self.aai_ep.build_complex_info_for_candidate(candidate, None, complex_list_empty, service_type, demand_name,
+                                                     triage_translator_data)
+        self.assertEqual(initial_candidate, candidate)
+        self.assertEqual(1, TraigeTranslator.collectDroppedCandiate.call_count)
+
+        self.aai_ep.build_complex_info_for_candidate(candidate, None, complex_list, service_type, demand_name,
+                                                     triage_translator_data)
+        self.assertEqual(initial_candidate, candidate)
+        self.assertEqual(2, TraigeTranslator.collectDroppedCandiate.call_count)
+
+        complex_list.pop()
+        self.aai_ep.build_complex_info_for_candidate(candidate, None, complex_list, service_type, demand_name,
+                                                     triage_translator_data)
+
+        self.assertEqual(candidate, {'city': u'example-city-val-27150', 'country': u'example-country-val-94173',
+                                     'region': u'example-region-val-13893', 'inventory_type': 'service',
+                                     'longitude': u'32.89948', 'state': u'example-state-val-59487',
+                                     'physical_location_id': 'clli1', 'latitude': u'example-latitude-val-89101',
+                                     'complex_name': u'clli1', 'location_id': 'some_location_id',
+                                     'candidate_id': 'some_id'})
+        self.assertEqual(2, TraigeTranslator.collectDroppedCandiate.call_count)
+
+    def test_resolve_vnf_parameters(self):
+        TraigeTranslator.collectDroppedCandiate = mock.MagicMock(return_value=None)
+        triage_translator_data = None
+        demand_name = 'vPGN'
+        service_type = 'vFW'
+        candidate = dict()
+        candidate['candidate_id'] = 'some_id'
+        candidate['location_id'] = 'some_location_id'
+        candidate['inventory_type'] = 'service'
+
+        generic_vnf_list_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_service_generic_vnf_list.json'
+        good_vnf = json.loads(open(generic_vnf_list_file).read())[0]
+        bad_generic_vnf_list_file = './conductor/tests/unit/data/plugins/inventory_provider/bad_generic_vnf_list.json'
+        bad_vnf = json.loads(open(bad_generic_vnf_list_file).read())[0]
+        region_response_file = './conductor/tests/unit/data/plugins/inventory_provider/vfmodule_region.json'
+        region_response = json.loads(open(region_response_file).read())
+
+        self.assertEqual("CloudOwner",
+                         self.aai_ep.resolve_cloud_owner_for_vnf(candidate, good_vnf, service_type,
+                                                                 demand_name, triage_translator_data).get('d_value'))
+        self.assertIsNone(self.aai_ep.resolve_cloud_owner_for_vnf(candidate, bad_vnf, service_type,
+                                                                  demand_name, triage_translator_data))
+
+        regions = list()
+        regions.append(region_response)
+        self.mock_get_regions = mock.patch.object(AAI, 'resolve_cloud_regions_by_cloud_region_id',
+                                                  return_value=regions)
+        self.mock_get_regions.start()
+
+        cloud_region_rl_data, cloud_region_ver_rl_data = self.aai_ep.resolve_cloud_region_id_and_version_for_vnf(
+            candidate, good_vnf, service_type, demand_name, triage_translator_data)
+        self.assertEqual("RegionOne", cloud_region_rl_data.get('d_value'))
+        self.assertEqual("1", cloud_region_ver_rl_data.get('d_value'))
+
+        self.assertEqual((None, None),
+                         self.aai_ep.resolve_cloud_region_id_and_version_for_vnf(candidate, bad_vnf, service_type,
+                                                                                 demand_name, triage_translator_data))
+        v_server_links = list()
+        v_server_links.append("/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner/RegionOne/tenants/\
+tenant/3c6c471ada7747fe8ff7f28e100b61e8/vservers/vserver/00bddefc-126e-4e4f-a18d-99b94d8d9a30")
+        v_server_links.append("/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner2/RegionOne2/tenants/\
+tenant/3c6c471ada7747fe8ff7f28e100b61e8/vservers/vserver/00bddefc-126e-4e4f-a18d-99b94d8d9a31")
+        self.assertEqual(v_server_links, self.aai_ep.resolve_v_server_links_for_vnf(bad_vnf))
+
+        customer_id = 'Demonstration'
+        self.assertEqual(customer_id,
+                         self.aai_ep.resolve_global_customer_id_for_vnf(candidate, good_vnf, customer_id, service_type,
+                                                                        demand_name,
+                                                                        triage_translator_data).get('d_value'))
+        self.assertEqual("3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c",
+                         self.aai_ep.resolve_service_instance_id_for_vnf(candidate, good_vnf, customer_id, service_type,
+                                                                         demand_name,
+                                                                         triage_translator_data).get('d_value'))
+        self.assertIsNone(self.aai_ep.resolve_service_instance_id_for_vnf(candidate, bad_vnf, customer_id, service_type,
+                                                                          demand_name, triage_translator_data))
+
+    def test_match_candidate_by_list(self):
+        TraigeTranslator.collectDroppedCandiate = mock.MagicMock(return_value=None)
+        triage_translator_data = None
+
+        candidate = dict()
+        candidate['candidate_id'] = 'some_id'
+        candidate['location_id'] = 'some_location_id'
+        candidate['inventory_type'] = 'service'
+
+        candidate_list_empty = list()
+        candidate_list = list()
+        candidate_list.append(candidate)
+
+        self.assertFalse(self.aai_ep.match_candidate_by_list(candidate, candidate_list_empty, True, 'demand',
+                                                             triage_translator_data)),
+        self.assertEqual(0, TraigeTranslator.collectDroppedCandiate.call_count)
+        self.assertTrue(self.aai_ep.match_candidate_by_list(candidate, candidate_list, True, 'demand',
+                                                            triage_translator_data))
+        self.assertEqual(1, TraigeTranslator.collectDroppedCandiate.call_count)
+        self.assertTrue(self.aai_ep.match_candidate_by_list(candidate, candidate_list, False, 'demand',
+                                                            triage_translator_data))
+        self.assertEqual(1, TraigeTranslator.collectDroppedCandiate.call_count)
+        self.assertFalse(self.aai_ep.match_candidate_by_list(candidate, candidate_list_empty, False, 'demand',
+                                                             triage_translator_data))
+        self.assertEqual(2, TraigeTranslator.collectDroppedCandiate.call_count)
+
     def test_match_hpa(self):
         flavor_json_file = \
             './conductor/tests/unit/data/plugins/inventory_provider/hpa_flavors.json'
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_candidates.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_candidates.json
new file mode 100644 (file)
index 0000000..7343f34
--- /dev/null
@@ -0,0 +1,73 @@
+{"vPGN": [{
+            "existing_placement": "false",
+            "service_resource_id": "vPGN-XX",
+            "vf-module-name": "vnf-pkg-r1-t2-mc",
+            "inventory_provider": "aai",
+            "cost": 1.0,
+            "vf-module-id": "d187d743-5932-4fb9-a42d-db0a5be5ba7e",
+            "nf-id": "fcbff633-47cc-4f38-a98d-4ba8285bd8b6",
+            "ipv4-oam-address": "oam_network_zb4J",
+            "location_id": "RegionOne",
+            "complex_name": "clli1",
+            "vservers": [{
+                "vserver-id": "00bddefc-126e-4e4f-a18d-99b94d8d9a30",
+                "l-interfaces": [{
+                        "macaddr": "fa:16:3e:c4:07:7f",
+                        "interface-name": "vnf-pkg-r1-t2-mc-vpg_private_2_port-mf7lu55usq7i",
+                        "ipv4-addresses": [
+                            "10.100.100.2"
+                        ],
+                        "ipv6-addresses": [],
+                        "interface-id": "4b333af1-90d6-42ae-8389-d440e6ff0e93",
+                        "network-name": "",
+                        "network-id": "59763a33-3296-4dc8-9ee6-2bdcd63322fc"
+                    },
+                    {
+                        "macaddr": "fa:16:3e:b5:86:38",
+                        "interface-name": "vnf-pkg-r1-t2-mc-vpg_private_1_port-734xxixicw6r",
+                        "ipv4-addresses": [
+                            "10.0.110.2"
+                        ],
+                        "ipv6-addresses": [],
+                        "interface-id": "85dd57e9-6e3a-48d0-a784-4598d627e798",
+                        "network-name": "",
+                        "network-id": "cdb4bc25-2412-4b77-bbd5-791a02f8776d"
+                    },
+                    {
+                        "macaddr": "fa:16:3e:ff:d8:6f",
+                        "interface-name": "vnf-pkg-r1-t2-mc-vpg_private_0_port-e5qdm3p5ijhe",
+                        "ipv4-addresses": [
+                            "192.168.10.200"
+                        ],
+                        "ipv6-addresses": [],
+                        "interface-id": "edaff25a-878e-4706-ad52-4e3d51cf6a82",
+                        "network-name": "",
+                        "network-id": "932ac514-639a-45b2-b1a3-4c5bb708b5c1"
+                    }
+                ],
+                "vserver-name": "zdfw1fwl01pgn01"
+            }],
+            "city": "example-city-val-27150",
+            "cloud_owner": "CloudOwner",
+            "vnf-type": "5G_EVE_Demo/5G_EVE_PKG 0",
+            "nf-name": "vFW-PKG-MC",
+            "inventory_type": "vfmodule",
+            "sriov_automation": "false",
+            "uniqueness": "false",
+            "candidate_id": "d187d743-5932-4fb9-a42d-db0a5be5ba7e",
+            "latitude": "example-latitude-val-89101",
+            "physical_location_id": "clli1",
+            "nf-type": "vnf",
+            "port_key": "vlan_port",
+            "vlan_key": "vlan_key",
+            "cloud_region_version": "1",
+            "host_id": "vFW-PKG-MC",
+            "ipv6-oam-address": "",
+            "location_type": "att_aic",
+            "country": "example-country-val-94173",
+            "region": "example-region-val-13893",
+            "service_instance_id": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c",
+            "state": "example-state-val-59487",
+            "longitude": "32.89948",
+            "vim-id": "CloudOwner_RegionOne"}
+]}
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_complex.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_complex.json
new file mode 100644 (file)
index 0000000..bcb1052
--- /dev/null
@@ -0,0 +1,85 @@
+{
+    "physical-location-id": "clli1",
+    "data-center-code": "example-data-center-code-val-5556",
+    "complex-name": "clli1",
+    "identity-url": "example-identity-url-val-56898",
+    "resource-version": "1554189552609",
+    "physical-location-type": "example-physical-location-type-val-7608",
+    "street1": "example-street1-val-34205",
+    "street2": "example-street2-val-99210",
+    "city": "example-city-val-27150",
+    "state": "example-state-val-59487",
+    "postal-code": "68871",
+    "country": "example-country-val-94173",
+    "region": "example-region-val-13893",
+    "latitude": "example-latitude-val-89101",
+    "longitude": "32.89948",
+    "elevation": "97.045443",
+    "lata": "example-lata-val-46073",
+    "relationship-list": {
+        "relationship": [
+            {
+                "related-to": "cloud-region",
+                "relationship-label": "org.onap.relationships.inventory.LocatedIn",
+                "related-link": "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner/RegionOne",
+                "relationship-data": [
+                    {
+                        "relationship-key": "cloud-region.cloud-owner",
+                        "relationship-value": "CloudOwner"
+                    },
+                    {
+                        "relationship-key": "cloud-region.cloud-region-id",
+                        "relationship-value": "RegionOne"
+                    }
+                ],
+                "related-to-property": [
+                    {
+                        "property-key": "cloud-region.owner-defined-type",
+                        "property-value": "OwnerType"
+                    }
+                ]
+            },
+            {
+                "related-to": "cloud-region",
+                "relationship-label": "org.onap.relationships.inventory.LocatedIn",
+                "related-link": "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/ja/asd",
+                "relationship-data": [
+                    {
+                        "relationship-key": "cloud-region.cloud-owner",
+                        "relationship-value": "ja"
+                    },
+                    {
+                        "relationship-key": "cloud-region.cloud-region-id",
+                        "relationship-value": "asd"
+                    }
+                ],
+                "related-to-property": [
+                    {
+                        "property-key": "cloud-region.owner-defined-type",
+                        "property-value": "asd"
+                    }
+                ]
+            },
+            {
+                "related-to": "cloud-region",
+                "relationship-label": "org.onap.relationships.inventory.LocatedIn",
+                "related-link": "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner/RegionTwo",
+                "relationship-data": [
+                    {
+                        "relationship-key": "cloud-region.cloud-owner",
+                        "relationship-value": "CloudOwner"
+                    },
+                    {
+                        "relationship-key": "cloud-region.cloud-region-id",
+                        "relationship-value": "RegionTwo"
+                    }
+                ],
+                "related-to-property": [
+                    {
+                        "property-key": "cloud-region.owner-defined-type"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_demand_list.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_demand_list.json
new file mode 100644 (file)
index 0000000..c8ddc9e
--- /dev/null
@@ -0,0 +1,20 @@
+{
+    "vPGN": [{
+        "service_resource_id": "vPGN-XX",
+        "inventory_provider": "aai",
+        "inventory_type": "vfmodule",
+        "port_key": "vlan_port",
+        "vlan_key": "vlan_key",
+        "unique": "false",
+        "attributes": {
+            "prov-status": "ACTIVE",
+            "global-customer-id": "Demonstration",
+            "model-version-id": "e02a7e5c-9d27-4360-ab7c-73bb83b07e3b",
+            "model-invariant-id": "762472ef-5284-4daa-ab32-3e7bee2ec355",
+            "orchestration-status": ["active"],
+            "cloud-region-id": "RegionOne",
+            "service_instance_id": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c"
+        },
+        "service_type": "vPGN-XX"
+    }]
+}
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_list.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_list.json
new file mode 100644 (file)
index 0000000..a2ca43c
--- /dev/null
@@ -0,0 +1,41 @@
+[{
+    "vf-module-id": "d187d743-5932-4fb9-a42d-db0a5be5ba7e",
+    "vf-module-name": "vnf-pkg-r1-t2-mc",
+    "heat-stack-id": "vnf-pkg-r1-t2-mc/215429f4-c2a2-4637-acfc-4d1bc9d72bc9",
+    "orchestration-status": "active",
+    "is-base-vf-module": true,
+    "automated-assignment": false,
+    "resource-version": "1554713261318",
+    "model-invariant-id": "d4a27723-b24e-46be-a1d1-633a80eade45",
+    "model-version-id": "9f6e6550-36e6-40b1-84eb-4c9f7c63d289",
+    "model-customization-id": "08b1a66a-90a4-45ab-b073-10b9b609e948",
+    "module-index": 0,
+    "relationship-list": {
+        "relationship": [{
+            "related-to": "vserver",
+            "relationship-label": "org.onap.relationships.inventory.Uses",
+            "related-link": "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner/RegionOne/tenants/tenant/3c6c471ada7747fe8ff7f28e100b61e8/vservers/vserver/00bddefc-126e-4e4f-a18d-99b94d8d9a30",
+            "relationship-data": [{
+                    "relationship-key": "cloud-region.cloud-owner",
+                    "relationship-value": "CloudOwner"
+                },
+                {
+                    "relationship-key": "cloud-region.cloud-region-id",
+                    "relationship-value": "RegionOne"
+                },
+                {
+                    "relationship-key": "tenant.tenant-id",
+                    "relationship-value": "3c6c471ada7747fe8ff7f28e100b61e8"
+                },
+                {
+                    "relationship-key": "vserver.vserver-id",
+                    "relationship-value": "00bddefc-126e-4e4f-a18d-99b94d8d9a30"
+                }
+            ],
+            "related-to-property": [{
+                "property-key": "vserver.vserver-name",
+                "property-value": "zdfw1fwl01pgn01"
+            }]
+        }]
+    }
+}]
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_region.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_region.json
new file mode 100644 (file)
index 0000000..4dd1cf8
--- /dev/null
@@ -0,0 +1,24 @@
+{
+    "cloud-owner": "CloudOwner",
+    "cloud-region-id": "RegionOne",
+    "cloud-type": "SharedNode",
+    "owner-defined-type": "OwnerType",
+    "cloud-region-version": "v1",
+    "cloud-zone": "CloudZone",
+    "resource-version": "1554189551045",
+    "relationship-list": {
+        "relationship": [
+            {
+                "related-to": "complex",
+                "relationship-label": "org.onap.relationships.inventory.LocatedIn",
+                "related-link": "/aai/v14/cloud-infrastructure/complexes/complex/clli1",
+                "relationship-data": [
+                    {
+                        "relationship-key": "complex.physical-location-id",
+                        "relationship-value": "clli1"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_service_generic_vnf_list.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_service_generic_vnf_list.json
new file mode 100644 (file)
index 0000000..cf61e4b
--- /dev/null
@@ -0,0 +1,79 @@
+[{
+    "vnf-id": "fcbff633-47cc-4f38-a98d-4ba8285bd8b6",
+    "vnf-name": "vFW-PKG-MC",
+    "vnf-type": "5G_EVE_Demo/5G_EVE_PKG 0",
+    "service-id": "f5728144-f4a2-4bf8-9f0e-4ee924235c42",
+    "prov-status": "ACTIVE",
+    "orchestration-status": "Active",
+    "ipv4-oam-address": "oam_network_zb4J",
+    "in-maint": false,
+    "is-closed-loop-disabled": false,
+    "resource-version": "1554713856131",
+    "model-invariant-id": "762472ef-5284-4daa-ab32-3e7bee2ec355",
+    "model-version-id": "e02a7e5c-9d27-4360-ab7c-73bb83b07e3b",
+    "model-customization-id": "83f1f72e-26ea-4e32-8dae-a7b3a833c7f6",
+    "nf-type": "",
+    "nf-function": "",
+    "nf-role": "",
+    "nf-naming-code": "",
+    "relationship-list": {
+        "relationship": [{
+                "related-to": "service-instance",
+                "relationship-label": "org.onap.relationships.inventory.ComposedOf",
+                "related-link": "/aai/v14/business/customers/customer/Demonstration/service-subscriptions/service-subscription/vFW/service-instances/service-instance/3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c",
+                "relationship-data": [{
+                        "relationship-key": "customer.global-customer-id",
+                        "relationship-value": "Demonstration"
+                    },
+                    {
+                        "relationship-key": "service-subscription.service-type",
+                        "relationship-value": "vFW"
+                    },
+                    {
+                        "relationship-key": "service-instance.service-instance-id",
+                        "relationship-value": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c"
+                    }
+                ],
+                "related-to-property": [{
+                    "property-key": "service-instance.service-instance-name",
+                    "property-value": "vFW-R1-Tenant1-LR"
+                }]
+            },
+            {
+                "related-to": "platform",
+                "relationship-label": "org.onap.relationships.inventory.Uses",
+                "related-link": "/aai/v14/business/platforms/platform/Platform-Demonstration",
+                "relationship-data": [{
+                    "relationship-key": "platform.platform-name",
+                    "relationship-value": "Platform-Demonstration"
+                }]
+            },
+            {
+                "related-to": "vserver",
+                "relationship-label": "tosca.relationships.HostedOn",
+                "related-link": "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner/RegionOne/tenants/tenant/3c6c471ada7747fe8ff7f28e100b61e8/vservers/vserver/00bddefc-126e-4e4f-a18d-99b94d8d9a30",
+                "relationship-data": [{
+                        "relationship-key": "cloud-region.cloud-owner",
+                        "relationship-value": "CloudOwner"
+                    },
+                    {
+                        "relationship-key": "cloud-region.cloud-region-id",
+                        "relationship-value": "RegionOne"
+                    },
+                    {
+                        "relationship-key": "tenant.tenant-id",
+                        "relationship-value": "3c6c471ada7747fe8ff7f28e100b61e8"
+                    },
+                    {
+                        "relationship-key": "vserver.vserver-id",
+                        "relationship-value": "00bddefc-126e-4e4f-a18d-99b94d8d9a30"
+                    }
+                ],
+                "related-to-property": [{
+                    "property-key": "vserver.vserver-name",
+                    "property-value": "zdfw1fwl01pgn01"
+                }]
+            }
+        ]
+    }
+}]
\ No newline at end of file
diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_vserver.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/vfmodule_vserver.json
new file mode 100644 (file)
index 0000000..9672734
--- /dev/null
@@ -0,0 +1,157 @@
+{
+    "vserver-id": "00bddefc-126e-4e4f-a18d-99b94d8d9a30",
+    "vserver-name": "zdfw1fwl01pgn01",
+    "vserver-name2": "zdfw1fwl01pgn01",
+    "prov-status": "ACTIVE",
+    "vserver-selflink": "http://192.168.186.11:8774/v2.1/3c6c471ada7747fe8ff7f28e100b61e8/servers/00bddefc-126e-4e4f-a18d-99b94d8d9a30",
+    "in-maint": false,
+    "is-closed-loop-disabled": false,
+    "resource-version": "1554713860350",
+    "relationship-list": {
+        "relationship": [
+            {
+                "related-to": "generic-vnf",
+                "relationship-label": "tosca.relationships.HostedOn",
+                "related-link": "/aai/v14/network/generic-vnfs/generic-vnf/fcbff633-47cc-4f38-a98d-4ba8285bd8b6",
+                "relationship-data": [
+                    {
+                        "relationship-key": "generic-vnf.vnf-id",
+                        "relationship-value": "fcbff633-47cc-4f38-a98d-4ba8285bd8b6"
+                    }
+                ],
+                "related-to-property": [
+                    {
+                        "property-key": "generic-vnf.vnf-name",
+                        "property-value": "vFW-PKG-MC"
+                    }
+                ]
+            },
+            {
+                "related-to": "vf-module",
+                "relationship-label": "org.onap.relationships.inventory.Uses",
+                "related-link": "/aai/v14/network/generic-vnfs/generic-vnf/fcbff633-47cc-4f38-a98d-4ba8285bd8b6/vf-modules/vf-module/d187d743-5932-4fb9-a42d-db0a5be5ba7e",
+                "relationship-data": [
+                    {
+                        "relationship-key": "generic-vnf.vnf-id",
+                        "relationship-value": "fcbff633-47cc-4f38-a98d-4ba8285bd8b6"
+                    },
+                    {
+                        "relationship-key": "vf-module.vf-module-id",
+                        "relationship-value": "d187d743-5932-4fb9-a42d-db0a5be5ba7e"
+                    }
+                ]
+            },
+            {
+                "related-to": "flavor",
+                "relationship-label": "org.onap.relationships.inventory.Uses",
+                "related-link": "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner/RegionOne/flavors/flavor/3",
+                "relationship-data": [
+                    {
+                        "relationship-key": "cloud-region.cloud-owner",
+                        "relationship-value": "CloudOwner"
+                    },
+                    {
+                        "relationship-key": "cloud-region.cloud-region-id",
+                        "relationship-value": "RegionOne"
+                    },
+                    {
+                        "relationship-key": "flavor.flavor-id",
+                        "relationship-value": "3"
+                    }
+                ],
+                "related-to-property": [
+                    {
+                        "property-key": "flavor.flavor-name",
+                        "property-value": "m1.medium"
+                    }
+                ]
+            },
+            {
+                "related-to": "image",
+                "relationship-label": "org.onap.relationships.inventory.Uses",
+                "related-link": "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CloudOwner/RegionOne/images/image/de9c10fb-21c9-4b7d-b0a0-f3f9fec722f4",
+                "relationship-data": [
+                    {
+                        "relationship-key": "cloud-region.cloud-owner",
+                        "relationship-value": "CloudOwner"
+                    },
+                    {
+                        "relationship-key": "cloud-region.cloud-region-id",
+                        "relationship-value": "RegionOne"
+                    },
+                    {
+                        "relationship-key": "image.image-id",
+                        "relationship-value": "de9c10fb-21c9-4b7d-b0a0-f3f9fec722f4"
+                    }
+                ],
+                "related-to-property": [
+                    {
+                        "property-key": "image.image-name",
+                        "property-value": "unknown"
+                    }
+                ]
+            }
+        ]
+    },
+    "l-interfaces": {
+        "l-interface": [
+            {
+                "interface-name": "vnf-pkg-r1-t2-mc-vpg_private_2_port-mf7lu55usq7i",
+                "interface-id": "4b333af1-90d6-42ae-8389-d440e6ff0e93",
+                "macaddr": "fa:16:3e:c4:07:7f",
+                "network-name": "59763a33-3296-4dc8-9ee6-2bdcd63322fc",
+                "is-port-mirrored": false,
+                "resource-version": "1554713868970",
+                "in-maint": false,
+                "is-ip-unnumbered": false,
+                "l3-interface-ipv4-address-list": [
+                    {
+                        "l3-interface-ipv4-address": "10.100.100.2",
+                        "l3-interface-ipv4-prefix-length": 32,
+                        "resource-version": "1554713868979",
+                        "neutron-network-id": "59763a33-3296-4dc8-9ee6-2bdcd63322fc",
+                        "neutron-subnet-id": "1bee4746-1ec1-4a67-995e-f3ac86999bc4"
+                    }
+                ]
+            },
+            {
+                "interface-name": "vnf-pkg-r1-t2-mc-vpg_private_1_port-734xxixicw6r",
+                "interface-id": "85dd57e9-6e3a-48d0-a784-4598d627e798",
+                "macaddr": "fa:16:3e:b5:86:38",
+                "network-name": "cdb4bc25-2412-4b77-bbd5-791a02f8776d",
+                "is-port-mirrored": false,
+                "resource-version": "1554713868144",
+                "in-maint": false,
+                "is-ip-unnumbered": false,
+                "l3-interface-ipv4-address-list": [
+                    {
+                        "l3-interface-ipv4-address": "10.0.110.2",
+                        "l3-interface-ipv4-prefix-length": 32,
+                        "resource-version": "1554713868198",
+                        "neutron-network-id": "cdb4bc25-2412-4b77-bbd5-791a02f8776d",
+                        "neutron-subnet-id": "fad946f8-3894-433b-af59-3a81f59da3b0"
+                    }
+                ]
+            },
+            {
+                "interface-name": "vnf-pkg-r1-t2-mc-vpg_private_0_port-e5qdm3p5ijhe",
+                "interface-id": "edaff25a-878e-4706-ad52-4e3d51cf6a82",
+                "macaddr": "fa:16:3e:ff:d8:6f",
+                "network-name": "932ac514-639a-45b2-b1a3-4c5bb708b5c1",
+                "is-port-mirrored": false,
+                "resource-version": "1554713860611",
+                "in-maint": false,
+                "is-ip-unnumbered": false,
+                "l3-interface-ipv4-address-list": [
+                    {
+                        "l3-interface-ipv4-address": "192.168.10.200",
+                        "l3-interface-ipv4-prefix-length": 32,
+                        "resource-version": "1554713862583",
+                        "neutron-network-id": "932ac514-639a-45b2-b1a3-4c5bb708b5c1",
+                        "neutron-subnet-id": "6f61389c-b0a8-4140-8a37-4a095966cde5"
+                    }
+                ]
+            }
+        ]
+    }
+}
\ No newline at end of file
index c9104c4..3f010db 100644 (file)
@@ -202,7 +202,7 @@ class TestDataEndpoint(unittest.TestCase):
     @mock.patch.object(service.LOG, 'info')
     @mock.patch.object(log_util, 'getTransactionId')
     @mock.patch.object(stevedore.ExtensionManager, 'map_method')
-    def test_reslove_demands(self, ext_mock, logutil_mock, info_mock,
+    def test_resolve_demands(self, ext_mock, logutil_mock, info_mock,
                              debug_mock,
                              error_mock):
         self.maxDiff = None
@@ -232,6 +232,48 @@ class TestDataEndpoint(unittest.TestCase):
         self.assertEqual(expected_response,
                          self.data_ep.resolve_demands(ctxt, req_json))
 
+    @mock.patch.object(service.LOG, 'error')
+    @mock.patch.object(service.LOG, 'debug')
+    @mock.patch.object(service.LOG, 'info')
+    @mock.patch.object(log_util, 'getTransactionId')
+    @mock.patch.object(stevedore.ExtensionManager, 'map_method')
+    def test_resolve_vfmodule_demands(self, ext_mock, logutil_mock, info_mock,
+                             debug_mock,
+                             error_mock):
+        self.maxDiff = None
+        req_json_file = './conductor/tests/unit/data/demands_vfmodule.json'
+        req_json = yaml.safe_load(open(req_json_file).read())
+        ctxt = {
+            'plan_id': uuid.uuid4(),
+            'keyspace': cfg.CONF.keyspace
+        }
+        logutil_mock.return_value = uuid.uuid4()
+        return_value = req_json['demands']['vFW-SINK']
+        ext_mock.return_value = [return_value]
+        expected_response = \
+            {'response': {'trans': {'translator_triage': [ [] ], 'plan_name': 'plan_name', 'plan_id': 'plan_abc'},
+                          'resolved_demands': [{'service_resource_id': 'vFW-SINK-XX', 'vlan_key': 'vlan_key',
+                                                'inventory_provider': 'aai', 'inventory_type': 'vfmodule',
+                                                'excluded_candidates': [
+                                                    {'candidate_id': 'e765d576-8755-4145-8536-0bb6d9b1dc9a',
+                                                     'inventory_type': 'vfmodule'
+                                                     }], 'port_key': 'vlan_port', 'service_type': 'vFW-SINK-XX',
+                                                'attributes': {'global-customer-id': 'Demonstration',
+                                                               'cloud-region-id': {'get_param': 'chosen_region'},
+                                                               'model-version-id':
+                                                                   '763731df-84fd-494b-b824-01fc59a5ff2d',
+                                                               'prov-status': 'ACTIVE',
+                                                               'service_instance_id': {'get_param': 'service_id'},
+                                                               'model-invariant-id':
+                                                                   'e7227847-dea6-4374-abca-4561b070fe7d',
+                                                               'orchestration-status': ['active']
+                                                               }
+                                                }]
+                          }, 'error': False}
+
+        self.assertEqual(expected_response,
+                         self.data_ep.resolve_demands(ctxt, req_json))
+
     @mock.patch.object(service.LOG, 'error')
     @mock.patch.object(service.LOG, 'info')
     @mock.patch.object(stevedore.ExtensionManager, 'names')