/*
* ============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.
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;
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,
@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() {
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);
+ }
}
--- /dev/null
+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;
+ }
+ }
+ }
+ }
+}
+
/*
* ============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.
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
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)
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") }
+ }
}
/*
* ============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");
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;
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;
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());
}
}
+ /**
+ * 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
}
}
+ /**
+ * 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<ModuleDefinition> moduleDefinitions =
+ cpsModuleService.getModuleDefinitionsByAnchorAndModule(dataspaceName, anchorName, moduleName,
+ moduleRevision);
+ return !moduleDefinitions.isEmpty();
+ }
+
private void exitApplication(final ApplicationStartedEvent applicationStartedEvent) {
SpringApplication.exit(applicationStartedEvent.getApplicationContext(), () -> EXIT_CODE_ON_ERROR);
}
/*
* ============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");
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
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)
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
NetworkCmProxyFacade networkCmProxyFacade
@Autowired
- NetworkCmProxyInventoryFacadeImpl NetworkCmProxyInventoryFacade
+ NetworkCmProxyInventoryFacadeImpl networkCmProxyInventoryFacade
@Autowired
NetworkCmProxyQueryService networkCmProxyQueryService
--- /dev/null
+/*
+ * ============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<DataNode> 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<DataNode> 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)
+ }
+
+}
--- /dev/null
+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;
+ }
+ }
+ }
+ }
+}
+