mirror of
https://github.com/corda/corda.git
synced 2025-06-18 07:08:15 +00:00
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:
@ -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')
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user