diff options
62 files changed, 1435 insertions, 507 deletions
diff --git a/cps-application/src/main/resources/application.yml b/cps-application/src/main/resources/application.yml index 810068081e..e724ef4443 100644 --- a/cps-application/src/main/resources/application.yml +++ b/cps-application/src/main/resources/application.yml @@ -101,10 +101,10 @@ app: async-m2m: topic: ${NCMP_ASYNC_M2M_TOPIC:ncmp-async-m2m} avc: - subscription-topic: ${NCMP_CM_AVC_SUBSCRIPTION:subscription} - subscription-forward-topic-prefix: ${NCMP_FORWARD_CM_AVC_SUBSCRIPTION:ncmp-dmi-cm-avc-subscription-} - subscription-response-topic: ${NCMP_RESPONSE_CM_AVC_SUBSCRIPTION:dmi-ncmp-cm-avc-subscription} - subscription-outcome-topic: ${NCMP_OUTCOME_CM_AVC_SUBSCRIPTION:subscription-response} + cm-subscription-ncmp-in: ${CM_SUBSCRIPTION_NCMP_IN_TOPIC:subscription} + cm-subscription-dmi-in: ${CM_SUBSCRIPTION_DMI_IN_TOPIC:ncmp-dmi-cm-avc-subscription} + cm-subscription-dmi-out: ${CM_SUBSCRIPTION_DMI_OUT_TOPIC:dmi-ncmp-cm-avc-subscription} + cm-subscription-ncmp-out: ${CM_SUBSCRIPTION_NCMP_OUT_TOPIC:subscription-response} cm-events-topic: ${NCMP_CM_EVENTS_TOPIC:cm-events} lcm: events: @@ -175,9 +175,10 @@ ncmp: dmi: httpclient: connectionTimeoutInSeconds: 30 - maximumConnectionsPerRoute: 50 + readTimeoutInSeconds: 30 + writeTimeoutInSeconds: 30 maximumConnectionsTotal: 100 - idleConnectionEvictionThresholdInSeconds: 5 + maximumInMemorySizeInMegabytes: 16 auth: username: ${DMI_USERNAME:cpsuser} password: ${DMI_PASSWORD:cpsr0cks!} diff --git a/cps-dependencies/pom.xml b/cps-dependencies/pom.xml index effaa06f62..f323cd7077 100644 --- a/cps-dependencies/pom.xml +++ b/cps-dependencies/pom.xml @@ -261,7 +261,7 @@ <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> - <version>1.18.24</version> + <version>1.18.32</version> </dependency> <dependency> <groupId>org.testcontainers</groupId> diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml index 8aa38c0f70..a9311d0861 100644 --- a/cps-ncmp-rest/docs/openapi/components.yaml +++ b/cps-ncmp-rest/docs/openapi/components.yaml @@ -578,7 +578,6 @@ components: in: query description: The format of resource identifier depend on the associated DMI Plugin implementation. For ONAP DMI Plugin it will be RESTConf paths but it can really be anything. required: true - allowReserved: true schema: type: string examples: @@ -598,7 +597,6 @@ components: required: false schema: type: string - allowReserved: true examples: sample 1: value: @@ -616,7 +614,6 @@ components: required: false schema: type: string - allowReserved: true examples: sample 1: value: @@ -628,7 +625,6 @@ components: required: true schema: type: string - allowReserved: true examples: sample 1: value: diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java index 1f87865f21..be5b93c47b 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java @@ -48,7 +48,7 @@ public class NcmpPassthroughResourceRequestHandler extends NcmpDatastoreRequestH private static final Object noReturn = null; - private static final int MAXIMUM_CM_HANDLES_PER_OPERATION = 50000; + private static final int MAXIMUM_CM_HANDLES_PER_OPERATION = 200; private static final String PAYLOAD_TOO_LARGE_TEMPLATE = "Operation '%s' affects too many (%d) cm handles"; diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java index d323691916..7263000896 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java @@ -23,9 +23,10 @@ package org.onap.cps.ncmp.rest.exceptions; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException; import org.onap.cps.ncmp.api.impl.exception.DmiRequestException; -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException; import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException; +import org.onap.cps.ncmp.api.impl.exception.InvalidDmiResourceUrlException; import org.onap.cps.ncmp.api.impl.exception.NcmpException; import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException; import org.onap.cps.ncmp.rest.controller.NetworkCmProxyController; @@ -69,14 +70,15 @@ public class NetworkCmProxyRestExceptionHandler { return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, exception); } - @ExceptionHandler({HttpClientRequestException.class}) + @ExceptionHandler({DmiClientRequestException.class}) public static ResponseEntity<Object> handleClientRequestExceptions( - final HttpClientRequestException httpClientRequestException) { - return wrapDmiErrorResponse(httpClientRequestException); + final DmiClientRequestException dmiClientRequestException) { + return wrapDmiErrorResponse(dmiClientRequestException); } @ExceptionHandler({DmiRequestException.class, DataValidationException.class, OperationNotSupportedException.class, - HttpMessageNotReadableException.class, InvalidTopicException.class, InvalidDatastoreException.class}) + HttpMessageNotReadableException.class, InvalidTopicException.class, InvalidDatastoreException.class, + InvalidDmiResourceUrlException.class}) public static ResponseEntity<Object> handleDmiRequestExceptions(final Exception exception) { return buildErrorResponse(HttpStatus.BAD_REQUEST, exception); } @@ -115,13 +117,13 @@ public class NetworkCmProxyRestExceptionHandler { return new ResponseEntity<>(errorMessage, status); } - private static ResponseEntity<Object> wrapDmiErrorResponse(final HttpClientRequestException - httpClientRequestException) { + private static ResponseEntity<Object> wrapDmiErrorResponse(final DmiClientRequestException + dmiClientRequestException) { final var dmiErrorMessage = new DmiErrorMessage(); final var dmiErrorResponse = new DmiErrorMessageDmiResponse(); - dmiErrorResponse.setHttpCode(httpClientRequestException.getHttpStatus()); - dmiErrorResponse.setBody(httpClientRequestException.getDetails()); - dmiErrorMessage.setMessage(httpClientRequestException.getMessage()); + dmiErrorResponse.setHttpCode(dmiClientRequestException.getHttpStatusCode()); + dmiErrorResponse.setBody(dmiClientRequestException.getResponseBodyAsString()); + dmiErrorMessage.setMessage(dmiClientRequestException.getMessage()); dmiErrorMessage.setDmiResponse(dmiErrorResponse); return new ResponseEntity<>(dmiErrorMessage, HttpStatus.BAD_GATEWAY); } diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy index a79ea25ab8..ea472cd931 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy @@ -31,13 +31,14 @@ import static org.onap.cps.ncmp.rest.exceptions.NetworkCmProxyRestExceptionHandl import static org.onap.cps.ncmp.rest.exceptions.NetworkCmProxyRestExceptionHandlerSpec.ApiType.NCMPINVENTORY import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA import groovy.json.JsonSlurper import org.mapstruct.factory.Mappers import org.onap.cps.TestUtils import org.onap.cps.ncmp.api.NetworkCmProxyDataService import org.onap.cps.ncmp.api.impl.exception.DmiRequestException -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException import org.onap.cps.ncmp.rest.controller.NcmpRestInputMapper import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler @@ -143,7 +144,7 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification { def 'Failing DMI Request - passthrough scenario'() { given: 'failing DMI request' - setupTestException(new HttpClientRequestException('Error Message Details NCMP', 'Bad Request from DMI', 400), NCMP) + setupTestException(new DmiClientRequestException(400, 'Error Message Details NCMP', 'Bad Request from DMI', UNABLE_TO_READ_RESOURCE_DATA), NCMP) when: 'the DMI request is executed' def response = performTestRequest(NCMP) then: 'NCMP service responds with 502 Bad Gateway status' diff --git a/cps-ncmp-service/pom.xml b/cps-ncmp-service/pom.xml index fc41da3aee..77e13dbae8 100644 --- a/cps-ncmp-service/pom.xml +++ b/cps-ncmp-service/pom.xml @@ -70,8 +70,8 @@ <artifactId>mapstruct-processor</artifactId> </dependency> <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-web</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- T E S T - D E P E N D E N C I E S --> <dependency> diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java index 798a280c8a..ef48c43d2a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation + * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,20 +21,31 @@ package org.onap.cps.ncmp.api.impl.client; +import static org.onap.cps.ncmp.api.NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING; +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA; +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + import com.fasterxml.jackson.databind.JsonNode; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Locale; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration.DmiProperties; -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException; +import org.onap.cps.ncmp.api.impl.config.DmiProperties; +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException; +import org.onap.cps.ncmp.api.impl.exception.InvalidDmiResourceUrlException; import org.onap.cps.ncmp.api.impl.operations.OperationType; -import org.springframework.http.HttpEntity; +import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; @Component @RequiredArgsConstructor @@ -43,8 +54,10 @@ public class DmiRestClient { private static final String HEALTH_CHECK_URL_EXTENSION = "/actuator/health"; private static final String NOT_SPECIFIED = ""; - private final RestTemplate restTemplate; + private static final String NO_AUTHORIZATION = null; + private final WebClient webClient; private final DmiProperties dmiProperties; + private final JsonObjectMapper jsonObjectMapper; /** * Sends POST operation to DMI with json body containing module references. @@ -59,14 +72,16 @@ public class DmiRestClient { final String requestBodyAsJsonString, final OperationType operationType, final String authorization) { - final var httpEntity = new HttpEntity<>(requestBodyAsJsonString, configureHttpHeaders(new HttpHeaders(), - authorization)); try { - return restTemplate.postForEntity(dmiResourceUrl, httpEntity, Object.class); - } catch (final HttpStatusCodeException httpStatusCodeException) { - final String exceptionMessage = "Unable to " + operationType.toString() + " resource data."; - throw new HttpClientRequestException(exceptionMessage, httpStatusCodeException.getResponseBodyAsString(), - httpStatusCodeException.getStatusCode().value()); + return ResponseEntity.ok(webClient.post().uri(toUri(dmiResourceUrl)) + .headers(httpHeaders -> configureHttpHeaders(httpHeaders, authorization)) + .body(BodyInserters.fromValue(requestBodyAsJsonString)) + .retrieve() + .bodyToMono(Object.class) + .onErrorMap(httpError -> handleDmiClientException(httpError, operationType.getOperationName())) + .block()); + } catch (final HttpServerErrorException e) { + throw handleDmiClientException(e, operationType.getOperationName()); } } @@ -77,13 +92,14 @@ public class DmiRestClient { * @return plugin health status ("UP" is all OK, "" (not-specified) in case of any exception) */ public String getDmiHealthStatus(final String dmiPluginBaseUrl) { - final HttpEntity<Object> httpHeaders = new HttpEntity<>(configureHttpHeaders(new HttpHeaders(), null)); try { - final JsonNode responseHealthStatus = - restTemplate.getForObject(dmiPluginBaseUrl + HEALTH_CHECK_URL_EXTENSION, - JsonNode.class, httpHeaders); + final JsonNode responseHealthStatus = webClient.get() + .uri(toUri(dmiPluginBaseUrl + HEALTH_CHECK_URL_EXTENSION)) + .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION)) + .retrieve() + .bodyToMono(JsonNode.class).block(); return responseHealthStatus == null ? NOT_SPECIFIED : - responseHealthStatus.get("status").asText(); + responseHealthStatus.get("status").asText(); } catch (final Exception e) { log.warn("Failed to retrieve health status from {}. Error Message: {}", dmiPluginBaseUrl, e.getMessage()); return NOT_SPECIFIED; @@ -96,7 +112,33 @@ public class DmiRestClient { } else if (authorization != null && authorization.toLowerCase(Locale.getDefault()).startsWith("bearer ")) { httpHeaders.add(HttpHeaders.AUTHORIZATION, authorization); } - httpHeaders.setContentType(MediaType.APPLICATION_JSON); return httpHeaders; } + + private static URI toUri(final String dmiResourceUrl) { + try { + return new URI(dmiResourceUrl); + } catch (final URISyntaxException e) { + throw new InvalidDmiResourceUrlException(dmiResourceUrl, BAD_REQUEST.value()); + } + } + + private DmiClientRequestException handleDmiClientException(final Throwable e, final String operationType) { + final String exceptionMessage = "Unable to " + operationType + " resource data."; + if (e instanceof WebClientResponseException wcre) { + if (wcre.getStatusCode().isSameCodeAs(HttpStatus.REQUEST_TIMEOUT)) { + throw new DmiClientRequestException(wcre.getStatusCode().value(), wcre.getMessage(), + jsonObjectMapper.asJsonString(wcre.getResponseBodyAsString()), DMI_SERVICE_NOT_RESPONDING); + } + throw new DmiClientRequestException(wcre.getStatusCode().value(), wcre.getMessage(), + jsonObjectMapper.asJsonString(wcre.getResponseBodyAsString()), UNABLE_TO_READ_RESOURCE_DATA); + + } + if (e instanceof HttpServerErrorException httpServerErrorException) { + throw new DmiClientRequestException(httpServerErrorException.getStatusCode().value(), exceptionMessage, + httpServerErrorException.getResponseBodyAsString(), DMI_SERVICE_NOT_RESPONDING); + } + throw new DmiClientRequestException(INTERNAL_SERVER_ERROR.value(), exceptionMessage, e.getMessage(), + UNKNOWN_ERROR); + } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiProperties.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiProperties.java new file mode 100644 index 0000000000..5453efecd0 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiProperties.java @@ -0,0 +1,55 @@ +/* + * ============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.config; + +import lombok.AccessLevel; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Getter +@Component +public class DmiProperties { + @Value("${ncmp.dmi.auth.username}") + private String authUsername; + @Value("${ncmp.dmi.auth.password}") + private String authPassword; + @Getter(AccessLevel.NONE) + @Value("${ncmp.dmi.api.base-path}") + private String dmiBasePath; + @Value("${ncmp.dmi.auth.enabled}") + private boolean dmiBasicAuthEnabled; + + /** + * Removes both leading and trailing slashes if they are present. + * + * @return dmi base path without any slashes ("/") + */ + public String getDmiBasePath() { + if (dmiBasePath.startsWith("/")) { + dmiBasePath = dmiBasePath.substring(1); + } + if (dmiBasePath.endsWith("/")) { + dmiBasePath = dmiBasePath.substring(0, dmiBasePath.length() - 1); + } + return dmiBasePath; + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfiguration.java new file mode 100644 index 0000000000..2e84f7f69f --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfiguration.java @@ -0,0 +1,70 @@ +/* + * ============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.config; + +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; + +@Configuration +@RequiredArgsConstructor +public class DmiWebClientConfiguration { + + private final HttpClientConfiguration httpClientConfiguration; + + /** + * Configures and create a WebClient bean that triggers an initialization (warmup) of the host name resolver and + * loads the necessary native libraries to avoid the extra time needed to load resources for first request. + * + * @return a WebClient instance. + */ + @Bean + public WebClient webClient() { + final ConnectionProvider dmiWebClientConnectionProvider = ConnectionProvider.create( + "dmiWebClientConnectionPool", httpClientConfiguration.getMaximumConnectionsTotal()); + + final HttpClient httpClient = HttpClient.create(dmiWebClientConnectionProvider) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, + httpClientConfiguration.getConnectionTimeoutInSeconds() * 1000) + .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler( + httpClientConfiguration.getReadTimeoutInSeconds(), TimeUnit.SECONDS)) + .addHandlerLast(new WriteTimeoutHandler( + httpClientConfiguration.getWriteTimeoutInSeconds(), TimeUnit.SECONDS))); + httpClient.warmup().block(); + return WebClient.builder() + .defaultHeaders(header -> header.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)) + .defaultHeaders(header -> header.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize( + httpClientConfiguration.getMaximumInMemorySizeInMegabytes() * 1024 * 1024)) + .build(); + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java index d547e31c68..d2521d9d1e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java @@ -1,6 +1,6 @@ /* * ============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. @@ -20,38 +20,19 @@ package org.onap.cps.ncmp.api.impl.config; -import java.time.Duration; -import java.time.temporal.ChronoUnit; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.convert.DurationUnit; +import org.springframework.stereotype.Component; @Getter @Setter -@ConfigurationProperties(prefix = "ncmp.dmi.httpclient", ignoreUnknownFields = true) +@Component +@ConfigurationProperties(prefix = "ncmp.dmi.httpclient") public class HttpClientConfiguration { - - /** - * The maximum time to establish a connection. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration connectionTimeoutInSeconds = Duration.ofSeconds(180); - - /** - * The maximum number of open connections per route. - */ - private int maximumConnectionsPerRoute = 50; - - /** - * The maximum total number of open connections. - */ - private int maximumConnectionsTotal = maximumConnectionsPerRoute * 2; - - /** - * The duration after which idle connections are evicted. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration idleConnectionEvictionThresholdInSeconds = Duration.ofSeconds(5); - + private Integer maximumConnectionsTotal = 100; + private Integer connectionTimeoutInSeconds = 30; + private Integer readTimeoutInSeconds = 30; + private Integer writeTimeoutInSeconds = 30; + private Integer maximumInMemorySizeInMegabytes = 1; } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java deleted file mode 100644 index c6ff116a7f..0000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 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.config; - -import java.util.Arrays; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.apache.hc.client5.http.config.ConnectionConfig; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; -import org.apache.hc.core5.util.TimeValue; -import org.apache.hc.core5.util.Timeout; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; - -@Configuration -@EnableConfigurationProperties(HttpClientConfiguration.class) -@RequiredArgsConstructor(access = AccessLevel.PROTECTED) -public class NcmpConfiguration { - - @Getter - @Component - public static class DmiProperties { - @Value("${ncmp.dmi.auth.username}") - private String authUsername; - @Value("${ncmp.dmi.auth.password}") - private String authPassword; - @Value("${ncmp.dmi.api.base-path}") - private String dmiBasePath; - @Value("${ncmp.dmi.auth.enabled}") - private boolean dmiBasicAuthEnabled; - } - - /** - * Rest template bean. - * - * @param restTemplateBuilder the rest template builder - * @param httpClientConfiguration the http client configuration - * @return rest template instance - */ - @Bean - @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) - public static RestTemplate restTemplate(final RestTemplateBuilder restTemplateBuilder, - final HttpClientConfiguration httpClientConfiguration) { - - final ConnectionConfig connectionConfig = ConnectionConfig.copy(ConnectionConfig.DEFAULT) - .setConnectTimeout(Timeout.of(httpClientConfiguration.getConnectionTimeoutInSeconds())) - .build(); - - final PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() - .setDefaultConnectionConfig(connectionConfig) - .setMaxConnTotal(httpClientConfiguration.getMaximumConnectionsTotal()) - .setMaxConnPerRoute(httpClientConfiguration.getMaximumConnectionsPerRoute()) - .build(); - - final CloseableHttpClient httpClient = HttpClients.custom() - .setConnectionManager(connectionManager) - .evictExpiredConnections() - .evictIdleConnections( - TimeValue.of(httpClientConfiguration.getIdleConnectionEvictionThresholdInSeconds())) - .build(); - - final ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); - - final RestTemplate restTemplate = restTemplateBuilder - .requestFactory(() -> requestFactory) - .setConnectTimeout(httpClientConfiguration.getConnectionTimeoutInSeconds()) - .build(); - - setRestTemplateMessageConverters(restTemplate); - return restTemplate; - } - - private static void setRestTemplateMessageConverters(final RestTemplate restTemplate) { - final MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = - new MappingJackson2HttpMessageConverter(); - mappingJackson2HttpMessageConverter.setSupportedMediaTypes( - Arrays.asList(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN)); - restTemplate.getMessageConverters().add(mappingJackson2HttpMessageConverter); - } - -} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionDmiOutEventConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionDmiOutEventConsumer.java index fb89aae3f8..3e072b0f91 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionDmiOutEventConsumer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionDmiOutEventConsumer.java @@ -51,7 +51,7 @@ public class CmNotificationSubscriptionDmiOutEventConsumer { * * @param cmNotificationSubscriptionDmiOutEventConsumerRecord the event to be consumed */ - @KafkaListener(topics = "${app.ncmp.avc.subscription-response-topic}", + @KafkaListener(topics = "${app.ncmp.avc.cm-subscription-dmi-out}", containerFactory = "cloudEventConcurrentKafkaListenerContainerFactory") public void consumeCmNotificationSubscriptionDmiOutEvent( final ConsumerRecord<String, CloudEvent> cmNotificationSubscriptionDmiOutEventConsumerRecord) { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionNcmpInEventConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionNcmpInEventConsumer.java index 70135b3079..2c544b7b6a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionNcmpInEventConsumer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionNcmpInEventConsumer.java @@ -28,7 +28,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionHandlerService; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent; -import org.springframework.beans.factory.annotation.Value; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; @@ -39,15 +38,12 @@ public class CmNotificationSubscriptionNcmpInEventConsumer { private final CmNotificationSubscriptionHandlerService cmNotificationSubscriptionHandlerService; - @Value("${notification.enabled:true}") - private boolean notificationFeatureEnabled; - /** * Consume the specified event. * * @param subscriptionEventConsumerRecord the event to be consumed */ - @KafkaListener(topics = "${app.ncmp.avc.subscription-topic}", + @KafkaListener(topics = "${app.ncmp.avc.cm-subscription-ncmp-in}", containerFactory = "cloudEventConcurrentKafkaListenerContainerFactory") public void consumeSubscriptionEvent(final ConsumerRecord<String, CloudEvent> subscriptionEventConsumerRecord) { final CloudEvent cloudEvent = subscriptionEventConsumerRecord.value(); @@ -60,7 +56,7 @@ public class CmNotificationSubscriptionNcmpInEventConsumer { if ("subscriptionCreateRequest".equals(cloudEvent.getType())) { log.info("Subscription for source {} with subscription id {} ...", cloudEvent.getSource(), subscriptionId); cmNotificationSubscriptionHandlerService.processSubscriptionCreateRequest( - cmNotificationSubscriptionNcmpInEvent); + cmNotificationSubscriptionNcmpInEvent); } } }
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionDmiInEventProducer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionDmiInEventProducer.java index 9fbe26848f..3273c556c6 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionDmiInEventProducer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionDmiInEventProducer.java @@ -25,7 +25,6 @@ import io.cloudevents.core.builder.CloudEventBuilder; import java.net.URI; import java.util.UUID; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.onap.cps.events.EventsPublisher; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent; import org.onap.cps.utils.JsonObjectMapper; @@ -34,7 +33,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; @Component -@Slf4j @RequiredArgsConstructor @ConditionalOnProperty(name = "notification.enabled", havingValue = "true", matchIfMissing = true) public class CmNotificationSubscriptionDmiInEventProducer { @@ -42,7 +40,7 @@ public class CmNotificationSubscriptionDmiInEventProducer { private final EventsPublisher<CloudEvent> eventsPublisher; private final JsonObjectMapper jsonObjectMapper; - @Value("${app.ncmp.avc.subscription-forward-topic-prefix}") + @Value("${app.ncmp.avc.cm-subscription-dmi-in}") private String cmNotificationSubscriptionDmiInEventTopic; /** @@ -65,9 +63,10 @@ public class CmNotificationSubscriptionDmiInEventProducer { final String dmiPluginName, final String eventType, final CmNotificationSubscriptionDmiInEvent cmNotificationSubscriptionDmiInEvent) { return CloudEventBuilder.v1().withId(UUID.randomUUID().toString()).withType(eventType) - .withSource(URI.create("NCMP")).withDataSchema(URI.create("org.onap.ncmp.dmi.cm.subscription:1.0.0")) - .withExtension("correlationid", subscriptionId.concat("#").concat(dmiPluginName)) - .withData(jsonObjectMapper.asJsonBytes(cmNotificationSubscriptionDmiInEvent)).build(); + .withSource(URI.create("NCMP")) + .withDataSchema(URI.create("org.onap.ncmp.dmi.cm.subscription:1.0.0")) + .withExtension("correlationid", subscriptionId.concat("#").concat(dmiPluginName)) + .withData(jsonObjectMapper.asJsonBytes(cmNotificationSubscriptionDmiInEvent)).build(); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionNcmpOutEventProducer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionNcmpOutEventProducer.java index ac5de07f0a..ed7ed2a0ba 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionNcmpOutEventProducer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionNcmpOutEventProducer.java @@ -48,7 +48,7 @@ import org.springframework.stereotype.Component; @ConditionalOnProperty(name = "notification.enabled", havingValue = "true", matchIfMissing = true) public class CmNotificationSubscriptionNcmpOutEventProducer { - @Value("${app.ncmp.avc.subscription-outcome-topic}") + @Value("${app.ncmp.avc.cm-subscription-ncmp-out}") private String cmNotificationSubscriptionNcmpOutEventTopic; @Value("${ncmp.timers.subscription-forwarding.dmi-response-timeout-ms}") diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/DmiClientRequestException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/DmiClientRequestException.java new file mode 100644 index 0000000000..ab0fa68938 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/DmiClientRequestException.java @@ -0,0 +1,54 @@ +/* + * ============LICENSE_START======================================================= + * 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. + * 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 lombok.Getter; +import org.onap.cps.ncmp.api.NcmpResponseStatus; + +/** + * Http Client Request exception from dmi service. + */ +@Getter +public class DmiClientRequestException extends NcmpException { + + private static final long serialVersionUID = 6659897770659834797L; + final NcmpResponseStatus ncmpResponseStatus; + final String message; + final String responseBodyAsString; + final int httpStatusCode; + + /** + * Constructor to form exception for dmi service response. + * + * @param httpStatusCode http response code from the client + * @param message response message from the client + * @param responseBodyAsString response body from the client + * @param ncmpResponseStatus ncmp status message and code + */ + public DmiClientRequestException(final int httpStatusCode, final String message, final String responseBodyAsString, + final NcmpResponseStatus ncmpResponseStatus) { + super(message, responseBodyAsString); + this.httpStatusCode = httpStatusCode; + this.message = message; + this.responseBodyAsString = responseBodyAsString; + this.ncmpResponseStatus = ncmpResponseStatus; + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidDmiResourceUrlException.java index 9d307e5d2e..270988b63b 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidDmiResourceUrlException.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022 Nordix Foundation + * 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. @@ -22,24 +22,16 @@ package org.onap.cps.ncmp.api.impl.exception; import lombok.Getter; -/** - * Http Client Request exception for passthrough scenarios. - */ @Getter -public class HttpClientRequestException extends NcmpException { +public class InvalidDmiResourceUrlException extends RuntimeException { + + private static final long serialVersionUID = 2928476384584894968L; - private static final long serialVersionUID = 6659897770659834797L; + private static final String INVALID_DMI_URL = "Invalid dmi resource url"; final Integer httpStatus; - /** - * Constructor to form exception for passthrough scenarios. - * - * @param message message details from NCMP - * @param details response body from the client available as details - * @param httpStatus http status code from the client - */ - public HttpClientRequestException(final String message, final String details, final Integer httpStatus) { - super(message, details); + public InvalidDmiResourceUrlException(final String details, final Integer httpStatus) { + super(String.format(INVALID_DMI_URL + ": %s", details)); this.httpStatus = httpStatus; } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperation.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperation.java index 2e66ac0bf1..7baac34b1f 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperation.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperation.java @@ -40,7 +40,7 @@ public class DmiDataOperation { private String options; private String resourceIdentifier; - private final List<CmHandle> cmHandles = new ArrayList<>(); + private final List<DmiOperationCmHandle> cmHandles = new ArrayList<>(); /** * Create and initialise a (outgoing) DMI data operation. diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java index a9ec1241bc..1e92bfe425 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java @@ -21,8 +21,6 @@ package org.onap.cps.ncmp.api.impl.operations; -import static org.onap.cps.ncmp.api.NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING; -import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA; import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING; import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ; @@ -35,8 +33,8 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.NcmpResponseStatus; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException; +import org.onap.cps.ncmp.api.impl.config.DmiProperties; +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException; import org.onap.cps.ncmp.api.impl.inventory.CmHandleState; import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence; import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; @@ -61,7 +59,7 @@ public class DmiDataOperations extends DmiOperations { public DmiDataOperations(final InventoryPersistence inventoryPersistence, final JsonObjectMapper jsonObjectMapper, - final NcmpConfiguration.DmiProperties dmiProperties, + final DmiProperties dmiProperties, final DmiRestClient dmiRestClient, final DmiServiceUrlBuilder dmiServiceUrlBuilder) { super(inventoryPersistence, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder); @@ -90,9 +88,14 @@ public class DmiDataOperations extends DmiOperations { final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); - final String dmiResourceDataUrl = getDmiRequestUrl(cmResourceAddress.datastoreName(), - cmResourceAddress.cmHandleId(), cmResourceAddress.resourceIdentifier(), optionsParamInQuery, - topicParamInQuery, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); + + final MultiValueMap<String, String> uriQueryParamsMap = getUriQueryParamsMap( + cmResourceAddress.resourceIdentifier(), optionsParamInQuery, + topicParamInQuery, yangModelCmHandle.getModuleSetTag()); + final Map<String, Object> uriVariableParamsMap = getUriVariableParamsMap(cmResourceAddress.datastoreName(), + yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA), cmResourceAddress.cmHandleId()); + final String dmiResourceDataUrl = getDmiRequestUrl(uriQueryParamsMap, uriVariableParamsMap); + return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, READ, authorization); } @@ -111,9 +114,13 @@ public class DmiDataOperations extends DmiOperations { final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId); final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); - final String dmiResourceDataUrl = getDmiRequestUrl(dataStoreName, cmHandleId, "/", - null, null, - yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); + + final MultiValueMap<String, String> uriQueryParamsMap = getUriQueryParamsMap("/", null, + null, yangModelCmHandle.getModuleSetTag()); + final Map<String, Object> uriVariableParamsMap = getUriVariableParamsMap(dataStoreName, + yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA), cmHandleId); + final String dmiResourceDataUrl = getDmiRequestUrl(uriQueryParamsMap, uriVariableParamsMap); + final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, READ, null); @@ -168,9 +175,13 @@ public class DmiDataOperations extends DmiOperations { final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId); final String jsonRequestBody = getDmiRequestBody(operationType, null, requestData, dataType, yangModelCmHandle); - final String dmiUrl = getDmiRequestUrl(PASSTHROUGH_RUNNING.getDatastoreName(), cmHandleId, resourceId, - null, null, - yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); + + final MultiValueMap<String, String> uriQueryParamsMap = getUriQueryParamsMap(resourceId, null, + null, yangModelCmHandle.getModuleSetTag()); + final Map<String, Object> uriVariableParamsMap = getUriVariableParamsMap(PASSTHROUGH_RUNNING.getDatastoreName(), + yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA), cmHandleId); + final String dmiUrl = getDmiRequestUrl(uriQueryParamsMap, uriVariableParamsMap); + final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonRequestBody, operationType, authorization); @@ -195,16 +206,23 @@ public class DmiDataOperations extends DmiOperations { return jsonObjectMapper.asJsonString(dmiRequestBody); } - private String getDmiRequestUrl(final String dataStoreName, - final String cmHandleId, - final String resourceId, - final String optionsParamInQuery, - final String topicParamInQuery, - final String dmiServiceName) { - return dmiServiceUrlBuilder.getDmiDatastoreUrl( - dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery, - topicParamInQuery), dmiServiceUrlBuilder.populateUriVariables(dataStoreName, dmiServiceName, - cmHandleId)); + private String getDmiRequestUrl(final MultiValueMap<String, String> uriQueryParamsMap, + final Map<String, Object> uriVariableParamsMap) { + return dmiServiceUrlBuilder.getDmiDatastoreUrl(uriQueryParamsMap, uriVariableParamsMap); + } + + private MultiValueMap<String, String> getUriQueryParamsMap(final String resourceId, + final String optionsParamInQuery, + final String topicParamInQuery, + final String moduleSetTagParamInQuery) { + return dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery, + topicParamInQuery, moduleSetTagParamInQuery); + } + + private Map<String, Object> getUriVariableParamsMap(final String dataStoreName, + final String dmiServiceName, + final String cmHandleId) { + return dmiServiceUrlBuilder.populateUriVariables(dataStoreName, dmiServiceName, cmHandleId); } private String getDmiServiceDataOperationRequestUrl(final String dmiServiceName, @@ -226,7 +244,7 @@ public class DmiDataOperations extends DmiOperations { } private static Set<String> getDistinctCmHandleIdsFromDataOperationRequest(final DataOperationRequest - dataOperationRequest) { + dataOperationRequest) { return dataOperationRequest.getDataOperationDefinitions().stream() .flatMap(dataOperationDefinition -> dataOperationDefinition.getCmHandleIds().stream()).collect(Collectors.toSet()); @@ -235,7 +253,7 @@ public class DmiDataOperations extends DmiOperations { private void buildDataOperationRequestUrlAndSendToDmiService(final String topicParamInQuery, final String requestId, final Map<String, List<DmiDataOperation>> - groupsOutPerDmiServiceName, + groupsOutPerDmiServiceName, final String authorization) { groupsOutPerDmiServiceName.forEach((dmiServiceName, dmiDataOperationRequestBodies) -> { @@ -256,36 +274,29 @@ public class DmiDataOperations extends DmiOperations { try { dmiRestClient.postOperationWithJsonData(dataOperationResourceUrl, dmiDataOperationRequestAsJsonString, READ, authorization); - } catch (final Exception exception) { - handleTaskCompletionException(exception, dataOperationResourceUrl, dmiDataOperationRequestBodies); + } catch (final DmiClientRequestException e) { + handleTaskCompletionException(e, dataOperationResourceUrl, dmiDataOperationRequestBodies); } } - private void handleTaskCompletionException(final Throwable throwable, + private void handleTaskCompletionException(final DmiClientRequestException dmiClientRequestException, final String dataOperationResourceUrl, final List<DmiDataOperation> dmiDataOperationRequestBodies) { - if (throwable != null) { - final MultiValueMap<String, String> dataOperationResourceUrlParameters = - UriComponentsBuilder.fromUriString(dataOperationResourceUrl).build().getQueryParams(); - final String topicName = dataOperationResourceUrlParameters.get("topic").get(0); - final String requestId = dataOperationResourceUrlParameters.get("requestId").get(0); - - final MultiValueMap<DmiDataOperation, Map<NcmpResponseStatus, List<String>>> - cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>(); - - dmiDataOperationRequestBodies.forEach(dmiDataOperationRequestBody -> { - final List<String> cmHandleIds = dmiDataOperationRequestBody.getCmHandles().stream() - .map(CmHandle::getId).toList(); - if (throwable.getCause() instanceof HttpClientRequestException) { - cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody, - Map.of(UNABLE_TO_READ_RESOURCE_DATA, cmHandleIds)); - } else { - cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody, - Map.of(DMI_SERVICE_NOT_RESPONDING, cmHandleIds)); - } - }); - ResourceDataOperationRequestUtils.publishErrorMessageToClientTopic(topicName, requestId, - cmHandleIdsPerResponseCodesPerOperation); - } + final MultiValueMap<String, String> dataOperationResourceUrlParameters = + UriComponentsBuilder.fromUriString(dataOperationResourceUrl).build().getQueryParams(); + final String topicName = dataOperationResourceUrlParameters.get("topic").get(0); + final String requestId = dataOperationResourceUrlParameters.get("requestId").get(0); + + final MultiValueMap<DmiDataOperation, Map<NcmpResponseStatus, List<String>>> + cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>(); + + dmiDataOperationRequestBodies.forEach(dmiDataOperationRequestBody -> { + final List<String> cmHandleIds = dmiDataOperationRequestBody.getCmHandles().stream() + .map(DmiOperationCmHandle::getId).toList(); + cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody, + Map.of(dmiClientRequestException.getNcmpResponseStatus(), cmHandleIds)); + }); + ResourceDataOperationRequestUtils.publishErrorMessageToClientTopic(topicName, requestId, + cmHandleIdsPerResponseCodesPerOperation); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java index 798f6de810..d54dcb8deb 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java @@ -34,7 +34,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; +import org.onap.cps.ncmp.api.impl.config.DmiProperties; import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence; import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; @@ -57,7 +57,7 @@ public class DmiModelOperations extends DmiOperations { */ public DmiModelOperations(final InventoryPersistence inventoryPersistence, final JsonObjectMapper jsonObjectMapper, - final NcmpConfiguration.DmiProperties dmiProperties, + final DmiProperties dmiProperties, final DmiRestClient dmiRestClient, final DmiServiceUrlBuilder dmiServiceUrlBuilder) { super(inventoryPersistence, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/CmHandle.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperationCmHandle.java index 618da74543..1bf2b77dcc 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/CmHandle.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperationCmHandle.java @@ -29,14 +29,21 @@ import lombok.Getter; @JsonInclude(JsonInclude.Include.NON_NULL) @Getter @Builder -public class CmHandle { +public class DmiOperationCmHandle { private String id; @JsonProperty("cmHandleProperties") private Map<String, String> dmiProperties; + private String moduleSetTag; - public static CmHandle buildCmHandleWithProperties(final String cmHandleId, - final Map<String, String> dmiProperties) { - return CmHandle.builder().id(cmHandleId).dmiProperties(dmiProperties).build(); + /** + * Builds Dmi Operation Cm Handle object with all its associated properties. + */ + public static DmiOperationCmHandle buildDmiOperationCmHandle(final String cmHandleId, + final Map<String, String> dmiProperties, + final String moduleSetTag) { + return DmiOperationCmHandle.builder().id(cmHandleId) + .dmiProperties(dmiProperties).moduleSetTag(moduleSetTag) + .build(); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java index c8d73eac63..c195ab3096 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation + * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,7 @@ package org.onap.cps.ncmp.api.impl.operations; import lombok.RequiredArgsConstructor; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; +import org.onap.cps.ncmp.api.impl.config.DmiProperties; import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence; import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; import org.onap.cps.utils.JsonObjectMapper; @@ -35,7 +35,7 @@ public class DmiOperations { protected final InventoryPersistence inventoryPersistence; protected final JsonObjectMapper jsonObjectMapper; - protected final NcmpConfiguration.DmiProperties dmiProperties; + protected final DmiProperties dmiProperties; protected final DmiRestClient dmiRestClient; protected final DmiServiceUrlBuilder dmiServiceUrlBuilder; @@ -44,6 +44,4 @@ public class DmiOperations { .pathSegment("{resourceName}") .buildAndExpand(dmiServiceName, dmiProperties.getDmiBasePath(), cmHandle, resourceName).toUriString(); } - - } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java index 04acaa5e9b..9234d3c2f1 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java @@ -20,12 +20,14 @@ package org.onap.cps.ncmp.api.impl.utils; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.util.Strings; import org.apache.logging.log4j.util.TriConsumer; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; +import org.onap.cps.ncmp.api.impl.config.DmiProperties; import org.onap.cps.spi.utils.CpsValidator; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; @@ -35,8 +37,7 @@ import org.springframework.web.util.UriComponentsBuilder; @Component @RequiredArgsConstructor public class DmiServiceUrlBuilder { - - private final NcmpConfiguration.DmiProperties dmiProperties; + private final DmiProperties dmiProperties; private final CpsValidator cpsValidator; /** @@ -133,20 +134,24 @@ public class DmiServiceUrlBuilder { * This method is used to populate map from query params. * * @param resourceId unique id of response for valid topic - * @param optionsParamInQuery options into url param - * @param topicParamInQuery topic into url param + * @param optionsParamInQuery options as provided by client + * @param topicParamInQuery topic as provided by client + * @param moduleSetTag module set tag associated with the given cm handle * @return all valid query params as map */ public MultiValueMap<String, String> populateQueryParams(final String resourceId, final String optionsParamInQuery, - final String topicParamInQuery) { + final String topicParamInQuery, + final String moduleSetTag) { final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); - getQueryParamConsumer().accept("resourceIdentifier", - resourceId, queryParams); + getQueryParamConsumer().accept("resourceIdentifier", resourceId, queryParams); getQueryParamConsumer().accept("options", optionsParamInQuery, queryParams); if (Strings.isNotEmpty(topicParamInQuery)) { getQueryParamConsumer().accept("topic", topicParamInQuery, queryParams); } + if (Strings.isNotEmpty(moduleSetTag)) { + getQueryParamConsumer().accept("moduleSetTag", moduleSetTag, queryParams); + } return queryParams; } @@ -168,7 +173,7 @@ public class DmiServiceUrlBuilder { private TriConsumer<String, String, MultiValueMap<String, String>> getQueryParamConsumer() { return (paramName, paramValue, paramMap) -> { if (Strings.isNotEmpty(paramValue)) { - paramMap.add(paramName, paramValue); + paramMap.add(paramName, URLEncoder.encode(paramValue, StandardCharsets.UTF_8)); } }; } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java index 4b016b37d1..dc4108cac0 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java @@ -39,8 +39,8 @@ import lombok.extern.slf4j.Slf4j; import org.onap.cps.events.EventsPublisher; import org.onap.cps.ncmp.api.NcmpResponseStatus; import org.onap.cps.ncmp.api.impl.inventory.CmHandleState; -import org.onap.cps.ncmp.api.impl.operations.CmHandle; import org.onap.cps.ncmp.api.impl.operations.DmiDataOperation; +import org.onap.cps.ncmp.api.impl.operations.DmiOperationCmHandle; import org.onap.cps.ncmp.api.impl.utils.DmiServiceNameOrganizer; import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; @@ -81,6 +81,8 @@ public class ResourceDataOperationRequestUtils { final Map<String, String> dmiServiceNamesPerCmHandleId = getDmiServiceNamesPerCmHandleId(dmiPropertiesPerCmHandleIdPerServiceName); + final Map<String, String> moduleSetTagPerCmHandle = getModuleSetTagPerCmHandleId(yangModelCmHandles); + for (final DataOperationDefinition dataOperationDefinitionIn : dataOperationRequestIn.getDataOperationDefinitions()) { final List<String> nonExistingCmHandleIds = new ArrayList<>(); @@ -97,9 +99,10 @@ public class ResourceDataOperationRequestUtils { } else { final DmiDataOperation dmiBatchOperationOut = getOrAddDmiBatchOperation(dmiServiceName, dataOperationDefinitionIn, dmiDataOperationsOutPerDmiServiceName); - final CmHandle cmHandle = CmHandle.buildCmHandleWithProperties(cmHandleId, - cmHandleIdProperties); - dmiBatchOperationOut.getCmHandles().add(cmHandle); + final DmiOperationCmHandle dmiOperationCmHandle = DmiOperationCmHandle + .buildDmiOperationCmHandle(cmHandleId, cmHandleIdProperties, + moduleSetTagPerCmHandle.get(cmHandleId)); + dmiBatchOperationOut.getCmHandles().add(dmiOperationCmHandle); } } } @@ -114,6 +117,14 @@ public class ResourceDataOperationRequestUtils { return dmiDataOperationsOutPerDmiServiceName; } + private static Map<String, String> getModuleSetTagPerCmHandleId( + final Collection<YangModelCmHandle> yangModelCmHandles) { + final Map<String, String> moduleSetTagPerCmHandle = new HashMap<>(yangModelCmHandles.size()); + yangModelCmHandles.forEach(yangModelCmHandle -> + moduleSetTagPerCmHandle.put(yangModelCmHandle.getId(), yangModelCmHandle.getModuleSetTag())); + return moduleSetTagPerCmHandle; + } + /** * Handles the async task completion for an entire data, publishing errors to client topic on task failure. * diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy index c8e34b1a5e..d2dce06b04 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation + * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,28 +21,33 @@ package org.onap.cps.ncmp.api.impl.client +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ +import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH +import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE +import static org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE +import static org.onap.cps.ncmp.api.NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA + import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration.DmiProperties; -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException +import org.onap.cps.ncmp.api.impl.config.DmiWebClientConfiguration +import org.onap.cps.ncmp.api.impl.exception.InvalidDmiResourceUrlException +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException import org.onap.cps.ncmp.utils.TestUtils +import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders -import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.test.context.ContextConfiguration import org.springframework.web.client.HttpServerErrorException -import org.springframework.web.client.RestTemplate +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono import spock.lang.Specification - -import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ -import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH -import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE +import org.springframework.web.reactive.function.client.WebClientResponseException +import org.onap.cps.ncmp.api.impl.config.DmiProperties @SpringBootTest @ContextConfiguration(classes = [DmiProperties, DmiRestClient, ObjectMapper]) @@ -52,61 +57,103 @@ class DmiRestClientSpec extends Specification { static final BASIC_AUTH_HEADER = 'Basic c29tZS11c2VyOnNvbWUtcGFzc3dvcmQ=' static final BEARER_AUTH_HEADER = 'Bearer my-bearer-token' - @SpringBean - RestTemplate mockRestTemplate = Mock(RestTemplate) - @Autowired - NcmpConfiguration.DmiProperties dmiProperties + DmiProperties dmiProperties @Autowired DmiRestClient objectUnderTest - @Autowired - ObjectMapper objectMapper + @SpringBean + WebClient mockWebClient = Mock(WebClient); - def responseFromRestTemplate = Mock(ResponseEntity) + @SpringBean + JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + + def mockRequestBodyUriSpec = Mock(WebClient.RequestBodyUriSpec) + def mockResponseSpec = Mock(WebClient.ResponseSpec) + def mockResponseEntity = Mock(ResponseEntity) + + def setup() { + mockRequestBodyUriSpec.uri(_) >> mockRequestBodyUriSpec + mockRequestBodyUriSpec.headers(_) >> mockRequestBodyUriSpec + mockRequestBodyUriSpec.retrieve() >> mockResponseSpec + } def 'DMI POST operation with JSON.'() { - given: 'the rest template returns a valid response entity for the expected parameters' - mockRestTemplate.postForEntity('my url', _ as HttpEntity, Object.class) >> responseFromRestTemplate + given: 'the web client returns a valid response entity for the expected parameters' + mockWebClient.post() >> mockRequestBodyUriSpec + mockRequestBodyUriSpec.body(_) >> mockRequestBodyUriSpec + def monoSpec = Mono.just(mockResponseEntity) + mockResponseSpec.bodyToMono(Object.class) >> monoSpec + monoSpec.block() >> mockResponseEntity when: 'POST operation is invoked' - def result = objectUnderTest.postOperationWithJsonData('my url', 'some json', READ, null) + def response = objectUnderTest.postOperationWithJsonData('/my/url', 'some json', READ, null) then: 'the output of the method is equal to the output from the test template' - result == responseFromRestTemplate + assert response.statusCode.value() == 200 + assert response.hasBody() } - def 'Failing DMI POST operation.'() { - given: 'the rest template returns a valid response entity' - def serverResponse = 'server response'.getBytes() - def httpServerErrorException = new HttpServerErrorException(HttpStatus.FORBIDDEN, 'status text', serverResponse, null) - mockRestTemplate.postForEntity(*_) >> { throw httpServerErrorException } + def 'Failing DMI POST operation for server error'() { + given: 'the web client throws an exception' + mockWebClient.post() >> { throw new HttpServerErrorException(SERVICE_UNAVAILABLE, null, null, null) } when: 'POST operation is invoked' - def result = objectUnderTest.postOperationWithJsonData('some url', 'some json', operation, null) - then: 'a Http Client Exception is thrown' - def thrown = thrown(HttpClientRequestException) + objectUnderTest.postOperationWithJsonData('/some', 'some json', READ, null) + then: 'a http client exception is thrown' + def thrown = thrown(DmiClientRequestException) + and: 'the exception has the relevant details from the error response' + assert thrown.ncmpResponseStatus.code == '102' + assert thrown.httpStatusCode == 503 + } + + def 'Failing DMI POST operation due to invalid dmi resource url.'() { + when: 'POST operation is invoked with invalid dmi resource url' + objectUnderTest.postOperationWithJsonData('/invalid dmi url', null, null, null) + then: 'invalid dmi resource url exception is thrown' + def thrown = thrown(InvalidDmiResourceUrlException) and: 'the exception has the relevant details from the error response' - assert thrown.httpStatus == 403 - assert thrown.message == "Unable to ${operation} resource data." - assert thrown.details == 'server response' - where: 'the following operation is executed' + assert thrown.httpStatus == 400 + assert thrown.message == 'Invalid dmi resource url: /invalid dmi url' + where: 'the following operations are executed' operation << [CREATE, READ, PATCH] } + def 'Dmi service sends client error response when #scenario'() { + given: 'the web client unable to return response entity but error' + mockWebClient.post() >> mockRequestBodyUriSpec + mockRequestBodyUriSpec.body(_) >> mockRequestBodyUriSpec + def monoSpec = Mono.error(new WebClientResponseException('message', httpStatusCode, null, null, null, null)) + mockResponseSpec.bodyToMono(Object.class) >> monoSpec + when: 'POST operation is invoked' + objectUnderTest.postOperationWithJsonData('/my/url', 'some json', READ, null) + then: 'a http client exception is thrown' + def thrown = thrown(DmiClientRequestException) + and: 'the exception has the relevant details from the error response' + assert thrown.ncmpResponseStatus.code == expectedNcmpResponseStatusCode + assert thrown.httpStatusCode == httpStatusCode + where: 'the following errors occur' + scenario | httpStatusCode | expectedNcmpResponseStatusCode + 'dmi request timeout' | 408 | DMI_SERVICE_NOT_RESPONDING.code + 'other error code' | 500 | UNABLE_TO_READ_RESOURCE_DATA.code + } + def 'Dmi trust level is determined by spring boot health status'() { given: 'a health check response' def dmiPluginHealthCheckResponseJsonData = TestUtils.getResourceFileContent('dmiPluginHealthCheckResponse.json') - def jsonNode = objectMapper.readValue(dmiPluginHealthCheckResponseJsonData, JsonNode.class) + def jsonNode = jsonObjectMapper.convertJsonString(dmiPluginHealthCheckResponseJsonData, JsonNode.class) ((ObjectNode) jsonNode).put('status', 'my status') - mockRestTemplate.getForObject(*_) >> {jsonNode} + def monoResponse = Mono.just(jsonNode) + mockWebClient.get() >> mockRequestBodyUriSpec + mockResponseSpec.bodyToMono(_) >> monoResponse + monoResponse.block() >> jsonNode when: 'get trust level of the dmi plugin' - def result = objectUnderTest.getDmiHealthStatus('some url') + def result = objectUnderTest.getDmiHealthStatus('some/url') then: 'the status value from the json is return' assert result == 'my status' } def 'Failing to get dmi plugin health status #scenario'() { given: 'rest template with #scenario' - mockRestTemplate.getForObject(*_) >> healthStatusResponse + mockWebClient.get() >> healthStatusResponse when: 'attempt to get health status of the dmi plugin' def result = objectUnderTest.getDmiHealthStatus('some url') then: 'result will be empty' @@ -132,5 +179,4 @@ class DmiRestClientSpec extends Specification { 'DMI basic auth disabled, with NCMP bearer token' | false | BEARER_AUTH_HEADER || BEARER_AUTH_HEADER 'DMI basic auth disabled, with NCMP basic auth' | false | BASIC_AUTH_HEADER || NO_AUTH_HEADER } - } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfigurationSpec.groovy new file mode 100644 index 0000000000..ee7ab3f28d --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfigurationSpec.groovy @@ -0,0 +1,52 @@ +/*- + * ============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.config + +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import org.springframework.web.reactive.function.client.WebClient +import spock.lang.Specification + +@SpringBootTest +@ContextConfiguration(classes = [HttpClientConfiguration]) +@TestPropertySource(properties = ['ncmp.dmi.httpclient.connectionTimeoutInSeconds=1', 'ncmp.dmi.httpclient.maximumInMemorySizeInMegabytes=1']) +@EnableConfigurationProperties +class DmiWebClientConfigurationSpec extends Specification { + + def httpClientConfiguration = Spy(HttpClientConfiguration.class) + + def objectUnderTest = new DmiWebClientConfiguration(httpClientConfiguration) + + def 'Web Client Configuration construction.'() { + expect: 'the system can create an instance' + new DmiWebClientConfiguration(httpClientConfiguration) != null + } + + def 'Creating a WebClient instance.'() { + given: 'WebClient configuration invoked' + def webClientInstance = objectUnderTest.webClient() + expect: 'the system can create an instance' + assert webClientInstance != null + assert webClientInstance instanceof WebClient + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy index 2c76b5bb44..4ede360e68 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy @@ -1,6 +1,6 @@ /*- * ============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. @@ -17,32 +17,31 @@ * SPDX-License-Identifier: Apache-2.0 * ============LICENSE_END========================================================= */ + package org.onap.cps.ncmp.api.impl.config -import java.time.Duration import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource -import org.springframework.test.context.support.AnnotationConfigContextLoader import spock.lang.Specification @SpringBootTest @ContextConfiguration(classes = [HttpClientConfiguration]) @EnableConfigurationProperties(HttpClientConfiguration.class) -@TestPropertySource(properties = ["ncmp.dmi.httpclient.connectionTimeoutInSeconds=1", "ncmp.dmi.httpclient.maximumConnectionsTotal=200"]) +@TestPropertySource(properties = ["ncmp.dmi.httpclient.readTimeoutInSeconds=123", "ncmp.dmi.httpclient.maximumConnectionsTotal=200"]) class HttpClientConfigurationSpec extends Specification { @Autowired private HttpClientConfiguration httpClientConfiguration def 'Test HttpClientConfiguration properties with custom and default values'() { - expect: 'custom property values' - assert httpClientConfiguration.getConnectionTimeoutInSeconds() == Duration.ofSeconds(1) - assert httpClientConfiguration.getMaximumConnectionsTotal() == 200 - and: 'default property values' - assert httpClientConfiguration.getMaximumConnectionsPerRoute() == 50 - assert httpClientConfiguration.getIdleConnectionEvictionThresholdInSeconds() == Duration.ofSeconds(5) + expect: 'properties are populated correctly' + assert httpClientConfiguration.connectionTimeoutInSeconds == 123 + assert httpClientConfiguration.readTimeoutInSeconds == 123 + assert httpClientConfiguration.writeTimeoutInSeconds == 30 + assert httpClientConfiguration.maximumConnectionsTotal == 200 + assert httpClientConfiguration.maximumInMemorySizeInMegabytes == 16 } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy deleted file mode 100644 index 74e3424054..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy +++ /dev/null @@ -1,70 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 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.config - -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.web.client.RestTemplateBuilder -import org.springframework.http.MediaType -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter -import org.springframework.test.context.ContextConfiguration -import org.springframework.web.client.RestTemplate -import spock.lang.Specification - -@SpringBootTest -@ContextConfiguration(classes = [NcmpConfiguration.DmiProperties, HttpClientConfiguration]) -class NcmpConfigurationSpec extends Specification{ - - @Autowired - NcmpConfiguration.DmiProperties dmiProperties - - @Autowired - HttpClientConfiguration httpClientConfiguration - - def mockRestTemplateBuilder = new RestTemplateBuilder() - - def 'NcmpConfiguration Construction.'() { - expect: 'the system can create an instance' - new NcmpConfiguration() != null - } - - def 'DMI Properties.'() { - expect: 'properties are set to values in test configuration yaml file' - dmiProperties.authUsername == 'some-user' - dmiProperties.authPassword == 'some-password' - } - - def 'Rest Template creation with CloseableHttpClient and MappingJackson2HttpMessageConverter.'() { - when: 'a rest template is created' - def result = NcmpConfiguration.restTemplate(mockRestTemplateBuilder, httpClientConfiguration) - then: 'the rest template is returned' - assert result instanceof RestTemplate - and: 'the rest template is created with httpclient5' - assert result.getRequestFactory() instanceof HttpComponentsClientHttpRequestFactory - assert ((HttpComponentsClientHttpRequestFactory) result.getRequestFactory()).getHttpClient() instanceof CloseableHttpClient; - and: 'a jackson media converter has been added' - def lastMessageConverter = result.getMessageConverters().get(result.getMessageConverters().size()-1) - lastMessageConverter instanceof MappingJackson2HttpMessageConverter - and: 'the jackson media converters supports the expected media types' - lastMessageConverter.getSupportedMediaTypes() == [MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN]; - } -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumerSpec.groovy index 9c84c51b2d..f07f3c1e6f 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumerSpec.groovy @@ -72,8 +72,6 @@ class CmNotificationSubscriptionNcmpInEventConsumerSpec extends MessagingBaseSpe .withSource(URI.create('some-resource')) .withExtension('correlationid', 'test-cmhandle1').build() def consumerRecord = new ConsumerRecord<String, CloudEvent>('topic-name', 0, 0, 'event-key', testCloudEventSent) - and: 'notifications are enabled' - objectUnderTest.notificationFeatureEnabled = true when: 'the valid event is consumed' objectUnderTest.consumeSubscriptionEvent(consumerRecord) then: 'an event is logged with level INFO' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy index eb6c7a0f48..d04052f792 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy @@ -21,10 +21,18 @@ package org.onap.cps.ncmp.api.impl.operations +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA +import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING +import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ +import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE + import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.events.EventsPublisher -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException +import org.onap.cps.ncmp.api.impl.config.DmiProperties +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext import org.onap.cps.ncmp.api.models.DataOperationRequest @@ -40,19 +48,8 @@ import org.springframework.http.ResponseEntity import org.springframework.test.context.ContextConfiguration import spock.lang.Shared -import java.util.concurrent.TimeoutException - -import static org.onap.cps.ncmp.api.NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING -import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA -import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent -import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL -import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING -import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE -import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ -import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE - @SpringBootTest -@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, NcmpConfiguration.DmiProperties, DmiDataOperations]) +@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, DmiProperties, DmiDataOperations]) class DmiDataOperationsSpec extends DmiOperationsBaseSpec { @SpringBean @@ -105,7 +102,7 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { and: 'a positive response from DMI service when it is called with valid request parameters' def responseFromDmi = new ResponseEntity<Object>(HttpStatus.ACCEPTED) def expectedDmiBatchResourceDataUrl = "ncmp/v1/data/topic=my-topic-name" - def expectedBatchRequestAsJson = '{"operations":[{"operation":"read","operationId":"operational-14","datastore":"ncmp-datastore:passthrough-operational","options":"some option","resourceIdentifier":"some resource identifier","cmHandles":[{"id":"some-cm-handle","cmHandleProperties":{"prop1":"val1"}}]}]}' + def expectedBatchRequestAsJson = '{"operations":[{"operation":"read","operationId":"operational-14","datastore":"ncmp-datastore:passthrough-operational","options":"some option","resourceIdentifier":"some resource identifier","cmHandles":[{"id":"some-cm-handle","moduleSetTag":"","cmHandleProperties":{"prop1":"val1"}}]}]}' mockDmiRestClient.postOperationWithJsonData(expectedDmiBatchResourceDataUrl, _, READ.operationName, NO_AUTH_HEADER) >> responseFromDmi dmiServiceUrlBuilder.getDataOperationRequestUrl(_, _) >> expectedDmiBatchResourceDataUrl when: 'get resource data for group of cm handles are invoked' @@ -114,26 +111,22 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { 1 * mockDmiRestClient.postOperationWithJsonData(expectedDmiBatchResourceDataUrl, expectedBatchRequestAsJson, READ, NO_AUTH_HEADER) } - def 'Execute (async) data operation from DMI service for #scenario.'() { + def 'Execute (async) data operation from DMI service with dmi client exception.'() { given: 'data operation request body and dmi resource url' def dmiDataOperation = DmiDataOperation.builder().operationId('some-operation-id').build() - dmiDataOperation.getCmHandles().add(CmHandle.builder().id('some-cm-handle-id').build()) + dmiDataOperation.getCmHandles().add(DmiOperationCmHandle.builder().id('some-cm-handle-id').build()) def dmiDataOperationResourceDataUrl = "http://dmi-service-name:dmi-port/dmi/v1/data?topic=my-topic-name&requestId=some-request-id" def actualDataOperationCloudEvent = null when: 'exception occurs after sending request to dmi service' - objectUnderTest.handleTaskCompletionException(new Throwable(exception), dmiDataOperationResourceDataUrl, List.of(dmiDataOperation)) + objectUnderTest.handleTaskCompletionException(new DmiClientRequestException(123, 'message', 'details', UNABLE_TO_READ_RESOURCE_DATA), dmiDataOperationResourceDataUrl, List.of(dmiDataOperation)) then: 'a cloud event is published' eventsPublisher.publishCloudEvent('my-topic-name', 'some-request-id', _) >> { args -> actualDataOperationCloudEvent = args[2] } and: 'the event contains the expected error details' def eventDataValue = extractDataValue(actualDataOperationCloudEvent) assert eventDataValue.operationId == dmiDataOperation.operationId assert eventDataValue.ids == dmiDataOperation.cmHandles.id - assert eventDataValue.statusCode == responseCode.code - assert eventDataValue.statusMessage == responseCode.message - where: 'the following exceptions are occurred' - scenario | exception || responseCode - 'http client request exception' | new HttpClientRequestException('error-message', 'error-details', HttpStatus.SERVICE_UNAVAILABLE.value()) || UNABLE_TO_READ_RESOURCE_DATA - 'timeout exception' | new TimeoutException() || DMI_SERVICE_NOT_RESPONDING + assert eventDataValue.statusCode == '103' + assert eventDataValue.statusMessage == UNABLE_TO_READ_RESOURCE_DATA.message } def 'call get all resource data.'() { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy index 9aab467479..88af0479db 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy @@ -23,7 +23,7 @@ package org.onap.cps.ncmp.api.impl.operations import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration +import org.onap.cps.ncmp.api.impl.config.DmiProperties import org.onap.cps.spi.model.ModuleReference import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean @@ -37,7 +37,7 @@ import spock.lang.Shared import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ @SpringBootTest -@ContextConfiguration(classes = [NcmpConfiguration.DmiProperties, DmiModelOperations]) +@ContextConfiguration(classes = [DmiProperties, DmiModelOperations]) class DmiModelOperationsSpec extends DmiOperationsBaseSpec { @Shared diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy index 72a0f2f11a..3518440cab 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation + * Copyright (C) 2021-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. @@ -22,7 +22,7 @@ package org.onap.cps.ncmp.api.impl.operations import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.impl.client.DmiRestClient -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration +import org.onap.cps.ncmp.api.impl.config.DmiProperties import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder import org.onap.cps.ncmp.api.impl.inventory.CmHandleState @@ -50,7 +50,7 @@ abstract class DmiOperationsBaseSpec extends Specification { ObjectMapper spyObjectMapper = Spy() @SpringBean - DmiServiceUrlBuilder dmiServiceUrlBuilder = new DmiServiceUrlBuilder(new NcmpConfiguration.DmiProperties(), mockCpsValidator) + DmiServiceUrlBuilder dmiServiceUrlBuilder = new DmiServiceUrlBuilder(new DmiProperties(), mockCpsValidator) def yangModelCmHandle = new YangModelCmHandle() def static dmiServiceName = 'some service name' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy index fbf2c3d78d..2c7fa654cf 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy @@ -22,10 +22,10 @@ package org.onap.cps.ncmp.api.impl.utils import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING +import org.onap.cps.ncmp.api.impl.config.DmiProperties import org.onap.cps.ncmp.api.impl.operations.RequiredDmiService import org.onap.cps.spi.utils.CpsValidator import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle import spock.lang.Specification @@ -34,7 +34,7 @@ class DmiServiceUrlBuilderSpec extends Specification { static YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle('dmiServiceName', 'dmiDataServiceName', 'dmiModuleServiceName', new NcmpServiceCmHandle(cmHandleId: 'some-cm-handle-id'),'my-module-set-tag', 'my-alternate-id', 'my-data-producer-identifier') - NcmpConfiguration.DmiProperties dmiProperties = new NcmpConfiguration.DmiProperties() + DmiProperties dmiProperties = new DmiProperties() def mockCpsValidator = Mock(CpsValidator) @@ -48,17 +48,19 @@ class DmiServiceUrlBuilderSpec extends Specification { given: 'uri variables' def uriVars = objectUnderTest.populateUriVariables(PASSTHROUGH_RUNNING.datastoreName, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA), 'cmHandle') and: 'query params' - def uriQueries = objectUnderTest.populateQueryParams(resourceId, 'optionsParamInQuery', topic) + def uriQueries = objectUnderTest.populateQueryParams(resourceId, 'optionsParamInQuery', topic, moduleSetTag) when: 'a dmi datastore service url is generated' def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars) then: 'service url is generated as expected' assert dmiServiceUrl == expectedDmiServiceUrl where: 'the following parameters are used' - scenario | topic | resourceId || expectedDmiServiceUrl - 'With valid resourceId' | 'topicParamInQuery' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery' - 'With Empty resourceId' | 'topicParamInQuery' | '' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?options=optionsParamInQuery&topic=topicParamInQuery' - 'With Empty dmi base path' | 'topicParamInQuery' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery' - 'With Empty topicParamInQuery' | '' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery' + scenario | topic | moduleSetTag | resourceId || expectedDmiServiceUrl + 'With valid resourceId' | 'topicParamInQuery' | '' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery' + 'With Empty resourceId' | 'topicParamInQuery' | '' | '' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?options=optionsParamInQuery&topic=topicParamInQuery' + 'With valid moduleSetTag' | 'topicParamInQuery' | 'module-set-tag1' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery&moduleSetTag=module-set-tag1' + 'With Empty moduleSetTag' | 'topicParamInQuery' | '' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery' + 'With Empty dmi base path' | 'topicParamInQuery' | '' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery' + 'With Empty topicParamInQuery' | '' | '' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery' } def 'Populate dmi data store url #scenario.'() { @@ -66,15 +68,16 @@ class DmiServiceUrlBuilderSpec extends Specification { dmiProperties.dmiBasePath = dmiBasePath def uriVars = objectUnderTest.populateUriVariables(PASSTHROUGH_RUNNING.datastoreName, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA), 'cmHandle') and: 'null query params' - def uriQueries = objectUnderTest.populateQueryParams(null, null, null) + def uriQueries = objectUnderTest.populateQueryParams(null, null, null, null) when: 'a dmi datastore service url is generated' def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars) then: 'the created dmi service url matches the expected' assert dmiServiceUrl == expectedDmiServiceUrl where: 'the following parameters are used' - scenario | decription | dmiBasePath || expectedDmiServiceUrl - 'with base path / ' | 'Invalid base path as it starts with /' | '/dmi' || 'dmiServiceName//dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running' - 'without base path / ' | 'Valid path as it does not starts with /' | 'dmi' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running' + scenario | decription | dmiBasePath || expectedDmiServiceUrl + 'base path starts with /' | 'Remove / from start of base path' | '/dmi' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running' + 'base path ends with / ' | 'Remove / from end of base path' | 'dmi/' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running' + 'base path without any / ' | 'base path does not contains any /' | 'dmi' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running' } def 'Bath request Url creation.'() { @@ -85,7 +88,7 @@ class DmiServiceUrlBuilderSpec extends Specification { when: 'a URL is created' def result = objectUnderTest.getDataOperationRequestUrl(batchRequestQueryParams, batchRequestUriVariables) then: 'it is formed correctly' - assert result.toString() == 'some-service/testBase/v1/data?topic=some topic&requestId=some id' + assert result.toString() == 'some-service/testBase/v1/data?topic=some+topic&requestId=some+id' } def 'Populate batch uri variables.'() { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy index 8df27bb62c..9028b9e5ed 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy @@ -90,6 +90,23 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec { 'dmi2' | 2 || 'operational-15' | 'ncmp-datastore:passthrough-operational' | ['ch4-dmi2'] } + def 'Process one data operation request with #serviceName and Module Set Tag set.'() { + given: 'data operation request' + def dataOperationRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json') + def dataOperationRequest = jsonObjectMapper.convertJsonString(dataOperationRequestJsonData, DataOperationRequest.class) + and: '1 known cm handles: ch1-dmi1' + def yangModelCmHandles = getYangModelCmHandlesForOneCmHandle() + when: 'data operation request is processed' + def operationsOutPerDmiServiceName = ResourceDataOperationRequestUtils.processPerDefinitionInDataOperationsRequest(clientTopic,'request-id', dataOperationRequest, yangModelCmHandles) + and: 'converted to a json node' + def dmiDataOperationRequestBody = operationsOutPerDmiServiceName['dmi1'] + def cmHandlesInRequestBody = dmiDataOperationRequestBody[0].cmHandles + then: 'it contains the correct operation details' + assert cmHandlesInRequestBody.size() == 1 + assert cmHandlesInRequestBody[0].id == 'ch1-dmi1' + assert cmHandlesInRequestBody[0].moduleSetTag == 'module-set-tag1' + } + 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)) @@ -156,4 +173,10 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec { new YangModelCmHandle(id: 'non-ready-cm-handle', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: advisedState) ] } + + static def getYangModelCmHandlesForOneCmHandle() { + def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')] + def readyState = new CompositeStateBuilder().withCmHandleState(CmHandleState.READY).withLastUpdatedTimeNow().build() + return [new YangModelCmHandle(id: 'ch1-dmi1', dmiServiceName: 'dmi1', moduleSetTag: 'module-set-tag1', dmiProperties: dmiProperties, compositeState: readyState)] + } } diff --git a/cps-ncmp-service/src/test/resources/application.yml b/cps-ncmp-service/src/test/resources/application.yml index 574b49982b..2a93f40816 100644 --- a/cps-ncmp-service/src/test/resources/application.yml +++ b/cps-ncmp-service/src/test/resources/application.yml @@ -1,5 +1,5 @@ # ============LICENSE_START======================================================= -# Copyright (C) 2021-2023 Nordix Foundation +# Copyright (C) 2021-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. @@ -30,14 +30,15 @@ app: async-m2m: topic: ncmp-async-m2m avc: - subscription-topic: subscription + cm-subscription-ncmp-in: subscription cm-events-topic: cm-events - subscription-forward-topic-prefix: ${NCMP_FORWARD_CM_AVC_SUBSCRIPTION:ncmp-dmi-cm-avc-subscription-} + cm-subscription-dmi-in: ${CM_SUBSCRIPTION_DMI_IN_TOPIC:ncmp-dmi-cm-avc-subscription} ncmp: dmi: httpclient: - connectionTimeoutInSeconds: 180 + connectionTimeoutInSeconds: 123 + maximumInMemorySizeInMegabytes: 16 auth: username: some-user password: some-password diff --git a/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/controller/DmiRestStubController.java b/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/controller/DmiRestStubController.java index f154be6dae..a596afdce2 100644 --- a/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/controller/DmiRestStubController.java +++ b/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/controller/DmiRestStubController.java @@ -37,9 +37,9 @@ import lombok.extern.slf4j.Slf4j; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import org.onap.cps.ncmp.api.impl.utils.EventDateTimeFormatter; -import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.CmHandle; import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.DataOperationRequest; import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.DmiDataOperationRequest; +import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.DmiOperationCmHandle; import org.onap.cps.ncmp.dmi.rest.stub.utils.ResourceFileReaderUtil; import org.onap.cps.ncmp.events.async1_0_0.Data; import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent; @@ -205,9 +205,12 @@ public class DmiRestStubController { @RequestParam(value = "resourceIdentifier") final String resourceIdentifier, @RequestParam(value = "options", required = false) final String options, @RequestParam(value = "topic", required = false) final String topic, + @RequestParam(value = "moduleSetTag", required = false) final String moduleSetTag, @RequestHeader(value = "Authorization", required = false) final String authorization) { log.info("DMI AUTH HEADER: {}", authorization); delay(dataForCmHandleDelayMs); + log.info("Module set tag received: {}", moduleSetTag); + final String sampleJson = ResourceFileReaderUtil.getResourceFileContent(applicationContext.getResource( ResourceLoader.CLASSPATH_URL_PREFIX + "data/operational/ietf-network-topology-sample-rfc8345.json")); return ResponseEntity.ok(sampleJson); @@ -235,8 +238,9 @@ public class DmiRestStubController { } dmiDataOperationRequest.getOperations().forEach(dmiDataOperation -> { final DataOperationEvent dataOperationEvent = getDataOperationEvent(dmiDataOperation); - dmiDataOperation.getCmHandles().forEach(cmHandle -> { - dataOperationEvent.getData().getResponses().get(0).setIds(List.of(cmHandle.getId())); + dmiDataOperation.getCmHandles().forEach(dmiOperationCmHandle -> { + log.info("Module Set Tag received: {}", dmiOperationCmHandle.getModuleSetTag()); + dataOperationEvent.getData().getResponses().get(0).setIds(List.of(dmiOperationCmHandle.getId())); final CloudEvent cloudEvent = buildAndGetCloudEvent(topic, requestId, dataOperationEvent); cloudEventKafkaTemplate.send(ncmpAsyncM2mTopic, UUID.randomUUID().toString(), cloudEvent); }); @@ -267,10 +271,11 @@ public class DmiRestStubController { private DataOperationEvent getDataOperationEvent(final DataOperationRequest dataOperationRequest) { final Response response = new Response(); + response.setOperationId(dataOperationRequest.getOperationId()); response.setStatusCode(SUCCESS.getCode()); response.setStatusMessage(SUCCESS.getMessage()); - response.setIds(dataOperationRequest.getCmHandles().stream().map(CmHandle::getId).toList()); + response.setIds(dataOperationRequest.getCmHandles().stream().map(DmiOperationCmHandle::getId).toList()); response.setResourceIdentifier(dataOperationRequest.getResourceIdentifier()); response.setOptions(dataOperationRequest.getOptions()); final String ietfNetworkTopologySample = ResourceFileReaderUtil diff --git a/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/model/data/operational/DataOperationRequest.java b/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/model/data/operational/DataOperationRequest.java index 85c649e233..410774932b 100644 --- a/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/model/data/operational/DataOperationRequest.java +++ b/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/model/data/operational/DataOperationRequest.java @@ -35,5 +35,5 @@ public class DataOperationRequest { private String datastore; private String options; private String resourceIdentifier; - private List<CmHandle> cmHandles = new ArrayList<>(); + private List<DmiOperationCmHandle> cmHandles = new ArrayList<>(); } diff --git a/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/model/data/operational/CmHandle.java b/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/model/data/operational/DmiOperationCmHandle.java index 93a90c917d..5cb24bc398 100644 --- a/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/model/data/operational/CmHandle.java +++ b/dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/model/data/operational/DmiOperationCmHandle.java @@ -27,7 +27,8 @@ import lombok.Setter; @Setter @Getter -public class CmHandle { +public class DmiOperationCmHandle { private String id; private Map<String, String> cmHandleProperties = new HashMap<>(); + private String moduleSetTag; } diff --git a/docs/api/swagger/ncmp/openapi.yaml b/docs/api/swagger/ncmp/openapi.yaml index 9f6a1b2b66..7b33fa156b 100644 --- a/docs/api/swagger/ncmp/openapi.yaml +++ b/docs/api/swagger/ncmp/openapi.yaml @@ -28,8 +28,7 @@ paths: schema: example: my-cm-handle type: string - - allowReserved: true - description: The format of resource identifier depend on the associated DMI + - description: The format of resource identifier depend on the associated DMI Plugin implementation. For ONAP DMI Plugin it will be RESTConf paths but it can really be anything. examples: @@ -139,8 +138,7 @@ paths: schema: example: my-cm-handle type: string - - allowReserved: true - description: The format of resource identifier depend on the associated DMI + - description: The format of resource identifier depend on the associated DMI Plugin implementation. For ONAP DMI Plugin it will be RESTConf paths but it can really be anything. examples: @@ -158,8 +156,7 @@ paths: required: true schema: type: string - - allowReserved: true - description: "options parameter in query, it is mandatory to wrap key(s)=value(s)\ + - description: "options parameter in query, it is mandatory to wrap key(s)=value(s)\ \ in parenthesis'()'. The format of options parameter depend on the associated\ \ DMI Plugin implementation." examples: @@ -177,8 +174,7 @@ paths: required: false schema: type: string - - allowReserved: true - description: topic parameter in query. + - description: topic parameter in query. examples: sample 1: value: @@ -276,8 +272,7 @@ paths: schema: example: my-cm-handle type: string - - allowReserved: true - description: The format of resource identifier depend on the associated DMI + - description: The format of resource identifier depend on the associated DMI Plugin implementation. For ONAP DMI Plugin it will be RESTConf paths but it can really be anything. examples: @@ -390,8 +385,7 @@ paths: schema: example: my-cm-handle type: string - - allowReserved: true - description: The format of resource identifier depend on the associated DMI + - description: The format of resource identifier depend on the associated DMI Plugin implementation. For ONAP DMI Plugin it will be RESTConf paths but it can really be anything. examples: @@ -509,8 +503,7 @@ paths: schema: example: my-cm-handle type: string - - allowReserved: true - description: The format of resource identifier depend on the associated DMI + - description: The format of resource identifier depend on the associated DMI Plugin implementation. For ONAP DMI Plugin it will be RESTConf paths but it can really be anything. examples: @@ -619,8 +612,7 @@ paths: is supported. operationId: executeDataOperationForCmHandles parameters: - - allowReserved: true - description: mandatory topic parameter in query. + - description: mandatory topic parameter in query. examples: sample 1: value: @@ -736,8 +728,7 @@ paths: schema: default: / type: string - - allowReserved: true - description: "options parameter in query, it is mandatory to wrap key(s)=value(s)\ + - description: "options parameter in query, it is mandatory to wrap key(s)=value(s)\ \ in parenthesis'()'. The format of options parameter depend on the associated\ \ DMI Plugin implementation." examples: @@ -755,8 +746,7 @@ paths: required: false schema: type: string - - allowReserved: true - description: topic parameter in query. + - description: topic parameter in query. examples: sample 1: value: @@ -1484,7 +1474,6 @@ components: example: my-cm-handle type: string resourceIdentifierInQuery: - allowReserved: true description: The format of resource identifier depend on the associated DMI Plugin implementation. For ONAP DMI Plugin it will be RESTConf paths but it can really be anything. @@ -1504,7 +1493,6 @@ components: schema: type: string optionsParamInQuery: - allowReserved: true description: "options parameter in query, it is mandatory to wrap key(s)=value(s)\ \ in parenthesis'()'. The format of options parameter depend on the associated\ \ DMI Plugin implementation." @@ -1524,7 +1512,6 @@ components: schema: type: string topicParamInQuery: - allowReserved: true description: topic parameter in query. examples: sample 1: @@ -1561,7 +1548,6 @@ components: example: application/yang-data+json type: string requiredTopicParamInQuery: - allowReserved: true description: mandatory topic parameter in query. examples: sample 1: diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpRestApiSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpRestApiSpec.groovy index 950cd65e72..5325f1a86e 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpRestApiSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpRestApiSpec.groovy @@ -20,9 +20,6 @@ package org.onap.cps.integration.functional -import org.onap.cps.integration.base.CpsIntegrationSpecBase -import org.springframework.http.MediaType -import spock.util.concurrent.PollingConditions import static org.hamcrest.Matchers.containsInAnyOrder import static org.hamcrest.Matchers.hasSize import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @@ -30,6 +27,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.onap.cps.integration.base.CpsIntegrationSpecBase +import org.springframework.http.MediaType +import spock.util.concurrent.PollingConditions + class NcmpRestApiSpec extends CpsIntegrationSpecBase { def 'Register CM Handles using REST API.'() { diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryByAlternateIdPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryByAlternateIdPerfTest.groovy index bbff5a83ff..4f33d030ed 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryByAlternateIdPerfTest.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryByAlternateIdPerfTest.groovy @@ -20,14 +20,16 @@ package org.onap.cps.integration.performance.ncmp -import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS - -import org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence -import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence import org.onap.cps.integration.ResourceMeter import org.onap.cps.integration.performance.base.NcmpPerfTestBase +import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence + import java.util.stream.Collectors +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.spi.FetchDescendantsOption.OMIT_DESCENDANTS + class CmHandleQueryByAlternateIdPerfTest extends NcmpPerfTestBase { InventoryPersistence objectUnderTest @@ -43,11 +45,11 @@ class CmHandleQueryByAlternateIdPerfTest extends NcmpPerfTestBase { and: 'the ids of the result are extracted and converted to xpath' def cpsXpaths = dataNodes.stream().map(dataNode -> "/dmi-registry/cm-handles[@id='${dataNode.leaves.id}']".toString() ).collect(Collectors.toSet()) and: 'a single get is executed to get all the parent objects and their descendants' - cpsDataService.getDataNodesForMultipleXpaths(NcmpPersistence.NCMP_DATASPACE_NAME, NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR, cpsXpaths, OMIT_DESCENDANTS) + cpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cpsXpaths, OMIT_DESCENDANTS) resourceMeter.stop() def durationInSeconds = resourceMeter.getTotalTimeInSeconds() print 'Total time in seconds to query ch handle by alternate id: ' + durationInSeconds then: 'the required operations are performed within required time and memory limit' - recordAndAssertResourceUsage('CpsPath Registry attributes Query', 1, durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB()) + recordAndAssertResourceUsage('Look up cm-handle by longest match alternate-id', 1, durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB()) } } diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy index d518234f5c..db0e24e938 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy @@ -20,7 +20,6 @@ package org.onap.cps.integration.performance.ncmp - import org.onap.cps.api.CpsQueryService import org.onap.cps.integration.ResourceMeter import org.onap.cps.integration.performance.base.NcmpPerfTestBase @@ -68,7 +67,7 @@ class CmHandleQueryPerfTest extends NcmpPerfTestBase { resourceMeter.stop() def durationInSeconds = resourceMeter.getTotalTimeInSeconds() then: 'the required operations are performed within required time' - recordAndAssertResourceUsage("CpsPath Registry attributes Query", 3.96, durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB()) + recordAndAssertResourceUsage("CpsPath Registry attributes Query", 3.96, durationInSeconds, 400, resourceMeter.getTotalMemoryUsageInMB()) and: 'all nodes are returned' result.size() == TOTAL_CM_HANDLES and: 'the tree contains all the expected descendants too' diff --git a/integration-test/src/test/resources/application.yml b/integration-test/src/test/resources/application.yml index 6fd3bcae4e..a4b9ea9c40 100644 --- a/integration-test/src/test/resources/application.yml +++ b/integration-test/src/test/resources/application.yml @@ -97,10 +97,10 @@ app: async-m2m: topic: ${NCMP_ASYNC_M2M_TOPIC:ncmp-async-m2m} avc: - subscription-topic: ${NCMP_CM_AVC_SUBSCRIPTION:subscription} - subscription-forward-topic-prefix: ${NCMP_FORWARD_CM_AVC_SUBSCRIPTION:ncmp-dmi-cm-avc-subscription-} - subscription-response-topic: ${NCMP_RESPONSE_CM_AVC_SUBSCRIPTION:dmi-ncmp-cm-avc-subscription} - subscription-outcome-topic: ${NCMP_OUTCOME_CM_AVC_SUBSCRIPTION:subscription-response} + cm-subscription-ncmp-in: ${CM_SUBSCRIPTION_NCMP_IN_TOPIC:subscription} + cm-subscription-dmi-in: ${CM_SUBSCRIPTION_DMI_IN_TOPIC:ncmp-dmi-cm-avc-subscription} + cm-subscription-dmi-out: ${CM_SUBSCRIPTION_DMI_OUT_TOPIC:dmi-ncmp-cm-avc-subscription} + cm-subscription-ncmp-out: ${CM_SUBSCRIPTION_NCMP_OUT_TOPIC:subscription-response} cm-events-topic: ${NCMP_CM_EVENTS_TOPIC:cm-events} lcm: events: @@ -169,6 +169,7 @@ ncmp: maximumConnectionsPerRoute: 50 maximumConnectionsTotal: 100 idleConnectionEvictionThresholdInSeconds: 5 + maximumInMemorySizeInMegabytes: 16 auth: username: dmi password: dmi diff --git a/k6-tests/README.md b/k6-tests/README.md new file mode 100644 index 0000000000..e26b18609c --- /dev/null +++ b/k6-tests/README.md @@ -0,0 +1,25 @@ +# k6 tests + +[k6](https://k6.io/) is used for performance tests. +k6 tests are written in JavaScript. + +## k6 installation +Follow the instructions in the [k6 installation guide](https://grafana.com/docs/k6/latest/set-up/install-k6/) +to get started. + +## Running the k6 test suites +Simply run the main script. (The script assumes k6 and docker-compose have been installed). +```shell +./run-k6-tests.sh +``` + +## Running k6 tests manually +Before running tests, ensure CPS/NCMP is running: +```shell +docker-compose -f docker-compose/docker-compose.yml --profile dmi-stub up +``` + +To run an individual test from command line, use +```shell +k6 run ncmp/1-create-cmhandles.js +``` diff --git a/k6-tests/ncmp/1-create-cmhandles.js b/k6-tests/ncmp/1-create-cmhandles.js new file mode 100644 index 0000000000..c32cd2f03d --- /dev/null +++ b/k6-tests/ncmp/1-create-cmhandles.js @@ -0,0 +1,56 @@ +/* + * ============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========================================================= + */ + +import http from 'k6/http'; +import exec from 'k6/execution'; +import { check } from 'k6'; +import { NCMP_BASE_URL, DMI_PLUGIN_URL, TOTAL_CM_HANDLES, makeBatchOfCmHandleIds } from './utils.js'; + +const BATCH_SIZE = 100; +export const options = { + vus: 1, + iterations: Math.ceil(TOTAL_CM_HANDLES / BATCH_SIZE), + thresholds: { + http_req_failed: ['rate == 0'], + http_req_duration: ['avg <= 1000'], + }, +}; + +export default function() { + const nextBatchOfCmHandleIds = makeBatchOfCmHandleIds(BATCH_SIZE, exec.scenario.iterationInTest); + const payload = { + "dmiPlugin": DMI_PLUGIN_URL, + "createdCmHandles": nextBatchOfCmHandleIds.map(cmHandleId => ({ + "cmHandle": cmHandleId, + "cmHandleProperties": { "neType": "RadioNode" }, + "publicCmHandleProperties": { + "Color": "yellow", + "Size": "small", + "Shape": "cube" + } + })), + }; + const response = http.post(NCMP_BASE_URL + '/ncmpInventory/v1/ch', JSON.stringify(payload), { + headers: { 'Content-Type': 'application/json' }, + }); + check(response, { + 'status equals 200': (r) => r.status === 200, + }); +} diff --git a/k6-tests/ncmp/10-mixed-load-test.js b/k6-tests/ncmp/10-mixed-load-test.js new file mode 100644 index 0000000000..d13afa54ab --- /dev/null +++ b/k6-tests/ncmp/10-mixed-load-test.js @@ -0,0 +1,88 @@ +/* + * ============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========================================================= + */ + +import http from 'k6/http'; +import { check } from 'k6'; +import { NCMP_BASE_URL, getRandomCmHandleId } from './utils.js' +import { searchRequest } from './search-base.js'; + +export const options = { + scenarios: { + passthrough_read: { + executor: 'constant-vus', + exec: 'passthrough_read', + vus: 10, + duration: '1m', + }, + id_search_module: { + executor: 'constant-vus', + exec: 'id_search_module', + vus: 5, + duration: '1m', + }, + cm_search_module: { + executor: 'constant-vus', + exec: 'cm_search_module', + vus: 5, + duration: '1m', + }, + }, + + thresholds: { + http_req_failed: ['rate==0'], + 'http_req_duration{scenario:passthrough_read}': ['avg <= 2540'], // DMI delay + 40 ms + 'http_req_duration{scenario:id_search_module}': ['avg <= 200'], + 'http_req_duration{scenario:cm_search_module}': ['avg <= 35_000'], + }, +}; + +export function passthrough_read() { + const cmHandleId = getRandomCmHandleId(); + const datastoreName = 'ncmp-datastore%3Apassthrough-operational'; + const url = `${NCMP_BASE_URL}/ncmp/v1/ch/${cmHandleId}/data/ds/${datastoreName}?resourceIdentifier=x&include-descendants=true` + const response = http.get(url); + check(response, { + 'status equals 200': (r) => r.status === 200, + }); +} + +export function id_search_module() { + const search_filter = { + "cmHandleQueryParameters": [ + { + "conditionName": "hasAllModules", + "conditionParameters": [ {"moduleName": "ietf-yang-types-1"} ] + } + ] + }; + searchRequest('id-searches', JSON.stringify(search_filter)); +} + +export function cm_search_module() { + const search_filter = { + "cmHandleQueryParameters": [ + { + "conditionName": "hasAllModules", + "conditionParameters": [ {"moduleName": "ietf-yang-types-1"} ] + } + ] + }; + searchRequest('searches', JSON.stringify(search_filter)); +} diff --git a/k6-tests/ncmp/11-delete-cmhandles.js b/k6-tests/ncmp/11-delete-cmhandles.js new file mode 100644 index 0000000000..daced8ddc8 --- /dev/null +++ b/k6-tests/ncmp/11-delete-cmhandles.js @@ -0,0 +1,48 @@ +/* + * ============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========================================================= + */ + +import http from 'k6/http'; +import exec from 'k6/execution'; +import { check } from 'k6'; +import { NCMP_BASE_URL, TOTAL_CM_HANDLES, makeBatchOfCmHandleIds } from './utils.js'; + +const BATCH_SIZE = 100; +export const options = { + vus: 1, + iterations: Math.ceil(TOTAL_CM_HANDLES / BATCH_SIZE), + thresholds: { + http_req_failed: ['rate == 0'], + http_req_duration: ['avg <= 1000'], + }, +}; + +export default function() { + const nextBatchOfCmHandleIds = makeBatchOfCmHandleIds(BATCH_SIZE, exec.scenario.iterationInTest); + const payload = { + "dmiPlugin": "http://ncmp-dmi-plugin-demo-and-csit-stub:8092", + "removedCmHandles": nextBatchOfCmHandleIds, + }; + const response = http.post(NCMP_BASE_URL + '/ncmpInventory/v1/ch', JSON.stringify(payload), { + headers: { 'Content-Type': 'application/json' }, + }); + check(response, { + 'status equals 200': (r) => r.status === 200, + }); +} diff --git a/k6-tests/ncmp/2-wait-for-cmhandles-to-be-ready.js b/k6-tests/ncmp/2-wait-for-cmhandles-to-be-ready.js new file mode 100644 index 0000000000..04a5c21dd6 --- /dev/null +++ b/k6-tests/ncmp/2-wait-for-cmhandles-to-be-ready.js @@ -0,0 +1,64 @@ +/* + * ============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========================================================= + */ + +import http from 'k6/http'; +import { sleep, fail } from 'k6'; +import { NCMP_BASE_URL, TOTAL_CM_HANDLES } from './utils.js'; + +export const options = { + vus: 1, + iterations: 1, + thresholds: { + http_req_failed: ['rate == 0'], + iteration_duration: ['max <= 300_000'], // 5 minutes + }, +}; + +export default function() { + waitForCmHandlesToBeReady(TOTAL_CM_HANDLES); +} + +function waitForCmHandlesToBeReady(totalCmHandles) { + const timeOutInSeconds = 6 * 60; + const pollingIntervalInSeconds = 10; + const maxRetries = Math.ceil(timeOutInSeconds / pollingIntervalInSeconds); + let cmHandlesReady = 0; + for (let currentTry = 0; currentTry <= maxRetries; currentTry++) { + sleep(pollingIntervalInSeconds); + try { + cmHandlesReady = getNumberOfReadyCmHandles(); + } catch (error) { + console.error(`Attempt ${currentTry + 1} - Error fetching CM handles: ${error.message}`); + } + console.log(`Attempt ${currentTry + 1} - ${cmHandlesReady}/${totalCmHandles} CM handles are READY`); + if (cmHandlesReady === totalCmHandles) { + console.log(`All ${totalCmHandles} CM handles are READY`); + return; + } + } + fail(`Timed out after ${timeoutInSeconds} seconds waiting for ${totalCmHandles} CM handles to be READY`); +} + +function getNumberOfReadyCmHandles() { + const endpointUrl = `${NCMP_BASE_URL}/cps/api/v2/dataspaces/NCMP-Admin/anchors/ncmp-dmi-registry/node?xpath=/dmi-registry&descendants=all`; + const jsonData = http.get(endpointUrl).json(); + const cmHandles = jsonData[0]["dmi-reg:dmi-registry"]["cm-handles"]; + return cmHandles.filter(cmhandle => cmhandle['state']['cm-handle-state'] === 'READY').length; +} diff --git a/k6-tests/ncmp/3-passthrough-read.js b/k6-tests/ncmp/3-passthrough-read.js new file mode 100644 index 0000000000..912cee5997 --- /dev/null +++ b/k6-tests/ncmp/3-passthrough-read.js @@ -0,0 +1,51 @@ +/* + * ============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========================================================= + */ + +import http from 'k6/http'; +import { check } from 'k6'; +import { Trend } from "k6/metrics"; +import { NCMP_BASE_URL, getRandomCmHandleId } from './utils.js' + +let ncmpOverheadTrend = new Trend("ncmp_overhead"); + +export const options = { + vus: 12, + duration: '30s', + thresholds: { + http_req_failed: ['rate == 0'], + ncmp_overhead: ['avg <= 40'], + }, +}; + +// The function that defines VU logic. +export default function() { + const cmHandleId = getRandomCmHandleId(); + const datastoreName = 'ncmp-datastore%3Apassthrough-operational'; + const url = `${NCMP_BASE_URL}/ncmp/v1/ch/${cmHandleId}/data/ds/${datastoreName}?resourceIdentifier=x&include-descendants=true` + const response = http.get(url); + check(response, { + 'status equals 200': (r) => r.status === 200, + }); + + // Calculate overhead assuming DMI data delay is 2500ms. + const dmiDelay = 2500; // This should be same as value DATA_FOR_CM_HANDLE_DELAY_MS in docker-compose.yml + const overhead = response.timings.duration - dmiDelay; + ncmpOverheadTrend.add(overhead); +} diff --git a/k6-tests/ncmp/4-id-search-no-filter.js b/k6-tests/ncmp/4-id-search-no-filter.js new file mode 100644 index 0000000000..774c609423 --- /dev/null +++ b/k6-tests/ncmp/4-id-search-no-filter.js @@ -0,0 +1,35 @@ +/* + * ============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========================================================= + */ + +import { searchRequest } from './search-base.js'; + +export const options = { + vus: 5, + duration: '60s', + thresholds: { + http_req_failed: ['rate == 0'], + http_req_duration: ['avg <= 800'], + }, +}; + +// The function that defines VU logic. +export default function() { + searchRequest('id-searches', '{}') +} diff --git a/k6-tests/ncmp/5-search-no-filter.js b/k6-tests/ncmp/5-search-no-filter.js new file mode 100644 index 0000000000..fa08d0c40c --- /dev/null +++ b/k6-tests/ncmp/5-search-no-filter.js @@ -0,0 +1,34 @@ +/* + * ============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========================================================= + */ + +import { searchRequest } from './search-base.js'; + +export const options = { + vus: 5, + duration: '60s', + thresholds: { + http_req_failed: ['rate == 0'], + http_req_duration: ['avg <= 35_000'], + }, +}; + +export default function() { + searchRequest('searches', '{}') +} diff --git a/k6-tests/ncmp/6-id-search-public-property.js b/k6-tests/ncmp/6-id-search-public-property.js new file mode 100644 index 0000000000..e8b584aecb --- /dev/null +++ b/k6-tests/ncmp/6-id-search-public-property.js @@ -0,0 +1,42 @@ +/* + * ============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========================================================= + */ + +import { searchRequest } from './search-base.js'; + +export const options = { + vus: 5, + duration: '30s', + thresholds: { + http_req_failed: ['rate == 0'], + http_req_duration: ['avg <= 5000'], + }, +}; + +export default function() { + const search_filter = { + "cmHandleQueryParameters": [ + { + "conditionName": "hasAllProperties", + "conditionParameters": [ {"Color": "yellow"}, {"Size": "small"} ] + } + ] + }; + searchRequest('id-searches', JSON.stringify(search_filter)); +} diff --git a/k6-tests/ncmp/7-search-public-property.js b/k6-tests/ncmp/7-search-public-property.js new file mode 100644 index 0000000000..3e052dabe0 --- /dev/null +++ b/k6-tests/ncmp/7-search-public-property.js @@ -0,0 +1,42 @@ +/* + * ============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========================================================= + */ + +import { searchRequest } from './search-base.js'; + +export const options = { + vus: 5, + duration: '30s', + thresholds: { + http_req_failed: ['rate == 0'], + http_req_duration: ['avg <= 35_000'], + }, +}; + +export default function() { + const search_filter = { + "cmHandleQueryParameters": [ + { + "conditionName": "hasAllProperties", + "conditionParameters": [ {"Color": "yellow"}, {"Size": "small"} ] + } + ] + }; + searchRequest('searches', JSON.stringify(search_filter)); +} diff --git a/k6-tests/ncmp/8-id-search-module.js b/k6-tests/ncmp/8-id-search-module.js new file mode 100644 index 0000000000..e99fbfeb28 --- /dev/null +++ b/k6-tests/ncmp/8-id-search-module.js @@ -0,0 +1,42 @@ +/* + * ============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========================================================= + */ + +import { searchRequest } from './search-base.js'; + +export const options = { + vus: 5, + duration: '30s', + thresholds: { + http_req_failed: ['rate == 0'], + http_req_duration: ['avg <= 200'], + }, +}; + +export default function() { + const search_filter = { + "cmHandleQueryParameters": [ + { + "conditionName": "hasAllModules", + "conditionParameters": [ {"moduleName": "ietf-yang-types-1"} ] + } + ] + }; + searchRequest('id-searches', JSON.stringify(search_filter)); +} diff --git a/k6-tests/ncmp/9-search-module.js b/k6-tests/ncmp/9-search-module.js new file mode 100644 index 0000000000..03eb9a67ab --- /dev/null +++ b/k6-tests/ncmp/9-search-module.js @@ -0,0 +1,42 @@ +/* + * ============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========================================================= + */ + +import { searchRequest } from './search-base.js'; + +export const options = { + vus: 5, + duration: '60s', + thresholds: { + http_req_failed: ['rate == 0'], + http_req_duration: ['avg <= 35_000'], + }, +}; + +export default function() { + const search_filter = { + "cmHandleQueryParameters": [ + { + "conditionName": "hasAllModules", + "conditionParameters": [ {"moduleName": "ietf-yang-types-1"} ] + } + ] + }; + searchRequest('searches', JSON.stringify(search_filter)); +} diff --git a/k6-tests/ncmp/run-all-tests.sh b/k6-tests/ncmp/run-all-tests.sh new file mode 100755 index 0000000000..f75cf2f701 --- /dev/null +++ b/k6-tests/ncmp/run-all-tests.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# +# Copyright 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. +# + +ALL_TEST_SCRIPTS=( \ +1-create-cmhandles.js \ +2-wait-for-cmhandles-to-be-ready.js \ +3-passthrough-read.js \ +4-id-search-no-filter.js \ +5-search-no-filter.js \ +6-id-search-public-property.js \ +7-search-public-property.js \ +8-id-search-module.js \ +9-search-module.js \ +10-mixed-load-test.js \ +11-delete-cmhandles.js \ +) + +pushd "$(dirname "$0")" || exit 1 + +number_of_failures=0 +for test_script in "${ALL_TEST_SCRIPTS[@]}"; do + echo "k6 run $test_script" + k6 --quiet run "$test_script" || ((number_of_failures++)) + echo +done + +popd || exit 1 + +echo "NCMP TEST FAILURES: $number_of_failures" +exit $number_of_failures diff --git a/k6-tests/ncmp/search-base.js b/k6-tests/ncmp/search-base.js new file mode 100644 index 0000000000..9c1b247a9d --- /dev/null +++ b/k6-tests/ncmp/search-base.js @@ -0,0 +1,35 @@ +/* + * ============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========================================================= + */ + +import http from 'k6/http'; +import { check } from 'k6'; +import { NCMP_BASE_URL, TOTAL_CM_HANDLES } from './utils.js'; + +export function searchRequest(searchType, searchFilter) { + const response = http.post(NCMP_BASE_URL + '/ncmp/v1/ch/' + searchType, searchFilter, { + headers: { 'Content-Type': 'application/json' }, + }); + check(response, { + 'status equals 200': (r) => r.status === 200, + }); + check(JSON.parse(response.body), { + 'returned list has expected CM-handles': (arr) => arr.length === TOTAL_CM_HANDLES, + }); +} diff --git a/k6-tests/ncmp/utils.js b/k6-tests/ncmp/utils.js new file mode 100644 index 0000000000..3b61b217e6 --- /dev/null +++ b/k6-tests/ncmp/utils.js @@ -0,0 +1,41 @@ +/* + * ============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========================================================= + */ + +export const NCMP_BASE_URL = 'http://localhost:8883'; +export const DMI_PLUGIN_URL = 'http://ncmp-dmi-plugin-demo-and-csit-stub:8092'; +export const TOTAL_CM_HANDLES = 20000 + +/* + * Makes a batch of CM-handle IDs. + * Given a batchSize=100 and batchNumber=2, it will generate ['ch-201', 'ch-202' ... 'ch-300'] + */ +export function makeBatchOfCmHandleIds(batchSize, batchNumber) { + const batchOfIds = []; + const startIndex = 1 + batchNumber * batchSize; + for (let i = 0; i < batchSize; i++) { + let cmHandleId = 'ch-' + (startIndex + i); + batchOfIds.push(cmHandleId); + } + return batchOfIds; +} + +export function getRandomCmHandleId() { + return 'ch-' + (Math.floor(Math.random() * TOTAL_CM_HANDLES) + 1); +} diff --git a/k6-tests/run-k6-tests.sh b/k6-tests/run-k6-tests.sh new file mode 100755 index 0000000000..9b8747b1ff --- /dev/null +++ b/k6-tests/run-k6-tests.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# +# Copyright 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. +# + +set -o errexit # Exit on most errors +set -o nounset # Disallow expansion of unset variables +set -o pipefail # Use last non-zero exit code in a pipeline +#set -o xtrace # Uncomment for debugging + +on_exit() { + rc=$? + ./teardown.sh + popd + echo "TEST FAILURES: $rc" + exit $rc +} +trap on_exit EXIT + +pushd "$(dirname "$0")" || exit 1 + +./setup.sh +./ncmp/run-all-tests.sh +NCMP_RESULT=$? + +# Note that the final steps are done in on_exit function after this exit! +exit $NCMP_RESULT diff --git a/k6-tests/setup.sh b/k6-tests/setup.sh new file mode 100755 index 0000000000..4805e2ea43 --- /dev/null +++ b/k6-tests/setup.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright 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. +# + +docker-compose -f ../docker-compose/docker-compose.yml --profile dmi-stub up -d + +echo "Waiting for CPS to start..." +READY_MESSAGE='Processing module sync fetched 0 advised cm handles from DB' +docker logs cps-and-ncmp -f | grep -m 1 "$READY_MESSAGE" >/dev/null || true diff --git a/k6-tests/teardown.sh b/k6-tests/teardown.sh new file mode 100755 index 0000000000..87cca72529 --- /dev/null +++ b/k6-tests/teardown.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# Copyright 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. +# + +echo '================================== docker info ==========================' +docker ps -a + +echo '================================== CPS-NCMP Logs ========================' +docker logs cps-and-ncmp + +echo 'Stopping, Removing all running containers...' +docker stop $(docker ps -aq) && docker rm $(docker ps -aq) + +echo 'Removing Volumes...' +docker volume prune -f + +echo 'Removing Networks...' +docker network prune -f |