Merge "Resource Configuration Snapshots Executor and API"
authorBrinda Santh Muthuramalingam <brindasanth@in.ibm.com>
Tue, 13 Aug 2019 15:52:55 +0000 (15:52 +0000)
committerGerrit Code Review <gerrit@onap.org>
Tue, 13 Aug 2019 15:52:55 +0000 (15:52 +0000)
27 files changed:
components/model-catalog/definition-type/starter-type/node_type/component-config-snapshots-executor.json [new file with mode: 0644]
ms/blueprintsprocessor/application/pom.xml
ms/blueprintsprocessor/application/src/main/java/org/onap/ccsdk/cds/blueprintsprocessor/SwaggerConfig.java
ms/blueprintsprocessor/functions/config-snapshots/pom.xml [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/ComponentConfigSnapshotsExecutor.kt [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshot.kt [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotRepository.kt [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotService.kt [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/ComponentConfigSnapshotsExecutorTest.kt [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotServiceTest.kt [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/application-test.properties [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/keystore.p12 [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/logback-test.xml [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/config-payload-candidate.json [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/config-payload-running.json [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/interface-candidate.xml [new file with mode: 0644]
ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/interface-running.xml [new file with mode: 0644]
ms/blueprintsprocessor/functions/pom.xml
ms/blueprintsprocessor/modules/inbounds/configs-api/pom.xml [new file with mode: 0644]
ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotController.kt [new file with mode: 0644]
ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotException.kt [new file with mode: 0644]
ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotExceptionHandler.kt [new file with mode: 0644]
ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotControllerTest.kt [new file with mode: 0644]
ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/application-test.properties [new file with mode: 0644]
ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/logback.xml [new file with mode: 0644]
ms/blueprintsprocessor/modules/inbounds/pom.xml
ms/blueprintsprocessor/parent/pom.xml

diff --git a/components/model-catalog/definition-type/starter-type/node_type/component-config-snapshots-executor.json b/components/model-catalog/definition-type/starter-type/node_type/component-config-snapshots-executor.json
new file mode 100644 (file)
index 0000000..1cc3666
--- /dev/null
@@ -0,0 +1,90 @@
+{
+  "description": "This is Resource configuration snapshots Execution Component.",
+  "version": "1.0.0",
+  "attributes": {
+    "config-snapshot-status": {
+      "required": true,
+      "type": "string"
+    },
+    "config-snapshot-message": {
+      "required": true,
+      "type": "string"
+    },
+    "config-snapshot-value": {
+      "required": false,
+      "type": "string"
+    }
+  },
+  "capabilities": {
+    "component-node": {
+      "type": "tosca.capabilities.Node"
+    }
+  },
+  "interfaces": {
+    "ComponentConfigSnapshotsExecutor": {
+      "operations": {
+        "process": {
+          "inputs": {
+            "operation": {
+              "description": "Operation to perform: fetch, store, diff. (required)",
+              "required": true,
+              "type": "string",
+              "constraints": [
+                {
+                  "valid_values": [
+                    "fetch",
+                    "store",
+                    "diff"
+                  ]
+                }
+              ]
+            },
+            "resource-id": {
+              "description": "Identifier for the resource config to operate on. (required)",
+              "required": true,
+              "type": "string"
+            },
+            "resource-type": {
+              "description": "Type of the resource config to operate on, e.g. PNF, VNF, etc. (required)",
+              "required": true,
+              "type": "string"
+            },
+            "resource-status" : {
+              "description": "Status of the resource to fetch or store, either RUNNING or CANDIDATE. (optional)",
+              "required" : false,
+              "type" : "string",
+              "default": "RUNNING",
+              "constraints": [
+                {
+                  "valid_values": [
+                    "RUNNING",
+                    "CANDIDATE"
+                  ]
+                }
+              ]
+            },
+            "resource-snapshot": {
+              "description": "The resource config snapshot to store for the resource identified by id/type/status. (store operation only)",
+              "required": false,
+              "type": "string"
+            },
+            "diff-content-type": {
+              "description": "Specify the type of content expected, to perform comparison on. (diff operation only)",
+              "required": false,
+              "type": "string",
+              "constraints": [
+                {
+                  "valid_values": [
+                    "xml",
+                    "json"
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  },
+  "derived_from": "tosca.nodes.Component"
+}
\ No newline at end of file
index a504ce3..7ee52a9 100755 (executable)
             <groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
             <artifactId>selfservice-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
+            <artifactId>configs-api</artifactId>
+        </dependency>
 
         <!-- Functions -->
         <dependency>
             <groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
             <artifactId>cli-executor</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
+            <artifactId>config-snapshots</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.h2database</groupId>
             <artifactId>h2</artifactId>
index 4df55ff..82693c4 100644 (file)
@@ -52,8 +52,8 @@ public class SwaggerConfig {
     private ApiInfo apiInfo() {
         return new ApiInfo(
             "CDS Blueprints Processor APIs",
-            "Provide APIs to interact with CBA, their related resolved resources and templates.",
-            "0.5.0",
+            "Provide APIs to interact with CBA, their resolved resources and templates, and stored resource configurations.",
+            "0.5.1",
             null,
             new Contact("CCSDK Team", "www.onap.org", "onap-discuss@lists.onap.org"),
             "Apache 2.0",
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/pom.xml b/ms/blueprintsprocessor/functions/config-snapshots/pom.xml
new file mode 100644 (file)
index 0000000..7963aa3
--- /dev/null
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright © 2019 Bell Canada.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>functions</artifactId>
+        <groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
+        <version>0.5.2-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
+    <artifactId>config-snapshots</artifactId>
+    <name>Blueprints Processor Function - Config Snapshots</name>
+    <description>Blueprints Processor Function - Config Snapshots</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.python</groupId>
+            <artifactId>jython-standalone</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mariadb.jdbc</groupId>
+            <artifactId>mariadb-java-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.fge</groupId>
+            <artifactId>json-patch</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.xmlunit</groupId>
+            <artifactId>xmlunit-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.hibernate</groupId>
+            <artifactId>hibernate-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/ComponentConfigSnapshotsExecutor.kt b/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/ComponentConfigSnapshotsExecutor.kt
new file mode 100644 (file)
index 0000000..eafcaf4
--- /dev/null
@@ -0,0 +1,262 @@
+/*
+ *  Copyright © 2019 Bell Canada.
+ *  Modifications Copyright © 2018-2019 IBM.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.functions.ansible.executor
+
+import com.github.fge.jsonpatch.diff.JsonDiff
+import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot.Status.RUNNING
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot.Status.CANDIDATE
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshotService
+import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractComponentFunction
+import org.onap.ccsdk.cds.controllerblueprints.core.*
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.config.ConfigurableBeanFactory
+import org.springframework.context.annotation.Scope
+import org.springframework.stereotype.Component
+import org.w3c.dom.Node
+import org.xmlunit.builder.DiffBuilder
+import org.xmlunit.builder.Input
+import org.xmlunit.diff.*
+
+
+/**
+ * ComponentConfigSnapshotsExecutor
+ *
+ * Component that retrieves the saved configuration snapshot as identified by the input parameters,
+ * named resource-id and resource-type.
+ *
+ * It reports the content of the requested snapshot via properties, config-snapshot-status
+ * and config-snapshot-value.  In case of error, details can be found in the config-snapshot-status
+ * and config-snapshot-message properties
+ *
+ * @author Serge Simard
+ */
+@Component("component-config-snapshots-executor")
+@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
+open class ComponentConfigSnapshotsExecutor(private val cfgSnapshotService: ResourceConfigSnapshotService) :
+        AbstractComponentFunction() {
+
+    companion object {
+        private val log = LoggerFactory.getLogger(ComponentConfigSnapshotsExecutor::class.java)
+
+        // input fields names accepted by this executor
+        const val INPUT_OPERATION = "operation"
+        const val INPUT_RESOURCE_ID = "resource-id"
+        const val INPUT_RESOURCE_TYPE = "resource-type"
+        const val INPUT_RESOURCE_STATUS = "resource-status"
+        const val INPUT_RESOURCE_SNAPSHOT = "resource-snapshot"
+        const val INPUT_DIFF_CONTENT_TYPE = "diff-content-type"
+
+        const val OPERATION_FETCH = "fetch"
+        const val OPERATION_STORE = "store"
+        const val OPERATION_DIFF = "diff"
+
+        const val DIFF_JSON = "JSON"
+        const val DIFF_XML = "XML"
+
+        // output fields names (and values) populated by this executor.
+        const val OUTPUT_STATUS = "config-snapshot-status"
+        const val OUTPUT_MESSAGE = "config-snapshot-message"
+        const val OUTPUT_SNAPSHOT = "config-snapshot-value"
+
+        const val OUTPUT_STATUS_SUCCESS = "success"
+        const val OUTPUT_STATUS_ERROR = "error"
+    }
+
+    /**
+     * Main entry point for ComponentConfigSnapshotsExecutor
+     * Supports 3 operations : fetch, store or diff
+     */
+    override suspend fun processNB(executionRequest: ExecutionServiceInput) {
+
+        val operation = getOptionalOperationInput(INPUT_OPERATION)?.returnNullIfMissing()?.textValue() ?: ""
+        val contentType = getOptionalOperationInput(INPUT_DIFF_CONTENT_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
+        val resourceId = getOptionalOperationInput(INPUT_RESOURCE_ID)?.returnNullIfMissing()?.textValue() ?: ""
+        val resourceType = getOptionalOperationInput(INPUT_RESOURCE_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
+        val resourceStatus = getOptionalOperationInput(INPUT_RESOURCE_STATUS)?.returnNullIfMissing()?.textValue() ?: RUNNING.name
+        val snapshot = getOptionalOperationInput(INPUT_RESOURCE_SNAPSHOT)?.returnNullIfMissing()?.textValue() ?: ""
+        val status = ResourceConfigSnapshot.Status.valueOf(resourceStatus)
+
+        when (operation) {
+            OPERATION_FETCH -> fetchConfigurationSnapshot(resourceId, resourceType, status)
+            OPERATION_STORE -> storeConfigurationSnapshot(snapshot, resourceId, resourceType, status)
+            OPERATION_DIFF  -> compareConfigurationSnapshot(resourceId, resourceType, contentType)
+
+            else -> setNodeOutputErrors(OUTPUT_STATUS_ERROR,
+                                "Operation parameter must be fetch, store or diff")
+        }
+    }
+
+    /**
+     * General error handling for the executor.
+     */
+    override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
+        setNodeOutputErrors(OUTPUT_STATUS_ERROR, "Error : ${runtimeException.message}")
+    }
+
+    /**
+     * Fetch a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
+     */
+    private suspend fun fetchConfigurationSnapshot(resourceId: String, resourceType: String,
+                                                   status : ResourceConfigSnapshot.Status = RUNNING) {
+        try {
+            val cfgSnapshotValue = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, status)
+            setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotValue)
+        } catch (er : NoSuchElementException) {
+            val message = "No Resource config snapshot identified by resourceId={$resourceId}, " +
+                    "resourceType={$resourceType} does not exists"
+            setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
+        }
+    }
+
+    /**
+     * Store a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
+     */
+    private suspend fun storeConfigurationSnapshot(cfgSnapshotValue : String, resourceId: String, resourceType: String,
+                                                   status : ResourceConfigSnapshot.Status = RUNNING) {
+        if (cfgSnapshotValue.isNotEmpty()) {
+            val cfgSnapshotSaved = cfgSnapshotService.write(cfgSnapshotValue, resourceId, resourceType, status)
+            setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotSaved.config_snapshot ?: "" )
+        } else {
+            val message = "Could not store config snapshot identified by resourceId={$resourceId},resourceType={$resourceType} does not exists"
+            setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
+        }
+    }
+
+    /**
+     * Compare two configs (RUNNING vs CANDIDATE) for resource identified by ID/type, using the specified contentType
+     */
+    private suspend fun compareConfigurationSnapshot(resourceId: String, resourceType: String, contentType : String) {
+
+        when (contentType.toUpperCase()) {
+            DIFF_JSON -> {
+                val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
+                val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
+
+                val patchNode = JsonDiff.asJson(cfgRunning.jsonAsJsonType(), cfgCandidate.jsonAsJsonType())
+                setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, patchNode.toString())
+            }
+            DIFF_XML -> {
+                val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
+                val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
+
+                val myDiff = DiffBuilder
+                        .compare(Input.fromString(cfgRunning))
+                        .withTest(Input.fromString(cfgCandidate))
+                        .checkForSimilar()
+                        .ignoreComments()
+                        .ignoreWhitespace()
+                        .normalizeWhitespace()
+                        .build()
+
+                setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, formatXmlDifferences(myDiff))
+            }
+            else -> {
+                val message = "Could not compare config snapshots for type $contentType"
+                setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
+            }
+        }
+    }
+
+    /**
+     * Utility function to set the output properties of the executor node
+     */
+    private fun setNodeOutputProperties(status: String, snapshot: String) {
+        setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
+        setAttribute(OUTPUT_SNAPSHOT, snapshot.asJsonPrimitive())
+        log.info("Setting output $OUTPUT_STATUS=$status, $OUTPUT_SNAPSHOT=$snapshot ")
+    }
+
+    /**
+     * Utility function to set the output properties and errors of the executor node, in case of errors
+     */
+    private fun setNodeOutputErrors(status: String, message: String) {
+        setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
+        setAttribute(OUTPUT_MESSAGE, message.asJsonPrimitive())
+        setAttribute(OUTPUT_SNAPSHOT, "".asJsonPrimitive())
+
+        log.info("Setting error and output $OUTPUT_STATUS=$status, $OUTPUT_MESSAGE=$message ")
+
+        addError(status, OUTPUT_MESSAGE, message)
+    }
+
+    /**
+     * Formats XmlUnit differences into xml-patch like response (RFC5261)
+     */
+    private fun formatXmlDifferences(differences : Diff) : String {
+        val output = StringBuilder()
+        output.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
+                "<diff>")
+        val diffIterator = differences.getDifferences().iterator()
+        while (diffIterator.hasNext()) {
+
+            val aDiff = diffIterator.next().comparison
+            when (aDiff.type) {
+                ComparisonType.ATTR_VALUE -> {
+                    output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
+                            .append(aDiff.testDetails.value)
+                            .append("</replace>")
+                }
+                ComparisonType.TEXT_VALUE -> {
+                    output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
+                            .append(aDiff.testDetails.value)
+                            .append("</replace>")
+                }
+                ComparisonType.CHILD_LOOKUP -> {
+                    output.append("<add sel=\"").append(aDiff.testDetails.parentXPath).append("\">")
+                            .append(formatNode(aDiff.testDetails.target))
+                            .append("</add>")
+                }
+                ComparisonType.CHILD_NODELIST_LENGTH -> {
+                    // Ignored; will be processed in the CHILD_LOOKUP case
+                }
+                else -> {
+                    log.warn("Unsupported XML difference found: $aDiff")
+                }
+            }
+        }
+        output.append("</diff>")
+
+        return output.toString()
+    }
+
+    /**
+     * Formats a node value obtained from an XmlUnit differences node
+     */
+    private fun formatNode(node: Node): String {
+        val output = StringBuilder()
+
+        val parentName = node.localName
+        output.append("<$parentName>")
+        if (node.hasChildNodes()) {
+            val nodes = node.childNodes
+            for (index in 1..nodes.length) {
+                val child = nodes.item(index-1)
+                if (child.nodeType == Node.TEXT_NODE || child.nodeType == Node.COMMENT_NODE) {
+                    output.append(child.nodeValue)
+                } else {
+                    output.append(formatNode(child))
+                }
+            }
+        }
+        output.append("</$parentName>")
+
+        return output.toString()
+    }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshot.kt b/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshot.kt
new file mode 100644 (file)
index 0000000..36a5470
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2019 Bell Canada
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db
+
+import com.fasterxml.jackson.annotation.JsonFormat
+import io.swagger.annotations.ApiModelProperty
+import org.hibernate.annotations.Proxy
+import org.springframework.data.annotation.LastModifiedDate
+import org.springframework.data.jpa.domain.support.AuditingEntityListener
+import java.io.Serializable
+import java.util.*
+import javax.persistence.Column
+import javax.persistence.Entity
+import javax.persistence.EntityListeners
+import javax.persistence.Id
+import javax.persistence.Lob
+import javax.persistence.Table
+import javax.persistence.Temporal
+import javax.persistence.TemporalType
+
+/**
+ * ResourceConfigSnapshot model
+ * Stores RUNNING or CANDIDATE resource configuration snapshots, captured during the execution
+ * of blueprints.  A resource is identified by an identifier and a type.
+ *
+ * @author Serge Simard
+ * @version 1.0
+ */
+@EntityListeners(AuditingEntityListener::class)
+@Entity
+@Table(name = "RESOURCE_CONFIG_SNAPSHOT")
+@Proxy(lazy = false)
+class ResourceConfigSnapshot : Serializable {
+
+    @get:ApiModelProperty(value = "Resource type.", required = true, example = "ServiceInstance, VfModule, VNF, PNF")
+    @Column(name = "resource_type", nullable = false)
+    var resourceType: String? = null
+
+    @get:ApiModelProperty(value = "ID associated with the resource type in the inventory system.", required = true)
+    @Column(name = "resource_id", nullable = false)
+    var resourceId: String? = null
+
+    @get:ApiModelProperty(value = "Status of the snapshot, either running or candidate.", required = true)
+    @Column(name = "status", nullable = false)
+    var status: Status? = null
+
+    @get:ApiModelProperty(value = "Snapshot of the resource as retrieved from resource.", required = true)
+    @Lob
+    @Column(name = "config_snapshot", nullable = false)
+    var config_snapshot: String? = null
+
+    @Id
+    @Column(name = "resource_config_snapshot_id")
+    var id: String? = null
+
+    @get:ApiModelProperty(value = "Creation date of the snapshot.", required = true)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
+    @LastModifiedDate
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name = "creation_date")
+    var createdDate = Date()
+
+    companion object {
+        private const val serialVersionUID = 1L
+    }
+
+    enum class Status(val state: String) {
+        RUNNING("RUNNING"),
+        CANDIDATE("CANDIDATE")
+    }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotRepository.kt b/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotRepository.kt
new file mode 100644 (file)
index 0000000..4ab7d7f
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 Bell Canada.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db
+
+import org.springframework.data.jpa.repository.JpaRepository
+import javax.transaction.Transactional
+
+/**
+ * JPA repository managing the underlying ResourceConfigSnapshot table.
+ *
+ * @author Serge Simard
+ * @version 1.0
+ */
+interface ResourceConfigSnapshotRepository : JpaRepository<ResourceConfigSnapshot, String> {
+
+    fun findByResourceIdAndResourceTypeAndStatus(
+        resourceId: String,
+        resourceType: String,
+        status : ResourceConfigSnapshot.Status): ResourceConfigSnapshot?
+
+    @Transactional
+    fun deleteByResourceIdAndResourceTypeAndStatus(
+        resourceId: String,
+        resourceType: String,
+        status : ResourceConfigSnapshot.Status)
+}
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotService.kt b/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotService.kt
new file mode 100644 (file)
index 0000000..50c90f3
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 Bell Canada.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot.Status.RUNNING
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintException
+import org.slf4j.LoggerFactory
+import org.springframework.dao.DataIntegrityViolationException
+import org.springframework.stereotype.Service
+import java.util.*
+import kotlin.NoSuchElementException
+
+/**
+ * ResourceConfigSnapshot managing service.
+ *
+ * @author Serge Simard
+ * @version 1.0
+ */
+@Service
+class ResourceConfigSnapshotService(private val repository: ResourceConfigSnapshotRepository) {
+
+    private val log = LoggerFactory.getLogger(ResourceConfigSnapshotService::class.toString())
+
+    suspend fun findByResourceIdAndResourceTypeAndStatus(resourceId: String, resourceType: String,
+                                                         status : ResourceConfigSnapshot.Status = RUNNING): String =
+            withContext(Dispatchers.IO) {
+                repository.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, status)
+                        ?.config_snapshot ?: throw NoSuchElementException()
+            }
+
+    suspend fun write(snapshot: String, resId: String, resType: String,
+                      status: ResourceConfigSnapshot.Status = RUNNING) : ResourceConfigSnapshot =
+            withContext(Dispatchers.IO) {
+
+                val resourceConfigSnapshotEntry = ResourceConfigSnapshot()
+                resourceConfigSnapshotEntry.id = UUID.randomUUID().toString()
+                resourceConfigSnapshotEntry.resourceId = resId
+                resourceConfigSnapshotEntry.resourceType = resType
+                resourceConfigSnapshotEntry.status = status
+                resourceConfigSnapshotEntry.config_snapshot = snapshot
+
+                // Overwrite configuration snapshot entry of resId/resType
+                if (resId.isNotEmpty() && resType.isNotEmpty()) {
+                    repository.findByResourceIdAndResourceTypeAndStatus(resId, resType, status)?.
+                        let {
+                            log.info("Overwriting configuration snapshot entry for resourceId=($resId), " +
+                                "resourceType=($resType), status=($status)")
+                            repository.deleteByResourceIdAndResourceTypeAndStatus(resId, resType, status)
+                        }
+                }
+                var storedSnapshot: ResourceConfigSnapshot
+                try {
+                    storedSnapshot = repository.saveAndFlush(resourceConfigSnapshotEntry)
+                    log.info("Stored configuration snapshot for resourceId=($resId), " +
+                            "resourceType=($resType), status=($status), " +
+                            "dated=(${storedSnapshot.createdDate})")
+                } catch (ex: DataIntegrityViolationException) {
+                    throw BluePrintException("Failed to store configuration snapshot entry.", ex)
+                }
+                storedSnapshot
+            }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/ComponentConfigSnapshotsExecutorTest.kt b/ms/blueprintsprocessor/functions/config-snapshots/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/ComponentConfigSnapshotsExecutorTest.kt
new file mode 100644 (file)
index 0000000..79dd930
--- /dev/null
@@ -0,0 +1,365 @@
+/*
+ *  Copyright © 2019 Bell Canada.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.functions.ansible.executor
+
+import com.fasterxml.jackson.databind.JsonNode
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintProperties
+import org.onap.ccsdk.cds.blueprintsprocessor.core.BlueprintPropertyConfiguration
+import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput
+import org.onap.ccsdk.cds.blueprintsprocessor.db.BluePrintDBLibConfiguration
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.ansible.executor.ComponentConfigSnapshotsExecutor.Companion.DIFF_JSON
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.ansible.executor.ComponentConfigSnapshotsExecutor.Companion.DIFF_XML
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.ansible.executor.ComponentConfigSnapshotsExecutor.Companion.OPERATION_DIFF
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.ansible.executor.ComponentConfigSnapshotsExecutor.Companion.OPERATION_FETCH
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.ansible.executor.ComponentConfigSnapshotsExecutor.Companion.OPERATION_STORE
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshotService
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive
+import org.onap.ccsdk.cds.controllerblueprints.core.config.BluePrintLoadConfiguration
+import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintMetadataUtils
+import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit4.SpringRunner
+
+@RunWith(SpringRunner::class)
+@ContextConfiguration(classes = [ResourceConfigSnapshotService::class,
+    BlueprintPropertyConfiguration::class, BluePrintProperties::class,
+    BluePrintDBLibConfiguration::class, BluePrintLoadConfiguration::class])
+@TestPropertySource(locations = ["classpath:application-test.properties"])
+@ComponentScan(basePackages = ["org.onap.ccsdk.cds.blueprintsprocessor", "org.onap.ccsdk.cds.controllerblueprints"])
+@EnableAutoConfiguration
+@Suppress("SameParameterValue")
+class ComponentConfigSnapshotsExecutorTest {
+
+    @Autowired
+    lateinit var cfgSnapshotService : ResourceConfigSnapshotService
+    lateinit var cfgSnapshotComponent : ComponentConfigSnapshotsExecutor
+    private var bluePrintRuntimeService = BluePrintMetadataUtils.getBluePrintRuntime("123456-1000",
+            "./../../../../components/model-catalog/blueprint-model/test-blueprint/remote_scripts")
+
+    private val resourceId = "1"
+    private val resourceType = "ServiceInstance"
+    private val props = mutableMapOf<String, JsonNode>()
+    private val nodeTemplateName = "nodeTemplateName"
+
+    private val executionRequest = ExecutionServiceInput()
+
+    @Before
+    fun setup() {
+        cfgSnapshotComponent = ComponentConfigSnapshotsExecutor(cfgSnapshotService)
+        props[ComponentConfigSnapshotsExecutor.INPUT_RESOURCE_ID] = resourceId.asJsonPrimitive()
+        props[ComponentConfigSnapshotsExecutor.INPUT_RESOURCE_TYPE] = resourceType.asJsonPrimitive()
+
+
+        cfgSnapshotComponent.operationInputs = props
+        cfgSnapshotComponent.bluePrintRuntimeService = bluePrintRuntimeService
+        cfgSnapshotComponent.nodeTemplateName = nodeTemplateName
+
+        cfgSnapshotComponent.executionServiceInput = executionRequest
+        cfgSnapshotComponent.processId = "12"
+        cfgSnapshotComponent.workflowName = "workflow"
+        cfgSnapshotComponent.stepName = "step"
+        cfgSnapshotComponent.interfaceName = "interfaceName"
+        cfgSnapshotComponent.operationName = "operationName"
+    }
+
+    @Test
+    fun processNBFetchWithResourceIdAndResourceTypeSingleFind() {
+        val snapshot = ResourceConfigSnapshot()
+        val snapshotConfig = "TEST1"
+        snapshot.config_snapshot = snapshotConfig
+
+        runBlocking {
+            try {
+                val resId = "121111"
+                val resType = "PNF"
+                cfgSnapshotService.write(snapshotConfig, resId, resType)
+                prepareRequestProperties(OPERATION_FETCH, resId, resType, ResourceConfigSnapshot.Status.RUNNING.name)
+
+                cfgSnapshotComponent.processNB(executionRequest)
+            } catch (e: BluePrintProcessorException) {
+                kotlin.test.assertEquals("Can't proceed with the cfg snapshot lookup: provide resource-id and resource-type.",
+                        e.message)
+                return@runBlocking
+            }
+            // then; we should get success and the TEST1 payload in our output properties
+            assertEquals(ComponentConfigSnapshotsExecutor.OUTPUT_STATUS_SUCCESS.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_STATUS))
+            assertEquals(snapshotConfig.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_SNAPSHOT))
+        }
+    }
+
+    @Test
+    fun processNBFetchCandidateWithResourceIdAndResourceTypeSingleFind() {
+        val snapshot = ResourceConfigSnapshot()
+        val snapshotConfig = "TEST"
+        snapshot.config_snapshot = snapshotConfig
+
+        runBlocking {
+            try {
+                val resId = "121111"
+                val resType = "PNF"
+                cfgSnapshotService.write(snapshotConfig, resId, resType,  ResourceConfigSnapshot.Status.CANDIDATE)
+                prepareRequestProperties(OPERATION_FETCH, resId, resType, ResourceConfigSnapshot.Status.CANDIDATE.name)
+
+                cfgSnapshotComponent.processNB(executionRequest)
+            } catch (e: BluePrintProcessorException) {
+                kotlin.test.assertEquals("Can't proceed with the cfg snapshot lookup: provide resource-id and resource-type.",
+                        e.message)
+                return@runBlocking
+            }
+            // then; we should get success and the TEST payload in our output properties
+            assertEquals(ComponentConfigSnapshotsExecutor.OUTPUT_STATUS_SUCCESS.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_STATUS))
+            assertEquals(snapshotConfig.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_SNAPSHOT))
+        }
+    }
+
+    @Test
+    fun processNBStoreWithResourceIdAndResourceType() {
+        val snapshot = ResourceConfigSnapshot()
+        val snapshotConfig = "PAYLOAD"
+        snapshot.config_snapshot = snapshotConfig
+
+        runBlocking {
+            try {
+                val resId = "121111"
+                val resType = "PNF"
+                prepareRequestProperties(OPERATION_STORE, resId, resType, snapshotConfig)
+
+                cfgSnapshotComponent.processNB(executionRequest)
+
+            } catch (e: BluePrintProcessorException) {
+                kotlin.test.assertEquals("Can't proceed with the cfg snapshot lookup: provide resource-id and resource-type.",
+                        e.message)
+                return@runBlocking
+            }
+
+            // then; we should get success and the PAYLOAD payload in our output properties
+            assertEquals(ComponentConfigSnapshotsExecutor.OUTPUT_STATUS_SUCCESS.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_STATUS))
+            assertEquals(snapshotConfig.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_SNAPSHOT))
+        }
+    }
+
+    @Test
+    fun processNBFetchNoneFound() {
+
+        runBlocking {
+            // when; asking for unknown resource Id/ resource Type combo; should get an error response
+            try {
+                prepareRequestProperties(OPERATION_FETCH, "asdasd",  "PNF", ResourceConfigSnapshot.Status.RUNNING.name)
+
+                cfgSnapshotComponent.processNB(executionRequest)
+
+            } catch (e: BluePrintProcessorException) {
+                kotlin.test.assertEquals("Can't proceed with the cfg snapshot lookup: provide resource-id and resource-type.",
+                        e.message)
+                return@runBlocking
+            }
+
+            // then; we should get error and the PAYLOAD payload in our output properties
+            assertTrue( bluePrintRuntimeService.getBluePrintError().errors.size > 0 )
+            assertEquals(ComponentConfigSnapshotsExecutor.OUTPUT_STATUS_ERROR.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_STATUS))
+        }
+    }
+
+    @Test
+    fun processNBErrorOperationUnknown() {
+
+        runBlocking {
+            // when; asking for unknown operation update; should get an error response
+            try {
+                prepareRequestProperties("update", "asdasd",  "PNF", ResourceConfigSnapshot.Status.RUNNING.name)
+
+                cfgSnapshotComponent.processNB(executionRequest)
+
+            } catch (e: BluePrintProcessorException) {
+                kotlin.test.assertEquals("Can't proceed with the cfg snapshot lookup: provide resource-id and resource-type.",
+                        e.message)
+                return@runBlocking
+            }
+
+            // then; we should get error in our output properties
+            assertTrue( bluePrintRuntimeService.getBluePrintError().errors.size == 1 )
+            assertEquals(ComponentConfigSnapshotsExecutor.OUTPUT_STATUS_ERROR.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_STATUS))
+            val msg = "Operation parameter must be fetch, store or diff"
+            assertEquals(msg.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_MESSAGE))
+        }
+    }
+
+    @Test
+    fun processNBErrorDiffContentTypeUnknown() {
+
+        runBlocking {
+            // when; asking for unknown content type diff operation; should get an error response
+            try {
+                prepareRequestProperties(OPERATION_DIFF, "asdasd",  "PNF", "YANG")
+
+                cfgSnapshotComponent.processNB(executionRequest)
+
+            } catch (e: BluePrintProcessorException) {
+                kotlin.test.assertEquals("Can't proceed with the cfg snapshot lookup: provide resource-id and resource-type.",
+                        e.message)
+                return@runBlocking
+            }
+
+            // then; we should get error in our output properties
+            assertTrue( bluePrintRuntimeService.getBluePrintError().errors.size == 1 )
+            assertEquals(ComponentConfigSnapshotsExecutor.OUTPUT_STATUS_ERROR.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_STATUS))
+            val message = "Could not compare config snapshots for type YANG"
+            assertEquals(message.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_MESSAGE))
+        }
+    }
+
+    @Test
+    fun processNBCompareTwoJsonConfigSnapshots() {
+
+        runBlocking {
+
+            // when; comparing RUNNING vs CANDIDATE json configs; should get an success response; with differences
+            try {
+                val resId = "131313"
+                val resType = "PNF"
+                preparePayload("config-payload-running.json", resId, resType, ResourceConfigSnapshot.Status.RUNNING)
+                preparePayload("config-payload-candidate.json", resId, resType, ResourceConfigSnapshot.Status.CANDIDATE)
+
+                prepareRequestProperties(OPERATION_DIFF, resId, resType, DIFF_JSON)
+                cfgSnapshotComponent.processNB(executionRequest)
+
+            } catch (e: BluePrintProcessorException) {
+                kotlin.test.assertEquals("Can't proceed with the cfg snapshot diff: provide resource-id and resource-type.",
+                        e.message)
+                return@runBlocking
+            }
+
+            // then; we should get success
+            assertTrue( bluePrintRuntimeService.getBluePrintError().errors.size == 0 )
+            assertEquals(ComponentConfigSnapshotsExecutor.OUTPUT_STATUS_SUCCESS.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_STATUS))
+
+            // then; we should get JSON-patches differences in our response property
+            val diffJson = "[{\"op\":\"add\",\"path\":\"/system-uptime-information/last-configured-time/new-child-object\",\"value\":{\"property\":\"value\"}}," +
+                    "{\"op\":\"replace\",\"path\":\"/system-uptime-information/system-booted-time/time-length\",\"value\":\"14:52:54\"}," +
+                    "{\"op\":\"replace\",\"path\":\"/system-uptime-information/time-source\",\"value\":\" DNS CLOCK \"}," +
+                    "{\"op\":\"add\",\"path\":\"/system-uptime-information/uptime-information/load-average-10\",\"value\":\"0.05\"}]"
+            assertEquals(diffJson.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_SNAPSHOT))
+        }
+    }
+
+    @Test
+    fun processNBCompareTwoXmlConfigSnapshots() {
+
+        runBlocking {
+
+            // when; comparing RUNNING vs CANDIDATE xml configs; should get an success response; with differences
+            try {
+                val resId = "141414"
+                val resType = "VNF"
+                preparePayload("interface-running.xml", resId, resType, ResourceConfigSnapshot.Status.RUNNING)
+                preparePayload("interface-candidate.xml", resId, resType, ResourceConfigSnapshot.Status.CANDIDATE)
+
+                prepareRequestProperties(OPERATION_DIFF, resId, resType, DIFF_XML)
+
+                cfgSnapshotComponent.processNB(executionRequest)
+
+            } catch (e: BluePrintProcessorException) {
+                kotlin.test.assertEquals("Can't proceed with the cfg snapshot diff: provide resource-id and resource-type.",
+                        e.message)
+                return@runBlocking
+            }
+
+            // then; we should get success
+            assertTrue( bluePrintRuntimeService.getBluePrintError().errors.size == 0 )
+            assertEquals(ComponentConfigSnapshotsExecutor.OUTPUT_STATUS_SUCCESS.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_STATUS))
+
+            // then; we should get XML-patches differences in our response property
+            val diffXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
+                    "<diff>" +
+                    "<replace sel=\"/output[1]/interface-information[1]/interface-flapped[1]/@seconds\">2343</replace>" +
+                    "<replace sel=\"/output[1]/interface-information[1]/interface-flapped[1]/text()[1]\">34</replace>" +
+                    "<replace sel=\"/output[1]/interface-information[1]/traffic-statistics[1]/input-packets[1]/text()[1]\">09098789</replace>" +
+                    "<replace sel=\"/output[1]/interface-information[1]/traffic-statistics[1]/output-packets[1]/text()[1]\">2828828</replace>" +
+                    "<add sel=\"/output[1]/interface-information[1]/physical-interface[1]\"><interface-name>TEGig400-int01</interface-name></add>" +
+                    "</diff>"
+            assertEquals(diffXml.asJsonPrimitive(),
+                    bluePrintRuntimeService.getNodeTemplateAttributeValue(nodeTemplateName,
+                            ComponentConfigSnapshotsExecutor.OUTPUT_SNAPSHOT))        }
+    }
+
+    private fun preparePayload(filename : String, resId : String, resType : String, status: ResourceConfigSnapshot.Status) {
+        runBlocking {
+            cfgSnapshotService.write(JacksonUtils.getClassPathFileContent("payload/requests/$filename"), resId, resType, status)
+        }
+    }
+
+    private fun prepareRequestProperties (oper : String, resId : String, resType : String, optional: String = "") {
+        cfgSnapshotComponent.operationInputs[ComponentConfigSnapshotsExecutor.INPUT_OPERATION] = oper.asJsonPrimitive()
+        cfgSnapshotComponent.operationInputs[ComponentConfigSnapshotsExecutor.INPUT_RESOURCE_ID] = resId.asJsonPrimitive()
+        cfgSnapshotComponent.operationInputs[ComponentConfigSnapshotsExecutor.INPUT_RESOURCE_TYPE] = resType.asJsonPrimitive()
+
+        // Optional inputs
+        cfgSnapshotComponent.operationInputs[ComponentConfigSnapshotsExecutor.INPUT_DIFF_CONTENT_TYPE] = "".asJsonPrimitive()
+        cfgSnapshotComponent.operationInputs[ComponentConfigSnapshotsExecutor.INPUT_RESOURCE_SNAPSHOT] = "".asJsonPrimitive()
+        cfgSnapshotComponent.operationInputs[ComponentConfigSnapshotsExecutor.INPUT_RESOURCE_STATUS] =
+                ResourceConfigSnapshot.Status.RUNNING.name.asJsonPrimitive()
+
+        when (oper) {
+            OPERATION_DIFF ->
+                cfgSnapshotComponent.operationInputs[ComponentConfigSnapshotsExecutor.INPUT_DIFF_CONTENT_TYPE] = optional.asJsonPrimitive()
+            OPERATION_STORE ->
+                cfgSnapshotComponent.operationInputs[ComponentConfigSnapshotsExecutor.INPUT_RESOURCE_SNAPSHOT] = optional.asJsonPrimitive()
+            OPERATION_FETCH ->
+                cfgSnapshotComponent.operationInputs[ComponentConfigSnapshotsExecutor.INPUT_RESOURCE_STATUS] = optional.asJsonPrimitive()
+        }
+    }
+}
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotServiceTest.kt b/ms/blueprintsprocessor/functions/config-snapshots/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotServiceTest.kt
new file mode 100644 (file)
index 0000000..2830cb5
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ *  Copyright © 2019 Bell Canada.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class ResourceConfigSnapshotServiceTest {
+
+    private val cfgRepository = mockk<ResourceConfigSnapshotRepository>()
+
+    private val cfgService = ResourceConfigSnapshotService(cfgRepository)
+
+    private val resourceId = "1"
+    private val resourceType = "PNF"
+    private val configSnapshot = "config_snapshot"
+    private val resourceStatus = ResourceConfigSnapshot.Status.RUNNING
+
+    @Test
+    fun findByResourceIdAndResourceTypeTest() {
+        val tr = ResourceConfigSnapshot()
+        tr.config_snapshot = "res"
+        runBlocking {
+            every {
+                cfgRepository.findByResourceIdAndResourceTypeAndStatus(any(), any(), any())
+            } returns tr
+            val res = cfgService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType)
+            assertEquals(tr.config_snapshot, res)
+        }
+    }
+
+    @Test(expected = NoSuchElementException::class)
+    fun notFoundEntryReturnsExceptionTest() {
+        val tr = ResourceConfigSnapshot()
+        runBlocking {
+            every {
+                cfgRepository.findByResourceIdAndResourceTypeAndStatus(any(), any(), any())
+            } returns tr
+            val snap = cfgService.findByResourceIdAndResourceTypeAndStatus("MISSING_ID", "UNKNOWN_TYPE")
+            assertTrue ( snap.isBlank(), "Not found but returned a non empty string" )
+        }
+    }
+
+    @Test
+    fun createNewResourceConfigSnapshotTest() {
+        val tr = ResourceConfigSnapshot()
+        runBlocking {
+            every { cfgRepository.saveAndFlush(any<ResourceConfigSnapshot>()) } returns tr
+            every {
+                cfgRepository.findByResourceIdAndResourceTypeAndStatus(any(), any(), any())
+            } returns null
+            val res = cfgService.write( configSnapshot, resourceId, resourceType, resourceStatus)
+            assertEquals(tr, res)
+        }
+    }
+
+    @Test
+    fun updateExistingResourceConfigSnapshotTest() {
+        val tr = ResourceConfigSnapshot()
+        runBlocking {
+            every { cfgRepository.saveAndFlush(any<ResourceConfigSnapshot>()) } returns tr
+            every {
+                cfgRepository.findByResourceIdAndResourceTypeAndStatus(any(), any(), any())
+            } returns tr
+            every {
+                cfgRepository.deleteByResourceIdAndResourceTypeAndStatus(any(), any(), any())
+            } returns Unit
+            val res = cfgService.write( configSnapshot, resourceId, resourceType)
+            verify {
+                cfgRepository.deleteByResourceIdAndResourceTypeAndStatus(eq(resourceId), eq(resourceType), eq(resourceStatus))
+            }
+            assertEquals(tr, res)
+        }
+    }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/application-test.properties b/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/application-test.properties
new file mode 100644 (file)
index 0000000..ce5b4e3
--- /dev/null
@@ -0,0 +1,32 @@
+#  Copyright © 2019 Bell Canada.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+blueprintsprocessor.db.primary.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
+blueprintsprocessor.db.primary.username=sa
+blueprintsprocessor.db.primary.password=
+blueprintsprocessor.db.primary.driverClassName=org.h2.Driver
+blueprintsprocessor.db.primary.hibernateHbm2ddlAuto=create-drop
+blueprintsprocessor.db.primary.hibernateDDLAuto=update
+blueprintsprocessor.db.primary.hibernateNamingStrategy=org.hibernate.cfg.ImprovedNamingStrategy
+blueprintsprocessor.db.primary.hibernateDialect=org.hibernate.dialect.H2Dialect
+# Controller Blueprints Core Configuration
+blueprintsprocessor.blueprintDeployPath=./target/blueprints/deploy
+blueprintsprocessor.blueprintArchivePath=./target/blueprints/archive
+
+# Python executor
+blueprints.processor.functions.python.executor.executionPath=./../../../../components/scripts/python/ccsdk_blueprints
+blueprints.processor.functions.python.executor.modulePaths=./../../../../components/scripts/python/ccsdk_blueprints
+
+# Executor Options
+blueprintprocessor.netconfExecutor.enabled=true
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/keystore.p12 b/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/keystore.p12
new file mode 100644 (file)
index 0000000..96b0d3a
Binary files /dev/null and b/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/keystore.p12 differ
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/logback-test.xml b/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/logback-test.xml
new file mode 100644 (file)
index 0000000..f33adcd
--- /dev/null
@@ -0,0 +1,35 @@
+<!--
+  ~ Copyright © 2019 Bell Canada
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <!-- encoders are assigned the type
+             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{55} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+
+    <logger name="org.springframework" level="warn"/>
+    <logger name="org.hibernate" level="info"/>
+    <logger name="org.onap.ccsdk.cds.blueprintsprocessor" level="info"/>
+
+    <root level="warn">
+        <appender-ref ref="STDOUT"/>
+    </root>
+
+</configuration>
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/config-payload-candidate.json b/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/config-payload-candidate.json
new file mode 100644 (file)
index 0000000..ad01287
--- /dev/null
@@ -0,0 +1,39 @@
+{
+  "system-uptime-information" :
+  {
+    "current-time" :
+    {
+      "date-time" : "2018-05-15 13:49:56 PDT"
+    },
+    "time-source" : " DNS CLOCK ",
+    "system-booted-time" :
+    {
+      "date-time" : "2018-05-15 10:57:02 PDT",
+      "time-length" : "14:52:54"
+    },
+    "protocols-started-time" :
+    {
+      "date-time" : "2018-05-15 10:59:33 PDT",
+      "time-length" : "02:50:23"
+    },
+    "last-configured-time" :
+    {
+      "date-time" : "2018-05-15 13:49:40 PDT",
+      "time-length" : "00:00:16",
+      "user" : "admin",
+      "new-child-object" : {
+        "property" : "value"
+      }
+    },
+    "uptime-information" :
+    {
+      "date-time" : "1:49PM",
+      "up-time" : "2:53",
+      "active-user-count" : "1",
+      "load-average-1" : "0.00",
+      "load-average-5" : "0.06",
+      "load-average-10" : "0.05",
+      "load-average-15" : "0.06"
+    }
+  }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/config-payload-running.json b/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/config-payload-running.json
new file mode 100644 (file)
index 0000000..9857eb9
--- /dev/null
@@ -0,0 +1,35 @@
+{
+  "system-uptime-information" :
+  {
+    "current-time" :
+    {
+      "date-time" : "2018-05-15 13:49:56 PDT"
+    },
+    "time-source" : " NTP CLOCK ",
+    "system-booted-time" :
+    {
+      "date-time" : "2018-05-15 10:57:02 PDT",
+      "time-length" : "02:52:54"
+    },
+    "protocols-started-time" :
+    {
+      "date-time" : "2018-05-15 10:59:33 PDT",
+      "time-length" : "02:50:23"
+    },
+    "last-configured-time" :
+    {
+      "date-time" : "2018-05-15 13:49:40 PDT",
+      "time-length" : "00:00:16",
+      "user" : "admin"
+    },
+    "uptime-information" :
+    {
+      "date-time" : "1:49PM",
+      "up-time" : "2:53",
+      "active-user-count" : "1",
+      "load-average-1" : "0.00",
+      "load-average-5" : "0.06",
+      "load-average-15" : "0.06"
+    }
+  }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/interface-candidate.xml b/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/interface-candidate.xml
new file mode 100644 (file)
index 0000000..d0673aa
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright © 2019 Bell Canada.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<output xmlns="http://yang.juniper.net/junos-qfx/rpc/interfaces">
+  <interface-information xmlns:junos="http://xml.juniper.net/junos/17.4R1/junos" junos:style="normal">
+      <ifd-specific-config-flags />
+      <if-config-flags />
+      <link-type>Full-Duplex</link-type>
+      <if-media-flags>
+        <ifmf-none />
+      </if-media-flags>
+      <interface-flapped junos:seconds="2343">34</interface-flapped>
+      <traffic-statistics junos:style="brief">
+        <input-packets>09098789</input-packets>
+        <output-packets>2828828</output-packets>
+      </traffic-statistics>
+    <physical-interface>
+        <interface-name>TEGig400-int01</interface-name>
+    </physical-interface>
+  </interface-information>
+</output>
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/interface-running.xml b/ms/blueprintsprocessor/functions/config-snapshots/src/test/resources/payload/requests/interface-running.xml
new file mode 100644 (file)
index 0000000..3d875c8
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright © 2019 Bell Canada.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<output xmlns="http://yang.juniper.net/junos-qfx/rpc/interfaces">
+  <interface-information xmlns:junos="http://xml.juniper.net/junos/17.4R1/junos" junos:style="normal">
+      <ifd-specific-config-flags />
+      <if-config-flags />
+      <link-type>Full-Duplex</link-type>
+      <if-media-flags>
+        <ifmf-none />
+      </if-media-flags>
+      <interface-flapped junos:seconds="0">Never</interface-flapped>
+      <traffic-statistics junos:style="brief">
+        <input-packets>0</input-packets>
+        <output-packets>0</output-packets>
+      </traffic-statistics>
+    <physical-interface/>
+  </interface-information>
+</output>
\ No newline at end of file
index 819373f..a5790b9 100755 (executable)
@@ -34,6 +34,7 @@
         <module>netconf-executor</module>
         <module>restconf-executor</module>
         <module>cli-executor</module>
+        <module>config-snapshots</module>
     </modules>
 
     <dependencies>
diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/pom.xml b/ms/blueprintsprocessor/modules/inbounds/configs-api/pom.xml
new file mode 100644 (file)
index 0000000..d6dacb5
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~  Copyright © 2019 Bell Canada
+  ~
+  ~  Licensed under the Apache License, Version 2.0 (the "License");
+  ~  you may not use this file except in compliance with the License.
+  ~  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~  Unless required by applicable law or agreed to in writing, software
+  ~  distributed under the License is distributed on an "AS IS" BASIS,
+  ~  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~  See the License for the specific language governing permissions and
+  ~  limitations under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
+        <artifactId>inbounds</artifactId>
+        <version>0.5.2-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>configs-api</artifactId>
+    <packaging>jar</packaging>
+    <name>Blueprints Processor Resource Configurations API</name>
+    <description>Blueprints Processor Resource Configurations API</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
+            <artifactId>config-snapshots</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.ccsdk.cds.controllerblueprints</groupId>
+            <artifactId>blueprint-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotController.kt b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotController.kt
new file mode 100644 (file)
index 0000000..eb79295
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * Copyright © 2019 Bell Canada
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.configs.api
+
+import com.fasterxml.jackson.databind.JsonNode
+import io.swagger.annotations.Api
+import io.swagger.annotations.ApiOperation
+import io.swagger.annotations.ApiParam
+import kotlinx.coroutines.runBlocking
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshotService
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive
+import org.springframework.http.MediaType
+import org.springframework.http.ResponseEntity
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.*
+
+/**
+ * Exposes Resource Configuration Snapshot API to store and retrieve stored resource configurations.
+ *
+ * @author Serge Simard
+ * @version 1.0
+ */
+@RestController
+@RequestMapping("/api/v1/configs")
+@Api(value = "/api/v1/configs",
+    description = "Interaction with stored configurations.")
+open class ResourceConfigSnapshotController(private val resourceConfigSnapshotService: ResourceConfigSnapshotService) {
+
+    @RequestMapping(path = ["/health-check"],
+        method = [RequestMethod.GET],
+        produces = [MediaType.APPLICATION_JSON_VALUE])
+    @ResponseBody
+    @ApiOperation(value = "Health Check", hidden = true)
+    fun ressCfgSnapshotControllerHealthCheck(): JsonNode = runBlocking {
+        "Success".asJsonPrimitive()
+    }
+
+    @RequestMapping(path = [""],
+        method = [RequestMethod.GET],
+        produces = [MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE])
+    @ApiOperation(value = "Retrieve a resource configuration snapshot.",
+        notes = "Retrieve a config snapshot, identified by its Resource Id and Type. " +
+                "An extra 'format' parameter can be passed to tell what content-type is expected.")
+    @ResponseBody
+    @PreAuthorize("hasRole('USER')")
+    fun get(
+            @ApiParam(value = "Resource Type associated of the resource configuration snapshot.", required = false)
+        @RequestParam(value = "resourceType", required = true) resourceType: String,
+
+            @ApiParam(value = "Resource Id associated of the resource configuration snapshot.", required = false)
+        @RequestParam(value = "resourceId", required = true) resourceId: String,
+
+            @ApiParam(value = "Status of the snapshot being retrieved.", defaultValue = "RUNNING", required = false)
+        @RequestParam(value = "status", required = false, defaultValue = "RUNNING") status: String,
+
+            @ApiParam(value = "Expected format of the snapshot being retrieved.", defaultValue = MediaType.TEXT_PLAIN_VALUE,
+            required = false)
+        @RequestParam(value = "format", required = false, defaultValue = MediaType.TEXT_PLAIN_VALUE)  format: String)
+
+        : ResponseEntity<String> = runBlocking {
+
+        var configSnapshot = ""
+
+        if (resourceType.isNotEmpty() && resourceId.isNotEmpty()) {
+            try {
+                configSnapshot = resourceConfigSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId,
+                        resourceType, ResourceConfigSnapshot.Status.valueOf(status.toUpperCase()))
+            } catch (ex : NoSuchElementException) {
+                throw ResourceConfigSnapshotException(
+                        "Could not find configuration snapshot entry for type $resourceType and Id $resourceId")
+            }
+        } else {
+            throw IllegalArgumentException("Missing param. You must specify resource-id and resource-type.")
+        }
+
+        var expectedContentType = format
+        if (expectedContentType.indexOf('/') < 0) {
+            expectedContentType = "application/$expectedContentType"
+        }
+        val expectedMediaType: MediaType = MediaType.valueOf(expectedContentType)
+
+        ResponseEntity.ok().contentType(expectedMediaType).body(configSnapshot)
+    }
+
+    @PostMapping("/{resourceType}/{resourceId}/{status}",
+        produces = [MediaType.APPLICATION_JSON_VALUE])
+    @ApiOperation(value = "Store a resource configuration snapshot identified by resourceId, resourceType, status.",
+        notes = "Store a resource configuration snapshot, identified by its resourceId and resourceType, " +
+                "and optionally its status, either RUNNING or CANDIDATE.",
+        response = ResourceConfigSnapshot::class, produces = MediaType.APPLICATION_JSON_VALUE)
+    @ResponseBody
+    @PreAuthorize("hasRole('USER')")
+    fun postWithResourceIdAndResourceType(
+        @ApiParam(value = "Resource Type associated with the resolution.", required = false)
+        @PathVariable(value = "resourceType", required = true) resourceType: String,
+        @ApiParam(value = "Resource Id associated with the resolution.", required = false)
+        @PathVariable(value = "resourceId", required = true) resourceId: String,
+        @ApiParam(value = "Status of the snapshot being retrieved.", defaultValue = "RUNNING", required = true)
+        @PathVariable(value = "status", required = true) status: String,
+        @ApiParam(value = "Config snapshot to store.", required = true)
+        @RequestBody snapshot: String): ResponseEntity<ResourceConfigSnapshot> = runBlocking {
+
+        val resultStored =
+                resourceConfigSnapshotService.write(snapshot, resourceId, resourceType,
+                                                    ResourceConfigSnapshot.Status.valueOf(status.toUpperCase()))
+
+        ResponseEntity.ok().body(resultStored)
+    }
+}
diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotException.kt b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotException.kt
new file mode 100644 (file)
index 0000000..1eeea98
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2019 Bell Canada.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.configs.api
+
+class ResourceConfigSnapshotException(message: String) : RuntimeException(message) {
+    var code: Int = 404
+}
diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotExceptionHandler.kt b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotExceptionHandler.kt
new file mode 100644 (file)
index 0000000..d21464e
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * Copyright © 2019 Bell Canada
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.configs.api
+
+import com.fasterxml.jackson.annotation.JsonFormat
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonTypeName
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
+import org.onap.ccsdk.cds.controllerblueprints.core.data.ErrorCode
+import org.slf4j.LoggerFactory
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import org.springframework.orm.jpa.JpaObjectRetrievalFailureException
+import org.springframework.dao.EmptyResultDataAccessException
+import org.springframework.dao.IncorrectResultSizeDataAccessException
+import org.springframework.web.server.ServerWebInputException
+import org.springframework.web.bind.annotation.ExceptionHandler
+import org.springframework.web.bind.annotation.RestControllerAdvice
+import java.io.Serializable
+import java.util.*
+
+/**
+ * Handle exceptions in ResourceConfigSnapshot API and provide relevant HTTP status codes and messages
+ *
+ * @author Serge Simard
+ * @version 1.0
+ */
+@RestControllerAdvice("org.onap.ccsdk.cds.blueprintsprocessor.configs.api")
+open class ResourceConfigSnapshotExceptionHandler {
+
+    private val log = LoggerFactory.getLogger(ResourceConfigSnapshotExceptionHandler::class.toString())
+
+    private val debugMsg = "Resource_Config_Snapshot_ExceptionHandler_Error_Message"
+
+    @ExceptionHandler
+    fun resourceConfigSnapshotExceptionHandler(e: BluePrintProcessorException): ResponseEntity<ErrorMessage> {
+        val errorCode = ErrorCode.BLUEPRINT_PATH_MISSING
+        return returnError(e, errorCode)
+    }
+
+    @ExceptionHandler
+    fun resourceConfigSnapshotExceptionHandler(e: ServerWebInputException): ResponseEntity<ErrorMessage> {
+        val errorCode = ErrorCode.INVALID_REQUEST_FORMAT
+        return returnError(e, errorCode, false)
+    }
+
+    @ExceptionHandler
+    fun resourceConfigSnapshotExceptionHandler(e: IllegalArgumentException): ResponseEntity<ErrorMessage> {
+        val errorCode = ErrorCode.INVALID_REQUEST_FORMAT
+        return returnError(e, errorCode, false)
+    }
+
+    @ExceptionHandler
+    fun resourceConfigSnapshotExceptionHandler(e: IncorrectResultSizeDataAccessException): ResponseEntity<ErrorMessage> {
+        val errorCode = ErrorCode.DUPLICATE_DATA
+        return returnError(e, errorCode)
+    }
+
+    @ExceptionHandler
+    fun resourceConfigSnapshotExceptionHandler(e: EmptyResultDataAccessException): ResponseEntity<ErrorMessage> {
+        val errorCode = ErrorCode.RESOURCE_NOT_FOUND
+        return returnError(e, errorCode, false)
+    }
+
+    @ExceptionHandler
+    fun resourceConfigSnapshotExceptionHandler(e: JpaObjectRetrievalFailureException): ResponseEntity<ErrorMessage> {
+        val errorCode = ErrorCode.RESOURCE_NOT_FOUND
+        return returnError(e, errorCode, false)
+    }
+
+    @ExceptionHandler
+    fun resourceConfigSnapshotExceptionHandler(e: Exception): ResponseEntity<ErrorMessage> {
+        val errorCode = ErrorCode.GENERIC_FAILURE
+        return returnError(e, errorCode)
+    }
+
+    @ExceptionHandler
+    fun resourceConfigSnapshotExceptionHandler(e: ResourceConfigSnapshotException): ResponseEntity<ErrorMessage> {
+        val errorCode = ErrorCode.RESOURCE_NOT_FOUND
+        return returnError(e, errorCode, false)
+    }
+
+    fun returnError(e: Exception, errorCode: ErrorCode, toBeLogged: Boolean = true): ResponseEntity<ErrorMessage> {
+        if (toBeLogged) {
+            log.error(e.message, e)
+        } else {
+            log.error(e.message)
+        }
+        val errorMessage =
+            ErrorMessage(errorCode.message(e.message!!),
+                errorCode.value,
+                debugMsg)
+        return ResponseEntity(errorMessage, HttpStatus.resolve(errorCode.httpCode)!!)
+    }
+}
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonTypeName("errorMessage")
+@JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME)
+class ErrorMessage(var message: String?, var code: Int?, var debugMessage: String?) : Serializable {
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
+    var timestamp = Date()
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotControllerTest.kt b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotControllerTest.kt
new file mode 100644 (file)
index 0000000..c3f18fc
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * Copyright © 2019 Bell Canada.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.configs.api
+
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintCoreConfiguration
+import org.onap.ccsdk.cds.controllerblueprints.core.interfaces.BluePrintCatalogService
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.autoconfigure.security.SecurityProperties
+import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.http.MediaType
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit4.SpringRunner
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.function.BodyInserters
+import java.util.*
+
+@RunWith(SpringRunner::class)
+@WebFluxTest
+@ContextConfiguration(classes = [BluePrintCoreConfiguration::class,
+    BluePrintCatalogService::class, SecurityProperties::class])
+@ComponentScan(basePackages = ["org.onap.ccsdk.cds.blueprintsprocessor", "org.onap.ccsdk.cds.controllerblueprints"])
+@TestPropertySource(locations = ["classpath:application-test.properties"])
+class ResourceConfigSnapshotControllerTest {
+
+    private val log = LoggerFactory.getLogger(ResourceConfigSnapshotControllerTest::class.toString())
+
+    @Autowired
+    lateinit var webTestClient: WebTestClient
+
+    val resourceId = "fcaa6ac3ff08"
+    val resourceType = "PNF"
+    val snapshotData = "PAYLOAD DATA"
+
+    var requestArguments = "resourceId=$resourceId&resourceType=$resourceType"
+
+    @Test
+    fun `ping return Success`() {
+        runBlocking {
+            webTestClient.get().uri("/api/v1/configs/health-check")
+                .exchange()
+                .expectStatus().isOk
+                .expectBody()
+                .equals("Success")
+        }
+    }
+
+    @Test
+    fun `update configuration is allowed and updates timestamp`() {
+        runBlocking {
+
+            webTestClient
+                    .post()
+                    .uri("/api/v1/configs/$resourceType/$resourceId/running")
+                    .body(BodyInserters.fromObject(snapshotData))
+                    .exchange()
+                    .expectStatus().is2xxSuccessful
+                    .expectBody()
+                    .jsonPath("$.createdDate")
+                        .value<String> { println(it) }
+
+            webTestClient
+                    .post()
+                    .uri("/api/v1/configs/$resourceType/$resourceId/running")
+                    .body(BodyInserters.fromObject(snapshotData))
+                    .exchange()
+                    .expectStatus().is2xxSuccessful
+                    .expectBody()
+                    .jsonPath("$.createdDate")
+                        .value<String> { println(it)}
+        }
+    }
+
+    @Test
+    fun `get returns requested JSON content-type`() {
+        runBlocking {
+            post(resourceType, "22", "RUNNING")
+            get("json", resourceType,"22", "RUNNING")
+        }
+    }
+
+    @Test
+    fun `get returns requested XML content-type`() {
+        runBlocking {
+            post(resourceType, "3", "CANDIDATE")
+            get("xml", resourceType, "3", "CANDIDATE")
+        }
+    }
+
+    @Test
+    fun `get returns 400 error if missing arg`() {
+        runBlocking {
+            val arguments = "artifactName=WRONGARG1&resolutionKey=WRONGARG1"
+
+            webTestClient.get().uri("/api/v1/configs?$arguments")
+                .exchange()
+                .expectStatus().isBadRequest
+        }
+    }
+
+    @Test
+    fun `get returns 400 error if wrong Status arg`() {
+        runBlocking {
+            val arguments = "resourceId=MISSING&resourceType=PNF&status=TOTALLY_WRONG"
+
+            webTestClient.get().uri("/api/v1/configs?$arguments")
+                    .exchange()
+                    .expectStatus().isBadRequest
+        }
+    }
+
+    @Test
+    fun `get returns 404 if entry not found`() {
+        runBlocking {
+
+            webTestClient
+                .get()
+                .uri("/api/v1/configs?resourceId=MISSING&resourceType=PNF")
+                .exchange()
+                .expectStatus().isNotFound
+        }
+    }
+
+    private fun post( resourceType: String, resourceId: String, status: String) {
+        webTestClient
+            .post()
+            .uri("/api/v1/configs/$resourceType/$resourceId/$status")
+            .body(BodyInserters.fromObject(snapshotData))
+            .exchange()
+            .expectStatus().is2xxSuccessful
+            .expectBody()
+    }
+
+    private fun get(expectedType : String, resourceType: String, resourceId: String, status: String) {
+        var requestArguments = "resourceId=$resourceId&resourceType=$resourceType&status=$status"
+
+        if (expectedType.isNotEmpty()) {
+            requestArguments = "$requestArguments&format=$expectedType"
+            webTestClient
+                .get()
+                .uri("/api/v1/configs?$requestArguments")
+                .exchange()
+                .expectStatus().is2xxSuccessful
+                .expectHeader().contentType(MediaType.valueOf("application/$expectedType"))
+                .expectBody().equals(snapshotData)
+        } else {
+            webTestClient
+                .get()
+                .uri("/api/v1/configs?$requestArguments")
+                .exchange()
+                .expectStatus().is2xxSuccessful
+                .expectHeader().contentType(MediaType.TEXT_PLAIN)
+                .expectBody().equals(snapshotData)
+        }
+    }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/application-test.properties b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/application-test.properties
new file mode 100644 (file)
index 0000000..e02ed89
--- /dev/null
@@ -0,0 +1,30 @@
+#  Copyright © 2019 Bell Canada.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+blueprintsprocessor.db.primary.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
+blueprintsprocessor.db.primary.username=sa
+blueprintsprocessor.db.primary.password=
+blueprintsprocessor.db.primary.driverClassName=org.h2.Driver
+blueprintsprocessor.db.primary.hibernateHbm2ddlAuto=create-drop
+blueprintsprocessor.db.primary.hibernateDDLAuto=update
+blueprintsprocessor.db.primary.hibernateNamingStrategy=org.hibernate.cfg.ImprovedNamingStrategy
+blueprintsprocessor.db.primary.hibernateDialect=org.hibernate.dialect.H2Dialect
+# Controller Blueprints Core Configuration
+blueprintsprocessor.blueprintDeployPath=./target/blueprints/deploy
+blueprintsprocessor.blueprintWorkingPath=./target/blueprints/work
+blueprintsprocessor.blueprintArchivePath=./target/blueprints/archive
+
+# Python executor
+blueprints.processor.functions.python.executor.executionPath=./../../../../components/scripts/python/ccsdk_blueprints
+blueprints.processor.functions.python.executor.modulePaths=./../../../../components/scripts/python/ccsdk_blueprints
diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/logback.xml b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/logback.xml
new file mode 100644 (file)
index 0000000..ed92b89
--- /dev/null
@@ -0,0 +1,35 @@
+<!--
+  ~ Copyright © 2019 Bell Canada
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <!-- encoders are assigned the type
+             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{100} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+
+    <logger name="org.springframework" level="warn"/>
+    <logger name="org.hibernate" level="info"/>
+    <logger name="org.onap.ccsdk.cds.blueprintsprocessor" level="info"/>
+
+    <root level="info">
+        <appender-ref ref="STDOUT"/>
+    </root>
+
+</configuration>
index 1685782..d539920 100644 (file)
@@ -29,6 +29,7 @@
     <description>Blueprints Processor Inbounds</description>
 
     <modules>
+        <module>configs-api</module>
         <module>designer-api</module>
         <module>resource-api</module>
         <module>selfservice-api</module>
index 554df8b..decb415 100755 (executable)
@@ -47,6 +47,8 @@
         <json.unit.version>2.8.0</json.unit.version>
         <mockkserver.version>5.5.1</mockkserver.version>
         <jsoup.version>1.10.3</jsoup.version>
+        <xmlunit.version>2.6.3</xmlunit.version>
+        <json-patch.version>1.9</json-patch.version>
     </properties>
     <dependencyManagement>
         <dependencies>
             </dependency>
 
             <!-- North Bound -->
+            <dependency>
+                <groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
+                <artifactId>configs-api</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
                 <artifactId>designer-api</artifactId>
                 <artifactId>cli-executor</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.onap.ccsdk.cds.blueprintsprocessor.functions</groupId>
+                <artifactId>config-snapshots</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+
+            <!-- Diff capability providers for config-snapshots -->
+            <dependency>
+                <groupId>com.github.fge</groupId>
+                <artifactId>json-patch</artifactId>
+                <version>${json-patch.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.xmlunit</groupId>
+                <artifactId>xmlunit-core</artifactId>
+                <version>${xmlunit.version}</version>
+            </dependency>
 
             <!-- Controller Blueprints Application Dependency -->
             <dependency>