<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>checkstyle</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<profiles>
<profile>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
# Copyright (C) 2021 Pantheon.tech
# Modifications Copyright (C) 2021-2022 Bell Canada
# Modifications Copyright (C) 2021-2024 Nordix Foundation
+# Modifications Copyright (C) 2024 TechMahindra Ltd
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
minimumIdle: 5
maximumPoolSize: 80
idleTimeout: 60000
- connectionTimeout: 120000
+ connectionTimeout: 30000
leakDetectionThreshold: 30000
pool-name: CpsDatabasePool
topic: ${DMI_CM_EVENTS_TOPIC:dmi-cm-events}
device-heartbeat:
topic: ${DMI_DEVICE_HEARTBEAT_TOPIC:dmi-device-heartbeat}
-
+ cps:
+ data-updated:
+ topic: ${CPS_CHANGE_EVENT_TOPIC:cps-data-updated-events}
notification:
enabled: true
- name: cps-ncmp-inventory
url: /api-docs/cps-ncmp/openapi-inventory.yaml
-
-
security:
# comma-separated uri patterns which do not require authorization
permit-uri: /actuator/**,/swagger-ui.html,/swagger-ui/**,/swagger-resources/**,/api-docs/**,/v3/api-docs/**
auth:
- username: ${CPS_USERNAME}
- password: ${CPS_PASSWORD}
+ username: ${CPS_USERNAME:cpsuser}
+ password: ${CPS_PASSWORD:cpsr0cks!}
# Actuator
management:
ncmp:
dmi:
httpclient:
- connectionTimeoutInSeconds: 180
+ connectionTimeoutInSeconds: 30
maximumConnectionsPerRoute: 50
maximumConnectionsTotal: 100
idleConnectionEvictionThresholdInSeconds: 5
auth:
- username: ${DMI_USERNAME}
- password: ${DMI_PASSWORD}
+ username: ${DMI_USERNAME:cpsuser}
+ password: ${DMI_PASSWORD:cpsr0cks!}
enabled: ${DMI_AUTH_ENABLED:true}
api:
base-path: dmi
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>cps-bom</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<packaging>pom</packaging>
<description>This artifact contains dependencyManagement declarations of all published CPS components.</description>
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>cps-dependencies</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<packaging>pom</packaging>
<name>${project.groupId}:${project.artifactId}</name>
<artifactId>hazelcast-spring</artifactId>
<version>5.3.1</version>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <version>4.12.0</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
"type": "object",
"properties": {
"observedTimestamp": {
- "description": "The timestamp when the data has been observed. The expected format is 'yyyy-MM-dd'T'HH:mm:ss.SSSZ'. Ex: '2020-12-01T00:00:00.000+0000' ",
+ "description": "The timestamp when the data has been observed. The expected format is 'yyyy-MM-dd'T'HH:mm:ss.SSSZ'. Ex: '2024-02-12T09:35:46.143+0530' ",
"type": "string"
},
"dataspaceName": {
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-ncmp-rest-stub</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
</parent>
<artifactId>cps-ncmp-rest-stub-app</artifactId>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-ncmp-rest-stub</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
</parent>
<artifactId>cps-ncmp-rest-stub-service</artifactId>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
private boolean allRegistrationsSuccessful(
final DmiPluginRegistrationErrorResponse dmiPluginRegistrationErrorResponse) {
return dmiPluginRegistrationErrorResponse.getFailedCreatedCmHandles().isEmpty()
- && dmiPluginRegistrationErrorResponse.getFailedUpdatedCmHandles().isEmpty()
- && dmiPluginRegistrationErrorResponse.getFailedRemovedCmHandles().isEmpty();
+ && dmiPluginRegistrationErrorResponse.getFailedUpdatedCmHandles().isEmpty()
+ && dmiPluginRegistrationErrorResponse.getFailedRemovedCmHandles().isEmpty()
+ && dmiPluginRegistrationErrorResponse.getFailedUpgradeCmHandles().isEmpty();
}
private DmiPluginRegistrationErrorResponse getFailureRegistrationResponse(
import java.util.Map;
import java.util.UUID;
+import java.util.function.BiConsumer;
import java.util.function.Supplier;
import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException;
import org.onap.cps.ncmp.api.impl.operations.DatastoreType;
import org.onap.cps.ncmp.api.impl.operations.OperationType;
+import org.onap.cps.ncmp.api.impl.utils.data.operation.ResourceDataOperationRequestUtils;
import org.onap.cps.ncmp.api.models.CmResourceAddress;
import org.onap.cps.ncmp.api.models.DataOperationRequest;
import org.onap.cps.ncmp.rest.exceptions.OperationNotSupportedException;
private static final Object noReturn = null;
- private static final int MAXIMUM_CM_HANDLES_PER_OPERATION = 50;
+ private static final int MAXIMUM_CM_HANDLES_PER_OPERATION = 50000;
private static final String PAYLOAD_TOO_LARGE_TEMPLATE = "Operation '%s' affects too many (%d) cm handles";
final DataOperationRequest dataOperationRequest,
final String authorization) {
final String requestId = UUID.randomUUID().toString();
- cpsNcmpTaskExecutor.executeTask(
+ cpsNcmpTaskExecutor.executeTaskWithErrorHandling(
getTaskSupplierForDataOperationRequest(topicParamInQuery, dataOperationRequest, requestId, authorization),
+ getTaskCompletionHandlerForDataOperationRequest(topicParamInQuery, dataOperationRequest, requestId),
timeOutInMilliSeconds);
return ResponseEntity.ok(Map.of("requestId", requestId));
}
};
}
+ private static BiConsumer<Object, Throwable> getTaskCompletionHandlerForDataOperationRequest(
+ final String topicParamInQuery,
+ final DataOperationRequest dataOperationRequest,
+ final String requestId) {
+ return (result, throwable) ->
+ ResourceDataOperationRequestUtils.handleAsyncTaskCompletionForDataOperationsRequest(topicParamInQuery,
+ requestId, dataOperationRequest, throwable);
+ }
+
}
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2022-2023 Nordix Foundation
+ * Copyright (C) 2022-2024 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
public class CpsNcmpTaskExecutor {
+ /**
+ * Execute a task asynchronously, and invoke completion handler when done.
+ *
+ * @param taskSupplier functional method is get() task needed to be executed asynchronously
+ * @param taskCompletionHandler the action to perform on task completion or error
+ * @param timeOutInMillis the time-out value in milliseconds
+ */
+ public void executeTaskWithErrorHandling(final Supplier<Object> taskSupplier,
+ final BiConsumer<Object, Throwable> taskCompletionHandler,
+ final long timeOutInMillis) {
+ CompletableFuture.supplyAsync(taskSupplier)
+ .orTimeout(timeOutInMillis, MILLISECONDS)
+ .whenCompleteAsync(taskCompletionHandler);
+ }
+
/**
* Execute a task asynchronously.
*
- * @param taskSupplier functional method is get() task need to executed asynchronously
+ * @param taskSupplier functional method is get() task needed to be executed asynchronously
* @param timeOutInMillis the time-out value in milliseconds
*/
public void executeTask(final Supplier<Object> taskSupplier, final long timeOutInMillis) {
- CompletableFuture.supplyAsync(taskSupplier::get)
- .orTimeout(timeOutInMillis, MILLISECONDS)
- .whenCompleteAsync((taskResult, throwable) -> handleTaskCompletion(throwable));
+ executeTaskWithErrorHandling(taskSupplier, (taskResult, throwable) -> handleTaskCompletion(throwable),
+ timeOutInMillis);
}
private void handleTaskCompletion(final Throwable throwable) {
and: 'async request id is generated'
assert response.contentAsString.contains('requestId')
then: 'the request is handled asynchronously'
- 1 * mockCpsTaskExecutor.executeTask(*_)
+ 1 * mockCpsTaskExecutor.executeTaskWithErrorHandling(*_)
where: 'the following data stores are used'
datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL]
}
def dmiRegistrationResponse = new DmiPluginRegistrationResponse(
createdCmHandles: [createCmHandleResponse],
updatedCmHandles: [updateCmHandleResponse],
- removedCmHandles: [removeCmHandleResponse]
+ removedCmHandles: [removeCmHandleResponse],
+ upgradedCmHandles: [upgradeCmHandleResponse]
)
mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse
when: 'registration endpoint is invoked'
responseBody.getFailedCreatedCmHandles() == expectedFailedCreatedCmHandle
responseBody.getFailedUpdatedCmHandles() == expectedFailedUpdateCmHandle
responseBody.getFailedRemovedCmHandles() == expectedFailedRemovedCmHandle
+ responseBody.getFailedUpgradeCmHandles() == expectedFailedUpgradedCmHandle
where:
- scenario | createCmHandleResponse | updateCmHandleResponse | removeCmHandleResponse || expectedFailedCreatedCmHandle | expectedFailedUpdateCmHandle | expectedFailedRemovedCmHandle
- 'only create failed' | expectedFailedResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') || [expectedUnknownErrorResponse('cm-handle-1')] | [] | []
- 'only update failed' | expectedSuccessResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') || [] | [expectedUnknownErrorResponse('cm-handle-2')] | []
- 'only delete failed' | expectedSuccessResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3') || [] | [] | [expectedUnknownErrorResponse('cm-handle-3')]
- 'all three failed' | expectedFailedResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3') || [expectedUnknownErrorResponse('cm-handle-1')] | [expectedUnknownErrorResponse('cm-handle-2')] | [expectedUnknownErrorResponse('cm-handle-3')]
- 'create update failed' | expectedFailedResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') || [expectedUnknownErrorResponse('cm-handle-1')] | [expectedUnknownErrorResponse('cm-handle-2')] | []
- 'create delete failed' | expectedFailedResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3') || [expectedUnknownErrorResponse('cm-handle-1')] | [] | [expectedUnknownErrorResponse('cm-handle-3')]
- 'update delete failed' | expectedSuccessResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3') || [] | [expectedUnknownErrorResponse('cm-handle-2')] | [expectedUnknownErrorResponse('cm-handle-3')]
+ scenario | createCmHandleResponse | updateCmHandleResponse | removeCmHandleResponse | upgradeCmHandleResponse || expectedFailedCreatedCmHandle | expectedFailedUpdateCmHandle | expectedFailedRemovedCmHandle | expectedFailedUpgradedCmHandle
+ 'only create failed' | expectedFailedResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') | expectedSuccessResponse('cm-handle-4') || [expectedUnknownErrorResponse('cm-handle-1')] | [] | [] | []
+ 'only update failed' | expectedSuccessResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') | expectedSuccessResponse('cm-handle-4') || [] | [expectedUnknownErrorResponse('cm-handle-2')] | [] | []
+ 'only delete failed' | expectedSuccessResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3') | expectedSuccessResponse('cm-handle-4') || [] | [] | [expectedUnknownErrorResponse('cm-handle-3')] | []
+ 'only upgrade failed' | expectedSuccessResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') | expectedFailedResponse('cm-handle-4') || [] | [] | [] | [expectedUnknownErrorResponse('cm-handle-4')]
+ 'all four failed' | expectedFailedResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3') | expectedFailedResponse('cm-handle-4') || [expectedUnknownErrorResponse('cm-handle-1')] | [expectedUnknownErrorResponse('cm-handle-2')] | [expectedUnknownErrorResponse('cm-handle-3')] | [expectedUnknownErrorResponse('cm-handle-4')]
+ 'create update failed' | expectedFailedResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') | expectedSuccessResponse('cm-handle-4') || [expectedUnknownErrorResponse('cm-handle-1')] | [expectedUnknownErrorResponse('cm-handle-2')] | [] | []
+ 'create delete failed' | expectedFailedResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3') | expectedSuccessResponse('cm-handle-4') || [expectedUnknownErrorResponse('cm-handle-1')] | [] | [expectedUnknownErrorResponse('cm-handle-3')] | []
+ 'update delete failed' | expectedSuccessResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3') | expectedSuccessResponse('cm-handle-4') || [] | [expectedUnknownErrorResponse('cm-handle-2')] | [expectedUnknownErrorResponse('cm-handle-3')] | []
+ 'delete upgrade failed' | expectedSuccessResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3') | expectedFailedResponse('cm-handle-4') || [] | [] | [expectedUnknownErrorResponse('cm-handle-3')] | [expectedUnknownErrorResponse('cm-handle-4')]
}
def 'Get all cm handle IDs by DMI plugin identifier.'() {
when: 'data operation request is executed'
objectUnderTest.executeRequest('someTopic', new DataOperationRequest(), NO_AUTH_HEADER)
then: 'the task is executed in an async fashion or not'
- expectedCalls * spiedCpsNcmpTaskExecutor.executeTask(*_)
+ expectedCalls * spiedCpsNcmpTaskExecutor.executeTaskWithErrorHandling(*_)
where: 'the following parameters are used'
scenario | notificationFeatureEnabled || expectedCalls
'on' | true || 1
when: 'data operation request is executed'
objectUnderTest.executeRequest('myTopic', dataOperationRequest, NO_AUTH_HEADER)
then: 'the task is executed in an async fashion'
- 1 * spiedCpsNcmpTaskExecutor.executeTask(*_)
+ 1 * spiedCpsNcmpTaskExecutor.executeTaskWithErrorHandling(*_)
and: 'the network service is invoked'
new PollingConditions().within(1) {
assert networkServiceMethodCalled == true
def 'Attempt to execute async data operation request with too many cm handles.'() {
given: 'a data operation definition with too many cm handles'
- def cmHandleIds = new String[51]
+ def tooMany = objectUnderTest.MAXIMUM_CM_HANDLES_PER_OPERATION+1
+ def cmHandleIds = new String[tooMany]
def dataOperationDefinition = new DataOperationDefinition(operationId: 'abc', operation: 'read', datastore: 'ncmp-datastore:passthrough-running', cmHandleIds: cmHandleIds)
when: 'data operation request is executed'
objectUnderTest.executeRequest('someTopic', new DataOperationRequest(dataOperationDefinitions:[dataOperationDefinition]), NO_AUTH_HEADER)
then: 'a payload too large exception is thrown'
def exceptionThrown = thrown(PayloadTooLargeException)
and: 'the error message contains the offending number of cm handles'
- assert exceptionThrown.message == "Operation 'abc' affects too many (51) cm handles"
+ assert exceptionThrown.message == "Operation 'abc' affects too many (${tooMany}) cm handles"
}
}
def objectUnderTest = new CpsNcmpTaskExecutor()
def logger = Spy(ListAppender<ILoggingEvent>)
def enoughTime = 100
+ def notEnoughTime = 10
void setup() {
((Logger) LoggerFactory.getLogger(CpsNcmpTaskExecutor.class)).addAppender(logger)
assert loggingEvent.formattedMessage.contains('original exception message')
}
+ def 'Task times out.'() {
+ when: 'task is executed without enough time to complete'
+ objectUnderTest.executeTask(taskSupplierForLongRunningTask(), notEnoughTime)
+ then: 'an event is logged with level ERROR'
+ new PollingConditions().within(1) {
+ def loggingEvent = getLoggingEvent()
+ assert loggingEvent.level == Level.ERROR
+ }
+ and: 'a timeout error message is logged'
+ assert loggingEvent.formattedMessage.contains('java.util.concurrent.TimeoutException')
+ }
+
def taskSupplier() {
return () -> 'hello world'
}
return () -> { throw new RuntimeException('original exception message') }
}
+ def taskSupplierForLongRunningTask() {
+ return () -> { sleep(enoughTime) }
+ }
+
def getLoggingEvent() {
return logger.list[0]
}
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
-/*-
+/*
* ============LICENSE_START=======================================================
* Copyright (C) 2023 Nordix Foundation.
* ================================================================================
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2023 Nordix Foundation
+ * Copyright (C) 2023-2024 Nordix Foundation
* ================================================================================
* 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.ncmp.api.impl.utils.context.CpsApplicationContext;
import org.onap.cps.utils.JsonObjectMapper;
-@Builder(buildMethodName = "setCloudEvent")
-public class NcmpCloudEventBuilder {
+@Builder
+public class NcmpEvent {
- private Object event;
+ private Object data;
private Map<String, String> extensions;
private String type;
@Builder.Default
- private static final String EVENT_SPEC_VERSION_V1 = "1.0.0";
+ private static final String CLOUD_EVENT_SPEC_VERSION_V1 = "1.0.0";
+ @Builder.Default
+ private static final String CLOUD_EVENT_SOURCE = "NCMP";
/**
* Creates ncmp cloud event with provided attributes.
*
* @return Cloud Event
*/
- public CloudEvent build() {
+ public CloudEvent asCloudEvent() {
final JsonObjectMapper jsonObjectMapper = CpsApplicationContext.getCpsBean(JsonObjectMapper.class);
final CloudEventBuilder cloudEventBuilder = CloudEventBuilder.v1()
.withId(UUID.randomUUID().toString())
- .withSource(URI.create("NCMP"))
+ .withSource(URI.create(CLOUD_EVENT_SOURCE))
.withType(type)
- .withDataSchema(URI.create("urn:cps:" + type + ":" + EVENT_SPEC_VERSION_V1))
+ .withDataSchema(URI.create("urn:cps:" + type + ":" + CLOUD_EVENT_SPEC_VERSION_V1))
.withTime(EventDateTimeFormatter.toIsoOffsetDateTime(
EventDateTimeFormatter.getCurrentIsoFormattedDateTime()))
- .withData(jsonObjectMapper.asJsonBytes(event));
+ .withData(jsonObjectMapper.asJsonBytes(data));
extensions.entrySet().stream()
.filter(extensionEntry -> StringUtils.isNotBlank(extensionEntry.getValue()))
.forEach(extensionEntry ->
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.onap.cps.events.EventsPublisher;
-import org.onap.cps.ncmp.api.impl.events.NcmpCloudEventBuilder;
+import org.onap.cps.ncmp.api.impl.events.NcmpEvent;
import org.onap.cps.ncmp.events.avc.ncmp_to_client.Avc;
import org.onap.cps.ncmp.events.avc.ncmp_to_client.AvcEvent;
import org.onap.cps.ncmp.events.avc.ncmp_to_client.Data;
final Map<String, String> extensions = createAvcEventExtensions(eventKey);
final CloudEvent avcCloudEvent =
- NcmpCloudEventBuilder.builder().type(AvcEvent.class.getTypeName())
- .event(avcEvent).extensions(extensions).setCloudEvent().build();
+ NcmpEvent.builder().type(AvcEvent.class.getTypeName())
+ .data(avcEvent).extensions(extensions).build().asCloudEvent();
eventsPublisher.publishCloudEvent(avcTopic, eventKey, avcCloudEvent);
}
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2024 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionDmiInEventProducer;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionNcmpOutEventProducer;
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent;
+import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class CmNotificationSubscriptionEventsHandler {
+ private final CmNotificationSubscriptionNcmpOutEventProducer cmNotificationSubscriptionNcmpOutEventProducer;
+ private final CmNotificationSubscriptionDmiInEventProducer cmNotificationSubscriptionDmiInEventProducer;
+
+ /**
+ * Publish the event to the client who requested the subscription with key as subscription id and event is Cloud
+ * Event compliant.
+ *
+ * @param subscriptionId Cm Subscription id
+ * @param eventType Type of event
+ * @param cmNotificationSubscriptionNcmpOutEvent Cm Notification Subscription Event for the
+ * client
+ * @param isScheduledEvent Determines if the event is to be scheduled
+ * or published now
+ */
+ public void publishCmNotificationSubscriptionNcmpOutEvent(final String subscriptionId, final String eventType,
+ final CmNotificationSubscriptionNcmpOutEvent
+ cmNotificationSubscriptionNcmpOutEvent,
+ final boolean isScheduledEvent) {
+ cmNotificationSubscriptionNcmpOutEventProducer.publishCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
+ eventType, cmNotificationSubscriptionNcmpOutEvent, isScheduledEvent);
+ }
+
+ /**
+ * Publish the event to the provided dmi plugin with key as subscription id and the event is in Cloud Event format.
+ *
+ * @param subscriptionId Cm Subscription id
+ * @param dmiPluginName Dmi Plugin Name
+ * @param eventType Type of event
+ * @param cmNotificationSubscriptionDmiInEvent Cm Notification Subscription event for Dmi
+ */
+ public void publishCmNotificationSubscriptionDmiInEvent(final String subscriptionId, final String dmiPluginName,
+ final String eventType,
+ final CmNotificationSubscriptionDmiInEvent
+ cmNotificationSubscriptionDmiInEvent) {
+ cmNotificationSubscriptionDmiInEventProducer.publishCmNotificationSubscriptionDmiInEvent(subscriptionId,
+ dmiPluginName, eventType, cmNotificationSubscriptionDmiInEvent);
+ }
+}
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionDmiInEventMapper;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionPredicate;
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent;
+import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class CmNotificationSubscriptionMappersHandler {
+
+ private final CmNotificationSubscriptionDmiInEventMapper cmNotificationSubscriptionDmiInEventMapper;
+ private final CmNotificationSubscriptionNcmpOutEventMapper cmNotificationSubscriptionNcmpOutEventMapper;
+
+ /**
+ * Mapper to form a request for the DMI Plugin for the Cm Notification Subscription.
+ *
+ * @param dmiCmNotificationSubscriptionPredicates Collection of Cm Notification Subscription predicates
+ * @return cm notification subscription dmi in event
+ */
+ public CmNotificationSubscriptionDmiInEvent toCmNotificationSubscriptionDmiInEvent(
+ final List<DmiCmNotificationSubscriptionPredicate> dmiCmNotificationSubscriptionPredicates) {
+ return cmNotificationSubscriptionDmiInEventMapper.toCmNotificationSubscriptionDmiInEvent(
+ dmiCmNotificationSubscriptionPredicates);
+ }
+
+ /**
+ * Mapper to form a response for the client for the Cm Notification Subscription.
+ *
+ * @param subscriptionId Cm Notification Subscription id
+ * @param dmiCmNotificationSubscriptionDetailsMap contains CmNotificationSubscriptionDetails per dmi plugin
+ * @return CmNotificationSubscriptionNcmpOutEvent to sent back to the client
+ */
+ public CmNotificationSubscriptionNcmpOutEvent toCmNotificationSubscriptionNcmpOutEvent(final String subscriptionId,
+ final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsMap) {
+ return cmNotificationSubscriptionNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
+ dmiCmNotificationSubscriptionDetailsMap);
+ }
+
+ /**
+ * Mapper to form a rejected response for the client for the Cm Notification Subscription Request.
+ *
+ * @param subscriptionId subscription id
+ * @param rejectedTargetFilters list of rejected target filters for the subscription request
+ * @return to sent back to the client
+ */
+ public CmNotificationSubscriptionNcmpOutEvent toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+ final String subscriptionId, final List<String> rejectedTargetFilters) {
+ return cmNotificationSubscriptionNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+ subscriptionId, rejectedTargetFilters);
+ }
+}
package org.onap.cps.ncmp.api.impl.events.cmsubscription;
-import static org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionNcmpOutEventProducer.buildAndGetCmNotificationNcmpOutEventAsCloudEvent;
+import static org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionNcmpOutEventProducer.buildAndGetCmNotificationNcmpOutEventAsCloudEvent;
import io.cloudevents.CloudEvent;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.events.EventsPublisher;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper;
import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
import org.onap.cps.utils.JsonObjectMapper;
@RequiredArgsConstructor
public class CmNotificationSubscriptionNcmpOutEventPublishingTask implements Runnable {
-
private final String topicName;
private final String subscriptionId;
private final String eventType;
private final EventsPublisher<CloudEvent> eventsPublisher;
private final JsonObjectMapper jsonObjectMapper;
- private final Map<String, Map<String, DmiCmNotificationSubscriptionDetails>> cmNotificationSubscriptionCache;
- private final CmNotificationSubscriptionNcmpOutEventMapper cmNotificationSubscriptionNcmpOutEventMapper;
+ private final CmNotificationSubscriptionMappersHandler cmNotificationSubscriptionMappersHandler;
+ private final DmiCmNotificationSubscriptionCacheHandler dmiCmNotificationSubscriptionCacheHandler;
/**
* Delegating the responsibility of publishing CmNotificationSubscriptionNcmpOutEvent as a separate task which will
@Override
public void run() {
final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsMap =
- cmNotificationSubscriptionCache.get(subscriptionId);
+ dmiCmNotificationSubscriptionCacheHandler.get(subscriptionId);
final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent =
- cmNotificationSubscriptionNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
+ cmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
dmiCmNotificationSubscriptionDetailsMap);
eventsPublisher.publishCloudEvent(topicName, subscriptionId,
buildAndGetCmNotificationNcmpOutEventAsCloudEvent(jsonObjectMapper, subscriptionId, eventType,
cmNotificationSubscriptionNcmpOutEvent));
+ dmiCmNotificationSubscriptionCacheHandler
+ .removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries(subscriptionId);
}
-
}
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus;
import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
/**
* Adds new subscription to the subscription cache.
*
- * @param subscriptionId subscription Id
+ * @param subscriptionId subscription id
* @param predicates subscription request predicates
*/
public void add(final String subscriptionId, final List<Predicate> predicates) {
cmNotificationSubscriptionCache.put(subscriptionId, createDmiCmNotificationSubscriptionsPerDmi(predicates));
}
+ /**
+ * Get cm notification subscription cache entry via subscription id.
+ *
+ * @param subscriptionId subscription id
+ * @return map of dmi cm notification subscriptions per dmi
+ */
+ public Map<String, DmiCmNotificationSubscriptionDetails> get(final String subscriptionId) {
+ return cmNotificationSubscriptionCache.get(subscriptionId);
+ }
+
+
+ /**
+ * Remove cache entries with CmNotificationSubscriptionStatus ACCEPTED/REJECTED via subscription id.
+ *
+ * @param subscriptionId subscription id as key in CM notification Subscription cache.
+ */
+ public void removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries(final String subscriptionId) {
+ final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionsPerDmi =
+ cmNotificationSubscriptionCache.get(subscriptionId);
+ final Map<String, DmiCmNotificationSubscriptionDetails> updatedDmiCmNotificationSubscriptionsPerDmi =
+ dmiCmNotificationSubscriptionsPerDmi.entrySet().stream().filter(
+ dmiCmNotificationSubscription ->
+ !isAcceptedOrRejected(dmiCmNotificationSubscription.getValue()))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ cmNotificationSubscriptionCache.put(subscriptionId, updatedDmiCmNotificationSubscriptionsPerDmi);
+ }
+
/**
* Creates map of subscription details per DMI.
*
*
*/
public void updateDmiCmNotificationSubscriptionStatusPerDmi(
- final String subscriptionId, final String dmiServiceName, final CmNotificationSubscriptionStatus status) {
+ final String subscriptionId, final String dmiServiceName, final CmNotificationSubscriptionStatus status) {
cmNotificationSubscriptionCache.get(subscriptionId).get(dmiServiceName)
- .setCmNotificationSubscriptionStatus(status);
+ .setCmNotificationSubscriptionStatus(status);
}
/**
*/
public void persistIntoDatabasePerDmi(final String subscriptionId, final String dmiServiceName) {
final List<DmiCmNotificationSubscriptionPredicate> dmiCmNotificationSubscriptionPredicateList =
- cmNotificationSubscriptionCache.get(subscriptionId).get(dmiServiceName)
- .getDmiCmNotificationSubscriptionPredicates();
+ cmNotificationSubscriptionCache.get(subscriptionId).get(dmiServiceName)
+ .getDmiCmNotificationSubscriptionPredicates();
for (final DmiCmNotificationSubscriptionPredicate dmiCmNotificationSubscriptionPredicate:
- dmiCmNotificationSubscriptionPredicateList) {
+ dmiCmNotificationSubscriptionPredicateList) {
final DatastoreType datastoreType = dmiCmNotificationSubscriptionPredicate.getDatastoreType();
final Set<String> cmHandles = dmiCmNotificationSubscriptionPredicate.getTargetCmHandleIds();
final Set<String> xpaths = dmiCmNotificationSubscriptionPredicate.getXpaths();
for (final String cmHandle: cmHandles) {
for (final String xpath: xpaths) {
- cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,
- cmHandle, xpath, subscriptionId);
+ cmNotificationSubscriptionPersistenceService.addCmNotificationSubscription(datastoreType, cmHandle,
+ xpath, subscriptionId);
}
}
}
}
return targetCmHandlesByDmiServiceNames;
}
+
+ private boolean isAcceptedOrRejected(
+ final DmiCmNotificationSubscriptionDetails dmiCmNotificationSubscription) {
+ return dmiCmNotificationSubscription.getCmNotificationSubscriptionStatus().toString().equals("ACCEPTED")
+ || dmiCmNotificationSubscription.getCmNotificationSubscriptionStatus().toString().equals("REJECTED");
+ }
}
\ No newline at end of file
* ============LICENSE_END=========================================================
*/
-package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+package org.onap.cps.ncmp.api.impl.events.cmsubscription.consumer;
import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler;
import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus;
import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.CmNotificationSubscriptionDmiOutEvent;
import org.springframework.kafka.annotation.KafkaListener;
* ============LICENSE_END=========================================================
*/
-package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+package org.onap.cps.ncmp.api.impl.events.cmsubscription.consumer;
import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent;
* ============LICENSE_END=========================================================
*/
-package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+package org.onap.cps.ncmp.api.impl.events.cmsubscription.producer;
import io.cloudevents.CloudEvent;
import io.cloudevents.core.builder.CloudEventBuilder;
* ============LICENSE_END=========================================================
*/
-package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+package org.onap.cps.ncmp.api.impl.events.cmsubscription.producer;
import io.cloudevents.CloudEvent;
import io.cloudevents.core.builder.CloudEventBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.events.EventsPublisher;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionMappersHandler;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionNcmpOutEventPublishingTask;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler;
import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
import org.onap.cps.utils.JsonObjectMapper;
import org.springframework.beans.factory.annotation.Value;
private final EventsPublisher<CloudEvent> eventsPublisher;
private final JsonObjectMapper jsonObjectMapper;
- private final Map<String, Map<String, DmiCmNotificationSubscriptionDetails>> cmNotificationSubscriptionCache;
- private final CmNotificationSubscriptionNcmpOutEventMapper cmNotificationSubscriptionNcmpOutEventMapper;
+ private final CmNotificationSubscriptionMappersHandler cmNotificationSubscriptionMappersHandler;
+ private final DmiCmNotificationSubscriptionCacheHandler dmiCmNotificationSubscriptionCacheHandler;
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
private static final Map<String, ScheduledFuture<?>> scheduledTasksPerSubscriptionId = new ConcurrentHashMap<>();
* or published now
*/
public void publishCmNotificationSubscriptionNcmpOutEvent(final String subscriptionId, final String eventType,
- final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent,
- final boolean isScheduledEvent) {
+ final CmNotificationSubscriptionNcmpOutEvent
+ cmNotificationSubscriptionNcmpOutEvent,
+ final boolean isScheduledEvent) {
if (isScheduledEvent && !scheduledTasksPerSubscriptionId.containsKey(subscriptionId)) {
final ScheduledFuture<?> scheduledFuture =
cmNotificationSubscriptionNcmpOutEvent);
log.info("Published CmNotificationSubscriptionEvent on demand for subscriptionId : {}", subscriptionId);
}
-
}
private ScheduledFuture<?> scheduleAndPublishCmNotificationSubscriptionNcmpOutEvent(final String subscriptionId,
- final String eventType) {
+ final String eventType) {
final CmNotificationSubscriptionNcmpOutEventPublishingTask
cmNotificationSubscriptionNcmpOutEventPublishingTask =
new CmNotificationSubscriptionNcmpOutEventPublishingTask(cmNotificationSubscriptionNcmpOutEventTopic,
- subscriptionId, eventType, eventsPublisher, jsonObjectMapper, cmNotificationSubscriptionCache,
- cmNotificationSubscriptionNcmpOutEventMapper);
+ subscriptionId, eventType, eventsPublisher, jsonObjectMapper,
+ cmNotificationSubscriptionMappersHandler, dmiCmNotificationSubscriptionCacheHandler);
return scheduledExecutorService.schedule(cmNotificationSubscriptionNcmpOutEventPublishingTask,
cmNotificationSubscriptionDmiOutEventTimeoutInMs, TimeUnit.MILLISECONDS);
}
private void publishCmNotificationSubscriptionNcmpOutEventNow(final String subscriptionId, final String eventType,
- final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent) {
+ final CmNotificationSubscriptionNcmpOutEvent
+ cmNotificationSubscriptionNcmpOutEvent) {
final CloudEvent cmNotificationSubscriptionNcmpOutEventAsCloudEvent =
buildAndGetCmNotificationNcmpOutEventAsCloudEvent(jsonObjectMapper, subscriptionId, eventType,
cmNotificationSubscriptionNcmpOutEvent);
eventsPublisher.publishCloudEvent(cmNotificationSubscriptionNcmpOutEventTopic, subscriptionId,
cmNotificationSubscriptionNcmpOutEventAsCloudEvent);
+ dmiCmNotificationSubscriptionCacheHandler
+ .removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries(subscriptionId);
}
- protected static CloudEvent buildAndGetCmNotificationNcmpOutEventAsCloudEvent(
+ /**
+ * Get an NCMP out event as cloud event.
+ *
+ * @param jsonObjectMapper JSON object mapper
+ * @param subscriptionId subscription id
+ * @param eventType event type
+ * @param cmNotificationSubscriptionNcmpOutEvent cm notification subscription NCMP out event
+ * @return cm notification subscription NCMP out event as cloud event
+ */
+ public static CloudEvent buildAndGetCmNotificationNcmpOutEventAsCloudEvent(
final JsonObjectMapper jsonObjectMapper, final String subscriptionId, final String eventType,
final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent) {
return CloudEventBuilder.v1().withId(UUID.randomUUID().toString()).withType(eventType)
- .withSource(URI.create("NCMP")).withDataSchema(URI.create("org.onap.ncmp.cm.subscription:1.0.0"))
- .withExtension("correlationid", subscriptionId)
- .withData(jsonObjectMapper.asJsonBytes(cmNotificationSubscriptionNcmpOutEvent)).build();
+ .withSource(URI.create("NCMP")).withDataSchema(URI.create("org.onap.ncmp.cm.subscription:1.0.0"))
+ .withExtension("correlationid", subscriptionId)
+ .withData(jsonObjectMapper.asJsonBytes(cmNotificationSubscriptionNcmpOutEvent)).build();
}
}
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionNcmpOutEventProducer;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionDelta;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionEventsHandler;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionMappersHandler;
import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionPredicate;
import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent;
import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.Predicate;
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent;
import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
import org.springframework.stereotype.Service;
public class CmNotificationSubscriptionHandlerServiceImpl implements CmNotificationSubscriptionHandlerService {
private final CmNotificationSubscriptionPersistenceService cmNotificationSubscriptionPersistenceService;
- private final CmNotificationSubscriptionNcmpOutEventMapper cmNotificationSubscriptionNcmpOutEventMapper;
- private final CmNotificationSubscriptionNcmpOutEventProducer cmNotificationSubscriptionNcmpOutEventProducer;
+ private final CmNotificationSubscriptionDelta cmNotificationSubscriptionDelta;
+ private final CmNotificationSubscriptionMappersHandler cmNotificationSubscriptionMappersHandler;
+ private final CmNotificationSubscriptionEventsHandler cmNotificationSubscriptionEventsHandler;
private final DmiCmNotificationSubscriptionCacheHandler dmiCmNotificationSubscriptionCacheHandler;
@Override
public void processSubscriptionCreateRequest(
- final CmNotificationSubscriptionNcmpInEvent cmNotificationSubscriptionNcmpInEvent) {
+ final CmNotificationSubscriptionNcmpInEvent cmNotificationSubscriptionNcmpInEvent) {
final String subscriptionId = cmNotificationSubscriptionNcmpInEvent.getData().getSubscriptionId();
final List<Predicate> predicates = cmNotificationSubscriptionNcmpInEvent.getData().getPredicates();
if (cmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId(subscriptionId)) {
dmiCmNotificationSubscriptionCacheHandler.add(subscriptionId, predicates);
+ sendSubscriptionCreateRequestToDmi(subscriptionId);
} else {
final Set<String> subscriptionTargetFilters = predicates.stream().flatMap(
- predicate -> predicate.getTargetFilter().stream()).collect(Collectors.toSet());
+ predicate -> predicate.getTargetFilter().stream()).collect(Collectors.toSet());
rejectAndPublishCmNotificationSubscriptionCreateRequest(subscriptionId,
new ArrayList<>(subscriptionTargetFilters));
}
private void rejectAndPublishCmNotificationSubscriptionCreateRequest(final String subscriptionId,
final List<String> subscriptionTargetFilters) {
final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent =
- cmNotificationSubscriptionNcmpOutEventMapper
- .toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(subscriptionId,
- subscriptionTargetFilters);
- cmNotificationSubscriptionNcmpOutEventProducer.publishCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
- "subscriptionCreateResponse", cmNotificationSubscriptionNcmpOutEvent, false);
+ cmNotificationSubscriptionMappersHandler
+ .toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(subscriptionId,
+ subscriptionTargetFilters);
+ cmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
+ "subscriptionCreateResponse", cmNotificationSubscriptionNcmpOutEvent, false);
}
-}
+
+ private void sendSubscriptionCreateRequestToDmi(final String subscriptionId) {
+ final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsMap =
+ dmiCmNotificationSubscriptionCacheHandler.get(subscriptionId);
+ dmiCmNotificationSubscriptionDetailsMap.forEach((dmiPluginName, dmiCmNotificationSubscriptionDetails) -> {
+ final List<DmiCmNotificationSubscriptionPredicate> dmiCmNotificationSubscriptionPredicates =
+ cmNotificationSubscriptionDelta.getDelta(
+ dmiCmNotificationSubscriptionDetails.getDmiCmNotificationSubscriptionPredicates());
+ final CmNotificationSubscriptionDmiInEvent cmNotificationSubscriptionDmiInEvent =
+ cmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionDmiInEvent(
+ dmiCmNotificationSubscriptionPredicates);
+ cmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent(subscriptionId,
+ dmiPluginName, "subscriptionCreateRequest", cmNotificationSubscriptionDmiInEvent);
+ });
+ }
+}
\ No newline at end of file
/**
* Check if we have an ongoing cm subscription based on the parameters.
*
- * @param datastoreType valid datastore type
- * @param cmHandleId cmhandle id
- * @param xpath valid xpath
+ * @param datastoreType the susbcription target datastore type
+ * @param cmHandleId the id of the cm handle for the susbcription
+ * @param xpath the target xpath
* @return true for ongoing cmsubscription , otherwise false
*/
boolean isOngoingCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
/**
* Get all ongoing cm notification subscription based on the parameters.
*
- * @param datastoreType valid datastore type
- * @param cmHandleId cmhandle id
- * @param xpath valid xpath
+ * @param datastoreType the susbcription target datastore type
+ * @param cmHandleId the id of the cm handle for the susbcription
+ * @param xpath the target xpath
* @return collection of subscription ids of ongoing cm notification subscription
*/
Collection<String> getOngoingCmNotificationSubscriptionIds(final DatastoreType datastoreType,
final String cmHandleId, final String xpath);
/**
- * Add or update cm notification subscription.
+ * Add cm notification subscription.
*
- * @param datastoreType valid datastore type
- * @param cmHandle cmhandle id
- * @param xpath valid xpath
- * @param newSubscriptionId subscription Id to be added
+ * @param datastoreType the susbcription target datastore type
+ * @param cmHandleId the id of the cm handle for the susbcription
+ * @param xpath the target xpath
+ * @param newSubscriptionId subscription id to be added
*/
- void addOrUpdateCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandle,
- final String xpath, final String newSubscriptionId);
+ void addCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
+ final String xpath, final String newSubscriptionId);
+
+ /**
+ * Remove cm notification Subscription.
+ *
+ * @param datastoreType the susbcription target datastore type
+ * @param cmHandleId the id of the cm handle for the susbcription
+ * @param xpath the target xpath
+ * @param subscriptionId subscription id to remove
+ */
+ void removeCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
+ final String xpath, final String subscriptionId);
+
}
+
import java.io.Serializable;
import java.time.OffsetDateTime;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@Override
public boolean isOngoingCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
- final String xpath) {
+ final String xpath) {
return !getOngoingCmNotificationSubscriptionIds(datastoreType, cmHandleId, xpath).isEmpty();
}
@Override
public Collection<String> getOngoingCmNotificationSubscriptionIds(final DatastoreType datastoreType,
- final String cmHandleId, final String xpath) {
+ final String cmHandleId, final String xpath) {
final String isOngoingCmSubscriptionCpsPathQuery =
CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
}
@Override
- public void addOrUpdateCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
- final String xpath, final String newSubscriptionId) {
- if (isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)) {
- final DataNode existingFilterNode =
- cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
- CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
- escapeQuotesByDoublingThem(xpath)),
- OMIT_DESCENDANTS).iterator().next();
- final Collection<String> existingSubscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
+ public void addCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
+ final String xpath, final String subscriptionId) {
+ if (isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)
+ && (!getOngoingCmNotificationSubscriptionIds(datastoreType, cmHandleId, xpath)
+ .contains(subscriptionId))) {
+ final DataNode subscriptionAsDataNode = getSubscriptionAsDataNode(datastoreType, cmHandleId, xpath);
+ final Collection<String> subscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
cmHandleId, xpath);
- if (!existingSubscriptionIds.contains(newSubscriptionId)) {
- updateListOfSubscribers(existingSubscriptionIds, newSubscriptionId, existingFilterNode);
- }
+ subscriptionIds.add(subscriptionId);
+ saveSubscriptionDetails(subscriptionAsDataNode, subscriptionIds);
+ } else {
+ addNewSubscriptionViaDatastore(datastoreType, cmHandleId, xpath, subscriptionId);
+ }
+ }
+
+ @Override
+ public void removeCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
+ final String xpath, final String subscriptionId) {
+ final DataNode subscriptionAsDataNode = getSubscriptionAsDataNode(datastoreType, cmHandleId, xpath);
+ final Collection<String> subscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
+ cmHandleId, xpath);
+ subscriptionIds.remove(subscriptionId);
+ saveSubscriptionDetails(subscriptionAsDataNode, subscriptionIds);
+ if (isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)) {
+ log.info("There are subscribers left for the following cps path {} :",
+ CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+ escapeQuotesByDoublingThem(xpath)));
} else {
- addNewSubscriptionViaDatastore(datastoreType, cmHandleId, xpath, newSubscriptionId);
+ log.info("No subscribers left for the following cps path {} :",
+ CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+ escapeQuotesByDoublingThem(xpath)));
+ deleteListOfSubscriptionsFor(datastoreType, cmHandleId, xpath);
}
}
+ private void deleteListOfSubscriptionsFor(final DatastoreType datastoreType, final String cmHandleId,
+ final String xpath) {
+ cpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
+ CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+ escapeQuotesByDoublingThem(xpath)),
+ OffsetDateTime.now());
+ }
+
+ private DataNode getSubscriptionAsDataNode(final DatastoreType datastoreType, final String cmHandleId,
+ final String xpath) {
+ return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
+ CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+ escapeQuotesByDoublingThem(xpath)),
+ OMIT_DESCENDANTS).iterator().next();
+ }
+
private void addNewSubscriptionViaDatastore(final DatastoreType datastoreType, final String cmHandleId,
final String xpath, final String newSubscriptionId) {
final String parentXpath = "/datastores/datastore[@name='%s']/cm-handles"
.formatted(datastoreType.getDatastoreName());
- final String updatedJson = String.format("{\"cm-handle\":[{\"id\":\"%s\",\"filters\":{\"filter\":"
+ final String subscriptionAsJson = String.format("{\"cm-handle\":[{\"id\":\"%s\",\"filters\":{\"filter\":"
+ "[{\"xpath\":\"%s\",\"subscriptionIds\":[\"%s\"]}]}}]}", cmHandleId, xpath, newSubscriptionId);
- cpsDataService.saveData(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, updatedJson,
+ cpsDataService.saveData(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, subscriptionAsJson,
OffsetDateTime.now(), ContentType.JSON);
}
- private void updateListOfSubscribers(final Collection<String> existingSubscriptionIds,
- final String newSubscriptionId, final DataNode existingFilterNode) {
- final String parentXpath = CpsPathUtil.getNormalizedParentXpath(existingFilterNode.getXpath());
- final List<String> updatedSubscribers = new ArrayList<>(existingSubscriptionIds);
- updatedSubscribers.add(newSubscriptionId);
- final Map<String, Serializable> updatedLeaves = new HashMap<>();
- updatedLeaves.put("xpath", existingFilterNode.getLeaves().get("xpath"));
- updatedLeaves.put("subscriptionIds", (Serializable) updatedSubscribers);
- final String updatedJson = "{\"filter\":[" + jsonObjectMapper.asJsonString(updatedLeaves) + "]}";
- cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, updatedJson,
- OffsetDateTime.now());
+ private void saveSubscriptionDetails(final DataNode subscriptionDetailsAsDataNode,
+ final Collection<String> subscriptionIds) {
+ final Map<String, Serializable> subscriptionDetailsAsMap = new HashMap<>();
+ subscriptionDetailsAsMap.put("xpath", subscriptionDetailsAsDataNode.getLeaves().get("xpath"));
+ subscriptionDetailsAsMap.put("subscriptionIds", (Serializable) subscriptionIds);
+ final String parentXpath = CpsPathUtil.getNormalizedParentXpath(subscriptionDetailsAsDataNode.getXpath());
+ final String subscriptionDetailsAsJson = "{\"filter\":["
+ + jsonObjectMapper.asJsonString(subscriptionDetailsAsMap).replace("'", "\"") + "]}";
+ cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath,
+ subscriptionDetailsAsJson, OffsetDateTime.now());
}
private static String escapeQuotesByDoublingThem(final String inputXpath) {
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.exception;
+
+import java.io.Serial;
+
+public class NoAlternateIdParentFoundException extends NcmpException {
+
+ @Serial
+ private static final long serialVersionUID = -2412915490233422945L;
+ private static final String ALTERNATE_ID_NOT_FOUND = "No matching (parent) cm handle found using alternate ids";
+
+ /**
+ * Constructor.
+ *
+ * @param cpsPath datanode cpsPath
+ */
+ public NoAlternateIdParentFoundException(final String cpsPath) {
+ super(ALTERNATE_ID_NOT_FOUND, String.format("cannot find a datanode with alternate id %s", cpsPath));
+ }
+}
*/
DataNode getCmHandleDataNodeByAlternateId(String alternateId);
+ /**
+ * Get data node that matches longest alternate id by removing elements (as defined by the separator string)
+ * from right to left.
+ *
+ * @param alternateId alternate ID
+ * @param separator a string that separates each element from the next.
+ * @return data node
+ */
+ DataNode getCmHandleDataNodeByLongestMatchAlternateId(final String alternateId, final String separator);
+
/**
* Get collection of data nodes of given cm handles.
*
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
import org.onap.cps.api.CpsAnchorService;
import org.onap.cps.api.CpsDataService;
import org.onap.cps.api.CpsModuleService;
+import org.onap.cps.ncmp.api.impl.exception.NoAlternateIdParentFoundException;
import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
import org.onap.cps.spi.FetchDescendantsOption;
@Override
public DataNode getCmHandleDataNodeByAlternateId(final String alternateId) {
- final String xPathForCmHandleByAlternateId = getXPathForCmHandleByAlternateId(alternateId);
+ final String cpsPathForCmHandleByAlternateId = getCpsPathForCmHandleByAlternateId(alternateId);
final Collection<DataNode> dataNodes = cmHandleQueries
- .queryNcmpRegistryByCpsPath(xPathForCmHandleByAlternateId, OMIT_DESCENDANTS);
+ .queryNcmpRegistryByCpsPath(cpsPathForCmHandleByAlternateId, OMIT_DESCENDANTS);
if (dataNodes.isEmpty()) {
throw new DataNodeNotFoundException(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
- xPathForCmHandleByAlternateId);
+ cpsPathForCmHandleByAlternateId);
}
return dataNodes.iterator().next();
}
+ @Override
+ public DataNode getCmHandleDataNodeByLongestMatchAlternateId(final String alternateId, final String separator) {
+ String bestMatch = alternateId;
+ while (StringUtils.isNotEmpty(bestMatch)) {
+ try {
+ return getCmHandleDataNodeByAlternateId(bestMatch);
+ } catch (final DataNodeNotFoundException ignored) {
+ bestMatch = getParentPath(bestMatch, separator);
+ }
+ }
+ throw new NoAlternateIdParentFoundException(alternateId);
+ }
+
@Override
public Collection<DataNode> getCmHandleDataNodes(final Collection<String> cmHandleIds) {
final Collection<String> xpaths = new ArrayList<>(cmHandleIds.size());
return NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@id='" + cmHandleId + "']";
}
- private static String getXPathForCmHandleByAlternateId(final String alternateId) {
+ private static String getCpsPathForCmHandleByAlternateId(final String alternateId) {
return NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@alternate-id='" + alternateId + "']";
}
private String createCmHandlesJsonData(final List<YangModelCmHandle> yangModelCmHandles) {
return "{\"cm-handles\":" + jsonObjectMapper.asJsonString(yangModelCmHandles) + "}";
}
+
+ private static String getParentPath(final String path, final String separator) {
+ final int lastSeparatorIndex = path.lastIndexOf(separator);
+ return lastSeparatorIndex < 0 ? "" : path.substring(0, lastSeparatorIndex);
+ }
}
/**
* This method populates uri variables.
*
- * @param dataStoreName data store name
+ * @param dataStoreName data store name
* @param dmiServiceName dmi service name
* @param cmHandleId cm handle id for dmi registration
* @return {@code String} dmi service url as string
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2023 Nordix Foundation
+ * Copyright (C) 2023-2024 Nordix Foundation
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.ncmp.api.NcmpResponseStatus;
-import org.onap.cps.ncmp.api.impl.events.NcmpCloudEventBuilder;
+import org.onap.cps.ncmp.api.impl.events.NcmpEvent;
import org.onap.cps.ncmp.api.impl.operations.DmiDataOperation;
import org.onap.cps.ncmp.events.async1_0_0.Data;
import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent;
final Data data = createPayloadFromDataOperationResponses(cmHandleIdsPerResponseCodesPerOperation);
dataOperationEvent.setData(data);
final Map<String, String> extensions = createDataOperationExtensions(requestId, clientTopic);
- return NcmpCloudEventBuilder.builder().type(DataOperationEvent.class.getName())
- .event(dataOperationEvent).extensions(extensions).setCloudEvent().build();
+ return NcmpEvent.builder().type(DataOperationEvent.class.getName())
+ .data(dataOperationEvent).extensions(extensions).build().asCloudEvent();
}
private static Data createPayloadFromDataOperationResponses(final MultiValueMap<DmiDataOperation,
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
DmiDataOperation.buildDmiDataOperationRequestBodyWithoutCmHandles(dataOperationDefinitionIn),
CM_HANDLES_NOT_READY, nonReadyCmHandleIds);
}
- if (!cmHandleIdsPerResponseCodesPerOperation.isEmpty()) {
- publishErrorMessageToClientTopic(topicParamInQuery, requestId, cmHandleIdsPerResponseCodesPerOperation);
- }
+ publishErrorMessageToClientTopic(topicParamInQuery, requestId, cmHandleIdsPerResponseCodesPerOperation);
return dmiDataOperationsOutPerDmiServiceName;
}
+ /**
+ * Handles the async task completion for an entire data, publishing errors to client topic on task failure.
+ *
+ * @param topicParamInQuery client given topic
+ * @param requestId unique identifier per request
+ * @param dataOperationRequest incoming data operation request details
+ * @param throwable error cause, or null if task completed with no exception
+ */
+ public static void handleAsyncTaskCompletionForDataOperationsRequest(
+ final String topicParamInQuery,
+ final String requestId,
+ final DataOperationRequest dataOperationRequest,
+ final Throwable throwable) {
+ if (throwable == null) {
+ log.info("Data operations request {} completed.", requestId);
+ } else if (throwable instanceof TimeoutException) {
+ log.error("Data operations request {} timed out.", requestId);
+ ResourceDataOperationRequestUtils.publishErrorMessageToClientTopicForEntireOperation(topicParamInQuery,
+ requestId, dataOperationRequest, NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING);
+ } else {
+ log.error("Data operations request {} failed.", requestId, throwable);
+ ResourceDataOperationRequestUtils.publishErrorMessageToClientTopicForEntireOperation(topicParamInQuery,
+ requestId, dataOperationRequest, NcmpResponseStatus.UNKNOWN_ERROR);
+ }
+ }
+
+ /**
+ * Creates data operation cloud event for when the entire data operation fails and publishes it to client topic.
+ *
+ * @param topicParamInQuery client given topic
+ * @param requestId unique identifier per request
+ * @param dataOperationRequestIn incoming data operation request details
+ * @param ncmpResponseStatus response code to be sent for all cm handle ids in all operations
+ */
+ private static void publishErrorMessageToClientTopicForEntireOperation(
+ final String topicParamInQuery,
+ final String requestId,
+ final DataOperationRequest dataOperationRequestIn,
+ final NcmpResponseStatus ncmpResponseStatus) {
+
+ final MultiValueMap<DmiDataOperation, Map<NcmpResponseStatus, List<String>>>
+ cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>();
+
+ for (final DataOperationDefinition dataOperationDefinitionIn :
+ dataOperationRequestIn.getDataOperationDefinitions()) {
+ cmHandleIdsPerResponseCodesPerOperation.add(
+ DmiDataOperation.buildDmiDataOperationRequestBodyWithoutCmHandles(dataOperationDefinitionIn),
+ Map.of(ncmpResponseStatus, dataOperationDefinitionIn.getCmHandleIds()));
+ }
+ publishErrorMessageToClientTopic(topicParamInQuery, requestId, cmHandleIdsPerResponseCodesPerOperation);
+ }
+
/**
* Creates data operation cloud event and publish it to client topic.
*
* @param clientTopic client given topic
* @param requestId unique identifier per request
- * @param cmHandleIdsPerResponseCodesPerOperation list of cm handle ids per operation with response code
+ * @param cmHandleIdsPerResponseCodesPerOperation list of cm handle ids per operation with response code
*/
public static void publishErrorMessageToClientTopic(final String clientTopic,
final String requestId,
final MultiValueMap<DmiDataOperation,
Map<NcmpResponseStatus, List<String>>>
cmHandleIdsPerResponseCodesPerOperation) {
- final CloudEvent dataOperationCloudEvent = DataOperationEventCreator.createDataOperationEvent(clientTopic,
- requestId, cmHandleIdsPerResponseCodesPerOperation);
- final EventsPublisher<CloudEvent> eventsPublisher = CpsApplicationContext.getCpsBean(EventsPublisher.class);
- eventsPublisher.publishCloudEvent(clientTopic, requestId, dataOperationCloudEvent);
+ if (!cmHandleIdsPerResponseCodesPerOperation.isEmpty()) {
+ final CloudEvent dataOperationCloudEvent = DataOperationEventCreator.createDataOperationEvent(clientTopic,
+ requestId, cmHandleIdsPerResponseCodesPerOperation);
+ final EventsPublisher<CloudEvent> eventsPublisher = CpsApplicationContext.getCpsBean(EventsPublisher.class);
+ eventsPublisher.publishCloudEvent(clientTopic, requestId, dataOperationCloudEvent);
+ }
}
private static Map<String, String> getDmiServiceNamesPerCmHandleId(
@Autowired
NcmpConfiguration.DmiProperties dmiProperties
-
+
@Autowired
HttpClientConfiguration httpClientConfiguration
-
+
def mockRestTemplateBuilder = new RestTemplateBuilder()
def 'NcmpConfiguration Construction.'() {
import com.fasterxml.jackson.databind.ObjectMapper
import io.cloudevents.CloudEvent
import org.onap.cps.events.EventsPublisher
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionDmiInEventProducer
import org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper
import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent
import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.Cmhandle
import io.cloudevents.CloudEvent
import io.cloudevents.core.builder.CloudEventBuilder
import org.apache.kafka.clients.consumer.ConsumerRecord
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.consumer.CmNotificationSubscriptionDmiOutEventConsumer
import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus
import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec
import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.CmNotificationSubscriptionDmiOutEvent
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2024 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.events.cmsubscription
+
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionNcmpOutEventProducer
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionDmiInEventProducer
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent
+import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent
+import spock.lang.Specification
+
+class CmNotificationSubscriptionEventsHandlerSpec extends Specification {
+
+ def mockCmNotificationSubscriptionNcmpOutEventProducer = Mock(CmNotificationSubscriptionNcmpOutEventProducer)
+ def mockCmNotificationSubscriptionDmiInEventProducer = Mock(CmNotificationSubscriptionDmiInEventProducer)
+
+ def objectUnderTest = new CmNotificationSubscriptionEventsHandler(mockCmNotificationSubscriptionNcmpOutEventProducer,
+ mockCmNotificationSubscriptionDmiInEventProducer)
+
+ def 'Publish cm notification subscription ncmp out event'() {
+ given: 'an ncmp out event'
+ def cmNotificationSubscriptionNcmpOutEvent = new CmNotificationSubscriptionNcmpOutEvent()
+ when: 'the method to publish cm notification subscription ncmp out event is called'
+ objectUnderTest.publishCmNotificationSubscriptionNcmpOutEvent("some-id",
+ "some-event", cmNotificationSubscriptionNcmpOutEvent, true)
+ then: 'the parameters is delegated to the correct method once'
+ 1 * mockCmNotificationSubscriptionNcmpOutEventProducer.publishCmNotificationSubscriptionNcmpOutEvent(
+ "some-id", "some-event", cmNotificationSubscriptionNcmpOutEvent, true)
+ }
+
+ def 'Publish cm notification subscription dmi in event'() {
+ given: 'a dmi in event'
+ def cmNotificationSubscriptionDmiInEvent = new CmNotificationSubscriptionDmiInEvent()
+ when: 'the method to publish cm notification subscription ncmp out event is called'
+ objectUnderTest.publishCmNotificationSubscriptionDmiInEvent("some-id",
+ "some-dmi", "some-event", cmNotificationSubscriptionDmiInEvent)
+ then: 'the parameters is delegated to the correct method once'
+ 1 * mockCmNotificationSubscriptionDmiInEventProducer.publishCmNotificationSubscriptionDmiInEvent("some-id",
+ "some-dmi", "some-event", cmNotificationSubscriptionDmiInEvent)
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.events.cmsubscription
+
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionDmiInEventMapper
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper
+import spock.lang.Specification
+
+class CmNotificationSubscriptionMappersHandlerSpec extends Specification{
+
+ def mockCmNotificationDmiInEventMapper = Mock(CmNotificationSubscriptionDmiInEventMapper)
+ def mockCmNotificationNcmpOutEventMapper = Mock(CmNotificationSubscriptionNcmpOutEventMapper)
+
+ def objectUnderTest = new CmNotificationSubscriptionMappersHandler(mockCmNotificationDmiInEventMapper,
+ mockCmNotificationNcmpOutEventMapper)
+
+ def 'Get cm notification subscription DMI in event'() {
+ given: 'a list of predicates'
+ def testListOfPredicates = []
+ when: 'method to create a cm notification subscription dmi in event is called with predicates'
+ objectUnderTest.toCmNotificationSubscriptionDmiInEvent(testListOfPredicates)
+ then: 'the parameters is delegated to the correct dmi in event mapper method'
+ 1 * mockCmNotificationDmiInEventMapper.toCmNotificationSubscriptionDmiInEvent(testListOfPredicates)
+ }
+
+ def 'Get cm notification subscription ncmp out event'() {
+ given: 'a subscription details map'
+ def testSubscriptionDetailsMap = [:]
+ when: 'method to create cm notification subscription ncmp out event is called with the following parameters'
+ objectUnderTest.toCmNotificationSubscriptionNcmpOutEvent("test-id", testSubscriptionDetailsMap)
+ then: 'the parameters is delegated to the correct ncmp out event mapper method'
+ 1 * mockCmNotificationNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEvent("test-id",
+ testSubscriptionDetailsMap)
+ }
+
+ def 'Get cm notification subscription ncmp out event for a rejected request'() {
+ given: 'a list of target filters'
+ def testRejectedTargetFilters = []
+ when: 'method to create cm notification subscription ncmp out event is called with the following parameters'
+ objectUnderTest.toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+ "test-id", testRejectedTargetFilters)
+ then: 'the parameters is delegated to the correct ncmp out event mapper method'
+ 1 * mockCmNotificationNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+ "test-id", testRejectedTargetFilters)
+ }
+}
import io.cloudevents.CloudEvent
import io.cloudevents.core.builder.CloudEventBuilder
import org.apache.kafka.clients.consumer.ConsumerRecord
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.consumer.CmNotificationSubscriptionNcmpInEventConsumer
import org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionHandlerService
import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec
import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent
def loggingEvent = getLoggingEvent()
assert loggingEvent.level == Level.INFO
and: 'the log indicates the task completed successfully'
- assert loggingEvent.formattedMessage == 'Subscription for source some-resource with subscription id cm-subscription-001 ...'
+ assert loggingEvent.formattedMessage == 'Subscription for source some-resource with subscription id test-id ...'
and: 'the subscription handler service is called once'
1 * mockCmNotificationSubscriptionHandlerService.processSubscriptionCreateRequest(_)
}
import com.fasterxml.jackson.databind.ObjectMapper
import io.cloudevents.CloudEvent
import org.onap.cps.events.EventsPublisher
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionNcmpOutEventProducer
import org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper
import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent
import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.Data
def mockEventsPublisher = Mock(EventsPublisher)
def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
- def mockCmNotificationSubscriptionCache = Mock(Map<String, Map<String, DmiCmNotificationSubscriptionDetails>>)
- def mockCmNotificationSubscriptionNcmpOutEventMapper = Mock(CmNotificationSubscriptionNcmpOutEventMapper)
+ def mockCmNotificationSubscriptionMappersHandler = Mock(CmNotificationSubscriptionMappersHandler)
+ def mockDmiCmNotificationSubscriptionCacheHandler = Mock(DmiCmNotificationSubscriptionCacheHandler)
- def objectUnderTest = new CmNotificationSubscriptionNcmpOutEventProducer(mockEventsPublisher, jsonObjectMapper, mockCmNotificationSubscriptionCache, mockCmNotificationSubscriptionNcmpOutEventMapper)
+ def objectUnderTest = new CmNotificationSubscriptionNcmpOutEventProducer(mockEventsPublisher, jsonObjectMapper,
+ mockCmNotificationSubscriptionMappersHandler, mockDmiCmNotificationSubscriptionCacheHandler)
def 'Create and #scenario Cm Notification Subscription NCMP out event'() {
given: 'a cm subscription response for the client'
assert CloudEventMapper.toTargetEvent(cmNotificationSubscriptionNcmpOutEventAsCloudEvent, CmNotificationSubscriptionNcmpOutEvent) == cmNotificationSubscriptionNcmpOutEvent
}
}
+ then: 'the cache handler is called once to remove accepted and rejected entries in cache'
+ 1 * mockDmiCmNotificationSubscriptionCacheHandler.removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries(subscriptionId)
}
import io.cloudevents.core.builder.CloudEventBuilder
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails
import org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionPersistenceService
import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
assert testCache.containsKey(subscriptionId)
}
+ def 'Get cache entry via subscription id'() {
+ given: 'the cache contains value for some-id'
+ testCache.put('some-id',[:])
+ when: 'the get method is called'
+ def result = objectUnderTest.get('some-id')
+ then: 'correct value is returned as expected'
+ assert result == [:]
+ }
+
+ def 'Remove accepted and rejected entries from cache via subscription id'() {
+ given: 'a map as the value for cache entry for some-id'
+ def testMap = [:]
+ testMap.put("dmi-1",
+ new DmiCmNotificationSubscriptionDetails([],CmNotificationSubscriptionStatus.ACCEPTED))
+ testMap.put("dmi-2",
+ new DmiCmNotificationSubscriptionDetails([],CmNotificationSubscriptionStatus.REJECTED))
+ testMap.put("dmi-3",
+ new DmiCmNotificationSubscriptionDetails([],CmNotificationSubscriptionStatus.PENDING))
+ testCache.put("test-id", testMap)
+ assert testCache.get("test-id").size() == 3
+ when: 'the method to remove accepted and rejected entries for test-id is called'
+ objectUnderTest.removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries("test-id")
+ then: 'all entries with status accepted/rejected are no longer present for test-id'
+ testCache.get("test-id").each { key, testResultMap ->
+ assert testResultMap.cmNotificationSubscriptionStatus != CmNotificationSubscriptionStatus.ACCEPTED
+ || testResultMap.cmNotificationSubscriptionStatus != CmNotificationSubscriptionStatus.REJECTED
+ }
+ and: 'the size of the map for cache entry test-id is as expected'
+ assert testCache.get("test-id").size() == 1
+ }
+
def 'Create map for DMI cm notification subscription per DMI service name'() {
given: 'list of predicates from the create subscription event'
def predicates = cmNotificationSubscriptionNcmpInEvent.getData().getPredicates()
when: 'subscription is persisted in database'
objectUnderTest.persistIntoDatabasePerDmi(subscriptionId,'dmi-1')
then: 'persistence service is called the correct number of times per dmi'
- 4 * mockCmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(_,_,_,subscriptionId)
+ 4 * mockCmNotificationSubscriptionPersistenceService.addCmNotificationSubscription(_,_,_,subscriptionId)
}
def setUpTestEvent(){
package org.onap.cps.ncmp.api.impl.events.cmsubscription.service
import com.fasterxml.jackson.databind.ObjectMapper
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionNcmpOutEventProducer
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionDelta
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionEventsHandler
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionMappersHandler
import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionDmiInEventMapper
import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails
import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent
+import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent
import org.onap.cps.ncmp.utils.TestUtils
import org.onap.cps.utils.JsonObjectMapper
import spock.lang.Specification
def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
def mockCmNotificationSubscriptionPersistenceService = Mock(CmNotificationSubscriptionPersistenceService);
- def mockCmNotificationSubscriptionNcmpOutEventMapper = Mock(CmNotificationSubscriptionNcmpOutEventMapper);
- def mockCmNotificationSubscriptionNcmpOutEventProducer = Mock(CmNotificationSubscriptionNcmpOutEventProducer);
+ def mockCmNotificationSubscriptionDelta = Mock(CmNotificationSubscriptionDelta);
+ def mockCmNotificationSubscriptionMappersHandler = Mock(CmNotificationSubscriptionMappersHandler);
+ def mockCmNotificationSubscriptionEventsHandler = Mock(CmNotificationSubscriptionEventsHandler);
def mockDmiCmNotificationSubscriptionCacheHandler = Mock(DmiCmNotificationSubscriptionCacheHandler);
- def objectUnderTest = new CmNotificationSubscriptionHandlerServiceImpl(mockCmNotificationSubscriptionPersistenceService, mockCmNotificationSubscriptionNcmpOutEventMapper, mockCmNotificationSubscriptionNcmpOutEventProducer, mockDmiCmNotificationSubscriptionCacheHandler)
+ def objectUnderTest = new CmNotificationSubscriptionHandlerServiceImpl(mockCmNotificationSubscriptionPersistenceService,
+ mockCmNotificationSubscriptionDelta, mockCmNotificationSubscriptionMappersHandler,
+ mockCmNotificationSubscriptionEventsHandler, mockDmiCmNotificationSubscriptionCacheHandler)
+
+ def testSubscriptionDetailsMap = ["dmi-1":new DmiCmNotificationSubscriptionDetails([], CmNotificationSubscriptionStatus.PENDING)]
+ def testListOfDeltaPredicates = []
def 'Consume valid and unique CmNotificationSubscriptionNcmpInEvent create message'() {
- given: 'a cmNotificationSubscriptionNcmp in event'
+ given: 'a cmNotificationSubscriptionNcmp in event with unique subscription id'
def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json')
def testEventConsumed = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class)
- mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId('cm-subscription-001') >> true
+ mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId("test-id") >> true
+ and: 'the cache handler returns for relevant subscription id'
+ 1 * mockDmiCmNotificationSubscriptionCacheHandler.get("test-id") >> testSubscriptionDetailsMap
+ and: 'the delta predicates is returned'
+ 1 * mockCmNotificationSubscriptionDelta.getDelta(_) >> testListOfDeltaPredicates
+ and: 'the DMI in event mapper returns cm notification subscription event'
+ def testDmiInEvent = new CmNotificationSubscriptionDmiInEvent()
+ 1 * mockCmNotificationSubscriptionMappersHandler
+ .toCmNotificationSubscriptionDmiInEvent(testListOfDeltaPredicates) >> testDmiInEvent
when: 'the valid and unique event is consumed'
objectUnderTest.processSubscriptionCreateRequest(testEventConsumed)
then: 'the subscription cache handler is called once'
- 1 * mockDmiCmNotificationSubscriptionCacheHandler.add('cm-subscription-001',_)
+ 1 * mockDmiCmNotificationSubscriptionCacheHandler.add('test-id',_)
+ and: 'the events handler method to publish DMI event is called correct number of times with the correct parameters'
+ testSubscriptionDetailsMap.size() * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent(
+ "test-id", "dmi-1", "subscriptionCreateRequest", testDmiInEvent)
}
def 'Consume valid and but non-unique CmNotificationSubscription create message'() {
given: 'a cmNotificationSubscriptionNcmp in event'
def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json')
def testEventConsumed = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class)
- mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId('cm-subscription-001') >> false
+ mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId('test-id') >> false
+ and: 'the NCMP out in event mapper returns an event for rejected request'
+ def testNcmpOutEvent = new CmNotificationSubscriptionNcmpOutEvent()
+ 1 * mockCmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+ "test-id",_) >> testNcmpOutEvent
when: 'the valid but non-unique event is consumed'
objectUnderTest.processSubscriptionCreateRequest(testEventConsumed)
- then: 'the subscription out event publisher is called once'
- 1 * mockCmNotificationSubscriptionNcmpOutEventProducer.publishCmNotificationSubscriptionNcmpOutEvent('cm-subscription-001', 'subscriptionCreateResponse', _, false)
+ then: 'the events handler method to publish DMI event is never called'
+ 0 * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent(_,_,_,_)
+ and: 'the events handler method to publish NCMP out event is called once'
+ 1 * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent(
+ 'test-id', 'subscriptionCreateResponse', testNcmpOutEvent, false)
}
}
def 'Add new subscriber to an ongoing cm notification subscription'() {
given: 'a valid cm subscription path query'
- def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y');
+ def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y')
and: 'a dataNode exists for the given cps path query'
mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions',
cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: cpsPathQuery, leaves: ['xpath': '/x/y','subscriptionIds': ['sub-1']])]
when: 'the method to add/update cm notification subscription is called'
- objectUnderTest.addOrUpdateCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1','/x/y', 'newSubId')
+ objectUnderTest.addCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1','/x/y', 'newSubId')
then: 'data service method to update list of subscribers is called once'
1 * mockCpsDataService.updateNodeLeaves(
'NCMP-Admin',
cpsPathQuery.formatted(datastoreName),
FetchDescendantsOption.OMIT_DESCENDANTS) >> []
when: 'the method to add/update cm notification subscription is called'
- objectUnderTest.addOrUpdateCmNotificationSubscription(datastoreType, 'ch-1','/x/y', 'newSubId')
+ objectUnderTest.addCmNotificationSubscription(datastoreType, 'ch-1','/x/y', 'newSubId')
then: 'data service method to update list of subscribers is called once with the correct parameters'
1 * mockCpsDataService.saveData(
'NCMP-Admin',
'passthrough_running' | DatastoreType.PASSTHROUGH_RUNNING || "ncmp-datastore:passthrough-running"
'passthrough_operational' | DatastoreType.PASSTHROUGH_OPERATIONAL || "ncmp-datastore:passthrough-operational"
}
+
+ def 'Remove subscriber from a list of an ongoing cm notification subscription'() {
+ given: 'a subscription exists when queried'
+ def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y')
+ mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions',
+ cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: cpsPathQuery, leaves: ['xpath': '/x/y','subscriptionIds': ['sub-1', 'sub-2']])]
+ when: 'the subscriber is removed'
+ objectUnderTest.removeCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1', '/x/y', 'sub-1')
+ then: 'the list of subscribers is updated'
+ 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'cm-data-subscriptions',
+ '/datastores/datastore[@name=\'ncmp-datastore:passthrough-running\']/cm-handles/cm-handle[@id=\'ch-1\']/filters',
+ '{"filter":[{"xpath":"/x/y","subscriptionIds":["sub-2"]}]}', _)
+ }
+
+ def 'Removing ongoing subscription with no subscribers'(){
+ given: 'a subscription exists when queried but has no subscribers'
+ def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y')
+ mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions',
+ cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: cpsPathQuery, leaves: ['xpath': '/x/y','subscriptionIds': []])]
+ when: 'a an ongoing subscription is refreshed'
+ objectUnderTest.removeCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1', '/x/y', 'sub-1')
+ then: 'the subscription with empty subscriber list is removed'
+ 1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'cm-data-subscriptions',
+ '/datastores/datastore[@name=\'ncmp-datastore:passthrough-running\']/cm-handles/cm-handle[@id=\'ch-1\']/filters/filter[@xpath=\'/x/y\']',
+ _)
+ }
}
\ No newline at end of file
package org.onap.cps.ncmp.api.impl.inventory
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+
import com.fasterxml.jackson.databind.ObjectMapper
import org.onap.cps.api.CpsAnchorService
import org.onap.cps.api.CpsDataService
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
import org.onap.cps.spi.CascadeDeleteAllowed
import org.onap.cps.spi.FetchDescendantsOption
+import org.onap.cps.ncmp.api.impl.exception.NoAlternateIdParentFoundException
import org.onap.cps.spi.exceptions.DataNodeNotFoundException
import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.ModuleDefinition
import org.onap.cps.utils.JsonObjectMapper
import spock.lang.Shared
import spock.lang.Specification
-
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
-import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
-import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
-
class InventoryPersistenceImplSpec extends Specification {
def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
assert objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id') == new DataNode()
}
+ def 'Find cm handle parent data node using alternate ids'() {
+ given: 'cm handle in the registry with alternateId /a/b'
+ def matchingCpsPath = "/dmi-registry/cm-handles[@alternate-id='/a/b']"
+ mockCmHandleQueries.queryNcmpRegistryByCpsPath(matchingCpsPath, OMIT_DESCENDANTS) >> [new DataNode()]
+ and: 'no other cm handle'
+ mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
+ expect: 'querying for alternate id a matching result found'
+ assert objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(alternateId, '/') != null
+ where: 'the following parameters are used'
+ scenario | alternateId
+ 'exact match' | '/a/b'
+ 'exact match with trailing separator' | '/a/b/'
+ 'child match' | '/a/b/c'
+ }
+
+ def 'Find cm handle parent data node using alternate ids mismatches'() {
+ given: 'cm handle in the registry with alternateId'
+ def matchingCpsPath = "/dmi-registry/cm-handles[@alternate-id='${cpsPath}]"
+ mockCmHandleQueries.queryNcmpRegistryByCpsPath(matchingCpsPath, OMIT_DESCENDANTS) >> [new DataNode()]
+ and: 'no other cm handle'
+ mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
+ when: 'attempt to find alternateId'
+ objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(alternateId, '/')
+ then: 'no alternate id found exception thrown'
+ def thrown = thrown(NoAlternateIdParentFoundException)
+ and: 'the exception has the relevant details from the error response'
+ assert thrown.message == 'No matching (parent) cm handle found using alternate ids'
+ assert thrown.details == 'cannot find a datanode with alternate id ' + alternateId
+ where: 'the following parameters are used'
+ scenario | alternateId | cpsPath
+ 'no match for parent only' | '/a' | '/a/b'
+ 'no match at all' | '/x/y/z' | '/a/b'
+ 'no match with trailing separator' | '/c/d/' | '/c/d'
+ }
+
def 'Attempt to get non existing cm handle data node by alternate id'() {
given: 'query service is invoked and returns empty collection of data nodes'
mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);
}
-
}
import io.cloudevents.kafka.impl.KafkaHeaders
import org.apache.kafka.clients.consumer.KafkaConsumer
import org.onap.cps.events.EventsPublisher
+import org.onap.cps.ncmp.api.NcmpResponseStatus
import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext
import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
import org.spockframework.spring.SpringBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
+
import java.time.Duration
+import java.util.concurrent.TimeoutException
import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent
@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, ObjectMapper])
class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec {
- def cloudEventKafkaConsumer = new KafkaConsumer<>(eventConsumerConfigProperties('test', CloudEventDeserializer))
def static clientTopic = 'my-topic-name'
def static dataOperationType = 'org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent'
def 'Process per data operation request with non-ready, non-existing cm handle and publish event to client specified topic'() {
given: 'consumer subscribing to client topic'
+ def cloudEventKafkaConsumer = new KafkaConsumer<>(eventConsumerConfigProperties('test-1', CloudEventDeserializer))
cloudEventKafkaConsumer.subscribe([clientTopic])
and: 'data operation request having non-ready and non-existing cm handle ids'
def dataOperationRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json')
when: 'data operation request is processed'
ResourceDataOperationRequestUtils.processPerDefinitionInDataOperationsRequest(clientTopic, 'request-id', dataOperationRequest, yangModelCmHandles)
and: 'subscribed client specified topic is polled and first record is selected'
- def consumerRecordOut = cloudEventKafkaConsumer.poll(Duration.ofMillis(1500))[0]
+ def consumerRecordOut = cloudEventKafkaConsumer.poll(Duration.ofMillis(1500)).last()
then: 'verify cloud compliant headers'
def consumerRecordOutHeaders = consumerRecordOut.headers()
assert KafkaHeaders.getParsedKafkaHeader(consumerRecordOutHeaders, 'ce_id') != null
and: 'data operation response event response size is 3'
dataOperationResponseEvent.data.responses.size() == 3
and: 'verify published data operation response as json string'
- def dataOperationResponseEventJson = TestUtils.getResourceFileContent('dataOperationResponseEvent.json')
+ def dataOperationResponseEventJson = TestUtils.getResourceFileContent('dataOperationResponseEvent.json')
jsonObjectMapper.asJsonString(dataOperationResponseEvent.data.responses) == dataOperationResponseEventJson
}
+ def 'Publish error response for entire data operations request when async task fails'() {
+ given: 'consumer subscribing to client topic'
+ def cloudEventKafkaConsumer = new KafkaConsumer<>(eventConsumerConfigProperties(consumerGroupId, CloudEventDeserializer))
+ cloudEventKafkaConsumer.subscribe([clientTopic])
+ and: 'data operation request having non-ready and non-existing cm handle ids'
+ def dataOperationRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json')
+ def dataOperationRequest = jsonObjectMapper.convertJsonString(dataOperationRequestJsonData, DataOperationRequest.class)
+ when: 'an error occurs for the entire data operations request'
+ ResourceDataOperationRequestUtils.handleAsyncTaskCompletionForDataOperationsRequest(clientTopic, 'request-id', dataOperationRequest, exceptionThrown)
+ and: 'subscribed client specified topic is polled and first record is selected'
+ def consumerRecordOut = cloudEventKafkaConsumer.poll(Duration.ofMillis(1500)).last()
+ def dataOperationResponseEvent = toTargetEvent(consumerRecordOut.value(), DataOperationEvent.class)
+ then: 'data operation response event response size is 3'
+ dataOperationResponseEvent.data.responses.size() == 3
+ and: 'all 3 have the expected error code'
+ dataOperationResponseEvent.data.responses.each {
+ assert it.statusCode == errorReportedToClientTopic.code
+ }
+ where:
+ scenario | exceptionThrown | consumerGroupId || errorReportedToClientTopic
+ 'task timed out' | new TimeoutException() | 'test-2' || NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING
+ 'unspecified error' | new RuntimeException() | 'test-3' || NcmpResponseStatus.UNKNOWN_ERROR
+ }
+
static def getYangModelCmHandles() {
def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
def readyState = new CompositeStateBuilder().withCmHandleState(CmHandleState.READY).withLastUpdatedTimeNow().build()
ncmp:
dmi:
+ httpclient:
+ connectionTimeoutInSeconds: 180
auth:
username: some-user
password: some-password
{
"data": {
- "subscriptionId": "cm-subscription-001",
+ "subscriptionId": "test-id",
"predicates": [
{
"targetFilter": ["ch1","ch2"],
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN);
static OffsetDateTime toOffsetDateTime(String datetTimestampAsString) {
- return ! StringUtils.hasLength(datetTimestampAsString)
- ? null : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER);
+ return !StringUtils.hasLength(datetTimestampAsString) ? null
+ : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER);
}
static String toString(OffsetDateTime offsetDateTime) {
return offsetDateTime != null ? ISO_TIMESTAMP_FORMATTER.format(offsetDateTime) : null;
}
-}
+}
\ No newline at end of file
<parent>\r
<groupId>org.onap.cps</groupId>\r
<artifactId>cps-parent</artifactId>\r
- <version>3.4.8-SNAPSHOT</version>\r
+ <version>3.4.9-SNAPSHOT</version>\r
<relativePath>../cps-parent/pom.xml</relativePath>\r
</parent>\r
\r
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020-2023 Nordix Foundation.
+ * Copyright (C) 2020-2024 Nordix Foundation.
* Modifications Copyright (C) 2020-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2022 TechMahindra Ltd.
import jakarta.transaction.Transactional;
import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.spi.entities.AnchorEntity;
import org.onap.cps.spi.entities.DataspaceEntity;
import org.onap.cps.spi.entities.SchemaSetEntity;
-import org.onap.cps.spi.entities.YangResourceModuleReference;
import org.onap.cps.spi.exceptions.AlreadyDefinedException;
import org.onap.cps.spi.exceptions.DataspaceInUseException;
-import org.onap.cps.spi.exceptions.DataspaceNotFoundException;
-import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException;
import org.onap.cps.spi.model.Anchor;
import org.onap.cps.spi.model.Dataspace;
import org.onap.cps.spi.repository.AnchorRepository;
import org.onap.cps.spi.repository.DataspaceRepository;
import org.onap.cps.spi.repository.SchemaSetRepository;
-import org.onap.cps.spi.repository.YangResourceRepository;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Component;
private final DataspaceRepository dataspaceRepository;
private final AnchorRepository anchorRepository;
private final SchemaSetRepository schemaSetRepository;
- private final YangResourceRepository yangResourceRepository;
@Override
public void createDataspace(final String dataspaceName) {
}
@Override
- public Collection<Anchor> queryAnchors(final String dataspaceName, final Collection<String> inputModuleNames) {
- try {
- validateDataspaceAndModuleNames(dataspaceName, inputModuleNames);
- } catch (DataspaceNotFoundException | ModuleNamesNotFoundException e) {
- log.info("Module search encountered unknown dataspace or modulename, treating this as nothing found");
- return Collections.emptySet();
- }
-
+ public Collection<String> queryAnchorNames(final String dataspaceName, final Collection<String> inputModuleNames) {
final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
- final Collection<AnchorEntity> anchorEntities = anchorRepository
- .getAnchorsByDataspaceIdAndModuleNames(dataspaceEntity.getId(), inputModuleNames, inputModuleNames.size());
- return anchorEntities.stream().map(CpsAdminPersistenceServiceImpl::toAnchor).collect(Collectors.toSet());
+ return anchorRepository.getAnchorNamesByDataspaceIdAndModuleNames(dataspaceEntity.getId(), inputModuleNames,
+ inputModuleNames.size());
}
@Override
private static Dataspace toDataspace(final DataspaceEntity dataspaceEntity) {
return Dataspace.builder().name(dataspaceEntity.getName()).build();
}
-
- private void validateDataspaceAndModuleNames(final String dataspaceName,
- final Collection<String> inputModuleNames) {
- final Collection<String> retrievedModuleReferences =
- yangResourceRepository.findAllModuleReferencesByDataspaceAndModuleNames(dataspaceName, inputModuleNames)
- .stream().map(YangResourceModuleReference::getModuleName)
- .collect(Collectors.toList());
- if (retrievedModuleReferences.isEmpty()) {
- verifyDataspaceName(dataspaceName);
- }
- if (inputModuleNames.size() > retrievedModuleReferences.size()) {
- final List<String> unknownModules = inputModuleNames.stream()
- .filter(moduleName -> !retrievedModuleReferences.contains(moduleName))
- .collect(Collectors.toList());
- throw new ModuleNamesNotFoundException(dataspaceName, unknownModules);
- }
- }
-
- private void verifyDataspaceName(final String dataspaceName) {
- dataspaceRepository.getByName(dataspaceName);
- }
}
@Query(value = """
SELECT
- anchor.*
+ anchor.name
FROM
yang_resource
JOIN schema_set_yang_resources ON schema_set_yang_resources.yang_resource_id = yang_resource.id
HAVING
COUNT(DISTINCT module_name) = :sizeOfModuleNames
""", nativeQuery = true)
- Collection<AnchorEntity> getAnchorsByDataspaceIdAndModuleNames(@Param("dataspaceId") int dataspaceId,
- @Param("moduleNames") String[] moduleNames,
- @Param("sizeOfModuleNames") int sizeOfModuleNames);
+ Collection<String> getAnchorNamesByDataspaceIdAndModuleNames(@Param("dataspaceId") int dataspaceId,
+ @Param("moduleNames") String[] moduleNames,
+ @Param("sizeOfModuleNames") int sizeOfModuleNames);
- default Collection<AnchorEntity> getAnchorsByDataspaceIdAndModuleNames(final int dataspaceId,
- final Collection<String> moduleNames,
- final int sizeOfModuleNames) {
+ default Collection<String> getAnchorNamesByDataspaceIdAndModuleNames(final int dataspaceId,
+ final Collection<String> moduleNames,
+ final int sizeOfModuleNames) {
final String[] moduleNamesArray = moduleNames.toArray(new String[0]);
- return getAnchorsByDataspaceIdAndModuleNames(dataspaceId, moduleNamesArray, sizeOfModuleNames);
+ return getAnchorNamesByDataspaceIdAndModuleNames(dataspaceId, moduleNamesArray, sizeOfModuleNames);
}
@Modifying
@Param("dataspaceName") String dataspaceName, @Param("anchorName") String anchorName,
@Param("moduleName") String moduleName, @Param("revision") String revision);
- @Query(value = """
- SELECT DISTINCT
- yang_resource.*
- FROM
- dataspace
- JOIN schema_set ON schema_set.dataspace_id = dataspace.id
- JOIN schema_set_yang_resources ON schema_set_yang_resources.schema_set_id = schema_set.id
- JOIN yang_resource ON yang_resource.id = schema_set_yang_resources.yang_resource_id
- WHERE
- dataspace.name = :dataspaceName
- AND yang_resource.module_name = ANY ( :moduleNames )
- """, nativeQuery = true)
- Set<YangResourceModuleReference> findAllModuleReferencesByDataspaceAndModuleNames(
- @Param("dataspaceName") String dataspaceName, @Param("moduleNames") String[] moduleNames);
-
- default Set<YangResourceModuleReference> findAllModuleReferencesByDataspaceAndModuleNames(
- final String dataspaceName, final Collection<String> moduleNames) {
- return findAllModuleReferencesByDataspaceAndModuleNames(dataspaceName, moduleNames.toArray(new String[0]));
- }
-
@Modifying
@Query(value = "DELETE FROM schema_set_yang_resources WHERE schema_set_id = :schemaSetId", nativeQuery = true)
void deleteSchemaSetYangResourceForSchemaSetId(@Param("schemaSetId") int schemaSetId);
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<artifactId>cps-service</artifactId>
<dependencies>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
+ <dependency>
+ <groupId>io.cloudevents</groupId>
+ <artifactId>cloudevents-json-jackson</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.cloudevents</groupId>
+ <artifactId>cloudevents-kafka</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.cloudevents</groupId>
+ <artifactId>cloudevents-spring</artifactId>
+ </dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
package org.onap.cps.api.impl;
import java.util.Collection;
-import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.onap.cps.api.CpsAnchorService;
import org.onap.cps.spi.CpsAdminPersistenceService;
@Override
public Collection<String> queryAnchorNames(final String dataspaceName, final Collection<String> moduleNames) {
cpsValidator.validateNameCharacters(dataspaceName);
- final Collection<Anchor> anchors = cpsAdminPersistenceService.queryAnchors(dataspaceName, moduleNames);
- return anchors.stream().map(Anchor::getName).collect(Collectors.toList());
+ return cpsAdminPersistenceService.queryAnchorNames(dataspaceName, moduleNames);
}
@Override
* Copyright (C) 2021-2024 Nordix Foundation
* Modifications Copyright (C) 2020-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
- * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
+ * Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
* Modifications Copyright (C) 2022 Deutsche Telekom AG
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
import org.onap.cps.api.CpsDataService;
import org.onap.cps.api.CpsDeltaService;
import org.onap.cps.cpspath.parser.CpsPathUtil;
+import org.onap.cps.events.CpsDataUpdateEventsService;
+import org.onap.cps.events.model.Data.Operation;
import org.onap.cps.spi.CpsDataPersistenceService;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.exceptions.DataValidationException;
private static final long DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS = 300L;
private final CpsDataPersistenceService cpsDataPersistenceService;
+ private final CpsDataUpdateEventsService cpsDataUpdateEventsService;
private final CpsAnchorService cpsAnchorService;
+
private final CpsValidator cpsValidator;
private final YangParser yangParser;
private final CpsDeltaService cpsDeltaService;
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> dataNodes = buildDataNodes(anchor, ROOT_NODE_XPATH, nodeData, contentType);
cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes);
+ sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.CREATE, observedTimestamp);
}
@Override
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> dataNodes = buildDataNodes(anchor, parentNodeXpath, nodeData, contentType);
cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes);
+ sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.CREATE, observedTimestamp);
}
@Override
cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
listElementDataNodeCollection);
}
+ sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
}
@Override
final Map<String, Map<String, Serializable>> xpathToUpdatedLeaves = dataNodesInPatch.stream()
.collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves));
cpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, xpathToUpdatedLeaves);
+ sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
}
@Override
for (final DataNode dataNodeUpdate : dataNodeUpdates) {
processDataNodeUpdate(anchor, dataNodeUpdate);
}
+ sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
}
@Override
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> dataNodes = buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON);
cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
+ sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
}
@Override
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> dataNodes = buildDataNodes(anchor, nodesJsonData);
cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
+ nodesJsonData.keySet().forEach(nodeXpath ->
+ sendDataUpdatedEvent(anchor, nodeXpath, Operation.UPDATE, observedTimestamp));
}
@Override
public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
final Collection<DataNode> dataNodes, final OffsetDateTime observedTimestamp) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
+ final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, dataNodes);
+ sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
}
@Override
final OffsetDateTime observedTimestamp) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
cpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath);
+ final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+ sendDataUpdatedEvent(anchor, dataNodeXpath, Operation.DELETE, observedTimestamp);
}
@Override
final Collection<String> dataNodeXpaths, final OffsetDateTime observedTimestamp) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, dataNodeXpaths);
+ final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+ dataNodeXpaths.forEach(dataNodeXpath ->
+ sendDataUpdatedEvent(anchor, dataNodeXpath, Operation.DELETE, observedTimestamp));
}
+
@Override
@Timed(value = "cps.data.service.datanode.delete.anchor",
description = "Time taken to delete all datanodes for an anchor")
final OffsetDateTime observedTimestamp) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName);
+ final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+ sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.DELETE, observedTimestamp);
}
@Override
cpsValidator.validateNameCharacters(dataspaceName);
cpsValidator.validateNameCharacters(anchorNames);
cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorNames);
+ for (final Anchor anchor : cpsAnchorService.getAnchors(dataspaceName, anchorNames)) {
+ sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.DELETE, observedTimestamp);
+ }
}
@Override
final OffsetDateTime observedTimestamp) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
cpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, listNodeXpath);
+ final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+ sendDataUpdatedEvent(anchor, listNodeXpath, Operation.DELETE, observedTimestamp);
}
private Collection<DataNode> buildDataNodes(final Anchor anchor, final Map<String, String> nodesJsonData) {
}
}
+ private void sendDataUpdatedEvent(final Anchor anchor, final String xpath,
+ final Operation operation, final OffsetDateTime observedTimestamp) {
+ try {
+ cpsDataUpdateEventsService.publishCpsDataUpdateEvent(anchor, xpath, operation, observedTimestamp);
+ } catch (final Exception exception) {
+ log.error("Failed to send message to notification service", exception);
+ }
+ }
}
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 TechMahindra Ltd.
+ * Copyright (C) 2024 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.events;
+
+import io.cloudevents.CloudEvent;
+import io.micrometer.core.annotation.Timed;
+import java.time.OffsetDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.events.model.CpsDataUpdatedEvent;
+import org.onap.cps.events.model.Data;
+import org.onap.cps.events.model.Data.Operation;
+import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.utils.DateTimeUtility;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CpsDataUpdateEventsService {
+
+ private final EventsPublisher<CpsDataUpdatedEvent> eventsPublisher;
+
+ @Value("${app.cps.data-updated.topic:cps-data-updated-events}")
+ private String topicName;
+
+ @Value("${notification.enabled:false}")
+ private boolean notificationsEnabled;
+
+ /**
+ * Publish the cps data update event with header to the public topic.
+ *
+ * @param anchor Anchor of the updated data
+ * @param xpath xpath of the updated data
+ * @param operation operation performed on the data
+ * @param observedTimestamp timestamp when data was updated.
+ */
+ @Timed(value = "cps.dataupdate.events.publish", description = "Time taken to publish Data Update event")
+ public void publishCpsDataUpdateEvent(final Anchor anchor, final String xpath,
+ final Operation operation, final OffsetDateTime observedTimestamp) {
+ if (notificationsEnabled) {
+ final CpsDataUpdatedEvent cpsDataUpdatedEvent = createCpsDataUpdatedEvent(anchor,
+ observedTimestamp, xpath, operation);
+ final String updateEventId = anchor.getDataspaceName() + ":" + anchor.getName();
+ final Map<String, String> extensions = createUpdateEventExtensions(updateEventId);
+ final CloudEvent cpsDataUpdatedEventAsCloudEvent =
+ CpsEvent.builder().type(CpsDataUpdatedEvent.class.getTypeName()).data(cpsDataUpdatedEvent)
+ .extensions(extensions).build().asCloudEvent();
+ eventsPublisher.publishCloudEvent(topicName, updateEventId, cpsDataUpdatedEventAsCloudEvent);
+ } else {
+ log.debug("Notifications disabled.");
+ }
+ }
+
+ private CpsDataUpdatedEvent createCpsDataUpdatedEvent(final Anchor anchor, final OffsetDateTime observedTimestamp,
+ final String xpath,
+ final Operation rootNodeOperation) {
+ final CpsDataUpdatedEvent cpsDataUpdatedEvent = new CpsDataUpdatedEvent();
+ final Data updateEventData = new Data();
+ updateEventData.setObservedTimestamp(DateTimeUtility.toString(observedTimestamp));
+ updateEventData.setDataspaceName(anchor.getDataspaceName());
+ updateEventData.setAnchorName(anchor.getName());
+ updateEventData.setSchemaSetName(anchor.getSchemaSetName());
+ updateEventData.setOperation(getRootNodeOperation(xpath, rootNodeOperation));
+ updateEventData.setXpath(xpath);
+ cpsDataUpdatedEvent.setData(updateEventData);
+ return cpsDataUpdatedEvent;
+ }
+
+ private Map<String, String> createUpdateEventExtensions(final String eventKey) {
+ final Map<String, String> extensions = new HashMap<>();
+ extensions.put("correlationid", eventKey);
+ return extensions;
+ }
+
+ private Operation getRootNodeOperation(final String xpath, final Operation operation) {
+ return isRootXpath(xpath) || isRootContainerNodeXpath(xpath) ? operation : Operation.UPDATE;
+ }
+
+ private static boolean isRootXpath(final String xpath) {
+ return "/".equals(xpath) || "".equals(xpath);
+ }
+
+ private static boolean isRootContainerNodeXpath(final String xpath) {
+ return 0 == xpath.lastIndexOf('/');
+ }
+}
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 TechMahindra Ltd.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.events;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.builder.CloudEventBuilder;
+import java.net.URI;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Builder;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.cps.utils.JsonObjectMapper;
+
+@Builder
+public class CpsEvent {
+
+ private Object data;
+ private Map<String, String> extensions;
+ private String type;
+ @Builder.Default
+ private static final String CLOUD_EVENT_SPEC_VERSION_V1 = "1.0.0";
+ @Builder.Default
+ private static final String CLOUD_EVENT_SOURCE = "CPS";
+
+ private final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper());
+
+ /**
+ * Creates ncmp cloud event with provided attributes.
+ *
+ * @return Cloud Event
+ */
+
+ public CloudEvent asCloudEvent() {
+ final CloudEventBuilder cloudEventBuilder = io.cloudevents.core.builder
+ .CloudEventBuilder.v1()
+ .withId(UUID.randomUUID().toString())
+ .withSource(URI.create(CLOUD_EVENT_SOURCE))
+ .withType(type)
+ .withDataSchema(URI.create("urn:cps:" + type + ":" + CLOUD_EVENT_SPEC_VERSION_V1))
+ .withData(jsonObjectMapper.asJsonBytes(data));
+ extensions.entrySet().stream()
+ .filter(extensionEntry -> StringUtils.isNotBlank(extensionEntry.getValue()))
+ .forEach(extensionEntry ->
+ cloudEventBuilder.withExtension(extensionEntry.getKey(), extensionEntry.getValue()));
+ return cloudEventBuilder.build();
+ }
+}
* @return a collection of anchor names in the given dataspace. The schema set for each anchor must include all the
* given module names
*/
- Collection<Anchor> queryAnchors(String dataspaceName, Collection<String> moduleNames);
+ Collection<String> queryAnchorNames(String dataspaceName, Collection<String> moduleNames);
/**
* Get an anchor in the given dataspace using the anchor name.
/*
* ============LICENSE_START=======================================================
* Copyright (c) 2021 Bell Canada.
+ * Copyright (C) 2024 Nordix Foundation.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
-import org.springframework.util.StringUtils;
public interface DateTimeUtility {
String ISO_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN);
- static OffsetDateTime toOffsetDateTime(String datetTimestampAsString) {
- return ! StringUtils.hasLength(datetTimestampAsString)
- ? null : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER);
- }
-
static String toString(OffsetDateTime offsetDateTime) {
return offsetDateTime != null ? ISO_TIMESTAMP_FORMATTER.format(offsetDateTime) : null;
}
def 'Query all anchor identifiers for a dataspace and module names.'() {
given: 'the persistence service is invoked with the expected parameters and returns a list of anchors'
- mockCpsAdminPersistenceService.queryAnchors('some-dataspace-name', ['some-module-name']) >> [new Anchor(name:'some-anchor-identifier')]
+ mockCpsAdminPersistenceService.queryAnchorNames('some-dataspace-name', ['some-module-name']) >> ['some-anchor-identifier']
when: 'query anchor names is called using a dataspace name and module name'
def result = objectUnderTest.queryAnchorNames('some-dataspace-name', ['some-module-name'])
then: 'get anchor identifiers returns the same anchor identifier returned by the persistence layer'
def 'Query all anchors with Module Names Not Found Exception in persistence layer.'() {
given: 'the persistence layer throws a Module Names Not Found Exception'
def originalException = new ModuleNamesNotFoundException('exception-ds', ['m1', 'm2'])
- mockCpsAdminPersistenceService.queryAnchors(*_) >> { throw originalException}
+ mockCpsAdminPersistenceService.queryAnchorNames(*_) >> { throw originalException}
when: 'attempt query anchors'
objectUnderTest.queryAnchorNames('some-dataspace-name', [])
then: 'the same exception is thrown (up)'
* Copyright (C) 2021-2024 Nordix Foundation
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2021-2022 Bell Canada.
- * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
+ * Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
* Modifications Copyright (C) 2022 Deutsche Telekom AG
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
package org.onap.cps.api.impl
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.core.read.ListAppender
import org.onap.cps.TestUtils
import org.onap.cps.api.CpsAnchorService
import org.onap.cps.api.CpsDeltaService
+import org.onap.cps.events.CpsDataUpdateEventsService
import org.onap.cps.spi.CpsDataPersistenceService
import org.onap.cps.spi.FetchDescendantsOption
import org.onap.cps.spi.exceptions.ConcurrencyException
import org.onap.cps.utils.YangParserHelper
import org.onap.cps.yang.YangTextSchemaSourceSet
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
+import org.slf4j.LoggerFactory
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
import spock.lang.Shared
import spock.lang.Specification
import java.time.OffsetDateTime
def mockCpsValidator = Mock(CpsValidator)
def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache)
def mockCpsDeltaService = Mock(CpsDeltaService);
+ def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)
- def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)
+ def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)
+
+ def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class)
+ def loggingListAppender
+ def applicationContext = new AnnotationConfigApplicationContext()
def setup() {
mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor
mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
+ logger.setLevel(Level.DEBUG)
+ loggingListAppender = new ListAppender()
+ logger.addAppender(loggingListAppender)
+ loggingListAppender.start()
+ applicationContext.refresh()
+ }
+
+ void cleanup() {
+ ((Logger) LoggerFactory.getLogger(CpsDataServiceImpl.class)).detachAndStopAllAppenders()
+ applicationContext.close()
}
@Shared
1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
}
+ def 'Exception is thrown while publishing the notification.'(){
+ given: 'schema set for given anchor and dataspace references test-tree model'
+ setupSchemaSetMocks('test-tree.yang')
+ when: 'publisher set to throw an exception'
+ mockDataUpdateEventsService.publishCpsDataUpdateEvent(_, _, _, _) >> { throw new Exception("publishing failed")}
+ and: 'an update event is performed'
+ objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/', '{"test-tree": {"branch": []}}', observedTimestamp)
+ then: 'the exception is not bubbled up'
+ noExceptionThrown()
+ and: "the exception message is logged"
+ def logs = loggingListAppender.list.toString()
+ assert logs.contains('Failed to send message to notification service')
+ }
def setupSchemaSetMocks(String... yangResources) {
def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
* Copyright (C) 2021-2024 Nordix Foundation.\r
* Modifications Copyright (C) 2021-2022 Bell Canada.\r
* Modifications Copyright (C) 2021 Pantheon.tech\r
- * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.\r
+ * Modifications Copyright (C) 2022-2024 TechMahindra Ltd.\r
* ================================================================================\r
* Licensed under the Apache License, Version 2.0 (the "License");\r
* you may not use this file except in compliance with the License.\r
import org.onap.cps.TestUtils\r
import org.onap.cps.api.CpsAnchorService\r
import org.onap.cps.api.CpsDeltaService\r
+import org.onap.cps.events.CpsDataUpdateEventsService\r
import org.onap.cps.spi.CpsDataPersistenceService\r
import org.onap.cps.spi.CpsModulePersistenceService\r
import org.onap.cps.spi.model.Anchor\r
def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService,\r
mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)\r
\r
- def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)\r
+ def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)\r
+ def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)\r
\r
def dataspaceName = 'someDataspace'\r
def anchorName = 'someAnchor'\r
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 TechMahindra Ltd.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.events
+
+import static org.onap.cps.events.model.Data.Operation.CREATE
+import static org.onap.cps.events.model.Data.Operation.DELETE
+import static org.onap.cps.events.model.Data.Operation.UPDATE
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import io.cloudevents.CloudEvent
+import io.cloudevents.core.CloudEventUtils
+import io.cloudevents.jackson.PojoCloudEventDataMapper
+import org.onap.cps.events.model.CpsDataUpdatedEvent
+import org.onap.cps.events.model.Data
+import org.onap.cps.spi.model.Anchor
+import org.onap.cps.utils.JsonObjectMapper
+import org.springframework.test.context.ContextConfiguration
+import spock.lang.Specification
+
+import java.time.OffsetDateTime
+
+@ContextConfiguration(classes = [ObjectMapper, JsonObjectMapper])
+class CpsDataUpdateEventsServiceSpec extends Specification {
+ def mockEventsPublisher = Mock(EventsPublisher)
+ def notificationsEnabled = true
+ def objectMapper = new ObjectMapper();
+
+ def objectUnderTest = new CpsDataUpdateEventsService(mockEventsPublisher)
+
+ def 'Create and Publish cps update event where events are #scenario'() {
+ given: 'an anchor, operation and observed timestamp'
+ def anchor = new Anchor('anchor01', 'dataspace01', 'schema01');
+ def operation = operationInRequest
+ def observedTimestamp = OffsetDateTime.now()
+ and: 'notificationsEnabled is #notificationsEnabled and it will be true as default'
+ objectUnderTest.notificationsEnabled = true
+ when: 'service is called to publish data update event'
+ objectUnderTest.topicName = "cps-core-event"
+ objectUnderTest.publishCpsDataUpdateEvent(anchor, xpath, operation, observedTimestamp)
+ then: 'the event contains the required attributes'
+ 1 * mockEventsPublisher.publishCloudEvent('cps-core-event', 'dataspace01:anchor01', _) >> {
+ args ->
+ {
+ def cpsDataUpdatedEvent = (args[2] as CloudEvent)
+ assert cpsDataUpdatedEvent.getExtension('correlationid') == 'dataspace01:anchor01'
+ assert cpsDataUpdatedEvent.type == 'org.onap.cps.events.model.CpsDataUpdatedEvent'
+ assert cpsDataUpdatedEvent.source.toString() == 'CPS'
+ def actualEventOperation = CloudEventUtils.mapData(cpsDataUpdatedEvent, PojoCloudEventDataMapper.from(objectMapper, CpsDataUpdatedEvent.class)).getValue().data.operation
+ assert actualEventOperation == expectedOperation
+ }
+ }
+ where: 'the following values are used'
+ scenario | xpath | operationInRequest || expectedOperation
+ 'empty xpath' | '' | CREATE || CREATE
+ 'root xpath and create operation' | '/' | CREATE || CREATE
+ 'root xpath and update operation' | '/' | UPDATE || UPDATE
+ 'root xpath and delete operation' | '/' | DELETE || DELETE
+ 'not root xpath and update operation' | 'test' | UPDATE || UPDATE
+ 'root node xpath and create operation' | '/test' | CREATE || CREATE
+ 'non root node xpath and update operation' | '/test/path' | CREATE || UPDATE
+ 'non root node xpath and delete operation' | '/test/path' | DELETE || UPDATE
+ }
+
+ def 'publish cps update event when notification service is disabled'() {
+ given: 'an anchor, operation and observed timestamp'
+ def anchor = new Anchor('anchor01', 'dataspace01', 'schema01');
+ def operation = CREATE
+ def observedTimestamp = OffsetDateTime.now()
+ and: 'notificationsEnabled is false'
+ objectUnderTest.notificationsEnabled = false
+ when: 'service is called to publish data update event'
+ objectUnderTest.topicName = "cps-core-event"
+ objectUnderTest.publishCpsDataUpdateEvent(anchor, '/', operation, observedTimestamp)
+ then: 'the event contains the required attributes'
+ 0 * mockEventsPublisher.publishCloudEvent('cps-core-event', 'dataspace01:anchor01', _)
+ }
+
+ def 'publish cps update event when no timestamp provided'() {
+ given: 'an anchor, operation and null timestamp'
+ def anchor = new Anchor('anchor01', 'dataspace01', 'schema01');
+ def operation = CREATE
+ def observedTimestamp = null
+ and: 'notificationsEnabled is true'
+ objectUnderTest.notificationsEnabled = true
+ when: 'service is called to publish data update event'
+ objectUnderTest.topicName = "cps-core-event"
+ objectUnderTest.publishCpsDataUpdateEvent(anchor, '/', operation, observedTimestamp)
+ then: 'the event is published'
+ 1 * mockEventsPublisher.publishCloudEvent('cps-core-event', 'dataspace01:anchor01', _)
+ }
+}
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>dmi-plugin-demo-and-csit-stub</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
</parent>
<artifactId>dmi-plugin-demo-and-csit-stub-app</artifactId>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>dmi-plugin-demo-and-csit-stub</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
</parent>
<artifactId>dmi-plugin-demo-and-csit-stub-service</artifactId>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
post:
description: This request will be handled asynchronously using messaging to
the supplied topic. The rest response will be an acknowledge with a requestId
- to identify the relevant messages.
+ to identify the relevant messages. A maximum of 50 cm handles per operation
+ is supported.
operationId: executeDataOperationForCmHandles
parameters:
- allowReserved: true
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
schema:
$ref: '#/components/schemas/ErrorMessage'
description: Forbidden
+ "413":
+ content:
+ application/json:
+ example:
+ status: 413
+ message: Payload Too Large error message
+ details: Payload Too Large error details
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ description: The request is larger than the server is willing or able to
+ process
"500":
content:
application/json:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
content:
application/json:
example:
- status: 400 BAD_REQUEST
+ status: 400
message: Bad request error message
details: Bad request error details
schema:
schema:
$ref: '#/components/schemas/ErrorMessage'
description: The specified resource was not found
+ PayloadTooLarge:
+ content:
+ application/json:
+ example:
+ status: 413
+ message: Payload Too Large error message
+ details: Payload Too Large error details
+ schema:
+ $ref: '#/components/schemas/ErrorMessage'
+ description: The request is larger than the server is willing or able to process
schemas:
ErrorMessage:
properties:
type: string
targetIds:
items:
+ description: "targeted cm handles, maximum of 50 supported. If this limit\
+ \ is exceeded the request wil be refused."
example: "[\"da310eecdb8d44c2acc0ddaae01174b1\",\"c748c58f8e0b438f9fd1f28370b17d47\"\
]"
type: string
+-----------------+------------------------------------------------------+-----------------------------------+
| 1 | successfully applied subscription | CM Data Notification Subscription |
+-----------------+------------------------------------------------------+-----------------------------------+
- | 100 | cm handle id(s) is(are) not found | Data Operation, Inventory |
+ | 100 | cm handle id(s) is(are) not found | All features |
+-----------------+------------------------------------------------------+-----------------------------------+
| 101 | cm handle(s) not ready | Data Operation |
+-----------------+------------------------------------------------------+-----------------------------------+
+-----------------+------------------------------------------------------+-----------------------------------+
| 107 | southbound system is busy | Data Operation |
+-----------------+------------------------------------------------------+-----------------------------------+
- | 108 | Unknown error | Inventory |
+ | 108 | Unknown error | All features |
+-----------------+------------------------------------------------------+-----------------------------------+
| 109 | cm-handle already exists | Inventory |
+-----------------+------------------------------------------------------+-----------------------------------+
.. * * * NEW DELHI * * *
.. =========================
+Version: 3.4.9
+==============
+
+Release Data
+------------
+
++--------------------------------------+--------------------------------------------------------+
+| **CPS Project** | |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Docker images** | onap/cps-and-ncmp:3.4.9 |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release designation** | 3.4.9 New Delhi |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release date** | Not yet released |
+| | |
++--------------------------------------+--------------------------------------------------------+
+
+Bug Fixes
+---------
+3.4.9
+
+Features
+--------
+
Version: 3.4.8
==============
| **Release designation** | 3.4.8 New Delhi |
| | |
+--------------------------------------+--------------------------------------------------------+
-| **Release date** | Not yet released |
+| **Release date** | 2024 May 1 |
| | |
+--------------------------------------+--------------------------------------------------------+
+Bug Fixes
+---------
+3.4.8
+ - `CPS-2186 <https://jira.onap.org/browse/CPS-2186>`_ Report async task failures to client topic during data operations request
+ - `CPS-2190 <https://jira.onap.org/browse/CPS-2190>`_ Improve performance of NCMP module searches
+ - `CPS-2194 <https://jira.onap.org/browse/CPS-2194>`_ Added defaults for CPS and DMI username and password
+ - `CPS-2204 <https://jira.onap.org/browse/CPS-2204>`_ Added error handling for yang module upgrade operation
+
Features
--------
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<dependencies>
<!-- T E S T D E P E N D E N C I E S -->
- <dependency>
- <groupId>org.codehaus.groovy</groupId>
- <artifactId>groovy</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter</artifactId>
- </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>cps-rest</artifactId>
<artifactId>cps-ncmp-service</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.codehaus.groovy</groupId>
+ <artifactId>groovy</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>kafka</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>postgresql</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>org.testcontainers</groupId>
- <artifactId>postgresql</artifactId>
- <scope>test</scope>
- </dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>spock</artifactId>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>org.testcontainers</groupId>
- <artifactId>kafka</artifactId>
- <scope>test</scope>
- </dependency>
</dependencies>
<profiles>
package org.onap.cps.integration.base
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
+
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
+import okhttp3.mockwebserver.MockWebServer
import org.onap.cps.api.CpsAnchorService
import org.onap.cps.api.CpsDataService
import org.onap.cps.api.CpsDataspaceService
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.ComponentScan
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
-import org.springframework.http.HttpStatus
-import org.springframework.http.MediaType
-import org.springframework.test.web.client.ExpectedCount
-import org.springframework.test.web.client.MockRestServiceServer
import org.springframework.test.web.servlet.MockMvc
-import org.springframework.web.client.RestTemplate
import org.testcontainers.spock.Testcontainers
import spock.lang.Shared
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
-import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
-
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = [CpsDataspaceService])
@Testcontainers
@EnableAutoConfiguration
@Autowired
NetworkCmProxyQueryService networkCmProxyQueryService
- @Autowired
- RestTemplate restTemplate
-
@Autowired
ModuleSyncWatchdog moduleSyncWatchdog
@Autowired
JsonObjectMapper jsonObjectMapper
- MockRestServiceServer mockDmiServer = null
+ MockWebServer mockDmiServer = null
+ DmiDispatcher dmiDispatcher = new DmiDispatcher()
+
+ def DMI_URL = null
- static DMI_URL = 'http://mock-dmi-server'
static NO_MODULE_SET_TAG = ''
static GENERAL_TEST_DATASPACE = 'generalTestDataspace'
static BOOKSTORE_SCHEMA_SET = 'bookstoreSchemaSet'
createStandardBookStoreSchemaSet(GENERAL_TEST_DATASPACE)
initialized = true
}
- mockDmiServer = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build()
+ mockDmiServer = new MockWebServer()
+ mockDmiServer.setDispatcher(dmiDispatcher)
+ mockDmiServer.start()
+ DMI_URL = String.format("http://%s:%s", mockDmiServer.getHostName(), mockDmiServer.getPort())
+ }
+
+ def cleanup() {
+ mockDmiServer.shutdown()
}
def static readResourceDataFile(filename) {
networkCmProxyDataService.updateDmiRegistrationAndSyncModule(new DmiPluginRegistration(dmiPlugin: dmiPlugin, removedCmHandles: cmHandleIds))
}
- def mockDmiResponsesForModuleSync(dmiPlugin, cmHandleId, dmiModuleReferencesResponse, dmiModuleResourcesResponse) {
- mockDmiServer.expect(requestTo("${dmiPlugin}/dmi/v1/ch/${cmHandleId}/modules"))
- .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(dmiModuleReferencesResponse))
- mockDmiServer.expect(requestTo("${dmiPlugin}/dmi/v1/ch/${cmHandleId}/moduleResources"))
- .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(dmiModuleResourcesResponse))
- }
-
- def mockDmiIsNotAvailableForModuleSync(dmiPlugin, cmHandleId) {
- mockDmiServer.expect(requestTo("${dmiPlugin}/dmi/v1/ch/${cmHandleId}/modules"))
- .andRespond(withStatus(HttpStatus.SERVICE_UNAVAILABLE))
- }
-
- def mockDmiWillRespondToHealthChecks(dmiPlugin) {
- mockDmiServer.expect(ExpectedCount.between(0, Integer.MAX_VALUE), requestTo("${dmiPlugin}/actuator/health"))
- .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body('{"status":"UP"}'))
- }
-
def overrideCmHandleLastUpdateTime(cmHandleId, newUpdateTime) {
String ISO_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN);
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.integration.base
+
+import static org.onap.cps.integration.base.CpsIntegrationSpecBase.readResourceDataFile
+
+import org.springframework.http.HttpHeaders
+import java.util.regex.Matcher
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.RecordedRequest
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+
+/**
+ * This class simulates responses from the DMI server in NCMP integration tests.
+ *
+ * It is to be used with a MockWebServer, using mockWebServer.setDispatcher(new DmiDispatcher()).
+ *
+ * It currently implements the following endpoints:
+ * - /actuator/health: healthcheck endpoint that responds with 200 OK / {"status":"UP"}
+ * - /dmi/v1/ch/{cmHandleId}/modules: returns module references for a CM handle
+ * - /dmi/v1/ch/{cmHandleId}/moduleResources: returns modules resources for a CM handle
+ *
+ * The module resource/reference responses are generated based on the module names in the map moduleNamesPerCmHandleId.
+ * To configure the DMI so that CM handle 'ch-1' will have modules 'M1' and 'M2', you may use:
+ * dmiDispatcher.moduleNamesPerCmHandleId.put('ch-1', ['M1', 'M2']);
+ *
+ * To simulate the DMI not being available, the boolean isAvailable may be set to false, in which case the mock server
+ * will always respond with 503 Service Unavailable.
+ */
+class DmiDispatcher extends Dispatcher {
+
+ static final MODULE_REFERENCES_RESPONSE_TEMPLATE = readResourceDataFile('mock-dmi-responses/moduleReferencesTemplate.json')
+ static final MODULE_RESOURCES_RESPONSE_TEMPLATE = readResourceDataFile('mock-dmi-responses/moduleResourcesTemplate.json')
+
+ def isAvailable = true
+
+ Map<String, List<String>> moduleNamesPerCmHandleId = [:]
+
+ @Override
+ MockResponse dispatch(RecordedRequest request) {
+ if (!isAvailable) {
+ return new MockResponse().setResponseCode(HttpStatus.SERVICE_UNAVAILABLE.value())
+ }
+ switch (request.path) {
+ case ~/^\/dmi\/v1\/ch\/(.*)\/modules$/:
+ def cmHandleId = Matcher.lastMatcher[0][1]
+ return getModuleReferencesResponse(cmHandleId)
+
+ case ~/^\/dmi\/v1\/ch\/(.*)\/moduleResources$/:
+ def cmHandleId = Matcher.lastMatcher[0][1]
+ return getModuleResourcesResponse(cmHandleId)
+
+ default:
+ throw new IllegalArgumentException('Mock DMI does not handle path ' + request.path)
+ }
+ }
+
+ private getModuleReferencesResponse(cmHandleId) {
+ def moduleReferences = '{"schemas":[' + getModuleNamesForCmHandle(cmHandleId).collect {
+ MODULE_REFERENCES_RESPONSE_TEMPLATE.replaceAll("<MODULE_NAME>", it)
+ }.join(',') + ']}'
+ return mockOkResponseWithBody(moduleReferences)
+ }
+
+ private getModuleResourcesResponse(cmHandleId) {
+ def moduleResources = '[' + getModuleNamesForCmHandle(cmHandleId).collect {
+ MODULE_RESOURCES_RESPONSE_TEMPLATE.replaceAll("<MODULE_NAME>", it)
+ }.join(',') + ']'
+ return mockOkResponseWithBody(moduleResources)
+ }
+
+ private getModuleNamesForCmHandle(cmHandleId) {
+ if (!moduleNamesPerCmHandleId.containsKey(cmHandleId)) {
+ throw new IllegalArgumentException('Mock DMI has no modules configured for ' + cmHandleId)
+ }
+ return moduleNamesPerCmHandleId.get(cmHandleId)
+ }
+
+ private static mockOkResponseWithBody(responseBody) {
+ return new MockResponse()
+ .setResponseCode(HttpStatus.OK.value())
+ .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+ .setBody(responseBody)
+ }
+}
objectUnderTest.deleteAnchor(GENERAL_TEST_DATASPACE, 'newAnchor')
}
- def 'Query anchors without any known modules and #scenario'() {
+ def 'Query anchors without any known modules'() {
when: 'querying for anchors with #scenario'
- def result = objectUnderTest.queryAnchorNames(dataspaceName, ['unknownModule'])
+ def result = objectUnderTest.queryAnchorNames(GENERAL_TEST_DATASPACE, ['unknownModule'])
then: 'an empty result is returned (no error)'
assert result == []
- where:
- scenario | dataspaceName
- 'non existing database' | 'nonExistingDataspace'
- 'just unknown module(s)' | GENERAL_TEST_DATASPACE
}
def 'Update anchor schema set.'() {
package org.onap.cps.integration.functional
-import java.time.Duration
-import org.onap.cps.integration.base.CpsIntegrationSpecBase
-import org.springframework.http.HttpHeaders
-import org.springframework.http.HttpStatus
-import org.springframework.http.MediaType
-import org.springframework.test.web.client.match.MockRestRequestMatchers
-
import static org.springframework.http.HttpMethod.GET
import static org.springframework.http.HttpMethod.DELETE
import static org.springframework.http.HttpMethod.PATCH
import static org.springframework.http.HttpMethod.POST
import static org.springframework.http.HttpMethod.PUT
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.method
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
-import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.RecordedRequest
+import org.jetbrains.annotations.NotNull
+import org.onap.cps.integration.base.CpsIntegrationSpecBase
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import spock.util.concurrent.PollingConditions
+
class NcmpBearerTokenPassthroughSpec extends CpsIntegrationSpecBase {
- static final MODULE_REFERENCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json')
- static final MODULE_RESOURCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json')
+ def lastAuthHeaderReceived = null
def setup() {
- mockDmiWillRespondToHealthChecks(DMI_URL)
- mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE, MODULE_RESOURCES_RESPONSE)
+ dmiDispatcher.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2']
registerCmHandle(DMI_URL, 'ch-1', NO_MODULE_SET_TAG)
- mockDmiServer.reset()
- mockDmiWillRespondToHealthChecks(DMI_URL)
+
+ mockDmiServer.setDispatcher(new Dispatcher() {
+ @Override
+ MockResponse dispatch(@NotNull RecordedRequest request) throws InterruptedException {
+ if (request.path == '/actuator/health') {
+ return new MockResponse()
+ .addHeader("Content-Type", MediaType.APPLICATION_JSON).setBody('{"status":"UP"}')
+ .setResponseCode(HttpStatus.OK.value())
+ } else {
+ lastAuthHeaderReceived = request.getHeader('Authorization')
+ return new MockResponse().setResponseCode(HttpStatus.OK.value())
+ }
+ }
+ })
}
def cleanup() {
}
def 'Bearer token is passed from NCMP to DMI in pass-through data operations.'() {
- given: 'DMI will expect to receive a request with a bearer token'
- def targetDmiUrl = "$DMI_URL/dmi/v1/ch/ch-1/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=my-resource-id"
- mockDmiServer.expect(requestTo(targetDmiUrl))
- .andExpect(MockRestRequestMatchers.header(HttpHeaders.AUTHORIZATION, 'Bearer some-bearer-token'))
- .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON))
-
when: 'a pass-through data request is sent to NCMP with a bearer token'
mvc.perform(request(httpMethod, '/ncmp/v1/ch/ch-1/data/ds/ncmp-datastore:passthrough-running')
.queryParam('resourceIdentifier', 'my-resource-id')
.andExpect(status().is2xxSuccessful())
then: 'DMI has received request with bearer token'
- mockDmiServer.verify()
+ lastAuthHeaderReceived == 'Bearer some-bearer-token'
where: 'all HTTP operations are applied'
httpMethod << [GET, POST, PUT, PATCH, DELETE]
}
def 'Basic auth header is NOT passed from NCMP to DMI in pass-through data operations.'() {
- given: 'DMI will expect to receive a request with no authorization header'
- def targetDmiUrl = "$DMI_URL/dmi/v1/ch/ch-1/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=my-resource-id"
- mockDmiServer.expect(requestTo(targetDmiUrl))
- .andExpect(MockRestRequestMatchers.headerDoesNotExist(HttpHeaders.AUTHORIZATION))
- .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON))
-
when: 'a pass-through data request is sent to NCMP with basic authentication'
mvc.perform(request(httpMethod, '/ncmp/v1/ch/ch-1/data/ds/ncmp-datastore:passthrough-running')
.queryParam('resourceIdentifier', 'my-resource-id')
.andExpect(status().is2xxSuccessful())
then: 'DMI has received request with no authorization header'
- mockDmiServer.verify()
+ lastAuthHeaderReceived == null
where: 'all HTTP operations are applied'
httpMethod << [GET, POST, PUT, PATCH, DELETE]
}
def 'Bearer token is passed from NCMP to DMI in async batch pass-through data operation.'() {
- given: 'DMI will expect to receive a request with a bearer token'
- mockDmiServer.expect(method(POST))
- .andExpect(MockRestRequestMatchers.header(HttpHeaders.AUTHORIZATION, 'Bearer some-bearer-token'))
- .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON))
-
when: 'a pass-through async data request is sent to NCMP with a bearer token'
def requestBody = """{"operations": [{
"operation": "read",
.andExpect(status().is2xxSuccessful())
then: 'DMI will receive the async request with bearer token'
- mockDmiServer.verify(Duration.ofSeconds(1))
+ new PollingConditions().within(3, () -> {
+ assert lastAuthHeaderReceived == 'Bearer some-bearer-token'
+ })
}
}
def kafkaConsumer = KafkaTestContainer.getConsumer('ncmp-group', StringDeserializer.class)
- static final MODULE_REFERENCES_RESPONSE_A = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json')
- static final MODULE_RESOURCES_RESPONSE_A = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json')
- static final MODULE_REFERENCES_RESPONSE_B = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_Response.json')
- static final MODULE_RESOURCES_RESPONSE_B = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_ResourcesResponse.json')
-
def setup() {
objectUnderTest = networkCmProxyDataService
- mockDmiWillRespondToHealthChecks(DMI_URL)
}
def 'CM Handle registration is successful.'() {
given: 'DMI will return modules when requested'
- mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
+ dmiDispatcher.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2']
and: 'consumer subscribed to topic'
kafkaConsumer.subscribe(['ncmp-events'])
and: 'the CM-handle has expected modules'
assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences('ch-1').moduleName.sort()
- and: 'DMI received expected requests'
- mockDmiServer.verify()
-
cleanup: 'deregister CM handle'
deregisterCmHandle(DMI_URL, 'ch-1')
}
def 'CM Handle goes to LOCKED state when DMI gives error during module sync.'() {
given: 'DMI is not available to handle requests'
- mockDmiIsNotAvailableForModuleSync(DMI_URL, 'ch-1')
+ dmiDispatcher.isAvailable = false
when: 'a CM-handle is registered for creation'
def cmHandleToCreate = new NcmpServiceCmHandle(cmHandleId: 'ch-1')
}
def 'Create a CM-handle with existing moduleSetTag.'() {
- given: 'existing CM-handles cm-1 with moduleSetTag "A", and cm-2 with moduleSetTag "B"'
- mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
- mockDmiResponsesForModuleSync(DMI_URL, 'ch-2', MODULE_REFERENCES_RESPONSE_B, MODULE_RESOURCES_RESPONSE_B)
+ given: 'DMI will return modules when requested'
+ dmiDispatcher.moduleNamesPerCmHandleId = ['ch-1': ['M1', 'M2'], 'ch-2': ['M1', 'M3']]
+ and: 'existing CM-handles cm-1 with moduleSetTag "A", and cm-2 with moduleSetTag "B"'
registerCmHandle(DMI_URL, 'ch-1', 'A')
registerCmHandle(DMI_URL, 'ch-2', 'B')
- assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences('ch-1').moduleName.sort()
- assert ['M1', 'M3'] == objectUnderTest.getYangResourcesModuleReferences('ch-2').moduleName.sort()
when: 'a CM-handle is registered for creation with moduleSetTag "B"'
def cmHandleToCreate = new NcmpServiceCmHandle(cmHandleId: 'ch-3', moduleSetTag: 'B')
def 'CM Handle retry after failed module sync.'() {
given: 'DMI is not initially available to handle requests'
- mockDmiIsNotAvailableForModuleSync(DMI_URL, 'ch-1')
- mockDmiIsNotAvailableForModuleSync(DMI_URL, 'ch-2')
- and: 'DMI will be available for retry'
- mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
- mockDmiResponsesForModuleSync(DMI_URL, 'ch-2', MODULE_REFERENCES_RESPONSE_B, MODULE_RESOURCES_RESPONSE_B)
+ dmiDispatcher.isAvailable = false
when: 'CM-handles are registered for creation'
def cmHandlesToCreate = [new NcmpServiceCmHandle(cmHandleId: 'ch-1'), new NcmpServiceCmHandle(cmHandleId: 'ch-2')]
assert objectUnderTest.getCmHandleCompositeState('ch-1').cmHandleState == CmHandleState.ADVISED
assert objectUnderTest.getCmHandleCompositeState('ch-2').cmHandleState == CmHandleState.ADVISED
- when: 'module sync runs'
+ when: 'DMI is available for retry'
+ dmiDispatcher.isAvailable = true
+ and: 'DMI will return expected modules'
+ dmiDispatcher.moduleNamesPerCmHandleId = ['ch-1': ['M1', 'M2'], 'ch-2': ['M1', 'M3']]
+ and: 'module sync runs'
moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
then: 'CM-handles go to READY state'
new PollingConditions().within(3, () -> {
and: 'CM-handles have expected module set tags (blank)'
assert objectUnderTest.getNcmpServiceCmHandle('ch-1').moduleSetTag == ''
assert objectUnderTest.getNcmpServiceCmHandle('ch-2').moduleSetTag == ''
- and: 'DMI received expected requests'
- mockDmiServer.verify()
cleanup: 'deregister CM handle'
deregisterCmHandles(DMI_URL, ['ch-1', 'ch-2'])
import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
import org.onap.cps.ncmp.api.models.DmiPluginRegistration
import org.onap.cps.ncmp.api.models.UpgradedCmHandles
-import org.springframework.http.HttpStatus
import spock.util.concurrent.PollingConditions
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.anything
-import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
-
class NcmpCmHandleUpgradeSpec extends CpsIntegrationSpecBase {
NetworkCmProxyDataService objectUnderTest
- static final INITIAL_MODULE_REFERENCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json')
- static final INITIAL_MODULE_RESOURCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json')
- static final UPDATED_MODULE_REFERENCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_Response.json')
- static final UPDATED_MODULE_RESOURCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_ResourcesResponse.json')
static final CM_HANDLE_ID = 'ch-1'
static final CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG = 'ch-2'
def setup() {
objectUnderTest = networkCmProxyDataService
- mockDmiWillRespondToHealthChecks(DMI_URL)
}
def 'Upgrade CM-handle with new moduleSetTag or no moduleSetTag.'() {
- given: 'DMI will return modules for initial registration'
- mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, INITIAL_MODULE_REFERENCES_RESPONSE, INITIAL_MODULE_RESOURCES_RESPONSE)
- and: 'DMI returns different modules for upgrade'
- mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, UPDATED_MODULE_REFERENCES_RESPONSE, UPDATED_MODULE_RESOURCES_RESPONSE)
-
- when: 'a CM-handle is created with expected initial modules: M1 and M2'
+ given: 'a CM-handle is created with expected initial modules: M1 and M2'
+ dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M2']
registerCmHandle(DMI_URL, CM_HANDLE_ID, initialModuleSetTag)
assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences(CM_HANDLE_ID).moduleName.sort()
- and: "the CM-handle is upgraded with given moduleSetTag '${updatedModuleSetTag}'"
+
+ when: "the CM-handle is upgraded with given moduleSetTag '${updatedModuleSetTag}'"
def cmHandlesToUpgrade = new UpgradedCmHandles(cmHandles: [CM_HANDLE_ID], moduleSetTag: updatedModuleSetTag)
def dmiPluginRegistrationResponse = networkCmProxyDataService.updateDmiRegistrationAndSyncModule(
new DmiPluginRegistration(dmiPlugin: DMI_URL, upgradedCmHandles: cmHandlesToUpgrade))
assert cmHandleCompositeState.lockReason.lockReasonCategory == LockReasonCategory.MODULE_UPGRADE
assert cmHandleCompositeState.lockReason.details == "Upgrade to ModuleSetTag: ${updatedModuleSetTag}"
- when: 'module sync runs'
+ when: 'DMI will return different modules for upgrade: M1 and M3'
+ dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M3']
+ and: 'module sync runs'
moduleSyncWatchdog.resetPreviouslyFailedCmHandles()
moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
then: 'CM-handle goes to READY state'
- new PollingConditions().within(3, () -> {
+ new PollingConditions().eventually {
assert CmHandleState.READY == objectUnderTest.getCmHandleCompositeState(CM_HANDLE_ID).cmHandleState
- })
+ }
and: 'the CM-handle has expected moduleSetTag'
assert objectUnderTest.getNcmpServiceCmHandle(CM_HANDLE_ID).moduleSetTag == updatedModuleSetTag
and: 'CM-handle has expected updated modules: M1 and M3'
assert ['M1', 'M3'] == objectUnderTest.getYangResourcesModuleReferences(CM_HANDLE_ID).moduleName.sort()
- and: 'DMI received expected requests'
- mockDmiServer.verify()
-
cleanup: 'deregister CM-handle'
deregisterCmHandle(DMI_URL, CM_HANDLE_ID)
def 'Upgrade CM-handle with existing moduleSetTag.'() {
given: 'DMI will return modules for registration'
- mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, INITIAL_MODULE_REFERENCES_RESPONSE, INITIAL_MODULE_RESOURCES_RESPONSE)
- mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG, UPDATED_MODULE_REFERENCES_RESPONSE, UPDATED_MODULE_RESOURCES_RESPONSE)
+ dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M2']
+ dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG] = ['M1', 'M3']
and: "an existing CM-handle handle with moduleSetTag '${updatedModuleSetTag}'"
registerCmHandle(DMI_URL, CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG, updatedModuleSetTag)
assert ['M1', 'M3'] == objectUnderTest.getYangResourcesModuleReferences(CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG).moduleName.sort()
moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
then: 'CM-handle goes to READY state'
- new PollingConditions().within(3, () -> {
+ new PollingConditions().eventually {
assert CmHandleState.READY == objectUnderTest.getCmHandleCompositeState(CM_HANDLE_ID).cmHandleState
- })
+ }
and: 'the CM-handle has expected moduleSetTag'
assert objectUnderTest.getNcmpServiceCmHandle(CM_HANDLE_ID).moduleSetTag == updatedModuleSetTag
def 'Skip upgrade of CM-handle with same moduleSetTag as before.'() {
given: 'an existing CM-handle with expected initial modules: M1 and M2'
- mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, INITIAL_MODULE_REFERENCES_RESPONSE, INITIAL_MODULE_RESOURCES_RESPONSE)
+ dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M2']
registerCmHandle(DMI_URL, CM_HANDLE_ID, 'same')
assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences(CM_HANDLE_ID).moduleName.sort()
}
def 'Upgrade of CM-handle fails due to DMI error.'() {
- given: 'DMI will return modules for initial registration'
- mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, INITIAL_MODULE_REFERENCES_RESPONSE, INITIAL_MODULE_RESOURCES_RESPONSE)
- and: 'DMI returns error code for upgrade'
- mockDmiServer.expect(anything()).andRespond(withStatus(HttpStatus.SERVICE_UNAVAILABLE))
-
- when: 'a CM-handle is created'
+ given: 'a CM-handle exists'
+ dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M2']
registerCmHandle(DMI_URL, CM_HANDLE_ID, 'oldTag')
- and: 'the CM-handle is upgraded'
+ and: 'DMI is not available for upgrade'
+ dmiDispatcher.isAvailable = false
+
+ when: 'the CM-handle is upgraded'
def cmHandlesToUpgrade = new UpgradedCmHandles(cmHandles: [CM_HANDLE_ID], moduleSetTag: 'newTag')
networkCmProxyDataService.updateDmiRegistrationAndSyncModule(
new DmiPluginRegistration(dmiPlugin: DMI_URL, upgradedCmHandles: cmHandlesToUpgrade))
moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
then: 'CM-handle goes to LOCKED state with reason MODULE_UPGRADE_FAILED'
- new PollingConditions().within(3, () -> {
+ new PollingConditions(timeout: 3).eventually {
def cmHandleCompositeState = objectUnderTest.getCmHandleCompositeState(CM_HANDLE_ID)
assert cmHandleCompositeState.cmHandleState == CmHandleState.LOCKED
assert cmHandleCompositeState.lockReason.lockReasonCategory == LockReasonCategory.MODULE_UPGRADE_FAILED
- })
+ }
and: 'the CM-handle has same moduleSetTag as before'
assert objectUnderTest.getNcmpServiceCmHandle(CM_HANDLE_ID).moduleSetTag == 'oldTag'
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
package org.onap.cps.integration.functional
import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING;
assert cmNotificationSubscriptionPersistenceService.
getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 0
when: 'we add a new cm notification subscription'
- cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath,
+ cmNotificationSubscriptionPersistenceService.addCmNotificationSubscription(datastoreType,cmHandleId,xpath,
'subId-1')
then: 'there is an ongoing cm subscription for that CM handle and xpath'
assert cmNotificationSubscriptionPersistenceService.isOngoingCmNotificationSubscription(datastoreType,cmHandleId,xpath)
getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 1
}
- def 'Adding a cm notification subscription to an already existing'() {
- given: 'an ongoing cm subscription'
+ def 'Adding a cm notification subscription to the already existing'() {
+ given: 'an ongoing cm subscription with the following details'
def datastoreType = PASSTHROUGH_RUNNING
def cmHandleId = 'ch-1'
def xpath = '/x/y'
- cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath,
- 'subId-1')
when: 'a new cm notification subscription is made for the SAME CM handle and xpath'
- cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath,
+ cmNotificationSubscriptionPersistenceService.addCmNotificationSubscription(datastoreType,cmHandleId,xpath,
'subId-2')
then: 'it is added to the ongoing list of subscription ids'
def subscriptionIds = cmNotificationSubscriptionPersistenceService.getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath)
and: 'both subscription ids exists for the CM handle and xpath'
assert subscriptionIds.contains("subId-1") && subscriptionIds.contains("subId-2")
}
+
+ def 'Removing cm notification subscriber among other subscribers'() {
+ given: 'an ongoing cm subscription with the following details'
+ def datastoreType = PASSTHROUGH_RUNNING
+ def cmHandleId = 'ch-1'
+ def xpath = '/x/y'
+ and: 'the number of subscribers is as follows'
+ def originalNumberOfSubscribers =
+ cmNotificationSubscriptionPersistenceService.getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size()
+ when: 'a subscriber is removed'
+ cmNotificationSubscriptionPersistenceService.removeCmNotificationSubscription(datastoreType,cmHandleId,xpath,'subId-2')
+ then: 'the number of subscribers is reduced by 1'
+ def updatedNumberOfSubscribers =
+ cmNotificationSubscriptionPersistenceService.getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size()
+ assert updatedNumberOfSubscribers == originalNumberOfSubscribers-1
+ }
+
+ def 'Removing the LAST cm notification subscriber for a given cm handle, datastore and xpath'() {
+ given: 'an ongoing cm subscription with the following details'
+ def datastoreType = PASSTHROUGH_RUNNING
+ def cmHandleId = 'ch-1'
+ def xpath = '/x/y'
+ and: 'there is only one subscriber'
+ assert cmNotificationSubscriptionPersistenceService
+ .getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 1
+ when: 'only subscriber is removed'
+ cmNotificationSubscriptionPersistenceService.removeCmNotificationSubscription(datastoreType,cmHandleId,xpath,'subId-1')
+ then: 'there are no longer any subscriptions for the cm handle, datastore and xpath'
+ assert !cmNotificationSubscriptionPersistenceService.isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)
+ }
+
}
class NcmpRestApiSpec extends CpsIntegrationSpecBase {
- static final MODULE_REFERENCES_RESPONSE_A = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json')
- static final MODULE_RESOURCES_RESPONSE_A = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json')
- static final MODULE_REFERENCES_RESPONSE_B = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_Response.json')
- static final MODULE_RESOURCES_RESPONSE_B = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_ResourcesResponse.json')
-
- def setup() {
- mockDmiWillRespondToHealthChecks(DMI_URL)
- }
-
def 'Register CM Handles using REST API.'() {
given: 'DMI will return modules'
- mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
- mockDmiResponsesForModuleSync(DMI_URL, 'ch-2', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
- mockDmiResponsesForModuleSync(DMI_URL, 'ch-3', MODULE_REFERENCES_RESPONSE_B, MODULE_RESOURCES_RESPONSE_B)
+ dmiDispatcher.moduleNamesPerCmHandleId = [
+ 'ch-1': ['M1', 'M2'],
+ 'ch-2': ['M1', 'M2'],
+ 'ch-3': ['M1', 'M3']
+ ]
and: 'a POST request is made to register the CM Handles'
def requestBody = '{"dmiPlugin":"'+DMI_URL+'","createdCmHandles":[{"cmHandle":"ch-1"},{"cmHandle":"ch-2"},{"cmHandle":"ch-3"}]}'
mvc.perform(post('/ncmpInventory/v1/ch').contentType(MediaType.APPLICATION_JSON).content(requestBody))
when: 'module sync runs'
moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
then: 'CM-handles go to READY state'
- new PollingConditions(timeout: 3, delay: 0.5).eventually {
- mvc.perform(get('/ncmp/v1/ch/ch-1'))
- .andExpect(status().isOk())
- .andExpect(jsonPath('$.state.cmHandleState').value('READY'))
+ new PollingConditions().eventually {
+ (1..3).each {
+ mvc.perform(get('/ncmp/v1/ch/ch-'+it))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath('$.state.cmHandleState').value('READY'))
+ }
}
}
]
}""".formatted(moduleName)
expect: "a search for module ${moduleName} returns expected CM handles"
- mvc.perform(post(DMI_URL+'/ncmp/v1/ch/id-searches').contentType(MediaType.APPLICATION_JSON).content(requestBodyWithModuleCondition))
+ mvc.perform(post('/ncmp/v1/ch/id-searches').contentType(MediaType.APPLICATION_JSON).content(requestBodyWithModuleCondition))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath('$[*]', containsInAnyOrder(expectedCmHandles.toArray())))
.andExpect(jsonPath('$', hasSize(expectedCmHandles.size())));
resourceMeter.stop()
def durationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'the operation completes within 12 seconds'
- recordAndAssertResourceUsage("Creating 33,000 books", 12, durationInSeconds, 150, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage("Creating 33,000 books", 16, durationInSeconds, 150, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Get data nodes from multiple xpaths 32K (2^15) limit exceeded.'() {
resourceMeter.stop()
def durationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'test data is deleted in 1 second'
- recordAndAssertResourceUsage("Deleting test data", 1, durationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage("Deleting test data", 0.1, durationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
}
def countDataNodes() {
resourceMeter.stop()
def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'delete duration is within expected time and memory used is within limit'
- recordAndAssertResourceUsage('Delete 100 containers', 2.5, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage('Delete 100 containers', 2.2, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Batch delete 100 container nodes'() {
resourceMeter.stop()
def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'delete duration is within expected time and memory used is within limit'
- recordAndAssertResourceUsage('Batch delete 100 containers', 0.6, deleteDurationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage('Batch delete 100 containers', 0.7, deleteDurationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Delete 100 list elements'() {
resourceMeter.stop()
def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'delete duration is within expected time and memory used is within limit'
- recordAndAssertResourceUsage('Delete 100 lists elements', 2.5, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage('Delete 100 lists elements', 2.1, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Batch delete 100 list elements'() {
resourceMeter.stop()
def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'delete duration is within expected time and memory used is within limit'
- recordAndAssertResourceUsage('Batch delete 100 lists elements', 0.6, deleteDurationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage('Batch delete 100 lists elements', 0.7, deleteDurationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Delete 100 whole lists'() {
resourceMeter.stop()
def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'delete duration is within expected time and memory used is within limit'
- recordAndAssertResourceUsage('Delete 100 whole lists', 6, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage('Delete 100 whole lists', 4, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Batch delete 100 whole lists'() {
resourceMeter.stop()
def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'delete duration is within expected time and memory used is within limit'
- recordAndAssertResourceUsage('Batch delete 100 whole lists', 5, deleteDurationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage('Batch delete 100 whole lists', 3, deleteDurationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Delete 1 large data node'() {
resourceMeter.stop()
def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'delete duration is within expected time and memory used is within limit'
- recordAndAssertResourceUsage('Delete data nodes for anchor', 2, deleteDurationInSeconds, 1, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage('Delete data nodes for anchor', 1.9, deleteDurationInSeconds, 1, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Batch delete 100 non-existing nodes'() {
resourceMeter.stop()
def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'delete duration is within expected time and memory used is within limit'
- recordAndAssertResourceUsage('Batch delete 100 non-existing', 7, deleteDurationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage('Batch delete 100 non-existing', 1, deleteDurationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Clean up test data'() {
recordAndAssertResourceUsage("Read datatrees with ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
where: 'the following parameters are used'
scenario | fetchDescendantsOption || durationLimit | memoryLimit | expectedNumberOfDataNodes
- 'no descendants' | OMIT_DESCENDANTS || 0.02 | 1 | 1
- 'direct descendants' | DIRECT_CHILDREN_ONLY || 0.06 | 5 | 1 + OPENROADM_DEVICES_PER_ANCHOR
- 'all descendants' | INCLUDE_ALL_DESCENDANTS || 2.5 | 250 | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+ 'no descendants' | OMIT_DESCENDANTS || 0.01 | 1 | 1
+ 'direct descendants' | DIRECT_CHILDREN_ONLY || 0.05 | 5 | 1 + OPENROADM_DEVICES_PER_ANCHOR
+ 'all descendants' | INCLUDE_ALL_DESCENDANTS || 1.2 | 250 | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
}
def 'Read data trees for multiple xpaths'() {
then: 'requested nodes and their descendants are returned'
assert countDataNodesInTree(result) == OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
and: 'all data is read within expected time and memory used is within limit'
- recordAndAssertResourceUsage("Read datatrees for multiple xpaths", 4 , durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage("Read datatrees for multiple xpaths", 1.8 , durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Read for multiple xpaths to non-existing datanodes'() {
then: 'no data is returned'
assert result.isEmpty()
and: 'the operation completes within within expected time'
- recordAndAssertResourceUsage("Read non-existing xpaths", 0.02, durationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage("Read non-existing xpaths", 0.01, durationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Read complete data trees using #scenario.'() {
recordAndAssertResourceUsage("Read datatrees using ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
where: 'the following xpaths are used'
scenario | xpath || durationLimit | memoryLimit | expectedNumberOfDataNodes
- 'openroadm root' | '/' || 2 | 250 | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
- 'openroadm top element' | '/openroadm-devices' || 2 | 250 | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
- 'openroadm whole list' | '/openroadm-devices/openroadm-device' || 3 | 250 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+ 'openroadm root' | '/' || 1.0 | 250 | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+ 'openroadm top element' | '/openroadm-devices' || 1.0 | 250 | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+ 'openroadm whole list' | '/openroadm-devices/openroadm-device' || 1.7 | 250 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
}
}
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.integration.performance.cps
+
+import org.onap.cps.integration.performance.base.CpsPerfTestBase
+import org.onap.cps.spi.model.ModuleReference
+
+class ModuleQueryPerfTest extends CpsPerfTestBase {
+
+ static final KILOBYTE = 1000
+ static final TOTAL_TEST_ANCHORS = 10_000
+ static final SCHEMA_SET_PREFIX = 'mySchemaSet'
+ static final ANCHOR_PREFIX = 'myAnchor'
+ static final MODULE_REVISION = '2024-04-25'
+ static final MODULE_TEMPLATE = """
+ module <MODULE_NAME> {
+ yang-version 1.1;
+ namespace "org:onap:cps:test:<MODULE_NAME>";
+ prefix tree;
+ revision "<MODULE_REVISION>" {
+ description "<DESCRIPTION>";
+ }
+ container tree {
+ list branch {
+ key "name";
+ leaf name {
+ type string;
+ }
+ }
+ }
+ }
+ """
+
+ def 'Module query - Preload test data (needed for other tests).'() {
+ given: 'a schema set with different sizes of Yang modules is created'
+ cpsModuleService.createSchemaSet(CPS_PERFORMANCE_TEST_DATASPACE, SCHEMA_SET_PREFIX + '0', [
+ 'module0.yang': makeYangModuleOfLength('module0', 1 * KILOBYTE),
+ 'module1.yang': makeYangModuleOfLength('module1', 1000 * KILOBYTE)
+ ])
+ and: 'these modules will be used again to create many schema sets'
+ def allModuleReferences = [
+ new ModuleReference('module0', MODULE_REVISION),
+ new ModuleReference('module1', MODULE_REVISION)
+ ]
+ when: 'many schema sets and anchors are created using those modules'
+ resourceMeter.start()
+ (1..TOTAL_TEST_ANCHORS).each {
+ def schemaSetName = SCHEMA_SET_PREFIX + it
+ def anchorName = ANCHOR_PREFIX + it
+ cpsModuleService.createSchemaSetFromModules(CPS_PERFORMANCE_TEST_DATASPACE, schemaSetName, [:], allModuleReferences)
+ cpsAnchorService.createAnchor(CPS_PERFORMANCE_TEST_DATASPACE, schemaSetName, anchorName)
+ }
+ resourceMeter.stop()
+ then: 'operation takes less than expected duration'
+ recordAndAssertResourceUsage('Module query test setup',
+ 45, resourceMeter.totalTimeInSeconds,
+ 500, resourceMeter.totalMemoryUsageInMB
+ )
+ }
+
+ def 'Querying anchors by module name is NOT dependant on the file size of the module.'() {
+ when: 'we search for anchors with given Yang module name'
+ resourceMeter.start()
+ def result = cpsAnchorService.queryAnchorNames(CPS_PERFORMANCE_TEST_DATASPACE, [yangModuleName])
+ resourceMeter.stop()
+ then: 'expected number of anchors is returned'
+ assert result.size() == TOTAL_TEST_ANCHORS
+ and: 'operation completes with expected resource usage'
+ recordAndAssertResourceUsage("Query for anchors with ${scenario}",
+ expectedTimeInSeconds, resourceMeter.totalTimeInSeconds,
+ 5, resourceMeter.totalMemoryUsageInMB)
+ where: 'the following parameters are used'
+ scenario | yangModuleName || expectedTimeInSeconds
+ '1 KB module' | 'module0' || 0.05
+ '1000 KB module' | 'module1' || 0.05
+ }
+
+ def 'Module query - Clean up test data.'() {
+ cleanup:
+ // FIXME this API has extremely high memory usage, therefore external batching must be used
+ for (int i = 1; i <= TOTAL_TEST_ANCHORS; i += 100) {
+ cpsModuleService.deleteSchemaSetsWithCascade(CPS_PERFORMANCE_TEST_DATASPACE, (i..i+100).collect {SCHEMA_SET_PREFIX + it})
+ }
+ cpsModuleService.deleteSchemaSetsWithCascade(CPS_PERFORMANCE_TEST_DATASPACE, [SCHEMA_SET_PREFIX + '0'])
+ }
+
+ // This makes a Yang module of approximately target length in bytes by padding the description field with many '*'
+ private static def makeYangModuleOfLength(moduleName, targetLength) {
+ def padding = String.valueOf('*').repeat(targetLength - MODULE_TEMPLATE.size()) // not exact
+ return MODULE_TEMPLATE
+ .replaceAll('<MODULE_NAME>', moduleName)
+ .replaceAll('<MODULE_REVISION>', MODULE_REVISION)
+ .replaceAll('<DESCRIPTION>', padding)
+ }
+}
recordAndAssertResourceUsage("Query 1 anchor ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
where: 'the following parameters are used'
scenario | cpsPath || durationLimit | memoryLimit | expectedNumberOfDataNodes
- 'top element' | '/openroadm-devices' || 2.5 | 400 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
- 'leaf condition' | '//openroadm-device[@ne-state="inservice"]' || 2.5 | 400 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
- 'ancestors' | '//openroadm-device/ancestor::openroadm-devices' || 2.5 | 400 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
- 'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 2.5 | 400 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
- 'non-existing data' | '/path/to/non-existing/node[@id="1"]' || 0.1 | 1 | 0
+ 'top element' | '/openroadm-devices' || 1.1 | 400 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
+ 'leaf condition' | '//openroadm-device[@ne-state="inservice"]' || 1.1 | 400 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+ 'ancestors' | '//openroadm-device/ancestor::openroadm-devices' || 1.1 | 400 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
+ 'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 1.1 | 400 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
+ 'non-existing data' | '/path/to/non-existing/node[@id="1"]' || 0.009 | 1 | 0
}
def 'Query complete data trees across all anchors with #scenario.'() {
recordAndAssertResourceUsage("Query across anchors ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
where: 'the following parameters are used'
scenario | cpspath || durationLimit | memoryLimit | expectedNumberOfDataNodes
- 'top element' | '/openroadm-devices' || 7 | 600 | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
- 'leaf condition' | '//openroadm-device[@ne-state="inservice"]' || 7 | 600 | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE)
- 'ancestors' | '//openroadm-device/ancestor::openroadm-devices' || 7 | 600 | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
- 'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 7 | 600 | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
+ 'top element' | '/openroadm-devices' || 3 | 600 | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
+ 'leaf condition' | '//openroadm-device[@ne-state="inservice"]' || 3 | 600 | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE)
+ 'ancestors' | '//openroadm-device/ancestor::openroadm-devices' || 3 | 600 | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
+ 'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 3 | 600 | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
}
def 'Query with leaf condition and #scenario.'() {
where: 'the following parameters are used'
scenario | fetchDescendantsOption || durationLimit | memoryLimit | expectedNumberOfDataNodes
'no descendants' | OMIT_DESCENDANTS || 0.1 | 6 | OPENROADM_DEVICES_PER_ANCHOR
- 'direct descendants' | DIRECT_CHILDREN_ONLY || 0.2 | 12 | OPENROADM_DEVICES_PER_ANCHOR * 2
- 'all descendants' | INCLUDE_ALL_DESCENDANTS || 2.5 | 200 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+ 'direct descendants' | DIRECT_CHILDREN_ONLY || 0.1 | 12 | OPENROADM_DEVICES_PER_ANCHOR * 2
+ 'all descendants' | INCLUDE_ALL_DESCENDANTS || 1.1 | 200 | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
}
def 'Query ancestors with #scenario.'() {
recordAndAssertResourceUsage("Query ancestors with ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
where: 'the following parameters are used'
scenario | fetchDescendantsOption || durationLimit | memoryLimit | expectedNumberOfDataNodes
- 'no descendants' | OMIT_DESCENDANTS || 0.1 | 3 | 1
- 'direct descendants' | DIRECT_CHILDREN_ONLY || 0.2 | 8 | 1 + OPENROADM_DEVICES_PER_ANCHOR
- 'all descendants' | INCLUDE_ALL_DESCENDANTS || 2.5 | 400 | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+ 'no descendants' | OMIT_DESCENDANTS || 0.08 | 3 | 1
+ 'direct descendants' | DIRECT_CHILDREN_ONLY || 0.1 | 8 | 1 + OPENROADM_DEVICES_PER_ANCHOR
+ 'all descendants' | INCLUDE_ALL_DESCENDANTS || 1.1 | 400 | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
}
}
timeLimit, resourceMeter.getTotalTimeInSeconds(),
memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
where:
- scenario | totalNodes | startId | changeLeaves || timeLimit | memoryLimit
- 'Replace 0 nodes with 100' | 100 | 1 | false || 2.5 | 200
- 'Replace 100 using same data' | 100 | 1 | false || 3.0 | 200
- 'Replace 100 with new leaf values' | 100 | 1 | true || 3.0 | 200
- 'Replace 100 with 100 new nodes' | 100 | 101 | false || 6.0 | 200
- 'Replace 50 existing and 50 new' | 100 | 151 | true || 4.5 | 200
- 'Replace 100 nodes with 0' | 0 | 1 | false || 3.0 | 200
+ scenario | totalNodes | startId | changeLeaves || timeLimit | memoryLimit
+ 'Replace 0 nodes with 100' | 100 | 1 | false || 3.2 | 200
+ 'Replace 100 using same data' | 100 | 1 | false || 5.6 | 200
+ 'Replace 100 with new leaf values' | 100 | 1 | true || 5.5 | 200
+ 'Replace 100 with 100 new nodes' | 100 | 101 | false || 10.0 | 200
+ 'Replace 50 existing and 50 new' | 100 | 151 | true || 8.0 | 200
+ 'Replace 100 nodes with 0' | 0 | 1 | false || 7.0 | 200
}
def 'Replace list content: #scenario.'() {
memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
where:
scenario | totalNodes | startId | changeLeaves || timeLimit | memoryLimit
- 'Replace list of 0 with 100' | 100 | 1 | false || 2.5 | 200
- 'Replace list of 100 using same data' | 100 | 1 | false || 3.0 | 200
- 'Replace list of 100 with new leaf values' | 100 | 1 | true || 3.0 | 200
- 'Replace list with 100 new nodes' | 100 | 101 | false || 6.0 | 200
- 'Replace list with 50 existing and 50 new' | 100 | 151 | true || 4.5 | 200
- 'Replace list of 100 nodes with 1' | 1 | 1 | false || 3.0 | 200
+ 'Replace list of 0 with 100' | 100 | 1 | false || 3.0 | 200
+ 'Replace list of 100 using same data' | 100 | 1 | false || 5.4 | 200
+ 'Replace list of 100 with new leaf values' | 100 | 1 | true || 5.6 | 200
+ 'Replace list with 100 new nodes' | 100 | 101 | false || 9.9 | 200
+ 'Replace list with 50 existing and 50 new' | 100 | 151 | true || 8.0 | 200
+ 'Replace list of 100 nodes with 1' | 1 | 1 | false || 7.0 | 200
}
def 'Update leaves for 100 data nodes.'() {
assert 100 == countDataNodes('/openroadm-devices/openroadm-device[@status="fail"]')
and: 'update completes within expected time and memory used is within limit'
recordAndAssertResourceUsage('Update leaves for 100 data nodes',
- 0.4, resourceMeter.getTotalTimeInSeconds(),
+ 0.3, resourceMeter.getTotalTimeInSeconds(),
120, resourceMeter.getTotalMemoryUsageInMB())
}
cpsAnchorService.deleteAnchor(CPS_PERFORMANCE_TEST_DATASPACE, WRITE_TEST_ANCHOR)
where:
totalNodes || expectedDuration | memoryLimit
- 50 || 2 | 100
- 100 || 4 | 200
- 200 || 7 | 400
- 400 || 14 | 500
+ 50 || 1.6 | 100
+ 100 || 3.3 | 200
+ 200 || 6.8 | 400
+ 400 || 13.0 | 500
}
def 'Writing bookstore data has exponential time.'() {
cpsAnchorService.deleteAnchor(CPS_PERFORMANCE_TEST_DATASPACE, WRITE_TEST_ANCHOR)
where:
totalBooks || expectedDuration | memoryLimit
- 800 || 0.5 | 50
- 1600 || 1.5 | 100
- 3200 || 6.0 | 150
- 6400 || 18.0 | 200
+ 800 || 0.3 | 50
+ 1600 || 0.8 | 100
+ 3200 || 2.6 | 150
+ 6400 || 6.7 | 200
}
def 'Writing openroadm list data using saveListElements.'() {
cpsAnchorService.deleteAnchor(CPS_PERFORMANCE_TEST_DATASPACE, WRITE_TEST_ANCHOR)
where:
totalNodes || expectedDuration | memoryLimit
- 50 || 2 | 100
- 100 || 4 | 200
- 200 || 7 | 400
- 400 || 14 | 500
+ 50 || 1.5 | 100
+ 100 || 3.0 | 200
+ 200 || 6.3 | 400
+ 400 || 14.0 | 500
}
}
def resultAfter = objectUnderTest.queryDataNodes(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_ANCHOR, cpsPath, INCLUDE_ALL_DESCENDANTS)
assert resultAfter.collect {it.leaves.subscribers.size()}.sum() == totalNumberOfEntries * (1 + numberOfCmDataSubscribers)
and: 'update matching subscription within 15 seconds'
- recordAndAssertResourceUsage("Update matching subscription", 15, durationInSeconds, 1000, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage("Update matching subscription", 11, durationInSeconds, 1000, resourceMeter.getTotalMemoryUsageInMB())
}
def 'Worst case new subscription (200x10 new entries).'() {
package org.onap.cps.integration.performance.ncmp
-import org.apache.commons.lang3.StringUtils
-import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
-import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncService
-import org.onap.cps.ncmp.api.impl.utils.YangDataConverter
-import org.onap.cps.spi.FetchDescendantsOption
-import org.onap.cps.spi.model.DataNode
-import org.springframework.beans.factory.annotation.Autowired
-import java.util.stream.Collectors
import org.onap.cps.api.CpsQueryService
import org.onap.cps.integration.ResourceMeter
import org.onap.cps.integration.performance.base.NcmpPerfTestBase
-import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+import java.util.stream.Collectors
+
import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
class CmHandleQueryPerfTest extends NcmpPerfTestBase {
resourceMeter.stop()
def durationInSeconds = resourceMeter.getTotalTimeInSeconds()
then: 'the required operations are performed within required time'
- recordAndAssertResourceUsage("CpsPath Registry attributes Query", 2, durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB())
+ recordAndAssertResourceUsage("CpsPath Registry attributes Query", 3.4, durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB())
and: 'all nodes are returned'
result.size() == TOTAL_CM_HANDLES
and: 'the tree contains all the expected descendants too'
expectedAverageResponseTime, averageResponseTime,
15, resourceMeter.totalMemoryUsageInMB)
where:
- expectedAverageResponseTime = 1 * MILLISECONDS
+ expectedAverageResponseTime = 6 * MILLISECONDS
}
def 'CM-handle is looked up by alternate-id.'() {
expectedAverageResponseTime, averageResponseTime,
15, resourceMeter.totalMemoryUsageInMB)
where:
- expectedAverageResponseTime = 10 * MILLISECONDS
+ expectedAverageResponseTime = 20 * MILLISECONDS
}
def 'A batch of CM-handles is looked up by alternate-id.'() {
expectedAverageResponseTime, averageResponseTime,
500, resourceMeter.totalMemoryUsageInMB)
where:
- expectedAverageResponseTime = 100 * MILLISECONDS
+ expectedAverageResponseTime = 360 * MILLISECONDS
}
}
minimumIdle: 5
maximumPoolSize: 80
idleTimeout: 60000
- connectionTimeout: 120000
+ connectionTimeout: 30000
leakDetectionThreshold: 30000
pool-name: CpsDatabasePool
queue-capacity: 500
wait-for-tasks-to-complete-on-shutdown: true
thread-name-prefix: Async-
- time-out-value-in-ms: 2000
+ time-out-value-in-ms: 60000
springdoc:
swagger-ui:
ncmp:
dmi:
httpclient:
- connectionTimeoutInSeconds: 180
+ connectionTimeoutInSeconds: 30
maximumConnectionsPerRoute: 50
maximumConnectionsTotal: 100
idleConnectionEvictionThresholdInSeconds: 5
+++ /dev/null
-[
- {
- "moduleName": "M1",
- "revision": "2024-01-01",
- "yangSource": "module M1 {\n\n namespace \"urn:ietf:params:xml:ns:yang:M1\";\n prefix \"yang\";\n\n organization\n \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n contact\n \"WG Web: <http://tools.ietf.org/wg/netmod/>\n WG List: <mailto:netmod@ietf.org>\n\n WG Chair: David Kessens\n <mailto:david.kessens@nsn.com>\n\n WG Chair: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\n\n Editor: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n description\n \"This module contains a collection of generally useful derived\n YANG data types.\n\n Copyright (c) 2013 IETF Trust and the persons identified as\n authors of the code. All rights reserved.\n\n Redistribution and use in source and binary forms, with or\n without modification, is permitted pursuant to, and subject\n to the license terms contained in, the Simplified BSD License\n set forth in Section 4.c of the IETF Trust's Legal Provisions\n Relating to IETF Documents\n (http://trustee.ietf.org/license-info).\n\n This version of this YANG module is part of RFC 6991; see\n the RFC itself for full legal notices.\";\n\n revision 2024-01-01 {\n description\n \"This revision adds the following new data types:\n - yang-identifier\n - hex-string\n - uuid\n - dotted-quad\";\n reference\n \"RFC 6991: Common YANG Data Types\";\n }\n\n revision 2010-09-24 {\n description\n \"Initial revision.\";\n reference\n \"RFC 6021: Common YANG Data Types\";\n }\n\n /*** collection of counter and gauge types ***/\n\n typedef counter32 {\n type uint32;\n description\n \"The counter32 type represents a non-negative integer\n that monotonically increases until it reaches a\n maximum value of 2^32-1 (4294967295 decimal), when it\n wraps around and starts increasing again from zero.\n\n Counters have no defined 'initial' value, and thus, a\n single value of a counter has (in general) no information\n content. Discontinuities in the monotonically increasing\n value normally occur at re-initialization of the\n management system, and at other times as specified in the\n description of a schema node using this type. If such\n other times can occur, for example, the creation of\n a schema node of type counter32 at times other than\n re-initialization, then a corresponding schema node\n should be defined, with an appropriate type, to indicate\n the last discontinuity.\n\n The counter32 type should not be used for configuration\n schema nodes. A default statement SHOULD NOT be used in\n combination with the type counter32.\n\n In the value set and its semantics, this type is equivalent\n to the Counter32 type of the SMIv2.\";\n reference\n \"RFC 2578: Structure of Management Information Version 2\n (SMIv2)\";\n }\n}\n"
- },
- {
- "moduleName": "M2",
- "revision": "2024-01-02",
- "yangSource": "module M2 {\n\n namespace \"urn:ietf:params:xml:ns:yang:M2\";\n prefix \"yang\";\n\n organization\n \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n contact\n \"WG Web: <http://tools.ietf.org/wg/netmod/>\n WG List: <mailto:netmod@ietf.org>\n\n WG Chair: David Kessens\n <mailto:david.kessens@nsn.com>\n\n WG Chair: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\n\n Editor: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n description\n \"This module contains a collection of generally useful derived\n YANG data types.\n\n Copyright (c) 2013 IETF Trust and the persons identified as\n authors of the code. All rights reserved.\n\n Redistribution and use in source and binary forms, with or\n without modification, is permitted pursuant to, and subject\n to the license terms contained in, the Simplified BSD License\n set forth in Section 4.c of the IETF Trust's Legal Provisions\n Relating to IETF Documents\n (http://trustee.ietf.org/license-info).\n\n This version of this YANG module is part of RFC 6991; see\n the RFC itself for full legal notices.\";\n\n revision 2024-01-02 {\n description\n \"This revision adds the following new data types:\n - yang-identifier\n - hex-string\n - uuid\n - dotted-quad\";\n reference\n \"RFC 6991: Common YANG Data Types\";\n }\n\n revision 2010-09-24 {\n description\n \"Initial revision.\";\n reference\n \"RFC 6021: Common YANG Data Types\";\n }\n\n /*** collection of counter and gauge types ***/\n\n typedef counter32 {\n type uint32;\n description\n \"The counter32 type represents a non-negative integer\n that monotonically increases until it reaches a\n maximum value of 2^32-1 (4294967295 decimal), when it\n wraps around and starts increasing again from zero.\n\n Counters have no defined 'initial' value, and thus, a\n single value of a counter has (in general) no information\n content. Discontinuities in the monotonically increasing\n value normally occur at re-initialization of the\n management system, and at other times as specified in the\n description of a schema node using this type. If such\n other times can occur, for example, the creation of\n a schema node of type counter32 at times other than\n re-initialization, then a corresponding schema node\n should be defined, with an appropriate type, to indicate\n the last discontinuity.\n\n The counter32 type should not be used for configuration\n schema nodes. A default statement SHOULD NOT be used in\n combination with the type counter32.\n\n In the value set and its semantics, this type is equivalent\n to the Counter32 type of the SMIv2.\";\n reference\n \"RFC 2578: Structure of Management Information Version 2\n (SMIv2)\";\n }\n}\n"
- }
-]
\ No newline at end of file
+++ /dev/null
-{
- "schemas": [
- {
- "moduleName": "M1",
- "revision": "2024-01-01"
- },
- {
- "moduleName": "M2",
- "revision": "2024-01-02"
- }
- ]
-}
\ No newline at end of file
+++ /dev/null
-[
- {
- "moduleName": "M1",
- "revision": "2024-01-01",
- "yangSource": "module M1 {\n\n namespace \"urn:ietf:params:xml:ns:yang:M1\";\n prefix \"yang\";\n\n organization\n \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n contact\n \"WG Web: <http://tools.ietf.org/wg/netmod/>\n WG List: <mailto:netmod@ietf.org>\n\n WG Chair: David Kessens\n <mailto:david.kessens@nsn.com>\n\n WG Chair: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\n\n Editor: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n description\n \"This module contains a collection of generally useful derived\n YANG data types.\n\n Copyright (c) 2013 IETF Trust and the persons identified as\n authors of the code. All rights reserved.\n\n Redistribution and use in source and binary forms, with or\n without modification, is permitted pursuant to, and subject\n to the license terms contained in, the Simplified BSD License\n set forth in Section 4.c of the IETF Trust's Legal Provisions\n Relating to IETF Documents\n (http://trustee.ietf.org/license-info).\n\n This version of this YANG module is part of RFC 6991; see\n the RFC itself for full legal notices.\";\n\n revision 2024-01-01 {\n description\n \"This revision adds the following new data types:\n - yang-identifier\n - hex-string\n - uuid\n - dotted-quad\";\n reference\n \"RFC 6991: Common YANG Data Types\";\n }\n\n revision 2010-09-24 {\n description\n \"Initial revision.\";\n reference\n \"RFC 6021: Common YANG Data Types\";\n }\n\n /*** collection of counter and gauge types ***/\n\n typedef counter32 {\n type uint32;\n description\n \"The counter32 type represents a non-negative integer\n that monotonically increases until it reaches a\n maximum value of 2^32-1 (4294967295 decimal), when it\n wraps around and starts increasing again from zero.\n\n Counters have no defined 'initial' value, and thus, a\n single value of a counter has (in general) no information\n content. Discontinuities in the monotonically increasing\n value normally occur at re-initialization of the\n management system, and at other times as specified in the\n description of a schema node using this type. If such\n other times can occur, for example, the creation of\n a schema node of type counter32 at times other than\n re-initialization, then a corresponding schema node\n should be defined, with an appropriate type, to indicate\n the last discontinuity.\n\n The counter32 type should not be used for configuration\n schema nodes. A default statement SHOULD NOT be used in\n combination with the type counter32.\n\n In the value set and its semantics, this type is equivalent\n to the Counter32 type of the SMIv2.\";\n reference\n \"RFC 2578: Structure of Management Information Version 2\n (SMIv2)\";\n }\n}\n"
- },
- {
- "moduleName": "M3",
- "revision": "2024-01-03",
- "yangSource": "module M3 {\n\n namespace \"urn:ietf:params:xml:ns:yang:M3\";\n prefix \"yang\";\n\n organization\n \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n contact\n \"WG Web: <http://tools.ietf.org/wg/netmod/>\n WG List: <mailto:netmod@ietf.org>\n\n WG Chair: David Kessens\n <mailto:david.kessens@nsn.com>\n\n WG Chair: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\n\n Editor: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n description\n \"This module contains a collection of generally useful derived\n YANG data types.\n\n Copyright (c) 2013 IETF Trust and the persons identified as\n authors of the code. All rights reserved.\n\n Redistribution and use in source and binary forms, with or\n without modification, is permitted pursuant to, and subject\n to the license terms contained in, the Simplified BSD License\n set forth in Section 4.c of the IETF Trust's Legal Provisions\n Relating to IETF Documents\n (http://trustee.ietf.org/license-info).\n\n This version of this YANG module is part of RFC 6991; see\n the RFC itself for full legal notices.\";\n\n revision 2024-01-03 {\n description\n \"This revision adds the following new data types:\n - yang-identifier\n - hex-string\n - uuid\n - dotted-quad\";\n reference\n \"RFC 6991: Common YANG Data Types\";\n }\n\n revision 2010-09-24 {\n description\n \"Initial revision.\";\n reference\n \"RFC 6021: Common YANG Data Types\";\n }\n\n /*** collection of counter and gauge types ***/\n\n typedef counter32 {\n type uint32;\n description\n \"The counter32 type represents a non-negative integer\n that monotonically increases until it reaches a\n maximum value of 2^32-1 (4294967295 decimal), when it\n wraps around and starts increasing again from zero.\n\n Counters have no defined 'initial' value, and thus, a\n single value of a counter has (in general) no information\n content. Discontinuities in the monotonically increasing\n value normally occur at re-initialization of the\n management system, and at other times as specified in the\n description of a schema node using this type. If such\n other times can occur, for example, the creation of\n a schema node of type counter32 at times other than\n re-initialization, then a corresponding schema node\n should be defined, with an appropriate type, to indicate\n the last discontinuity.\n\n The counter32 type should not be used for configuration\n schema nodes. A default statement SHOULD NOT be used in\n combination with the type counter32.\n\n In the value set and its semantics, this type is equivalent\n to the Counter32 type of the SMIv2.\";\n reference\n \"RFC 2578: Structure of Management Information Version 2\n (SMIv2)\";\n }\n}\n"
- }
-]
\ No newline at end of file
+++ /dev/null
-{
- "schemas": [
- {
- "moduleName": "M1",
- "revision": "2024-01-01"
- },
- {
- "moduleName": "M3",
- "revision": "2024-01-03"
- }
- ]
-}
\ No newline at end of file
--- /dev/null
+{
+ "moduleName": "<MODULE_NAME>",
+ "revision": "2024-01-01"
+}
--- /dev/null
+{
+ "moduleName": "<MODULE_NAME>",
+ "revision": "2024-01-01",
+ "yangSource": "module <MODULE_NAME> {\n\n namespace \"urn:ietf:params:xml:ns:yang:<MODULE_NAME>\";\n prefix \"yang\";\n\n organization\n \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n contact\n \"WG Web: <http://tools.ietf.org/wg/netmod/>\n WG List: <mailto:netmod@ietf.org>\n\n WG Chair: David Kessens\n <mailto:david.kessens@nsn.com>\n\n WG Chair: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\n\n Editor: Juergen Schoenwaelder\n <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n description\n \"This module contains a collection of generally useful derived\n YANG data types.\n\n Copyright (c) 2013 IETF Trust and the persons identified as\n authors of the code. All rights reserved.\n\n Redistribution and use in source and binary forms, with or\n without modification, is permitted pursuant to, and subject\n to the license terms contained in, the Simplified BSD License\n set forth in Section 4.c of the IETF Trust's Legal Provisions\n Relating to IETF Documents\n (http://trustee.ietf.org/license-info).\n\n This version of this YANG module is part of RFC 6991; see\n the RFC itself for full legal notices.\";\n\n revision 2024-01-01 {\n description\n \"This revision adds the following new data types:\n - yang-identifier\n - hex-string\n - uuid\n - dotted-quad\";\n reference\n \"RFC 6991: Common YANG Data Types\";\n }\n\n revision 2010-09-24 {\n description\n \"Initial revision.\";\n reference\n \"RFC 6021: Common YANG Data Types\";\n }\n\n /*** collection of counter and gauge types ***/\n\n typedef counter32 {\n type uint32;\n description\n \"The counter32 type represents a non-negative integer\n that monotonically increases until it reaches a\n maximum value of 2^32-1 (4294967295 decimal), when it\n wraps around and starts increasing again from zero.\n\n Counters have no defined 'initial' value, and thus, a\n single value of a counter has (in general) no information\n content. Discontinuities in the monotonically increasing\n value normally occur at re-initialization of the\n management system, and at other times as specified in the\n description of a schema node using this type. If such\n other times can occur, for example, the creation of\n a schema node of type counter32 at times other than\n re-initialization, then a corresponding schema node\n should be defined, with an appropriate type, to indicate\n the last discontinuity.\n\n The counter32 type should not be used for configuration\n schema nodes. A default statement SHOULD NOT be used in\n combination with the type counter32.\n\n In the value set and its semantics, this type is equivalent\n to the Counter32 type of the SMIv2.\";\n reference\n \"RFC 2578: Structure of Management Information Version 2\n (SMIv2)\";\n }\n}\n"
+}
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
\r
<groupId>org.onap.cps</groupId>\r
<artifactId>cps-aggregator</artifactId>\r
- <version>3.4.8-SNAPSHOT</version>\r
+ <version>3.4.9-SNAPSHOT</version>\r
<packaging>pom</packaging>\r
\r
<name>cps</name>\r
--- /dev/null
+distribution_type: container
+container_release_tag: 3.4.8
+project: cps
+log_dir: cps-maven-docker-stage-master/940/
+ref: ad46c250eebd2eec9a99991371f825c778336182
+containers:
+ - name: 'cps-and-ncmp'
+ version: '3.4.8-20240501T114419Z'
--- /dev/null
+distribution_type: maven
+log_dir: cps-maven-stage-master/948/
+project: cps
+version: 3.4.8
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>spotbugs</artifactId>
- <version>3.4.8-SNAPSHOT</version>
+ <version>3.4.9-SNAPSHOT</version>
<properties>
<nexusproxy>https://nexus.onap.org</nexusproxy>
major=3
minor=4
-patch=8
+patch=9
base_version=${major}.${minor}.${patch}