2 * Copyright © 2019 Bell Canada.
3 * Modifications Copyright © 2018-2019 IBM.
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 package org.onap.ccsdk.cds.blueprintsprocessor.functions.ansible.executor
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.*
39 * ComponentConfigSnapshotsExecutor
41 * Component that retrieves the saved configuration snapshot as identified by the input parameters,
42 * named resource-id and resource-type.
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
48 * @author Serge Simard
50 @Component("component-config-snapshots-executor")
51 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
52 open class ComponentConfigSnapshotsExecutor(private val cfgSnapshotService: ResourceConfigSnapshotService) :
53 AbstractComponentFunction() {
56 private val log = LoggerFactory.getLogger(ComponentConfigSnapshotsExecutor::class.java)
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"
66 const val OPERATION_FETCH = "fetch"
67 const val OPERATION_STORE = "store"
68 const val OPERATION_DIFF = "diff"
70 const val DIFF_JSON = "JSON"
71 const val DIFF_XML = "XML"
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"
78 const val OUTPUT_STATUS_SUCCESS = "success"
79 const val OUTPUT_STATUS_ERROR = "error"
83 * Main entry point for ComponentConfigSnapshotsExecutor
84 * Supports 3 operations : fetch, store or diff
86 override suspend fun processNB(executionRequest: ExecutionServiceInput) {
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)
97 OPERATION_FETCH -> fetchConfigurationSnapshot(resourceId, resourceType, status)
98 OPERATION_STORE -> storeConfigurationSnapshot(snapshot, resourceId, resourceType, status)
99 OPERATION_DIFF -> compareConfigurationSnapshot(resourceId, resourceType, contentType)
101 else -> setNodeOutputErrors(OUTPUT_STATUS_ERROR,
102 "Operation parameter must be fetch, store or diff")
107 * General error handling for the executor.
109 override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
110 setNodeOutputErrors(OUTPUT_STATUS_ERROR, "Error : ${runtimeException.message}")
114 * Fetch a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
116 private suspend fun fetchConfigurationSnapshot(resourceId: String, resourceType: String,
117 status : ResourceConfigSnapshot.Status = RUNNING) {
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)
129 * Store a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
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 ?: "" )
137 val message = "Could not store config snapshot identified by resourceId={$resourceId},resourceType={$resourceType} does not exists"
138 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
143 * Compare two configs (RUNNING vs CANDIDATE) for resource identified by ID/type, using the specified contentType
145 private suspend fun compareConfigurationSnapshot(resourceId: String, resourceType: String, contentType : String) {
147 when (contentType.toUpperCase()) {
149 val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
150 val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
152 val patchNode = JsonDiff.asJson(cfgRunning.jsonAsJsonType(), cfgCandidate.jsonAsJsonType())
153 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, patchNode.toString())
156 val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
157 val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
159 val myDiff = DiffBuilder
160 .compare(Input.fromString(cfgRunning))
161 .withTest(Input.fromString(cfgCandidate))
165 .normalizeWhitespace()
168 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, formatXmlDifferences(myDiff))
171 val message = "Could not compare config snapshots for type $contentType"
172 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
178 * Utility function to set the output properties of the executor node
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 ")
187 * Utility function to set the output properties and errors of the executor node, in case of errors
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())
194 log.info("Setting error and output $OUTPUT_STATUS=$status, $OUTPUT_MESSAGE=$message ")
196 addError(status, OUTPUT_MESSAGE, message)
200 * Formats XmlUnit differences into xml-patch like response (RFC5261)
202 private fun formatXmlDifferences(differences : Diff) : String {
203 val output = StringBuilder()
204 output.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
206 val diffIterator = differences.getDifferences().iterator()
207 while (diffIterator.hasNext()) {
209 val aDiff = diffIterator.next().comparison
211 ComparisonType.ATTR_VALUE -> {
212 output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
213 .append(aDiff.testDetails.value)
214 .append("</replace>")
216 ComparisonType.TEXT_VALUE -> {
217 output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
218 .append(aDiff.testDetails.value)
219 .append("</replace>")
221 ComparisonType.CHILD_LOOKUP -> {
222 output.append("<add sel=\"").append(aDiff.testDetails.parentXPath).append("\">")
223 .append(formatNode(aDiff.testDetails.target))
226 ComparisonType.CHILD_NODELIST_LENGTH -> {
227 // Ignored; will be processed in the CHILD_LOOKUP case
230 log.warn("Unsupported XML difference found: $aDiff")
234 output.append("</diff>")
236 return output.toString()
240 * Formats a node value obtained from an XmlUnit differences node
242 private fun formatNode(node: Node): String {
243 val output = StringBuilder()
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)
254 output.append(formatNode(child))
258 output.append("</$parentName>")
260 return output.toString()