Introduce Hazelcast for alternateId-cmHandle relation 74/136974/26
authorleventecsanyi <levente.csanyi@est.tech>
Thu, 11 Jan 2024 11:53:26 +0000 (12:53 +0100)
committerToine Siebelink <toine.siebelink@est.tech>
Thu, 1 Feb 2024 09:37:09 +0000 (09:37 +0000)
    - added new Hazelcast config
    - added cache initialization in CmHandleIdMapper
    - added unit tests
    - updated deployment.rst
    - refactored cache updating

Issue-ID: CPS-1988
Change-Id: Iea6f884e584bf8cea8612ddbced4329e783c60a5
Signed-off-by: leventecsanyi <levente.csanyi@est.tech>
12 files changed:
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyCmHandleQueryService.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/embeddedcache/AlternateIdCacheConfig.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapper.java [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/AlternateIdCacheConfigSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapperSpec.groovy [new file with mode: 0644]
docs/deployment.rst

index f29fd68..06522f8 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2023 Nordix Foundation
+ *  Copyright (C) 2022-2024 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -61,4 +61,11 @@ public interface NetworkCmProxyCmHandleQueryService {
      * @return collection of cm handles
      */
     Collection<NcmpServiceCmHandle> queryCmHandles(CmHandleQueryServiceParameters cmHandleQueryServiceParameters);
+
+    /**
+     * Query and return all cm handle objects.
+     *
+     * @return collection of cm handles
+     */
+    Collection<NcmpServiceCmHandle> getAllCmHandles();
 }
index 1f6c948..6f92a9e 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2023 Nordix Foundation
+ *  Copyright (C) 2022-2024 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -97,6 +97,12 @@ public class NetworkCmProxyCmHandleQueryServiceImpl implements NetworkCmProxyCmH
         return getNcmpServiceCmHandles(cmHandleIds);
     }
 
+    @Override
+    public Collection<NcmpServiceCmHandle> getAllCmHandles() {
+        final DataNode dataNode = inventoryPersistence.getDataNode(NCMP_DMI_REGISTRY_PARENT).iterator().next();
+        return dataNode.getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet());
+    }
+
     private Collection<String> queryCmHandlesByDmiPlugin(
             final CmHandleQueryServiceParameters cmHandleQueryServiceParameters) {
         final Map<String, String> dmiPropertyQueryPairs =
@@ -216,11 +222,6 @@ public class NetworkCmProxyCmHandleQueryServiceImpl implements NetworkCmProxyCmH
         return Collections.emptyList();
     }
 
-    private Collection<NcmpServiceCmHandle> getAllCmHandles() {
-        final DataNode dataNode = inventoryPersistence.getDataNode(NCMP_DMI_REGISTRY_PARENT).iterator().next();
-        return dataNode.getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet());
-    }
-
     private Collection<String> getAllCmHandleIds() {
         final DataNode dataNode = inventoryPersistence.getDataNode(NCMP_DMI_REGISTRY_PARENT, DIRECT_CHILDREN_ONLY)
                 .iterator().next();
index 469d75a..3fa0504 100755 (executable)
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021 highstreet technologies GmbH
- *  Modifications Copyright (C) 2021-2023 Nordix Foundation
+ *  Modifications Copyright (C) 2021-2024 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
@@ -48,6 +48,7 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.ncmp.api.NcmpResponseStatus;
 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService;
@@ -65,6 +66,7 @@ import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations;
 import org.onap.cps.ncmp.api.impl.operations.OperationType;
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel;
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevelManager;
+import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper;
 import org.onap.cps.ncmp.api.impl.utils.CmHandleQueryConditions;
 import org.onap.cps.ncmp.api.impl.utils.InventoryQueryConditions;
 import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
@@ -104,10 +106,12 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     private final IMap<String, Object> moduleSyncStartedOnCmHandles;
     private final Map<String, TrustLevel> trustLevelPerDmiPlugin;
     private final TrustLevelManager trustLevelManager;
+    private final CmHandleIdMapper cmHandleIdMapper;
 
     @Override
     public DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule(
         final DmiPluginRegistration dmiPluginRegistration) {
+        cacheAlternateIds(dmiPluginRegistration.getCreatedCmHandles());
         dmiPluginRegistration.validateDmiPluginRegistration();
         final DmiPluginRegistrationResponse dmiPluginRegistrationResponse = new DmiPluginRegistrationResponse();
 
@@ -367,10 +371,17 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
 
         yangModelCmHandles.removeIf(yangModelCmHandle -> notDeletedCmHandles.contains(yangModelCmHandle.getId()));
         updateCmHandleStateBatch(yangModelCmHandles, CmHandleState.DELETED);
+        removeEntriesFromAlternateIdCache(yangModelCmHandles);
 
         return cmHandleRegistrationResponses;
     }
 
+    private void removeEntriesFromAlternateIdCache(final Collection<YangModelCmHandle> yangModelCmHandles) {
+        for (final YangModelCmHandle yangModelCmHandle : yangModelCmHandles) {
+            cmHandleIdMapper.removeMapping(yangModelCmHandle.getId());
+        }
+    }
+
     protected List<CmHandleRegistrationResponse> parseAndProcessUpgradedCmHandlesInRegistration(
         final DmiPluginRegistration dmiPluginRegistration) {
 
@@ -527,4 +538,12 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
         }
     }
 
