ENT-6378 Migrate corda-shell to external repo (#7005)

Remove the shell code from the OS code base, this includes the modules:

- `:tools:shell`
- `:tools:shell-cli`

The shell will be run within a node if it exists within the node's `drivers` directory. 

This is done by using a `URLClassloader` to load the `InteractiveShell` class into Corda's JVM process and running `startShell` and `runLocalShell`.

Running the shell within the `:samples` will require adding:

```
cordaDriver "net.corda:corda-shell:<corda_shell_version>"
```

To the module's `build.gradle` containing `deployNodes`. The script will then include the shell in the created nodes.
This commit is contained in:
Dan Newton
2022-01-12 11:54:18 +00:00
committed by GitHub
parent 78aed771b2
commit 56c9d6404f
54 changed files with 178 additions and 4514 deletions

View File

@ -94,7 +94,7 @@ processTestResources {
dependencies {
compile project(':node-api')
compile project(':client:rpc')
compile project(':tools:shell')
compile project(':client:jackson')
compile project(':tools:cliutils')
compile project(':common-validation')
compile project(':common-configuration-parsing')

View File

@ -83,6 +83,7 @@ import net.corda.node.internal.cordapp.JarScanningCordappLoader
import net.corda.node.internal.cordapp.VirtualCordapp
import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy
import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy
import net.corda.node.internal.shell.InteractiveShell
import net.corda.node.services.ContractUpgradeHandler
import net.corda.node.services.FinalityHandler
import net.corda.node.services.NotaryChangeHandler
@ -99,8 +100,7 @@ import net.corda.node.services.api.WritableTransactionStorage
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.node.services.config.shell.determineUnsafeUsers
import net.corda.node.services.config.shell.toShellConfig
import net.corda.node.services.config.shell.toShellConfigMap
import net.corda.node.services.config.shouldInitCrashShell
import net.corda.node.services.diagnostics.NodeDiagnosticsService
import net.corda.node.services.events.NodeSchedulerService
@ -166,7 +166,6 @@ import net.corda.nodeapi.internal.persistence.RestrictedEntityManager
import net.corda.nodeapi.internal.persistence.SchemaMigration
import net.corda.nodeapi.internal.persistence.contextDatabase
import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
import net.corda.tools.shell.InteractiveShell
import org.apache.activemq.artemis.utils.ReusableLatch
import org.jolokia.jvmagent.JolokiaServer
import org.jolokia.jvmagent.JolokiaServerConfig
@ -689,16 +688,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
open fun startShell() {
if (configuration.shouldInitCrashShell()) {
val shellConfiguration = configuration.toShellConfig()
shellConfiguration.sshdPort?.let {
val shellConfiguration = configuration.toShellConfigMap()
shellConfiguration["sshdPort"]?.let {
log.info("Binding Shell SSHD server on port $it.")
}
val unsafeUsers = determineUnsafeUsers(configuration)
org.crsh.ssh.term.CRaSHCommand.setUserInfo(unsafeUsers, true, false)
log.info("Setting unsafe users as: ${unsafeUsers}")
InteractiveShell.startShell(shellConfiguration, cordappLoader.appClassLoader)
InteractiveShell.startShellIfInstalled(configuration, shellConfiguration, cordappLoader)
}
}

View File

@ -8,21 +8,42 @@ import net.corda.cliutils.printError
import net.corda.common.logging.CordaVersion
import net.corda.common.logging.errorReporting.CordaErrorContextProvider
import net.corda.common.logging.errorReporting.ErrorCode
import net.corda.common.logging.errorReporting.ErrorReporting
import net.corda.common.logging.errorReporting.report
import net.corda.core.contracts.HashAttachmentConstraint
import net.corda.core.crypto.Crypto
import net.corda.core.internal.*
import net.corda.core.internal.Emoji
import net.corda.core.internal.HashAgility
import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.errors.AddressBindingException
import net.corda.core.internal.exists
import net.corda.core.internal.isDirectory
import net.corda.core.internal.location
import net.corda.core.internal.randomOrNull
import net.corda.core.internal.safeSymbolicRead
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.loggerFor
import net.corda.node.*
import net.corda.common.logging.errorReporting.ErrorReporting
import net.corda.common.logging.errorReporting.report
import net.corda.node.NodeCmdLineOptions
import net.corda.node.SerialFilter
import net.corda.node.SharedNodeCmdLineOptions
import net.corda.node.VersionInfo
import net.corda.node.defaultSerialFilter
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
import net.corda.node.internal.subcommands.*
import net.corda.node.internal.shell.InteractiveShell
import net.corda.node.internal.subcommands.ClearNetworkCacheCli
import net.corda.node.internal.subcommands.GenerateNodeInfoCli
import net.corda.node.internal.subcommands.GenerateRpcSslCertsCli
import net.corda.node.internal.subcommands.InitialRegistration
import net.corda.node.internal.subcommands.InitialRegistrationCli
import net.corda.node.internal.subcommands.RunMigrationScriptsCli
import net.corda.node.internal.subcommands.SynchroniseSchemasCli
import net.corda.node.internal.subcommands.ValidateConfigurationCli
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logConfigurationErrors
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logRawConfig
import net.corda.node.services.config.NodeConfiguration
@ -33,13 +54,11 @@ import net.corda.nodeapi.internal.JVMAgentUtilities
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi
import org.slf4j.bridge.SLF4JBridgeHandler
import picocli.CommandLine.Mixin
import java.io.IOException
import java.io.RandomAccessFile
import java.lang.NullPointerException
import java.lang.management.ManagementFactory
import java.net.InetAddress
import java.nio.channels.UnresolvedAddressException
@ -236,29 +255,25 @@ open class NodeStartup : NodeStartupLogging {
val loadedCodapps = node.services.cordappProvider.cordapps.filter { it.isLoaded }
logLoadedCorDapps(loadedCodapps)
node.nodeReadyFuture.thenMatch({
// Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits.
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
node.nodeReadyFuture.thenMatch(
{
// Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits.
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
// Don't start the shell if there's no console attached.
if (node.configuration.shouldStartLocalShell()) {
node.startupComplete.then {
try {
InteractiveShell.runLocalShell(node::stop)
} catch (e: Exception) {
logger.error("Shell failed to start", e)
}
// Don't start the shell if there's no console attached.
// Look for shell here??
if (node.configuration.shouldStartLocalShell()) {
InteractiveShell.runLocalShellIfInstalled(node.configuration.baseDirectory, node::stop)
}
}
if (node.configuration.shouldStartSSHDaemon()) {
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
}
},
{ th ->
logger.error("Unexpected exception during registration", th)
})
if (node.configuration.shouldStartSSHDaemon()) {
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
}
},
{ th ->
logger.error("Unexpected exception during registration", th)
})
node.run()
}

View File

@ -0,0 +1,98 @@
package net.corda.node.internal.shell
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.shell.determineUnsafeUsers
import net.corda.nodeapi.internal.cordapp.CordappLoader
import org.slf4j.LoggerFactory
import java.io.File
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
object InteractiveShell {
private val log = LoggerFactory.getLogger(InteractiveShell::class.java)
private const val INTERACTIVE_SHELL_CLASS = "net.corda.tools.shell.InteractiveShell"
private const val CRASH_COMMAND_CLASS = "org.crsh.ssh.term.CRaSHCommand"
private const val START_SHELL_METHOD = "startShell"
private const val RUN_LOCAL_SHELL_METHOD = "runLocalShell"
private const val SET_USER_INFO_METHOD = "setUserInfo"
fun startShellIfInstalled(configuration: NodeConfiguration, shellConfiguration: Map<String, Any?>, cordappLoader: CordappLoader) {
val shellJar = getSingleShellJarInDriversDirectory(configuration.baseDirectory)
if (shellJar != null) {
try {
val classLoader = URLClassLoader(arrayOf(shellJar.toPath().toUri().toURL()), javaClass.classLoader)
setUnsafeUsers(classLoader, configuration)
startShell(classLoader, shellConfiguration, cordappLoader)
} catch (e: Exception) {
log.error("Shell failed to start", e)
}
}
}
/**
* Only call this after [startShellIfInstalled] has been called or the required classes will not be loaded into the current classloader.
*/
fun runLocalShellIfInstalled(baseDirectory: Path, onExit: () -> Unit = {}) {
val shellJar = getSingleShellJarInDriversDirectory(baseDirectory)
if (shellJar != null) {
try {
runLocalShell(javaClass.classLoader, onExit)
} catch (e: Exception) {
log.error("Shell failed to start", e)
}
}
}
private fun getSingleShellJarInDriversDirectory(baseDirectory: Path): File? {
val uriToDriversDirectory = Paths.get("${baseDirectory}/drivers").toUri()
val files = File(uriToDriversDirectory)
.listFiles()
?.filter { "corda-shell" in it.name }
?.filter { "jar" == it.extension }
?: emptyList()
return if (files.isNotEmpty()) {
check(files.size == 1) {
("More than one corda-shell jar installed in /drivers directory. " +
"Remove all corda-shell jars except for the one that should be used").also {
log.error(it)
}
}
files.single()
} else {
null
}
}
private fun setUnsafeUsers(classLoader: ClassLoader, configuration: NodeConfiguration) {
val unsafeUsers = determineUnsafeUsers(configuration)
val clazz = classLoader.loadClass(CRASH_COMMAND_CLASS)
clazz.getDeclaredMethod(SET_USER_INFO_METHOD, Set::class.java, Boolean::class.java, Boolean::class.java)
.invoke(null, unsafeUsers, true, false)
log.info("Setting unsafe users as: $unsafeUsers")
}
private fun startShell(classLoader: ClassLoader, shellConfiguration: Map<String, Any?>, cordappLoader: CordappLoader) {
val clazz = classLoader.loadClass(INTERACTIVE_SHELL_CLASS)
val instance = clazz.getDeclaredConstructor()
.apply { this.isAccessible = true }
.newInstance()
clazz.getDeclaredMethod(START_SHELL_METHOD, Map::class.java, ClassLoader::class.java, Boolean::class.java)
.invoke(instance, shellConfiguration, cordappLoader.appClassLoader, false)
log.info("INTERACTIVE SHELL STARTED ABSTRACT NODE")
}
private fun runLocalShell(classLoader: ClassLoader, onExit: () -> Unit = {}) {
val clazz = classLoader.loadClass(INTERACTIVE_SHELL_CLASS)
// Gets the existing instance created by [startShell] as [InteractiveShell] is a static instance
val instance = clazz.getDeclaredConstructor()
.apply { this.isAccessible = true }
.newInstance()
clazz.getDeclaredMethod(RUN_LOCAL_SHELL_METHOD, Function0::class.java).invoke(instance, onExit)
log.info("INTERACTIVE SHELL STARTED")
}
}

View File

@ -11,17 +11,18 @@ import net.corda.core.internal.notary.NotaryServiceFlow
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.node.services.config.schema.v1.V1NodeConfigurationSpec
import net.corda.node.services.config.shell.SSHDConfiguration
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
import net.corda.notary.experimental.raft.RaftConfig
import net.corda.tools.shell.SSHDConfiguration
import java.net.URL
import java.nio.file.Path
import java.time.Duration
import java.util.*
import java.util.Properties
import java.util.UUID
import javax.security.auth.x500.X500Principal
val Int.MB: Long get() = this * 1024L * 1024L

View File

@ -8,6 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.seconds
import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.node.services.config.shell.SSHDConfiguration
import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
@ -15,11 +16,11 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.config.SslConfiguration
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.tools.shell.SSHDConfiguration
import java.net.URL
import java.nio.file.Path
import java.time.Duration
import java.util.*
import java.util.Properties
import java.util.UUID
import javax.security.auth.x500.X500Principal
data class NodeConfigurationImpl(

View File

@ -40,13 +40,13 @@ import net.corda.node.services.config.schema.parsers.toProperties
import net.corda.node.services.config.schema.parsers.toURL
import net.corda.node.services.config.schema.parsers.toUUID
import net.corda.node.services.config.schema.parsers.validValue
import net.corda.node.services.config.shell.SSHDConfiguration
import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
import net.corda.notary.experimental.raft.RaftConfig
import net.corda.tools.shell.SSHDConfiguration
internal object UserSpec : Configuration.Specification<User>("User") {
private val username by string().optional()

View File

@ -0,0 +1,27 @@
package net.corda.node.services.config.shell
data class SSHDConfiguration(val port: Int) {
companion object {
internal const val INVALID_PORT_FORMAT = "Invalid port: %s"
private const val MISSING_PORT_FORMAT = "Missing port: %s"
/**
* Parses a string of the form port into a [SSHDConfiguration].
* @throws IllegalArgumentException if the port is missing or the string is garbage.
*/
@JvmStatic
fun parse(str: String): SSHDConfiguration {
require(str.isNotBlank()) { MISSING_PORT_FORMAT.format(str) }
val port = try {
str.toInt()
} catch (ex: NumberFormatException) {
throw IllegalArgumentException("Port syntax is invalid, expected port")
}
return SSHDConfiguration(port)
}
}
init {
require(port in (0..0xffff)) { INVALID_PORT_FORMAT.format(port) }
}
}

View File

@ -3,22 +3,23 @@ package net.corda.node.services.config.shell
import net.corda.core.internal.div
import net.corda.node.internal.clientSslOptionsCompatibleWith
import net.corda.node.services.config.NodeConfiguration
import net.corda.tools.shell.ShellConfiguration
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR
import net.corda.tools.shell.ShellConfiguration.Companion.SSHD_HOSTKEY_DIR
private const val COMMANDS_DIR = "shell-commands"
private const val CORDAPPS_DIR = "cordapps"
private const val SSHD_HOSTKEY_DIR = "ssh"
//re-packs data to Shell specific classes
fun NodeConfiguration.toShellConfig() = ShellConfiguration(
commandsDirectory = this.baseDirectory / COMMANDS_DIR,
cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR,
user = INTERNAL_SHELL_USER,
password = internalShellPassword,
permissions = internalShellPermissions(!this.localShellUnsafe),
localShellAllowExitInSafeMode = this.localShellAllowExitInSafeMode,
localShellUnsafe = this.localShellUnsafe,
hostAndPort = this.rpcOptions.address,
ssl = clientSslOptionsCompatibleWith(this.rpcOptions),
sshdPort = this.sshd?.port,
sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR,
noLocalShell = this.noLocalShell)
fun NodeConfiguration.toShellConfigMap() = mapOf(
"commandsDirectory" to this.baseDirectory / COMMANDS_DIR,
"cordappsDirectory" to this.baseDirectory.toString() / CORDAPPS_DIR,
"user" to INTERNAL_SHELL_USER,
"password" to internalShellPassword,
"permissions" to internalShellPermissions(!this.localShellUnsafe),
"localShellAllowExitInSafeMode" to this.localShellAllowExitInSafeMode,
"localShellUnsafe" to this.localShellUnsafe,
"hostAndPort" to this.rpcOptions.address,
"ssl" to clientSslOptionsCompatibleWith(this.rpcOptions),
"sshdPort" to this.sshd?.port,
"sshHostKeyDirectory" to this.baseDirectory / SSHD_HOSTKEY_DIR,
"noLocalShell" to this.noLocalShell
)

View File

@ -10,10 +10,10 @@ import net.corda.core.internal.div
import net.corda.core.internal.toPath
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds
import net.corda.node.services.config.shell.SSHDConfiguration
import net.corda.nodeapi.internal.config.getBooleanCaseInsensitive
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.tools.shell.SSHDConfiguration
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertEquals