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