From: mpriyank Date: Wed, 6 Aug 2025 12:48:07 +0000 (+0100) Subject: Split the data sync request using module and root references X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=f379ff74546a8bf5f99b9a4912082e81897b37e7;p=cps.git Split the data sync request using module and root references - Introduced a method to get concatenated module and root node for a given cm handle(using already synched modules) to use options field - The request is splitted as some devices dont support fetching the data using root url and the devices that support that can in turn return huge amount of data in one go. - Updated the data sync logic to make multiple calls for the initial data sync - The failing requests are ignored and the algorithm works on best effort basis - Testware to support all the above use cases Issue-ID: CPS-2758 Change-Id: I3d50ef8448705efdd004c6a1ee039e9691f14815 Signed-off-by: mpriyank --- diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java index 4975c35323..b64d4569e3 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java @@ -114,16 +114,18 @@ public class DmiDataOperations { * * @param cmHandleId network resource identifier * @param requestId requestId for async responses + * @param options options field for filtered response * @return {@code ResponseEntity} response entity */ - public ResponseEntity getAllResourceDataFromDmi(final String cmHandleId, final String requestId) { + public ResponseEntity getAllResourceDataFromDmi(final String cmHandleId, final String requestId, + final String options) { final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId); final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); final UrlTemplateParameters urlTemplateParameters = getUrlTemplateParameters( - PASSTHROUGH_OPERATIONAL.getDatastoreName(), yangModelCmHandle, "/", null, + PASSTHROUGH_OPERATIONAL.getDatastoreName(), yangModelCmHandle, "/", options, null); return dmiRestClient.synchronousPostOperationWithJsonData(DATA, urlTemplateParameters, jsonRequestBody, READ, DmiRestClient.NO_AUTHORIZATION); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdog.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdog.java index 708077915b..5e498bb3c6 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdog.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdog.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2024 Nordix Foundation + * Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,14 +24,18 @@ import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_D import com.hazelcast.map.IMap; import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsDataService; +import org.onap.cps.api.CpsModuleService; import org.onap.cps.ncmp.api.inventory.DataStoreSyncState; import org.onap.cps.ncmp.api.inventory.models.CompositeState; import org.onap.cps.ncmp.impl.inventory.InventoryPersistence; +import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -44,11 +48,9 @@ public class DataSyncWatchdog { private static final boolean DATA_SYNC_DONE = true; private final InventoryPersistence inventoryPersistence; - + private final CpsModuleService cpsModuleService; private final CpsDataService cpsDataService; - private final ModuleOperationsUtils moduleOperationsUtils; - private final IMap dataSyncSemaphores; /** @@ -57,29 +59,57 @@ public class DataSyncWatchdog { */ @Scheduled(initialDelayString = "${ncmp.timers.cm-handle-data-sync.initial-delay-ms:40000}", fixedDelayString = "${ncmp.timers.cm-handle-data-sync.sleep-time-ms:30000}") - public void executeUnSynchronizedReadyCmHandlePoll() { - moduleOperationsUtils.getUnsynchronizedReadyCmHandles().forEach(unSynchronizedReadyCmHandle -> { - final String cmHandleId = unSynchronizedReadyCmHandle.getId(); - if (hasPushedIntoSemaphoreMap(cmHandleId)) { - log.info("Executing data sync on {}", cmHandleId); - final CompositeState compositeState = inventoryPersistence - .getCmHandleState(cmHandleId); - final String resourceData = moduleOperationsUtils.getResourceData(cmHandleId); - if (resourceData == null) { - log.error("Error retrieving resource data for Cm-Handle: {}", cmHandleId); - } else { - cpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId, - resourceData, OffsetDateTime.now()); - setSyncStateToSynchronized().accept(compositeState); - inventoryPersistence.saveCmHandleState(cmHandleId, compositeState); - updateDataSyncSemaphoreMap(cmHandleId); - log.info("Data sync finished for {}", cmHandleId); - } - } else { - log.info("{} already processed by another instance", cmHandleId); + public void executeUnsynchronizedReadyCmHandleForInitialDataSync() { + final List unsynchronizedReadyCmHandles = + moduleOperationsUtils.getUnsynchronizedReadyCmHandles(); + unsynchronizedReadyCmHandles.stream().map(YangModelCmHandle::getId).forEach(this::synchronizeCmHandle); + } + + private void synchronizeCmHandle(final String cmHandleId) { + + if (!hasPushedIntoSemaphoreMap(cmHandleId)) { + log.debug("{} already processed by another instance", cmHandleId); + return; + } + + try { + performDataSynchronizationForCmHandle(cmHandleId); + } catch (final Exception exception) { + log.error("Failed to complete data sync for CM handle: {}", cmHandleId, exception); + } + } + + private void performDataSynchronizationForCmHandle(final String cmHandleId) { + log.info("Executing data sync on {}", cmHandleId); + final CompositeState compositeState = inventoryPersistence.getCmHandleState(cmHandleId); + final Collection rootNodeReferences = + cpsModuleService.getRootNodeReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId); + + for (final String rootNodeReference : rootNodeReferences) { + synchronizeRootNodeReferences(cmHandleId, rootNodeReference); + } + + setSyncStateToSynchronized().accept(compositeState); + inventoryPersistence.saveCmHandleState(cmHandleId, compositeState); + updateDataSyncSemaphoreMap(cmHandleId); + log.info("Data sync finished for {}", cmHandleId); + } + + private void synchronizeRootNodeReferences(final String cmHandleId, final String rootNodeReference) { + final String options = String.format("(fields=%s)", rootNodeReference); + + try { + final String resourceData = moduleOperationsUtils.getResourceData(cmHandleId, options); + if (resourceData == null) { + log.warn("No resource data found for CM handle: {} with options: {}", cmHandleId, options); + return; } - }); - log.debug("No Cm-Handles currently found in READY State and Operational Sync State is UNSYNCHRONIZED"); + cpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId, resourceData, + OffsetDateTime.now()); + } catch (final Exception exception) { + log.error("Failed to sync module and root node for CM handle: {} with options: {}", cmHandleId, options, + exception); + } } private Consumer setSyncStateToSynchronized() { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtils.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtils.java index 203198d49d..e7e654006d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtils.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtils.java @@ -56,6 +56,7 @@ public class ModuleOperationsUtils { private final CmHandleQueryService cmHandleQueryService; private final DmiDataOperations dmiDataOperations; private final JsonObjectMapper jsonObjectMapper; + private static final String RETRY_ATTEMPT_KEY = "attempt"; private static final String MODULE_SET_TAG_KEY = "moduleSetTag"; public static final String MODULE_SET_TAG_MESSAGE_FORMAT = "Upgrade to ModuleSetTag: %s"; @@ -167,9 +168,9 @@ public class ModuleOperationsUtils { * @param cmHandleId cm handle id * @return optional string containing the resource data */ - public String getResourceData(final String cmHandleId) { + public String getResourceData(final String cmHandleId, final String options) { final ResponseEntity resourceDataResponseEntity = dmiDataOperations.getAllResourceDataFromDmi( - cmHandleId, UUID.randomUUID().toString()); + cmHandleId, UUID.randomUUID().toString(), options); if (resourceDataResponseEntity.getStatusCode().is2xxSuccessful()) { return getFirstResource(resourceDataResponseEntity.getBody()); } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy index 1f4da93b6c..fa8a346caa 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy @@ -151,11 +151,11 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { mockYangModelCmHandleRetrieval([yangModelCmHandleProperty], 'my-module-set-tag') and: 'a positive response from DMI service when it is called with the expected parameters' def responseFromDmi = new ResponseEntity(HttpStatus.OK) - def expectedTemplateWithVariables = new UrlTemplateParameters('myServiceName/dmi/v1/ch/{cmHandleId}/data/ds/{datastore}?resourceIdentifier={resourceIdentifier}', ['resourceIdentifier': '/', 'datastore': 'ncmp-datastore:passthrough-operational', 'cmHandleId': cmHandleId]) + def expectedTemplateWithVariables = new UrlTemplateParameters('myServiceName/dmi/v1/ch/{cmHandleId}/data/ds/{datastore}?resourceIdentifier={resourceIdentifier}&options={options}', ['resourceIdentifier': '/', 'datastore': 'ncmp-datastore:passthrough-operational', 'cmHandleId': cmHandleId, 'options': OPTIONS_PARAM]) def expectedJson = '{"operation":"read","cmHandleProperties":{"prop1":"val1"},"moduleSetTag":"my-module-set-tag"}' mockDmiRestClient.synchronousPostOperationWithJsonData(DATA, expectedTemplateWithVariables, expectedJson, READ, null) >> responseFromDmi when: 'get resource data is invoked' - def result = objectUnderTest.getAllResourceDataFromDmi(cmHandleId, NO_REQUEST_ID) + def result = objectUnderTest.getAllResourceDataFromDmi(cmHandleId, NO_REQUEST_ID, OPTIONS_PARAM) then: 'the result is the response from the DMI service' assert result == responseFromDmi } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdogSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdogSpec.groovy index ae6cb120a5..03a302910d 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdogSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdogSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation + * Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ package org.onap.cps.ncmp.impl.inventory.sync import com.hazelcast.map.IMap import org.onap.cps.api.CpsDataService +import org.onap.cps.api.CpsModuleService import org.onap.cps.ncmp.api.inventory.models.CompositeState import org.onap.cps.ncmp.api.inventory.DataStoreSyncState import org.onap.cps.ncmp.impl.inventory.InventoryPersistence @@ -34,34 +35,33 @@ import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_D class DataSyncWatchdogSpec extends Specification { def mockInventoryPersistence = Mock(InventoryPersistence) - + def mockCpsModuleService = Mock(CpsModuleService) def mockCpsDataService = Mock(CpsDataService) - - def mockSyncUtils = Mock(ModuleOperationsUtils) - + def mockModuleOperationUtils = Mock(ModuleOperationsUtils) def mockDataSyncSemaphores = Mock(IMap) def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}' - def objectUnderTest = new DataSyncWatchdog(mockInventoryPersistence, mockCpsDataService, mockSyncUtils, mockDataSyncSemaphores) + def objectUnderTest = new DataSyncWatchdog(mockInventoryPersistence, mockCpsModuleService, mockCpsDataService, mockModuleOperationUtils, mockDataSyncSemaphores) def compositeState = getCompositeState() - def yangModelCmHandle1 = createSampleYangModelCmHandle('cm-handle-1') - def yangModelCmHandle2 = createSampleYangModelCmHandle('cm-handle-2') def 'Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED.'() { given: 'sample resource data' def resourceData = jsonString and: 'sync utilities returns a cm handle twice' - mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1, yangModelCmHandle2] + mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1, yangModelCmHandle2] + and: 'we have the module and root nodes references to form the options field' + mockCpsModuleService.getRootNodeReferences(_, 'cm-handle-1') >> ['some-module-1:some-root-node'] + mockCpsModuleService.getRootNodeReferences(_, 'cm-handle-2') >> ['some-module-2:some-root-node'] when: 'data sync poll is executed' - objectUnderTest.executeUnSynchronizedReadyCmHandlePoll() + objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync() then: 'the inventory persistence cm handle returns a composite state for the first cm handle' 1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState and: 'the sync util returns first resource data' - 1 * mockSyncUtils.getResourceData('cm-handle-1') >> resourceData + 1 * mockModuleOperationUtils.getResourceData('cm-handle-1', '(fields=some-module-1:some-root-node)') >> resourceData and: 'the cm-handle data is saved' 1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-1', jsonString, _) and: 'the first cm handle operational sync state is updated' @@ -69,7 +69,7 @@ class DataSyncWatchdogSpec extends Specification { then: 'the inventory persistence cm handle returns a composite state for the second cm handle' 1 * mockInventoryPersistence.getCmHandleState('cm-handle-2') >> compositeState and: 'the sync util returns first resource data' - 1 * mockSyncUtils.getResourceData('cm-handle-2') >> resourceData + 1 * mockModuleOperationUtils.getResourceData('cm-handle-2', '(fields=some-module-2:some-root-node)') >> resourceData and: 'the cm-handle data is saved' 1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-2', jsonString, _) and: 'the second cm handle operational sync state is updated from "UNSYNCHRONIZED" to "SYNCHRONIZED"' @@ -78,28 +78,68 @@ class DataSyncWatchdogSpec extends Specification { def 'Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED without resource data.'() { given: 'sync utilities returns a cm handle' - mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] + mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] + and: 'the module service returns the module and root nodes references to form the options field' + mockCpsModuleService.getRootNodeReferences(_,'cm-handle-1') >> ['some-module-1:some-root-node'] when: 'data sync poll is executed' - objectUnderTest.executeUnSynchronizedReadyCmHandlePoll() + objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync() then: 'the inventory persistence cm handle returns a composite state for the first cm handle' 1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState and: 'the sync util returns no resource data' - 1 * mockSyncUtils.getResourceData('cm-handle-1') >> null + 1 * mockModuleOperationUtils.getResourceData('cm-handle-1', '(fields=some-module-1:some-root-node)') >> null and: 'the cm-handle data is not saved' 0 * mockCpsDataService.saveData(*_) } def 'Data Sync for Cm Handle that is already being processed.'() { given: 'sync utilities returns a cm handle' - mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] + mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] + and: 'the module service returns the module and root nodes references to form the options field' + mockCpsModuleService.getRootNodeReferences(_,'cm-handle-1') >> ['some-module-1:some-root-node'] and: 'the shared data sync semaphore indicate it is already being processed' mockDataSyncSemaphores.putIfAbsent('cm-handle-1', _, _, _) >> 'something (not null)' when: 'data sync poll is executed' - objectUnderTest.executeUnSynchronizedReadyCmHandlePoll() + objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync() then: 'it is NOT processed e.g. state is not requested' 0 * mockInventoryPersistence.getCmHandleState(*_) } + def 'Data sync handles exception during overall cm handle processing.'() { + given: 'sync utilities returns a cm handle' + mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] + and: 'semaphore map allows processing' + mockDataSyncSemaphores.putIfAbsent('cm-handle-1', false, _, _) >> null + and: 'getting cm handle state throws exception' + mockInventoryPersistence.getCmHandleState('cm-handle-1') >> { throw new RuntimeException('some exception') } + when: 'data sync poll is executed' + objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync() + then: 'no exception is thrown' + noExceptionThrown() + } + + def 'Data sync handles exception during resource data retrieval.'() { + given: 'sync utilities returns a cm handle' + mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] + and: 'semaphore map allows processing' + mockDataSyncSemaphores.putIfAbsent('cm-handle-1', false, _, _) >> null + and: 'module operations returns module and root nodes references' + mockCpsModuleService.getRootNodeReferences(_,'cm-handle-1') >> ['some-module-1:some-root-node', 'some-module-2:some-root-node'] + when: 'data sync poll is executed' + objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync() + then: 'cm handle state is retrieved' + 1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState + and: 'first module sync succeeds' + 1 * mockModuleOperationUtils.getResourceData('cm-handle-1', '(fields=some-module-1:some-root-node)') >> jsonString + 1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-1', jsonString, _) + and: 'second module sync throws exception' + 1 * mockModuleOperationUtils.getResourceData('cm-handle-1', '(fields=some-module-2:some-root-node)') >> { throw new RuntimeException('Some network error') } + and: 'no data is saved for failed module' + 0 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-1', _, _) + and: 'cm handle state is still updated (processing continues after module failure)' + 1 * mockInventoryPersistence.saveCmHandleState('cm-handle-1', compositeState) + 1 * mockDataSyncSemaphores.replace('cm-handle-1', true) + } + def createSampleYangModelCmHandle(cmHandleId) { def compositeState = getCompositeState() return new YangModelCmHandle(id: cmHandleId, compositeState: compositeState) diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtilsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtilsSpec.groovy index fdd7e47875..5fa89614d3 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtilsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtilsSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2024 Nordix Foundation + * Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved. * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,6 +26,7 @@ import ch.qos.logback.classic.Logger import ch.qos.logback.core.read.ListAppender import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.api.CpsModuleService import org.onap.cps.ncmp.api.inventory.models.CompositeState import org.onap.cps.ncmp.api.inventory.models.CompositeStateBuilder import org.onap.cps.ncmp.impl.data.DmiDataOperations @@ -49,9 +50,7 @@ import static org.onap.cps.ncmp.api.inventory.models.LockReasonCategory.MODULE_U class ModuleOperationsUtilsSpec extends Specification{ def mockCmHandleQueries = Mock(CmHandleQueryService) - def mockDmiDataOperations = Mock(DmiDataOperations) - def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) def objectUnderTest = new ModuleOperationsUtils(mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper) @@ -154,16 +153,16 @@ class ModuleOperationsUtilsSpec extends Specification{ JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString) and: 'DMI operations are mocked to return a response based on the scenario' def responseEntity = new ResponseEntity<>(statusCode == HttpStatus.OK ? jsonNode : null, statusCode) - mockDmiDataOperations.getAllResourceDataFromDmi('cm-handle-123', _) >> responseEntity + mockDmiDataOperations.getAllResourceDataFromDmi('cm-handle-123', _, 'some options') >> responseEntity when: 'get resource data is called' - def result = objectUnderTest.getResourceData('cm-handle-123') + def actualResult = objectUnderTest.getResourceData('cm-handle-123', 'some options') then: 'the returned data matches the expected result' - assert result == expectedResult + assert actualResult == expectedResult where: - scenario | statusCode | expectedResult - 'successful response' | HttpStatus.OK | '{"stores:bookstore":{"categories":[{"code":"01"}]}}' - 'response with not found status' | HttpStatus.NOT_FOUND | null - 'response with internal server error' | HttpStatus.INTERNAL_SERVER_ERROR | null + scenario | statusCode || expectedResult + 'successful response' | HttpStatus.OK || '{"stores:bookstore":{"categories":[{"code":"01"}]}}' + 'response with not found status' | HttpStatus.NOT_FOUND || null + 'response with internal server error' | HttpStatus.INTERNAL_SERVER_ERROR || null } def 'Extract module set tag and number of attempt when lock reason contains #scenario'() { diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java index 2494be4021..460f4504e3 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-2025 Nordix Foundation + * Copyright (C) 2020-2025 OpenInfra Foundation Europe. All rights reserved. * Modifications Copyright (C) 2020-2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ @@ -171,4 +171,13 @@ public interface CpsModuleService { */ void deleteAllUnusedYangModuleData(String dataspaceName); + /** + * Get the collection of concatenated module-name:root-node of the provided anchor in the given dataspace. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @return collection of module and root nodes concatenated with : as separator + */ + Collection getRootNodeReferences(String dataspaceName, String anchorName); + } diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsModuleServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsModuleServiceImpl.java index e50325c739..cf3e6bbf9c 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsModuleServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsModuleServiceImpl.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-2025 Nordix Foundation + * Copyright (C) 2020-2025 OpenInfra Foundation Europe. All rights reserved. * Modifications Copyright (C) 2020-2021 Pantheon.tech * Modifications Copyright (C) 2022 Bell Canada * Modifications Copyright (C) 2022 TechMahindra Ltd @@ -38,6 +38,7 @@ import org.onap.cps.api.model.SchemaSet; import org.onap.cps.api.parameters.CascadeDeleteAllowed; import org.onap.cps.spi.CpsModulePersistenceService; import org.onap.cps.utils.CpsValidator; +import org.onap.cps.utils.YangParser; import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder; import org.onap.cps.yang.YangTextSchemaSourceSet; import org.springframework.stereotype.Service; @@ -52,6 +53,7 @@ public class CpsModuleServiceImpl implements CpsModuleService { private final CpsAnchorService cpsAnchorService; private final CpsValidator cpsValidator; private final TimedYangTextSchemaSourceSetBuilder timedYangTextSchemaSourceSetBuilder; + private final YangParser yangParser; @Override @Timed(value = "cps.module.service.schemaset.create", @@ -179,6 +181,12 @@ public class CpsModuleServiceImpl implements CpsModuleService { cpsModulePersistenceService.deleteAllUnusedYangModuleData(dataspaceName); } + @Override + public Collection getRootNodeReferences(final String dataspaceName, final String anchorName) { + final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); + return yangParser.getRootNodeReferences(anchor); + } + private boolean isCascadeDeleteProhibited(final CascadeDeleteAllowed cascadeDeleteAllowed) { return CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED == cascadeDeleteAllowed; } diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangParser.java b/cps-service/src/main/java/org/onap/cps/utils/YangParser.java index d998f096e6..dfce4bf2e5 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/YangParser.java +++ b/cps-service/src/main/java/org/onap/cps/utils/YangParser.java @@ -27,6 +27,8 @@ import static org.onap.cps.utils.YangParserHelper.VALIDATE_ONLY; import io.micrometer.core.annotation.Timed; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.exceptions.DataValidationException; @@ -116,6 +118,23 @@ public class YangParser { return convertToCpsPath(restConfStylePath, schemaContext); } + /** + * Get the collection of concatenated module-name:root-node of the provided anchor. + * + * @param anchor Anchor + * @return Concatentated module and root node + */ + public Set getRootNodeReferences(final Anchor anchor) { + final SchemaContext schemaContext = getSchemaContext(anchor); + return schemaContext.getModules() + .stream() + .flatMap(module -> + module.getChildNodes() + .stream().map(rootNode -> + module.getName() + ":" + rootNode.getQName().getLocalName())) + .collect(Collectors.toSet()); + } + private SchemaContext getSchemaContext(final Anchor anchor) { return yangTextSchemaSourceSetCache.get(anchor.getDataspaceName(), anchor.getSchemaSetName()) .getSchemaContext(); diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsModuleServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsModuleServiceImplSpec.groovy index 48db53c882..21ec9123b9 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsModuleServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsModuleServiceImplSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-2025 Nordix Foundation + * Copyright (C) 2020-2025 OpenInfra Foundation Europe. All rights reserved. * Modifications Copyright (C) 2020-2021 Pantheon.tech * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2022 TechMahindra Ltd. @@ -34,6 +34,7 @@ import org.onap.cps.api.model.Anchor import org.onap.cps.api.model.ModuleDefinition import org.onap.cps.api.model.ModuleReference import org.onap.cps.api.model.SchemaSet +import org.onap.cps.utils.YangParser import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder import org.onap.cps.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder @@ -49,8 +50,9 @@ class CpsModuleServiceImplSpec extends Specification { def mockCpsAnchorService = Mock(CpsAnchorService) def mockCpsValidator = Mock(CpsValidator) def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder() + def mockYangParser = Mock(YangParser) - def objectUnderTest = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) + def objectUnderTest = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder,mockYangParser) def 'Create schema set.'() { when: 'Create schema set method is invoked' @@ -266,6 +268,16 @@ class CpsModuleServiceImplSpec extends Specification { 1 * mockCpsModulePersistenceService.schemaSetExists('some-dataspace-name', 'some-schema-set-name') } + def 'Get module and root nodes'() { + given: 'an anchor' + def myAnchor = createAnchors(1)[0] + mockCpsAnchorService.getAnchor('my-dataspace', 'my-anchor-1') >> myAnchor + when: 'module and root nodes are fetched for my anchor' + objectUnderTest.getRootNodeReferences('my-dataspace', 'my-anchor-1') + then: 'the call is delegated to the yang parser with correct anchor' + 1 * mockYangParser.getRootNodeReferences(myAnchor) + } + def getModuleReferences() { return [new ModuleReference('some module name','some revision name')] } diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy index 4713283c9b..5d1c937876 100755 --- a/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy @@ -45,7 +45,7 @@ class E2ENetworkSliceSpec extends Specification { def mockCpsValidator = Mock(CpsValidator) def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder() def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, timedYangTextSchemaSourceSetBuilder) - def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) + def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder,yangParser) def mockCpsDataUpdateEventsProducer = Mock(CpsDataUpdateEventsProducer) def dataNodeFactory = new DataNodeFactoryImpl(yangParser) def cpsDataServiceImpl = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsDataUpdateEventsProducer, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser) diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy index 93ee182d94..ad661f6e8a 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy @@ -29,7 +29,9 @@ import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder import org.onap.cps.yang.YangTextSchemaSourceSet import org.opendaylight.yangtools.yang.common.QName import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode import org.opendaylight.yangtools.yang.model.api.ListSchemaNode +import org.opendaylight.yangtools.yang.model.api.Module import org.opendaylight.yangtools.yang.model.api.SchemaContext import spock.lang.Specification @@ -158,4 +160,20 @@ class YangParserSpec extends Specification { 1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema') } + def 'Get module and root node references using an anchor'() { + given: 'a schema context with module and root node' + def mockModule = Mock(Module) { + getName() >> 'bookstore' + } + def mockRootNode = Mock(DataSchemaNode) { + getQName() >> QName.create('bookstore', 'book') + } + mockModule.getChildNodes() >> [mockRootNode] + mockSchemaContext.getModules() >> [mockModule] + when: 'we get module and root nodes references for the anchor' + def result = objectUnderTest.getRootNodeReferences(anchor) + then: 'the result contains expected module:rootnode combination' + assert result == ['bookstore:book'] as Set + } + }