diff options
26 files changed, 658 insertions, 200 deletions
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryService.java index f1f71dc57c..9cbc6b0650 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryService.java @@ -84,6 +84,16 @@ public interface CmHandleQueryService { Collection<DataNode> queryNcmpRegistryByCpsPath(String cpsPath, FetchDescendantsOption fetchDescendantsOption); /** + * Method to return data nodes representing the cm handles. + * + * @param cpsPath cps path for which the cmHandle is requested + * @param queryResultLimit the maximum number of data nodes to return; if less than 1, returns all matching nodes + * @return a list of data nodes representing the cm handles. + */ + Collection<DataNode> queryNcmpRegistryByCpsPath(String cpsPath, FetchDescendantsOption fetchDescendantsOption, + int queryResultLimit); + + /** * Method to check the state of a cm handle with given id. * * @param cmHandleId cm handle id diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImpl.java index 890522ca60..8b6934b54c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImpl.java @@ -54,6 +54,7 @@ import org.springframework.stereotype.Component; public class CmHandleQueryServiceImpl implements CmHandleQueryService { private static final String ANCESTOR_CM_HANDLES = "/ancestor::cm-handles"; private static final String ALTERNATE_ID = "alternate-id"; + private static final Integer NO_LIMIT = 0; private final CpsDataService cpsDataService; private final CpsQueryService cpsQueryService; @@ -99,8 +100,15 @@ public class CmHandleQueryServiceImpl implements CmHandleQueryService { @Override public Collection<DataNode> queryNcmpRegistryByCpsPath(final String cpsPath, final FetchDescendantsOption fetchDescendantsOption) { + return queryNcmpRegistryByCpsPath(cpsPath, fetchDescendantsOption, NO_LIMIT); + } + + @Override + public Collection<DataNode> queryNcmpRegistryByCpsPath(final String cpsPath, + final FetchDescendantsOption fetchDescendantsOption, + final int queryResultLimit) { return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cpsPath, - fetchDescendantsOption); + fetchDescendantsOption, queryResultLimit); } @Override diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java index 75c52f3c60..ea8c3ca78d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java @@ -71,7 +71,7 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class CmHandleRegistrationService { - private static final int DELETE_BATCH_SIZE = 100; + private static final int DELETE_BATCH_SIZE = 300; private final CmHandleRegistrationServicePropertyHandler cmHandleRegistrationServicePropertyHandler; private final InventoryPersistence inventoryPersistence; diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java index 7f6fe76dcc..cf98b31614 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java @@ -59,7 +59,7 @@ import org.springframework.stereotype.Component; @Component public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements InventoryPersistence { - private static final int CMHANDLE_BATCH_SIZE = 100; + private static final int CMHANDLE_BATCH_SIZE = 300; private final CpsModuleService cpsModuleService; private final CpsValidator cpsValidator; @@ -145,7 +145,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv final Collection<DataNode> cmHandlesAsDataNodes = cmHandleQueryService.queryNcmpRegistryByCpsPath( - cpsPathForCmHandlesByReferences, INCLUDE_ALL_DESCENDANTS); + cpsPathForCmHandlesByReferences, INCLUDE_ALL_DESCENDANTS, cmHandleReferences.size()); return YangDataConverter.toYangModelCmHandles(cmHandlesAsDataNodes); } @@ -195,7 +195,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv public YangModelCmHandle getYangModelCmHandleByAlternateId(final String alternateId) { final String cpsPathForCmHandleByAlternateId = getCpsPathForCmHandleByAlternateId(alternateId); final Collection<DataNode> dataNodes = cmHandleQueryService - .queryNcmpRegistryByCpsPath(cpsPathForCmHandleByAlternateId, OMIT_DESCENDANTS); + .queryNcmpRegistryByCpsPath(cpsPathForCmHandleByAlternateId, OMIT_DESCENDANTS, 1); if (dataNodes.isEmpty()) { throw new CmHandleNotFoundException(alternateId); } @@ -209,7 +209,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv } final String cpsPathForCmHandlesByAlternateIds = getCpsPathForCmHandlesByAlternateIds(alternateIds); final Collection<DataNode> dataNodes = cmHandleQueryService.queryNcmpRegistryByCpsPath( - cpsPathForCmHandlesByAlternateIds, INCLUDE_ALL_DESCENDANTS); + cpsPathForCmHandlesByAlternateIds, INCLUDE_ALL_DESCENDANTS, alternateIds.size()); return YangDataConverter.toYangModelCmHandles(dataNodes); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncWatchdog.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncWatchdog.java index 6eefedb633..8c9ec03dd9 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncWatchdog.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncWatchdog.java @@ -44,7 +44,7 @@ public class ModuleSyncWatchdog { private final ModuleSyncTasks moduleSyncTasks; private final IMap<String, String> cpsAndNcmpLock; - private static final int MODULE_SYNC_BATCH_SIZE = 100; + private static final int MODULE_SYNC_BATCH_SIZE = 300; private static final String VALUE_FOR_HAZELCAST_IN_PROGRESS_MAP = "Started"; /** diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImplSpec.groovy index 884d968c4f..6ac42db782 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImplSpec.groovy @@ -131,7 +131,7 @@ class CmHandleQueryServiceImplSpec extends Specification { def cmHandleState = CmHandleState.ADVISED and: 'the persistence service returns a list of data nodes' mockCpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - "//state[@cm-handle-state='ADVISED']", OMIT_DESCENDANTS) >> sampleDataNodes + "//state[@cm-handle-state='ADVISED']", OMIT_DESCENDANTS, 0) >> sampleDataNodes when: 'cm handles are fetched by state' def result = objectUnderTest.queryCmHandleIdsByState(cmHandleState) then: 'the returned result matches the result from the persistence service' @@ -171,7 +171,7 @@ class CmHandleQueryServiceImplSpec extends Specification { def 'Retrieve Cm Handles By Operational Sync State : UNSYNCHRONIZED'() { given: 'cps data service returns a list of data nodes' mockCpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes + '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS, 0) >> sampleDataNodes when: 'cm handles are fetched by the UNSYNCHRONIZED operational sync state' def result = objectUnderTest.queryCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) then: 'the returned result is a list of data nodes returned by cps data service' @@ -184,7 +184,7 @@ class CmHandleQueryServiceImplSpec extends Specification { def cpsPath = "//state[@cm-handle-state='LOCKED']" and: 'cps data service returns a valid data node for cm handle ancestor' mockCpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - cpsPath + '/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS) + cpsPath + '/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS, 0) >> Arrays.asList(cmHandleDataNode) when: 'get cm handles by cps path is invoked' def result = objectUnderTest.queryCmHandleAncestorsByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS) @@ -198,7 +198,7 @@ class CmHandleQueryServiceImplSpec extends Specification { def cpsPath = "//cm-handles[@alternate-id='1']" and: 'cps data service returns a valid data node' mockCpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - cpsPath, INCLUDE_ALL_DESCENDANTS) + cpsPath, INCLUDE_ALL_DESCENDANTS, 0) >> Arrays.asList(cmHandleDataNode) when: 'get cm handles by cps path is invoked' def result = objectUnderTest.queryCmHandleAncestorsByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS) diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImplSpec.groovy index 2ba8505aaa..5619c5ac57 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImplSpec.groovy @@ -164,7 +164,7 @@ class InventoryPersistenceImplSpec extends Specification { def "Retrieve multiple YangModelCmHandles using cm handle references"() { given: 'the cps data service returns 2 data nodes from the DMI registry' def dataNodes = [new DataNode(xpath: xpath, leaves: ['id': cmHandleId, 'alternate-id':alternateId]), new DataNode(xpath: xpath2, leaves: ['id': cmHandleId2,'alternate-id':alternateId2])] - mockCmHandleQueries.queryNcmpRegistryByCpsPath(_, INCLUDE_ALL_DESCENDANTS) >> dataNodes + mockCmHandleQueries.queryNcmpRegistryByCpsPath(_, INCLUDE_ALL_DESCENDANTS, _) >> dataNodes when: 'retrieving the yang modelled cm handle' def results = objectUnderTest.getYangModelCmHandlesFromCmHandleReferences([cmHandleId, cmHandleId2]) then: 'verify both have returned and cmhandleIds are correct' @@ -311,7 +311,7 @@ class InventoryPersistenceImplSpec extends Specification { def expectedXPath = '/dmi-registry/cm-handles[@alternate-id=\'alternate id\']' def expectedDataNode = new DataNode(xpath: expectedXPath, leaves: [id: 'id', alternateId: 'alternate id']) and: 'query service is invoked with expected xpath' - mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS) >> [expectedDataNode] + mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS, _) >> [expectedDataNode] mockYangDataConverter.toYangModelCmHandle(expectedDataNode) >> new YangModelCmHandle(id: 'id') expect: 'getting the yang model cm handle' assert objectUnderTest.getYangModelCmHandleByAlternateId('alternate id') == new YangModelCmHandle(id: 'id') diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java index 4ede0d9c90..2c896dc3cd 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022-2024 Nordix Foundation + * Modifications Copyright (C) 2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +40,9 @@ import org.onap.cps.cpspath.parser.antlr4.CpsPathParser; @NoArgsConstructor(access = AccessLevel.PACKAGE) public class CpsPathUtil { + public static final String ROOT_NODE_XPATH = "/"; + public static final String NO_PARENT_PATH = ""; + /** * Returns a normalized xpath path query. * @@ -46,6 +50,9 @@ public class CpsPathUtil { * @return a normalized xpath String. */ public static String getNormalizedXpath(final String xpathSource) { + if (ROOT_NODE_XPATH.equals(xpathSource)) { + return NO_PARENT_PATH; + } return getCpsPathBuilder(xpathSource).build().getNormalizedXpath(); } diff --git a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy index 29bb3c7b58..03aecc2acd 100644 --- a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy +++ b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022-2024 Nordix Foundation + * Modifications Copyright (C) 2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +25,11 @@ import spock.lang.Specification class CpsPathUtilSpec extends Specification { + def 'Normalized xpath for root.'() { + expect: 'root node xpath is parsed' + assert CpsPathUtil.getNormalizedXpath('/') == '' + } + def 'Normalized xpaths for list index values using #scenario'() { when: 'xpath with #scenario is parsed' def result = CpsPathUtil.getNormalizedXpath(xpath) @@ -36,7 +42,7 @@ class CpsPathUtilSpec extends Specification { 'single quotes' | "/parent/child[@common-leaf-name='123']" } - def 'Normalized parent paths of absolute paths'() { + def 'Normalized parent paths of absolute paths.'() { when: 'a given cps path is parsed' def result = CpsPathUtil.getNormalizedParentXpath(cpsPath) then: 'the result is the expected parent path' @@ -54,7 +60,7 @@ class CpsPathUtilSpec extends Specification { '/parent/child/name[text()="value"]' || '/parent' } - def 'Normalized parent paths of descendant paths'() { + def 'Normalized parent paths of descendant paths.'() { when: 'a given cps path is parsed' def result = CpsPathUtil.getNormalizedParentXpath(cpsPath) then: 'the result is the expected parent path' @@ -72,7 +78,7 @@ class CpsPathUtilSpec extends Specification { '//parent/child/name[text()="value"]' || '//parent' } - def 'Get node ID sequence for given xpath'() { + def 'Get node ID sequence for given xpath with #scenario.'() { when: 'a given xpath with #scenario is parsed' def result = CpsPathUtil.getXpathNodeIdSequence(xpath) then: 'the result is the expected node ID sequence' @@ -89,7 +95,7 @@ class CpsPathUtilSpec extends Specification { 'does not include ancestor node' | '/parent/child/ancestor::grandparent' || ["parent","child"] } - def 'Recognizing (absolute) xpaths to List elements'() { + def 'Recognizing (absolute) xpaths to List elements.'() { expect: 'check for list returns the correct values' assert CpsPathUtil.isPathToListElement(xpath) == expectList where: 'the following xpaths are used' @@ -101,7 +107,7 @@ class CpsPathUtilSpec extends Specification { '/parent/ancestor::grandparent[@id=1]' || false } - def 'Parsing Exception'() { + def 'Parsing Exception.'() { when: 'a invalid xpath is parsed' CpsPathUtil.getNormalizedXpath('///') then: 'a path parsing exception is thrown' diff --git a/cps-service/src/main/java/org/onap/cps/api/DataNodeFactory.java b/cps-service/src/main/java/org/onap/cps/api/DataNodeFactory.java new file mode 100644 index 0000000000..1e3410c7f4 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/api/DataNodeFactory.java @@ -0,0 +1,83 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * 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.api; + +import java.util.Collection; +import java.util.Map; +import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DataNode; +import org.onap.cps.utils.ContentType; + +public interface DataNodeFactory { + + /** + * Create data nodes using an anchor, xpath, and JSON/XML string. + * + * @param anchor name of Anchor sharing same schema structure as the JSON/XML string + * @param xpath xpath of the data node + * @param nodeData JSON/XML data string + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithAnchorXpathAndNodeData(Anchor anchor, String xpath, + String nodeData, ContentType contentType); + + /** + * Create data nodes using an anchor, parent data node xpath, and JSON/XML string. + * + * @param anchor name of Anchor sharing same schema structure as the JSON/XML string + * @param parentNodeXpath xpath of the parent data node + * @param nodeData JSON/XML data string + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithAnchorParentXpathAndNodeData(Anchor anchor, + String parentNodeXpath, + String nodeData, + ContentType contentType); + + /** + * Create data nodes using a map of xpath to JSON/XML data, and anchor name. + * + * @param anchor name of Anchor sharing same schema structure as the JSON/XML string + * @param nodesData map of xpath and node JSON/XML data + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithAnchorAndXpathToNodeData(Anchor anchor, + Map<String, String> nodesData, + ContentType contentType); + + /** + * Create data nodes using a map of YANG resource name to content, xpath, and JSON/XML string. + * + * @param yangResourcesNameToContentMap map of YANG resource name to content + * @param xpath xpath of the data node + * @param nodeData JSON/XML data string + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithYangResourceXpathAndNodeData( + Map<String, String> yangResourcesNameToContentMap, + String xpath, String nodeData, + ContentType contentType); + +} diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java index 9f70ac9132..ab6f7a2df8 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java @@ -3,7 +3,7 @@ * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,9 @@ package org.onap.cps.impl; +import static org.onap.cps.cpspath.parser.CpsPathUtil.NO_PARENT_PATH; +import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH; + import io.micrometer.core.annotation.Timed; import java.io.Serializable; import java.time.OffsetDateTime; @@ -39,7 +42,7 @@ import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsAnchorService; import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsDeltaService; -import org.onap.cps.api.exceptions.DataValidationException; +import org.onap.cps.api.DataNodeFactory; import org.onap.cps.api.model.Anchor; import org.onap.cps.api.model.DataNode; import org.onap.cps.api.model.DeltaReport; @@ -54,7 +57,6 @@ import org.onap.cps.utils.DataMapUtils; import org.onap.cps.utils.JsonObjectMapper; import org.onap.cps.utils.PrefixResolver; import org.onap.cps.utils.YangParser; -import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.springframework.stereotype.Service; @Service @@ -62,14 +64,12 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class CpsDataServiceImpl implements CpsDataService { - private static final String ROOT_NODE_XPATH = "/"; - private static final String PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH = ""; private static final long DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS = 300L; - private static final String NO_DATA_NODES = "No data nodes."; private final CpsDataPersistenceService cpsDataPersistenceService; private final CpsDataUpdateEventsService cpsDataUpdateEventsService; private final CpsAnchorService cpsAnchorService; + private final DataNodeFactory dataNodeFactory; private final CpsValidator cpsValidator; private final YangParser yangParser; @@ -90,8 +90,8 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodesWithParentNodeXpath(anchor, ROOT_NODE_XPATH, nodeData, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, ROOT_NODE_XPATH, nodeData, contentType); cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes); sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.CREATE, observedTimestamp); } @@ -110,8 +110,8 @@ public class CpsDataServiceImpl implements CpsDataService { final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.CREATE, observedTimestamp); } @@ -124,9 +124,9 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> listElementDataNodeCollection = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); - if (isRootNodeXpath(parentNodeXpath)) { + final Collection<DataNode> listElementDataNodeCollection = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); + if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, listElementDataNodeCollection); } else { cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath, @@ -163,8 +163,8 @@ public class CpsDataServiceImpl implements CpsDataService { final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodesInPatch = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> dataNodesInPatch = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); final Map<String, Map<String, Serializable>> xpathToUpdatedLeaves = dataNodesInPatch.stream() .collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves)); cpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, xpathToUpdatedLeaves); @@ -180,8 +180,9 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodeUpdates = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON); + final Collection<DataNode> dataNodeUpdates = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, dataNodeUpdatesAsJson, + ContentType.JSON); for (final DataNode dataNodeUpdate : dataNodeUpdates) { processDataNodeUpdate(anchor, dataNodeUpdate); } @@ -256,8 +257,8 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp); } @@ -266,13 +267,14 @@ public class CpsDataServiceImpl implements CpsDataService { @Timed(value = "cps.data.service.datanode.descendants.batch.update", description = "Time taken to update a batch of data nodes and descendants") public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName, - final Map<String, String> nodeDataPerXPath, + final Map<String, String> nodeDataPerParentNodeXPath, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = buildDataNodesWithParentNodeXpath(anchor, nodeDataPerXPath, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorAndXpathToNodeData(anchor, nodeDataPerParentNodeXPath, contentType); cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); - nodeDataPerXPath.keySet().forEach(nodeXpath -> + nodeDataPerParentNodeXPath.keySet().forEach(nodeXpath -> sendDataUpdatedEvent(anchor, nodeXpath, Operation.UPDATE, observedTimestamp)); } @@ -283,8 +285,8 @@ public class CpsDataServiceImpl implements CpsDataService { final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> newListElements = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> newListElements = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp); } @@ -362,7 +364,7 @@ public class CpsDataServiceImpl implements CpsDataService { public void validateData(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, final ContentType contentType) { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final String xpath = ROOT_NODE_XPATH.equals(parentNodeXpath) ? PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH : + final String xpath = ROOT_NODE_XPATH.equals(parentNodeXpath) ? NO_PARENT_PATH : CpsPathUtil.getNormalizedXpath(parentNodeXpath); yangParser.validateData(contentType, nodeData, anchor, xpath); } @@ -373,8 +375,8 @@ public class CpsDataServiceImpl implements CpsDataService { final Collection<DataNode> sourceDataNodesRebuilt = new ArrayList<>(); if (sourceDataNodes != null) { final String sourceDataNodesAsJson = getDataNodesAsJson(sourceAnchor, sourceDataNodes); - sourceDataNodesRebuilt.addAll( - buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, sourceDataNodesAsJson, ContentType.JSON)); + sourceDataNodesRebuilt.addAll(dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData( + sourceAnchor, xpath, sourceDataNodesAsJson, ContentType.JSON)); } return sourceDataNodesRebuilt; } @@ -383,10 +385,12 @@ public class CpsDataServiceImpl implements CpsDataService { final Map<String, String> yangResourceContentPerName, final String targetData) { if (yangResourceContentPerName.isEmpty()) { - return buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, targetData, ContentType.JSON); + return dataNodeFactory + .createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, targetData, ContentType.JSON); } else { - return buildDataNodesWithYangResourceAndXpath(yangResourceContentPerName, xpath, - targetData, ContentType.JSON); + return dataNodeFactory + .createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, + targetData, ContentType.JSON); } } @@ -415,105 +419,6 @@ public class CpsDataServiceImpl implements CpsDataService { return prefixToDataNodes; } - private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor, - final Map<String, String> nodesJsonData, - final ContentType contentType) { - final Collection<DataNode> dataNodes = new ArrayList<>(); - for (final Map.Entry<String, String> nodeJsonData : nodesJsonData.entrySet()) { - dataNodes.addAll(buildDataNodesWithParentNodeXpath(anchor, nodeJsonData.getKey(), - nodeJsonData.getValue(), contentType)); - } - return dataNodes; - } - - private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor, final String parentNodeXpath, - final String nodeData, final ContentType contentType) { - - if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { - final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, - anchor, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "No data nodes provided"); - } - return dataNodes; - } - final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(parentNodeXpath); - final ContainerNode containerNode = - yangParser.parseData(contentType, nodeData, anchor, normalizedParentNodeXpath); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withParentNodeXpath(normalizedParentNodeXpath) - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "No data nodes provided"); - } - return dataNodes; - } - - private Collection<DataNode> buildDataNodesWithParentNodeXpath( - final Map<String, String> yangResourceContentPerName, final String xpath, - final String nodeData, final ContentType contentType) { - - if (isRootNodeXpath(xpath)) { - final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, - yangResourceContentPerName, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "Data nodes were not found under the xpath " + xpath); - } - return dataNodes; - } - final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(xpath); - final ContainerNode containerNode = - yangParser.parseData(contentType, nodeData, yangResourceContentPerName, normalizedParentNodeXpath); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withParentNodeXpath(normalizedParentNodeXpath) - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "Data nodes were not found under the xpath " + xpath); - } - return dataNodes; - } - - private Collection<DataNode> buildDataNodesWithAnchorAndXpath(final Anchor anchor, final String xpath, - final String nodeData, - final ContentType contentType) { - - if (!isRootNodeXpath(xpath)) { - final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(xpath); - if (parentNodeXpath.isEmpty()) { - return buildDataNodesWithParentNodeXpath(anchor, ROOT_NODE_XPATH, nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(anchor, xpath, nodeData, contentType); - } - - private Collection<DataNode> buildDataNodesWithYangResourceAndXpath( - final Map<String, String> yangResourceContentPerName, final String xpath, - final String nodeData, final ContentType contentType) { - if (!isRootNodeXpath(xpath)) { - final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(xpath); - if (parentNodeXpath.isEmpty()) { - return buildDataNodesWithParentNodeXpath(yangResourceContentPerName, ROOT_NODE_XPATH, - nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(yangResourceContentPerName, parentNodeXpath, - nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(yangResourceContentPerName, xpath, nodeData, contentType); - } - - private static boolean isRootNodeXpath(final String xpath) { - return ROOT_NODE_XPATH.equals(xpath); - } - private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) { cpsDataPersistenceService.batchUpdateDataLeaves(anchor.getDataspaceName(), anchor.getName(), Collections.singletonMap(dataNodeUpdate.getXpath(), dataNodeUpdate.getLeaves())); diff --git a/cps-service/src/main/java/org/onap/cps/impl/DataNodeFactoryImpl.java b/cps-service/src/main/java/org/onap/cps/impl/DataNodeFactoryImpl.java new file mode 100644 index 0000000000..76db887c8e --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/impl/DataNodeFactoryImpl.java @@ -0,0 +1,107 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * 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.impl; + +import static org.onap.cps.cpspath.parser.CpsPathUtil.NO_PARENT_PATH; +import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.onap.cps.api.DataNodeFactory; +import org.onap.cps.api.exceptions.DataValidationException; +import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DataNode; +import org.onap.cps.cpspath.parser.CpsPathUtil; +import org.onap.cps.utils.ContentType; +import org.onap.cps.utils.YangParser; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DataNodeFactoryImpl implements DataNodeFactory { + + private final YangParser yangParser; + + @Override + public Collection<DataNode> createDataNodesWithAnchorAndXpathToNodeData(final Anchor anchor, + final Map<String, String> nodesDataPerParentNodeXpath, + final ContentType contentType) { + final Collection<DataNode> dataNodes = new ArrayList<>(); + for (final Map.Entry<String, String> nodeDataToParentNodeXpath : nodesDataPerParentNodeXpath.entrySet()) { + dataNodes.addAll(createDataNodesWithAnchorParentXpathAndNodeData(anchor, nodeDataToParentNodeXpath.getKey(), + nodeDataToParentNodeXpath.getValue(), contentType)); + } + return dataNodes; + } + + @Override + public Collection<DataNode> createDataNodesWithAnchorXpathAndNodeData(final Anchor anchor, final String xpath, + final String nodeData, + final ContentType contentType) { + final String xpathToBuildNodes = isRootNodeXpath(xpath) ? NO_PARENT_PATH : + CpsPathUtil.getNormalizedParentXpath(xpath); + final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, anchor, xpathToBuildNodes); + return convertToDataNodes(xpathToBuildNodes, containerNode); + } + + @Override + public Collection<DataNode> createDataNodesWithAnchorParentXpathAndNodeData(final Anchor anchor, + final String parentNodeXpath, + final String nodeData, + final ContentType contentType) { + + final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(parentNodeXpath); + final ContainerNode containerNode = + yangParser.parseData(contentType, nodeData, anchor, normalizedParentNodeXpath); + return convertToDataNodes(normalizedParentNodeXpath, containerNode); + } + + @Override + public Collection<DataNode> createDataNodesWithYangResourceXpathAndNodeData( + final Map<String, String> yangResourceContentPerName, + final String xpath, final String nodeData, + final ContentType contentType) { + final String normalizedParentNodeXpath = isRootNodeXpath(xpath) ? NO_PARENT_PATH : + CpsPathUtil.getNormalizedParentXpath(xpath); + final ContainerNode containerNode = + yangParser.parseData(contentType, nodeData, yangResourceContentPerName, normalizedParentNodeXpath); + return convertToDataNodes(normalizedParentNodeXpath, containerNode); + } + + private static Collection<DataNode> convertToDataNodes(final String normalizedParentNodeXpath, + final ContainerNode containerNode) { + final Collection<DataNode> dataNodes = new DataNodeBuilder() + .withParentNodeXpath(normalizedParentNodeXpath) + .withContainerNode(containerNode) + .buildCollection(); + if (dataNodes.isEmpty()) { + throw new DataValidationException("No Data Nodes", "The request did not return any data nodes for xpath " + + normalizedParentNodeXpath); + } + return dataNodes; + } + + private static boolean isRootNodeXpath(final String xpath) { + return ROOT_NODE_XPATH.equals(xpath); + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy index abcda6c696..6b90e557dc 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. - * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,9 +68,10 @@ class CpsDataServiceImplSpec extends Specification { def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService) def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) def mockPrefixResolver = Mock(PrefixResolver) + def dataNodeFactory = new DataNodeFactoryImpl(yangParser) def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, - mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) + dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) def loggingListAppender @@ -107,8 +108,9 @@ class CpsDataServiceImplSpec extends Specification { def 'Saving #scenario data.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') - when: 'save data method is invoked with test-tree #scenario data' + and: 'JSON/XML data is fetched from resource file' def data = TestUtils.getResourceFileContent(dataFile) + when: 'save data method is invoked with test-tree #scenario data' objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, @@ -131,7 +133,7 @@ class CpsDataServiceImplSpec extends Specification { assert exceptionThrown.message.startsWith(expectedMessage) where: 'given parameters' scenario | invalidData | contentType || expectedMessage - 'no data nodes' | '{}' | ContentType.JSON || 'No data nodes' + 'no data nodes' | '{}' | ContentType.JSON || 'No Data Nodes' 'invalid json' | '{invalid json' | ContentType.JSON || 'Data Validation Failed' 'invalid xml' | '<invalid xml' | ContentType.XML || 'Data Validation Failed' } @@ -139,8 +141,9 @@ class CpsDataServiceImplSpec extends Specification { def 'Saving list element data fragment under Root node.'() { given: 'schema set for given anchor and dataspace references bookstore model' setupSchemaSetMocks('bookstore.yang') - when: 'save data method is invoked with list element json data' + and: 'JSON data associated with bookstore model' def jsonData = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}' + when: 'save data method is invoked with list element json data' objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp, ContentType.JSON) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, @@ -159,8 +162,8 @@ class CpsDataServiceImplSpec extends Specification { def 'Saving child data fragment under existing node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') - when: 'save data method is invoked with test-tree json data' def jsonData = '{"branch": [{"name": "New"}]}' + when: 'save data method is invoked with test-tree json data' objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree', @@ -169,7 +172,7 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) } - def 'Saving list element data fragment under existing JSON/XML node.'() { + def 'Saving list element data fragment under existing #scenario .'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with list element data' @@ -187,12 +190,13 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) where: - data | contentType - '{"branch": [{"name": "A"}, {"name": "B"}]}' | ContentType.JSON - '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>A</name></branch><branch><name>B</name></branch></test-tree>' | ContentType.XML + scenario | data | contentType + 'JSON data' | '{"branch": [{"name": "A"}, {"name": "B"}]}' | ContentType.JSON + 'XML data' | '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>A</name></branch><branch><name>B</name></branch></test-tree>' | ContentType.XML + } - def 'Saving empty list element data fragment for JSON/XML data.'() { + def 'Saving empty list element data fragment for #scenario.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with an empty list' @@ -200,9 +204,9 @@ class CpsDataServiceImplSpec extends Specification { then: 'invalid data exception is thrown' thrown(DataValidationException) where: - data | contentType - '{"branch": []}' | ContentType.JSON - '<test-tree><branch></branch></test-tree>' | ContentType.XML + scenario | data | contentType + 'JSON data' | '{"branch": []}' | ContentType.JSON + 'XML data' | '<test-tree><branch></branch></test-tree>' | ContentType.XML } def 'Get all data nodes #scenario.'() { diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/DataNodeFactorySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/DataNodeFactorySpec.groovy new file mode 100644 index 0000000000..082fb33a61 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/impl/DataNodeFactorySpec.groovy @@ -0,0 +1,196 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * 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.impl + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.core.read.ListAppender +import org.onap.cps.TestUtils +import org.onap.cps.api.CpsAnchorService +import org.onap.cps.api.exceptions.DataValidationException +import org.onap.cps.api.model.Anchor +import org.onap.cps.utils.ContentType +import org.onap.cps.utils.YangParser +import org.onap.cps.utils.YangParserHelper +import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder +import org.onap.cps.yang.YangTextSchemaSourceSet +import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Specification + +class DataNodeFactorySpec extends Specification { + + def mockCpsAnchorService = Mock(CpsAnchorService) + def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) + def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder) + def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder) + def objectUnderTest = new DataNodeFactoryImpl(yangParser) + + def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) + def loggingListAppender + def applicationContext = new AnnotationConfigApplicationContext() + + def dataspaceName = 'some-dataspace' + def anchorName = 'some-anchor' + def schemaSetName = 'some-schema-set' + def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build() + + def setup() { + mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor + logger.setLevel(Level.DEBUG) + loggingListAppender = new ListAppender() + logger.addAppender(loggingListAppender) + loggingListAppender.start() + applicationContext.refresh() + } + + void cleanup() { + ((Logger) LoggerFactory.getLogger(DataNodeFactoryImpl.class)).detachAndStopAllAppenders() + applicationContext.close() + } + + def 'Create data nodes using anchor and map of xpath to #scenario'() { + given:'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithAnchorAndXpathToNodeData(anchor, xpathToNodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == expectedDataNodes + and: 'data nodes have expected xpaths' + dataNodes.stream().map { it.getXpath() }.toList().containsAll(expectedXpaths) + where: 'the following data was used' + scenario | xpathToNodeData | contentType || expectedDataNodes | expectedXpaths + 'JSON Data' | ['/' : "{'test-tree': {'branch': []}}", '/test-tree' : "{'branch': [{'name':'Name'}]}"] | ContentType.JSON || 2 | ['/test-tree', "/test-tree/branch[@name='Name']"] + 'XML Data' | ['/test-tree' : '<branch><name>Name</name></branch>'] | ContentType.XML || 1 | ["/test-tree/branch[@name='Name']"] + } + + def 'Create data nodes using anchor, xpath and #scenario string'() { + given:'xpath, json string and schema set for given anchor and dataspace references test-tree model' + def xpath = '/' + def nodeData = TestUtils.getResourceFileContent(data) + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithAnchorXpathAndNodeData(anchor, xpath, nodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == 1 + and: 'data nodes have expected xpaths' + dataNodes[0].getXpath() == '/test-tree' + where: 'the following data was used' + scenario | data | contentType + 'JSON' | 'test-tree.json' | ContentType.JSON + 'XML' | 'test-tree.xml' | ContentType.XML + } + + def 'Building data nodes using anchor, xpath and #scenario'() { + given:'xpath, invalid json string and schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + objectUnderTest.createDataNodesWithAnchorXpathAndNodeData(anchor, '/test-tree', invalidData, contentType) + then: 'expected number of data nodes are created' + def exceptionThrown = thrown(DataValidationException) + assert exceptionThrown.message.startsWith(expectedMessage) + where: + scenario | invalidData | contentType || expectedMessage + 'no data nodes' | '{}' | ContentType.JSON || 'No Data Nodes' + 'invalid json' | '{invalid json' | ContentType.JSON || 'Data Validation Failed' + 'invalid xml' | '<invalid xml' | ContentType.XML || 'Data Validation Failed' + } + + def 'Create data nodes using anchor, parent node xpath and #scenario string'() { + given:'parent node xpath, json string and schema set for given anchor and dataspace references test-tree model' + def parentXpath = '/test-tree' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentXpath, nodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == 1 + and: 'data nodes have expected xpaths' + dataNodes[0].getXpath() == "/test-tree/branch[@name='A']" + where: 'the following data was used' + scenario | nodeData | contentType + 'JSON' | '{"branch": [{"name": "A"}]}' | ContentType.JSON + 'XML' | '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>A</name></branch></test-tree>' | ContentType.XML + } + + def 'Create data nodes using anchor, parent node xpath and invalid #scenario string'() { + given:'parent node xpath, invalid json string and schema set for given anchor and dataspace references test-tree model' + def parentXpath = '/test-tree' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + objectUnderTest.createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentXpath, invalidData, contentType) + then: 'expected number of data nodes are created' + def exceptionThrown = thrown(DataValidationException) + assert exceptionThrown.message.startsWith(expectedMessage) + where: + scenario | invalidData | contentType || expectedMessage + 'no data nodes' | '{"branch": []}' | ContentType.JSON || 'No Data Nodes' + 'invalid json' | '<test-tree><branch></branch></test-tree>' | ContentType.JSON || 'Data Validation Failed' + } + + def 'Create data nodes using schema, xpath and #scenario string'() { + given:'xpath, json string and schema set for given anchor and dataspace references bookstore model' + def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + setupSchemaSetMocksForDelta(yangResourcesNameToContentMap) + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithYangResourceXpathAndNodeData(yangResourcesNameToContentMap, '/', nodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == 1 + and: 'data nodes have expected xpath' + dataNodes[0].getXpath() == '/bookstore' + where: 'the following data was used' + scenario | nodeData | contentType + 'JSON' | '{"bookstore":{"bookstore-name":"Easons"}}' | ContentType.JSON + 'XML' | "<bookstore xmlns=\"org:onap:ccsdk:sample\"><bookstore-name>Easons</bookstore-name></bookstore>" | ContentType.XML + } + + def 'Create data nodes using schema, xpath and invalid #scenario string'() { + given:'xpath, invalid json string and schema set for given anchor and dataspace references bookstore model' + def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + setupSchemaSetMocksForDelta(yangResourcesNameToContentMap) + when: 'attempt to create data nodes' + objectUnderTest.createDataNodesWithYangResourceXpathAndNodeData(yangResourcesNameToContentMap, '/', invalidData, contentType) + then: 'expected number of data nodes are created' + def exceptionThrown = thrown(DataValidationException) + assert exceptionThrown.message.startsWith(expectedMessage) + where: + scenario | invalidData | contentType || expectedMessage + 'no json nodes' | '{}' | ContentType.JSON || 'No Data Nodes' + 'no xml nodes' | '"<bookstore xmlns=\"org:onap:ccsdk:sample\"/>' | ContentType.XML || 'Data Validation Failed' + 'invalid json' | '{invalid' | ContentType.JSON || 'Data Validation Failed' + 'invalid xml' | '<invalid' | ContentType.XML || 'Data Validation Failed' + } + + def setupSchemaSetMocks(String... yangResources) { + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources) + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext + } + + def setupSchemaSetMocksForDelta(Map<String, String> yangResourcesNameToContentMap) { + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + mockTimedYangTextSchemaSourceSetBuilder.getYangTextSchemaSourceSet(yangResourcesNameToContentMap) >> mockYangTextSchemaSourceSet + mockYangTextSchemaSourceSetCache.get(_, _) >> mockYangTextSchemaSourceSet + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap).getSchemaContext() + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext + } +} 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 db5b4f104e..f91570136c 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 @@ -3,7 +3,7 @@ * Copyright (C) 2021-2025 Nordix Foundation. * Modifications Copyright (C) 2021-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,8 @@ class E2ENetworkSliceSpec extends Specification { mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService) - def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, + def dataNodeFactory = new DataNodeFactoryImpl(yangParser) + def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) def dataspaceName = 'someDataspace' def anchorName = 'someAnchor' diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 3b7cc6063a..49646731e2 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -94,7 +94,7 @@ services: test: wget -q -O - http://localhost:8080/actuator/health/readiness | grep -q '{"status":"UP"}' || exit 1 interval: 10s timeout: 10s - retries: 3 + retries: 10 start_period: 60s nginx: diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy index 9b79af95ff..bb69f2f544 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy @@ -272,16 +272,26 @@ abstract class CpsIntegrationSpecBase extends Specification { } def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset) { - registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy.UNIQUE) + registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy.UNIQUE, { id -> "alt=${id}" }) } - def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy moduleNameStrategy ) { + def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy moduleNameStrategy) { + registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, moduleNameStrategy, { id -> "alt=${id}" }) + } + + def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy moduleNameStrategy, Closure<String> alternateIdGenerator) { + registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, moduleNameStrategy, alternateIdGenerator) + } + + def registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy moduleNameStrategy, Closure<String> alternateIdGenerator) { def cmHandles = [] def id = offset def modulePrefix = moduleNameStrategy.OVERLAPPING.equals(moduleNameStrategy) ? 'same' : moduleSetTag - def moduleReferences = (1..200).collect { "${modulePrefix}Module${it}" } + def moduleReferences = (1..200).collect { "${modulePrefix}Module${it}" } + (1..numberOfCmHandles).each { - def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: "ch-${id}", moduleSetTag: moduleSetTag, alternateId: "alt=${id}") + def alternateId = alternateIdGenerator(id) + def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: "ch-${id}", moduleSetTag: moduleSetTag, alternateId: alternateId) cmHandles.add(ncmpServiceCmHandle) dmiDispatcher1.moduleNamesPerCmHandleId[ncmpServiceCmHandle.cmHandleId] = moduleReferences dmiDispatcher2.moduleNamesPerCmHandleId[ncmpServiceCmHandle.cmHandleId] = moduleReferences diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/WriteDataJobPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/WriteDataJobPerfTest.groovy new file mode 100644 index 0000000000..27ff155471 --- /dev/null +++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/WriteDataJobPerfTest.groovy @@ -0,0 +1,69 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 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.integration.performance.ncmp + +import org.onap.cps.integration.ResourceMeter +import org.onap.cps.integration.base.CpsIntegrationSpecBase +import org.onap.cps.ncmp.api.datajobs.DataJobService +import org.onap.cps.ncmp.api.datajobs.models.DataJobMetadata +import org.onap.cps.ncmp.api.datajobs.models.DataJobWriteRequest +import org.onap.cps.ncmp.api.datajobs.models.WriteOperation +import org.springframework.beans.factory.annotation.Autowired + +/** + * This test does not depend on common performance test data. Hence it just extends the integration spec base. + */ +class WriteDataJobPerfTest extends CpsIntegrationSpecBase { + + @Autowired + DataJobService dataJobService + + def resourceMeter = new ResourceMeter() + + def populateDataJobWriteRequests(int numberOfWriteOperations) { + def writeOperations = [] + for (int i = 1; i <= numberOfWriteOperations; i++) { + def basePath = "/SubNetwork=Europe/SubNetwork=Ireland/MeContext=MyRadioNode${i}/ManagedElement=MyManagedElement${i}" + writeOperations.add(new WriteOperation("${basePath}/SomeChild=child-1", 'operation1', '1', null)) + writeOperations.add(new WriteOperation("${basePath}/SomeChild=child-2", 'operation2', '2', null)) + writeOperations.add(new WriteOperation(basePath, 'operation3', '3', null)) + } + return new DataJobWriteRequest(writeOperations) + } + + def 'Performance test for writeDataJob method'() { + given: 'register 1,000 cm handles (with alternative ids)' + registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(DMI1_URL, 'tagA', 1000, 1, ModuleNameStrategy.UNIQUE, { it -> "/SubNetwork=Europe/SubNetwork=Ireland/MeContext=MyRadioNode${it}/ManagedElement=MyManagedElement${it}" }) + def authorization = 'my authorization header' + def numberOfWriteOperations = 1000 + def dataJobWriteRequest = populateDataJobWriteRequests(numberOfWriteOperations) + def myDataJobMetadata = new DataJobMetadata('d1', '', '') + def dataJobId = 'my-data-job-id' + when: 'sending a write job to NCMP with dynamically generated write operations' + resourceMeter.start() + dataJobService.writeDataJob(authorization, dataJobId, myDataJobMetadata, dataJobWriteRequest) + resourceMeter.stop() + then: 'record the result. Not asserted, just recorded in See https://lf-onap.atlassian.net/browse/CPS-2691' + println "*** CPS-2691 Execution time: ${resourceMeter.totalTimeInSeconds} seconds" + cleanup: 'deregister test cm handles' + deregisterSequenceOfCmHandles(DMI1_URL, 1000, 1) + } +} diff --git a/k6-tests/make-logs.sh b/k6-tests/make-logs.sh new file mode 100644 index 0000000000..60976247e5 --- /dev/null +++ b/k6-tests/make-logs.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +# Copyright 2025 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. +# + +SERVICE_NAME="cps-and-ncmp" +TIMESTAMP=$(date +"%Y%m%d%H%M%S") +LOG_DIR="${WORKSPACE:-.}/logs" +TEMP_DIR="$LOG_DIR/temp_$TIMESTAMP" +ZIP_FILE="$LOG_DIR/${SERVICE_NAME}_logs_$TIMESTAMP.zip" + +mkdir -p "$LOG_DIR" +mkdir -p "$TEMP_DIR" + +# Store logs for cps-and-ncmp containers to temp directory +CONTAINER_IDS=$(docker ps --filter "name=$SERVICE_NAME" --format "{{.ID}}") +for CONTAINER_ID in $CONTAINER_IDS; do + CONTAINER_NAME=$(docker inspect --format="{{.Name}}" "$CONTAINER_ID" | sed 's/\///g') + LOG_FILE="$TEMP_DIR/${CONTAINER_NAME}_logs_$TIMESTAMP.log" + docker logs "$CONTAINER_ID" > "$LOG_FILE" +done + +# Zip the logs +zip -r "$ZIP_FILE" "$TEMP_DIR" +echo "Logs saved to $ZIP_FILE inside workspace" + +# Clean temp files +rm -r "$TEMP_DIR" + +# Delete logs older than 2 weeks +find "$LOG_DIR" -name "${SERVICE_NAME}_logs_*.zip" -mtime +14 -delete diff --git a/k6-tests/ncmp/common/cmhandle-crud.js b/k6-tests/ncmp/common/cmhandle-crud.js index 285028f13c..3b6c3ff7b7 100644 --- a/k6-tests/ncmp/common/cmhandle-crud.js +++ b/k6-tests/ncmp/common/cmhandle-crud.js @@ -51,19 +51,30 @@ export function waitForAllCmHandlesToBeReady() { function createCmHandlePayload(cmHandleIds) { return { "dmiPlugin": DMI_PLUGIN_URL, - "createdCmHandles": cmHandleIds.map((cmHandleId, index) => ({ - "cmHandle": cmHandleId, - "alternateId": cmHandleId.replace('ch-', 'Subnetwork=Europe,ManagedElement='), - "moduleSetTag": MODULE_SET_TAGS[index % MODULE_SET_TAGS.length], - "cmHandleProperties": { - "id": "123" - }, - "publicCmHandleProperties": { - "Color": "yellow", - "Size": "small", - "Shape": "cube" - } - })), + "createdCmHandles": cmHandleIds.map((cmHandleId, index) => { + // Ensure unique networkSegment within range 1-10 + let networkSegmentId = Math.floor(Math.random() * 10) + 1; // Random between 1-10 + let moduleTag = MODULE_SET_TAGS[index % MODULE_SET_TAGS.length]; + + return { + "cmHandle": cmHandleId, + "alternateId": cmHandleId.replace('ch-', 'Region=NorthAmerica,Segment='), + "moduleSetTag": moduleTag, + "cmHandleProperties": { + "segmentId": index + 1, + "networkSegment": `Region=NorthAmerica,Segment=${networkSegmentId}`, // Unique within range 1-10 + "deviceIdentifier": `Element=RadioBaseStation_5G_${index + 1000}`, // Unique per cmHandle + "hardwareVersion": `HW-${moduleTag}`, // Shares uniqueness with moduleSetTag + "softwareVersion": `Firmware_${moduleTag}`, // Shares uniqueness with moduleSetTag + "syncStatus": "ACTIVE", + "nodeCategory": "VirtualNode" + }, + "publicCmHandleProperties": { + "systemId": index + 1, + "systemName": "ncmp" + } + }; + }), }; } diff --git a/k6-tests/ncmp/common/passthrough-crud.js b/k6-tests/ncmp/common/passthrough-crud.js index a3d48fd590..eed1ab5190 100644 --- a/k6-tests/ncmp/common/passthrough-crud.js +++ b/k6-tests/ncmp/common/passthrough-crud.js @@ -67,7 +67,7 @@ export function legacyBatchRead(cmHandleIds) { } function getRandomCmHandleReference(useAlternateId) { - const prefix = useAlternateId ? 'Subnetwork=Europe,ManagedElement=' : 'ch-'; + const prefix = useAlternateId ? 'Region=NorthAmerica,Segment=' : 'ch-'; return `${prefix}${randomIntBetween(1, TOTAL_CM_HANDLES)}`; } diff --git a/k6-tests/ncmp/common/search-base.js b/k6-tests/ncmp/common/search-base.js index af2caf71ec..af7d153416 100644 --- a/k6-tests/ncmp/common/search-base.js +++ b/k6-tests/ncmp/common/search-base.js @@ -51,7 +51,7 @@ const SEARCH_PARAMETERS_PER_SCENARIO = { "cmHandleQueryParameters": [ { "conditionName": "hasAllProperties", - "conditionParameters": [{"Color": "yellow"}] + "conditionParameters": [{"systemName": "ncmp"}] } ] }, diff --git a/k6-tests/ncmp/common/utils.js b/k6-tests/ncmp/common/utils.js index ee3e9c7b4b..57ab2ea17f 100644 --- a/k6-tests/ncmp/common/utils.js +++ b/k6-tests/ncmp/common/utils.js @@ -27,7 +27,7 @@ export const DMI_PLUGIN_URL = testConfig.hosts.dmiStubUrl; export const CONTAINER_UP_TIME_IN_SECONDS = testConfig.hosts.containerUpTimeInSeconds; export const LEGACY_BATCH_TOPIC_NAME = 'legacy_batch_topic'; export const TOTAL_CM_HANDLES = 50000; -export const REGISTRATION_BATCH_SIZE = 100; +export const REGISTRATION_BATCH_SIZE = 2000; export const READ_DATA_FOR_CM_HANDLE_DELAY_MS = 300; // must have same value as in docker-compose.yml export const WRITE_DATA_FOR_CM_HANDLE_DELAY_MS = 670; // must have same value as in docker-compose.yml export const CONTENT_TYPE_JSON_PARAM = {'Content-Type': 'application/json'}; diff --git a/k6-tests/setup.sh b/k6-tests/setup.sh index c01a0f6c60..d990475522 100755 --- a/k6-tests/setup.sh +++ b/k6-tests/setup.sh @@ -26,11 +26,11 @@ docker-compose \ --profile dmi-stub \ up --quiet-pull --detach --wait || exit 1 - if [[ "$testProfile" == "kpi" ]]; then - ACTUATOR_PORT=8883 - elif [[ "$testProfile" == "endurance" ]]; then - ACTUATOR_PORT=8884 - fi +if [[ "$testProfile" == "kpi" ]]; then + ACTUATOR_PORT=8883 +elif [[ "$testProfile" == "endurance" ]]; then + ACTUATOR_PORT=8884 +fi echo "Build information:" curl --silent --show-error http://localhost:$ACTUATOR_PORT/actuator/info diff --git a/k6-tests/teardown.sh b/k6-tests/teardown.sh index 10db7ac7e0..7804a73286 100755 --- a/k6-tests/teardown.sh +++ b/k6-tests/teardown.sh @@ -18,11 +18,9 @@ echo '================================== docker info ==========================' docker ps -a -echo '================================== CPS-NCMP Logs ========================' -for CONTAINER_ID in $(docker ps --filter "name=cps-and-ncmp" --format "{{.ID}}"); do - echo "CPS-NCMP Logs for container: $CONTAINER_ID" - docker logs "$CONTAINER_ID" -done +# Zip and store logs for the containers +chmod +x make-logs.sh +./make-logs.sh testProfile=$1 docker_compose_shutdown_cmd="docker-compose -f ../docker-compose/docker-compose.yml --profile dmi-stub --project-name $testProfile down --volumes" diff --git a/test-tools/generate-metrics-report.sh b/test-tools/generate-metrics-report.sh index 7d94e5b49f..4d99adfdff 100755 --- a/test-tools/generate-metrics-report.sh +++ b/test-tools/generate-metrics-report.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright 2023 Nordix Foundation. +# Copyright 2023-2025 Nordix Foundation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -75,13 +75,13 @@ function generate_report() { grep --invert-match "^#" $TEMP_DIR/metrics-raw.txt | sort | sed 's/,[}]/}\t/' >$TEMP_DIR/metrics-all.txt # Extract useful metrics. - grep -E "^cps_|^spring_data_" $TEMP_DIR/metrics-all.txt >$TEMP_DIR/metrics-cps.txt + grep -E "^cps_|^spring_data_|^http_server_|^http_client_|^tasks_scheduled_execution_|^spring_kafka_template_|^spring_kafka_listener_" $TEMP_DIR/metrics-all.txt >$TEMP_DIR/metrics-cps.txt # Extract into columns. - grep "_count" $TEMP_DIR/metrics-cps.txt | sed 's/_count//' | cut -f 1 >$TEMP_DIR/column1.txt - grep "_count" $TEMP_DIR/metrics-cps.txt | cut -f 2 >$TEMP_DIR/column2.txt - grep "_sum" $TEMP_DIR/metrics-cps.txt | cut -f 2 >$TEMP_DIR/column3.txt - grep "_max" $TEMP_DIR/metrics-cps.txt | cut -f 2 >$TEMP_DIR/column4.txt + grep "_count" $TEMP_DIR/metrics-cps.txt | sed 's/_count//' | cut -d ' ' -f 1 >$TEMP_DIR/column1.txt + grep "_count" $TEMP_DIR/metrics-cps.txt | cut -d ' ' -f 2 >$TEMP_DIR/column2.txt + grep "_sum" $TEMP_DIR/metrics-cps.txt | cut -d ' ' -f 2 >$TEMP_DIR/column3.txt + grep "_max" $TEMP_DIR/metrics-cps.txt | cut -d ' ' -f 2 >$TEMP_DIR/column4.txt # Combine columns into report. paste $TEMP_DIR/column{1,2,3,4}.txt >$TEMP_DIR/report.txt |