+    private void cacheAlternateIds(final Collection<NcmpServiceCmHandle> ncmpServiceCmHandles) {
+        for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) {
+            if (!StringUtils.isEmpty(ncmpServiceCmHandle.getAlternateId())) {
+                cmHandleIdMapper.addMapping(ncmpServiceCmHandle.getCmHandleId(), ncmpServiceCmHandle.getAlternateId());
+            }
+        }
+    }
+
 }
index 1520932..13b3fca 100644 (file)
@@ -43,9 +43,9 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence;
+import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper;
 import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
@@ -67,6 +67,7 @@ public class NetworkCmProxyDataServicePropertyHandler {
     private final InventoryPersistence inventoryPersistence;
     private final CpsDataService cpsDataService;
     private final JsonObjectMapper jsonObjectMapper;
+    private final CmHandleIdMapper cmHandleIdMapper;
 
     /**
      * Iterates over incoming ncmpServiceCmHandles and update the dataNodes based on the updated attributes.
@@ -105,19 +106,15 @@ public class NetworkCmProxyDataServicePropertyHandler {
     private void updateAlternateId(final DataNode existingCmHandleDataNode,
                                    final NcmpServiceCmHandle ncmpServiceCmHandle) {
         final String newAlternateId = ncmpServiceCmHandle.getAlternateId();
-        if (!StringUtils.isEmpty(newAlternateId)) {
-            final String existingAlternateId = (String) existingCmHandleDataNode.getLeaves().get("alternate-id");
-            if (StringUtils.isEmpty(existingAlternateId)) {
+        if (cmHandleIdMapper.addMapping(ncmpServiceCmHandle.getCmHandleId(), newAlternateId)) {
+            try {
                 final YangModelCmHandle yangModelCmHandle =
                         YangDataConverter.convertCmHandleToYangModel(existingCmHandleDataNode,
                                 ncmpServiceCmHandle.getCmHandleId());
                 setAndUpdateAlternateId(yangModelCmHandle, newAlternateId);
-            } else {
-                if (!newAlternateId.equals(existingAlternateId)) {
-                    log.warn("Unable to update alternateId for cmHandle {}. "
-                                    + "Value for alternateId has been set previously.",
-                            ncmpServiceCmHandle.getCmHandleId());
-                }
+            } catch (final Exception e) {
+                cmHandleIdMapper.removeMapping(ncmpServiceCmHandle.getCmHandleId());
+                throw e;
             }
         }
     }
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/embeddedcache/AlternateIdCacheConfig.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/embeddedcache/AlternateIdCacheConfig.java
new file mode 100644 (file)
index 0000000..a69d144
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * ============LICENSE_START========================================================
+ *  Copyright (C) 2024 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.config.embeddedcache;
+
+import com.hazelcast.config.MapConfig;
+import java.util.Map;
+import org.onap.cps.cache.HazelcastCacheConfig;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class AlternateIdCacheConfig extends HazelcastCacheConfig {
+
+    private static final MapConfig alternateIdPerCmHandleIdMapConfig =
+            createMapConfig("alternateIdPerCmHandleIdMapConfig");
+
+    private static final MapConfig cmHandleIdPerAlternateIdMapConfig =
+            createMapConfig("cmHandleIdPerAlternateIdMapConfig");
+
+    /**
+     * Distributed instance of alternate id cache containing the alternate id per cm handle id.
+     *
+     * @return the cached map.
+     */
+    @Bean
+    public Map<String, String> alternateIdPerCmHandleId() {
+        return createHazelcastInstance("hazelcastInstanceAlternateIdPerCmHandleIdMap",
+                alternateIdPerCmHandleIdMapConfig).getMap("alternateIdPerCmHandleId");
+    }
+
+    /**
+     * Distributed instance of alternate id cache containing the cm handle id per alternate id.
+     *
+     * @return the cached map.
+     */
+    @Bean
+    public Map<String, String> cmHandleIdPerAlternateId() {
+        return createHazelcastInstance("hazelcastInstanceCmHandleIdPerAlternateIdMap",
+                cmHandleIdPerAlternateIdMapConfig).getMap("cmHandleIdPerAlternateId");
+    }
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapper.java
new file mode 100644 (file)
index 0000000..8175fb5
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * ============LICENSE_START========================================================
+ * Copyright (c) 2024 Nordix Foundation.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an 'AS IS' BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.utils;
+
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class CmHandleIdMapper {
+
+    private final Map<String, String> alternateIdPerCmHandleId;
+    private final Map<String, String> cmHandleIdPerAlternateId;
+    private final NetworkCmProxyCmHandleQueryService networkCmProxyCmHandleQueryService;
+
+    private boolean cacheIsInitialized = false;
+
+    public String cmHandleIdToAlternateId(final String cmHandleId) {
+        initializeCache();
+        return alternateIdPerCmHandleId.get(cmHandleId);
+    }
+
+    public String alternateIdToCmHandleId(final String alternateId) {
+        initializeCache();
+        return cmHandleIdPerAlternateId.get(alternateId);
+    }
+
+    public boolean addMapping(final String cmHandleId, final String alternateId) {
+        initializeCache();
+        return addMappingWithValidation(cmHandleId, alternateId);
+    }
+
+
+    private boolean addMappingWithValidation(final String cmHandleId, final String alternateId) {
+        if (alternateIdPerCmHandleId.containsKey(cmHandleId)) {
+            final String originalAlternateId = alternateIdPerCmHandleId.get(cmHandleId);
+            if (!originalAlternateId.equals(alternateId)) {
+                log.warn("Alternate id update ignored, cannot update cm handle {}, already has an alternate id of {}",
+                        cmHandleId, originalAlternateId);
+            }
+            return false;
+        }
+        if (StringUtils.isBlank(alternateId)) {
+            return false;
+        }
+        alternateIdPerCmHandleId.put(cmHandleId, alternateId);
+        cmHandleIdPerAlternateId.put(alternateId, cmHandleId);
+        return true;
+    }
+
+    public void removeMapping(final String cmHandleId) {
+        final String alternateId = alternateIdPerCmHandleId.remove(cmHandleId);
+        cmHandleIdPerAlternateId.remove(alternateId);
+    }
+
+    private void initializeCache() {
+        if (!cacheIsInitialized) {
+            networkCmProxyCmHandleQueryService.getAllCmHandles().forEach(cmHandle ->
+                addMappingWithValidation(cmHandle.getCmHandleId(), cmHandle.getAlternateId())
+            );
+            log.info("Alternate ID cache initialized from DB with {} cm handle/alternate id pairs ",
+                    alternateIdPerCmHandleId.size());
+            cacheIsInitialized = true;
+        }
+    }
+}
index f565ede..59bf942 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2023 Nordix Foundation
+ *  Copyright (C) 2021-2024 Nordix Foundation
  *  Modifications Copyright (C) 2022 Bell Canada
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,6 +22,7 @@
 package org.onap.cps.ncmp.api.impl
 
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevelManager
+import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper
 import org.onap.cps.ncmp.api.models.UpgradedCmHandles
 
 import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLES_NOT_FOUND
@@ -71,6 +72,7 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
     def mockModuleSyncStartedOnCmHandles = Mock(IMap<String, Object>)
     def trustLevelPerDmiPlugin = [:]
     def mockTrustLevelManager = Mock(TrustLevelManager)
+    def mockCmHandleIdMapper = Mock(CmHandleIdMapper)
     def objectUnderTest = getObjectUnderTest()
 
     def 'DMI Registration: Create, Update, Delete & Upgrade operations are processed in the right order'() {
@@ -432,11 +434,21 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
         'an unexpected exception'    | 'cmhandle'             | new RuntimeException('Failed')            || UNKNOWN_ERROR        | 'Failed'
     }
 
+    def 'Adding data to alternate id caches.'() {
+        given: 'a registration with three CM Handles to be created'
+            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
+                    createdCmHandles: [new NcmpServiceCmHandle(cmHandleId: 'cmhandle1', alternateId: 'my-alternate-id-1')])
+        when: 'the DMI plugin registration happens'
+            objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
+        then: 'the new alternate id is added to the cache'
+            1 * mockCmHandleIdMapper.addMapping('cmhandle1', 'my-alternate-id-1')
+    }
+
     def getObjectUnderTest() {
         return Spy(new NetworkCmProxyDataServiceImpl(spiedJsonObjectMapper, mockDmiDataOperations,
                 mockNetworkCmProxyDataServicePropertyHandler, mockInventoryPersistence, mockCmHandleQueries,
                 stubbedNetworkCmProxyCmHandlerQueryService, mockLcmEventsCmHandleStateHandler, mockCpsDataService,
-                mockModuleSyncStartedOnCmHandles, trustLevelPerDmiPlugin, mockTrustLevelManager))
+                mockModuleSyncStartedOnCmHandles, trustLevelPerDmiPlugin as Map<String, TrustLevel>, mockTrustLevelManager, mockCmHandleIdMapper))
     }
 
     def addPersistedYangModelCmHandles(ids) {
index 4f7b726..c1af902 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2023 Nordix Foundation
+ *  Copyright (C) 2021-2024 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
@@ -22,6 +22,9 @@
  */
 
 package org.onap.cps.ncmp.api.impl
+
+import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper
+
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
@@ -78,6 +81,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
     def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
     def stubTrustLevelPerDmiPlugin = Stub(Map<String, TrustLevel>)
     def mockTrustLevelManager = Mock(TrustLevelManager)
+    def mockCmHandleIdMapper = Mock(CmHandleIdMapper)
 
     def NO_TOPIC = null
     def NO_REQUEST_ID = null
@@ -97,7 +101,9 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             mockCpsDataService,
             stubModuleSyncStartedOnCmHandles,
             stubTrustLevelPerDmiPlugin,
-            mockTrustLevelManager)
+            mockTrustLevelManager,
+            mockCmHandleIdMapper
+    )
 
     def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
 
index 2a15e6c..f94c34c 100644 (file)
 
 package org.onap.cps.ncmp.api.impl
 
-import ch.qos.logback.classic.Level
-import ch.qos.logback.classic.Logger
-import ch.qos.logback.classic.spi.ILoggingEvent
+
 import com.fasterxml.jackson.databind.ObjectMapper
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.slf4j.LoggerFactory
+import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper
 
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
@@ -37,7 +33,6 @@ import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLE_INVALID_ID
 import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR
 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
 
-import ch.qos.logback.core.read.ListAppender
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.utils.JsonObjectMapper
 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence
@@ -53,20 +48,9 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
     def mockInventoryPersistence = Mock(InventoryPersistence)
     def mockCpsDataService = Mock(CpsDataService)
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
-    def logger = Spy(ListAppender<ILoggingEvent>)
-
-    @BeforeEach
-    void setup() {
-        ((Logger) LoggerFactory.getLogger(NetworkCmProxyDataServicePropertyHandler.class)).addAppender(logger);
-        logger.start();
-    }
+    def mockCmHandleIdMapper = Mock(CmHandleIdMapper)
 
-    @AfterEach
-    void teardown() {
-        ((Logger) LoggerFactory.getLogger(NetworkCmProxyDataServicePropertyHandler.class)).detachAndStopAllAppenders();
-    }
-
-    def objectUnderTest = new NetworkCmProxyDataServicePropertyHandler(mockInventoryPersistence, mockCpsDataService, jsonObjectMapper)
+    def objectUnderTest = new NetworkCmProxyDataServicePropertyHandler(mockInventoryPersistence, mockCpsDataService, jsonObjectMapper, mockCmHandleIdMapper)
     def static cmHandleId = 'myHandle1'
     def static cmHandleXpath = "/dmi-registry/cm-handles[@id='${cmHandleId}']"
 
