From ba897d6fde0802ec1b1f9087f49573c0994f6ab6 Mon Sep 17 00:00:00 2001 From: sourabh_sourabh Date: Wed, 27 Aug 2025 11:44:10 +0100 Subject: [PATCH] Modified Inventory loader to handle module install or upgrade based on flag Issue-ID: CPS-2970 Change-Id: I5b8b0aa2db3fcad6da2a5dd774720f22855523cd Signed-off-by: sourabh_sourabh --- .../onap/cps/ncmp/init/InventoryModelLoader.java | 65 +++++++-- .../resources/models/dmi-registry@2025-07-22.yang | 162 +++++++++++++++++++++ .../cps/ncmp/init/InventoryModelLoaderSpec.groovy | 50 ++++++- .../org/onap/cps/init/AbstractModelLoader.java | 35 ++++- .../onap/cps/init/AbstractModelLoaderSpec.groovy | 36 ++++- docker-compose/docker-compose.yml | 1 + .../integration/base/CpsIntegrationSpecBase.groovy | 2 +- .../ModuleUpgradeServiceIntegrationSpec.groovy | 94 ++++++++++++ .../data/inventory/dmi-registry@2025-07-22.yang | 162 +++++++++++++++++++++ 9 files changed, 586 insertions(+), 21 deletions(-) create mode 100644 cps-ncmp-service/src/main/resources/models/dmi-registry@2025-07-22.yang create mode 100644 integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/inventory/ModuleUpgradeServiceIntegrationSpec.groovy create mode 100644 integration-test/src/test/resources/data/inventory/dmi-registry@2025-07-22.yang diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/InventoryModelLoader.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/InventoryModelLoader.java index 514d9b8fe4..51ba3ecf8e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/InventoryModelLoader.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/InventoryModelLoader.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023-2024 Nordix Foundation + * Copyright (C) 2023-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. @@ -31,6 +31,7 @@ import org.onap.cps.api.CpsDataspaceService; import org.onap.cps.api.CpsModuleService; import org.onap.cps.init.AbstractModelLoader; import org.onap.cps.ncmp.utils.events.NcmpInventoryModelOnboardingFinishedEvent; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -39,9 +40,12 @@ import org.springframework.stereotype.Service; public class InventoryModelLoader extends AbstractModelLoader { private final ApplicationEventPublisher applicationEventPublisher; - private static final String NEW_MODEL_FILE_NAME = "dmi-registry@2024-02-23.yang"; - private static final String NEW_SCHEMA_SET_NAME = "dmi-registry-2024-02-23"; - private static final String REGISTRY_DATANODE_NAME = "dmi-registry"; + private static final String PREVIOUS_SCHEMA_SET_NAME = "dmi-registry-2024-02-23"; + private static final String NEW_INVENTORY_SCHEMA_SET_NAME = "dmi-registry-2025-07-22"; + private static final String INVENTORY_YANG_MODULE_NAME = "dmi-registry"; + + @Value("${ncmp.inventory.model.upgrade.r20250722.enabled:false}") + private boolean newRevisionEnabled; public InventoryModelLoader(final CpsDataspaceService cpsDataspaceService, final CpsModuleService cpsModuleService, @@ -54,19 +58,35 @@ public class InventoryModelLoader extends AbstractModelLoader { @Override public void onboardOrUpgradeModel() { - updateInventoryModel(); - log.info("Inventory Model updated successfully"); + final String schemaToInstall = newRevisionEnabled ? NEW_INVENTORY_SCHEMA_SET_NAME : PREVIOUS_SCHEMA_SET_NAME; + if (newRevisionEnabled) { + if (doesAnchorExist(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR)) { + final String moduleRevision = getModuleRevision(schemaToInstall); + if (isModuleRevisionInstalled(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, INVENTORY_YANG_MODULE_NAME, + moduleRevision)) { + log.info("Revision {} is already installed.", moduleRevision); + } else { + upgradeInventoryModel(); + performInventoryDataMigration(); + } + } else { + installInventoryModel(schemaToInstall); + } + } else { + installInventoryModel(schemaToInstall); + } applicationEventPublisher.publishEvent(new NcmpInventoryModelOnboardingFinishedEvent(this)); } - private void updateInventoryModel() { + private void installInventoryModel(final String schemaSetName) { createDataspace(NCMP_DATASPACE_NAME); createDataspace(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME); - createSchemaSet(NCMP_DATASPACE_NAME, NEW_SCHEMA_SET_NAME, NEW_MODEL_FILE_NAME); - createAnchor(NCMP_DATASPACE_NAME, NEW_SCHEMA_SET_NAME, NCMP_DMI_REGISTRY_ANCHOR); - updateAnchorSchemaSet(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NEW_SCHEMA_SET_NAME); - createTopLevelDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, REGISTRY_DATANODE_NAME); + final String yangFileName = toYangFileName(schemaSetName); + createSchemaSet(NCMP_DATASPACE_NAME, schemaSetName, yangFileName); + createAnchor(NCMP_DATASPACE_NAME, schemaSetName, NCMP_DMI_REGISTRY_ANCHOR); + createTopLevelDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, INVENTORY_YANG_MODULE_NAME); deleteOldButNotThePreviousSchemaSets(); + log.info("Inventory model {} installed successfully,", schemaSetName); } private void deleteOldButNotThePreviousSchemaSets() { @@ -75,4 +95,27 @@ public class InventoryModelLoader extends AbstractModelLoader { deleteUnusedSchemaSets(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME); } + private void upgradeInventoryModel() { + final String yangFileName = toYangFileName(NEW_INVENTORY_SCHEMA_SET_NAME); + createSchemaSet(NCMP_DATASPACE_NAME, NEW_INVENTORY_SCHEMA_SET_NAME, yangFileName); + cpsAnchorService.updateAnchorSchemaSet(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + NEW_INVENTORY_SCHEMA_SET_NAME); + log.info("Inventory upgraded successfully to model {}", NEW_INVENTORY_SCHEMA_SET_NAME); + } + + private void performInventoryDataMigration() { + // TODO further implementation is pending + //1. Load all the cm handles (in batch) + //2. Copy the state and known properties + log.info("Inventory module data migration is completed successfully."); + } + + private static String toYangFileName(final String schemaSetName) { + return INVENTORY_YANG_MODULE_NAME + "@" + getModuleRevision(schemaSetName) + ".yang"; + } + + private static String getModuleRevision(final String schemaSetName) { + // Extract the revision part ( for example: 2024-02-23) + return schemaSetName.substring(INVENTORY_YANG_MODULE_NAME.length() + 1); + } } diff --git a/cps-ncmp-service/src/main/resources/models/dmi-registry@2025-07-22.yang b/cps-ncmp-service/src/main/resources/models/dmi-registry@2025-07-22.yang new file mode 100644 index 0000000000..4405c144d8 --- /dev/null +++ b/cps-ncmp-service/src/main/resources/models/dmi-registry@2025-07-22.yang @@ -0,0 +1,162 @@ +module dmi-registry { + + yang-version 1.1; + + namespace "org:onap:cps:ncmp"; + + prefix dmi-reg; + + contact "toine.siebelink@est.tech"; + + revision "2025-07-22" { + description + "Added dmi-registry.cm-handles.dmi-properties (string) to replace (now deprecated) dmi-registry.cm-handles.additional-properties (list). + Added dmi-registry.cm-handles.cm-handle-state to replace (now deprecated) and dmi-registry.cm-handles.state.cm-handle-state"; + } + + revision "2024-02-23" { + description + "Added data-producer-identifier"; + } + + revision "2023-11-27" { + description + "Added alternate-id"; + } + + revision "2023-08-23" { + description + "Added module-set-tag"; + } + + revision "2022-05-10" { + description + "Added data-sync-enabled, sync-state with state, last-sync-time, data-store-sync-state with operational and running syncstate"; + } + + revision "2022-02-10" { + description + "Added state, lock-reason, lock-reason-details to aid with cmHandle sync and timestamp to aid with retry/timeout scenarios"; + } + + revision "2021-12-13" { + description + "Added new list of public-properties and additional-properties for a Cm-Handle which are exposed to clients of the NCMP interface"; + } + + revision "2021-10-20" { + description + "Added dmi-data-service-name & dmi-model-service-name to allow separate DMI instances for each responsibility"; + } + + revision "2021-05-20" { + description + "Initial Version"; + } + + grouping LockReason { + leaf reason { + type string; + } + leaf details { + type string; + } + } + + grouping SyncState { + leaf sync-state { + type string; + } + leaf last-sync-time { + type string; + } + } + + grouping Datastores { + container operational { + uses SyncState; + } + container running { + uses SyncState; + } + } + + container dmi-registry { + list cm-handles { + key "id"; + leaf id { + type string; + } + leaf dmi-service-name { + type string; + } + leaf dmi-data-service-name { + type string; + } + leaf dmi-model-service-name { + type string; + } + leaf module-set-tag { + type string; + } + leaf alternate-id { + type string; + } + leaf data-producer-identifier { + type string; + } + leaf dmi-properties { + type string; + } + leaf cm-handle-state { + type string; + } + + list additional-properties { + key "name"; + leaf name { + type string; + } + leaf value { + type string; + } + status deprecated; // Replaced by dmi-properties + } + + list public-properties { + key "name"; + leaf name { + type string; + } + leaf value { + type string; + } + } + + container state { + leaf cm-handle-state { + type string; + status deprecated; + } + + container lock-reason { + uses LockReason; + } + + leaf last-update-time { + type string; + } + + leaf data-sync-enabled { + type boolean; + default "false"; + } + + container datastores { + uses Datastores; + } + } + } + } +} + diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy index dc6ec4120b..dfc7c10522 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023-2024 Nordix Foundation + * Copyright (C) 2023-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. @@ -27,7 +27,9 @@ import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsDataspaceService import org.onap.cps.api.CpsModuleService +import org.onap.cps.api.exceptions.AnchorNotFoundException import org.onap.cps.api.model.Dataspace +import org.onap.cps.api.model.ModuleDefinition import org.slf4j.LoggerFactory import org.springframework.boot.context.event.ApplicationStartedEvent import org.springframework.context.ApplicationEventPublisher @@ -48,12 +50,15 @@ class InventoryModelLoaderSpec extends Specification { def applicationContext = new AnnotationConfigApplicationContext() - def expectedYangResourceToContentMap + def expectedPreviousYangResourceToContentMap + def expectedNewYangResourceToContentMap def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) def loggingListAppender void setup() { - expectedYangResourceToContentMap = objectUnderTest.mapYangResourcesToContent('dmi-registry@2024-02-23.yang') + expectedPreviousYangResourceToContentMap = objectUnderTest.mapYangResourcesToContent('dmi-registry@2024-02-23.yang') + expectedNewYangResourceToContentMap = objectUnderTest.mapYangResourcesToContent('dmi-registry@2025-07-22.yang') + objectUnderTest.newRevisionEnabled = true logger.setLevel(Level.DEBUG) loggingListAppender = new ListAppender() logger.addAppender(loggingListAppender) @@ -68,17 +73,50 @@ class InventoryModelLoaderSpec extends Specification { def 'Onboard subscription model via application ready event.'() { given: 'dataspace is ready for use' + objectUnderTest.newRevisionEnabled = false mockCpsAdminService.getDataspace(NCMP_DATASPACE_NAME) >> new Dataspace('') when: 'the application is started' objectUnderTest.onApplicationEvent(Mock(ApplicationStartedEvent)) then: 'the module service is used to create the new schema set from the correct resource' - 1 * mockCpsModuleService.createSchemaSet(NCMP_DATASPACE_NAME, 'dmi-registry-2024-02-23', expectedYangResourceToContentMap) - and: 'the admin service is used to update the anchor' - 1 * mockCpsAnchorService.updateAnchorSchemaSet(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'dmi-registry-2024-02-23') + 1 * mockCpsModuleService.createSchemaSet(NCMP_DATASPACE_NAME, 'dmi-registry-2024-02-23', expectedPreviousYangResourceToContentMap) and: 'No schema sets are being removed by the module service (yet)' 0 * mockCpsModuleService.deleteSchemaSet(NCMP_DATASPACE_NAME, _, _) and: 'application event publisher is called once' 1 * mockApplicationEventPublisher.publishEvent(_) } + def 'Install new model revision'() { + given: 'the anchor does not exist' + mockCpsAnchorService.getAnchor(_, _) >> { throw new AnchorNotFoundException('', '') } + when: 'the inventory model loader is triggered' + objectUnderTest.onboardOrUpgradeModel() + then: 'a new schema set for the 2025-07-22 revision is installed' + 1 * mockCpsModuleService.createSchemaSet(NCMP_DATASPACE_NAME, 'dmi-registry-2025-07-22', expectedNewYangResourceToContentMap) + } + + def 'Upgrade model revision'() { + given: 'the anchor exists and new module revision is not installed' + mockCpsAnchorService.getAnchor(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR) >> {} + mockCpsModuleService.getModuleDefinitionsByAnchorAndModule(_, _, _, _) >> Collections.emptyList() + when: 'the inventory model loader is triggered' + objectUnderTest.onboardOrUpgradeModel() + then: 'the new schema set for the 2025-07-22 revision is created' + 1 * mockCpsModuleService.createSchemaSet(NCMP_DATASPACE_NAME, 'dmi-registry-2025-07-22', expectedNewYangResourceToContentMap) + and: 'the anchor is updated to point to the new schema set' + 1 * mockCpsAnchorService.updateAnchorSchemaSet(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'dmi-registry-2025-07-22') + and: 'log messages confirm successful upgrade' + assert loggingListAppender.list.any { it.message.contains("Inventory upgraded successfully") } + } + + def 'Skip upgrade model revision when new revision already installed'() { + given: 'the anchor exists and the new model revision is already installed' + mockCpsAnchorService.getAnchor(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR) >> {} + mockCpsModuleService.getModuleDefinitionsByAnchorAndModule(_, _, _, _) >> [new ModuleDefinition('', '', '')] + when: 'the inventory model loader is triggered' + objectUnderTest.onboardOrUpgradeModel() + then: 'no new schema set is created' + 0 * mockCpsModuleService.createSchemaSet(_, _, _) + and: 'a log message confirms the revision is already installed' + assert loggingListAppender.list.any { it.message.contains("already installed") } + } } diff --git a/cps-service/src/main/java/org/onap/cps/init/AbstractModelLoader.java b/cps-service/src/main/java/org/onap/cps/init/AbstractModelLoader.java index df068c68a6..a87c36ef47 100644 --- a/cps-service/src/main/java/org/onap/cps/init/AbstractModelLoader.java +++ b/cps-service/src/main/java/org/onap/cps/init/AbstractModelLoader.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023-2025 Nordix Foundation + * Copyright (C) 2023-2025 OpenInfra Foundation Europe. All rights reserved. * Modifications Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -34,8 +35,10 @@ import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsDataspaceService; import org.onap.cps.api.CpsModuleService; import org.onap.cps.api.exceptions.AlreadyDefinedException; +import org.onap.cps.api.exceptions.AnchorNotFoundException; import org.onap.cps.api.exceptions.DuplicatedYangResourceException; import org.onap.cps.api.exceptions.ModelOnboardingException; +import org.onap.cps.api.model.ModuleDefinition; import org.onap.cps.api.parameters.CascadeDeleteAllowed; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.boot.SpringApplication; @@ -47,7 +50,7 @@ public abstract class AbstractModelLoader implements ModelLoader { protected final CpsDataspaceService cpsDataspaceService; private final CpsModuleService cpsModuleService; - private final CpsAnchorService cpsAnchorService; + protected final CpsAnchorService cpsAnchorService; protected final CpsDataService cpsDataService; private final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()); @@ -117,6 +120,23 @@ public abstract class AbstractModelLoader implements ModelLoader { } } + /** + * Checks whether the specified anchor exists within the given dataspace. + * + * @param dataspaceName the name of the dataspace + * @param anchorName the name of the anchor within the dataspace + * @return {@code true} if the anchor exists, {@code false} otherwise + */ + public boolean doesAnchorExist(final String dataspaceName, final String anchorName) { + try { + cpsAnchorService.getAnchor(dataspaceName, anchorName); + return true; + } catch (final AnchorNotFoundException anchorNotFoundException) { + log.debug("Anchor '{}' not found in dataspace '{}'", anchorName, dataspaceName); + return false; + } + } + /** * Create initial top level data node. * @param dataspaceName dataspace name @@ -184,6 +204,17 @@ public abstract class AbstractModelLoader implements ModelLoader { } } + /** + * Checks if the specified revision of a module is installed. + */ + protected boolean isModuleRevisionInstalled(final String dataspaceName, final String anchorName, + final String moduleName, final String moduleRevision) { + final Collection moduleDefinitions = + cpsModuleService.getModuleDefinitionsByAnchorAndModule(dataspaceName, anchorName, moduleName, + moduleRevision); + return !moduleDefinitions.isEmpty(); + } + private void exitApplication(final ApplicationStartedEvent applicationStartedEvent) { SpringApplication.exit(applicationStartedEvent.getApplicationContext(), () -> EXIT_CODE_ON_ERROR); } diff --git a/cps-service/src/test/groovy/org/onap/cps/init/AbstractModelLoaderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/init/AbstractModelLoaderSpec.groovy index c3cb4f205b..8692e60752 100644 --- a/cps-service/src/test/groovy/org/onap/cps/init/AbstractModelLoaderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/init/AbstractModelLoaderSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023-2025 Nordix Foundation + * Copyright (C) 2023-2025 OpenInfra Foundation Europe. All rights reserved. * Modification Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,8 +28,10 @@ import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsDataspaceService import org.onap.cps.api.CpsModuleService +import org.onap.cps.api.exceptions.AnchorNotFoundException import org.onap.cps.api.exceptions.DuplicatedYangResourceException import org.onap.cps.api.exceptions.ModelOnboardingException +import org.onap.cps.api.model.ModuleDefinition import org.onap.cps.api.parameters.CascadeDeleteAllowed import org.onap.cps.api.exceptions.AlreadyDefinedException import org.slf4j.LoggerFactory @@ -242,6 +244,38 @@ class AbstractModelLoaderSpec extends Specification { assert thrown.details.contains('test message') } + def 'Checking if an anchor exists'() { + given: 'the anchor service returns an anchor without throwing an exception' + mockCpsAnchorService.getAnchor('my-dataspace', 'my-anchor') >> {} + when: 'checking if the anchor exists' + def result = objectUnderTest.doesAnchorExist('my-dataspace', 'my-anchor') + then: 'the expected boolean value is returned' + assert result == true + } + + def 'Checking if an anchor exists with unknown anchor'() { + given: 'the anchor service throws an exception' + def anchorNotFoundException = new AnchorNotFoundException('my-dataspace', 'missing-anchor') + mockCpsAnchorService.getAnchor('my-dataspace', 'missing-anchor') >> {throw anchorNotFoundException} + when: 'checking if the anchor exists' + def result = objectUnderTest.doesAnchorExist('my-dataspace', 'missing-anchor') + then: 'the expected boolean value is returned' + assert result == false + } + + def 'Checking if module revision is installed when: #scenario'() { + given: 'the module service returns module definitions' + mockCpsModuleService.getModuleDefinitionsByAnchorAndModule('some-dataspace', 'some-anchor', 'some-module', 'my-revision') >> moduleDefinitions + when: 'checking if a module revision is installed' + def result = objectUnderTest.isModuleRevisionInstalled('some-dataspace', 'some-anchor', 'some-module', 'my-revision') + then: 'the result matches expectation' + assert result == expectedResult + where: 'the following scenarios are used' + scenario || moduleDefinitions || expectedResult + 'Module revision exists' || [Mock(ModuleDefinition)] || true + 'Module revision does not exist' || [] || false + } + private void assertLogContains(String message) { def logs = loggingListAppender.list.toString() assert logs.contains(message) diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index e9fd9da60c..8b55d07e61 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -74,6 +74,7 @@ services: JAVA_TOOL_OPTIONS: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" ### DEBUG: Uncomment next line to enable java debugging ### JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 + NCMP_INVENTORY_MODEL_UPGRADE_R20250722_ENABLED: 'false' restart: on-failure:3 deploy: replicas: 0 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 7de0ad5fe8..d8c1e16848 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 @@ -131,7 +131,7 @@ abstract class CpsIntegrationSpecBase extends Specification { NetworkCmProxyFacade networkCmProxyFacade @Autowired - NetworkCmProxyInventoryFacadeImpl NetworkCmProxyInventoryFacade + NetworkCmProxyInventoryFacadeImpl networkCmProxyInventoryFacade @Autowired NetworkCmProxyQueryService networkCmProxyQueryService diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/inventory/ModuleUpgradeServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/inventory/ModuleUpgradeServiceIntegrationSpec.groovy new file mode 100644 index 0000000000..104166d754 --- /dev/null +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/inventory/ModuleUpgradeServiceIntegrationSpec.groovy @@ -0,0 +1,94 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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. + * 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.functional.ncmp.inventory + +import org.onap.cps.api.model.DataNode +import org.onap.cps.api.model.ModuleReference +import org.onap.cps.api.parameters.FetchDescendantsOption +import org.onap.cps.integration.base.FunctionalSpecBase +import org.onap.cps.ncmp.api.inventory.models.* +import org.onap.cps.ncmp.impl.NetworkCmProxyInventoryFacadeImpl +import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle +import org.onap.cps.ncmp.impl.utils.YangDataConverter + +class ModuleUpgradeServiceIntegrationSpec extends FunctionalSpecBase { + + NetworkCmProxyInventoryFacadeImpl objectUnderTest + def cmHandleId = 'ch-1' + + def setup() { + objectUnderTest = networkCmProxyInventoryFacade + } + + def 'CM Handle registry (inventory) model upgrade poc (incl. backward compatibility)'() { + given: 'DMI plugin provides initial modules for the CM handle' + dmiDispatcher1.moduleNamesPerCmHandleId[cmHandleId] = ['M1', 'M2'] + and: 'NCMP already has an existing module reference (old revision)' + def existingModule = new ModuleReference(moduleName: "dmi-registry", revision: "2024-02-23") + cpsModulePersistenceService.getYangResourceModuleReferences(_, _) >> [existingModule] + when: 'A CM-handle is registered' + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: DMI1_URL) + dmiPluginRegistration.setCreatedCmHandles([new NcmpServiceCmHandle(cmHandleId: cmHandleId, additionalProperties: ['addProp1': 'some-value'])]) + def dmiPluginRegistrationResponse = objectUnderTest.updateDmiRegistration(dmiPluginRegistration) + then: 'The CM-handle registration succeeds' + assert dmiPluginRegistrationResponse.createdCmHandles == [CmHandleRegistrationResponse.createSuccessResponse(cmHandleId)] + and: 'The CM-handle is initialized with ADVISED state' + assert CmHandleState.ADVISED == objectUnderTest.getCmHandleCompositeState(cmHandleId).cmHandleState + then: 'The module sync watchdog is invoked for advised CM-handles' + moduleSyncWatchdog.moduleSyncAdvisedCmHandles() + then: 'After module sync, the CM-handle transitions to READY state' + assert CmHandleState.READY == objectUnderTest.getCmHandleCompositeState(cmHandleId).cmHandleState + when: 'A new version of the dmi-registry module (upgrade) is available' + def newYangContent = readResourceDataFile('inventory/dmi-registry@2025-07-22.yang') + def newYangResourceContentPerName = ["dmi-registry@2025-07-22.yang": newYangContent] + then: 'The schema set is upgraded with the new module revision' + cpsModulePersistenceService.createSchemaSet('NCMP-Admin', 'dmi-registry-2025-07-22', newYangResourceContentPerName) + cpsAnchorService.updateAnchorSchemaSet('NCMP-Admin','ncmp-dmi-registry','dmi-registry-2025-07-22') + when: 'that state gets updated to a different value' + final Collection cmHandleDataNodes = inventoryPersistence.getCmHandleDataNodeByCmHandleId('ch-1', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + YangModelCmHandle yangModelCmHandle= YangDataConverter.toYangModelCmHandle(cmHandleDataNodes[0]) + CompositeState compositeState= yangModelCmHandle.getCompositeState() + compositeState.setCmHandleState(CmHandleState.LOCKED) + then: 'the CM handle gets saved' + inventoryPersistence.saveCmHandleState(cmHandleId, compositeState) + and: 'we load the CM handle again' + final Collection updatedCmHandleDataNodes = inventoryPersistence.getCmHandleDataNodeByCmHandleId(cmHandleId, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + YangModelCmHandle updatedYangModelCmHandle= YangDataConverter.toYangModelCmHandle(updatedCmHandleDataNodes[0]) + and: 'the state has the new value i.e. load and save worked successfully' + assert updatedYangModelCmHandle.getCompositeState().cmHandleState == CmHandleState.LOCKED + when: 'The CM-handle additional properties are updated' + def dmiPluginRegistrationToUpdate = new DmiPluginRegistration(dmiPlugin: DMI1_URL) + dmiPluginRegistrationToUpdate.setUpdatedCmHandles([new NcmpServiceCmHandle(cmHandleId: cmHandleId, additionalProperties: ['addProp1': 'value1','addProp2': 'value2'])]) + def updatedDmiPluginRegistrationResponse = objectUnderTest.updateDmiRegistration(dmiPluginRegistrationToUpdate) + then: 'The update response confirms SUCCESS for the CM-handle' + assert updatedDmiPluginRegistrationResponse.updatedCmHandles.size() == 1 + def updatedHandleResponse = updatedDmiPluginRegistrationResponse.updatedCmHandles[0] + assert updatedHandleResponse.cmHandle == cmHandleId + assert updatedHandleResponse.status == CmHandleRegistrationResponse.Status.SUCCESS + and: 'Reloaded CM-handle contains the new additional properties (backward compatibility preserved)' + def updatedCmHandleDataNodesAfterUpdate = inventoryPersistence.getCmHandleDataNodeByCmHandleId(cmHandleId, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + def reloadedCmHandleAfterUpdate= YangDataConverter.toYangModelCmHandle(updatedCmHandleDataNodesAfterUpdate[0]) + assert reloadedCmHandleAfterUpdate.additionalProperties.collectEntries { [it.name, it.value] } == [addProp1: "value1", addProp2: "value2"] + cleanup: 'deregister CM handle' + deregisterCmHandle(DMI1_URL, cmHandleId) + } + +} diff --git a/integration-test/src/test/resources/data/inventory/dmi-registry@2025-07-22.yang b/integration-test/src/test/resources/data/inventory/dmi-registry@2025-07-22.yang new file mode 100644 index 0000000000..4405c144d8 --- /dev/null +++ b/integration-test/src/test/resources/data/inventory/dmi-registry@2025-07-22.yang @@ -0,0 +1,162 @@ +module dmi-registry { + + yang-version 1.1; + + namespace "org:onap:cps:ncmp"; + + prefix dmi-reg; + + contact "toine.siebelink@est.tech"; + + revision "2025-07-22" { + description + "Added dmi-registry.cm-handles.dmi-properties (string) to replace (now deprecated) dmi-registry.cm-handles.additional-properties (list). + Added dmi-registry.cm-handles.cm-handle-state to replace (now deprecated) and dmi-registry.cm-handles.state.cm-handle-state"; + } + + revision "2024-02-23" { + description + "Added data-producer-identifier"; + } + + revision "2023-11-27" { + description + "Added alternate-id"; + } + + revision "2023-08-23" { + description + "Added module-set-tag"; + } + + revision "2022-05-10" { + description + "Added data-sync-enabled, sync-state with state, last-sync-time, data-store-sync-state with operational and running syncstate"; + } + + revision "2022-02-10" { + description + "Added state, lock-reason, lock-reason-details to aid with cmHandle sync and timestamp to aid with retry/timeout scenarios"; + } + + revision "2021-12-13" { + description + "Added new list of public-properties and additional-properties for a Cm-Handle which are exposed to clients of the NCMP interface"; + } + + revision "2021-10-20" { + description + "Added dmi-data-service-name & dmi-model-service-name to allow separate DMI instances for each responsibility"; + } + + revision "2021-05-20" { + description + "Initial Version"; + } + + grouping LockReason { + leaf reason { + type string; + } + leaf details { + type string; + } + } + + grouping SyncState { + leaf sync-state { + type string; + } + leaf last-sync-time { + type string; + } + } + + grouping Datastores { + container operational { + uses SyncState; + } + container running { + uses SyncState; + } + } + + container dmi-registry { + list cm-handles { + key "id"; + leaf id { + type string; + } + leaf dmi-service-name { + type string; + } + leaf dmi-data-service-name { + type string; + } + leaf dmi-model-service-name { + type string; + } + leaf module-set-tag { + type string; + } + leaf alternate-id { + type string; + } + leaf data-producer-identifier { + type string; + } + leaf dmi-properties { + type string; + } + leaf cm-handle-state { + type string; + } + + list additional-properties { + key "name"; + leaf name { + type string; + } + leaf value { + type string; + } + status deprecated; // Replaced by dmi-properties + } + + list public-properties { + key "name"; + leaf name { + type string; + } + leaf value { + type string; + } + } + + container state { + leaf cm-handle-state { + type string; + status deprecated; + } + + container lock-reason { + uses LockReason; + } + + leaf last-update-time { + type string; + } + + leaf data-sync-enabled { + type boolean; + default "false"; + } + + container datastores { + uses Datastores; + } + } + } + } +} + -- 2.16.6