From 5e4eb8c3bd9886e4c3ee2089b54236a01a99566b Mon Sep 17 00:00:00 2001 From: egernug Date: Fri, 21 Jun 2024 12:53:06 +0100 Subject: Create module structure in DMI Plugin To push the stub in to DMI Plugin the Plugin will need to be modulized akin to CPS/NCMP. This involves creating a new module in the repo and porting the packages in src into this module Issue-ID: CPS-2284 Change-Id: Iffa4eded4e49c220891fe73c30ea3b2f12a9e66d Signed-off-by: egernug --- .../java/org/onap/cps/ncmp/dmi/Application.java | 33 ++ .../onap/cps/ncmp/dmi/config/DmiConfiguration.java | 86 +++++ .../onap/cps/ncmp/dmi/config/DmiPluginConfig.java | 51 +++ .../cps/ncmp/dmi/config/WebSecurityConfig.java | 105 ++++++ .../cps/ncmp/dmi/config/kafka/KafkaConfig.java | 142 +++++++ .../rest/controller/DmiDatajobsRestController.java | 60 +++ .../exception/CloudEventConstructionException.java | 37 ++ .../exception/CmHandleRegistrationException.java | 36 ++ .../onap/cps/ncmp/dmi/exception/DmiException.java | 59 +++ .../ncmp/dmi/exception/DmiExceptionHandler.java | 74 ++++ .../dmi/exception/HttpClientRequestException.java | 44 +++ .../dmi/exception/InvalidDatastoreException.java | 32 ++ .../exception/ModuleResourceNotFoundException.java | 38 ++ .../dmi/exception/ModulesNotFoundException.java | 38 ++ .../onap/cps/ncmp/dmi/exception/SdncException.java | 49 +++ .../dmi/notifications/async/AsyncTaskExecutor.java | 121 ++++++ .../async/DmiAsyncRequestResponseEventCreator.java | 90 +++++ .../DmiAsyncRequestResponseEventProducer.java | 47 +++ .../avc/DmiDataAvcCloudEventCreator.java | 104 ++++++ .../notifications/avc/DmiDataAvcEventProducer.java | 50 +++ .../avc/DmiDataAvcEventSimulationController.java | 61 ++++ ...NotificationSubscriptionDmiInEventConsumer.java | 108 ++++++ ...nSubscriptionDmiOutEventToCloudEventMapper.java | 62 ++++ .../model/CmNotificationSubscriptionStatus.java | 32 ++ .../dmi/notifications/mapper/CloudEventMapper.java | 62 ++++ .../dmi/rest/controller/DmiRestController.java | 253 +++++++++++++ .../rest/controller/handlers/DatastoreType.java | 65 ++++ .../org/onap/cps/ncmp/dmi/service/DmiService.java | 95 +++++ .../onap/cps/ncmp/dmi/service/DmiServiceImpl.java | 196 ++++++++++ .../ncmp/dmi/service/YangResourceExtractor.java | 59 +++ .../ncmp/dmi/service/client/NcmpRestClient.java | 64 ++++ .../dmi/service/client/SdncRestconfClient.java | 87 +++++ .../ncmp/dmi/service/model/CmHandleOperation.java | 35 ++ .../ncmp/dmi/service/model/CreatedCmHandle.java | 36 ++ .../ncmp/dmi/service/model/ModuleReference.java | 37 ++ .../cps/ncmp/dmi/service/model/ModuleSchema.java | 36 ++ .../ncmp/dmi/service/operation/SdncOperations.java | 270 ++++++++++++++ dmi-service/src/main/resources/application.yml | 128 +++++++ dmi-service/src/main/resources/logback-spring.xml | 75 ++++ .../ncmp/dmi/api/kafka/MessagingBaseSpec.groovy | 79 ++++ .../ncmp/dmi/config/DmiConfigurationSpec.groovy | 66 ++++ .../cps/ncmp/dmi/config/DmiPluginConfigSpec.groovy | 52 +++ .../ncmp/dmi/config/kafka/KafkaConfigSpec.groovy | 62 ++++ .../DmiDatajobsRestControllerSpec.groovy | 69 ++++ .../async/AsyncTaskExecutorIntegrationSpec.groovy | 110 ++++++ .../avc/AvcEventExecutorIntegrationSpec.groovy | 61 ++++ ...cationSubscriptionDmiInEventConsumerSpec.groovy | 149 ++++++++ ...riptionDmiOutEventToCloudEventMapperSpec.groovy | 69 ++++ .../mapper/CloudEventMapperSpec.groovy | 53 +++ .../rest/controller/ControllerSecuritySpec.groovy | 76 ++++ .../rest/controller/DmiRestControllerSpec.groovy | 406 +++++++++++++++++++++ .../cps/ncmp/dmi/service/DmiServiceImplSpec.groovy | 273 ++++++++++++++ .../dmi/service/YangResourceExtractorSpec.groovy | 98 +++++ .../dmi/service/client/NcmpRestClientSpec.groovy | 57 +++ .../service/client/SdncRestconfClientSpec.groovy | 102 ++++++ .../service/operation/SdncOperationsSpec.groovy | 176 +++++++++ .../test/java/org/onap/cps/ncmp/dmi/TestUtils.java | 54 +++ .../ncmp/dmi/rest/controller/TestController.java | 35 ++ dmi-service/src/test/resources/ModuleSchema.json | 15 + dmi-service/src/test/resources/application.yml | 79 ++++ .../cmNotificationSubscriptionCreationEvent.json | 43 +++ .../test/resources/createDataWithNormalChar.json | 8 + .../test/resources/createDataWithSpecialChar.json | 8 + dmi-service/src/test/resources/deleteData.json | 8 + .../src/test/resources/moduleResources.json | 18 + dmi-service/src/test/resources/patchData.json | 8 + dmi-service/src/test/resources/readData.json | 9 + dmi-service/src/test/resources/updateData.json | 8 + 68 files changed, 5308 insertions(+) create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/Application.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/DmiConfiguration.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/DmiPluginConfig.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/WebSecurityConfig.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/kafka/KafkaConfig.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/datajobs/rest/controller/DmiDatajobsRestController.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/CloudEventConstructionException.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/CmHandleRegistrationException.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/DmiException.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/DmiExceptionHandler.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/HttpClientRequestException.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/InvalidDatastoreException.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/ModuleResourceNotFoundException.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/ModulesNotFoundException.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/SdncException.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/AsyncTaskExecutor.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/DmiAsyncRequestResponseEventCreator.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/DmiAsyncRequestResponseEventProducer.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcCloudEventCreator.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcEventProducer.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcEventSimulationController.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiInEventConsumer.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiOutEventToCloudEventMapper.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/model/CmNotificationSubscriptionStatus.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/mapper/CloudEventMapper.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/DmiRestController.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/handlers/DatastoreType.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/DmiService.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/DmiServiceImpl.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/YangResourceExtractor.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/NcmpRestClient.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClient.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/CmHandleOperation.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/CreatedCmHandle.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/ModuleReference.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/ModuleSchema.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java create mode 100644 dmi-service/src/main/resources/application.yml create mode 100644 dmi-service/src/main/resources/logback-spring.xml create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/api/kafka/MessagingBaseSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/DmiConfigurationSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/DmiPluginConfigSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/kafka/KafkaConfigSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/datajobs/rest/controller/DmiDatajobsRestControllerSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/async/AsyncTaskExecutorIntegrationSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/avc/AvcEventExecutorIntegrationSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiInEventConsumerSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiOutEventToCloudEventMapperSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/mapper/CloudEventMapperSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/ControllerSecuritySpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/DmiRestControllerSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/DmiServiceImplSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/YangResourceExtractorSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/client/NcmpRestClientSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClientSpec.groovy create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/SdncOperationsSpec.groovy create mode 100644 dmi-service/src/test/java/org/onap/cps/ncmp/dmi/TestUtils.java create mode 100644 dmi-service/src/test/java/org/onap/cps/ncmp/dmi/rest/controller/TestController.java create mode 100644 dmi-service/src/test/resources/ModuleSchema.json create mode 100644 dmi-service/src/test/resources/application.yml create mode 100644 dmi-service/src/test/resources/cmNotificationSubscriptionCreationEvent.json create mode 100644 dmi-service/src/test/resources/createDataWithNormalChar.json create mode 100644 dmi-service/src/test/resources/createDataWithSpecialChar.json create mode 100644 dmi-service/src/test/resources/deleteData.json create mode 100644 dmi-service/src/test/resources/moduleResources.json create mode 100644 dmi-service/src/test/resources/patchData.json create mode 100644 dmi-service/src/test/resources/readData.json create mode 100644 dmi-service/src/test/resources/updateData.json (limited to 'dmi-service/src') diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/Application.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/Application.java new file mode 100644 index 00000000..69d21ba1 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/Application.java @@ -0,0 +1,33 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(final String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/DmiConfiguration.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/DmiConfiguration.java new file mode 100644 index 00000000..83ef6f89 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/DmiConfiguration.java @@ -0,0 +1,86 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * Provides access to cps base url and cps authentication. + */ +@Configuration +public class DmiConfiguration { + + private static final int TIMEOUT = 5000; + + @Getter + @Component + public static class CpsProperties { + + @Value("${cps-core.baseUrl}") + private String baseUrl; + @Value("${cps-core.dmiRegistrationUrl}") + private String dmiRegistrationUrl; + @Value("${cps-core.auth.username}") + private String authUsername; + @Value("${cps-core.auth.password}") + private String authPassword; + } + + @Getter + @Component + public static class SdncProperties { + + @Value("${sdnc.baseUrl}") + private String baseUrl; + @Value("${sdnc.auth.username}") + private String authUsername; + @Value("${sdnc.auth.password}") + private String authPassword; + @Value("${sdnc.topologyId}") + public String topologyId; + } + + /** + * Returns restTemplate bean for the spring context. + * + * @param restTemplateBuilder restTemplate builder + * @return {@code RestTemplate} rest template + */ + @Bean + public RestTemplate restTemplate(final RestTemplateBuilder restTemplateBuilder) { + final RestTemplate restTemplate = restTemplateBuilder.build(); + setCustomRequestFactoryToSupportPatch(restTemplate); + return restTemplate; + } + + private void setCustomRequestFactoryToSupportPatch(final RestTemplate restTemplate) { + final HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setConnectTimeout(TIMEOUT); + restTemplate.setRequestFactory(requestFactory); + } +} \ No newline at end of file diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/DmiPluginConfig.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/DmiPluginConfig.java new file mode 100644 index 00000000..fb22b358 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/DmiPluginConfig.java @@ -0,0 +1,51 @@ +/* + * ============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.dmi.config; + +import lombok.Getter; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + + +@Configuration +public class DmiPluginConfig { + + /** + * Swagger-ui configuration using springdoc. + */ + @Bean("dmi-plugin-api") + public GroupedOpenApi api() { + return GroupedOpenApi.builder().group("dmi-plugin-api") + .pathsToMatch("/swagger-ui/**,/swagger-resources/**,/v3/api-docs").build(); + } + + @Getter + @Component + public static class DmiPluginProperties { + + @Value("${dmi.service.url}") + private String dmiServiceUrl; + } +} + diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/WebSecurityConfig.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/WebSecurityConfig.java new file mode 100644 index 00000000..ac92cb4a --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/WebSecurityConfig.java @@ -0,0 +1,105 @@ +/* + * ============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.dmi.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +/** + * Configuration class to implement application security. + * It enforces Basic Authentication access control. + */ +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + + private static final String USER_ROLE = "USER"; + + private final String username; + private final String password; + private final String[] permitUris; + + /** + * Constructor. Accepts parameters from configuration. + * + * @param permitUris comma-separated list of uri patterns for endpoints permitted + * @param username username + * @param password password + */ + public WebSecurityConfig( + @Autowired @Value("${security.permit-uri}") final String permitUris, + @Autowired @Value("${security.auth.username}") final String username, + @Autowired @Value("${security.auth.password}") final String password + ) { + super(); + this.permitUris = permitUris.isEmpty() ? new String[] {"/v3/api-docs"} : permitUris.split("\\s{0,9},\\s{0,9}"); + this.username = username; + this.password = password; + } + + /** + * Return the configuration for secure access to the modules REST end points. + * + * @param http the HTTP security settings. + * @return the HTTP security settings. + */ + @Bean + // The team decided to disable default CSRF Spring protection and not implement CSRF tokens validation. + // ncmp is a stateless REST API that is not as vulnerable to CSRF attacks as web applications running in + // web browsers are. ncmp does not manage sessions, each request requires the authentication token in the header. + // See https://docs.spring.io/spring-security/site/docs/5.3.8.RELEASE/reference/html5/#csrf + @SuppressWarnings("squid:S4502") + public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception { + http + .httpBasic(httpBasicCustomizer -> {}) + .authorizeHttpRequests(authorizeHttpRequestsCustomizer -> { + authorizeHttpRequestsCustomizer.requestMatchers(permitUris).permitAll(); + authorizeHttpRequestsCustomizer.anyRequest().authenticated(); + }) + .csrf(AbstractHttpConfigurer::disable); + + return http.build(); + } + + /** + * In memory user authenticaion details. + * + * @return in memory authentication. + */ + @Bean + public InMemoryUserDetailsManager userDetailsService() { + final UserDetails user = User.builder() + .username(username) + .password("{noop}" + password) + .roles(USER_ROLE) + .build(); + return new InMemoryUserDetailsManager(user); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/kafka/KafkaConfig.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/kafka/KafkaConfig.java new file mode 100644 index 00000000..25ee92ae --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/config/kafka/KafkaConfig.java @@ -0,0 +1,142 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.dmi.config.kafka; + +import io.cloudevents.CloudEvent; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.springframework.boot.autoconfigure.kafka.KafkaProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.serializer.JsonDeserializer; +import org.springframework.kafka.support.serializer.JsonSerializer; + +/** + * kafka Configuration for legacy and cloud events. + * + * @param valid legacy event to be published over the wire. + */ +@Configuration +@EnableKafka +@RequiredArgsConstructor +public class KafkaConfig { + + private final KafkaProperties kafkaProperties; + + /** + * This sets the strategy for creating legacy Kafka producer instance from kafka properties defined into + * application.yml and replaces value-serializer by JsonSerializer. + * + * @return legacy event producer instance. + */ + @Bean + public ProducerFactory legacyEventProducerFactory() { + final Map producerConfigProperties = kafkaProperties.buildProducerProperties(); + producerConfigProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + return new DefaultKafkaProducerFactory<>(producerConfigProperties); + } + + /** + * The ConsumerFactory implementation is to produce new legacy instance for provided kafka properties defined + * into application.yml and replaces deserializer-value by JsonDeserializer. + * + * @return an instance of legacy consumer factory. + */ + @Bean + public ConsumerFactory legacyEventConsumerFactory() { + final Map consumerConfigProperties = kafkaProperties.buildConsumerProperties(); + consumerConfigProperties.put("spring.deserializer.value.delegate.class", JsonDeserializer.class); + return new DefaultKafkaConsumerFactory<>(consumerConfigProperties); + } + + /** + * This sets the strategy for creating cloud Kafka producer instance from kafka properties defined into + * application.yml with CloudEventSerializer. + * + * @return cloud event producer instance. + */ + @Bean + public ProducerFactory cloudEventProducerFactory() { + final Map producerConfigProperties = kafkaProperties.buildProducerProperties(); + return new DefaultKafkaProducerFactory<>(producerConfigProperties); + } + + /** + * The ConsumerFactory implementation to produce new legacy instance for provided kafka properties defined + * into application.yml having CloudEventDeserializer as deserializer-value. + * + * @return an instance of cloud consumer factory. + */ + @Bean + public ConsumerFactory cloudEventConsumerFactory() { + final Map consumerConfigProperties = kafkaProperties.buildConsumerProperties(); + return new DefaultKafkaConsumerFactory<>(consumerConfigProperties); + } + + /** + * A legacy Kafka event template for executing high-level operations. The legacy producer factory ensure this. + * + * @return an instance of legacy Kafka template. + */ + @Bean + @Primary + public KafkaTemplate legacyEventKafkaTemplate() { + final KafkaTemplate kafkaTemplate = new KafkaTemplate<>(legacyEventProducerFactory()); + kafkaTemplate.setConsumerFactory(legacyEventConsumerFactory()); + return kafkaTemplate; + } + + /** + * A cloud Kafka event template for executing high-level operations. The cloud producer factory ensure this. + * + * @return an instance of cloud Kafka template. + */ + @Bean + public KafkaTemplate cloudEventKafkaTemplate() { + final KafkaTemplate kafkaTemplate = new KafkaTemplate<>(cloudEventProducerFactory()); + kafkaTemplate.setConsumerFactory(cloudEventConsumerFactory()); + return kafkaTemplate; + } + + /** + * A cloud Kafka event template for executing high-level operations. The cloud producer factory ensure this. + * + * @return an instance of cloud Kafka template. + */ + @Bean + public ConcurrentKafkaListenerContainerFactory + cloudEventConcurrentKafkaListenerContainerFactory() { + final ConcurrentKafkaListenerContainerFactory containerFactory = + new ConcurrentKafkaListenerContainerFactory<>(); + containerFactory.setConsumerFactory(cloudEventConsumerFactory()); + return containerFactory; + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/datajobs/rest/controller/DmiDatajobsRestController.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/datajobs/rest/controller/DmiDatajobsRestController.java new file mode 100644 index 00000000..bbc1c20d --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/datajobs/rest/controller/DmiDatajobsRestController.java @@ -0,0 +1,60 @@ +/* + * ============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.dmi.datajobs.rest.controller; + +import org.onap.cps.ncmp.dmi.datajobs.model.SubjobReadRequest; +import org.onap.cps.ncmp.dmi.datajobs.model.SubjobWriteRequest; +import org.onap.cps.ncmp.dmi.datajobs.rest.api.DmiDatajobApi; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("${rest.api.dmi-base-path}") +@RestController +public class DmiDatajobsRestController implements DmiDatajobApi { + /** + * * This method is not implemented for ONAP DMI plugin. + * + * @param requestId Identifier for the overall Datajob (required) + * @param subjobReadRequest Operation body (optional) + * @return (@ code ResponseEntity) response entity + */ + @Override + public ResponseEntity readDataJob(final String requestId, + final SubjobReadRequest subjobReadRequest) { + + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * * This method is not implemented for ONAP DMI plugin. + * + * @param requestId Identifier for the overall Datajob (required) + * @param subjobWriteRequest Operation body (optional) + * @return (@ code ResponseEntity) response entity + */ + @Override + public ResponseEntity writeDataJob(final String requestId, + final SubjobWriteRequest subjobWriteRequest) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/CloudEventConstructionException.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/CloudEventConstructionException.java new file mode 100644 index 00000000..f61c156a --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/CloudEventConstructionException.java @@ -0,0 +1,37 @@ +/* + * ============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.dmi.exception; + +public class CloudEventConstructionException extends DmiException { + + private static final long serialVersionUID = 7747941311132087621L; + + /** + * CloudEventConstructionException. + * + * @param message the error message + * @param details the error details + * @param cause the error cause + */ + public CloudEventConstructionException(final String message, final String details, final Throwable cause) { + super(message, details, cause); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/CmHandleRegistrationException.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/CmHandleRegistrationException.java new file mode 100644 index 00000000..1874389e --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/CmHandleRegistrationException.java @@ -0,0 +1,36 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.exception; + +public class CmHandleRegistrationException extends DmiException { + + private static final long serialVersionUID = 8973438585188332404L; + + /** + * Constructor. + * + * @param details the error details + */ + public CmHandleRegistrationException(final String details) { + super("Not able to register the given cm-handles.", details); + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/DmiException.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/DmiException.java new file mode 100644 index 00000000..c099a1cc --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/DmiException.java @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.exception; + +import lombok.Getter; + +/** + * Dmi exception. + */ +public class DmiException extends RuntimeException { + + private static final long serialVersionUID = 1481520410918497487L; + + @Getter + final String details; + + /** + * Constructor. + * + * @param message the error message + * @param details the error details + */ + public DmiException(final String message, final String details) { + super(message); + this.details = details; + } + + /** + * Constructor. + * + * @param message the error message + * @param details the error details + * @param cause the cause of the exception + */ + public DmiException(final String message, final String details, final Throwable cause) { + super(message, cause); + this.details = details; + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/DmiExceptionHandler.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/DmiExceptionHandler.java new file mode 100644 index 00000000..49db7d8b --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/DmiExceptionHandler.java @@ -0,0 +1,74 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.exception; + +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.dmi.model.ErrorMessage; +import org.onap.cps.ncmp.dmi.rest.controller.DmiRestController; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice(assignableTypes = {DmiRestController.class}) +public class DmiExceptionHandler { + + private DmiExceptionHandler() { + } + + /** + * Default exception handler. + * + * @param exception the exception to handle + * @return response with response code 500. + */ + @ExceptionHandler + public static ResponseEntity handleInternalServerErrorExceptions(final Exception exception) { + return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, exception); + } + + @ExceptionHandler({ModulesNotFoundException.class, ModuleResourceNotFoundException.class}) + public static ResponseEntity handleNotFoundExceptions(final DmiException exception) { + return buildErrorResponse(HttpStatus.NOT_FOUND, exception); + } + + @ExceptionHandler({CmHandleRegistrationException.class, DmiException.class}) + public static ResponseEntity handleAnyOtherDmiExceptions(final DmiException exception) { + return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, exception); + } + + private static ResponseEntity buildErrorResponse(final HttpStatus httpStatus, final Exception exception) { + logForNonDmiException(exception); + final var errorMessage = new ErrorMessage(); + errorMessage.setStatus(httpStatus.toString()); + errorMessage.setMessage(exception.getMessage()); + errorMessage.setDetails(exception instanceof DmiException ? ((DmiException) exception).getDetails() : + "Check logs for details."); + return new ResponseEntity<>(errorMessage, httpStatus); + } + + private static void logForNonDmiException(final Exception exception) { + if (exception.getCause() != null || !(exception instanceof DmiException)) { + log.error("Exception occurred", exception); + } + } +} \ No newline at end of file diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/HttpClientRequestException.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/HttpClientRequestException.java new file mode 100644 index 00000000..b4b0249f --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/HttpClientRequestException.java @@ -0,0 +1,44 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2022 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.dmi.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class HttpClientRequestException extends DmiException { + + private static final long serialVersionUID = 881438585188332404L; + + private final HttpStatus httpStatus; + + /** + * Constructor. + * + * @param cmHandle cmHandle identifier + * @param details response body from the client available as details + * @param httpStatus http status from the client + */ + public HttpClientRequestException(final String cmHandle, final String details, final HttpStatus httpStatus) { + super("Resource data request failed for CM Handle: " + cmHandle, details); + this.httpStatus = httpStatus; + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/InvalidDatastoreException.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/InvalidDatastoreException.java new file mode 100644 index 00000000..aa5b0cb7 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/InvalidDatastoreException.java @@ -0,0 +1,32 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.dmi.exception; + +public class InvalidDatastoreException extends RuntimeException { + /** + * Instantiates a new Invalid datastore exception. + * + * @param message the message + */ + public InvalidDatastoreException(final String message) { + super(message); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/ModuleResourceNotFoundException.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/ModuleResourceNotFoundException.java new file mode 100644 index 00000000..65db2712 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/ModuleResourceNotFoundException.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.exception; + +public class ModuleResourceNotFoundException extends DmiException { + + private static final long serialVersionUID = 4764849097602543408L; + + private static final String ERROR_MESSAGE = "Module resource not found for given cmHandle: "; + + /** + * Constructor. + * + * @param cmHandle the cm handle + * @param details the details of the error + */ + public ModuleResourceNotFoundException(final String cmHandle, final String details) { + super(ERROR_MESSAGE + cmHandle, details); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/ModulesNotFoundException.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/ModulesNotFoundException.java new file mode 100644 index 00000000..ded54d91 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/ModulesNotFoundException.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.exception; + +public class ModulesNotFoundException extends DmiException { + + private static final long serialVersionUID = 980438585188332404L; + + private static final String ERROR_MESSAGE = "Not able to register the given cm-handles: "; + + /** + * Constructor. + * + * @param cmHandle cmHandle identifier + * @param details the error details + */ + public ModulesNotFoundException(final String cmHandle, final String details) { + super(ERROR_MESSAGE + cmHandle, details); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/SdncException.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/SdncException.java new file mode 100644 index 00000000..a83485a9 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/exception/SdncException.java @@ -0,0 +1,49 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Bell Canada + * ================================================================================ + * 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.dmi.exception; + +import org.springframework.http.HttpStatus; + +/* +Use this exception when SDNC contract fails + */ +public class SdncException extends DmiException { + + private static final long serialVersionUID = -2076096996672060566L; + + /** + * Constructor. + * + * @param message message + * @param httpStatus httpStatus + * @param responseBody responseBody + */ + public SdncException(final String message, final HttpStatus httpStatus, final String responseBody) { + super(message, String.format("sdnc http status: %s, response body : %s ", + httpStatus.toString(), + responseBody)); + } + + public SdncException(final String message, final String details, final Throwable cause) { + super(message, details, cause); + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/AsyncTaskExecutor.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/AsyncTaskExecutor.java new file mode 100644 index 00000000..6a1c6d1d --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/AsyncTaskExecutor.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 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.dmi.notifications.async; + +import com.google.gson.JsonObject; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.dmi.exception.HttpClientRequestException; +import org.onap.cps.ncmp.dmi.model.DataAccessRequest; +import org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AsyncTaskExecutor { + + private final DmiAsyncRequestResponseEventProducer dmiAsyncRequestResponseEventProducer; + + private static final DmiAsyncRequestResponseEventCreator dmiAsyncRequestResponseEventCreator = + new DmiAsyncRequestResponseEventCreator(); + + private static final Map operationToHttpStatusMap = new HashMap<>(6); + + static { + operationToHttpStatusMap.put(null, HttpStatus.OK); + operationToHttpStatusMap.put(DataAccessRequest.OperationEnum.READ, HttpStatus.OK); + operationToHttpStatusMap.put(DataAccessRequest.OperationEnum.CREATE, HttpStatus.CREATED); + operationToHttpStatusMap.put(DataAccessRequest.OperationEnum.PATCH, HttpStatus.OK); + operationToHttpStatusMap.put(DataAccessRequest.OperationEnum.UPDATE, HttpStatus.OK); + operationToHttpStatusMap.put(DataAccessRequest.OperationEnum.DELETE, HttpStatus.NO_CONTENT); + } + + /** + * Execute task asynchronously and publish response to supplied topic. + * + * @param taskSupplier functional method is get() task need to executed asynchronously + * @param topicName topic name where message need to be published + * @param requestId unique requestId for async request + * @param operation the operation performed + * @param timeOutInMilliSeconds task timeout in milliseconds + */ + public void executeAsyncTask(final Supplier taskSupplier, + final String topicName, + final String requestId, + final DataAccessRequest.OperationEnum operation, + final int timeOutInMilliSeconds) { + CompletableFuture.supplyAsync(taskSupplier::get) + .orTimeout(timeOutInMilliSeconds, TimeUnit.MILLISECONDS) + .whenCompleteAsync((resourceDataAsJson, throwable) -> { + if (throwable == null) { + final String status = operationToHttpStatusMap.get(operation).getReasonPhrase(); + final String code = String.valueOf(operationToHttpStatusMap.get(operation).value()); + publishAsyncEvent(topicName, requestId, resourceDataAsJson, status, code); + } else { + log.error("Error occurred with async request {}", throwable.getMessage()); + publishAsyncFailureEvent(topicName, requestId, throwable); + } + }); + log.info("Async task completed."); + } + + private void publishAsyncEvent(final String topicName, + final String requestId, + final String resourceDataAsJson, + final String status, + final String code) { + final DmiAsyncRequestResponseEvent cpsAsyncRequestResponseEvent = + dmiAsyncRequestResponseEventCreator.createEvent(resourceDataAsJson, topicName, requestId, status, code); + + dmiAsyncRequestResponseEventProducer.sendMessage(requestId, cpsAsyncRequestResponseEvent); + } + + private void publishAsyncFailureEvent(final String topicName, + final String requestId, + final Throwable throwable) { + HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + + if (throwable instanceof HttpClientRequestException) { + final HttpClientRequestException httpClientRequestException = (HttpClientRequestException) throwable; + httpStatus = httpClientRequestException.getHttpStatus(); + } + + final JsonObject errorDetails = new JsonObject(); + errorDetails.addProperty("errorDetails", throwable.getMessage()); + publishAsyncEvent( + topicName, + requestId, + errorDetails.toString(), + httpStatus.getReasonPhrase(), + String.valueOf(httpStatus.value()) + ); + } +} + + + diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/DmiAsyncRequestResponseEventCreator.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/DmiAsyncRequestResponseEventCreator.java new file mode 100644 index 00000000..1e6c84b1 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/DmiAsyncRequestResponseEventCreator.java @@ -0,0 +1,90 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 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.dmi.notifications.async; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.UUID; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.dmi.Application; +import org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent; +import org.onap.cps.ncmp.event.model.EventContent; + +/** + * Helper to create DmiAsyncRequestResponseEvent. + */ +@Slf4j +public class DmiAsyncRequestResponseEventCreator { + + private static final DateTimeFormatter dateTimeFormatter + = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Create an event. + * + * @param resourceDataAsJson the resource data as json + * @param topicParamInQuery the topic to send response to + * @param requestId the request id + * @param status the status of the request + * @param code the code of the response + * + * @return DmiAsyncRequestResponseEvent + */ + public DmiAsyncRequestResponseEvent createEvent(final String resourceDataAsJson, + final String topicParamInQuery, + final String requestId, + final String status, + final String code) { + final DmiAsyncRequestResponseEvent dmiAsyncRequestResponseEvent = new DmiAsyncRequestResponseEvent(); + + dmiAsyncRequestResponseEvent.setEventId(UUID.randomUUID().toString()); + dmiAsyncRequestResponseEvent.setEventCorrelationId(requestId); + dmiAsyncRequestResponseEvent.setEventType(DmiAsyncRequestResponseEvent.class.getName()); + dmiAsyncRequestResponseEvent.setEventSchema("urn:cps:" + DmiAsyncRequestResponseEvent.class.getName()); + dmiAsyncRequestResponseEvent.setEventSchemaVersion("v1"); + dmiAsyncRequestResponseEvent.setEventSource(Application.class.getPackageName()); + dmiAsyncRequestResponseEvent.setEventTarget(topicParamInQuery); + dmiAsyncRequestResponseEvent.setEventTime(ZonedDateTime.now().format(dateTimeFormatter)); + dmiAsyncRequestResponseEvent.setEventContent(getEventContent(resourceDataAsJson, status, code)); + + return dmiAsyncRequestResponseEvent; + } + + @SneakyThrows + private EventContent getEventContent(final String resourceDataAsJson, final String status, final String code) { + final EventContent eventContent = new EventContent(); + + eventContent.setResponseDataSchema("urn:cps:" + DmiAsyncRequestResponseEvent.class.getName() + ":v1"); + eventContent.setResponseStatus(status); + eventContent.setResponseCode(code); + + eventContent.setAdditionalProperty("response-data", + objectMapper.readValue(resourceDataAsJson, HashMap.class)); + + return eventContent; + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/DmiAsyncRequestResponseEventProducer.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/DmiAsyncRequestResponseEventProducer.java new file mode 100644 index 00000000..00fea330 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/async/DmiAsyncRequestResponseEventProducer.java @@ -0,0 +1,47 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 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.dmi.notifications.async; + +import lombok.RequiredArgsConstructor; +import org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DmiAsyncRequestResponseEventProducer { + + private final KafkaTemplate kafkaTemplate; + + @Value("${app.ncmp.async.topic}") + private String dmiNcmpTopic; + + /** + * Sends message to the configured topic with a message key. + * + * @param requestId the request id + * @param dmiAsyncRequestResponseEvent the event to publish + */ + public void sendMessage(final String requestId, final DmiAsyncRequestResponseEvent dmiAsyncRequestResponseEvent) { + kafkaTemplate.send(dmiNcmpTopic, requestId, dmiAsyncRequestResponseEvent); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcCloudEventCreator.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcCloudEventCreator.java new file mode 100644 index 00000000..b8bd277d --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcCloudEventCreator.java @@ -0,0 +1,104 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.dmi.notifications.avc; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import java.net.URI; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.events.avc1_0_0.AvcEvent; +import org.onap.cps.ncmp.events.avc1_0_0.Data; +import org.onap.cps.ncmp.events.avc1_0_0.DatastoreChanges; +import org.onap.cps.ncmp.events.avc1_0_0.Edit; +import org.onap.cps.ncmp.events.avc1_0_0.IetfYangPatchYangPatch; +import org.onap.cps.ncmp.events.avc1_0_0.PushChangeUpdate; +import org.onap.cps.ncmp.events.avc1_0_0.Value; + +/** + * Helper to create AvcEvents. + */ +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DmiDataAvcCloudEventCreator { + + private static final DateTimeFormatter dateTimeFormatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Creates CloudEvent for DMI Data AVC. + * + * @param eventCorrelationId correlationid + * @return Cloud Event + */ + public static CloudEvent createCloudEvent(final String eventCorrelationId) { + + CloudEvent cloudEvent = null; + + try { + cloudEvent = CloudEventBuilder.v1().withId(UUID.randomUUID().toString()).withSource(URI.create("NCMP")) + .withType(AvcEvent.class.getName()) + .withDataSchema(URI.create("urn:cps:" + AvcEvent.class.getName() + ":1.0.0")) + .withExtension("correlationid", eventCorrelationId) + .withData(objectMapper.writeValueAsBytes(createDmiDataAvcEvent())).build(); + } catch (final JsonProcessingException jsonProcessingException) { + log.error("Unable to convert object to json : {}", jsonProcessingException.getMessage()); + } + + return cloudEvent; + } + + private static AvcEvent createDmiDataAvcEvent() { + final AvcEvent avcEvent = new AvcEvent(); + final Data data = new Data(); + final PushChangeUpdate pushChangeUpdate = new PushChangeUpdate(); + final DatastoreChanges datastoreChanges = new DatastoreChanges(); + final IetfYangPatchYangPatch ietfYangPatchYangPatch = new IetfYangPatchYangPatch(); + ietfYangPatchYangPatch.setPatchId("abcd"); + final Edit edit1 = new Edit(); + final Value value = new Value(); + final Map attributeMap = new LinkedHashMap<>(); + attributeMap.put("isHoAllowed", false); + value.setAttributes(List.of(attributeMap)); + edit1.setEditId("editId"); + edit1.setOperation("replace"); + edit1.setTarget("target_xpath"); + edit1.setValue(value); + ietfYangPatchYangPatch.setEdit(List.of(edit1)); + datastoreChanges.setIetfYangPatchYangPatch(ietfYangPatchYangPatch); + pushChangeUpdate.setDatastoreChanges(datastoreChanges); + data.setPushChangeUpdate(pushChangeUpdate); + + avcEvent.setData(data); + return avcEvent; + } + +} \ No newline at end of file diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcEventProducer.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcEventProducer.java new file mode 100644 index 00000000..075dcf20 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcEventProducer.java @@ -0,0 +1,50 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.dmi.notifications.avc; + + +import io.cloudevents.CloudEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class DmiDataAvcEventProducer { + + private final KafkaTemplate cloudEventKafkaTemplate; + + /** + * Publishing DMI Data AVC event payload as CloudEvent. + * + * @param requestId the request id + * @param cloudAvcEvent event with data as DMI DataAVC event + */ + public void publishDmiDataAvcCloudEvent(final String requestId, final CloudEvent cloudAvcEvent) { + final ProducerRecord producerRecord = + new ProducerRecord<>("dmi-cm-events", requestId, cloudAvcEvent); + cloudEventKafkaTemplate.send(producerRecord); + log.debug("AVC event sent"); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcEventSimulationController.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcEventSimulationController.java new file mode 100644 index 00000000..c5fb8fbe --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/avc/DmiDataAvcEventSimulationController.java @@ -0,0 +1,61 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.dmi.notifications.avc; + +import io.cloudevents.CloudEvent; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +@RequestMapping("${rest.api.dmi-base-path}") +@RestController +@Slf4j +@RequiredArgsConstructor +public class DmiDataAvcEventSimulationController { + + private final DmiDataAvcEventProducer dmiDataAvcEventProducer; + + /** + * Simulate Event for AVC. + * + * @param numberOfSimulatedEvents number of events to be generated + * @return ResponseEntity + */ + @GetMapping(path = "/v1/simulateDmiDataEvent") + public ResponseEntity simulateEvents( + @RequestParam("numberOfSimulatedEvents") final Integer numberOfSimulatedEvents) { + + for (int i = 0; i < numberOfSimulatedEvents; i++) { + final String eventCorrelationId = UUID.randomUUID().toString(); + final CloudEvent cloudEvent = DmiDataAvcCloudEventCreator.createCloudEvent(eventCorrelationId); + dmiDataAvcEventProducer.publishDmiDataAvcCloudEvent(eventCorrelationId, cloudEvent); + } + + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiInEventConsumer.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiInEventConsumer.java new file mode 100644 index 00000000..3a9838b0 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiInEventConsumer.java @@ -0,0 +1,108 @@ +/* + * ============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.dmi.notifications.cmsubscription; + +import io.cloudevents.CloudEvent; +import lombok.RequiredArgsConstructor; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.onap.cps.ncmp.dmi.notifications.cmsubscription.model.CmNotificationSubscriptionStatus; +import org.onap.cps.ncmp.dmi.notifications.mapper.CloudEventMapper; +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.CmNotificationSubscriptionDmiOutEvent; +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.Data; +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CmNotificationSubscriptionDmiInEventConsumer { + + + @Value("${app.dmi.avc.cm-subscription-dmi-out}") + private String cmNotificationSubscriptionDmiOutTopic; + @Value("${dmi.service.name}") + private String dmiName; + private final KafkaTemplate cloudEventKafkaTemplate; + + /** + * Consume the cmNotificationSubscriptionDmiInCloudEvent event. + * + * @param cmNotificationSubscriptionDmiInCloudEvent the event to be consumed + */ + @KafkaListener(topics = "${app.dmi.avc.cm-subscription-dmi-in}", + containerFactory = "cloudEventConcurrentKafkaListenerContainerFactory") + public void consumeCmNotificationSubscriptionDmiInEvent( + final ConsumerRecord cmNotificationSubscriptionDmiInCloudEvent) { + final CmNotificationSubscriptionDmiInEvent cmNotificationSubscriptionDmiInEvent = + CloudEventMapper.toTargetEvent(cmNotificationSubscriptionDmiInCloudEvent.value(), + CmNotificationSubscriptionDmiInEvent.class); + if (cmNotificationSubscriptionDmiInEvent != null) { + final String subscriptionId = cmNotificationSubscriptionDmiInCloudEvent.value().getId(); + final String subscriptionType = cmNotificationSubscriptionDmiInCloudEvent.value().getType(); + final String correlationId = String.valueOf(cmNotificationSubscriptionDmiInCloudEvent.value() + .getExtension("correlationid")); + + if ("subscriptionCreateRequest".equals(subscriptionType)) { + createAndSendCmNotificationSubscriptionDmiOutEvent(subscriptionId, "subscriptionCreateResponse", + correlationId, CmNotificationSubscriptionStatus.ACCEPTED); + } else if ("subscriptionDeleteRequest".equals(subscriptionType)) { + createAndSendCmNotificationSubscriptionDmiOutEvent(subscriptionId, "subscriptionDeleteResponse", + correlationId, CmNotificationSubscriptionStatus.ACCEPTED); + } + } + } + + /** + * Create Dmi out event object and send to response topic. + * + * @param eventKey the events key + * @param subscriptionType the subscriptions type + * @param correlationId the events correlation Id + * @param cmNotificationSubscriptionStatus subscriptions status accepted/rejected + */ + public void createAndSendCmNotificationSubscriptionDmiOutEvent( + final String eventKey, final String subscriptionType, final String correlationId, + final CmNotificationSubscriptionStatus cmNotificationSubscriptionStatus) { + + final CmNotificationSubscriptionDmiOutEvent cmNotificationSubscriptionDmiOutEvent = + new CmNotificationSubscriptionDmiOutEvent(); + final Data cmNotificationSubscriptionDmiOutEventData = new Data(); + + if (cmNotificationSubscriptionStatus.equals(CmNotificationSubscriptionStatus.ACCEPTED)) { + cmNotificationSubscriptionDmiOutEventData.setStatusCode("1"); + cmNotificationSubscriptionDmiOutEventData.setStatusMessage("ACCEPTED"); + } else { + cmNotificationSubscriptionDmiOutEventData.setStatusCode("104"); + cmNotificationSubscriptionDmiOutEventData.setStatusMessage("REJECTED"); + } + cmNotificationSubscriptionDmiOutEvent.setData(cmNotificationSubscriptionDmiOutEventData); + + cloudEventKafkaTemplate.send(cmNotificationSubscriptionDmiOutTopic, eventKey, + CmNotificationSubscriptionDmiOutEventToCloudEventMapper.toCloudEvent(cmNotificationSubscriptionDmiOutEvent, + subscriptionType, dmiName, correlationId)); + + } + + + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiOutEventToCloudEventMapper.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiOutEventToCloudEventMapper.java new file mode 100644 index 00000000..51205da2 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiOutEventToCloudEventMapper.java @@ -0,0 +1,62 @@ +/* + * ============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.dmi.notifications.cmsubscription; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import java.net.URI; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.onap.cps.ncmp.dmi.exception.CloudEventConstructionException; +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.CmNotificationSubscriptionDmiOutEvent; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CmNotificationSubscriptionDmiOutEventToCloudEventMapper { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Maps SubscriptionEventResponse to a CloudEvent. + * + * @param cmSubscriptionDmiOutEvent object. + * @param subscriptionType String of subscription type. + * @param dmiName String of dmiName. + * @param correlationId String of correlationId. + * @return CloudEvent built. + */ + public static CloudEvent toCloudEvent(final CmNotificationSubscriptionDmiOutEvent cmSubscriptionDmiOutEvent, + final String subscriptionType, final String dmiName, + final String correlationId) { + try { + return CloudEventBuilder.v1().withId(UUID.randomUUID().toString()).withSource(URI.create(dmiName)) + .withType(subscriptionType) + .withDataSchema(URI.create("urn:cps:org.onap.ncmp.dmi.cm.subscription:1.0.0")) + .withExtension("correlationid", correlationId) + .withData(objectMapper.writeValueAsBytes(cmSubscriptionDmiOutEvent)).build(); + } catch (final Exception ex) { + throw new CloudEventConstructionException("The Cloud Event could not be constructed", + "Invalid object passed", ex); + } + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/model/CmNotificationSubscriptionStatus.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/model/CmNotificationSubscriptionStatus.java new file mode 100644 index 00000000..40b1297f --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/cmsubscription/model/CmNotificationSubscriptionStatus.java @@ -0,0 +1,32 @@ +/* + * ============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.dmi.notifications.cmsubscription.model; + +public enum CmNotificationSubscriptionStatus { + + ACCEPTED("ACCEPTED"), REJECTED("REJECTED"); + + private final String cmNotificationSubscriptionStatusValue; + + CmNotificationSubscriptionStatus(final String cmNotificationSubscriptionStatusValue) { + this.cmNotificationSubscriptionStatusValue = cmNotificationSubscriptionStatusValue; + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/mapper/CloudEventMapper.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/mapper/CloudEventMapper.java new file mode 100644 index 00000000..8f196cfc --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/notifications/mapper/CloudEventMapper.java @@ -0,0 +1,62 @@ +/* + * ============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.dmi.notifications.mapper; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.CloudEventUtils; +import io.cloudevents.core.data.PojoCloudEventData; +import io.cloudevents.jackson.PojoCloudEventDataMapper; +import io.cloudevents.rw.CloudEventRWException; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CloudEventMapper { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Generic method to map cloud event data to target event class object. + * + * @param cloudEvent input cloud event + * @param targetEventClass target event class + * @param target event class type + * @return mapped target event + */ + public static T toTargetEvent(final CloudEvent cloudEvent, final Class targetEventClass) { + PojoCloudEventData mappedCloudEvent = null; + + try { + mappedCloudEvent = + CloudEventUtils.mapData(cloudEvent, PojoCloudEventDataMapper.from(objectMapper, targetEventClass)); + + } catch (final CloudEventRWException cloudEventRwException) { + log.error("Unable to map cloud event to target event class type : {} with cause : {}", targetEventClass, + cloudEventRwException.getMessage()); + } + + return mappedCloudEvent == null ? null : mappedCloudEvent.getValue(); + } + +} \ No newline at end of file diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/DmiRestController.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/DmiRestController.java new file mode 100644 index 00000000..cad5e726 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/DmiRestController.java @@ -0,0 +1,253 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2023 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada + * ================================================================================ + * 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.dmi.rest.controller; + +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.dmi.model.CmHandles; +import org.onap.cps.ncmp.dmi.model.DataAccessRequest; +import org.onap.cps.ncmp.dmi.model.ModuleReferencesRequest; +import org.onap.cps.ncmp.dmi.model.ModuleResourcesReadRequest; +import org.onap.cps.ncmp.dmi.model.ModuleSet; +import org.onap.cps.ncmp.dmi.model.ResourceDataOperationRequests; +import org.onap.cps.ncmp.dmi.model.YangResources; +import org.onap.cps.ncmp.dmi.notifications.async.AsyncTaskExecutor; +import org.onap.cps.ncmp.dmi.rest.api.DmiPluginApi; +import org.onap.cps.ncmp.dmi.rest.api.DmiPluginInternalApi; +import org.onap.cps.ncmp.dmi.rest.controller.handlers.DatastoreType; +import org.onap.cps.ncmp.dmi.service.DmiService; +import org.onap.cps.ncmp.dmi.service.model.ModuleReference; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("${rest.api.dmi-base-path}") +@RestController +@Slf4j +@RequiredArgsConstructor +public class DmiRestController implements DmiPluginApi, DmiPluginInternalApi { + + private final DmiService dmiService; + private final ObjectMapper objectMapper; + private final AsyncTaskExecutor asyncTaskExecutor; + private static final Map operationToHttpStatusMap = new HashMap<>(6); + + @Value("${notification.async.executor.time-out-value-in-ms:2000}") + private int timeOutInMillis; + + static { + operationToHttpStatusMap.put(null, HttpStatus.OK); + operationToHttpStatusMap.put(OperationEnum.READ, HttpStatus.OK); + operationToHttpStatusMap.put(OperationEnum.CREATE, HttpStatus.CREATED); + operationToHttpStatusMap.put(OperationEnum.PATCH, HttpStatus.OK); + operationToHttpStatusMap.put(OperationEnum.UPDATE, HttpStatus.OK); + operationToHttpStatusMap.put(OperationEnum.DELETE, HttpStatus.NO_CONTENT); + } + + @Override + public ResponseEntity getModuleReferences(final String cmHandle, + final ModuleReferencesRequest body) { + // For onap-dmi-plugin we don't need cmHandleProperties, so DataAccessReadRequest is not used. + final ModuleSet moduleSet = dmiService.getModulesForCmHandle(cmHandle); + return ResponseEntity.ok(moduleSet); + } + + @Override + public ResponseEntity retrieveModuleResources( + final String cmHandle, + final ModuleResourcesReadRequest moduleResourcesReadRequest) { + final List moduleReferences = convertRestObjectToJavaApiObject(moduleResourcesReadRequest); + final YangResources yangResources = dmiService.getModuleResources(cmHandle, moduleReferences); + log.info("Module set tag received: {}", moduleResourcesReadRequest.getModuleSetTag()); + return new ResponseEntity<>(yangResources, HttpStatus.OK); + } + + /** + * This method register given list of cm-handles to ncmp. + * + * @param cmHandles list of cm-handles + * @return (@ code ResponseEntity) response entity + */ + public ResponseEntity registerCmHandles(final CmHandles cmHandles) { + final List cmHandlesList = cmHandles.getCmHandles(); + if (cmHandlesList.isEmpty()) { + return new ResponseEntity<>("Need at least one cmHandle to process.", HttpStatus.BAD_REQUEST); + } + dmiService.registerCmHandles(cmHandlesList); + return new ResponseEntity<>("cm-handle registered successfully.", HttpStatus.CREATED); + } + + /** + * This method is not implemented for ONAP DMI plugin. + * + * @param topic client given topic name + * @param requestId requestId generated by NCMP as an ack for client + * @param resourceDataOperationRequests list of operation details + * @return (@ code ResponseEntity) response entity + */ + @Override + public ResponseEntity getResourceDataForCmHandleDataOperation(final String topic, final String requestId, + final ResourceDataOperationRequests resourceDataOperationRequests) { + log.info("Request Details (for testing purposes)"); + log.info("Request Id: {}", requestId); + log.info("Topic: {}", topic); + + log.info("Details of the first Operation"); + log.info("Resource Identifier: {}", resourceDataOperationRequests.get(0).getResourceIdentifier()); + log.info("Module Set Tag: {}", resourceDataOperationRequests.get(0).getCmHandles().get(0).getModuleSetTag()); + log.info("Operation Id: {}", resourceDataOperationRequests.get(0).getOperationId()); + log.info("Cm Handles: {}", resourceDataOperationRequests.get(0).getCmHandles()); + log.info("Options: {}", resourceDataOperationRequests.get(0).getOptions()); + + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * This method fetches the resource for given cm handle using pass through operational or running datastore. + * It filters the response on the basis of options query parameters and returns response. Passthrough Running + * supports both read and write operation whereas passthrough operational does not support write operations. + * + * @param datastoreName name of the datastore + * @param cmHandle cm handle identifier + * @param resourceIdentifier resource identifier to fetch data + * @param optionsParamInQuery options query parameter + * @param topicParamInQuery topic name for (triggering) async responses + * @param dataAccessRequest data Access Request + * @return {@code ResponseEntity} response entity + */ + @Override + public ResponseEntity dataAccessPassthrough(final String datastoreName, + final String cmHandle, + final String resourceIdentifier, + final String optionsParamInQuery, + final String topicParamInQuery, + final DataAccessRequest dataAccessRequest) { + log.info("Module set tag: {}", dataAccessRequest.getModuleSetTag()); + if (DatastoreType.PASSTHROUGH_OPERATIONAL == DatastoreType.fromDatastoreName(datastoreName)) { + return dataAccessPassthroughOperational(resourceIdentifier, cmHandle, dataAccessRequest, + optionsParamInQuery, topicParamInQuery); + } + return dataAccessPassthroughRunning(resourceIdentifier, cmHandle, dataAccessRequest, + optionsParamInQuery, topicParamInQuery); + } + + private ResponseEntity dataAccessPassthroughOperational(final String resourceIdentifier, + final String cmHandle, + final DataAccessRequest dataAccessRequest, + final String optionsParamInQuery, + final String topicParamInQuery) { + if (isReadOperation(dataAccessRequest)) { + if (hasTopic(topicParamInQuery)) { + return handleAsyncRequest(resourceIdentifier, cmHandle, dataAccessRequest, optionsParamInQuery, + topicParamInQuery); + } + + final String resourceDataAsJson = dmiService.getResourceData(cmHandle, resourceIdentifier, + optionsParamInQuery, DmiService.RESTCONF_CONTENT_PASSTHROUGH_OPERATIONAL_QUERY_PARAM); + return ResponseEntity.ok(resourceDataAsJson); + } + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + private ResponseEntity dataAccessPassthroughRunning(final String resourceIdentifier, + final String cmHandle, + final DataAccessRequest dataAccessRequest, + final String optionsParamInQuery, + final String topicParamInQuery) { + if (hasTopic(topicParamInQuery)) { + asyncTaskExecutor.executeAsyncTask(() -> + getSdncResponseForPassThroughRunning( + resourceIdentifier, + cmHandle, + dataAccessRequest, + optionsParamInQuery), + topicParamInQuery, + dataAccessRequest.getRequestId(), + dataAccessRequest.getOperation(), + timeOutInMillis + ); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + final String sdncResponse = + getSdncResponseForPassThroughRunning(resourceIdentifier, cmHandle, dataAccessRequest, optionsParamInQuery); + return new ResponseEntity<>(sdncResponse, operationToHttpStatusMap.get(dataAccessRequest.getOperation())); + } + + private String getSdncResponseForPassThroughRunning(final String resourceIdentifier, + final String cmHandle, + final DataAccessRequest dataAccessRequest, + final String optionsParamInQuery) { + if (isReadOperation(dataAccessRequest)) { + return dmiService.getResourceData(cmHandle, resourceIdentifier, optionsParamInQuery, + DmiService.RESTCONF_CONTENT_PASSTHROUGH_RUNNING_QUERY_PARAM); + } + + return dmiService.writeData(dataAccessRequest.getOperation(), cmHandle, resourceIdentifier, + dataAccessRequest.getDataType(), dataAccessRequest.getData()); + } + + private boolean isReadOperation(final DataAccessRequest dataAccessRequest) { + return dataAccessRequest.getOperation() == null + || dataAccessRequest.getOperation().equals(DataAccessRequest.OperationEnum.READ); + } + + private List convertRestObjectToJavaApiObject( + final ModuleResourcesReadRequest moduleResourcesReadRequest) { + return objectMapper + .convertValue(moduleResourcesReadRequest.getData().getModules(), + new TypeReference>() {}); + } + + private boolean hasTopic(final String topicParamInQuery) { + return !(topicParamInQuery == null || topicParamInQuery.isBlank()); + } + + private ResponseEntity handleAsyncRequest(final String resourceIdentifier, + final String cmHandle, + final DataAccessRequest dataAccessRequest, + final String optionsParamInQuery, + final String topicParamInQuery) { + asyncTaskExecutor.executeAsyncTask(() -> + dmiService.getResourceData( + cmHandle, + resourceIdentifier, + optionsParamInQuery, + DmiService.RESTCONF_CONTENT_PASSTHROUGH_OPERATIONAL_QUERY_PARAM), + topicParamInQuery, + dataAccessRequest.getRequestId(), + dataAccessRequest.getOperation(), + timeOutInMillis + ); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/handlers/DatastoreType.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/handlers/DatastoreType.java new file mode 100644 index 00000000..3f280403 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/handlers/DatastoreType.java @@ -0,0 +1,65 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.dmi.rest.controller.handlers; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import lombok.Getter; +import org.onap.cps.ncmp.dmi.exception.InvalidDatastoreException; + +@Getter +public enum DatastoreType { + + PASSTHROUGH_RUNNING("ncmp-datastore:passthrough-running"), + PASSTHROUGH_OPERATIONAL("ncmp-datastore:passthrough-operational"); + + DatastoreType(final String datastoreName) { + this.datastoreName = datastoreName; + } + + private final String datastoreName; + private static final Map datastoreNameToDatastoreType = new HashMap<>(); + + static { + Arrays.stream(DatastoreType.values()).forEach( + type -> datastoreNameToDatastoreType.put(type.getDatastoreName(), type)); + } + + /** + * From datastore name get datastore type. + * + * @param datastoreName the datastore name + * @return the datastore type + */ + public static DatastoreType fromDatastoreName(final String datastoreName) { + + final DatastoreType datastoreType = datastoreNameToDatastoreType.get(datastoreName); + + if (null == datastoreType) { + throw new InvalidDatastoreException(datastoreName + " is an invalid datastore name"); + } + + return datastoreType; + } + +} + diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/DmiService.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/DmiService.java new file mode 100644 index 00000000..f0826a81 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/DmiService.java @@ -0,0 +1,95 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2023 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada + * ================================================================================ + * 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.dmi.service; + +import jakarta.validation.constraints.NotNull; +import java.util.List; +import org.onap.cps.ncmp.dmi.exception.DmiException; +import org.onap.cps.ncmp.dmi.model.DataAccessRequest; +import org.onap.cps.ncmp.dmi.model.ModuleSet; +import org.onap.cps.ncmp.dmi.model.YangResources; +import org.onap.cps.ncmp.dmi.service.model.ModuleReference; + + + +/** + * Interface for handling Dmi plugin Data. + */ +public interface DmiService { + + String RESTCONF_CONTENT_PASSTHROUGH_OPERATIONAL_QUERY_PARAM = "content=all"; + String RESTCONF_CONTENT_PASSTHROUGH_RUNNING_QUERY_PARAM = "content=config"; + + /** + * This method fetches all modules for given Cm Handle. + * + * @param cmHandle cm-handle to fetch the modules information + * @return {@code String} returns all modules + * @throws DmiException can throw dmi exception + */ + ModuleSet getModulesForCmHandle(String cmHandle) throws DmiException; + + /** + * This method used to register the given {@code CmHandles} which contains list of {@code CmHandle} to cps + * repository. + * + * @param cmHandles list of cm-handles + */ + void registerCmHandles(List cmHandles); + + /** + * Get module resources for the given cm handle and modules. + * + * @param cmHandle cmHandle + * @param modules a list of module data + * @return returns all yang resources + */ + YangResources getModuleResources(String cmHandle, List modules); + + /** + * This method use to fetch the resource data from cm handle for the given datastore and resource + * Identifier. Options query parameter are used to filter the response from network resource. + * + * @param cmHandle cm handle identifier + * @param resourceIdentifier resource identifier + * @param optionsParamInQuery options query parameter + * @param restconfContentQueryParam restconf content i.e. datastore to use + * @return {@code Object} response from network function + */ + String getResourceData(@NotNull String cmHandle, + @NotNull String resourceIdentifier, + String optionsParamInQuery, + String restconfContentQueryParam); + + /** + * Write resource data to sdnc (will default to 'content=config', does not need to be specified). + * + * @param cmHandle cmHandle + * @param resourceIdentifier resource identifier + * @param dataType accept header parameter + * @param data request data + * @return response from sdnc + */ + String writeData(DataAccessRequest.OperationEnum operation, String cmHandle, + String resourceIdentifier, String dataType, + String data); +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/DmiServiceImpl.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/DmiServiceImpl.java new file mode 100644 index 00000000..6acbe09b --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/DmiServiceImpl.java @@ -0,0 +1,196 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2023 Nordix Foundation + * Modifications Copyright (C) 2021-2022 Bell Canada + * ================================================================================ + * 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.dmi.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.dmi.config.DmiPluginConfig.DmiPluginProperties; +import org.onap.cps.ncmp.dmi.exception.CmHandleRegistrationException; +import org.onap.cps.ncmp.dmi.exception.DmiException; +import org.onap.cps.ncmp.dmi.exception.HttpClientRequestException; +import org.onap.cps.ncmp.dmi.exception.ModuleResourceNotFoundException; +import org.onap.cps.ncmp.dmi.exception.ModulesNotFoundException; +import org.onap.cps.ncmp.dmi.model.DataAccessRequest; +import org.onap.cps.ncmp.dmi.model.ModuleSet; +import org.onap.cps.ncmp.dmi.model.ModuleSetSchemasInner; +import org.onap.cps.ncmp.dmi.model.YangResource; +import org.onap.cps.ncmp.dmi.model.YangResources; +import org.onap.cps.ncmp.dmi.service.client.NcmpRestClient; +import org.onap.cps.ncmp.dmi.service.model.CmHandleOperation; +import org.onap.cps.ncmp.dmi.service.model.CreatedCmHandle; +import org.onap.cps.ncmp.dmi.service.model.ModuleReference; +import org.onap.cps.ncmp.dmi.service.model.ModuleSchema; +import org.onap.cps.ncmp.dmi.service.operation.SdncOperations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class DmiServiceImpl implements DmiService { + + private SdncOperations sdncOperations; + private NcmpRestClient ncmpRestClient; + private ObjectMapper objectMapper; + private DmiPluginProperties dmiPluginProperties; + + /** + * Constructor. + * + * @param dmiPluginProperties dmiPluginProperties + * @param ncmpRestClient ncmpRestClient + * @param sdncOperations sdncOperations + * @param objectMapper objectMapper + */ + public DmiServiceImpl(final DmiPluginProperties dmiPluginProperties, + final NcmpRestClient ncmpRestClient, + final SdncOperations sdncOperations, final ObjectMapper objectMapper) { + this.dmiPluginProperties = dmiPluginProperties; + this.ncmpRestClient = ncmpRestClient; + this.objectMapper = objectMapper; + this.sdncOperations = sdncOperations; + } + + @Override + public ModuleSet getModulesForCmHandle(final String cmHandle) throws DmiException { + final Collection moduleSchemas = sdncOperations.getModuleSchemasFromNode(cmHandle); + if (moduleSchemas.isEmpty()) { + throw new ModulesNotFoundException(cmHandle, "SDNC returned no modules for given cm-handle."); + } else { + final ModuleSet moduleSet = new ModuleSet(); + moduleSchemas.forEach(moduleSchema -> + moduleSet.addSchemasItem(toModuleSetSchemas(moduleSchema))); + return moduleSet; + } + } + + @Override + public YangResources getModuleResources(final String cmHandle, final List moduleReferences) { + final YangResources yangResources = new YangResources(); + for (final ModuleReference moduleReference : moduleReferences) { + final String moduleRequest = createModuleRequest(moduleReference); + final ResponseEntity responseEntity = sdncOperations.getModuleResource(cmHandle, moduleRequest); + if (responseEntity.getStatusCode() == HttpStatus.OK) { + final YangResource yangResource = YangResourceExtractor.toYangResource(moduleReference, responseEntity); + yangResources.add(yangResource); + } else if (responseEntity.getStatusCode() == HttpStatus.NOT_FOUND) { + log.error("SDNC did not return a module resource for the given cmHandle {}", cmHandle); + throw new ModuleResourceNotFoundException(cmHandle, + "SDNC did not return a module resource for the given cmHandle."); + } else { + log.error("Error occurred when getting module resources from SDNC for the given cmHandle {}", cmHandle); + throw new HttpClientRequestException( + cmHandle, responseEntity.getBody(), (HttpStatus) responseEntity.getStatusCode()); + } + } + return yangResources; + } + + @Override + public void registerCmHandles(final List cmHandles) { + final CmHandleOperation cmHandleOperation = new CmHandleOperation(); + cmHandleOperation.setDmiPlugin(dmiPluginProperties.getDmiServiceUrl()); + final List createdCmHandleList = new ArrayList<>(); + for (final String cmHandle : cmHandles) { + final CreatedCmHandle createdCmHandle = new CreatedCmHandle(); + createdCmHandle.setCmHandle(cmHandle); + createdCmHandleList.add(createdCmHandle); + } + cmHandleOperation.setCreatedCmHandles(createdCmHandleList); + final String cmHandlesJson; + try { + cmHandlesJson = objectMapper.writeValueAsString(cmHandleOperation); + } catch (final JsonProcessingException e) { + log.error("Parsing error occurred while converting cm-handles to JSON {}", cmHandles); + throw new DmiException("Internal Server Error.", + "Parsing error occurred while converting given cm-handles object list to JSON "); + } + final ResponseEntity responseEntity = ncmpRestClient.registerCmHandlesWithNcmp(cmHandlesJson); + if (!responseEntity.getStatusCode().is2xxSuccessful()) { + throw new CmHandleRegistrationException(responseEntity.getBody()); + } + } + + private ModuleSetSchemasInner toModuleSetSchemas(final ModuleSchema moduleSchema) { + final ModuleSetSchemasInner moduleSetSchemas = new ModuleSetSchemasInner(); + moduleSetSchemas.setModuleName(moduleSchema.getIdentifier()); + moduleSetSchemas.setNamespace(moduleSchema.getNamespace()); + moduleSetSchemas.setRevision(moduleSchema.getVersion()); + return moduleSetSchemas; + } + + @Override + public String getResourceData(final String cmHandle, + final String resourceIdentifier, + final String optionsParamInQuery, + final String restconfContentQueryParam) { + final ResponseEntity responseEntity = sdncOperations.getResouceDataForOperationalAndRunning(cmHandle, + resourceIdentifier, + optionsParamInQuery, + restconfContentQueryParam); + return prepareAndSendResponse(responseEntity, cmHandle); + } + + @Override + public String writeData(final DataAccessRequest.OperationEnum operation, + final String cmHandle, + final String resourceIdentifier, + final String dataType, final String data) { + final ResponseEntity responseEntity = + sdncOperations.writeData(operation, cmHandle, resourceIdentifier, dataType, data); + return prepareAndSendResponse(responseEntity, cmHandle); + } + + private String prepareAndSendResponse(final ResponseEntity responseEntity, final String cmHandle) { + if (responseEntity.getStatusCode().is2xxSuccessful()) { + return responseEntity.getBody(); + } else { + throw new HttpClientRequestException(cmHandle, responseEntity.getBody(), + (HttpStatus) responseEntity.getStatusCode()); + } + } + + private String createModuleRequest(final ModuleReference moduleReference) { + final Map ietfNetconfModuleReferences = new LinkedHashMap<>(); + ietfNetconfModuleReferences.put("ietf-netconf-monitoring:identifier", moduleReference.getName()); + ietfNetconfModuleReferences.put("ietf-netconf-monitoring:version", moduleReference.getRevision()); + final ObjectWriter objectWriter = objectMapper.writer().withRootName("ietf-netconf-monitoring:input"); + final String moduleRequest; + try { + moduleRequest = objectWriter.writeValueAsString(ietfNetconfModuleReferences); + } catch (final JsonProcessingException e) { + log.error("JSON exception occurred when creating the module request for the given module reference {}", + moduleReference.getName()); + throw new DmiException("Unable to process JSON.", + "JSON exception occurred when creating the module request.", e); + } + return moduleRequest; + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/YangResourceExtractor.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/YangResourceExtractor.java new file mode 100644 index 00000000..d41240b9 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/YangResourceExtractor.java @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.service; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.dmi.exception.ModuleResourceNotFoundException; +import org.onap.cps.ncmp.dmi.model.YangResource; +import org.onap.cps.ncmp.dmi.service.model.ModuleReference; +import org.springframework.http.ResponseEntity; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class YangResourceExtractor { + + static YangResource toYangResource(final ModuleReference moduleReference, + final ResponseEntity responseEntity) { + final YangResource yangResource = new YangResource(); + yangResource.setModuleName(moduleReference.getName()); + yangResource.setRevision(moduleReference.getRevision()); + yangResource.setYangSource(extractYangSourceFromBody(responseEntity)); + return yangResource; + } + + private static String extractYangSourceFromBody(final ResponseEntity responseEntity) { + final JsonObject responseBodyAsJsonObject = new Gson().fromJson(responseEntity.getBody(), JsonObject.class); + final JsonObject monitoringOutputAsJsonObject = + responseBodyAsJsonObject.getAsJsonObject("ietf-netconf-monitoring:output"); + if (monitoringOutputAsJsonObject == null + || monitoringOutputAsJsonObject.getAsJsonPrimitive("data") == null) { + log.error("Error occurred when trying to parse the response body from sdnc {}", responseEntity.getBody()); + throw new ModuleResourceNotFoundException(responseEntity.getBody(), + "Error occurred when trying to parse the response body from sdnc."); + } + return monitoringOutputAsJsonObject.getAsJsonPrimitive("data").getAsString(); + } + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/NcmpRestClient.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/NcmpRestClient.java new file mode 100644 index 00000000..94783f3b --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/NcmpRestClient.java @@ -0,0 +1,64 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.service.client; + +import org.onap.cps.ncmp.dmi.config.DmiConfiguration.CpsProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +public class NcmpRestClient { + + private CpsProperties cpsProperties; + private RestTemplate restTemplate; + + public NcmpRestClient(final CpsProperties cpsProperties, final RestTemplate restTemplate) { + this.cpsProperties = cpsProperties; + this.restTemplate = restTemplate; + } + + /** + * Register a cmHandle with NCMP using a HTTP call. + * @param jsonData json data + * @return the response entity + */ + public ResponseEntity registerCmHandlesWithNcmp(final String jsonData) { + final var ncmpRegistrationUrl = buildNcmpRegistrationUrl(); + final var httpHeaders = new HttpHeaders(); + httpHeaders.setBasicAuth(cpsProperties.getAuthUsername(), cpsProperties.getAuthPassword()); + httpHeaders.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + final var httpEntity = new HttpEntity<>(jsonData, httpHeaders); + return restTemplate.exchange(ncmpRegistrationUrl, HttpMethod.POST, httpEntity, String.class); + } + + private String buildNcmpRegistrationUrl() { + return UriComponentsBuilder + .fromHttpUrl(cpsProperties.getBaseUrl()) + .path(cpsProperties.getDmiRegistrationUrl()) + .toUriString(); + } +} \ No newline at end of file diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClient.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClient.java new file mode 100644 index 00000000..179707ab --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClient.java @@ -0,0 +1,87 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2022 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.dmi.service.client; + +import org.onap.cps.ncmp.dmi.config.DmiConfiguration.SdncProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class SdncRestconfClient { + + private SdncProperties sdncProperties; + private RestTemplate restTemplate; + + public SdncRestconfClient(final SdncProperties sdncProperties, final RestTemplate restTemplate) { + this.sdncProperties = sdncProperties; + this.restTemplate = restTemplate; + } + + /** + * restconf get operation on sdnc. + * + * @param getResourceUrl sdnc get url + * @return the response entity + */ + public ResponseEntity getOperation(final String getResourceUrl) { + return getOperation(getResourceUrl, new HttpHeaders()); + } + + /** + * Overloaded restconf get operation on sdnc with http headers. + * + * @param getResourceUrl sdnc get url + * @param httpHeaders http headers + * @return the response entity + */ + public ResponseEntity getOperation(final String getResourceUrl, final HttpHeaders httpHeaders) { + return httpOperationWithJsonData(HttpMethod.GET, getResourceUrl, null, httpHeaders); + } + + /** + * restconf http operations on sdnc. + * + * @param httpMethod HTTP Method + * @param resourceUrl sdnc resource url + * @param jsonData json data + * @param httpHeaders HTTP Headers + * @return response entity + */ + public ResponseEntity httpOperationWithJsonData(final HttpMethod httpMethod, + final String resourceUrl, + final String jsonData, + final HttpHeaders httpHeaders) { + final String sdncBaseUrl = sdncProperties.getBaseUrl(); + final String sdncRestconfUrl = sdncBaseUrl.concat(resourceUrl); + httpHeaders.setBasicAuth(sdncProperties.getAuthUsername(), sdncProperties.getAuthPassword()); + final HttpEntity httpEntity; + if (jsonData == null) { + httpEntity = new HttpEntity<>(httpHeaders); + } else { + httpEntity = new HttpEntity<>(jsonData, httpHeaders); + } + return restTemplate.exchange(sdncRestconfUrl, httpMethod, httpEntity, String.class); + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/CmHandleOperation.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/CmHandleOperation.java new file mode 100644 index 00000000..82eac92a --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/CmHandleOperation.java @@ -0,0 +1,35 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.service.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Setter +public class CmHandleOperation { + + private String dmiPlugin; + private List createdCmHandles; +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/CreatedCmHandle.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/CreatedCmHandle.java new file mode 100644 index 00000000..6ab6a01e --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/CreatedCmHandle.java @@ -0,0 +1,36 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.service.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Setter +public class CreatedCmHandle { + + private String cmHandle; + private Map cmHandleProperties; + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/ModuleReference.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/ModuleReference.java new file mode 100644 index 00000000..75c37dff --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/ModuleReference.java @@ -0,0 +1,37 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.service.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +/** + * Module Reference. + */ +@Getter +@Setter +@EqualsAndHashCode +public class ModuleReference { + + private String name; + private String revision; +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/ModuleSchema.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/ModuleSchema.java new file mode 100644 index 00000000..c77e0e41 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/model/ModuleSchema.java @@ -0,0 +1,36 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * Modifications Copyright (C) 2021 Bell Canada + * ================================================================================ + * 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.dmi.service.model; + +import java.util.List; +import lombok.Data; + +@Data +public class ModuleSchema { + + private String identifier; + private String version; + private String format; + private String namespace; + private List location; + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java new file mode 100644 index 00000000..fd94e634 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java @@ -0,0 +1,270 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2023 Nordix Foundation + * Modifications Copyright (C) 2021-2022 Bell Canada + * ================================================================================ + * 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.dmi.service.operation; + +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.JsonPathException; +import com.jayway.jsonpath.TypeRef; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.onap.cps.ncmp.dmi.config.DmiConfiguration.SdncProperties; +import org.onap.cps.ncmp.dmi.exception.SdncException; +import org.onap.cps.ncmp.dmi.model.DataAccessRequest; +import org.onap.cps.ncmp.dmi.service.client.SdncRestconfClient; +import org.onap.cps.ncmp.dmi.service.model.ModuleSchema; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +public class SdncOperations { + + private static final String TOPOLOGY_URL_TEMPLATE_DATA = "/rests/data/network-topology:network-topology/"; + private static final String TOPOLOGY_URL_TEMPLATE_OPERATIONAL = + "/rests/operations/network-topology:network-topology/"; + private static final String GET_SCHEMA_URL = "ietf-netconf-monitoring:netconf-state/schemas"; + private static final String GET_SCHEMA_SOURCES_URL = "/ietf-netconf-monitoring:get-schema"; + private static final String PATH_TO_MODULE_SCHEMAS = "$.ietf-netconf-monitoring:schemas.schema"; + private static final int QUERY_PARAM_SPLIT_LIMIT = 2; + private static final int QUERY_PARAM_VALUE_INDEX = 1; + private static final int QUERY_PARAM_NAME_INDEX = 0; + + private static EnumMap operationToHttpMethodMap = new EnumMap<>(OperationEnum.class); + + static { + operationToHttpMethodMap.put(OperationEnum.READ, HttpMethod.GET); + operationToHttpMethodMap.put(OperationEnum.CREATE, HttpMethod.POST); + operationToHttpMethodMap.put(OperationEnum.PATCH, HttpMethod.PATCH); + operationToHttpMethodMap.put(OperationEnum.UPDATE, HttpMethod.PUT); + operationToHttpMethodMap.put(OperationEnum.DELETE, HttpMethod.DELETE); + } + + private final SdncProperties sdncProperties; + private final SdncRestconfClient sdncRestconfClient; + private final String topologyUrlData; + private final String topologyUrlOperational; + + private Configuration jsonPathConfiguration = Configuration.builder() + .mappingProvider(new JacksonMappingProvider()) + .jsonProvider(new JacksonJsonProvider()) + .build(); + + /** + * Constructor for {@code SdncOperations}. This method also manipulates url properties. + * + * @param sdncProperties {@code SdncProperties} + * @param sdncRestconfClient {@code SdncRestconfClient} + */ + public SdncOperations(final SdncProperties sdncProperties, final SdncRestconfClient sdncRestconfClient) { + this.sdncProperties = sdncProperties; + this.sdncRestconfClient = sdncRestconfClient; + topologyUrlOperational = getTopologyUrlOperational(); + topologyUrlData = getTopologyUrlData(); + } + + /** + * This method fetches list of modules usind sdnc client. + * + * @param nodeId node id for node + * @return a collection of module schemas + */ + public Collection getModuleSchemasFromNode(final String nodeId) { + final String urlWithNodeId = prepareGetSchemaUrl(nodeId); + final ResponseEntity modulesResponseEntity = sdncRestconfClient.getOperation(urlWithNodeId); + if (modulesResponseEntity.getStatusCode() == HttpStatus.OK) { + final String modulesResponseBody = modulesResponseEntity.getBody(); + if (modulesResponseBody == null || modulesResponseBody.isBlank()) { + return Collections.emptyList(); + } + return convertToModuleSchemas(modulesResponseBody); + } + throw new SdncException( + String.format("SDNC failed to get Modules Schema for node %s", nodeId), + (HttpStatus) modulesResponseEntity.getStatusCode(), modulesResponseEntity.getBody()); + } + + /** + * Get module schema. + * + * @param nodeId node ID + * @param moduleProperties module properties + * @return response entity + */ + public ResponseEntity getModuleResource(final String nodeId, final String moduleProperties) { + final String getYangResourceUrl = prepareGetOperationSchemaUrl(nodeId); + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + return sdncRestconfClient.httpOperationWithJsonData( + HttpMethod.POST, getYangResourceUrl, moduleProperties, httpHeaders); + } + + /** + * This method fetches the resource data for given node identifier on given resource using sdnc client. + * + * @param nodeId network resource identifier + * @param resourceId resource identifier + * @param optionsParamInQuery fields query + * @param restConfContentQueryParam restConf content query param + * @return {@code ResponseEntity} response entity + */ + public ResponseEntity getResouceDataForOperationalAndRunning(final String nodeId, + final String resourceId, + final String optionsParamInQuery, + final String restConfContentQueryParam) { + final String getResourceDataUrl = prepareResourceDataUrl(nodeId, + resourceId, buildQueryParamMap(optionsParamInQuery, restConfContentQueryParam)); + return sdncRestconfClient.getOperation(getResourceDataUrl); + } + + /** + * Write resource data. + * + * @param nodeId network resource identifier + * @param resourceId resource identifier + * @param contentType http content type + * @param requestData request data + * @return {@code ResponseEntity} response entity + */ + public ResponseEntity writeData(final DataAccessRequest.OperationEnum operation, + final String nodeId, + final String resourceId, + final String contentType, + final String requestData) { + final String getResourceDataUrl = prepareWriteUrl(nodeId, resourceId); + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.parseMediaType(contentType)); + final HttpMethod httpMethod = operationToHttpMethodMap.get(operation); + return sdncRestconfClient.httpOperationWithJsonData(httpMethod, getResourceDataUrl, requestData, httpHeaders); + } + + private MultiValueMap buildQueryParamMap(final String optionsParamInQuery, + final String restConfContentQueryParam) { + return getQueryParamsAsMap(optionsParamInQuery, restConfContentQueryParam); + } + + private MultiValueMap getQueryParamsAsMap(final String optionsParamInQuery, + final String restConfContentQueryParam) { + final MultiValueMap queryParams = new LinkedMultiValueMap<>(); + if (optionsParamInQuery != null && !optionsParamInQuery.isBlank()) { + queryParams.setAll(extractQueryParams(optionsParamInQuery, restConfContentQueryParam)); + } + return queryParams; + } + + private String stripParenthesisFromOptionsQuery(final String optionsParamInQuery) { + return optionsParamInQuery.substring(1, optionsParamInQuery.length() - 1); + } + + private String prepareGetSchemaUrl(final String nodeId) { + return addResource(addTopologyDataUrlwithNode(nodeId), GET_SCHEMA_URL); + } + + private String prepareWriteUrl(final String nodeId, final String resourceId) { + return addResource(addTopologyDataUrlwithNode(nodeId), resourceId); + } + + private String prepareGetOperationSchemaUrl(final String nodeId) { + return UriComponentsBuilder.fromUriString(topologyUrlOperational) + .pathSegment("node={nodeId}") + .pathSegment("yang-ext:mount") + .path(GET_SCHEMA_SOURCES_URL) + .buildAndExpand(nodeId).toUriString(); + } + + private String prepareResourceDataUrl(final String nodeId, + final String resourceId, + final MultiValueMap queryMap) { + return addQuery(addResource(addTopologyDataUrlwithNode(nodeId), resourceId), queryMap); + } + + private String addResource(final String url, final String resourceId) { + return UriComponentsBuilder.fromUriString(url) + .pathSegment(resourceId) + .buildAndExpand().toUriString(); + } + + private String addQuery(final String url, final MultiValueMap queryMap) { + return UriComponentsBuilder.fromUriString(url) + .queryParams(queryMap) + .buildAndExpand().toUriString(); + } + + private String addTopologyDataUrlwithNode(final String nodeId) { + return UriComponentsBuilder.fromUriString(topologyUrlData) + .pathSegment("node={nodeId}") + .pathSegment("yang-ext:mount") + .buildAndExpand(nodeId).toUriString(); + } + + private List convertToModuleSchemas(final String modulesListAsJson) { + try { + return JsonPath.using(jsonPathConfiguration).parse(modulesListAsJson).read( + PATH_TO_MODULE_SCHEMAS, new TypeRef<>() { + }); + } catch (final JsonPathException jsonPathException) { + throw new SdncException("SDNC Response processing failed", + "SDNC response is not in the expected format.", jsonPathException); + } + } + + private String getTopologyUrlData() { + return UriComponentsBuilder.fromUriString(TOPOLOGY_URL_TEMPLATE_DATA) + .path("topology={topologyId}") + .buildAndExpand(this.sdncProperties.getTopologyId()).toUriString(); + } + + private String getTopologyUrlOperational() { + return UriComponentsBuilder.fromUriString( + TOPOLOGY_URL_TEMPLATE_OPERATIONAL) + .path("topology={topologyId}") + .buildAndExpand(this.sdncProperties.getTopologyId()).toUriString(); + } + + private Map extractQueryParams(final String optionsParamInQuery, + final String restConfContentQueryParam) { + final String QueryParamsAsString = stripParenthesisFromOptionsQuery(optionsParamInQuery) + + "," + restConfContentQueryParam; + final String[] splitTempQueryByComma = QueryParamsAsString.split(","); + return Arrays.stream(splitTempQueryByComma) + .map(queryParamPair -> queryParamPair.split("=", QUERY_PARAM_SPLIT_LIMIT)) + .filter(queryParam -> queryParam.length > 1) + .collect(Collectors.toMap( + queryParam -> queryParam[QUERY_PARAM_NAME_INDEX], + queryParam -> queryParam[QUERY_PARAM_VALUE_INDEX])); + } +} diff --git a/dmi-service/src/main/resources/application.yml b/dmi-service/src/main/resources/application.yml new file mode 100644 index 00000000..003aa191 --- /dev/null +++ b/dmi-service/src/main/resources/application.yml @@ -0,0 +1,128 @@ +# ============LICENSE_START======================================================= +# Copyright (C) 2021-2024 Nordix Foundation +# Modifications Copyright (C) 2021 Bell Canada. +# ================================================================================ +# 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========================================================= + +server: + port: 8080 + +dmi: + service: + url: ${DMI_SERVICE_URL} + name: ${DMI_SERVICE_NAME:ncmp-dmi-plugin} + +rest: + api: + dmi-base-path: /dmi + +security: + permit-uri: /actuator/**,/swagger-ui.html,/swagger-ui/**,/swagger-resources/**,/api-docs/**,/v3/api-docs/** + auth: + username: ${DMI_USERNAME} + password: ${DMI_PASSWORD} + +# When updating to sprinboot 2.6.4 an exception would occur when starting the container +# "Failed to start bean 'documentationPluginsBootstrapper'. +# This is a known issue with springfox and springboot introduced in 2.6.x: +# https://github.com/springfox/springfox/issues/3462 +spring: + application: + name: ncmp-dmi-plugin + mvc: + pathmatch: + matching-strategy: ANT_PATH_MATCHER + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVER:localhost:9092} + security: + protocol: PLAINTEXT + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: io.cloudevents.kafka.CloudEventSerializer + client-id: ncmp-dmi-plugin + consumer: + group-id: ${NCMP_CONSUMER_GROUP_ID:ncmp-group} + key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer + value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer + properties: + spring.deserializer.key.delegate.class: org.apache.kafka.common.serialization.StringDeserializer + spring.deserializer.value.delegate.class: io.cloudevents.kafka.CloudEventDeserializer + spring.json.use.type.headers: false + + jackson: + serialization: + FAIL_ON_EMPTY_BEANS: false + +app: + ncmp: + async: + topic: ${NCMP_ASYNC_M2M_TOPIC:ncmp-async-m2m} + dmi: + avc: + 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} + +notification: + async: + executor: + time-out-value-in-ms: 2000 + +# Actuator +management: + endpoints: + web: + exposure: + include: info,health,loggers,prometheus + endpoint: + health: + show-details: always + # kubernetes probes: liveness and readiness + probes: + enabled: true + loggers: + enabled: true + +cps-core: + baseUrl: http://${CPS_CORE_HOST}:${CPS_CORE_PORT} + dmiRegistrationUrl : /ncmpInventory/v1/ch + auth: + username: ${CPS_CORE_USERNAME} + password: ${CPS_CORE_PASSWORD} + +sdnc: + baseUrl: http://${SDNC_HOST}:${SDNC_PORT} + topologyId: ${SDNC_TOPOLOGY_ID:topology-netconf} + auth: + username: ${SDNC_USERNAME} + password: ${SDNC_PASSWORD} + +logging: + format: json + level: + org.springframework: ERROR + org.onap.cps: DEBUG + pattern: + console: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" + file: "%d %p %c{1.} [%t] %m%n" + file: dmi.log + +springdoc: + swagger-ui: + disable-swagger-default-url: true + urlsPrimaryName: query + urls: + - name: query + url: /api-docs/openapi.yaml diff --git a/dmi-service/src/main/resources/logback-spring.xml b/dmi-service/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..355209b4 --- /dev/null +++ b/dmi-service/src/main/resources/logback-spring.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + true + + { + "logTimeStamp": "${currentTimeStamp:-}", + "logTypeName": "", + "logLevel": "%level", + "traceId": "%X{traceId:-}", + "statusCode": "", + "principalId": "${username:-}", + "serviceName": "${springAppName:-}", + "message": "%message", + "spanId": "%X{spanId:-}", + "processId": "${PID:-}", + "threadName": "%thread", + "class": "%logger{40}", + "exception": "%wEx" + } + + + + + + + + + + + + + + + + + + + + + + diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/api/kafka/MessagingBaseSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/api/kafka/MessagingBaseSpec.groovy new file mode 100644 index 00000000..13dd043d --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/api/kafka/MessagingBaseSpec.groovy @@ -0,0 +1,79 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.dmi.api.kafka + +import io.cloudevents.CloudEvent +import io.cloudevents.kafka.CloudEventDeserializer +import io.cloudevents.kafka.CloudEventSerializer +import org.apache.kafka.clients.consumer.KafkaConsumer +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.support.serializer.JsonSerializer +import org.springframework.test.context.DynamicPropertyRegistry +import org.springframework.test.context.DynamicPropertySource +import org.testcontainers.containers.KafkaContainer +import org.testcontainers.utility.DockerImageName +import spock.lang.Specification + +class MessagingBaseSpec extends Specification { + + def setupSpec() { + kafkaTestContainer.start() + } + + def cleanupSpec() { + kafkaTestContainer.stop() + } + + static kafkaTestContainer = new KafkaContainer(DockerImageName.parse('registry.nordix.org/onaptest/confluentinc/cp-kafka:6.2.1').asCompatibleSubstituteFor('confluentinc/cp-kafka')) + + def producerConfigProperties(valueSerializer) { + return [('bootstrap.servers'): kafkaTestContainer.getBootstrapServers().split(',')[0], + ('retries') : 0, + ('batch-size') : 16384, + ('linger.ms') : 1, + ('buffer.memory') : 33554432, + ('key.serializer') : StringSerializer, + ('value.serializer') : valueSerializer] + } + + def consumerConfigProperties(consumerGroupId, valueDeserializer) { + return [('bootstrap.servers') : kafkaTestContainer.getBootstrapServers().split(',')[0], + ('key.deserializer') : StringDeserializer, + ('value.deserializer'): valueDeserializer, + ('auto.offset.reset') : 'earliest', + ('group.id') : consumerGroupId + ] + } + + def kafkaTemplate = new KafkaTemplate<>(new DefaultKafkaProducerFactory(producerConfigProperties(JsonSerializer))) + def kafkaConsumer = new KafkaConsumer<>(consumerConfigProperties('ncmp-test-group', StringDeserializer)) + + def cloudEventKafkaTemplate = new KafkaTemplate(new DefaultKafkaProducerFactory(producerConfigProperties(CloudEventSerializer))) + def cloudEventKafkaConsumer = new KafkaConsumer<>(consumerConfigProperties('ncmp-test-group', CloudEventDeserializer)) + + @DynamicPropertySource + static void registerKafkaProperties(DynamicPropertyRegistry dynamicPropertyRegistry) { + dynamicPropertyRegistry.add('spring.kafka.bootstrap-servers', kafkaTestContainer::getBootstrapServers) + } +} diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/DmiConfigurationSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/DmiConfigurationSpec.groovy new file mode 100644 index 00000000..9d80b71f --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/DmiConfigurationSpec.groovy @@ -0,0 +1,66 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.config + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@SpringBootTest +@ContextConfiguration(classes = [DmiConfiguration.CpsProperties, DmiConfiguration.SdncProperties]) +class DmiConfigurationSpec extends Specification { + + @Autowired + DmiConfiguration.CpsProperties cpsProperties + + @Autowired + DmiConfiguration.SdncProperties sdncProperties + + def 'CPS properties configuration.'() { + expect: 'CPS properties are set to values in test configuration yaml file' + cpsProperties.baseUrl == 'some url for cps' + cpsProperties.dmiRegistrationUrl == 'some registration url' + cpsProperties.authUsername == 'some cps core user' + cpsProperties.authPassword == 'some cps core password' + } + + def 'SDNC properties configuration.'() { + expect: 'SDNC properties are set to values in test configuration yaml file' + sdncProperties.authUsername == 'test' + sdncProperties.authPassword == 'test' + sdncProperties.baseUrl == 'http://test' + sdncProperties.topologyId == 'test-topology' + } + + def 'Rest template building.'() { + given: 'a DMI configuration' + DmiConfiguration objectUnderTest = new DmiConfiguration() + and: 'a rest template builder' + RestTemplateBuilder mockRestTemplateBuilder = Spy(RestTemplateBuilder) + when: 'rest template method is invoked' + objectUnderTest.restTemplate(mockRestTemplateBuilder) + then: 'DMI configuration uses the build method on the template builder' + 1 * mockRestTemplateBuilder.build() + } + +} diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/DmiPluginConfigSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/DmiPluginConfigSpec.groovy new file mode 100644 index 00000000..c09403d7 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/DmiPluginConfigSpec.groovy @@ -0,0 +1,52 @@ +/* + * ============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.dmi.config + +import org.springdoc.core.models.GroupedOpenApi +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@SpringBootTest +@ContextConfiguration(classes = [DmiPluginConfig.DmiPluginProperties]) +class DmiPluginConfigSpec extends Specification { + + @Autowired + DmiPluginConfig.DmiPluginProperties dmiPluginProperties + + def 'DMI plugin properties configuration.'() { + expect: 'DMI plugin properties are set to values in test configuration yaml file' + dmiPluginProperties.dmiServiceUrl == 'some url for the dmi service' + } + + def 'DMI plugin api creation.'() { + given: 'a DMI plugin configuration' + DmiPluginConfig objectUnderTest = new DmiPluginConfig() + when: 'the api method is invoked' + def result = objectUnderTest.api() + then: 'a spring web plugin docket is returned' + result instanceof GroupedOpenApi + and: 'it is named "dmi-plugin-api"' + result.group == 'dmi-plugin-api' + } + +} diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/kafka/KafkaConfigSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/kafka/KafkaConfigSpec.groovy new file mode 100644 index 00000000..a3bf52b3 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/config/kafka/KafkaConfigSpec.groovy @@ -0,0 +1,62 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.dmi.config.kafka + +import io.cloudevents.CloudEvent +import io.cloudevents.kafka.CloudEventDeserializer +import io.cloudevents.kafka.CloudEventSerializer +import org.spockframework.spring.EnableSharedInjection +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.kafka.KafkaProperties +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.support.serializer.JsonDeserializer +import org.springframework.kafka.support.serializer.JsonSerializer +import spock.lang.Shared +import spock.lang.Specification + +@SpringBootTest(classes = [KafkaProperties, KafkaConfig]) +@EnableSharedInjection +@EnableConfigurationProperties +class KafkaConfigSpec extends Specification { + + @Shared + @Autowired + KafkaTemplate legacyEventKafkaTemplate + + @Shared + @Autowired + KafkaTemplate cloudEventKafkaTemplate + + def 'Verify kafka template serializer and deserializer configuration for #eventType.'() { + expect: 'kafka template is instantiated' + assert kafkaTemplateInstance.properties['beanName'] == beanName + and: 'verify event key and value serializer' + assert kafkaTemplateInstance.properties['producerFactory'].configs['value.serializer'].asType(String.class).contains(valueSerializer.getCanonicalName()) + and: 'verify event key and value deserializer' + assert kafkaTemplateInstance.properties['consumerFactory'].configs['spring.deserializer.value.delegate.class'].asType(String.class).contains(delegateDeserializer.getCanonicalName()) + where: 'the following event type is used' + eventType | kafkaTemplateInstance || beanName | valueSerializer | delegateDeserializer + 'legacy event' | legacyEventKafkaTemplate || 'legacyEventKafkaTemplate' | JsonSerializer | JsonDeserializer + 'cloud event' | cloudEventKafkaTemplate || 'cloudEventKafkaTemplate' | CloudEventSerializer | CloudEventDeserializer + } +} diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/datajobs/rest/controller/DmiDatajobsRestControllerSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/datajobs/rest/controller/DmiDatajobsRestControllerSpec.groovy new file mode 100644 index 00000000..c55f53c1 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/datajobs/rest/controller/DmiDatajobsRestControllerSpec.groovy @@ -0,0 +1,69 @@ +/* + * ============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.dmi.datajobs.rest.controller + +import org.onap.cps.ncmp.dmi.config.WebSecurityConfig +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.Import +import org.springframework.http.HttpStatus +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.MockMvc +import spock.lang.Specification + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post + +@Import(WebSecurityConfig) +@WebMvcTest(DmiDatajobsRestController.class) +@WithMockUser +class DmiDatajobsRestControllerSpec extends Specification{ + + @Autowired + private MockMvc mvc + + @Value('${rest.api.dmi-base-path}/v1') + def basePathV1 + + def 'write request should return 501 HTTP Status' () { + given: 'URL to write a data job' + def getModuleUrl = "${basePathV1}/writeJob/001" + when: 'the request is posted' + def response = mvc.perform( + post(getModuleUrl) + .contentType('application/3gpp-json-patch+json') + ).andReturn().response + then: 'response value is Not Implemented' + response.status == HttpStatus.NOT_IMPLEMENTED.value() + } + + def 'read request should return 501 HTTP Status' () { + given: 'URL to write a data job' + def getModuleUrl = "${basePathV1}/readJob/001" + when: 'the request is posted' + def response = mvc.perform( + post(getModuleUrl) + .contentType('application/3gpp-json-patch+json') + ).andReturn().response + then: 'response value is Not Implemented' + response.status == HttpStatus.NOT_IMPLEMENTED.value() + } +} diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/async/AsyncTaskExecutorIntegrationSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/async/AsyncTaskExecutorIntegrationSpec.groovy new file mode 100644 index 00000000..12ca05cf --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/async/AsyncTaskExecutorIntegrationSpec.groovy @@ -0,0 +1,110 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-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.dmi.notifications.async + +import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.ncmp.dmi.api.kafka.MessagingBaseSpec +import org.onap.cps.ncmp.dmi.exception.HttpClientRequestException +import org.onap.cps.ncmp.dmi.model.DataAccessRequest +import org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent +import org.spockframework.spring.SpringBean +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpStatus +import org.springframework.test.annotation.DirtiesContext +import org.testcontainers.spock.Testcontainers + +import java.time.Duration +import java.util.function.Supplier + +@SpringBootTest(classes = [AsyncTaskExecutor, DmiAsyncRequestResponseEventProducer]) +@Testcontainers +@DirtiesContext +class AsyncTaskExecutorIntegrationSpec extends MessagingBaseSpec { + + @SpringBean + DmiAsyncRequestResponseEventProducer cpsAsyncRequestResponseEventProducer = + new DmiAsyncRequestResponseEventProducer(kafkaTemplate) + + def spiedObjectMapper = Spy(ObjectMapper) + def mockSupplier = Mock(Supplier) + + def objectUnderTest = new AsyncTaskExecutor(cpsAsyncRequestResponseEventProducer) + + private static final String TEST_TOPIC = 'test-topic' + + def setup() { + cpsAsyncRequestResponseEventProducer.dmiNcmpTopic = TEST_TOPIC + kafkaConsumer.subscribe([TEST_TOPIC] as List) + } + + def cleanup() { + kafkaConsumer.close() + } + + def 'Publish and Subscribe message - success'() { + when: 'a successful event is published' + objectUnderTest.publishAsyncEvent(TEST_TOPIC, '12345','{}', 'OK', '200') + and: 'the topic is polled' + def records = kafkaConsumer.poll(Duration.ofMillis(1500)) + then: 'the record received is the event sent' + def record = records.iterator().next() + DmiAsyncRequestResponseEvent event = spiedObjectMapper.readValue(record.value(), DmiAsyncRequestResponseEvent) + and: 'the status & code matches expected' + assert event.getEventContent().getResponseStatus() == 'OK' + assert event.getEventContent().getResponseCode() == '200' + } + + def 'Publish and Subscribe message - failure'() { + when: 'a failure event is published' + def exception = new HttpClientRequestException('some cm handle', 'Node not found', HttpStatus.INTERNAL_SERVER_ERROR) + objectUnderTest.publishAsyncFailureEvent(TEST_TOPIC, '67890', exception) + and: 'the topic is polled' + def records = kafkaConsumer.poll(Duration.ofMillis(1500)) + then: 'the record received is the event sent' + def record = records.iterator().next() + DmiAsyncRequestResponseEvent event = spiedObjectMapper.readValue(record.value(), DmiAsyncRequestResponseEvent) + and: 'the status & code matches expected' + assert event.getEventContent().getResponseStatus() == 'Internal Server Error' + assert event.getEventContent().getResponseCode() == '500' + } + + def 'Execute an Async Task using asyncTaskExecutor and throw an error'() { + given: 'A task to be executed' + def requestId = '123456' + def operationEnum = DataAccessRequest.OperationEnum.CREATE + def timeOut = 100 + when: 'AsyncTask has been executed' + objectUnderTest.executeAsyncTask(taskSupplierForFailingTask(), TEST_TOPIC, requestId, operationEnum, timeOut) + def records = kafkaConsumer.poll(Duration.ofMillis(1500)) + then: 'the record received is the event sent' + def record = records.iterator().next() + DmiAsyncRequestResponseEvent event = spiedObjectMapper.readValue(record.value(), DmiAsyncRequestResponseEvent) + and: 'the status & code matches expected' + assert event.getEventContent().getResponseStatus() == 'Internal Server Error' + assert event.getEventContent().getResponseCode() == '500' + + } + + def taskSupplierForFailingTask() { + return () -> { throw new RuntimeException('original exception message') } + } + +} \ No newline at end of file diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/avc/AvcEventExecutorIntegrationSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/avc/AvcEventExecutorIntegrationSpec.groovy new file mode 100644 index 00000000..a7557bb9 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/avc/AvcEventExecutorIntegrationSpec.groovy @@ -0,0 +1,61 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.dmi.notifications.avc + +import com.fasterxml.jackson.databind.ObjectMapper +import io.cloudevents.core.CloudEventUtils +import io.cloudevents.jackson.PojoCloudEventDataMapper +import org.onap.cps.ncmp.dmi.api.kafka.MessagingBaseSpec +import org.onap.cps.ncmp.events.avc1_0_0.AvcEvent +import org.spockframework.spring.SpringBean +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.annotation.DirtiesContext +import org.testcontainers.spock.Testcontainers + +import java.time.Duration + +@SpringBootTest(classes = [DmiDataAvcEventProducer]) +@Testcontainers +@DirtiesContext +class AvcEventExecutorIntegrationSpec extends MessagingBaseSpec { + + @SpringBean + DmiDataAvcEventProducer dmiDataAvcEventProducer = new DmiDataAvcEventProducer(cloudEventKafkaTemplate) + + def dmiService = new DmiDataAvcEventSimulationController(dmiDataAvcEventProducer) + + def objectMapper = new ObjectMapper() + + def 'Publish Avc Event'() { + given: 'a simulated event' + dmiService.simulateEvents(1) + and: 'a consumer subscribed to dmi-cm-events topic' + cloudEventKafkaConsumer.subscribe(['dmi-cm-events']) + when: 'the next event record is consumed' + def record = cloudEventKafkaConsumer.poll(Duration.ofMillis(1500)).iterator().next() + then: 'record has correct topic' + assert record.topic == 'dmi-cm-events' + and: 'the record value can be mapped to an avcEvent' + def dmiDataAvcEvent = record.value() + def convertedAvcEvent = CloudEventUtils.mapData(dmiDataAvcEvent, PojoCloudEventDataMapper.from(objectMapper, AvcEvent.class)).getValue() + assert convertedAvcEvent != null + } +} \ No newline at end of file diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiInEventConsumerSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiInEventConsumerSpec.groovy new file mode 100644 index 00000000..f1f476f6 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiInEventConsumerSpec.groovy @@ -0,0 +1,149 @@ +/* + * ============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.dmi.notifications.cmsubscription + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import com.fasterxml.jackson.databind.ObjectMapper +import io.cloudevents.CloudEvent +import io.cloudevents.core.builder.CloudEventBuilder +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.onap.cps.ncmp.dmi.TestUtils +import org.onap.cps.ncmp.dmi.api.kafka.MessagingBaseSpec +import org.onap.cps.ncmp.dmi.notifications.cmsubscription.model.CmNotificationSubscriptionStatus +import org.onap.cps.ncmp.dmi.notifications.mapper.CloudEventMapper +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.CmNotificationSubscriptionDmiOutEvent +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.Data +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent +import org.slf4j.LoggerFactory +import org.spockframework.spring.SpringBean +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.annotation.DirtiesContext +import org.testcontainers.spock.Testcontainers + +import java.sql.Timestamp +import java.time.Duration +import java.time.OffsetDateTime +import java.time.ZoneId + + +@Testcontainers +@DirtiesContext +class CmNotificationSubscriptionDmiInEventConsumerSpec extends MessagingBaseSpec { + def objectMapper = new ObjectMapper() + def testTopic = 'dmi-ncmp-cm-avc-subscription' + def testDmiName = 'test-ncmp-dmi' + + @SpringBean + CmNotificationSubscriptionDmiInEventConsumer objectUnderTest = new CmNotificationSubscriptionDmiInEventConsumer(cloudEventKafkaTemplate) + + def logger = Spy(ListAppender) + + void setup() { + ((Logger) LoggerFactory.getLogger(CloudEventMapper.class)).addAppender(logger) + logger.start() + } + + void cleanup() { + ((Logger) LoggerFactory.getLogger(CloudEventMapper.class)).detachAndStopAllAppenders() + } + + def 'Sends subscription cloud event response successfully.'() { + given: 'an subscription event response' + objectUnderTest.dmiName = testDmiName + objectUnderTest.cmNotificationSubscriptionDmiOutTopic = testTopic + def correlationId = 'test-subscriptionId#test-ncmp-dmi' + def cmSubscriptionDmiOutEventData = new Data(statusCode: subscriptionStatusCode, statusMessage: subscriptionStatusMessage) + def subscriptionEventResponse = + new CmNotificationSubscriptionDmiOutEvent().withData(cmSubscriptionDmiOutEventData) + and: 'consumer has a subscription' + kafkaConsumer.subscribe([testTopic] as List) + when: 'an event is published' + def eventKey = UUID.randomUUID().toString() + objectUnderTest.createAndSendCmNotificationSubscriptionDmiOutEvent(eventKey, "subscriptionCreatedStatus", correlationId, subscriptionAcceptanceType) + and: 'topic is polled' + def records = kafkaConsumer.poll(Duration.ofMillis(1500)) + then: 'poll returns one record and close kafkaConsumer' + assert records.size() == 1 + def record = records.iterator().next() + kafkaConsumer.close() + and: 'the record value matches the expected event value' + def expectedValue = objectMapper.writeValueAsString(subscriptionEventResponse) + assert expectedValue == record.value + assert eventKey == record.key + where: 'given #scenario' + scenario | subscriptionAcceptanceType | subscriptionStatusCode | subscriptionStatusMessage + 'Subscription is Accepted' | CmNotificationSubscriptionStatus.ACCEPTED | '1' | 'ACCEPTED' + 'Subscription is Rejected' | CmNotificationSubscriptionStatus.REJECTED | '104' | 'REJECTED' + } + + def 'Consume valid message.'() { + given: 'an event' + objectUnderTest.dmiName = testDmiName + def eventKey = UUID.randomUUID().toString() + def timestamp = new Timestamp(1679521929511) + def jsonData = TestUtils.getResourceFileContent('cmNotificationSubscriptionCreationEvent.json') + def subscriptionEvent = objectMapper.readValue(jsonData, CmNotificationSubscriptionDmiInEvent.class) + objectUnderTest.cmNotificationSubscriptionDmiOutTopic = testTopic + def cloudEvent = CloudEventBuilder.v1().withId(UUID.randomUUID().toString()).withSource(URI.create('test-ncmp-dmi')) + .withType(subscriptionType) + .withDataSchema(URI.create("urn:cps:" + CmNotificationSubscriptionDmiInEvent.class.getName() + ":1.0.0")) + .withExtension("correlationid", eventKey) + .withTime(OffsetDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC"))) + .withData(objectMapper.writeValueAsBytes(subscriptionEvent)).build() + def testEventSent = new ConsumerRecord('topic-name', 0, 0, eventKey, cloudEvent) + when: 'the valid event is consumed' + objectUnderTest.consumeCmNotificationSubscriptionDmiInEvent(testEventSent) + then: 'no exception is thrown' + noExceptionThrown() + where: 'given #scenario' + scenario | subscriptionType + 'Subscription Create Event' | "subscriptionCreated" + 'Subscription Delete Event' | "subscriptionDeleted" + } + + def 'Consume invalid message.'() { + given: 'an invalid event body' + objectUnderTest.dmiName = testDmiName + def eventKey = UUID.randomUUID().toString() + def timestamp = new Timestamp(1679521929511) + def invalidJsonBody = "/////" + objectUnderTest.cmNotificationSubscriptionDmiOutTopic = testTopic + def cloudEvent = CloudEventBuilder.v1().withId(UUID.randomUUID().toString()).withSource(URI.create('test-ncmp-dmi')) + .withType("subscriptionCreated") + .withDataSchema(URI.create("urn:cps:org.onap.ncmp.dmi.cm.subscription:1.0.0")) + .withTime(OffsetDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC"))) + .withExtension("correlationid", eventKey).withData(objectMapper.writeValueAsBytes(invalidJsonBody)).build() + def testEventSent = new ConsumerRecord('topic-name', 0, 0, eventKey, cloudEvent) + when: 'the invalid event is consumed' + objectUnderTest.consumeCmNotificationSubscriptionDmiInEvent(testEventSent) + then: 'exception is thrown and event is logged' + def loggingEvent = getLoggingEvent() + assert loggingEvent.level == Level.ERROR + assert loggingEvent.formattedMessage.contains('Unable to map cloud event to target event class type') + } + + def getLoggingEvent() { + return logger.list[0] + } +} \ No newline at end of file diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiOutEventToCloudEventMapperSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiOutEventToCloudEventMapperSpec.groovy new file mode 100644 index 00000000..8ca629f1 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/cmsubscription/CmNotificationSubscriptionDmiOutEventToCloudEventMapperSpec.groovy @@ -0,0 +1,69 @@ +/* + * ============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.dmi.notifications.cmsubscription + +import com.fasterxml.jackson.databind.ObjectMapper +import io.cloudevents.core.builder.CloudEventBuilder +import org.onap.cps.ncmp.dmi.exception.CloudEventConstructionException +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.CmNotificationSubscriptionDmiOutEvent +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.Data +import org.spockframework.spring.SpringBean +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import spock.lang.Specification + +@SpringBootTest(classes = [ObjectMapper]) +class CmNotificationSubscriptionDmiOutEventToCloudEventMapperSpec extends Specification { + + @Autowired + def objectMapper = new ObjectMapper() + + @SpringBean + CmNotificationSubscriptionDmiOutEventToCloudEventMapper objectUnderTest = new CmNotificationSubscriptionDmiOutEventToCloudEventMapper() + + def 'Convert a Cm Subscription DMI Out Event to CloudEvent successfully.'() { + given: 'a Cm Subscription DMI Out Event and an event key' + def dmiName = 'test-ncmp-dmi' + def correlationId = 'subscription1#test-ncmp-dmi' + def cmSubscriptionDmiOutEventData = new Data(statusCode: "1", statusMessage: "accepted") + def cmSubscriptionDmiOutEvent = + new CmNotificationSubscriptionDmiOutEvent().withData(cmSubscriptionDmiOutEventData) + when: 'a Cm Subscription DMI Out Event is converted' + def result = objectUnderTest.toCloudEvent(cmSubscriptionDmiOutEvent, "subscriptionCreatedStatus", dmiName, correlationId) + then: 'Cm Subscription DMI Out Event is converted as expected' + def expectedCloudEvent = CloudEventBuilder.v1().withId(UUID.randomUUID().toString()).withSource(URI.create('test-ncmp-dmi')) + .withType("subscriptionCreated") + .withDataSchema(URI.create("urn:cps:" + CmNotificationSubscriptionDmiOutEvent.class.getName() + ":1.0.0")) + .withExtension("correlationid", correlationId) + .withData(objectMapper.writeValueAsBytes(cmSubscriptionDmiOutEvent)).build() + assert expectedCloudEvent.data == result.data + } + + def 'Map the Cloud Event to data of the subscription event with null parameters causes an exception'() { + given: 'an empty subscription response event and event key' + def correlationId = 'subscription1#test-ncmp-dmi' + def cmSubscriptionDmiOutEvent = new CmNotificationSubscriptionDmiOutEvent() + when: 'the cm subscription dmi out Event map to data of cloud event' + objectUnderTest.toCloudEvent(cmSubscriptionDmiOutEvent, "subscriptionCreatedStatus", null , correlationId) + then: 'a run time exception is thrown' + thrown(CloudEventConstructionException) + } +} diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/mapper/CloudEventMapperSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/mapper/CloudEventMapperSpec.groovy new file mode 100644 index 00000000..0b404776 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/notifications/mapper/CloudEventMapperSpec.groovy @@ -0,0 +1,53 @@ +/* + * ============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.dmi.notifications.mapper + +import com.fasterxml.jackson.databind.ObjectMapper +import io.cloudevents.core.builder.CloudEventBuilder +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import spock.lang.Specification + +@SpringBootTest(classes = [ObjectMapper]) +class CloudEventMapperSpec extends Specification { + + @Autowired + ObjectMapper objectMapper + + def 'Cloud event to Target event type when it is #scenario'() { + expect: 'Events mapped correctly' + assert mappedCloudEvent == (CloudEventMapper.toTargetEvent(testCloudEvent(), targetClass) != null) + where: 'below are the scenarios' + scenario | targetClass || mappedCloudEvent + 'valid concrete type' | CmNotificationSubscriptionNcmpInEvent.class || true + 'invalid concrete type' | ArrayList.class || false + } + + def testCloudEvent() { + return CloudEventBuilder.v1().withData(objectMapper.writeValueAsBytes(new CmNotificationSubscriptionNcmpInEvent())) + .withId("cmhandle1") + .withSource(URI.create('test-source')) + .withDataSchema(URI.create('test')) + .withType('org.onap.cm.events.cm-subscription') + .build() + } +} diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/ControllerSecuritySpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/ControllerSecuritySpec.groovy new file mode 100644 index 00000000..3f5d4a80 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/ControllerSecuritySpec.groovy @@ -0,0 +1,76 @@ +/* + * ============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.dmi.rest.controller + +import org.onap.cps.ncmp.dmi.config.WebSecurityConfig +import org.springframework.context.annotation.Import + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.http.HttpStatus +import org.springframework.test.web.servlet.MockMvc +import spock.lang.Specification + +@WebMvcTest(controllers = TestController.class) +@Import(WebSecurityConfig) +class ControllerSecuritySpec extends Specification { + + @Autowired + MockMvc mvc + + def testEndpoint = '/test' + + def 'Get request with valid authentication'() { + when: 'request is sent with authentication' + def response = mvc.perform( + get(testEndpoint).header("Authorization", 'Basic Y3BzdXNlcjpjcHNyMGNrcyE=') + ).andReturn().response + then: 'HTTP OK status code is returned' + assert response.status == HttpStatus.OK.value() + } + + def 'Get request without authentication'() { + when: 'request is sent without authentication' + def response = mvc.perform(get(testEndpoint)).andReturn().response + then: 'HTTP Unauthorized status code is returned' + assert response.status == HttpStatus.UNAUTHORIZED.value() + } + + def 'Get request with invalid authentication'() { + when: 'request is sent with invalid authentication' + def response = mvc.perform( + get(testEndpoint).header("Authorization", 'Basic invalid auth') + ).andReturn().response + then: 'HTTP Unauthorized status code is returned' + assert response.status == HttpStatus.UNAUTHORIZED.value() + } + + def 'Security Config #scenario permit URIs'() { + expect: 'can create a web security configuration' + new WebSecurityConfig(permitUris,'user','password') + where: 'the following string of permit URIs is provided' + scenario | permitUris + 'with' | 'a,b' + 'without' | '' + } +} diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/DmiRestControllerSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/DmiRestControllerSpec.groovy new file mode 100644 index 00000000..a519de7b --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/DmiRestControllerSpec.groovy @@ -0,0 +1,406 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2023 Nordix Foundation + * Modifications Copyright (C) 2021-2022 Bell Canada + * ================================================================================ + * 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.dmi.rest.controller + + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import org.onap.cps.ncmp.dmi.TestUtils +import org.onap.cps.ncmp.dmi.config.WebSecurityConfig +import org.onap.cps.ncmp.dmi.exception.DmiException +import org.onap.cps.ncmp.dmi.exception.ModuleResourceNotFoundException +import org.onap.cps.ncmp.dmi.exception.ModulesNotFoundException +import org.onap.cps.ncmp.dmi.model.ModuleSet +import org.onap.cps.ncmp.dmi.model.ModuleSetSchemasInner +import org.onap.cps.ncmp.dmi.model.YangResource +import org.onap.cps.ncmp.dmi.model.YangResources +import org.onap.cps.ncmp.dmi.notifications.async.AsyncTaskExecutor +import org.onap.cps.ncmp.dmi.notifications.async.DmiAsyncRequestResponseEventProducer +import org.onap.cps.ncmp.dmi.service.DmiService +import org.onap.cps.ncmp.dmi.service.model.ModuleReference +import org.slf4j.LoggerFactory +import org.spockframework.spring.SpringBean +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.Import +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.MockMvc +import spock.lang.Specification + +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.CREATE +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.DELETE +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.PATCH +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.READ +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.UPDATE +import static org.springframework.http.HttpStatus.BAD_REQUEST +import static org.springframework.http.HttpStatus.CREATED +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import static org.springframework.http.HttpStatus.NO_CONTENT +import static org.springframework.http.HttpStatus.OK +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post + +@Import(WebSecurityConfig) +@WebMvcTest(DmiRestController.class) +@WithMockUser +class DmiRestControllerSpec extends Specification { + + @Autowired + private MockMvc mvc + + @SpringBean + DmiService mockDmiService = Mock() + + @SpringBean + DmiAsyncRequestResponseEventProducer cpsAsyncRequestResponseEventProducer = new DmiAsyncRequestResponseEventProducer(Mock(KafkaTemplate)) + + @SpringBean + AsyncTaskExecutor asyncTaskExecutor = new AsyncTaskExecutor(cpsAsyncRequestResponseEventProducer) + + def logger = Spy(ListAppender) + + void setup() { + ((Logger) LoggerFactory.getLogger(DmiRestController.class)).addAppender(logger) + logger.start() + } + + void cleanup() { + ((Logger) LoggerFactory.getLogger(DmiRestController.class)).detachAndStopAllAppenders() + } + + @Value('${rest.api.dmi-base-path}/v1') + def basePathV1 + + def 'Get all modules.'() { + given: 'URL for getting all modules and some request data' + def getModuleUrl = "$basePathV1/ch/node1/modules" + def someValidJson = '{}' + and: 'DMI service returns some module' + def moduleSetSchema = new ModuleSetSchemasInner(namespace:'some-namespace', + moduleName:'some-moduleName', + revision:'some-revision') + def moduleSetSchemasList = [moduleSetSchema] as List + def moduleSet = new ModuleSet() + moduleSet.schemas(moduleSetSchemasList) + mockDmiService.getModulesForCmHandle('node1') >> moduleSet + when: 'the request is posted' + def response = mvc.perform(post(getModuleUrl) + .contentType(MediaType.APPLICATION_JSON).content(someValidJson)) + .andReturn().response + then: 'status is OK' + response.status == OK.value() + and: 'the response content matches the result from the DMI service' + response.getContentAsString() == '{"schemas":[{"moduleName":"some-moduleName","revision":"some-revision","namespace":"some-namespace"}]}' + } + + def 'Get all modules with exception handling of #scenario.'() { + given: 'URL for getting all modules and some request data' + def getModuleUrl = "$basePathV1/ch/node1/modules" + def someValidJson = '{}' + and: 'a #exception is thrown during the process' + mockDmiService.getModulesForCmHandle('node1') >> { throw exception } + when: 'the request is posted' + def response = mvc.perform( post(getModuleUrl) + .contentType(MediaType.APPLICATION_JSON).content(someValidJson)) + .andReturn().response + then: 'response status is #expectedResponse' + response.status == expectedResponse.value() + where: 'the scenario is #scenario' + scenario | exception || expectedResponse + 'dmi service exception' | new DmiException('','') || HttpStatus.INTERNAL_SERVER_ERROR + 'no modules found' | new ModulesNotFoundException('','') || HttpStatus.NOT_FOUND + 'any other runtime exception' | new RuntimeException() || HttpStatus.INTERNAL_SERVER_ERROR + 'runtime exception with cause' | new RuntimeException('', new RuntimeException()) || HttpStatus.INTERNAL_SERVER_ERROR + } + + def 'Register given list.'() { + given: 'register cm handle url and cmHandles' + def registerCmhandlesPost = "${basePathV1}/inventory/cmHandles" + def cmHandleJson = '{"cmHandles":["node1", "node2"]}' + when: 'the request is posted' + def response = mvc.perform( + post(registerCmhandlesPost) + .contentType(MediaType.APPLICATION_JSON) + .content(cmHandleJson) + ).andReturn().response + then: 'register cm handles in dmi service is invoked with correct parameters' + 1 * mockDmiService.registerCmHandles(_ as List) + and: 'response status is created' + response.status == CREATED.value() + } + + def 'register cm handles called with empty content.'() { + given: 'register cm handle url and empty json' + def registerCmhandlesPost = "${basePathV1}/inventory/cmHandles" + def emptyJson = '{"cmHandles":[]}' + when: 'the request is posted' + def response = mvc.perform( + post(registerCmhandlesPost).contentType(MediaType.APPLICATION_JSON) + .content(emptyJson) + ).andReturn().response + then: 'response status is "bad request"' + response.status == BAD_REQUEST.value() + and: 'dmi service is not called' + 0 * mockDmiService.registerCmHandles(_) + } + + def 'Retrieve module resources.'() { + given: 'URL to get module resources' + def getModulesEndpoint = "$basePathV1/ch/some-cm-handle/moduleResources" + and: 'request data to get some modules' + String jsonData = TestUtils.getResourceFileContent('moduleResources.json') + and: 'the DMI service returns the yang resources' + ModuleReference moduleReference1 = new ModuleReference(name: 'ietf-yang-library', revision: '2016-06-21') + ModuleReference moduleReference2 = new ModuleReference(name: 'nc-notifications', revision: '2008-07-14') + def moduleReferences = [moduleReference1, moduleReference2] + def yangResources = new YangResources() + def yangResource = new YangResource(yangSource: '"some-data"', moduleName: 'NAME', revision: 'REVISION') + yangResources.add(yangResource) + mockDmiService.getModuleResources('some-cm-handle', moduleReferences) >> yangResources + when: 'the request is posted' + def response = mvc.perform(post(getModulesEndpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonData)).andReturn().response + then: 'a OK status is returned' + response.status == OK.value() + and: 'the response content matches the result from the DMI service' + response.getContentAsString() == '[{"yangSource":"\\"some-data\\"","moduleName":"NAME","revision":"REVISION"}]' + } + + def 'Retrieve module resources with exception handling.'() { + given: 'URL to get module resources' + def getModulesEndpoint = "$basePathV1/ch/some-cm-handle/moduleResources" + and: 'request data to get some modules' + String jsonData = TestUtils.getResourceFileContent('moduleResources.json') + and: 'the system throws a not-found exception (during the processing)' + mockDmiService.getModuleResources('some-cm-handle', _) >> { throw Mock(ModuleResourceNotFoundException.class) } + when: 'the request is posted' + def response = mvc.perform(post(getModulesEndpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonData)).andReturn().response + then: 'a not found status is returned' + response.status == HttpStatus.NOT_FOUND.value() + } + + def 'Retrieve module resources and ensure module set tag is logged.'() { + given: 'URL to get module resources' + def getModulesEndpoint = "$basePathV1/ch/some-cm-handle/moduleResources" + and: 'request data to get some modules' + String jsonData = TestUtils.getResourceFileContent('moduleResources.json') + and: 'the DMI service returns the yang resources' + def moduleReferences = [] + def yangResources = new YangResources() + def yangResource = new YangResource() + yangResources.add(yangResource) + mockDmiService.getModuleResources('some-cm-handle', moduleReferences) >> yangResources + when: 'the request is posted' + mvc.perform(post(getModulesEndpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonData)) + then: 'the module set tag is logged' + def loggingMessage = getLoggingMessage(0) + assert loggingMessage.contains('module-set-tag1') + } + + def 'Get resource data for pass-through operational.'() { + given: 'Get resource data url and some request data' + def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-operational" + + "?resourceIdentifier=parent/child&options=(fields=myfields,depth=5)" + def someValidJson = '{}' + when: 'the request is posted' + def response = mvc.perform( + post(getResourceDataForCmHandleUrl).contentType(MediaType.APPLICATION_JSON).content(someValidJson) + ).andReturn().response + then: 'response status is ok' + response.status == OK.value() + and: 'dmi service method to get resource data is invoked once' + 1 * mockDmiService.getResourceData('some-cmHandle', + 'parent/child', + '(fields=myfields,depth=5)', + 'content=all') + } + + def 'Get resource data for pass-through operational with write request (invalid).'() { + given: 'Get resource data url' + def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-operational" + + "?resourceIdentifier=parent/child&options=(fields=myfields,depth=5)" + and: 'an invalid write request data for "create" operation' + def jsonData = '{"operation":"create"}' + when: 'the request is posted' + def response = mvc.perform( + post(getResourceDataForCmHandleUrl).contentType(MediaType.APPLICATION_JSON).content(jsonData) + ).andReturn().response + then: 'response status is bad request' + response.status == BAD_REQUEST.value() + and: 'dmi service is not invoked' + 0 * mockDmiService.getResourceData(*_) + } + + def 'Get resource data for invalid datastore'() { + given: 'Get resource data url' + def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/dummy-datastore" + + "?resourceIdentifier=parent/child&options=(fields=myfields,depth=5)" + and: 'an invalid write request data for "create" operation' + def jsonData = '{"operation":"create"}' + when: 'the request is posted' + def response = mvc.perform( + post(getResourceDataForCmHandleUrl).contentType(MediaType.APPLICATION_JSON).content(jsonData) + ).andReturn().response + then: 'response status is internal server error' + response.status == INTERNAL_SERVER_ERROR.value() + and: 'response contains expected error message' + response.contentAsString.contains('dummy-datastore is an invalid datastore name') + } + + def 'data with #scenario operation using passthrough running.'() { + given: 'write data for passthrough running url' + def writeDataForPassthroughRunning = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-running" + + "?resourceIdentifier=some-resourceIdentifier" + and: 'request data for #scenario' + def jsonData = TestUtils.getResourceFileContent(requestBodyFile) + and: 'dmi service is called' + mockDmiService.writeData(operationEnum, 'some-cmHandle', + 'some-resourceIdentifier', dataType, + 'normal request body' ) >> '{some-json}' + when: 'the request is posted' + def response = mvc.perform( + post(writeDataForPassthroughRunning).contentType(MediaType.APPLICATION_JSON) + .content(jsonData) + ).andReturn().response + then: 'response status is #expectedResponseStatus' + response.status == expectedResponseStatus + and: 'the response content matches the result from the DMI service' + response.getContentAsString() == expectedJsonResponse + where: 'given request body and data' + scenario | requestBodyFile | operationEnum | dataType || expectedResponseStatus | expectedJsonResponse + 'Create' | 'createDataWithNormalChar.json' | CREATE | 'application/json' || CREATED.value() | '{some-json}' + 'Update' | 'updateData.json' | UPDATE | 'application/json' || OK.value() | '{some-json}' + 'Delete' | 'deleteData.json' | DELETE | 'application/json' || NO_CONTENT.value() | '{some-json}' + 'Read' | 'readData.json' | READ | 'application/json' || OK.value() | '' + 'Patch' | 'patchData.json' | PATCH | 'application/yang.patch+json' || OK.value() | '{some-json}' + } + + def 'Create data using passthrough for special characters.'(){ + given: 'create data for cmHandle url' + def writeDataForCmHandlePassthroughRunning = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-running" + + "?resourceIdentifier=some-resourceIdentifier" + and: 'request data with special characters' + def jsonData = TestUtils.getResourceFileContent('createDataWithSpecialChar.json') + and: 'dmi service returns data' + mockDmiService.writeData(CREATE, 'some-cmHandle', 'some-resourceIdentifier', 'application/json', + 'data with quote \" and new line \n') >> '{some-json}' + when: 'the request is posted' + def response = mvc.perform( + post(writeDataForCmHandlePassthroughRunning).contentType(MediaType.APPLICATION_JSON).content(jsonData) + ).andReturn().response + then: 'response status is CREATED' + response.status == CREATED.value() + and: 'the response content matches the result from the DMI service' + response.getContentAsString() == '{some-json}' + } + + def 'PassThrough Returns OK when topic is used for async'(){ + given: 'Passthrough read URL and request data with a topic (parameter)' + def readPassThroughUrl ="${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:" + + resourceIdentifier + + '?resourceIdentifier=some-resourceIdentifier&topic=test-topic' + def jsonData = TestUtils.getResourceFileContent('readData.json') + when: 'the request is posted' + def response = mvc.perform( + post(readPassThroughUrl).contentType(MediaType.APPLICATION_JSON).content(jsonData) + ).andReturn().response + then: 'response status is OK' + assert response.status == HttpStatus.NO_CONTENT.value() + where: 'the following values are used' + resourceIdentifier << ['passthrough-operational', 'passthrough-running'] + } + + def 'PassThrough logs module set tag'(){ + given: 'Passthrough read URL and request data with a module set tag (parameter)' + def readPassThroughUrl ="${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:" + + 'passthrough-running?resourceIdentifier=some-resourceIdentifier' + def jsonData = TestUtils.getResourceFileContent('readData.json') + when: 'the request is posted' + mvc.perform( + post(readPassThroughUrl).contentType(MediaType.APPLICATION_JSON).content(jsonData)) + then: 'response status is OK' + def loggingMessage = getLoggingMessage(0) + assert loggingMessage.contains('module-set-tag-example') + } + + def 'Get resource data for pass-through running with #scenario value in resource identifier param.'() { + given: 'Get resource data url' + def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-running" + + "?resourceIdentifier="+resourceIdentifier+"&options=(fields=myfields,depth=5)" + and: 'some valid json data' + def json = '{"cmHandleProperties" : { "prop1" : "value1", "prop2" : "value2"}}' + when: 'the request is posted' + def response = mvc.perform( + post(getResourceDataForCmHandleUrl).contentType(MediaType.APPLICATION_JSON).content(json) + ).andReturn().response + then: 'response status is ok' + response.status == OK.value() + and: 'dmi service method to get resource data is invoked once with correct parameters' + 1 * mockDmiService.getResourceData('some-cmHandle', + resourceIdentifier, + '(fields=myfields,depth=5)', + 'content=config') + where: 'tokens are used in the resource identifier parameter' + scenario | resourceIdentifier + '/' | 'id/with/slashes' + '?' | 'idWith?' + ',' | 'idWith,' + '=' | 'idWith=' + '[]' | 'idWith[]' + '? needs to be encoded as %3F' | 'idWith%3F' + + } + + def 'Execute a data operation for a list of operations.'() { + given: 'an endpoint for a data operation request with list of cmhandles in request body' + def resourceDataUrl = "$basePathV1/data?topic=client-topic-name&requestId=some-requestId" + and: 'list of operation details are received into request body' + def dataOperationRequestBody = '[{"operation": "read", "operationId": "14", "datastore": "ncmp-datastore:passthrough-operational", "options": "some options", "resourceIdentifier": "some resourceIdentifier",' + + '"cmHandles": [ {"id": "cmHandle123", "moduleSetTag": "module-set-tag1", "cmHandleProperties": { "myProp`": "some value", "otherProp": "other value"}}]}]' + when: 'the dmi resource data for dataOperation api is called.' + def response = mvc.perform( + post(resourceDataUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestBody) + ).andReturn().response + then: 'the resource data operation endpoint returns the not implemented response' + assert response.status == 501 + and: 'the job details are correctly received (logged)' + assert getLoggingMessage(1).contains('some-requestId') + assert getLoggingMessage(2).contains('client-topic-name') + assert getLoggingMessage(4).contains('some resourceIdentifier') + assert getLoggingMessage(5).contains('module-set-tag1') + and: 'the operation Id is correctly received (logged)' + assert getLoggingMessage(6).contains('14') + } + + def getLoggingMessage(int index) { + return logger.list[index].formattedMessage + } +} \ No newline at end of file diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/DmiServiceImplSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/DmiServiceImplSpec.groovy new file mode 100644 index 00000000..8531d35f --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/DmiServiceImplSpec.groovy @@ -0,0 +1,273 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2021-2022 Bell Canada + * ================================================================================ + * 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.dmi.service + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ObjectWriter +import org.onap.cps.ncmp.dmi.config.DmiPluginConfig +import org.onap.cps.ncmp.dmi.exception.CmHandleRegistrationException +import org.onap.cps.ncmp.dmi.exception.DmiException +import org.onap.cps.ncmp.dmi.exception.ModuleResourceNotFoundException +import org.onap.cps.ncmp.dmi.exception.ModulesNotFoundException +import org.onap.cps.ncmp.dmi.exception.HttpClientRequestException +import org.onap.cps.ncmp.dmi.service.model.ModuleReference +import org.onap.cps.ncmp.dmi.model.YangResource +import org.onap.cps.ncmp.dmi.model.YangResources +import org.onap.cps.ncmp.dmi.service.client.NcmpRestClient +import org.onap.cps.ncmp.dmi.service.model.ModuleSchema +import org.onap.cps.ncmp.dmi.service.operation.SdncOperations +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import spock.lang.Specification + +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.CREATE +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.UPDATE + +class DmiServiceImplSpec extends Specification { + + + def mockNcmpRestClient = Mock(NcmpRestClient) + def mockDmiPluginProperties = Mock(DmiPluginConfig.DmiPluginProperties) + def spyObjectMapper = Spy(ObjectMapper) + def mockObjectMapper = Mock(ObjectMapper) + def mockSdncOperations = Mock(SdncOperations) + def objectUnderTest = new DmiServiceImpl(mockDmiPluginProperties, mockNcmpRestClient, mockSdncOperations, spyObjectMapper) + + def 'Register cm handles with ncmp.'() { + given: 'some cm-handle ids' + def givenCmHandlesList = ['node1', 'node2'] + and: 'json payload' + def expectedJson = '{"dmiPlugin":"test-dmi-service","createdCmHandles":[{"cmHandle":"node1"},{"cmHandle":"node2"}]}' + and: 'process returns "test-dmi-service" for service name' + mockDmiPluginProperties.getDmiServiceUrl() >> 'test-dmi-service' + when: 'the cm handles are registered' + objectUnderTest.registerCmHandles(givenCmHandlesList) + then: 'register cm handle with ncmp is called with the expected json and return "created" status' + 1 * mockNcmpRestClient.registerCmHandlesWithNcmp(expectedJson) >> new ResponseEntity<>(HttpStatus.CREATED) + } + + def 'Register cm handles with ncmp called with exception #scenario.'() { + given: 'some cm-handle ids' + def cmHandlesList = ['node1', 'node2'] + and: 'process returns "test-dmi-service" for service name' + mockDmiPluginProperties.getDmiServiceUrl() >> 'test-dmi-service' + and: 'returns #responseEntity' + mockNcmpRestClient.registerCmHandlesWithNcmp(_ as String) >> responseEntity + when: 'the cm handles are registered' + objectUnderTest.registerCmHandles(cmHandlesList) + then: 'a registration exception is thrown' + thrown(CmHandleRegistrationException.class) + where: 'given #scenario' + scenario | responseEntity + 'ncmp rest client returns bad request' | new ResponseEntity<>(HttpStatus.BAD_REQUEST) + 'ncmp rest client returns internal server error' | new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR) + } + + def 'Register cm handles with ncmp with wrong data.'() { + given: 'some cm-handle ids' + def cmHandlesList = ['node1', 'node2'] + and: ' "JsonProcessingException" occurs during parsing' + objectUnderTest.objectMapper = mockObjectMapper + mockObjectMapper.writeValueAsString(_) >> { throw new JsonProcessingException('some error.') } + when: 'the cmHandles are registered' + objectUnderTest.registerCmHandles(cmHandlesList) + then: 'a dmi exception is thrown' + thrown(DmiException.class) + } + + def ' Get modules for a cm-handle.'() { + given: 'a cm handle' + def cmHandle = 'node1' + and: 'process returns one module schema for the cmhandle' + def moduleSchema = new ModuleSchema( + identifier: "example-identifier", + namespace: "example:namespace", + version: "example-version") + mockSdncOperations.getModuleSchemasFromNode(cmHandle) >> List.of(moduleSchema) + when: 'modules for cmHandle is requested' + def result = objectUnderTest.getModulesForCmHandle(cmHandle) + then: 'one module is returned' + result.schemas.size() == 1 + and: 'module has expected values' + with(result.schemas[0]) { + it.getRevision() == moduleSchema.getVersion() + it.getModuleName() == moduleSchema.getIdentifier() + it.getNamespace() == moduleSchema.getNamespace(); + } + } + + def 'no modules found for the cmhandle.'() { + given: 'cm handle id' + def cmHandle = 'node1' + and: 'process returns no modules' + mockSdncOperations.getModuleSchemasFromNode(cmHandle) >> Collections.emptyList(); + when: 'modules for cm-handle is requested' + objectUnderTest.getModulesForCmHandle(cmHandle) + then: 'module not found exception is thrown' + thrown(ModulesNotFoundException) + } + + def 'Get multiple module resources.'() { + given: 'a cmHandle' + def cmHandle = 'some-cmHandle' + and: 'multiple module references' + def moduleReference1 = new ModuleReference(name: 'name-1', revision: 'revision-1') + def moduleReference2 = new ModuleReference(name: 'name-2', revision: 'revision-2') + def moduleList = [moduleReference1, moduleReference2] + when: 'module resources is requested' + def result = objectUnderTest.getModuleResources(cmHandle, moduleList) + then: 'SDNC operation service is called same number of module references given' + 2 * mockSdncOperations.getModuleResource(cmHandle, _) >>> [new ResponseEntity('{"ietf-netconf-monitoring:output": {"data": "some-data1"}}', HttpStatus.OK), + new ResponseEntity('{"ietf-netconf-monitoring:output": {"data": "some-data2"}}', HttpStatus.OK)] + and: 'the result contains the expected properties' + def yangResources = new YangResources() + def yangResource1 = new YangResource(yangSource: 'some-data1', moduleName: 'name-1', revision: 'revision-1') + def yangResource2 = new YangResource(yangSource: 'some-data2', moduleName: 'name-2', revision: 'revision-2') + yangResources.add(yangResource1) + yangResources.add(yangResource2) + assert result == yangResources + } + + def 'Get a module resource with module resource not found exception for #scenario.'() { + given: 'a cmHandle and module reference list' + def cmHandle = 'some-cmHandle' + def moduleReference = new ModuleReference(name: 'NAME', revision: 'REVISION') + def moduleList = [moduleReference] + when: 'module resources is requested' + objectUnderTest.getModuleResources(cmHandle, moduleList) + then: 'SDNC operation service is called once with a response body that contains no data' + 1 * mockSdncOperations.getModuleResource(cmHandle, _) >> new ResponseEntity(responseBody, HttpStatus.OK) + and: 'an exception is thrown' + thrown(ModuleResourceNotFoundException) + where: 'the following values are returned' + scenario | responseBody + 'a response body containing no data object' | '{"ietf-netconf-monitoring:output": {"null": "some-data"}}' + 'a response body containing no ietf-netconf-monitoring:output object' | '{"null": {"data": "some-data"}}' + } + + def 'Get module resources when sdnc returns #scenario response.'() { + given: 'sdnc returns a #scenario response' + mockSdncOperations.getModuleResource(_ as String, _ as String) >> new ResponseEntity('some-response-body', httpStatus) + when: 'module resources is requested' + objectUnderTest.getModuleResources('some-cmHandle', [new ModuleReference()] as LinkedList) + then: '#expectedException is thrown' + thrown(expectedException) + where: 'the following values are returned' + scenario | httpStatus || expectedException + 'not found' | HttpStatus.NOT_FOUND || ModuleResourceNotFoundException + 'internal server error' | HttpStatus.INTERNAL_SERVER_ERROR || DmiException + } + + def 'Get module resources with JSON processing exception.'() { + given: 'a json processing exception during process' + def mockObjectWriter = Mock(ObjectWriter) + spyObjectMapper.writer() >> mockObjectWriter + mockObjectWriter.withRootName(_) >> mockObjectWriter + def jsonProcessingException = new JsonProcessingException('') + mockObjectWriter.writeValueAsString(_) >> { throw jsonProcessingException } + when: 'module resources is requested' + objectUnderTest.getModuleResources('some-cmHandle', [new ModuleReference()] as LinkedList) + then: 'an exception is thrown' + def thrownException = thrown(DmiException.class) + and: 'the exception has the expected message and details' + thrownException.message == 'Unable to process JSON.' + thrownException.details == 'JSON exception occurred when creating the module request.' + and: 'the cause is the original json processing exception' + thrownException.cause == jsonProcessingException + } + + def 'Get resource data for passthrough operational.'() { + given: 'sdnc operation returns OK response' + mockSdncOperations.getResouceDataForOperationalAndRunning( + 'someCmHandle', + 'someResourceId', + '(fields=x/y/z,depth=10,test=abc)', + 'content=all') >> new ResponseEntity<>('response json', HttpStatus.OK) + when: 'resource data is requested' + def response = objectUnderTest.getResourceData( + 'someCmHandle', + 'someResourceId', + '(fields=x/y/z,depth=10,test=abc)', + 'content=all') + then: 'response matches the response returned from the SDNC service' + response == 'response json' + } + + def 'Get resource data with not found exception.'() { + given: 'sdnc operation returns "NOT_FOUND" response' + mockSdncOperations.getResouceDataForOperationalAndRunning(*_) >> new ResponseEntity<>(HttpStatus.NOT_FOUND) + when: 'resource data is requested' + objectUnderTest.getResourceData('someCmHandle', 'someResourceId', + '(fields=x/y/z,depth=10,test=abc)', 'content=config') + then: 'http client request exception' + thrown(HttpClientRequestException.class) + } + + def 'Get resource data for passthrough running.'() { + given: 'sdnc operation returns OK response' + mockSdncOperations.getResouceDataForOperationalAndRunning(*_) >> new ResponseEntity<>('response json', HttpStatus.OK) + when: 'resource data is requested' + def response = objectUnderTest.getResourceData( + 'someCmHandle', + 'someResourceId', + '(fields=x/y/z,depth=10,test=abc)', + 'content=config') + then: 'response have expected json' + response == 'response json' + } + + def 'Write resource data for passthrough running with a #scenario from sdnc.'() { + given: 'sdnc returns a response with #scenario' + mockSdncOperations.writeData(operationEnum, _, _, _, _) >> new ResponseEntity('response json', httpResponse) + when: 'resource data is written to sdnc' + def response = objectUnderTest.writeData(operationEnum, 'some-cmHandle', + 'some-resourceIdentifier', 'some-dataType', '{some-data}') + then: 'the response matches the expected data' + response == 'response json' + where: 'the following values are used' + scenario | httpResponse | operationEnum + '200 OK with an update operation' | HttpStatus.OK | UPDATE + '201 CREATED with a create operation' | HttpStatus.CREATED | CREATE + } + + def 'Write resource data with special characters.'() { + given: 'sdnc returns a created response' + mockSdncOperations.writeData(CREATE, 'some-cmHandle', + 'some-resourceIdentifier', 'some-dataType', 'data with quote " and \n new line') >> new ResponseEntity('response json', HttpStatus.CREATED) + when: 'resource data is written to sdnc' + def response = objectUnderTest.writeData(CREATE, 'some-cmHandle', + 'some-resourceIdentifier', 'some-dataType', 'data with quote " and \n new line') + then: 'the response matches the expected data' + response == 'response json' + } + + def 'Write resource data for passthrough running with a 500 response from sdnc.'() { + given: 'sdnc returns internal server error response' + mockSdncOperations.writeData(CREATE, _, _, _, _) >> new ResponseEntity('response json', HttpStatus.INTERNAL_SERVER_ERROR) + when: 'resource data is written to sdnc' + objectUnderTest.writeData(CREATE, 'some-cmHandle', + 'some-resourceIdentifier', 'some-dataType', _ as String) + then: 'a dmi exception is thrown' + thrown(DmiException.class) + } +} \ No newline at end of file diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/YangResourceExtractorSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/YangResourceExtractorSpec.groovy new file mode 100644 index 00000000..656cfcb5 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/YangResourceExtractorSpec.groovy @@ -0,0 +1,98 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.service + +import com.google.gson.JsonSyntaxException +import org.onap.cps.ncmp.dmi.exception.ModuleResourceNotFoundException +import org.onap.cps.ncmp.dmi.service.model.ModuleReference +import org.springframework.http.ResponseEntity +import spock.lang.Specification + +class YangResourceExtractorSpec extends Specification { + + static def BACK_SLASH = '\\'; + static def NEW_LINE = '\n'; + static def QUOTE = '"'; + static def TAB = '\t'; + + static def YANG_ESCAPED_NEW_LINE = '\\n' + static def YANG_ESCAPED_BACK_SLASH = '\\\\' + static def YANG_ESCAPED_QUOTE = '\\"' + static def YANG_ESCAPED_TAB = '\\t' + + static def SDNC_OUTPUT_JSON_NAME = '"ietf-netconf-monitoring:output"' + + def moduleReference = new ModuleReference(name: 'test', revision: 'rev') + def responseEntity = Mock(ResponseEntity) + + def 'Extract yang resource with escaped characters in the source.'() { + given: 'a response entity with a data field of value #jsonValue' + responseEntity.getBody() >> '{' + SDNC_OUTPUT_JSON_NAME + ': { "data": "' + jsonValue + '" }}' + when: 'the yang resource is extracted' + def result = YangResourceExtractor.toYangResource(moduleReference, responseEntity) + then: 'the yang source string is as expected' + result.getYangSource() == expectedString + where: 'the following data is used' + jsonValue || expectedString + 'line1' + YANG_ESCAPED_NEW_LINE + 'line2' || 'line1' + NEW_LINE + 'line2' + 'a' + YANG_ESCAPED_BACK_SLASH+'b' || 'a'+BACK_SLASH +'b' + 'a' + YANG_ESCAPED_QUOTE + 'b' || 'a'+QUOTE+'b' + 'a' + YANG_ESCAPED_TAB + 'b' || 'a'+TAB+'b' + } + + def 'Extract yang resource with escaped characters in the source inside escaped double quotes.'() { + given: 'a response entity with a data field of value #jsonValue wrapped in escaped double quotes' + responseEntity.getBody() >> '{' + SDNC_OUTPUT_JSON_NAME + ': { "data": "' + YANG_ESCAPED_QUOTE + jsonValue + YANG_ESCAPED_QUOTE + '" }}' + when: 'the yang resource is extracted' + def result = YangResourceExtractor.toYangResource(moduleReference, responseEntity) + then: 'the yang source string is as expected' + result.getYangSource() == expectedString + where: 'the following data is used' + jsonValue || expectedString + 'line1' + YANG_ESCAPED_NEW_LINE + 'line2' || '"line1' + NEW_LINE + 'line2"' + 'a' + YANG_ESCAPED_BACK_SLASH+'b' || '"a'+BACK_SLASH +'b"' + 'a' + YANG_ESCAPED_QUOTE + 'b' || '"a'+QUOTE+'b"' + 'a' + YANG_ESCAPED_TAB + 'b' || '"a'+TAB+'b"' + } + + def 'Attempt to extract yang resource with un-escaped double quotes in the source.'() { + given: 'a response entity with a data field with unescaped double quotes' + responseEntity.getBody() >> '{' + SDNC_OUTPUT_JSON_NAME + ': { "data": "' + QUOTE + 'some data' + QUOTE + '" }}' + when: 'Json is converted to String' + YangResourceExtractor.toYangResource(moduleReference, responseEntity) + then: 'the output of the method is equal to the output from the test template' + thrown(JsonSyntaxException) + } + + def 'Attempt to extract yang resource without #without.'() { + given: 'a response entity with a body without #without' + responseEntity.getBody() >> jsonBody + when: 'Json is converted to String' + YangResourceExtractor.toYangResource(moduleReference, responseEntity) + then: 'the output of the method is equal to the output from the test template' + thrown(ModuleResourceNotFoundException) + where: + without | jsonBody + 'data' | '{' + SDNC_OUTPUT_JSON_NAME + ': { "something": "else" }}' + SDNC_OUTPUT_JSON_NAME | '{"something:else": { "data": "data" }}' + } + +} diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/client/NcmpRestClientSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/client/NcmpRestClientSpec.groovy new file mode 100644 index 00000000..4d7e27e2 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/client/NcmpRestClientSpec.groovy @@ -0,0 +1,57 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2022 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.dmi.service.client + +import org.onap.cps.ncmp.dmi.config.DmiConfiguration +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity +import org.springframework.web.client.RestTemplate +import spock.lang.Specification + +class NcmpRestClientSpec extends Specification { + def objectUnderTest = new NcmpRestClient(mockCpsProperties, mockRestTemplate) + def mockCpsProperties = Mock(DmiConfiguration.CpsProperties) + def mockRestTemplate = Mock(RestTemplate) + + def setup() { + objectUnderTest.cpsProperties = mockCpsProperties + objectUnderTest.restTemplate = mockRestTemplate + } + + def 'Register a cm handle.'() { + given: 'some request data' + def someRequestData = 'some request data' + and: 'configuration data' + mockCpsProperties.baseUrl >> 'http://some-uri' + mockCpsProperties.dmiRegistrationUrl >> 'some-url' + mockCpsProperties.authUsername >> 'some-username' + mockCpsProperties.authPassword >> 'some-password' + and: 'the rest template returns a valid response entity' + def mockResponseEntity = Mock(ResponseEntity) + when: 'registering a cm handle' + def result = objectUnderTest.registerCmHandlesWithNcmp(someRequestData) + then: 'the rest template is called with the correct uri and original request data in the body' + 1 * mockRestTemplate.exchange({ it.toString() == 'http://some-uri/some-url' }, + HttpMethod.POST, { it.body.contains(someRequestData) }, String.class) >> mockResponseEntity + and: 'the output of the method is equal to the output from the rest template service' + result == mockResponseEntity + } +} \ No newline at end of file diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClientSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClientSpec.groovy new file mode 100644 index 00000000..f334f780 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClientSpec.groovy @@ -0,0 +1,102 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2022 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.dmi.service.client + +import org.onap.cps.ncmp.dmi.config.DmiConfiguration +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity +import org.springframework.web.client.RestTemplate +import spock.lang.Specification + +import static org.springframework.http.HttpMethod.GET +import static org.springframework.http.HttpMethod.POST +import static org.springframework.http.HttpMethod.DELETE +import static org.springframework.http.HttpMethod.PUT + +class SdncRestconfClientSpec extends Specification { + + def mockSdncProperties = Mock(DmiConfiguration.SdncProperties) + def mockRestTemplate = Mock(RestTemplate) + def objectUnderTest = new SdncRestconfClient(mockSdncProperties, mockRestTemplate) + + def 'SDNC GET operation.'() { + given: 'a get resource url' + def getResourceUrl = '/getResourceUrl' + and: 'test configuration data' + setupTestConfigurationData() + and: 'the process returns a valid response entity' + def mockResponseEntity = Mock(ResponseEntity) + mockRestTemplate.exchange({ it.toString() == 'http://some-uri/getResourceUrl' }, + HttpMethod.GET, _ as HttpEntity, String.class) >> mockResponseEntity + when: 'the resource is fetched' + def result = objectUnderTest.getOperation(getResourceUrl) + then: 'the output of the method is equal to the output from the rest template service' + result == mockResponseEntity + } + + def 'SDNC #scenario operation called.'() { + given: 'some request data' + def someRequestData = 'some request data' + and: 'a url for get module resources' + def getModuleResourceUrl = '/getModuleResourceUrl' + and: 'test configuration data' + setupTestConfigurationData() + and: 'the process returns a valid response entity' + def mockResponseEntity = Mock(ResponseEntity) + when: 'the resource is fetched' + def result = objectUnderTest.httpOperationWithJsonData(expectedHttpMethod, getModuleResourceUrl, someRequestData, new HttpHeaders()) + then: 'the rest template is called with the correct uri and json in the body' + 1 * mockRestTemplate.exchange({ it.toString() == 'http://some-uri/getModuleResourceUrl' }, + expectedHttpMethod, { it.body.contains(someRequestData) }, String.class) >> mockResponseEntity + and: 'the output of the method is the same as the output from the rest template service' + result == mockResponseEntity + where: 'the following values are used' + scenario || expectedHttpMethod + 'POST' || POST + 'PUT' || PUT + 'GET' || GET + 'DELETE' || DELETE + } + + def 'SDNC GET operation with headers.'() { + given: 'a get url' + def getResourceUrl = '/getResourceUrl' + and: 'test configuration data' + setupTestConfigurationData() + and: 'the process returns a valid response entity' + def mockResponseEntity = Mock(ResponseEntity) + mockRestTemplate.exchange({ it.toString() == 'http://some-uri/getResourceUrl' }, + HttpMethod.GET, _ as HttpEntity, String.class) >> mockResponseEntity + when: 'the resource is fetched with headers' + def result = objectUnderTest.getOperation(getResourceUrl, new HttpHeaders()) + then: 'the output of the method is equal to the output from the rest template service' + result == mockResponseEntity + } + + def setupTestConfigurationData() { + mockSdncProperties.baseUrl >> 'http://some-uri' + mockSdncProperties.authUsername >> 'some-username' + mockSdncProperties.authPassword >> 'some-password' + mockSdncProperties.topologyId >> 'some-topology-id' + } +} \ No newline at end of file diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/SdncOperationsSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/SdncOperationsSpec.groovy new file mode 100644 index 00000000..9dcb72e6 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/SdncOperationsSpec.groovy @@ -0,0 +1,176 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2021-2022 Bell Canada + * ================================================================================ + * 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.dmi.service.operation + +import org.onap.cps.ncmp.dmi.TestUtils +import org.onap.cps.ncmp.dmi.config.DmiConfiguration +import org.onap.cps.ncmp.dmi.exception.SdncException +import org.onap.cps.ncmp.dmi.service.client.SdncRestconfClient +import org.spockframework.spring.SpringBean +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.CREATE +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.DELETE +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.PATCH +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.UPDATE +import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.READ + +@SpringBootTest +@ContextConfiguration(classes = [DmiConfiguration.SdncProperties, SdncOperations]) +class SdncOperationsSpec extends Specification { + + @SpringBean + SdncRestconfClient mockSdncRestClient = Mock() + + @Autowired + SdncOperations objectUnderTest + + def 'get modules from node.'() { + given: 'a node id and url' + def nodeId = 'node1' + def expectedUrl = '/rests/data/network-topology:network-topology/topology=test-topology/node=node1/yang-ext:mount/ietf-netconf-monitoring:netconf-state/schemas' + and: 'sdnc returns one module during process' + mockSdncRestClient.getOperation(expectedUrl) >> + ResponseEntity.ok(TestUtils.getResourceFileContent('ModuleSchema.json')) + when: 'module schemas from node are fetched' + def moduleSchemas = objectUnderTest.getModuleSchemasFromNode(nodeId) + then: 'one module is found' + moduleSchemas.size() == 1 + and: 'module schema has expected values' + with(moduleSchemas[0]) { + it.getIdentifier() == "example-identifier" + it.getNamespace() == "example:namespace" + it.getVersion() == "example-version" + it.getFormat() == "example-format" + it.getLocation() == ["example-location"] + } + } + + def 'No modules from Node: SDNC Response - #scenario .'() { + given: 'node id and url' + def nodeId = 'node1' + def expectedUrl = '/rests/data/network-topology:network-topology/topology=test-topology/node=node1/yang-ext:mount/ietf-netconf-monitoring:netconf-state/schemas' + and: 'sdnc operation returns #scenario' + mockSdncRestClient.getOperation(expectedUrl) >> ResponseEntity.ok(responseBody) + when: 'the module schemas are requested' + def moduleSchemas = objectUnderTest.getModuleSchemasFromNode(nodeId) + then: 'no module schemas are returned' + moduleSchemas.size() == 0 + where: + scenario | responseBody + 'null response body' | null + 'empty response body ' | '' + 'no module schema' | '{ "ietf-netconf-monitoring:schemas" : { "schema" : [] } } ' + } + + def 'Error handling - modules from node: #scenario'() { + given: 'node id and url' + def nodeId = 'node1' + def expectedUrl = '/rests/data/network-topology:network-topology/topology=test-topology/node=node1/yang-ext:mount/ietf-netconf-monitoring:netconf-state/schemas' + and: '#scenario is returned during process' + mockSdncRestClient.getOperation(expectedUrl) >> new ResponseEntity<>(sdncResponseBody, sdncHttpStatus) + when: 'module schemas from node are fetched' + objectUnderTest.getModuleSchemasFromNode(nodeId) + then: 'SDNCException is thrown' + def thrownException = thrown(SdncException) + thrownException.getDetails().contains(expectedExceptionDetails) + where: + scenario | sdncHttpStatus | sdncResponseBody || expectedExceptionDetails + 'failed response from SDNC' | HttpStatus.BAD_REQUEST | '{ "errorMessage" : "incorrect input"}' || '{ "errorMessage" : "incorrect input"}' + 'invalid json response' | HttpStatus.OK | 'invalid-json' || 'SDNC response is not in the expected format' + 'response in unexpected json schema' | HttpStatus.OK | '{ "format" : "incorrect" }' || 'SDNC response is not in the expected format' + } + + def 'Get module resources from SDNC.'() { + given: 'node id and url' + def nodeId = 'some-node' + def expectedUrl = '/rests/operations/network-topology:network-topology/topology=test-topology/node=some-node/yang-ext:mount/ietf-netconf-monitoring:get-schema' + when: 'module resource is fetched with the expected parameters' + objectUnderTest.getModuleResource(nodeId, 'some-json-data') + then: 'the SDNC Rest client is invoked with the correct parameters' + 1 * mockSdncRestClient.httpOperationWithJsonData(HttpMethod.POST, expectedUrl, 'some-json-data', _ as HttpHeaders) + } + + def 'Get resource data from node to SDNC.'() { + given: 'expected url' + def expectedUrl = '/rests/data/network-topology:network-topology/topology=test-topology/node=node1/yang-ext:mount/testResourceId?a=1&b=2&content=testContent' + when: 'resource data is fetched for given node ID' + objectUnderTest.getResouceDataForOperationalAndRunning('node1', 'testResourceId', + '(a=1,b=2)', 'content=testContent') + then: 'the SDNC get operation is executed with the correct URL' + 1 * mockSdncRestClient.getOperation(expectedUrl) + } + + def 'Write resource data with #scenario operation to SDNC.'() { + given: 'expected url' + def expectedUrl = '/rests/data/network-topology:network-topology/topology=test-topology/node=node1/yang-ext:mount/testResourceId' + when: 'write resource data for passthrough running is called' + objectUnderTest.writeData(operationEnum, 'node1', 'testResourceId', 'application/json', 'requestData') + then: 'the #expectedHttpMethod operation is executed with the correct parameters' + 1 * mockSdncRestClient.httpOperationWithJsonData(expectedHttpMethod, expectedUrl, 'requestData', _ as HttpHeaders) + where: 'the following values are used' + scenario | operationEnum || expectedHttpMethod + 'Create' | CREATE || HttpMethod.POST + 'Update' | UPDATE || HttpMethod.PUT + 'Read' | READ || HttpMethod.GET + 'Delete' | DELETE || HttpMethod.DELETE + 'Patch' | PATCH || HttpMethod.PATCH + } + + def 'build query param list for SDNC where options #scenario'() { + when: 'query param list is built' + def result = objectUnderTest.buildQueryParamMap(optionsParamInQuery, 'd=4') + .toSingleValueMap().toString() + then: 'result matches the expected result' + result == expectedResult + where: 'following parameters are used' + scenario | optionsParamInQuery || expectedResult + 'is single key-value pair' | '(a=x)' || '[a:x, d:4]' + 'is multiple key-value pairs' | '(a=x,b=y,c=z)' || '[a:x, b:y, c:z, d:4]' + 'has / as special char' | '(a=x,b=y,c=t/z)' || '[a:x, b:y, c:t/z, d:4]' + 'has " as special char' | '(a=x,b=y,c="z")' || '[a:x, b:y, c:"z", d:4]' + 'has [] as special char' | '(a=x,b=y,c=[z])' || '[a:x, b:y, c:[z], d:4]' + 'has = in value' | '(a=(x=y),b=x=y)' || '[a:(x=y), b:x=y, d:4]' + 'is empty' | '' || '[:]' + 'is null' | null || '[:]' + } + + def 'options parameters contains a comma #scenario'() { + when: 'query param list is built with #scenario' + def result = objectUnderTest.buildQueryParamMap(optionsParamInQuery, 'd=4').toSingleValueMap() + then: 'expect 2 elements from options where we are ignoring empty query param value, +1 from content query param (2+1) = 3 elements' + def expectedNoOfElements = 3 + and: 'results contains equal elements as expected' + result.size() == expectedNoOfElements + where: 'following parameters are used' + scenario | optionsParamInQuery + '"," in value' | '(a=(x,y),b=y)' + '"," in string value' | '(a="x,y",b=y)' + } +} diff --git a/dmi-service/src/test/java/org/onap/cps/ncmp/dmi/TestUtils.java b/dmi-service/src/test/java/org/onap/cps/ncmp/dmi/TestUtils.java new file mode 100644 index 00000000..c10d91a5 --- /dev/null +++ b/dmi-service/src/test/java/org/onap/cps/ncmp/dmi/TestUtils.java @@ -0,0 +1,54 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +/** + * Common convenience methods for testing. + */ +public class TestUtils { + + /** + * Convert a file in the test resource folder to file. + * + * @param filename to name of the file in test/resources + * @return the file + * @throws IOException when there is an IO issue + */ + public static File readFile(final String filename) { + return new File(ClassLoader.getSystemClassLoader().getResource(filename).getFile()); + } + + /** + * Convert a file in the test resource folder to a string. + * + * @param filename to name of the file in test/resources + * @return the content of the file as a String + * @throws IOException when there is an IO issue + */ + public static String getResourceFileContent(final String filename) throws IOException { + final File file = readFile(filename); + return new String(Files.readAllBytes(file.toPath())); + } +} diff --git a/dmi-service/src/test/java/org/onap/cps/ncmp/dmi/rest/controller/TestController.java b/dmi-service/src/test/java/org/onap/cps/ncmp/dmi/rest/controller/TestController.java new file mode 100644 index 00000000..5240e239 --- /dev/null +++ b/dmi-service/src/test/java/org/onap/cps/ncmp/dmi/rest/controller/TestController.java @@ -0,0 +1,35 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 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.dmi.rest.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @GetMapping("/test") + ResponseEntity test() { + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/dmi-service/src/test/resources/ModuleSchema.json b/dmi-service/src/test/resources/ModuleSchema.json new file mode 100644 index 00000000..50c67154 --- /dev/null +++ b/dmi-service/src/test/resources/ModuleSchema.json @@ -0,0 +1,15 @@ +{ + "ietf-netconf-monitoring:schemas": { + "schema": [ + { + "identifier": "example-identifier", + "version": "example-version", + "format": "example-format", + "namespace": "example:namespace", + "location": [ + "example-location" + ] + } + ] + } +} \ No newline at end of file diff --git a/dmi-service/src/test/resources/application.yml b/dmi-service/src/test/resources/application.yml new file mode 100644 index 00000000..ddc2b45f --- /dev/null +++ b/dmi-service/src/test/resources/application.yml @@ -0,0 +1,79 @@ +# ============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========================================================= + +rest: + api: + dmi-base-path: /dmi + +security: + permit-uri: /actuator/**,/swagger-ui/**,/swagger-resources/**,/v3/api-docs + auth: + username: cpsuser + password: cpsr0cks! + +sdnc: + baseUrl: http://test + topologyId: test-topology + auth: + username: test + password: test + +cps-core: + baseUrl: some url for cps + dmiRegistrationUrl: some registration url + auth: + username: some cps core user + password: some cps core password + +dmi: + service: + url: some url for the dmi service + avc: + subscription-topic: ncmp-dmi-cm-avc-subscription + subscription-response-topic: dmi-ncmp-cm-avc-subscription + +spring: + application: + name: ncmp-dmi-plugin + mvc: + pathmatch: + matching-strategy: ANT_PATH_MATCHER + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVER} + security: + protocol: PLAINTEXT + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: io.cloudevents.kafka.CloudEventSerializer + client-id: ncmp-dmi-plugin + consumer: + group-id: ${NCMP_CONSUMER_GROUP_ID:ncmp-group} + key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer + value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer + properties: + spring.deserializer.key.delegate.class: org.apache.kafka.common.serialization.StringDeserializer + spring.deserializer.value.delegate.class: io.cloudevents.kafka.CloudEventDeserializer + spring.json.use.type.headers: false + +app: + ncmp: + async: + topic: ${NCMP_ASYNC_M2M_TOPIC:ncmp-async-m2m} + +logging: + format: json diff --git a/dmi-service/src/test/resources/cmNotificationSubscriptionCreationEvent.json b/dmi-service/src/test/resources/cmNotificationSubscriptionCreationEvent.json new file mode 100644 index 00000000..3b780976 --- /dev/null +++ b/dmi-service/src/test/resources/cmNotificationSubscriptionCreationEvent.json @@ -0,0 +1,43 @@ +{ + "data": { + "cmhandles": [ + { + "cmhandleId": "CMHandle1", + "private-properties": { + "prop1": "prop-value" + } + }, + { + "cmhandleId": "CMHandle2", + "private-properties": { + "prop-x": "prop-valuex", + "prop-p": "prop-valuep" + } + }, + { + "cmhandleId": "CMHandle3", + "private-properties": { + "prop-y": "prop-valuey" + } + } + ], + "predicates": [ + { + "targetFilter": [ + "CMHandle1", + "CMHandle2", + "CMHandle3" + ], + "scopeFilter": { + "datastore": "ncmp-datastore:passthrough-running", + "xpath-filter": [ + "//_3gpp-nr-nrm-gnbdufunction:GNBDUFunction/_3gpp-nr-nrm-nrcelldu:NRCellDU/", + "//_3gpp-nr-nrm-gnbcuupfunction:GNBCUUPFunction//", + "//_3gpp-nr-nrm-gnbcucpfunction:GNBCUCPFunction/_3gpp-nr-nrm-nrcelldu:NRCellCU//", + "//_3gpp-nr-nrm-nrsectorcarrier:NRSectorCarrier//" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/dmi-service/src/test/resources/createDataWithNormalChar.json b/dmi-service/src/test/resources/createDataWithNormalChar.json new file mode 100644 index 00000000..31cdf1c5 --- /dev/null +++ b/dmi-service/src/test/resources/createDataWithNormalChar.json @@ -0,0 +1,8 @@ +{ + "operation": "create", + "dataType": "application/json", + "data": "normal request body", + "cmHandleProperties": { + "some-property": "some-property-value" + } +} \ No newline at end of file diff --git a/dmi-service/src/test/resources/createDataWithSpecialChar.json b/dmi-service/src/test/resources/createDataWithSpecialChar.json new file mode 100644 index 00000000..1e7622ee --- /dev/null +++ b/dmi-service/src/test/resources/createDataWithSpecialChar.json @@ -0,0 +1,8 @@ +{ + "operation": "create", + "dataType": "application/json", + "data": "data with quote \" and new line \n", + "cmHandleProperties": { + "some-property": "some-property-value" + } +} \ No newline at end of file diff --git a/dmi-service/src/test/resources/deleteData.json b/dmi-service/src/test/resources/deleteData.json new file mode 100644 index 00000000..2233fa01 --- /dev/null +++ b/dmi-service/src/test/resources/deleteData.json @@ -0,0 +1,8 @@ +{ + "operation": "delete", + "dataType": "application/json", + "data": "normal request body", + "cmHandleProperties": { + "some-property": "some-property-value" + } +} \ No newline at end of file diff --git a/dmi-service/src/test/resources/moduleResources.json b/dmi-service/src/test/resources/moduleResources.json new file mode 100644 index 00000000..23adfcba --- /dev/null +++ b/dmi-service/src/test/resources/moduleResources.json @@ -0,0 +1,18 @@ +{ + "data": { + "modules": [ + { + "name": "ietf-yang-library", + "revision": "2016-06-21" + }, + { + "name": "nc-notifications", + "revision": "2008-07-14" + } + ] + }, + "cmHandleProperties": { + "subsystemId": "system-001" + }, + "moduleSetTag": "module-set-tag1" +} diff --git a/dmi-service/src/test/resources/patchData.json b/dmi-service/src/test/resources/patchData.json new file mode 100644 index 00000000..e5466eaf --- /dev/null +++ b/dmi-service/src/test/resources/patchData.json @@ -0,0 +1,8 @@ +{ + "operation": "patch", + "dataType": "application/yang.patch+json", + "data": "normal request body", + "cmHandleProperties": { + "some-property": "some-property-value" + } +} \ No newline at end of file diff --git a/dmi-service/src/test/resources/readData.json b/dmi-service/src/test/resources/readData.json new file mode 100644 index 00000000..53f6d2ed --- /dev/null +++ b/dmi-service/src/test/resources/readData.json @@ -0,0 +1,9 @@ +{ + "operation": "read", + "dataType": "application/json", + "data": "normal request body", + "cmHandleProperties": { + "some-property": "some-property-value" + }, + "moduleSetTag": "module-set-tag-example" +} \ No newline at end of file diff --git a/dmi-service/src/test/resources/updateData.json b/dmi-service/src/test/resources/updateData.json new file mode 100644 index 00000000..7cbf4f0c --- /dev/null +++ b/dmi-service/src/test/resources/updateData.json @@ -0,0 +1,8 @@ +{ + "operation": "update", + "dataType": "application/json", + "data": "normal request body", + "cmHandleProperties": { + "some-property": "some-property-value" + } +} \ No newline at end of file -- cgit 1.2.3-korg