@@ -198,36 +182,41 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
             2 * mockInventoryPersistence.replaceListContent(cmHandleXpath,_)
     }
 
-    def 'Update CM Handle Alternate ID when #scenario'() {
-        given: 'an existing cm handle with alternate id #existingAlternateId'
-            DataNode existingCmHandleDataNode = new DataNode(xpath: cmHandleXpath, leaves: ['alternate-id': oldAlternateId])
-        and: 'an update request with an alternate id #newAlternateId'
-            def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, alternateId: newAlternateId)
-        when: 'update data node leaves is called with the update request'
+    def 'Update CM Handle Alternate ID with #scenario'() {
+        given: 'an existing cm handle'
+            DataNode existingCmHandleDataNode = new DataNode(xpath: cmHandleXpath)
+        and: 'an update request with an alternate id'
+            def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, alternateId: 'alt-1' )
+        when: 'update alternate id method is called with the update request'
             objectUnderTest.updateAlternateId(existingCmHandleDataNode, ncmpServiceCmHandle)
         then: 'the update node leaves method is invoked as many times as expected'
-            numberOfInvocations * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', _, _) >> { args ->
-                assert args[3].contains(newAlternateId)
-            }
-        and: 'correct information is logged'
-            def lastLoggingEvent = logger.list[0]
-            if (expectLogWarning) {
-                assert lastLoggingEvent.level == Level.WARN
-                assert lastLoggingEvent.formattedMessage.contains('Unable')
-            } else if (numberOfInvocations == 1) {
-                assert lastLoggingEvent.level == Level.INFO
-                assert lastLoggingEvent.formattedMessage.contains('Updating alternateId')
-            } else {
-                assert lastLoggingEvent == null
-            }
+            callsToDataService * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', _, _) >>
+                    { args ->
+                        assert args[3].contains('alt-1')
+                    }
+            mockCmHandleIdMapper.addMapping(cmHandleId, 'alt-1') >> isNewMapping
         where: 'following updates are attempted'
