180ad7b48dbe41bef28b5933232dae0182968c5e
[ccsdk/cds.git] / ms / blueprintsprocessor / functions / config-snapshots / src / main / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / functions / config / snapshots / ComponentConfigSnapshotsExecutor.kt
1 /*
2  *  Copyright © 2019 Bell Canada.
3  *  Modifications Copyright © 2018-2019 IBM.
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
18 package org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots
19
20 import com.github.fge.jsonpatch.diff.JsonDiff
21 import org.apache.logging.log4j.util.Strings
22 import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput
23 import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot
24 import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot.Status.RUNNING
25 import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot.Status.CANDIDATE
26 import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshotService
27 import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractComponentFunction
28 import org.onap.ccsdk.cds.controllerblueprints.core.*
29 import org.slf4j.LoggerFactory
30 import org.springframework.beans.factory.config.ConfigurableBeanFactory
31 import org.springframework.context.annotation.Scope
32 import org.springframework.stereotype.Component
33 import org.w3c.dom.Node
34 import org.xmlunit.builder.DiffBuilder
35 import org.xmlunit.builder.Input
36 import org.xmlunit.diff.*
37
38
39 /**
40  * ComponentConfigSnapshotsExecutor
41  *
42  * Component that retrieves the saved configuration snapshot as identified by the input parameters,
43  * named resource-id and resource-type.
44  *
45  * It reports the content of the requested snapshot via properties, config-snapshot-status
46  * and config-snapshot-value.  In case of error, details can be found in the config-snapshot-status
47  * and config-snapshot-message properties
48  *
49  * @author Serge Simard
50  */
51 @Component("component-config-snapshots-executor")
52 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
53 open class ComponentConfigSnapshotsExecutor(private val cfgSnapshotService: ResourceConfigSnapshotService) :
54         AbstractComponentFunction() {
55
56     companion object {
57         private val log = LoggerFactory.getLogger(ComponentConfigSnapshotsExecutor::class.java)
58
59         // input fields names accepted by this executor
60         const val INPUT_OPERATION = "operation"
61         const val INPUT_RESOURCE_ID = "resource-id"
62         const val INPUT_RESOURCE_TYPE = "resource-type"
63         const val INPUT_RESOURCE_STATUS = "resource-status"
64         const val INPUT_RESOURCE_SNAPSHOT = "resource-snapshot"
65         const val INPUT_DIFF_CONTENT_TYPE = "diff-content-type"
66
67         const val OPERATION_FETCH = "fetch"
68         const val OPERATION_STORE = "store"
69         const val OPERATION_DIFF = "diff"
70
71         const val DIFF_JSON = "JSON"
72         const val DIFF_XML = "XML"
73
74         // output fields names (and values) populated by this executor.
75         const val OUTPUT_STATUS = "config-snapshot-status"
76         const val OUTPUT_MESSAGE = "config-snapshot-message"
77         const val OUTPUT_SNAPSHOT = "config-snapshot-value"
78
79         const val OUTPUT_STATUS_SUCCESS = "success"
80         const val OUTPUT_STATUS_ERROR = "error"
81     }
82
83     /**
84      * Main entry point for ComponentConfigSnapshotsExecutor
85      * Supports 3 operations : fetch, store or diff
86      */
87     override suspend fun processNB(executionRequest: ExecutionServiceInput) {
88
89         val operation = getOptionalOperationInput(INPUT_OPERATION)?.returnNullIfMissing()?.textValue() ?: ""
90         val contentType = getOptionalOperationInput(INPUT_DIFF_CONTENT_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
91         val resourceId = getOptionalOperationInput(INPUT_RESOURCE_ID)?.returnNullIfMissing()?.textValue() ?: ""
92         val resourceType = getOptionalOperationInput(INPUT_RESOURCE_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
93         val resourceStatus = getOptionalOperationInput(INPUT_RESOURCE_STATUS)?.returnNullIfMissing()?.textValue() ?: RUNNING.name
94         val snapshot = getOptionalOperationInput(INPUT_RESOURCE_SNAPSHOT)?.returnNullIfMissing()?.textValue() ?: ""
95         val status = ResourceConfigSnapshot.Status.valueOf(resourceStatus)
96
97         when (operation) {
98             OPERATION_FETCH -> fetchConfigurationSnapshot(resourceId, resourceType, status)
99             OPERATION_STORE -> storeConfigurationSnapshot(snapshot, resourceId, resourceType, status)
100             OPERATION_DIFF  -> compareConfigurationSnapshot(resourceId, resourceType, contentType)
101
102             else -> setNodeOutputErrors(OUTPUT_STATUS_ERROR,
103                                 "Operation parameter must be fetch, store or diff")
104         }
105     }
106
107     /**
108      * General error handling for the executor.
109      */
110     override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
111         setNodeOutputErrors(OUTPUT_STATUS_ERROR, "Error : ${runtimeException.message}")
112     }
113
114     /**
115      * Fetch a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
116      */
117     private suspend fun fetchConfigurationSnapshot(resourceId: String, resourceType: String,
118                                                    status : ResourceConfigSnapshot.Status = RUNNING) {
119         try {
120             val cfgSnapshotValue = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, status)
121             setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotValue)
122         } catch (er : NoSuchElementException) {
123             val message = "No Resource config snapshot identified by resourceId={$resourceId}, " +
124                     "resourceType={$resourceType} does not exists"
125             setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
126         }
127     }
128
129     /**
130      * Store a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
131      */
132     private suspend fun storeConfigurationSnapshot(cfgSnapshotValue : String, resourceId: String, resourceType: String,
133                                                    status : ResourceConfigSnapshot.Status = RUNNING) {
134         if (cfgSnapshotValue.isNotEmpty()) {
135             val cfgSnapshotSaved = cfgSnapshotService.write(cfgSnapshotValue, resourceId, resourceType, status)
136             setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotSaved.config_snapshot ?: "" )
137         } else {
138             val message = "Could not store config snapshot identified by resourceId={$resourceId},resourceType={$resourceType} does not exists"
139             setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
140         }
141     }
142
143     /**
144      * Compare two configs (RUNNING vs CANDIDATE) for resource identified by ID/type, using the specified contentType
145      */
146     private suspend fun compareConfigurationSnapshot(resourceId: String, resourceType: String, contentType : String) {
147
148         val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
149         val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
150
151         if (cfgRunning.isEmpty() || cfgCandidate.isEmpty()) {
152             setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, Strings.EMPTY)
153             return
154         }
155
156         when (contentType.toUpperCase()) {
157             DIFF_JSON -> {
158                 val patchNode = JsonDiff.asJson(cfgRunning.jsonAsJsonType(), cfgCandidate.jsonAsJsonType())
159                 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, patchNode.toString())
160             }
161             DIFF_XML -> {
162                 val myDiff = DiffBuilder
163                         .compare(Input.fromString(cfgRunning))
164                         .withTest(Input.fromString(cfgCandidate))
165                         .checkForSimilar()
166                         .ignoreComments()
167                         .ignoreWhitespace()
168                         .normalizeWhitespace()
169                         .build()
170
171                 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, formatXmlDifferences(myDiff))
172             }
173             else -> {
174                 val message = "Could not compare config snapshots for type $contentType"
175                 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
176             }
177         }
178     }
179
180     /**
181      * Utility function to set the output properties of the executor node
182      */
183     private fun setNodeOutputProperties(status: String, snapshot: String) {
184         setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
185         setAttribute(OUTPUT_SNAPSHOT, snapshot.asJsonPrimitive())
186         log.info("Setting output $OUTPUT_STATUS=$status, $OUTPUT_SNAPSHOT=$snapshot ")
187     }
188
189     /**
190      * Utility function to set the output properties and errors of the executor node, in case of errors
191      */
192     private fun setNodeOutputErrors(status: String, message: String) {
193         setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
194         setAttribute(OUTPUT_MESSAGE, message.asJsonPrimitive())
195         setAttribute(OUTPUT_SNAPSHOT, "".asJsonPrimitive())
196
197         log.info("Setting error and output $OUTPUT_STATUS=$status, $OUTPUT_MESSAGE=$message ")
198
199         addError(status, OUTPUT_MESSAGE, message)
200     }
201
202     /**
203      * Formats XmlUnit differences into xml-patch like response (RFC5261)
204      */
205     private fun formatXmlDifferences(differences : Diff) : String {
206         val output = StringBuilder()
207         output.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
208                 "<diff>")
209         val diffIterator = differences.getDifferences().iterator()
210         while (diffIterator.hasNext()) {
211
212             val aDiff = diffIterator.next().comparison
213             when (aDiff.type) {
214                 ComparisonType.ATTR_VALUE -> {
215                     output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
216                             .append(aDiff.testDetails.value)
217                             .append("</replace>")
218                 }
219                 ComparisonType.TEXT_VALUE -> {
220                     output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
221                             .append(aDiff.testDetails.value)
222                             .append("</replace>")
223                 }
224                 ComparisonType.CHILD_LOOKUP -> {
225                     output.append("<add sel=\"").append(aDiff.testDetails.parentXPath).append("\">")
226                             .append(formatNode(aDiff.testDetails.target))
227                             .append("</add>")
228                 }
229                 ComparisonType.CHILD_NODELIST_LENGTH -> {
230                     // Ignored; will be processed in the CHILD_LOOKUP case
231                 }
232                 else -> {
233                     log.warn("Unsupported XML difference found: $aDiff")
234                 }
235             }
236         }
237         output.append("</diff>")
238
239         return output.toString()
240     }
241
242     /**
243      * Formats a node value obtained from an XmlUnit differences node
244      */
245     private fun formatNode(node: Node): String {
246         val output = StringBuilder()
247
248         val parentName = node.localName
249         output.append("<$parentName>")
250         if (node.hasChildNodes()) {
251             val nodes = node.childNodes
252             for (index in 1..nodes.length) {
253                 val child = nodes.item(index-1)
254                 if (child.nodeType == Node.TEXT_NODE || child.nodeType == Node.COMMENT_NODE) {
255                     output.append(child.nodeValue)
256                 } else {
257                     output.append(formatNode(child))
258                 }
259             }
260         }
261         output.append("</$parentName>")
262
263         return output.toString()
264     }
265 }