import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLE_INVALID_ID;
import static org.onap.cps.ncmp.impl.inventory.CmHandleRegistrationServicePropertyHandler.PropertyType.ADDITIONAL_PROPERTY;
import static org.onap.cps.ncmp.impl.inventory.CmHandleRegistrationServicePropertyHandler.PropertyType.PUBLIC_PROPERTY;
-import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DATASPACE_NAME;
-import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR;
-import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT;
import com.google.common.collect.ImmutableMap;
import com.hazelcast.map.IMap;
-import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import org.onap.cps.ncmp.impl.inventory.sync.lcm.CmHandleTransitionPair;
import org.onap.cps.ncmp.impl.inventory.sync.lcm.LcmEventsHelper;
import org.onap.cps.ncmp.impl.utils.YangDataConverter;
-import org.onap.cps.utils.ContentType;
import org.onap.cps.utils.JsonObjectMapper;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
final String cmHandleId = ncmpServiceCmHandle.getCmHandleId();
final String newAlternateId = ncmpServiceCmHandle.getAlternateId();
if (StringUtils.isNotBlank(newAlternateId)) {
- setAndUpdateCmHandleField(ncmpServiceCmHandle.getCmHandleId(), "alternate-id", newAlternateId);
+ inventoryPersistence.updateCmHandleField(
+ ncmpServiceCmHandle.getCmHandleId(),
+ "alternate-id",
+ newAlternateId);
cmHandleIdPerAlternateId.delete(cmHandleId);
cmHandleIdPerAlternateId.set(newAlternateId, cmHandleId);
}
targetDataProducerIdentifier);
return;
}
- setAndUpdateCmHandleField(cmHandleId, "data-producer-identifier", targetDataProducerIdentifier);
+
+ inventoryPersistence.updateCmHandleField(
+ cmHandleId,
+ "data-producer-identifier",
+ targetDataProducerIdentifier);
log.debug("dataProducerIdentifier for cmHandle {} updated from {} to {}", cmHandleId,
currentDataProducerIdentifier, targetDataProducerIdentifier);
sendLcmEventForDataProducerIdentifier(cmHandleId, currentYangModelCmHandle);
return new DataNodeBuilder().withXpath(xpath).withLeaves(ImmutableMap.copyOf(updatedLeaves)).build();
}
- private void setAndUpdateCmHandleField(final String cmHandleIdToUpdate, final String fieldName,
- final String newFieldValue) {
- final Map<String, Map<String, String>> dmiRegistryData = new HashMap<>(1);
- final Map<String, String> cmHandleData = new HashMap<>(2);
- cmHandleData.put("id", cmHandleIdToUpdate);
- cmHandleData.put(fieldName, newFieldValue);
- dmiRegistryData.put("cm-handles", cmHandleData);
- cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
- jsonObjectMapper.asJsonString(dmiRegistryData), OffsetDateTime.now(), ContentType.JSON);
- log.debug("Updating {} for cmHandle {} with value : {})", fieldName, cmHandleIdToUpdate, newFieldValue);
- }
-
enum PropertyType {
ADDITIONAL_PROPERTY("additional-properties"), PUBLIC_PROPERTY("public-properties");
import org.onap.cps.api.parameters.FetchDescendantsOption;
import org.onap.cps.ncmp.api.inventory.models.CompositeState;
import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
+import org.onap.cps.ncmp.impl.models.CmHandleStateUpdate;
public interface InventoryPersistence extends NcmpPersistence {
* @return boolean
*/
boolean isExistingCmHandleId(String cmHandleId);
+
+ /**
+ * Updates the specified field of a CM handle with a new value in the DMI registry.
+ *
+ * @param cmHandleIdToUpdate the unique identifier of the CM handle to be updated
+ * @param fieldName the name of the field within the CM handle to be updated
+ * @param newFieldValue the new value to be set for the
+ * specified field of the CM handle
+ */
+ void updateCmHandleField(String cmHandleIdToUpdate, String fieldName, String newFieldValue);
+
+
+ /**
+ * Bulk updates the specified fields of a batch of CM handles with a new value in the DMI registry.
+ *
+ * @param fieldName the name of the field within the CM handle to be updated
+ * @param cmHandleIdToValue the CM handle to be updated and new value
+ */
+ void updateCmHandleFields(final String fieldName, final Map<String, String> cmHandleIdToValue);
+
+ /**
+ * Method to update a batch of cm handles status to the value in CompositeState.
+ *
+ * @param cmHandleStateUpdates the cmHandleId and state change being performed on it
+ */
+ void bulkUpdateCmHandleStates(List<CmHandleStateUpdate> cmHandleStateUpdates);
+
+
}
import org.onap.cps.ncmp.api.inventory.models.CompositeState;
import org.onap.cps.ncmp.api.inventory.models.CompositeStateBuilder;
import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
+import org.onap.cps.ncmp.impl.models.CmHandleStateUpdate;
import org.onap.cps.ncmp.impl.utils.YangDataConverter;
import org.onap.cps.utils.ContentType;
import org.onap.cps.utils.CpsValidator;
}
}
+ /**
+ * Updates the specified field of a CM handle with a new value in the DMI registry.
+ *
+ * @param cmHandleId the unique identifier of the CM handle to be updated
+ * @param fieldName the name of the field within the CM handle to be updated
+ * @param fieldValue the new value to be set for
+ * the specified field of the CM handle
+ */
+ @Override
+ public void updateCmHandleField(final String cmHandleId, final String fieldName,
+ final String fieldValue) {
+ updateCmHandleFields(fieldName, Collections.singletonMap(cmHandleId, fieldValue));
+ }
+
+ @Override
+ public void updateCmHandleFields(final String fieldName, final Map<String, String> newValuePerCmHandleId) {
+ if (!newValuePerCmHandleId.isEmpty()) {
+ final Map<String, Object> targetCmHandleStatePerCmHandleId = new HashMap<>();
+ final List<Map<String, String>> targetCmHandleStatesPerCmHandleIds = new ArrayList<>();
+
+ for (final Map.Entry<String, String> entry : newValuePerCmHandleId.entrySet()) {
+ final Map<String, String> cmHandleData = new HashMap<>();
+ cmHandleData.put("id", entry.getKey());
+ cmHandleData.put(fieldName, entry.getValue());
+ targetCmHandleStatesPerCmHandleIds.add(cmHandleData);
+ log.debug("Updating {} for cmHandle {} to {}", fieldName, entry.getKey(), entry.getValue());
+ }
+ targetCmHandleStatePerCmHandleId.put("cm-handles", targetCmHandleStatesPerCmHandleIds);
+ cpsDataService.updateNodeLeaves(
+ NCMP_DATASPACE_NAME,
+ NCMP_DMI_REGISTRY_ANCHOR,
+ NCMP_DMI_REGISTRY_PARENT,
+ jsonObjectMapper.asJsonString(targetCmHandleStatePerCmHandleId),
+ OffsetDateTime.now(),
+ ContentType.JSON);
+ }
+ }
+
+ @Override
+ public void bulkUpdateCmHandleStates(final List<CmHandleStateUpdate> cmHandleStateUpdates) {
+ final Map<String, String> mappedCmHandleStateUpdates = cmHandleStateUpdates.stream()
+ .collect(Collectors.toMap(
+ CmHandleStateUpdate::cmHandleId,
+ CmHandleStateUpdate::state
+ ));
+ updateCmHandleFields("cm-handle-state", mappedCmHandleStateUpdates);
+ }
+
private static String getXPathForCmHandleById(final String cmHandleId) {
return NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@id='" + cmHandleId + "']";
}
--- /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.ncmp.impl.models;
+
+public record CmHandleStateUpdate (String cmHandleId, String state) {}
--- /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.ncmp.init;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.ncmp.api.inventory.NetworkCmProxyInventoryFacade;
+import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle;
+import org.onap.cps.ncmp.impl.inventory.CmHandleQueryService;
+import org.onap.cps.ncmp.impl.inventory.InventoryPersistence;
+import org.onap.cps.ncmp.impl.models.CmHandleStateUpdate;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DataMigration {
+
+ public final InventoryPersistence inventoryPersistence;
+ protected int batchSize = 300;
+ private final CmHandleQueryService cmHandleQueryService;
+ private final NetworkCmProxyInventoryFacade networkCmProxyInventoryFacade;
+
+
+ /**
+ * Migration of CompositeState CmHandleState into a new top level attribute.
+ * One off migration job.
+ */
+ public void migrateInventoryToModelRelease20250722() {
+ log.info("Inventory data migration started");
+ final List<String> cmHandleIds = new ArrayList<>(cmHandleQueryService.getAllCmHandleReferences(false));
+ log.info("Number of cm handles to process {}", cmHandleIds.size());
+ final int totalCmHandleIds = cmHandleIds.size();
+ for (int batchStart = 0; batchStart < totalCmHandleIds; batchStart += batchSize) {
+ final int batchEnd = Math.min(batchStart + batchSize, cmHandleIds.size());
+ final List<String> batchIds = cmHandleIds.subList(batchStart, batchEnd);
+ migrateBatch(batchIds);
+ }
+ log.info("Inventory Cm Handle data migration completed.");
+ }
+
+ private void migrateBatch(final List<String> cmHandleIds) {
+ log.debug("Processing batch of {} Cm Handles", cmHandleIds.size());
+ final List<CmHandleStateUpdate> cmHandleStateUpdates = new ArrayList<>();
+ for (final String cmHandleId : cmHandleIds) {
+ try {
+ final NcmpServiceCmHandle ncmpServiceCmHandle =
+ networkCmProxyInventoryFacade.getNcmpServiceCmHandle(cmHandleId);
+ final String valueFromOldModel = ncmpServiceCmHandle.getCompositeState().getCmHandleState().name();
+ cmHandleStateUpdates.add(new CmHandleStateUpdate(
+ ncmpServiceCmHandle.getCmHandleId(),
+ valueFromOldModel
+ ));
+ } catch (final Exception e) {
+ log.error("Failed to process CM handle {} state", cmHandleId, e);
+ }
+ }
+ try {
+ inventoryPersistence.bulkUpdateCmHandleStates(cmHandleStateUpdates);
+ log.debug("Successfully updated Cm Handles");
+ } catch (final Exception e) {
+ log.error("Failed to perform bulk update for batch", e);
+ }
+ }
+}
+
import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME;
import lombok.extern.slf4j.Slf4j;
-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.impl.CpsServicesBundle;
import org.onap.cps.init.AbstractModelLoader;
import org.onap.cps.init.ModelLoaderLock;
import org.onap.cps.init.actuator.ReadinessManager;
@Order(2)
public class InventoryModelLoader extends AbstractModelLoader {
+ private final DataMigration dataMigration;
private final ApplicationEventPublisher applicationEventPublisher;
private static final String PREVIOUS_SCHEMA_SET_NAME = "dmi-registry-2024-02-23";
* the NCMP inventory model schema sets and managing readiness state during migration.
*/
public InventoryModelLoader(final ModelLoaderLock modelLoaderLock,
- final CpsDataspaceService cpsDataspaceService,
- final CpsModuleService cpsModuleService,
- final CpsAnchorService cpsAnchorService,
- final CpsDataService cpsDataService,
+ final CpsServicesBundle cpsServicesBundle,
final ApplicationEventPublisher applicationEventPublisher,
- final ReadinessManager readinessManager) {
- super(modelLoaderLock, cpsDataspaceService, cpsModuleService, cpsAnchorService, cpsDataService,
+ final ReadinessManager readinessManager,
+ final DataMigration dataMigration) {
+ super(modelLoaderLock,
+ cpsServicesBundle.getDataspaceService(),
+ cpsServicesBundle.getModuleService(),
+ cpsServicesBundle.getAnchorService(),
+ cpsServicesBundle.getDataService(),
readinessManager);
this.applicationEventPublisher = applicationEventPublisher;
+ this.dataMigration = dataMigration;
}
@Override
log.info("Model Loader #2: Inventory upgraded successfully to model {}", NEW_INVENTORY_SCHEMA_SET_NAME);
}
- private void performInventoryDataMigration() {
-
- //1. Load all the cm handles (in batch)
- //2. Copy the state and known properties
- log.info("Model Loader #2: Inventory module data migration is completed successfully.");
- }
-
private static String toYangFileName(final String schemaSetName) {
return INVENTORY_YANG_MODULE_NAME + "@" + getModuleRevision(schemaSetName) + ".yang";
}
private void upgradeAndMigrateInventoryModel() {
upgradeInventoryModel();
- performInventoryDataMigration();
+ dataMigration.migrateInventoryToModelRelease20250722();
}
}
given: 'cm handles request'
def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, alternateId: 'alt-1')]
and: 'the cm handle per alternate id cache returns a value'
- mockCmHandleIdPerAlternateId.get(_) >> 'someId'
+ mockCmHandleIdPerAlternateId.get(_) >> cmHandleId
and: 'a data node found'
def dataNode = new DataNode(xpath: cmHandleXpath, leaves: ['id': cmHandleId, 'alternate-id': 'alt-1'])
mockInventoryPersistence.getCmHandleDataNodeByCmHandleId(cmHandleId, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
when: 'cm handle properties is updated'
def response = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
- then: 'the update is delegated to cps data service with correct parameters'
- 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', _, _, ContentType.JSON) >>
- { args ->
- assert args[3].contains('alt-1')
- }
+ then: 'the update is delegated to inventory persistence with correct parameters'
+ 1 * mockInventoryPersistence.updateCmHandleField(cmHandleId, 'alternate-id', 'alt-1')
and: 'one successful registration response'
response.size() == 1
and: 'the response shows success for the given cm handle id'
}
def 'Update CM Handle data producer identifier from #scenario'() {
- given: 'an existing cm handle with old data producer identifier'
+ given: 'an existing cm handle with old data producer identifier'
DataNode existingCmHandleDataNode = new DataNode(xpath: cmHandleXpath, leaves: ['id': 'cmHandleId', 'data-producer-identifier': oldDataProducerIdentifier])
- and: 'an update request with a new data producer identifier'
+ and: 'an update request with a new data producer identifier'
def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'cmHandleId', dataProducerIdentifier: 'New Data Producer Identifier')
- and: 'the inventory persistence returns updated yang model'
+ and: 'the inventory persistence returns updated yang model'
1 * mockInventoryPersistence.getYangModelCmHandle('cmHandleId') >> createYangModelCmHandle('cmHandleId', 'New Data Producer Identifier')
- when: 'data producer identifier is updated'
+ when: 'data producer identifier is updated'
objectUnderTest.updateDataProducerIdentifier(existingCmHandleDataNode, ncmpServiceCmHandle)
- then: 'the update node leaves method is invoked once with correct parameters'
- 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', _, _, ContentType.JSON) >> { args ->
- assert args[3].contains('New Data Producer Identifier')
- }
- and: 'LCM event is sent'
+ then: 'the update node leaves method is invoked once with correct parameters'
+ 1 * mockInventoryPersistence.updateCmHandleField('cmHandleId', 'data-producer-identifier', 'New Data Producer Identifier')
+ and: 'LCM event is sent'
1 * mockLcmEventsHelper.sendLcmEventBatchAsynchronously({ cmHandleTransitionPairs ->
assert cmHandleTransitionPairs[0].targetYangModelCmHandle.dataProducerIdentifier == 'New Data Producer Identifier'
})
when: 'update data producer identifier is called'
objectUnderTest.updateDataProducerIdentifier(existingCmHandleDataNode, ncmpServiceCmHandle)
then: 'the update node leaves method is invoked once with correct parameters'
- 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', _, _, ContentType.JSON) >> { args ->
- assert args[3].contains('newDataProducerIdentifier')
- }
+ 1 * mockInventoryPersistence.updateCmHandleField('cmHandleId', 'data-producer-identifier', 'newDataProducerIdentifier')
and: 'LCM event is sent'
1 * mockLcmEventsHelper.sendLcmEventBatchAsynchronously( { cmHandleTransitionPairs ->
assert cmHandleTransitionPairs[0].targetYangModelCmHandle.dataProducerIdentifier == 'newDataProducerIdentifier'
assert cmHandleTransitionPairs[0].currentYangModelCmHandle.dataProducerIdentifier == 'oldDataProducerIdentifier'
})
- and: 'correct information is logged'
- def loggingEvent = logger.list[1]
- assert loggingEvent.level == Level.DEBUG
- assert loggingEvent.formattedMessage.contains('updated from oldDataProducerIdentifier to newDataProducerIdentifier')
}
def 'Update CM Handle data producer identifier with null or blank target identifier'() {
import org.onap.cps.ncmp.api.inventory.models.CmHandleState
import org.onap.cps.ncmp.api.inventory.models.CompositeState
import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
+import org.onap.cps.ncmp.impl.models.CmHandleStateUpdate
import org.onap.cps.utils.ContentType
import org.onap.cps.utils.CpsValidator
import org.onap.cps.utils.JsonObjectMapper
def mockCmHandleIdPerAlternateId = Mock(IMap)
- def objectUnderTest = new InventoryPersistenceImpl(mockCpsValidator, spiedJsonObjectMapper, mockCpsAnchorService, mockCpsModuleService, mockCpsDataService, mockCmHandleIdPerAlternateId)
+ def objectUnderTest = Spy(new InventoryPersistenceImpl(mockCpsValidator, spiedJsonObjectMapper, mockCpsAnchorService, mockCpsModuleService, mockCpsDataService, mockCmHandleIdPerAlternateId))
def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
.format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
- def cmHandleId = 'some-cm-handle'
+ def cmHandleId = 'ch-1'
+ def updates = [
+ new CmHandleStateUpdate("ch-1", "READY"),
+ new CmHandleStateUpdate("ch-2", "DELETING")
+ ]
def alternateId = 'some-alternate-id'
def leaves = ["id":cmHandleId, "alternateId":alternateId,"dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
- def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
+ def xpath = "/dmi-registry/cm-handles[@id='ch-1']"
def cmHandleId2 = 'another-cm-handle'
def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='myPublicProperty']", leaves: ["name":"myPublicProperty","value":"myPublicValue"])]
@Shared
- def childDataNodesForCmHandleWithAdditionalProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='myAdditionalProperty']", leaves: ["name":"myAdditionalProperty", "value":"myAdditionalValue"])]
+ def childDataNodesForCmHandleWithAdditionalProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='ch-1']/additional-properties[@name='myAdditionalProperty']", leaves: ["name":"myAdditionalProperty", "value":"myAdditionalValue"])]
@Shared
- def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='myPublicProperty']", leaves: ["name":"myPublicProperty","value":"myPublicValue"])]
+ def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='ch-1']/public-properties[@name='myPublicProperty']", leaves: ["name":"myPublicProperty","value":"myPublicValue"])]
@Shared
- def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
+ def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='ch-1']/state", leaves: ['cm-handle-state': 'ADVISED'])]
def 'Retrieve CmHandle using datanode with #scenario.'() {
given: 'the cps data service returns a data node from the DMI registry'
def 'Get a Cm Handle Composite State.'() {
given: 'a valid cm handle id'
- def cmHandleId = 'Some-Cm-Handle'
+ def cmHandleId = 'ch-1'
def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
and: 'cps data service returns a valid data node'
mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
- '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', INCLUDE_ALL_DESCENDANTS) >> [dataNode]
+ '/dmi-registry/cm-handles[@id=\'ch-1\']/state', INCLUDE_ALL_DESCENDANTS) >> [dataNode]
when: 'get cm handle state is invoked'
def result = objectUnderTest.getCmHandleState(cmHandleId)
then: 'result has returned the correct cm handle state'
def 'Update Cm Handle with #scenario State.'() {
given: 'a cm handle and a composite state'
- def cmHandleId = 'Some-Cm-Handle'
+ def cmHandleId = 'ch-1'
def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
when: 'update cm handle state is invoked with the #scenario state'
objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
then: 'update node leaves is invoked with the correct params'
- 1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime, ContentType.JSON)
+ 1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'ch-1\']', expectedJsonData, _ as OffsetDateTime, ContentType.JSON)
where: 'the following states are used'
scenario | cmHandleState || expectedJsonData
'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
and: 'alternate id cache contains the given cm handle reference'
mockCmHandleIdPerAlternateId.containsKey(_) >> true
when: 'update cm handle state is invoked with the #scenario state'
- def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
+ def cmHandleStateMap = ['ch-11' : compositeState1, 'ch-12' : compositeState2]
objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
then: 'update node leaves is invoked with the correct params'
1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime, ContentType.JSON)
where: 'the following states are used'
scenario | cmHandleState || cmHandlesJsonDataMap
- 'READY' | CmHandleState.READY || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
- 'LOCKED' | CmHandleState.LOCKED || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
- 'DELETING' | CmHandleState.DELETING || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
+ 'READY' | CmHandleState.READY || ['/dmi-registry/cm-handles[@id=\'ch-11\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'ch-12\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
+ 'LOCKED' | CmHandleState.LOCKED || ['/dmi-registry/cm-handles[@id=\'ch-11\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'ch-12\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
+ 'DELETING' | CmHandleState.DELETING || ['/dmi-registry/cm-handles[@id=\'ch-11\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'ch-12\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
}
def 'Update cm handle states when #scenario in alternate id cache.'() {
given: 'a map of cm handles composite states'
def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, lastUpdateTime: formattedDateAndTime)
- def cmHandleStateMap = ['some-cm-handle' : compositeState]
+ def cmHandleStateMap = ['ch-1' : compositeState]
and: 'alternate id cache returns #scenario'
mockCmHandleIdPerAlternateId.containsKey(_) >> keyExists
mockCmHandleIdPerAlternateId.containsValue(_) >> valueExists
assert result.size() == 2
assert result.id.containsAll([cmHandleId, cmHandleId2])
}
+
+ def 'Update Cm Handle Field.'(){
+ when: 'update is called.'
+ objectUnderTest.updateCmHandleField('ch-1', 'my field', 'my new value')
+ then: 'call is delegated to updateCmHandleFields'
+ 1 * objectUnderTest.updateCmHandleFields('my field', ['ch-1':'my new value'])
+ }
+
+ def 'Bulk update cm handle state.'(){
+ when: 'bulk update is called'
+ objectUnderTest.bulkUpdateCmHandleStates(updates)
+ then: 'call is made to update the fileds of the cm handle'
+ 1 * objectUnderTest.updateCmHandleFields('cm-handle-state', ['ch-1':'READY','ch-2':'DELETING'])
+ }
+
}
--- /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.ncmp.init
+
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.read.ListAppender
+import org.onap.cps.ncmp.api.inventory.NetworkCmProxyInventoryFacade
+import org.onap.cps.ncmp.api.inventory.models.CompositeState
+import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle
+import org.onap.cps.ncmp.impl.inventory.CmHandleQueryService
+import org.onap.cps.ncmp.impl.inventory.InventoryPersistence
+import org.slf4j.LoggerFactory
+import spock.lang.Specification
+import spock.lang.Subject
+
+import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.ADVISED
+import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.READY
+
+class DataMigrationSpec extends Specification{
+
+ def mockCmHandleQueryService = Mock(CmHandleQueryService)
+ def mockNetworkCmProxyInventoryFacade = Mock(NetworkCmProxyInventoryFacade)
+ def mockInventoryPersistence = Mock(InventoryPersistence)
+ def cmHandle1 = new NcmpServiceCmHandle(cmHandleId: 'ch-1', dmiServiceName: 'dmi1', compositeState: new CompositeState(cmHandleState: READY))
+ def cmHandle2 = new NcmpServiceCmHandle(cmHandleId: 'ch-2', dmiServiceName: 'dmi1', compositeState: new CompositeState(cmHandleState: ADVISED))
+ def cmHandle3 = new NcmpServiceCmHandle(cmHandleId: 'ch-3', dmiServiceName: 'dmi2', compositeState: new CompositeState(cmHandleState: READY))
+
+ def logger = Spy(ListAppender<ILoggingEvent>)
+
+ def setup() {
+ mockNetworkCmProxyInventoryFacade.getNcmpServiceCmHandle('ch-1') >> cmHandle1
+ mockNetworkCmProxyInventoryFacade.getNcmpServiceCmHandle('ch-2') >> cmHandle2
+ mockNetworkCmProxyInventoryFacade.getNcmpServiceCmHandle('ch-3') >> cmHandle3
+ setupLogger(Level.ERROR)
+ }
+
+ def cleanup() {
+ ((Logger) LoggerFactory.getLogger(DataMigration.class)).detachAndStopAllAppenders()
+ }
+
+
+ @Subject
+ def objectUnderTest = Spy(new DataMigration(mockInventoryPersistence, mockCmHandleQueryService, mockNetworkCmProxyInventoryFacade))
+
+ def 'CM Handle migration.'() {
+ given: 'a list of CM handle IDs'
+ def cmHandleIds = ['ch-1', 'ch-2', 'ch-3']
+ mockCmHandleQueryService.getAllCmHandleReferences(false) >> cmHandleIds
+ when: 'migration is performed'
+ objectUnderTest.migrateInventoryToModelRelease20250722()
+ then: 'handles are processed in bulk'
+ 1 * mockInventoryPersistence.bulkUpdateCmHandleStates({ cmHandleStateUpdates ->
+ def actualData = cmHandleStateUpdates.collect { [id: it.cmHandleId, state: it.state] }
+ assert actualData.size() == 3
+ assert actualData.containsAll([
+ [id: 'ch-1', state: 'READY'],
+ [id: 'ch-2', state: 'ADVISED'],
+ [id: 'ch-3', state: 'READY']
+ ])
+ })
+ }
+
+ def 'CM Handle migration with exception for a cm handle in batch.'() {
+ given: 'a faulty CM handle ID'
+ def cmHandleIds = ['faultyCmHandle']
+ mockCmHandleQueryService.getAllCmHandleReferences(false) >> cmHandleIds
+ and: 'an exception is thrown when getting cm handle'
+ mockNetworkCmProxyInventoryFacade.getNcmpServiceCmHandle('faultyCmHandle') >> { throw new RuntimeException('Simulated failure') }
+ when: 'migration is performed'
+ objectUnderTest.migrateInventoryToModelRelease20250722()
+ then: 'migration processes no batches'
+ 1 * mockInventoryPersistence.bulkUpdateCmHandleStates([])
+ }
+
+ def 'Migrate batch with error.'() {
+ given: 'a cm handle'
+ def cmHandleIds = ['ch-1']
+ mockCmHandleQueryService.getAllCmHandleReferences(false) >> cmHandleIds
+ and: 'an exception happens updating cm handle states'
+ mockInventoryPersistence.bulkUpdateCmHandleStates(*_) >> {
+ throw new RuntimeException('Simulated failure')
+ }
+ when: 'migration is performed'
+ objectUnderTest.migrateInventoryToModelRelease20250722()
+ then: 'exception is caught and logged'
+ def loggingEvent = logger.list[0]
+ assert loggingEvent.level == Level.ERROR
+ assert loggingEvent.formattedMessage.contains('Failed to perform bulk update for batch')
+ }
+
+ def setupLogger(level) {
+ def setupLogger = ((Logger) LoggerFactory.getLogger(DataMigration))
+ setupLogger.setLevel(level)
+ setupLogger.addAppender(logger)
+ logger.start()
+ }
+}
import org.onap.cps.api.model.ModuleDefinition
import org.onap.cps.init.ModelLoaderLock
import org.onap.cps.init.actuator.ReadinessManager
+import org.onap.cps.impl.CpsServicesBundle
import org.slf4j.LoggerFactory
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.ApplicationEventPublisher
def mockCpsModuleService = Mock(CpsModuleService)
def mockCpsDataService = Mock(CpsDataService)
def mockCpsAnchorService = Mock(CpsAnchorService)
+ def cpsServices = new CpsServicesBundle(
+ mockCpsAdminService,
+ mockCpsModuleService,
+ mockCpsAnchorService,
+ mockCpsDataService
+ )
+
def mockApplicationEventPublisher = Mock(ApplicationEventPublisher)
def mockReadinessManager = Mock(ReadinessManager)
- def objectUnderTest = new InventoryModelLoader(mockModelLoaderLock, mockCpsAdminService, mockCpsModuleService, mockCpsAnchorService, mockCpsDataService, mockApplicationEventPublisher, mockReadinessManager)
+ def mockDataMigration = Mock(DataMigration)
+ def objectUnderTest = new InventoryModelLoader(mockModelLoaderLock, cpsServices, mockApplicationEventPublisher, mockReadinessManager, mockDataMigration)
def applicationContext = new AnnotationConfigApplicationContext()
applicationContext.close()
}
+ def callPrivatePerformInventoryDataMigration() {
+ def method = objectUnderTest.class.getDeclaredMethod('upgradeAndMigrateInventoryModel')
+ method.accessible = true
+ method.invoke(objectUnderTest)
+ }
+
+
def 'Onboard subscription model via application ready event.'() {
given: 'dataspace is ready for use with default newRevisionEnabled flag'
objectUnderTest.newRevisionEnabled = false
assert loggingListAppender.list.any { it.message.contains("already installed") }
}
+ def "Perform inventory data migration to Release20250722"() {
+ when: 'the migration is performed'
+ callPrivatePerformInventoryDataMigration()
+ then: 'the call is delegated to the Data Migration service'
+ 1 * mockDataMigration.migrateInventoryToModelRelease20250722()
+ }
+
+
}
--- /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.config;
+
+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.impl.CpsServicesBundle;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class CpsServicesConfig {
+
+ @Bean
+ public CpsServicesBundle cpsServices(final CpsDataspaceService dataspaceService,
+ final CpsModuleService moduleService,
+ final CpsAnchorService anchorService,
+ final CpsDataService dataService) {
+ return new CpsServicesBundle(dataspaceService, moduleService, anchorService, dataService);
+ }
+
+}
--- /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.impl;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.api.CpsAnchorService;
+import org.onap.cps.api.CpsDataService;
+import org.onap.cps.api.CpsDataspaceService;
+import org.onap.cps.api.CpsModuleService;
+
+@Getter
+@RequiredArgsConstructor
+public class CpsServicesBundle {
+
+ private final CpsDataspaceService dataspaceService;
+ private final CpsModuleService moduleService;
+ private final CpsAnchorService anchorService;
+ private final CpsDataService dataService;
+
+}
\ No newline at end of file
--- /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.config
+
+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.impl.CpsServicesBundle
+import spock.lang.Specification
+
+class CpsServicesConfigSpec extends Specification {
+
+ def dataspaceService = Mock(CpsDataspaceService)
+ def moduleService = Mock(CpsModuleService)
+ def anchorService = Mock(CpsAnchorService)
+ def dataService = Mock(CpsDataService)
+
+ def 'cpsServices returns bundle wired with given services'() {
+ given: 'a cps service config'
+ def objectUnderTest = new CpsServicesConfig()
+ when: 'cpsServices bean method is invoked'
+ CpsServicesBundle bundle = objectUnderTest.cpsServices(
+ dataspaceService,
+ moduleService,
+ anchorService,
+ dataService
+ )
+ then: 'it is wired with the same instances that were passed in'
+ assert bundle.dataspaceService == dataspaceService
+ assert bundle.moduleService == moduleService
+ assert bundle.anchorService == anchorService
+ assert bundle.dataService == dataService
+ }
+}
\ No newline at end of file