-            scenario                     | oldAlternateId | newAlternateId || numberOfInvocations | expectLogWarning
-            'old alternate id null'      | null           | 'new'          || 1                   | false
-            'old alternate id empty'     | ''             | 'new'          || 1                   | false
-            'old alternate id not empty' | 'old'          | 'new'          || 0                   | true
-            'same alternate id'          | 'old'          | 'old'          || 0                   | false
-            'empty new alternate id'     | 'old'          | ''             || 0                   | false
-            'null new alternate id'      | 'old'          | null           || 0                   | false
+            scenario                | isNewMapping || callsToDataService
+            'new alternate id   '   | true         || 1
+            'existing alternate id' | false        || 0
+    }
+
+    def 'Alternate ID removed from cache when persisting fails.'() {
+        given: 'an existing data node and an update request with an alternate id'
+            def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, alternateId: 'alt-1')
+            DataNode existingCmHandleDataNode = new DataNode(xpath: cmHandleXpath, leaves: ['alternate-id': null])
+        and: 'a new mapping is added'
+            mockCmHandleIdMapper.addMapping(cmHandleId, 'alt-1') >> true
+        and: 'but an exception occurs while saving'
+            def originalException = new NullPointerException('some exception')
+            mockCpsDataService.updateNodeLeaves(*_) >> { throw originalException }
+        when: 'updating of alternate id called'
+            objectUnderTest.updateAlternateId(existingCmHandleDataNode, ncmpServiceCmHandle)
+        then: 'the original exception is thrown up'
+            def thrownException = thrown(NullPointerException)
+            assert thrownException == originalException
+        and: 'the mapping is removed from the cache'
+            1 * mockCmHandleIdMapper.removeMapping(cmHandleId)
     }
 
     def convertToProperties(expectedPropertiesAfterUpdateAsMap) {
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/AlternateIdCacheConfigSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/AlternateIdCacheConfigSpec.groovy
new file mode 100644 (file)
index 0000000..71c5293
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * ============LICENSE_START========================================================
+ *  Copyright (C) 2024 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.config.embeddedcache
+
+import com.hazelcast.core.Hazelcast
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import spock.lang.Specification
+
+@SpringBootTest(classes = [AlternateIdCacheConfig])
+class AlternateIdCacheConfigSpec extends Specification {
+
+    @Autowired
+    private Map<String, String> cmHandleIdPerAlternateId
+    @Autowired
+    private Map<String, String> alternateIdPerCmHandleId
+
+    def 'Embedded (hazelcast) cache for alternate id - cm handle id caches.'() {
+        expect: 'system is able to create an instance of the Alternate ID Cache'
+            assert null != cmHandleIdPerAlternateId
+            assert null != alternateIdPerCmHandleId
+        and: 'there are at least 2 instances'
+            assert Hazelcast.allHazelcastInstances.size() > 1
+        and: 'Alternate ID Caches are present'
+            assert Hazelcast.allHazelcastInstances.name.contains('hazelcastInstanceAlternateIdPerCmHandleIdMap')
+                    && Hazelcast.allHazelcastInstances.name.contains('hazelcastInstanceCmHandleIdPerAlternateIdMap')
+    }
+
+    def 'Verify configs of the alternate id distributed objects.'(){
+        when: 'retrieving the map config of module set tag'
+            def alternateIdConfig =  Hazelcast.getHazelcastInstanceByName('hazelcastInstanceAlternateIdPerCmHandleIdMap').config
+            def alternateIdMapConfig = alternateIdConfig.mapConfigs.get('alternateIdPerCmHandleIdMapConfig')
+            def cmHandleIdConfig =  Hazelcast.getHazelcastInstanceByName('hazelcastInstanceCmHandleIdPerAlternateIdMap').config
+            def cmHandleIdIdMapConfig = cmHandleIdConfig.mapConfigs.get('cmHandleIdPerAlternateIdMapConfig')
+        then: 'the map configs have the correct number of backups'
+            assert alternateIdMapConfig.backupCount == 3
+            assert alternateIdMapConfig.asyncBackupCount == 3
+            assert cmHandleIdIdMapConfig.backupCount == 3
+            assert cmHandleIdIdMapConfig.asyncBackupCount == 3
+    }
+}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapperSpec.groovy
new file mode 100644 (file)
index 0000000..0a2962e
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * ============LICENSE_START========================================================
+ * Copyright (c) 2024 Nordix Foundation.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an 'AS IS' BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.utils
+
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.read.ListAppender
+import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import org.slf4j.LoggerFactory
+import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService
+import spock.lang.Specification
+
+class CmHandleIdMapperSpec extends Specification {
+
+    def alternateIdPerCmHandle = new HashMap<String, String>()
+    def cmHandlePerAlternateId = new HashMap<String, String>()
+    def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandleQueryService)
+
+    def objectUnderTest = new CmHandleIdMapper(alternateIdPerCmHandle, cmHandlePerAlternateId, mockCpsCmHandlerQueryService)
+
+    def logger = Spy(ListAppender<ILoggingEvent>)
+
+    def setup() {
+        ((Logger) LoggerFactory.getLogger(CmHandleIdMapper.class)).addAppender(logger)
+        logger.start()
+        mockCpsCmHandlerQueryService.getAllCmHandles() >> []
+        assert objectUnderTest.addMapping('my cmhandle id', 'my alternate id')
+    }
+
+    void cleanup() {
+        ((Logger) LoggerFactory.getLogger(CmHandleIdMapper.class)).detachAndStopAllAppenders()
+    }
+
+    def 'Checking entries in the cache.'() {
+        expect: 'the alternate id can be converted to cmhandle id'
+            assert objectUnderTest.alternateIdToCmHandleId('my alternate id') == 'my cmhandle id'
+        and: 'the cmhandle id can be converted to alternate id'
+            assert objectUnderTest.cmHandleIdToAlternateId('my cmhandle id') == 'my alternate id'
+    }
+
+    def 'Attempt adding #scenario alternate id.'() {
+        expect: 'cmhandle id - alternate id mapping fails'
+            assert objectUnderTest.addMapping('ch-1', alternateId) == false
+        and: 'alternate id looked up by cmhandle id unsuccessfully'
+            assert objectUnderTest.cmHandleIdToAlternateId('ch-1') == null
+        where: 'alternate id has an invalid value'
+            scenario | alternateId
+            'empty'  | ''
+            'blank'  | '  '
+            'null'   | null
+    }
+
+    def 'Remove an entry from the cache.'() {
+        when: 'removing an entry'
+            objectUnderTest.removeMapping('my cmhandle id')
+        then: 'converting alternate id returns null'
+            assert objectUnderTest.alternateIdToCmHandleId('my alternate id') == null
+        and: 'converting cmhandle id returns null'
+            assert objectUnderTest.cmHandleIdToAlternateId('my cmhandle id') == null
+    }
+
+    def 'Cannot update existing alternate id.'() {
+        given: 'attempt to update an existing alternate id'
+            objectUnderTest.addMapping('my cmhandle id', 'other id')
+        expect: 'still returns the original alternate id'
+            assert objectUnderTest.cmHandleIdToAlternateId('my cmhandle id') == 'my alternate id'
+        and: 'converting other alternate id returns null'
+            assert objectUnderTest.alternateIdToCmHandleId('other id') == null
+        and: 'a warning is logged with the original alternate id'
+            def lastLoggingEvent = logger.list[1]
+            assert lastLoggingEvent.level == Level.WARN
+            assert lastLoggingEvent.formattedMessage.contains('my alternate id')
+    }
+
+    def 'Update existing alternate id with the same value.'() {
+        expect: 'update an existing alternate id with the same value returns false (no update)'
+            assert objectUnderTest.addMapping('my cmhandle id', 'my alternate id') == false
+        and: 'conversion still returns the original alternate id'
+            assert objectUnderTest.cmHandleIdToAlternateId('my cmhandle id') == 'my alternate id'
+    }
+
+    def 'Initializing cache #scenario.'() {
+        when: 'the cache is (re-)initialized'
+            objectUnderTest.cacheIsInitialized = false
+            objectUnderTest.initializeCache()
+        then: 'the alternate id can be converted to cmhandle id'
+            assert objectUnderTest.alternateIdToCmHandleId('alt-1') == convertedCmHandleId
+        and: 'the cm handle id can be converted to alternate id'
+            assert objectUnderTest.cmHandleIdToAlternateId('ch-1') == convertedAlternatId
+        and: 'the query service is called to get the initial data'
+            1 * mockCpsCmHandlerQueryService.getAllCmHandles() >> persistedCmHandles
+        where: 'the initial data has a cm handle #scenario'
+            scenario                  | persistedCmHandles                                                  || convertedAlternatId | convertedCmHandleId
+            'with alternate id'       | [new NcmpServiceCmHandle(cmHandleId: 'ch-1', alternateId: 'alt-1')] || 'alt-1'             | 'ch-1'
+            'without alternate id'    | [new NcmpServiceCmHandle(cmHandleId: 'ch-1')]                       || null                | null
+            'with blank alternate id' | [new NcmpServiceCmHandle(cmHandleId: 'ch-1', alternateId: ' ')]     || null                | null
+    }
+}
\ No newline at end of file
index 7ba163d..ca7824d 100644 (file)
@@ -1,6 +1,6 @@
 .. This work is licensed under a Creative Commons Attribution 4.0 International License.
 .. http://creativecommons.org/licenses/by/4.0
