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