5218db43da3dd0fb8039079fdd75aa2332772dba
[cps/ncmp-dmi-plugin.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2023-2024 Nordix Foundation
4  *  ================================================================================
5  *  Licensed under the Apache License, Version 2.0 (the "License");
6  *  you may not use this file except in compliance with the License.
7  *  You may obtain a copy of the License at
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  *
17  *  SPDX-License-Identifier: Apache-2.0
18  *  ============LICENSE_END=========================================================
19  */
20
21 package org.onap.cps.ncmp.dmi.rest.stub.controller;
22
23
24 import com.fasterxml.jackson.core.JsonProcessingException;
25 import com.fasterxml.jackson.databind.JsonNode;
26 import com.fasterxml.jackson.databind.ObjectMapper;
27 import io.cloudevents.CloudEvent;
28 import io.cloudevents.core.builder.CloudEventBuilder;
29 import java.net.URI;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.UUID;
35 import java.util.concurrent.atomic.AtomicInteger;
36 import java.util.stream.Collectors;
37 import lombok.RequiredArgsConstructor;
38 import lombok.extern.slf4j.Slf4j;
39 import org.json.simple.parser.JSONParser;
40 import org.json.simple.parser.ParseException;
41 import org.onap.cps.ncmp.dmi.datajobs.model.SubjobWriteRequest;
42 import org.onap.cps.ncmp.dmi.datajobs.model.SubjobWriteResponse;
43 import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.DataOperationRequest;
44 import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.DmiDataOperationRequest;
45 import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.DmiOperationCmHandle;
46 import org.onap.cps.ncmp.dmi.rest.stub.utils.EventDateTimeFormatter;
47 import org.onap.cps.ncmp.dmi.rest.stub.utils.ResourceFileReaderUtil;
48 import org.onap.cps.ncmp.events.async1_0_0.Data;
49 import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent;
50 import org.onap.cps.ncmp.events.async1_0_0.Response;
51 import org.springframework.beans.factory.annotation.Value;
52 import org.springframework.context.ApplicationContext;
53 import org.springframework.core.io.Resource;
54 import org.springframework.core.io.ResourceLoader;
55 import org.springframework.http.HttpStatus;
56 import org.springframework.http.ResponseEntity;
57 import org.springframework.kafka.core.KafkaTemplate;
58 import org.springframework.web.bind.annotation.DeleteMapping;
59 import org.springframework.web.bind.annotation.GetMapping;
60 import org.springframework.web.bind.annotation.PathVariable;
61 import org.springframework.web.bind.annotation.PostMapping;
62 import org.springframework.web.bind.annotation.PutMapping;
63 import org.springframework.web.bind.annotation.RequestBody;
64 import org.springframework.web.bind.annotation.RequestHeader;
65 import org.springframework.web.bind.annotation.RequestMapping;
66 import org.springframework.web.bind.annotation.RequestParam;
67 import org.springframework.web.bind.annotation.RestController;
68
69 @RestController
70 @RequestMapping("${rest.api.dmi-stub-base-path}")
71 @Slf4j
72 @RequiredArgsConstructor
73 public class DmiRestStubController {
74
75     private static final String DEFAULT_TAG = "tagD";
76     private static final String DEFAULT_PASSTHROUGH_OPERATION = "read";
77     private static final String dataOperationEventType = "org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent";
78     private static final Map<String, String> moduleSetTagPerCmHandleId = new HashMap<>();
79     private final KafkaTemplate<String, CloudEvent> cloudEventKafkaTemplate;
80     private final ObjectMapper objectMapper;
81     private final ApplicationContext applicationContext;
82     @Value("${app.ncmp.async-m2m.topic}")
83     private String ncmpAsyncM2mTopic;
84     @Value("${delay.module-references-delay-ms}")
85     private long moduleReferencesDelayMs;
86     @Value("${delay.module-resources-delay-ms}")
87     private long moduleResourcesDelayMs;
88     @Value("${delay.read-data-for-cm-handle-delay-ms}")
89     private long readDataForCmHandleDelayMs;
90     @Value("${delay.write-data-for-cm-handle-delay-ms}")
91     private long writeDataForCmHandleDelayMs;
92     private final AtomicInteger subJobWriteRequestCounter = new AtomicInteger();
93
94     /**
95      * This code defines a REST API endpoint for adding new the module set tag mapping. The endpoint receives the
96      * cmHandleId and moduleSetTag as request body and add into moduleSetTagPerCmHandleId map with the provided
97      * values.
98      *
99      * @param requestBody map of cmHandleId and moduleSetTag
100      * @return a ResponseEntity object containing the updated moduleSetTagPerCmHandleId map as the response body
101      */
102     @PostMapping("/v1/tagMapping")
103     public ResponseEntity<Map<String, String>> addTagForMapping(@RequestBody final Map<String, String> requestBody) {
104         moduleSetTagPerCmHandleId.putAll(requestBody);
105         return new ResponseEntity<>(requestBody, HttpStatus.CREATED);
106     }
107
108     /**
109      * This code defines a GET endpoint of  module set tag mapping.
110      *
111      * @return The map represents the module set tag mapping.
112      */
113     @GetMapping("/v1/tagMapping")
114     public ResponseEntity<Map<String, String>> getTagMapping() {
115         return ResponseEntity.ok(moduleSetTagPerCmHandleId);
116     }
117
118     /**
119      * This code defines a GET endpoint of  module set tag by cm handle ID.
120      *
121      * @return The map represents the module set tag mapping filtered by cm handle ID.
122      */
123     @GetMapping("/v1/tagMapping/ch/{cmHandleId}")
124     public ResponseEntity<String> getTagMappingByCmHandleId(@PathVariable final String cmHandleId) {
125         return ResponseEntity.ok(moduleSetTagPerCmHandleId.get(cmHandleId));
126     }
127
128     /**
129      * This code defines a REST API endpoint for updating the module set tag mapping. The endpoint receives the
130      * cmHandleId and moduleSetTag as request body and updates the moduleSetTagPerCmHandleId map with the provided
131      * values.
132      *
133      * @param requestBody map of cmHandleId and moduleSetTag
134      * @return a ResponseEntity object containing the updated moduleSetTagPerCmHandleId map as the response body
135      */
136
137     @PutMapping("/v1/tagMapping")
138     public ResponseEntity<Map<String, String>> updateTagMapping(@RequestBody final Map<String, String> requestBody) {
139         moduleSetTagPerCmHandleId.putAll(requestBody);
140         return ResponseEntity.noContent().build();
141     }
142
143     /**
144      * It contains a method to delete an entry from the moduleSetTagPerCmHandleId map.
145      * The method takes a cmHandleId as a parameter and removes the corresponding entry from the map.
146      *
147      * @return a ResponseEntity containing the updated map.
148      */
149     @DeleteMapping("/v1/tagMapping/ch/{cmHandleId}")
150     public ResponseEntity<String> deleteTagMappingByCmHandleId(@PathVariable final String cmHandleId) {
151         moduleSetTagPerCmHandleId.remove(cmHandleId);
152         return ResponseEntity.ok(String.format("Mapping of %s is deleted successfully", cmHandleId));
153     }
154
155     /**
156      * Get all modules for given cm handle.
157      *
158      * @param cmHandleId              The identifier for a network function, network element, subnetwork,
159      *                                or any other cm object by managed Network CM Proxy
160      * @param moduleReferencesRequest module references request body
161      * @return ResponseEntity response entity having module response as json string.
162      */
163     @PostMapping("/v1/ch/{cmHandleId}/modules")
164     public ResponseEntity<String> getModuleReferences(@PathVariable("cmHandleId") final String cmHandleId,
165                                                       @RequestBody final Object moduleReferencesRequest) {
166         delay(moduleReferencesDelayMs);
167         try {
168             log.info("Incoming DMI request body: {}",
169                     objectMapper.writeValueAsString(moduleReferencesRequest));
170         } catch (final JsonProcessingException jsonProcessingException) {
171             log.info("Unable to parse dmi data operation request to json string");
172         }
173         final String moduleResponseContent = getModuleResourceResponse(cmHandleId,
174                 "ModuleResponse.json");
175         log.info("cm handle: {} requested for modules", cmHandleId);
176         return ResponseEntity.ok(moduleResponseContent);
177     }
178
179     /**
180      * Retrieves module resources for a given cmHandleId.
181      *
182      * @param cmHandleId                 The identifier for a network function, network element, subnetwork,
183      *                                   or any other cm object by managed Network CM Proxy
184      * @param moduleResourcesReadRequest module resources read request body
185      * @return ResponseEntity response entity having module resources response as json string.
186      */
187     @PostMapping("/v1/ch/{cmHandleId}/moduleResources")
188     public ResponseEntity<String> retrieveModuleResources(
189             @PathVariable("cmHandleId") final String cmHandleId,
190             @RequestBody final Object moduleResourcesReadRequest) {
191         delay(moduleResourcesDelayMs);
192         final String moduleResourcesResponseContent = getModuleResourceResponse(cmHandleId,
193                 "ModuleResourcesResponse.json");
194         log.info("cm handle: {} requested for modules resources", cmHandleId);
195         return ResponseEntity.ok(moduleResourcesResponseContent);
196     }
197
198     /**
199      * Create resource data from passthrough operational or running for a cm handle.
200      *
201      * @param cmHandleId              The identifier for a network function, network element, subnetwork,
202      *                                or any other cm object by managed Network CM Proxy
203      * @param datastoreName           datastore name
204      * @param resourceIdentifier      resource identifier
205      * @param options                 options
206      * @param topic                   client given topic name
207      * @return (@ code ResponseEntity) response entity
208      */
209     @PostMapping("/v1/ch/{cmHandleId}/data/ds/{datastoreName}")
210     public ResponseEntity<String> getResourceDataForCmHandle(
211             @PathVariable("cmHandleId") final String cmHandleId,
212             @PathVariable("datastoreName") final String datastoreName,
213             @RequestParam(value = "resourceIdentifier") final String resourceIdentifier,
214             @RequestParam(value = "options", required = false) final String options,
215             @RequestParam(value = "topic", required = false) final String topic,
216             @RequestHeader(value = "Authorization", required = false) final String authorization,
217             @RequestBody final String requestBody) {
218         log.info("DMI AUTH HEADER: {}", authorization);
219         final String passthroughOperationType = getPassthroughOperationType(requestBody);
220         if (passthroughOperationType.equals("read")) {
221             delay(readDataForCmHandleDelayMs);
222         } else {
223             delay(writeDataForCmHandleDelayMs);
224         }
225         log.info("Logging request body {}", requestBody);
226
227         final String sampleJson = ResourceFileReaderUtil.getResourceFileContent(applicationContext.getResource(
228                 ResourceLoader.CLASSPATH_URL_PREFIX + "data/ietf-network-topology-sample-rfc8345.json"));
229         return ResponseEntity.ok(sampleJson);
230     }
231
232     /**
233      * This method is not implemented for ONAP DMI plugin.
234      *
235      * @param topic                   client given topic name
236      * @param requestId               requestId generated by NCMP as an ack for client
237      * @param dmiDataOperationRequest list of operation details
238      * @return (@ code ResponseEntity) response entity
239      */
240     @PostMapping("/v1/data")
241     public ResponseEntity<Void> getResourceDataForCmHandleDataOperation(
242             @RequestParam(value = "topic") final String topic,
243             @RequestParam(value = "requestId") final String requestId,
244             @RequestBody final DmiDataOperationRequest dmiDataOperationRequest) {
245         delay(writeDataForCmHandleDelayMs);
246         try {
247             log.info("Request received from the NCMP to DMI Plugin: {}",
248                     objectMapper.writeValueAsString(dmiDataOperationRequest));
249         } catch (final JsonProcessingException jsonProcessingException) {
250             log.info("Unable to process dmi data operation request to json string");
251         }
252         dmiDataOperationRequest.getOperations().forEach(dmiDataOperation -> {
253             final DataOperationEvent dataOperationEvent = getDataOperationEvent(dmiDataOperation);
254             dmiDataOperation.getCmHandles().forEach(dmiOperationCmHandle -> {
255                 log.info("Module Set Tag received: {}", dmiOperationCmHandle.getModuleSetTag());
256                 dataOperationEvent.getData().getResponses().get(0).setIds(List.of(dmiOperationCmHandle.getId()));
257                 final CloudEvent cloudEvent = buildAndGetCloudEvent(topic, requestId, dataOperationEvent);
258                 cloudEventKafkaTemplate.send(ncmpAsyncM2mTopic, UUID.randomUUID().toString(), cloudEvent);
259             });
260         });
261         return new ResponseEntity<>(HttpStatus.ACCEPTED);
262     }
263
264     /**
265      * Consume sub-job write requests from NCMP.
266      *
267      * @param subJobWriteRequest            contains a collection of write requests and metadata.
268      * @param destination                   the destination of the results. ( e.g. S3 Bucket).
269      * @return (@ code ResponseEntity) response for the write request.
270      */
271     @PostMapping("/v1/cmwriteJob")
272     public ResponseEntity<SubjobWriteResponse> consumeWriteSubJobs(
273                                                         @RequestBody final SubjobWriteRequest subJobWriteRequest,
274                                                         @RequestParam("destination") final String destination) {
275         log.debug("Destination: {}", destination);
276         log.debug("Request body: {}", subJobWriteRequest);
277         return ResponseEntity.ok(new SubjobWriteResponse(String.valueOf(subJobWriteRequestCounter.incrementAndGet()),
278                 "some-dmi-service-name", "my-data-producer-id"));
279     }
280
281     /**
282      * Retrieves the status of a given data job identified by {@code requestId} and {@code dataProducerJobId}.
283      *
284      * @param dataProducerId    ID of the producer registered by DMI for the alternateIDs
285      *                          in the operations in this request.
286      * @param dataProducerJobId Identifier of the data producer job.
287      * @return A ResponseEntity with HTTP status 200 (OK) and the data job's status as a string.
288      */
289     @GetMapping("/v1/cmwriteJob/dataProducer/{dataProducerId}/dataProducerJob/{dataProducerJobId}/status")
290     public ResponseEntity<Map<String, String>> retrieveDataJobStatus(
291             @PathVariable("dataProducerId") final String dataProducerId,
292             @PathVariable("dataProducerJobId") final String dataProducerJobId) {
293         log.info("Received request to retrieve data job status. Request ID: {}, Data Producer Job ID: {}",
294                 dataProducerId, dataProducerJobId);
295         return ResponseEntity.ok(Map.of("status", "FINISHED"));
296     }
297
298     /**
299      * Retrieves the result of a given data job identified by {@code requestId} and {@code dataProducerJobId}.
300      *
301      * @param requestId             Identifier for the overall Datajob (required)
302      * @param dataProducerJobId     Identifier for the data producer job (required)
303      * @param dataProducerId        Identifier for the data producer as a query parameter (required)
304      * @param destination           The destination of the results, Kafka topic name or s3 bucket name (required)
305      * @return A ResponseEntity with HTTP status 200 (OK) and the data job's result as an Object.
306      */
307     @GetMapping("/v1/dataJob/{requestId}/dataProducerJob/{dataProducerJobId}/result")
308     public ResponseEntity<Object> retrieveDataJobResult(
309             @PathVariable("requestId") final String requestId,
310             @PathVariable("dataProducerJobId") final String dataProducerJobId,
311             @RequestParam(name = "dataProducerId") String dataProducerId,
312             @RequestParam(name = "destination") String destination) {
313         log.debug("Received request to retrieve data job result. Request ID: {}, Data Producer Job ID: {}, " +
314                         "Data Producer ID: {}, Destination: {}",
315                 requestId, dataProducerJobId, dataProducerId, destination);
316         return ResponseEntity.ok(Map.of("result", "some status"));
317     }
318
319     private CloudEvent buildAndGetCloudEvent(final String topic, final String requestId,
320                                              final DataOperationEvent dataOperationEvent) {
321         CloudEvent cloudEvent = null;
322         try {
323             cloudEvent = CloudEventBuilder.v1()
324                     .withId(UUID.randomUUID().toString())
325                     .withSource(URI.create("DMI"))
326                     .withType(dataOperationEventType)
327                     .withDataSchema(URI.create("urn:cps:" + dataOperationEventType + ":1.0.0"))
328                     .withTime(EventDateTimeFormatter.toIsoOffsetDateTime(
329                             EventDateTimeFormatter.getCurrentIsoFormattedDateTime()))
330                     .withData(objectMapper.writeValueAsBytes(dataOperationEvent))
331                     .withExtension("destination", topic)
332                     .withExtension("correlationid", requestId)
333                     .build();
334         } catch (final JsonProcessingException jsonProcessingException) {
335             log.error("Unable to parse event into bytes. cause : {}", jsonProcessingException.getMessage());
336         }
337         return cloudEvent;
338     }
339
340     private DataOperationEvent getDataOperationEvent(final DataOperationRequest dataOperationRequest) {
341         final Response response = new Response();
342
343         response.setOperationId(dataOperationRequest.getOperationId());
344         response.setStatusCode("0");
345         response.setStatusMessage("Successfully applied changes");
346         response.setIds(dataOperationRequest.getCmHandles().stream().map(DmiOperationCmHandle::getId)
347                 .collect(Collectors.toList()));
348         response.setResourceIdentifier(dataOperationRequest.getResourceIdentifier());
349         response.setOptions(dataOperationRequest.getOptions());
350         final String ietfNetworkTopologySample = ResourceFileReaderUtil.getResourceFileContent(
351                 applicationContext.getResource(ResourceLoader.CLASSPATH_URL_PREFIX
352                         + "data/ietf-network-topology-sample-rfc8345.json"));
353         final JSONParser jsonParser = new JSONParser();
354         try {
355             response.setResult(jsonParser.parse(ietfNetworkTopologySample));
356         } catch (final ParseException parseException) {
357             log.error("Unable to parse event result as json object. cause : {}", parseException.getMessage());
358         }
359         final List<Response> responseList = new ArrayList<>(1);
360         responseList.add(response);
361         final Data data = new Data();
362         data.setResponses(responseList);
363         final DataOperationEvent dataOperationEvent = new DataOperationEvent();
364         dataOperationEvent.setData(data);
365         return dataOperationEvent;
366     }
367
368     private String getModuleResourceResponse(final String cmHandleId, final String moduleResponseType) {
369         if (moduleSetTagPerCmHandleId.isEmpty()) {
370             log.info("Using default module responses of type ietfYang");
371             return ResourceFileReaderUtil.getResourceFileContent(applicationContext.getResource(
372                     ResourceLoader.CLASSPATH_URL_PREFIX
373                             + String.format("module/ietfYang-%s", moduleResponseType)));
374         }
375         final String moduleSetTag = moduleSetTagPerCmHandleId.getOrDefault(cmHandleId, DEFAULT_TAG);
376         final String moduleResponseFilePath = String.format("module/%s-%s", moduleSetTag, moduleResponseType);
377         final Resource moduleResponseResource = applicationContext.getResource(
378                 ResourceLoader.CLASSPATH_URL_PREFIX + moduleResponseFilePath);
379         log.info("Using module responses from : {}", moduleResponseFilePath);
380         return ResourceFileReaderUtil.getResourceFileContent(moduleResponseResource);
381     }
382
383     private String getPassthroughOperationType(final String requestBody) {
384         try {
385             final JsonNode rootNode = objectMapper.readTree(requestBody);
386             return rootNode.path("operation").asText();
387         } catch (final JsonProcessingException jsonProcessingException) {
388             log.error("Invalid JSON format. cause : {}", jsonProcessingException.getMessage());
389         }
390         return DEFAULT_PASSTHROUGH_OPERATION;
391     }
392
393     private void delay(final long milliseconds) {
394         try {
395             Thread.sleep(milliseconds);
396         } catch (final InterruptedException e) {
397             log.error("Thread sleep interrupted: {}", e.getMessage());
398             Thread.currentThread().interrupt();
399         }
400     }
401 }