-.. Copyright (C) 2021-2022 Nordix Foundation
+.. Copyright (C) 2021-2024 Nordix Foundation
 .. Modifications Copyright (C) 2021 Bell Canada.
 
 .. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
@@ -316,24 +316,28 @@ These instances require some additional ports to be available. The default range
 
 Below are the list of distributed datastructures that we have.
 
-+--------------+---------------------------------+----------------------------------------------------------+
-| Component    | Datastructure name              |                 Use                                      |
-+==============+=================================+==========================================================+
-| cps-core     | anchorDataCache                 | Used to resolve prefix for the container name.           |
-+--------------+---------------------------------+----------------------------------------------------------+
-| cps-ncmp     | moduleSyncStartedOnCmHandles    | Watchdog process to register cmHandles.                  |
-+--------------+---------------------------------+----------------------------------------------------------+
-| cps-ncmp     | dataSyncSemaphores              | Watchdog process to sync data from the nodes.            |
-+--------------+---------------------------------+----------------------------------------------------------+
-| cps-ncmp     | moduleSyncWorkQueue             | Queue used internally for workers to pick the task.      |
-+--------------+---------------------------------+----------------------------------------------------------+
-| cps-ncmp     | untrustworthyCmHandlesSet       | Stores untrustworthy cmHandles whose TrustLevel is NONE. |
-+--------------+---------------------------------+----------------------------------------------------------+
-| cps-ncmp     | trustLevelPerDmiPlugin          | Stores the TrustLevel for the dmi-plugins.               |
-+--------------+---------------------------------+----------------------------------------------------------+
-| cps-ncmp     | moduleSetTagCacheMapConfig      | Stores the Module Set Tags for cmHandles.                |
-+--------------+---------------------------------+----------------------------------------------------------+
-| cps-ncmp     | cmSubscriptionEventCache        | Stores and tracks CmSubscription requests.               |
-+--------------+---------------------------------+----------------------------------------------------------+
-
-Total number of caches : 8
++--------------+------------------------------------+-----------------------------------------------------------+
+| Component    | Datastructure name                 |                 Use                                       |
++==============+====================================+===========================================================+
+| cps-core     | anchorDataCache                    | Used to resolve prefix for the container name.            |
++--------------+------------------------------------+-----------------------------------------------------------+
+| cps-ncmp     | moduleSyncStartedOnCmHandles       | Watchdog process to register cm handles.                  |
++--------------+------------------------------------+-----------------------------------------------------------+
+| cps-ncmp     | dataSyncSemaphores                 | Watchdog process to sync data from the nodes.             |
++--------------+------------------------------------+-----------------------------------------------------------+
+| cps-ncmp     | moduleSyncWorkQueue                | Queue used internally for workers to pick the task.       |
++--------------+------------------------------------+-----------------------------------------------------------+
+| cps-ncmp     | untrustworthyCmHandlesSet          | Stores untrustworthy cm handles whose trust level is NONE.|
++--------------+------------------------------------+-----------------------------------------------------------+
+| cps-ncmp     | trustLevelPerDmiPlugin             | Stores the trust level for the dmi-plugins.               |
++--------------+------------------------------------+-----------------------------------------------------------+
+| cps-ncmp     | moduleSetTagCacheMapConfig         | Stores the module set tags for cm handles.                |
++--------------+------------------------------------+-----------------------------------------------------------+
+| cps-ncmp     | cmSubscriptionEventCache           | Stores and tracks cm notification subscription requests.  |
++--------------+------------------------------------+-----------------------------------------------------------+
+| cps-ncmp     | alternateIdPerCmHandleId           | Stores the alternate id for each cm handle id.            |
++--------------+------------------------------------+-----------------------------------------------------------+
+| cps-ncmp     | cmHandleIdPerAlternateId           | Stores the cm handle id for each alternate id.            |
++--------------+------------------------------------+-----------------------------------------------------------+
+
+Total number of caches : 10