diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappInfo.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappInfo.kt new file mode 100644 index 0000000000..9f7fccfc90 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappInfo.kt @@ -0,0 +1,15 @@ +package net.corda.core.cordapp + +import net.corda.core.crypto.SecureHash +import net.corda.core.serialization.CordaSerializable + +@CordaSerializable +data class CordappInfo(val type: String, + val name: String, + val shortName: String, + val minimumPlatformVersion: Int, + val targetPlatformVersion: Int, + val version: String, + val vendor: String, + val licence: String, + val jarHash: SecureHash.SHA256) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index ecc03e5763..8e98570a0d 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -12,6 +12,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.NetworkParameters +import net.corda.core.node.NodeDiagnosticInfo import net.corda.core.node.NodeInfo import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.NetworkMapCache @@ -280,6 +281,11 @@ interface CordaRPCOps : RPCOps { /** Returns Node's NodeInfo, assuming this will not change while the node is running. */ fun nodeInfo(): NodeInfo + /** + * Returns Node's NodeDiagnosticInfo, including the version details as well as the information about installed CorDapps. + */ + fun nodeDiagnosticInfo(): NodeDiagnosticInfo + /** * Returns network's notary identities, assuming this will not change while the node is running. * diff --git a/core/src/main/kotlin/net/corda/core/node/NodeDiagnosticInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeDiagnosticInfo.kt new file mode 100644 index 0000000000..3838817f72 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/NodeDiagnosticInfo.kt @@ -0,0 +1,11 @@ +package net.corda.core.node + +import net.corda.core.cordapp.CordappInfo +import net.corda.core.serialization.CordaSerializable + +@CordaSerializable +data class NodeDiagnosticInfo(val version: String, + val revision: String, + val platformVersion: Int, + val vendor: String, + val cordapps: List) \ No newline at end of file diff --git a/docs/source/api-rpc.rst b/docs/source/api-rpc.rst index 442c675a36..f55e467d94 100644 --- a/docs/source/api-rpc.rst +++ b/docs/source/api-rpc.rst @@ -17,8 +17,10 @@ The key RPC operations exposed by the node are: * Start one of the node's registered flows * ``CordaRPCOps.startTrackedFlowDynamic`` * As above, but also returns a progress handle for the flow +* ``CordaRPCOps.nodeDiagnosticInfo`` + * Returns diagnostic information about the node, including the version and CorDapp details * ``CordaRPCOps.nodeInfo`` - * Returns information about the node + * Returns the network map entry of the node, including its address and identity details as well as the platform version information * ``CordaRPCOps.currentNodeTime`` * Returns the current time according to the node's clock * ``CordaRPCOps.partyFromKey/CordaRPCOps.wellKnownPartyFromX500Name`` diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst index 9f8f2f3e3f..4a4549678e 100644 --- a/docs/source/node-administration.rst +++ b/docs/source/node-administration.rst @@ -242,3 +242,40 @@ If the above holds, Corda components will benefit from the following: * A timely recovery from deletion or corruption of configuration files (e.g., ``node.conf``, ``node-info`` files, etc.), database drivers, CorDapps binaries and configuration, and certificate directories, provided backups are available to restore from. .. warning:: Private keys used to sign transactions should be preserved with the utmost care. The recommendation is to keep at least two separate copies on a storage not connected to the Internet. + +Checking node version and installed CorDapps +-------------------------------------------- + +A ``nodeDiagnosticInfo`` RPC call can be made to obtain version information about the Corda platform running on the node. The returned ``NodeDiagnosticInfo`` object also includes information about the CorDapps installed on the node. +The RPC call is also available as the ``run nodeDiagnosticInfo`` command executable from the Corda shell that can be accessed via the local terminal, SSH, or as the standalone shell. + +Example ++++++++ + +Here is a sample output displayed by the ``run nodeDiagnosticInfo`` command executed from the Corda shell: + +.. code-block:: none + + version: "|corda_version|" + revision: "d7e4a0050049be357999f57f69d8bca41a2b8274" + platformVersion: 4 + vendor: "Corda Open Source" + cordapps: + - type: "Contract CorDapp" + name: "corda-finance-contracts-|corda_version|" + shortName: "Corda Finance Demo" + minimumPlatformVersion: 1 + targetPlatformVersion: 4 + version: "1" + vendor: "R3" + licence: "Open Source (Apache 2)" + jarHash: "570EEB9DF4B43680586F3BE663F9C5844518BC2E410EAF9904E8DEE930B7E45C" + - type: "Workflow CorDapp" + name: "corda-finance-workflows-|corda_version|" + shortName: "Corda Finance Demo" + minimumPlatformVersion: 1 + targetPlatformVersion: 4 + version: "1" + vendor: "R3" + licence: "Open Source (Apache 2)" + jarHash: "6EA4E0B36010F1DD27B5677F3686B4713BA40C316804A4188DCA20F477FDB23F" diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt new file mode 100644 index 0000000000..0470cdd2cb --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/NodeRPCTests.kt @@ -0,0 +1,46 @@ +package net.corda.node + +import net.corda.core.internal.PLATFORM_VERSION +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class NodeRPCTests { + private val CORDA_VERSION_REGEX = "\\d+(\\.\\d+)?(-\\w+)?".toRegex() // e.g. "5.0-SNAPSHOT" + private val CORDA_VENDOR = "Corda Open Source" + private val CORDAPPS = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP) + private val CORDAPP_TYPES = setOf("Contract CorDapp", "Workflow CorDapp") + private val CORDAPP_CONTRACTS_NAME_REGEX = "corda-finance-contracts-$CORDA_VERSION_REGEX".toRegex() + private val CORDAPP_WORKFLOWS_NAME_REGEX = "corda-finance-workflows-$CORDA_VERSION_REGEX".toRegex() + private val CORDAPP_SHORT_NAME = "Corda Finance Demo" + private val CORDAPP_VENDOR = "R3" + private val CORDAPP_LICENCE = "Open Source (Apache 2)" + private val HEXADECIMAL_REGEX = "[0-9a-fA-F]+".toRegex() + + @Test + fun `run nodeDiagnosticInfo`() { + driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = CORDAPPS)) { + val nodeDiagnosticInfo = startNode().get().rpc.nodeDiagnosticInfo() + assertTrue(nodeDiagnosticInfo.version.matches(CORDA_VERSION_REGEX)) + assertTrue(nodeDiagnosticInfo.revision.matches(HEXADECIMAL_REGEX)) + assertEquals(PLATFORM_VERSION, nodeDiagnosticInfo.platformVersion) + assertEquals(CORDA_VENDOR, nodeDiagnosticInfo.vendor) + assertEquals(CORDAPPS.size, nodeDiagnosticInfo.cordapps.size) + assertEquals(CORDAPP_TYPES, nodeDiagnosticInfo.cordapps.map { it.type }.toSet()) + assertTrue(nodeDiagnosticInfo.cordapps.any { it.name.matches(CORDAPP_CONTRACTS_NAME_REGEX) }) + assertTrue(nodeDiagnosticInfo.cordapps.any { it.name.matches(CORDAPP_WORKFLOWS_NAME_REGEX) }) + val cordappInfo = nodeDiagnosticInfo.cordapps.first() + assertEquals(CORDAPP_SHORT_NAME, cordappInfo.shortName) + assertTrue(cordappInfo.version.all { it.isDigit() }) + assertEquals(CORDAPP_VENDOR, cordappInfo.vendor) + assertEquals(CORDAPP_LICENCE, cordappInfo.licence) + assertTrue(cordappInfo.minimumPlatformVersion <= PLATFORM_VERSION) + assertTrue(cordappInfo.targetPlatformVersion <= PLATFORM_VERSION) + assertTrue(cordappInfo.jarHash.toString().matches(HEXADECIMAL_REGEX)) + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 4f61748e1e..0eb24de54d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -1,11 +1,14 @@ package net.corda.node.internal import net.corda.client.rpc.notUsed +import net.corda.common.logging.CordaVersion import net.corda.core.CordaRuntimeException import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext import net.corda.core.context.InvocationOrigin import net.corda.core.contracts.ContractState +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappInfo import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic @@ -21,6 +24,7 @@ import net.corda.core.internal.messaging.InternalCordaRPCOps import net.corda.core.internal.sign import net.corda.core.messaging.* import net.corda.core.node.NetworkParameters +import net.corda.core.node.NodeDiagnosticInfo import net.corda.core.node.NodeInfo import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.NetworkMapCache @@ -165,6 +169,32 @@ internal class CordaRPCOpsImpl( return services.myInfo } + override fun nodeDiagnosticInfo(): NodeDiagnosticInfo { + return NodeDiagnosticInfo( + version = CordaVersion.releaseVersion, + revision = CordaVersion.revision, + platformVersion = CordaVersion.platformVersion, + vendor = CordaVersion.vendor, + cordapps = services.cordappProvider.cordapps + .filter { !it.jarPath.toString().endsWith("corda-core-${CordaVersion.releaseVersion}.jar") } + .map { CordappInfo( + type = when (it.info) { + is Cordapp.Info.Contract -> "Contract CorDapp" + is Cordapp.Info.Workflow -> "Workflow CorDapp" + else -> "CorDapp" + }, + name = it.name, + shortName = it.info.shortName, + minimumPlatformVersion = it.minimumPlatformVersion, + targetPlatformVersion = it.targetPlatformVersion, + version = it.info.version, + vendor = it.info.vendor, + licence = it.info.licence, + jarHash = it.jarHash) + } + ) + } + override fun notaryIdentities(): List { return services.networkMapCache.notaryIdentities } @@ -373,4 +403,4 @@ internal class CordaRPCOpsImpl( private inline fun Class<*>.checkIsA() { require(TARGET::class.java.isAssignableFrom(this)) { "$name is not a ${TARGET::class.java.name}" } } -} \ No newline at end of file +}