diff options
author | Serge Simard <serge@agilitae.com> | 2019-08-08 10:55:57 -0400 |
---|---|---|
committer | Serge Simard <serge@agilitae.com> | 2019-08-13 14:57:10 +0000 |
commit | 34c424689a52614fb414d65899282497fe25b164 (patch) | |
tree | 91fdafc99a280064f0ef19c1576fdcbbae303cfc /ms/blueprintsprocessor/functions/config-snapshots/src/main | |
parent | 571c49342a905616298d923a9d29a201ae4ecd11 (diff) |
Resource Configuration Snapshots Executor and API
Issue-ID: CCSDK-1604
Signed-off-by: Serge Simard <serge@agilitae.com>
Change-Id: I349c649e941431b48a309123489d26fb22e0e50a
Signed-off-by: Serge Simard <serge@agilitae.com>
Diffstat (limited to 'ms/blueprintsprocessor/functions/config-snapshots/src/main')
4 files changed, 463 insertions, 0 deletions
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 index 000000000..eafcaf44b --- /dev/null +++ b/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/ComponentConfigSnapshotsExecutor.kt @@ -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 index 000000000..36a547032 --- /dev/null +++ b/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshot.kt @@ -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 index 000000000..4ab7d7f0e --- /dev/null +++ b/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotRepository.kt @@ -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 index 000000000..50c90f332 --- /dev/null +++ b/ms/blueprintsprocessor/functions/config-snapshots/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/config/snapshots/db/ResourceConfigSnapshotService.kt @@ -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 |