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