mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
add experimental bootstrapper (#3009)
* add experimental bootstrapper (cherry picked from commit 63665d1) refactor code to be less Azure Specific Use node classes for notaries add local docker backend move to tools directoy apply fixes for local docker RPC admin port add extraParams field to context to allow dynamic backend selection begin refactor to move all common node/notary functionality into single implementations node and notaries now share the same code path as much as possible. refactor network building logic into api class port Main.kt to use new networkbuilder api add gui fix issues with local docker ports not being exposed on localhost make push and instantiate async operations add ability to "add" a node after network has been built to gui tidy up backend selection via command line and GUI allow region selection for AZURE instantiations remove old network map based node.conf and network map dockerfile tidy up constructors of the various node stage objects tidy up artefact name add network-name selection dialog * print out help * exclude transitive dep onto log4j to suppress error print out * windows fixes for local docker * fixes to allow "devs.XXXX" resource groups in line with the new devops policy of having named resourceGroups * add extra logging around constructing azure backend
This commit is contained in:
parent
dd564cfc79
commit
66294df34f
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -115,6 +115,8 @@
|
||||
<module name="loadtest_test" target="1.8" />
|
||||
<module name="mock_main" target="1.8" />
|
||||
<module name="mock_test" target="1.8" />
|
||||
<module name="network-bootstrapper_main" target="1.8" />
|
||||
<module name="network-bootstrapper_test" target="1.8" />
|
||||
<module name="network-visualiser_main" target="1.8" />
|
||||
<module name="network-visualiser_test" target="1.8" />
|
||||
<module name="node-api_main" target="1.8" />
|
||||
|
@ -43,6 +43,7 @@ include 'tools:graphs'
|
||||
include 'tools:bootstrapper'
|
||||
include 'tools:blobinspector'
|
||||
include 'tools:shell'
|
||||
include 'tools:network-bootstrapper'
|
||||
include 'example-code'
|
||||
project(':example-code').projectDir = file("$settingsDir/docs/source/example-code")
|
||||
include 'samples:attachment-demo'
|
||||
|
71
tools/network-bootstrapper/build.gradle
Normal file
71
tools/network-bootstrapper/build.gradle
Normal file
@ -0,0 +1,71 @@
|
||||
buildscript {
|
||||
|
||||
ext.tornadofx_version = '1.7.15'
|
||||
ext.controlsfx_version = '8.40.12'
|
||||
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1'
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'application'
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
|
||||
dependencies {
|
||||
|
||||
compile "com.microsoft.azure:azure:1.8.0"
|
||||
compile "com.github.docker-java:docker-java:3.0.6"
|
||||
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-junit"
|
||||
|
||||
compile project(':node-api')
|
||||
compile project(':node')
|
||||
|
||||
compile group: "com.typesafe", name: "config", version: typesafe_config_version
|
||||
compile group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.9.0"
|
||||
compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.0"
|
||||
compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+"
|
||||
compile group: 'info.picocli', name: 'picocli', version: '3.0.1'
|
||||
|
||||
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
|
||||
compile "no.tornado:tornadofx:$tornadofx_version"
|
||||
|
||||
compile "org.controlsfx:controlsfx:$controlsfx_version"
|
||||
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
baseName = 'network-bootstrapper'
|
||||
classifier = null
|
||||
version = null
|
||||
zip64 true
|
||||
mainClassName = 'net.corda.bootstrapper.Main'
|
||||
}
|
||||
|
||||
task buildNetworkBootstrapper(dependsOn: shadowJar) {
|
||||
}
|
||||
|
||||
configurations {
|
||||
compile.exclude group: "log4j", module: "log4j"
|
||||
compile.exclude group: "org.apache.logging.log4j"
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package net.corda.bootstrapper;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.stage.StageStyle;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
public class GuiUtils {
|
||||
|
||||
public static void showException(String title, String message, Throwable exception) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.initStyle(StageStyle.UTILITY);
|
||||
alert.setTitle("Exception");
|
||||
alert.setHeaderText(title);
|
||||
alert.setContentText(message);
|
||||
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
exception.printStackTrace(pw);
|
||||
String exceptionText = sw.toString();
|
||||
|
||||
Label label = new Label("Details:");
|
||||
|
||||
TextArea textArea = new TextArea(exceptionText);
|
||||
textArea.setEditable(false);
|
||||
textArea.setWrapText(true);
|
||||
|
||||
textArea.setMaxWidth(Double.MAX_VALUE);
|
||||
textArea.setMaxHeight(Double.MAX_VALUE);
|
||||
GridPane.setVgrow(textArea, Priority.ALWAYS);
|
||||
GridPane.setHgrow(textArea, Priority.ALWAYS);
|
||||
|
||||
GridPane expContent = new GridPane();
|
||||
expContent.setMaxWidth(Double.MAX_VALUE);
|
||||
expContent.add(label, 0, 0);
|
||||
expContent.add(textArea, 0, 1);
|
||||
|
||||
alert.getDialogPane().setExpandableContent(expContent);
|
||||
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
import com.microsoft.azure.management.resources.ResourceGroup
|
||||
import com.microsoft.azure.management.resources.fluentcore.arm.Region
|
||||
|
||||
class Constants {
|
||||
|
||||
companion object {
|
||||
val NODE_P2P_PORT = 10020
|
||||
val NODE_SSHD_PORT = 12222
|
||||
val NODE_RPC_PORT = 10003
|
||||
val NODE_RPC_ADMIN_PORT = 10005
|
||||
|
||||
val BOOTSTRAPPER_DIR_NAME = ".bootstrapper"
|
||||
|
||||
fun getContextMapper(): ObjectMapper {
|
||||
val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule()
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
objectMapper.registerModule(object : SimpleModule() {}.let {
|
||||
it.addSerializer(Region::class.java, object : JsonSerializer<Region>() {
|
||||
override fun serialize(value: Region, gen: JsonGenerator, serializers: SerializerProvider?) {
|
||||
gen.writeString(value.name())
|
||||
}
|
||||
})
|
||||
it.addDeserializer(Region::class.java, object : JsonDeserializer<Region>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Region {
|
||||
return Region.findByLabelOrName(p.valueAsString)
|
||||
}
|
||||
})
|
||||
})
|
||||
return objectMapper
|
||||
}
|
||||
|
||||
val ALPHA_NUMERIC_ONLY_REGEX = "[^\\p{IsAlphabetic}\\p{IsDigit}]".toRegex()
|
||||
val ALPHA_NUMERIC_DOT_AND_UNDERSCORE_ONLY_REGEX = "[^\\p{IsAlphabetic}\\p{IsDigit}._]".toRegex()
|
||||
val REGION_ARG_NAME = "REGION"
|
||||
|
||||
fun ResourceGroup.restFriendlyName(): String {
|
||||
return this.name().replace(ALPHA_NUMERIC_ONLY_REGEX, "").toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
@file:JvmName("Main")
|
||||
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import net.corda.bootstrapper.backends.Backend
|
||||
import net.corda.bootstrapper.backends.Backend.BackendType.AZURE
|
||||
import net.corda.bootstrapper.cli.AzureParser
|
||||
import net.corda.bootstrapper.cli.CliParser
|
||||
import net.corda.bootstrapper.cli.CommandLineInterface
|
||||
import net.corda.bootstrapper.cli.GuiSwitch
|
||||
import net.corda.bootstrapper.gui.Gui
|
||||
import net.corda.bootstrapper.serialization.SerializationEngine
|
||||
import picocli.CommandLine
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
SerializationEngine.init()
|
||||
|
||||
val entryPointArgs = GuiSwitch();
|
||||
CommandLine(entryPointArgs).parse(*args)
|
||||
|
||||
if (entryPointArgs.usageHelpRequested) {
|
||||
CommandLine.usage(AzureParser(), System.out)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (entryPointArgs.gui) {
|
||||
Gui.main(args)
|
||||
} else {
|
||||
val baseArgs = CliParser()
|
||||
CommandLine(baseArgs).parse(*args)
|
||||
val argParser: CliParser = when (baseArgs.backendType) {
|
||||
AZURE -> {
|
||||
val azureArgs = AzureParser()
|
||||
CommandLine(azureArgs).parse(*args)
|
||||
azureArgs
|
||||
}
|
||||
Backend.BackendType.LOCAL_DOCKER -> baseArgs
|
||||
}
|
||||
CommandLineInterface().run(argParser)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import net.corda.bootstrapper.backends.Backend
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import net.corda.bootstrapper.nodes.*
|
||||
import net.corda.bootstrapper.notaries.NotaryCopier
|
||||
import net.corda.bootstrapper.notaries.NotaryFinder
|
||||
import java.io.File
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
interface NetworkBuilder {
|
||||
|
||||
companion object {
|
||||
fun instance(): NetworkBuilder {
|
||||
return NetworkBuilderImpl()
|
||||
}
|
||||
}
|
||||
|
||||
fun onNodeLocated(callback: (FoundNode) -> Unit): NetworkBuilder
|
||||
fun onNodeCopied(callback: (CopiedNode) -> Unit): NetworkBuilder
|
||||
fun onNodeBuild(callback: (BuiltNode) -> Unit): NetworkBuilder
|
||||
fun onNodePushed(callback: (PushedNode) -> Unit): NetworkBuilder
|
||||
fun onNodeInstance(callback: (NodeInstance) -> Unit): NetworkBuilder
|
||||
|
||||
fun withNodeCounts(map: Map<String, Int>): NetworkBuilder
|
||||
fun withNetworkName(networtName: String): NetworkBuilder
|
||||
fun withBasedir(baseDir: File): NetworkBuilder
|
||||
fun withBackend(backendType: Backend.BackendType): NetworkBuilder
|
||||
fun withBackendOptions(options: Map<String, String>): NetworkBuilder
|
||||
|
||||
fun build(): CompletableFuture<Pair<List<NodeInstance>, Context>>
|
||||
}
|
||||
|
||||
private class NetworkBuilderImpl : NetworkBuilder {
|
||||
|
||||
|
||||
@Volatile
|
||||
private var onNodeLocatedCallback: ((FoundNode) -> Unit) = {}
|
||||
@Volatile
|
||||
private var onNodeCopiedCallback: ((CopiedNode) -> Unit) = {}
|
||||
@Volatile
|
||||
private var onNodeBuiltCallback: ((BuiltNode) -> Unit) = {}
|
||||
@Volatile
|
||||
private var onNodePushedCallback: ((PushedNode) -> Unit) = {}
|
||||
@Volatile
|
||||
private var onNodeInstanceCallback: ((NodeInstance) -> Unit) = {}
|
||||
@Volatile
|
||||
private var nodeCounts = mapOf<String, Int>()
|
||||
@Volatile
|
||||
private lateinit var networkName: String
|
||||
@Volatile
|
||||
private var workingDir: File? = null
|
||||
private val cacheDirName = Constants.BOOTSTRAPPER_DIR_NAME
|
||||
@Volatile
|
||||
private var backendType = Backend.BackendType.LOCAL_DOCKER
|
||||
@Volatile
|
||||
private var backendOptions: Map<String, String> = mapOf()
|
||||
|
||||
override fun onNodeLocated(callback: (FoundNode) -> Unit): NetworkBuilder {
|
||||
this.onNodeLocatedCallback = callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onNodeCopied(callback: (CopiedNode) -> Unit): NetworkBuilder {
|
||||
this.onNodeCopiedCallback = callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onNodeBuild(callback: (BuiltNode) -> Unit): NetworkBuilder {
|
||||
this.onNodeBuiltCallback = callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onNodePushed(callback: (PushedNode) -> Unit): NetworkBuilder {
|
||||
this.onNodePushedCallback = callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onNodeInstance(callback: (NodeInstance) -> Unit): NetworkBuilder {
|
||||
this.onNodeInstanceCallback = callback;
|
||||
return this
|
||||
}
|
||||
|
||||
override fun withNodeCounts(map: Map<String, Int>): NetworkBuilder {
|
||||
nodeCounts = ConcurrentHashMap(map.entries.map { it.key.toLowerCase() to it.value }.toMap())
|
||||
return this
|
||||
}
|
||||
|
||||
override fun withNetworkName(networtName: String): NetworkBuilder {
|
||||
this.networkName = networtName
|
||||
return this
|
||||
}
|
||||
|
||||
override fun withBasedir(baseDir: File): NetworkBuilder {
|
||||
this.workingDir = baseDir
|
||||
return this
|
||||
}
|
||||
|
||||
override fun withBackend(backendType: Backend.BackendType): NetworkBuilder {
|
||||
this.backendType = backendType
|
||||
return this
|
||||
}
|
||||
|
||||
override fun withBackendOptions(options: Map<String, String>): NetworkBuilder {
|
||||
this.backendOptions = HashMap(options)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): CompletableFuture<Pair<List<NodeInstance>, Context>> {
|
||||
val cacheDir = File(workingDir, cacheDirName)
|
||||
val baseDir = workingDir!!
|
||||
val context = Context(networkName, backendType, backendOptions)
|
||||
if (cacheDir.exists()) cacheDir.deleteRecursively()
|
||||
val (containerPusher, instantiator, volume) = Backend.fromContext(context, cacheDir)
|
||||
val nodeFinder = NodeFinder(baseDir)
|
||||
val notaryFinder = NotaryFinder(baseDir)
|
||||
val notaryCopier = NotaryCopier(cacheDir)
|
||||
|
||||
val nodeInstantiator = NodeInstantiator(instantiator, context)
|
||||
val nodeBuilder = NodeBuilder()
|
||||
val nodeCopier = NodeCopier(cacheDir)
|
||||
val nodePusher = NodePusher(containerPusher, context)
|
||||
|
||||
val nodeDiscoveryFuture = CompletableFuture.supplyAsync {
|
||||
val foundNodes = nodeFinder.findNodes()
|
||||
.map { it to nodeCounts.getOrDefault(it.name.toLowerCase(), 1) }
|
||||
.toMap()
|
||||
foundNodes
|
||||
}
|
||||
|
||||
val notaryDiscoveryFuture = CompletableFuture.supplyAsync {
|
||||
val copiedNotaries = notaryFinder.findNotaries()
|
||||
.map { foundNode: FoundNode ->
|
||||
notaryCopier.copyNotary(foundNode)
|
||||
}
|
||||
volume.notariesForNetworkParams(copiedNotaries)
|
||||
copiedNotaries
|
||||
}
|
||||
|
||||
val notariesFuture = notaryDiscoveryFuture.thenCompose { copiedNotaries ->
|
||||
copiedNotaries
|
||||
.map { copiedNotary ->
|
||||
nodeBuilder.buildNode(copiedNotary)
|
||||
}.map { builtNotary ->
|
||||
nodePusher.pushNode(builtNotary)
|
||||
}.map { pushedNotary ->
|
||||
pushedNotary.thenApplyAsync { nodeInstantiator.createInstanceRequest(it) }
|
||||
}.map { instanceRequest ->
|
||||
instanceRequest.thenComposeAsync { request ->
|
||||
nodeInstantiator.instantiateNotaryInstance(request)
|
||||
}
|
||||
}.toSingleFuture()
|
||||
}
|
||||
|
||||
val nodesFuture = notaryDiscoveryFuture.thenCombineAsync(nodeDiscoveryFuture) { _, nodeCount ->
|
||||
nodeCount.keys
|
||||
.map { foundNode ->
|
||||
nodeCopier.copyNode(foundNode).let {
|
||||
onNodeCopiedCallback.invoke(it)
|
||||
it
|
||||
}
|
||||
}.map { copiedNode: CopiedNode ->
|
||||
nodeBuilder.buildNode(copiedNode).let {
|
||||
onNodeBuiltCallback.invoke(it)
|
||||
it
|
||||
}
|
||||
}.map { builtNode ->
|
||||
nodePusher.pushNode(builtNode).thenApplyAsync {
|
||||
onNodePushedCallback.invoke(it)
|
||||
it
|
||||
}
|
||||
}.map { pushedNode ->
|
||||
pushedNode.thenApplyAsync {
|
||||
nodeInstantiator.createInstanceRequests(it, nodeCount)
|
||||
}
|
||||
}.map { instanceRequests ->
|
||||
instanceRequests.thenComposeAsync { requests ->
|
||||
requests.map { request ->
|
||||
nodeInstantiator.instantiateNodeInstance(request)
|
||||
.thenApplyAsync { nodeInstance ->
|
||||
context.registerNode(nodeInstance)
|
||||
onNodeInstanceCallback.invoke(nodeInstance)
|
||||
nodeInstance
|
||||
}
|
||||
}.toSingleFuture()
|
||||
}
|
||||
}.toSingleFuture()
|
||||
}.thenCompose { it }.thenApplyAsync { it.flatten() }
|
||||
|
||||
return notariesFuture.thenCombineAsync(nodesFuture, { _, nodeInstances ->
|
||||
context.networkInitiated = true
|
||||
nodeInstances to context
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> List<CompletableFuture<T>>.toSingleFuture(): CompletableFuture<List<T>> {
|
||||
return CompletableFuture.allOf(*this.toTypedArray()).thenApplyAsync {
|
||||
this.map { it.getNow(null) }
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package net.corda.bootstrapper.backends
|
||||
|
||||
import com.microsoft.azure.CloudException
|
||||
import com.microsoft.azure.credentials.AzureCliCredentials
|
||||
import com.microsoft.azure.management.Azure
|
||||
import com.microsoft.rest.LogLevel
|
||||
import net.corda.bootstrapper.Constants
|
||||
import net.corda.bootstrapper.containers.instance.azure.AzureInstantiator
|
||||
import net.corda.bootstrapper.containers.push.azure.AzureContainerPusher
|
||||
import net.corda.bootstrapper.containers.push.azure.RegistryLocator
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import net.corda.bootstrapper.volumes.azure.AzureSmbVolume
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
data class AzureBackend(override val containerPusher: AzureContainerPusher,
|
||||
override val instantiator: AzureInstantiator,
|
||||
override val volume: AzureSmbVolume) : Backend {
|
||||
|
||||
companion object {
|
||||
|
||||
val LOG = LoggerFactory.getLogger(AzureBackend::class.java)
|
||||
|
||||
private val azure: Azure = kotlin.run {
|
||||
Azure.configure()
|
||||
.withLogLevel(LogLevel.NONE)
|
||||
.authenticate(AzureCliCredentials.create())
|
||||
.withDefaultSubscription()
|
||||
}
|
||||
|
||||
fun fromContext(context: Context): AzureBackend {
|
||||
val resourceGroupName = context.networkName.replace(Constants.ALPHA_NUMERIC_DOT_AND_UNDERSCORE_ONLY_REGEX, "")
|
||||
val resourceGroup = try {
|
||||
LOG.info("Attempting to find existing resourceGroup with name: $resourceGroupName")
|
||||
val foundResourceGroup = azure.resourceGroups().getByName(resourceGroupName)
|
||||
|
||||
if (foundResourceGroup == null) {
|
||||
LOG.info("No existing resourceGroup found creating new resourceGroup with name: $resourceGroupName")
|
||||
azure.resourceGroups().define(resourceGroupName).withRegion(context.extraParams[Constants.REGION_ARG_NAME]).create()
|
||||
} else {
|
||||
LOG.info("Found existing resourceGroup, reusing")
|
||||
foundResourceGroup
|
||||
}
|
||||
} catch (e: CloudException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
val registryLocatorFuture = CompletableFuture.supplyAsync {
|
||||
RegistryLocator(azure, resourceGroup)
|
||||
}
|
||||
val containerPusherFuture = registryLocatorFuture.thenApplyAsync {
|
||||
AzureContainerPusher(azure, it.registry)
|
||||
}
|
||||
val azureNetworkStore = CompletableFuture.supplyAsync { AzureSmbVolume(azure, resourceGroup) }
|
||||
val azureInstantiatorFuture = azureNetworkStore.thenCombine(registryLocatorFuture,
|
||||
{ azureVolume, registryLocator ->
|
||||
AzureInstantiator(azure, registryLocator.registry, azureVolume, resourceGroup)
|
||||
}
|
||||
)
|
||||
return AzureBackend(containerPusherFuture.get(), azureInstantiatorFuture.get(), azureNetworkStore.get())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package net.corda.bootstrapper.backends
|
||||
|
||||
import net.corda.bootstrapper.backends.Backend.BackendType.AZURE
|
||||
import net.corda.bootstrapper.backends.Backend.BackendType.LOCAL_DOCKER
|
||||
import net.corda.bootstrapper.containers.instance.Instantiator
|
||||
import net.corda.bootstrapper.containers.push.ContainerPusher
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import net.corda.bootstrapper.volumes.Volume
|
||||
import java.io.File
|
||||
|
||||
interface Backend {
|
||||
companion object {
|
||||
fun fromContext(context: Context, baseDir: File): Backend {
|
||||
return when (context.backendType) {
|
||||
AZURE -> AzureBackend.fromContext(context)
|
||||
LOCAL_DOCKER -> DockerBackend.fromContext(context, baseDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val containerPusher: ContainerPusher
|
||||
val instantiator: Instantiator
|
||||
val volume: Volume
|
||||
|
||||
enum class BackendType {
|
||||
AZURE, LOCAL_DOCKER
|
||||
}
|
||||
|
||||
operator fun component1(): ContainerPusher {
|
||||
return containerPusher
|
||||
}
|
||||
|
||||
operator fun component2(): Instantiator {
|
||||
return instantiator
|
||||
}
|
||||
|
||||
operator fun component3(): Volume {
|
||||
return volume
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package net.corda.bootstrapper.backends
|
||||
|
||||
import net.corda.bootstrapper.containers.instance.docker.DockerInstantiator
|
||||
import net.corda.bootstrapper.containers.push.docker.DockerContainerPusher
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import net.corda.bootstrapper.volumes.docker.LocalVolume
|
||||
import java.io.File
|
||||
|
||||
class DockerBackend(override val containerPusher: DockerContainerPusher,
|
||||
override val instantiator: DockerInstantiator,
|
||||
override val volume: LocalVolume) : Backend {
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromContext(context: Context, baseDir: File): DockerBackend {
|
||||
val dockerContainerPusher = DockerContainerPusher()
|
||||
val localVolume = LocalVolume(baseDir, context)
|
||||
val dockerInstantiator = DockerInstantiator(localVolume, context)
|
||||
return DockerBackend(dockerContainerPusher, dockerInstantiator, localVolume)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,69 @@
|
||||
package net.corda.bootstrapper.cli
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.bootstrapper.Constants
|
||||
import net.corda.bootstrapper.NetworkBuilder
|
||||
import net.corda.bootstrapper.backends.Backend
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import net.corda.bootstrapper.nodes.NodeAdder
|
||||
import net.corda.bootstrapper.nodes.NodeInstantiator
|
||||
import net.corda.bootstrapper.toSingleFuture
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import java.io.File
|
||||
|
||||
class CommandLineInterface {
|
||||
|
||||
|
||||
fun run(parsedArgs: CliParser) {
|
||||
val baseDir = parsedArgs.baseDirectory
|
||||
val cacheDir = File(baseDir, Constants.BOOTSTRAPPER_DIR_NAME)
|
||||
val networkName = parsedArgs.name
|
||||
val objectMapper = Constants.getContextMapper()
|
||||
val contextFile = File(cacheDir, "$networkName.yaml")
|
||||
if (parsedArgs.isNew()) {
|
||||
val (_, context) = NetworkBuilder.instance()
|
||||
.withBasedir(baseDir)
|
||||
.withNetworkName(networkName)
|
||||
.withNodeCounts(parsedArgs.nodes)
|
||||
.onNodeBuild { builtNode -> println("Built node: ${builtNode.name} to image: ${builtNode.localImageId}") }
|
||||
.onNodePushed { pushedNode -> println("Pushed node: ${pushedNode.name} to: ${pushedNode.remoteImageName}") }
|
||||
.onNodeInstance { instance ->
|
||||
println("Instance of ${instance.name} with id: ${instance.nodeInstanceName} on address: " +
|
||||
"${instance.reachableAddress} {ssh:${instance.portMapping[Constants.NODE_SSHD_PORT]}, " +
|
||||
"p2p:${instance.portMapping[Constants.NODE_P2P_PORT]}}")
|
||||
}
|
||||
.withBackend(parsedArgs.backendType)
|
||||
.withBackendOptions(parsedArgs.backendOptions())
|
||||
.build().getOrThrow()
|
||||
persistContext(contextFile, objectMapper, context)
|
||||
} else {
|
||||
val context = setupContextFromExisting(contextFile, objectMapper, networkName)
|
||||
val (_, instantiator, _) = Backend.fromContext(context, cacheDir)
|
||||
val nodeAdder = NodeAdder(context, NodeInstantiator(instantiator, context))
|
||||
parsedArgs.nodesToAdd.map {
|
||||
nodeAdder.addNode(context, Constants.ALPHA_NUMERIC_ONLY_REGEX.replace(it.toLowerCase(), ""))
|
||||
}.toSingleFuture().getOrThrow()
|
||||
persistContext(contextFile, objectMapper, context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setupContextFromExisting(contextFile: File, objectMapper: ObjectMapper, networkName: String): Context {
|
||||
return contextFile.let {
|
||||
if (it.exists()) {
|
||||
it.inputStream().use {
|
||||
objectMapper.readValue(it, Context::class.java)
|
||||
}
|
||||
} else {
|
||||
throw IllegalStateException("No existing network context found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun persistContext(contextFile: File, objectMapper: ObjectMapper, context: Context?) {
|
||||
contextFile.outputStream().use {
|
||||
objectMapper.writeValue(it, context)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package net.corda.bootstrapper.cli
|
||||
|
||||
import com.microsoft.azure.management.resources.fluentcore.arm.Region
|
||||
import net.corda.bootstrapper.Constants
|
||||
import net.corda.bootstrapper.backends.Backend
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Option
|
||||
import java.io.File
|
||||
|
||||
open class GuiSwitch {
|
||||
|
||||
@Option(names = ["-h", "--help"], usageHelp = true, description = ["display this help message"])
|
||||
var usageHelpRequested: Boolean = false
|
||||
|
||||
@Option(names = ["-g", "--gui"], description = ["Run in Gui Mode"])
|
||||
var gui = false
|
||||
|
||||
@CommandLine.Unmatched
|
||||
var unmatched = arrayListOf<String>()
|
||||
}
|
||||
|
||||
open class CliParser : GuiSwitch() {
|
||||
|
||||
@Option(names = ["-n", "--network-name"], description = ["The resource grouping to use"], required = true)
|
||||
lateinit var name: String
|
||||
|
||||
@Option(names = ["-d", "--nodes-directory"], description = ["The directory to search for nodes in"])
|
||||
var baseDirectory = File(System.getProperty("user.dir"))
|
||||
|
||||
@Option(names = ["-b", "--backend"], description = ["The backend to use when instantiating nodes"])
|
||||
var backendType: Backend.BackendType = Backend.BackendType.LOCAL_DOCKER
|
||||
|
||||
@Option(names = ["-nodes"], split = ":", description = ["The number of each node to create NodeX:2 will create two instances of NodeX"])
|
||||
var nodes: MutableMap<String, Int> = hashMapOf()
|
||||
|
||||
@Option(names = ["--add", "-a"])
|
||||
var nodesToAdd: MutableList<String> = arrayListOf()
|
||||
|
||||
fun isNew(): Boolean {
|
||||
return nodesToAdd.isEmpty()
|
||||
}
|
||||
|
||||
open fun backendOptions(): Map<String, String> {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AzureParser : CliParser() {
|
||||
|
||||
companion object {
|
||||
val regions = Region.values().map { it.name() to it }.toMap()
|
||||
}
|
||||
|
||||
@Option(names = ["-r", "--region"], description = ["The azure region to use"], converter = [RegionConverter::class])
|
||||
var region: Region = Region.EUROPE_WEST
|
||||
|
||||
class RegionConverter : CommandLine.ITypeConverter<Region> {
|
||||
override fun convert(value: String): Region {
|
||||
return regions[value] ?: throw Error("Unknown azure region: $value")
|
||||
}
|
||||
}
|
||||
|
||||
override fun backendOptions(): Map<String, String> {
|
||||
return mapOf(Constants.REGION_ARG_NAME to region.name())
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package net.corda.bootstrapper.containers.instance
|
||||
|
||||
data class InstanceInfo(val groupId: String,
|
||||
val instanceName: String,
|
||||
val instanceAddress: String,
|
||||
val reachableAddress: String,
|
||||
val portMapping: Map<Int, Int> = emptyMap())
|
@ -0,0 +1,18 @@
|
||||
package net.corda.bootstrapper.containers.instance
|
||||
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
|
||||
interface Instantiator {
|
||||
fun instantiateContainer(imageId: String,
|
||||
portsToOpen: List<Int>,
|
||||
instanceName: String,
|
||||
env: Map<String, String>? = null): CompletableFuture<Pair<String, Map<Int, Int>>>
|
||||
|
||||
|
||||
companion object {
|
||||
val ADDITIONAL_NODE_INFOS_PATH = "/opt/corda/additional-node-infos"
|
||||
}
|
||||
|
||||
fun getExpectedFQDN(instanceName: String): String
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package net.corda.bootstrapper.containers.instance.azure
|
||||
|
||||
import com.microsoft.azure.management.Azure
|
||||
import com.microsoft.azure.management.containerinstance.ContainerGroup
|
||||
import com.microsoft.azure.management.containerinstance.ContainerGroupRestartPolicy
|
||||
import com.microsoft.azure.management.containerregistry.Registry
|
||||
import com.microsoft.azure.management.resources.ResourceGroup
|
||||
import com.microsoft.rest.ServiceCallback
|
||||
import net.corda.bootstrapper.Constants.Companion.restFriendlyName
|
||||
import net.corda.bootstrapper.containers.instance.Instantiator
|
||||
import net.corda.bootstrapper.containers.instance.Instantiator.Companion.ADDITIONAL_NODE_INFOS_PATH
|
||||
import net.corda.bootstrapper.containers.push.azure.RegistryLocator.Companion.parseCredentials
|
||||
import net.corda.bootstrapper.volumes.azure.AzureSmbVolume
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class AzureInstantiator(private val azure: Azure,
|
||||
private val registry: Registry,
|
||||
private val azureSmbVolume: AzureSmbVolume,
|
||||
private val resourceGroup: ResourceGroup
|
||||
) : Instantiator {
|
||||
override fun instantiateContainer(imageId: String,
|
||||
portsToOpen: List<Int>,
|
||||
instanceName: String,
|
||||
env: Map<String, String>?): CompletableFuture<Pair<String, Map<Int, Int>>> {
|
||||
|
||||
findAndKillExistingContainerGroup(resourceGroup, buildIdent(instanceName))
|
||||
|
||||
LOG.info("Starting instantiation of container: $instanceName using $imageId")
|
||||
val registryAddress = registry.loginServerUrl()
|
||||
val (username, password) = registry.parseCredentials();
|
||||
val mountName = "node-setup"
|
||||
val future = CompletableFuture<Pair<String, Map<Int, Int>>>().also {
|
||||
azure.containerGroups().define(buildIdent(instanceName))
|
||||
.withRegion(resourceGroup.region())
|
||||
.withExistingResourceGroup(resourceGroup)
|
||||
.withLinux()
|
||||
.withPrivateImageRegistry(registryAddress, username, password)
|
||||
.defineVolume(mountName)
|
||||
.withExistingReadWriteAzureFileShare(azureSmbVolume.shareName)
|
||||
.withStorageAccountName(azureSmbVolume.storageAccountName)
|
||||
.withStorageAccountKey(azureSmbVolume.storageAccountKey)
|
||||
.attach()
|
||||
.defineContainerInstance(instanceName)
|
||||
.withImage(imageId)
|
||||
.withExternalTcpPorts(*portsToOpen.toIntArray())
|
||||
.withVolumeMountSetting(mountName, ADDITIONAL_NODE_INFOS_PATH)
|
||||
.withEnvironmentVariables(env ?: emptyMap())
|
||||
.attach().withRestartPolicy(ContainerGroupRestartPolicy.ON_FAILURE)
|
||||
.withDnsPrefix(buildIdent(instanceName))
|
||||
.createAsync(object : ServiceCallback<ContainerGroup> {
|
||||
override fun failure(t: Throwable?) {
|
||||
it.completeExceptionally(t)
|
||||
}
|
||||
|
||||
override fun success(result: ContainerGroup) {
|
||||
val fqdn = result.fqdn()
|
||||
LOG.info("Completed instantiation: $instanceName is running at $fqdn with port(s) $portsToOpen exposed")
|
||||
it.complete(result.fqdn() to portsToOpen.map { it to it }.toMap())
|
||||
}
|
||||
})
|
||||
}
|
||||
return future
|
||||
}
|
||||
|
||||
private fun buildIdent(instanceName: String) = "$instanceName-${resourceGroup.restFriendlyName()}"
|
||||
|
||||
override fun getExpectedFQDN(instanceName: String): String {
|
||||
return "${buildIdent(instanceName)}.${resourceGroup.region().name()}.azurecontainer.io"
|
||||
}
|
||||
|
||||
fun findAndKillExistingContainerGroup(resourceGroup: ResourceGroup, containerName: String): ContainerGroup? {
|
||||
val existingContainer = azure.containerGroups().getByResourceGroup(resourceGroup.name(), containerName)
|
||||
if (existingContainer != null) {
|
||||
LOG.info("Found an existing instance of: $containerName destroying ContainerGroup")
|
||||
azure.containerGroups().deleteByResourceGroup(resourceGroup.name(), containerName)
|
||||
}
|
||||
return existingContainer;
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = LoggerFactory.getLogger(AzureInstantiator::class.java)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package net.corda.bootstrapper.containers.instance.docker
|
||||
|
||||
import com.github.dockerjava.api.model.*
|
||||
import net.corda.bootstrapper.Constants
|
||||
import net.corda.bootstrapper.containers.instance.Instantiator
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import net.corda.bootstrapper.docker.DockerUtils
|
||||
import net.corda.bootstrapper.volumes.docker.LocalVolume
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
|
||||
class DockerInstantiator(private val volume: LocalVolume,
|
||||
private val context: Context) : Instantiator {
|
||||
|
||||
val networkId = setupNetwork();
|
||||
|
||||
override fun instantiateContainer(imageId: String,
|
||||
portsToOpen: List<Int>,
|
||||
instanceName: String,
|
||||
env: Map<String, String>?): CompletableFuture<Pair<String, Map<Int, Int>>> {
|
||||
|
||||
val localClient = DockerUtils.createLocalDockerClient()
|
||||
val convertedEnv = buildDockerEnv(env)
|
||||
val nodeInfosVolume = Volume(Instantiator.ADDITIONAL_NODE_INFOS_PATH)
|
||||
val existingContainers = localClient.listContainersCmd().withShowAll(true).exec()
|
||||
.map { it.names.first() to it }
|
||||
.filter { it.first.endsWith(instanceName) }
|
||||
existingContainers.forEach { (_, container) ->
|
||||
try {
|
||||
localClient.killContainerCmd(container.id).exec()
|
||||
LOG.info("Found running container: $instanceName killed")
|
||||
} catch (e: Throwable) {
|
||||
//container not running
|
||||
}
|
||||
try {
|
||||
localClient.removeContainerCmd(container.id).exec()
|
||||
LOG.info("Found existing container: $instanceName removed")
|
||||
} catch (e: Throwable) {
|
||||
//this *only* occurs of the container had been previously scheduled for removal
|
||||
//but did not complete before this attempt was begun.
|
||||
}
|
||||
|
||||
}
|
||||
LOG.info("starting local docker instance of: $imageId with name $instanceName and env: $env")
|
||||
val ports = (portsToOpen + Constants.NODE_RPC_ADMIN_PORT).map { ExposedPort.tcp(it) }.map { PortBinding(null, it) }.let { Ports(*it.toTypedArray()) }
|
||||
val createCmd = localClient.createContainerCmd(imageId)
|
||||
.withName(instanceName)
|
||||
.withVolumes(nodeInfosVolume)
|
||||
.withBinds(Bind(volume.getPath(), nodeInfosVolume))
|
||||
.withPortBindings(ports)
|
||||
.withExposedPorts(ports.bindings.map { it.key })
|
||||
.withPublishAllPorts(true)
|
||||
.withNetworkMode(networkId)
|
||||
.withEnv(convertedEnv).exec()
|
||||
|
||||
localClient.startContainerCmd(createCmd.id).exec()
|
||||
val foundContainer = localClient.listContainersCmd().exec()
|
||||
.filter { it.id == (createCmd.id) }
|
||||
.firstOrNull()
|
||||
|
||||
val portMappings = foundContainer?.ports?.map {
|
||||
(it.privatePort ?: 0) to (it.publicPort ?: 0)
|
||||
}?.toMap()?.toMap()
|
||||
?: portsToOpen.map { it to it }.toMap()
|
||||
|
||||
|
||||
|
||||
return CompletableFuture.completedFuture(("localhost") to portMappings)
|
||||
}
|
||||
|
||||
private fun buildDockerEnv(env: Map<String, String>?) =
|
||||
(env ?: emptyMap()).entries.map { (key, value) -> "$key=$value" }.toList()
|
||||
|
||||
override fun getExpectedFQDN(instanceName: String): String {
|
||||
return instanceName
|
||||
}
|
||||
|
||||
private fun setupNetwork(): String {
|
||||
val createLocalDockerClient = DockerUtils.createLocalDockerClient()
|
||||
val existingNetworks = createLocalDockerClient.listNetworksCmd().withNameFilter(context.safeNetworkName).exec()
|
||||
return if (existingNetworks.isNotEmpty()) {
|
||||
if (existingNetworks.size > 1) {
|
||||
throw IllegalStateException("Multiple local docker networks found with name ${context.safeNetworkName}")
|
||||
} else {
|
||||
LOG.info("Found existing network with name: ${context.safeNetworkName} reusing")
|
||||
existingNetworks.first().id
|
||||
}
|
||||
} else {
|
||||
val result = createLocalDockerClient.createNetworkCmd().withName(context.safeNetworkName).exec()
|
||||
LOG.info("Created local docker network: ${result.id} with name: ${context.safeNetworkName}")
|
||||
result.id
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = LoggerFactory.getLogger(DockerInstantiator::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package net.corda.bootstrapper.containers.push
|
||||
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
interface ContainerPusher {
|
||||
fun pushContainerToImageRepository(localImageId: String,
|
||||
remoteImageName: String,
|
||||
networkName: String): CompletableFuture<String>
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package net.corda.bootstrapper.containers.push.azure
|
||||
|
||||
import com.github.dockerjava.api.async.ResultCallback
|
||||
import com.github.dockerjava.api.model.PushResponseItem
|
||||
import com.microsoft.azure.management.Azure
|
||||
import com.microsoft.azure.management.containerregistry.Registry
|
||||
import net.corda.bootstrapper.containers.push.ContainerPusher
|
||||
import net.corda.bootstrapper.containers.push.azure.RegistryLocator.Companion.parseCredentials
|
||||
import net.corda.bootstrapper.docker.DockerUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
|
||||
class AzureContainerPusher(private val azure: Azure, private val azureRegistry: Registry) : ContainerPusher {
|
||||
|
||||
|
||||
override fun pushContainerToImageRepository(localImageId: String,
|
||||
remoteImageName: String,
|
||||
networkName: String): CompletableFuture<String> {
|
||||
|
||||
|
||||
val (registryUser, registryPassword) = azureRegistry.parseCredentials()
|
||||
val dockerClient = DockerUtils.createDockerClient(
|
||||
azureRegistry.loginServerUrl(),
|
||||
registryUser,
|
||||
registryPassword)
|
||||
|
||||
val privateRepoUrl = "${azureRegistry.loginServerUrl()}/$remoteImageName".toLowerCase()
|
||||
dockerClient.tagImageCmd(localImageId, privateRepoUrl, networkName).exec()
|
||||
val result = CompletableFuture<String>()
|
||||
dockerClient.pushImageCmd("$privateRepoUrl:$networkName")
|
||||
.withAuthConfig(dockerClient.authConfig())
|
||||
.exec(object : ResultCallback<PushResponseItem> {
|
||||
override fun onComplete() {
|
||||
LOG.info("completed PUSH image: $localImageId to registryURL: $privateRepoUrl:$networkName")
|
||||
result.complete("$privateRepoUrl:$networkName")
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
}
|
||||
|
||||
override fun onNext(`object`: PushResponseItem) {
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable?) {
|
||||
result.completeExceptionally(throwable)
|
||||
}
|
||||
|
||||
override fun onStart(closeable: Closeable?) {
|
||||
LOG.info("starting PUSH image: $localImageId to registryURL: $privateRepoUrl:$networkName")
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = LoggerFactory.getLogger(AzureContainerPusher::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
package net.corda.bootstrapper.containers.push.azure
|
||||
|
||||
import com.microsoft.azure.management.Azure
|
||||
import com.microsoft.azure.management.containerregistry.AccessKeyType
|
||||
import com.microsoft.azure.management.containerregistry.Registry
|
||||
import com.microsoft.azure.management.resources.ResourceGroup
|
||||
import net.corda.bootstrapper.Constants.Companion.restFriendlyName
|
||||
import net.corda.bootstrapper.containers.instance.azure.AzureInstantiator
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class RegistryLocator(private val azure: Azure,
|
||||
private val resourceGroup: ResourceGroup) {
|
||||
|
||||
|
||||
val registry: Registry = locateRegistry()
|
||||
|
||||
|
||||
private fun locateRegistry(): Registry {
|
||||
LOG.info("Attempting to find existing registry with name: ${resourceGroup.restFriendlyName()}")
|
||||
val found = azure.containerRegistries().getByResourceGroup(resourceGroup.name(), resourceGroup.restFriendlyName())
|
||||
|
||||
if (found == null) {
|
||||
LOG.info("Did not find existing container registry - creating new registry with name ${resourceGroup.restFriendlyName()}")
|
||||
return azure.containerRegistries()
|
||||
.define(resourceGroup.restFriendlyName())
|
||||
.withRegion(resourceGroup.region().name())
|
||||
.withExistingResourceGroup(resourceGroup)
|
||||
.withBasicSku()
|
||||
.withRegistryNameAsAdminUser()
|
||||
.create()
|
||||
|
||||
} else {
|
||||
LOG.info("found existing registry with name: ${resourceGroup.restFriendlyName()} reusing")
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun Registry.parseCredentials(): Pair<String, String> {
|
||||
val credentials = this.credentials
|
||||
return credentials.username() to
|
||||
(credentials.accessKeys()[AccessKeyType.PRIMARY]
|
||||
?: throw IllegalStateException("no registry password found"))
|
||||
}
|
||||
|
||||
val LOG = LoggerFactory.getLogger(AzureInstantiator::class.java)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,15 @@
|
||||
package net.corda.bootstrapper.containers.push.docker
|
||||
|
||||
import net.corda.bootstrapper.containers.push.ContainerPusher
|
||||
import net.corda.bootstrapper.docker.DockerUtils
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class DockerContainerPusher : ContainerPusher {
|
||||
|
||||
|
||||
override fun pushContainerToImageRepository(localImageId: String, remoteImageName: String, networkName: String): CompletableFuture<String> {
|
||||
val dockerClient = DockerUtils.createLocalDockerClient()
|
||||
dockerClient.tagImageCmd(localImageId, remoteImageName, networkName).withForce().exec()
|
||||
return CompletableFuture.completedFuture("$remoteImageName:$networkName")
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.corda.bootstrapper.context
|
||||
|
||||
import net.corda.bootstrapper.Constants
|
||||
import net.corda.bootstrapper.backends.Backend
|
||||
import net.corda.bootstrapper.nodes.NodeInstanceRequest
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class Context(val networkName: String, val backendType: Backend.BackendType, backendOptions: Map<String, String> = emptyMap()) {
|
||||
|
||||
|
||||
@Volatile
|
||||
var safeNetworkName: String = networkName.replace(Constants.ALPHA_NUMERIC_ONLY_REGEX, "").toLowerCase()
|
||||
|
||||
@Volatile
|
||||
var nodes: MutableMap<String, ConcurrentHashSet<PersistableNodeInstance>> = ConcurrentHashMap()
|
||||
|
||||
@Volatile
|
||||
var networkInitiated: Boolean = false
|
||||
|
||||
@Volatile
|
||||
var extraParams = ConcurrentHashMap<String, String>(backendOptions)
|
||||
|
||||
private fun registerNode(name: String, nodeInstanceRequest: NodeInstanceRequest) {
|
||||
nodes.computeIfAbsent(name, { _ -> ConcurrentHashSet() }).add(nodeInstanceRequest.toPersistable())
|
||||
}
|
||||
|
||||
fun registerNode(request: NodeInstanceRequest) {
|
||||
registerNode(request.name, request)
|
||||
}
|
||||
|
||||
|
||||
data class PersistableNodeInstance(
|
||||
val groupName: String,
|
||||
val groupX500: CordaX500Name?,
|
||||
val instanceName: String,
|
||||
val instanceX500: String,
|
||||
val localImageId: String?,
|
||||
val remoteImageName: String,
|
||||
val rpcPort: Int?,
|
||||
val fqdn: String,
|
||||
val rpcUser: String,
|
||||
val rpcPassword: String)
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromInstanceRequest(nodeInstanceRequest: NodeInstanceRequest): PersistableNodeInstance {
|
||||
return PersistableNodeInstance(
|
||||
nodeInstanceRequest.name,
|
||||
nodeInstanceRequest.nodeConfig.myLegalName,
|
||||
nodeInstanceRequest.nodeInstanceName,
|
||||
nodeInstanceRequest.actualX500,
|
||||
nodeInstanceRequest.localImageId,
|
||||
nodeInstanceRequest.remoteImageName,
|
||||
nodeInstanceRequest.nodeConfig.rpcOptions.address!!.port,
|
||||
nodeInstanceRequest.expectedFqName,
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun NodeInstanceRequest.toPersistable(): PersistableNodeInstance {
|
||||
return fromInstanceRequest(this)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
package net.corda.bootstrapper.docker
|
||||
|
||||
import com.github.dockerjava.api.DockerClient
|
||||
import com.github.dockerjava.core.DefaultDockerClientConfig
|
||||
import com.github.dockerjava.core.DockerClientBuilder
|
||||
import com.github.dockerjava.core.DockerClientConfig
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
|
||||
object DockerUtils {
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun createDockerClient(registryServerUrl: String, username: String, password: String): DockerClient {
|
||||
return DockerClientBuilder.getInstance(createDockerClientConfig(registryServerUrl, username, password))
|
||||
.build()
|
||||
}
|
||||
|
||||
fun createLocalDockerClient(): DockerClient {
|
||||
return if (SystemUtils.IS_OS_WINDOWS) {
|
||||
DockerClientBuilder.getInstance("tcp://127.0.0.1:2375").build()
|
||||
} else {
|
||||
DockerClientBuilder.getInstance().build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDockerClientConfig(registryServerUrl: String, username: String, password: String): DockerClientConfig {
|
||||
return DefaultDockerClientConfig.createDefaultConfigBuilder()
|
||||
.withDockerTlsVerify(false)
|
||||
.withRegistryUrl(registryServerUrl)
|
||||
.withRegistryUsername(username)
|
||||
.withRegistryPassword(password)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,323 @@
|
||||
package net.corda.bootstrapper.gui
|
||||
|
||||
import com.microsoft.azure.management.resources.fluentcore.arm.Region
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.collections.transformation.SortedList
|
||||
import javafx.event.EventHandler
|
||||
import javafx.scene.control.ChoiceDialog
|
||||
import javafx.scene.control.TableView.CONSTRAINED_RESIZE_POLICY
|
||||
import javafx.scene.control.TextInputDialog
|
||||
import javafx.scene.input.MouseEvent
|
||||
import javafx.scene.layout.Priority
|
||||
import javafx.stage.DirectoryChooser
|
||||
import net.corda.bootstrapper.Constants
|
||||
import net.corda.bootstrapper.GuiUtils
|
||||
import net.corda.bootstrapper.NetworkBuilder
|
||||
import net.corda.bootstrapper.backends.Backend
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import net.corda.bootstrapper.nodes.*
|
||||
import net.corda.bootstrapper.notaries.NotaryFinder
|
||||
import org.apache.commons.lang3.RandomStringUtils
|
||||
import tornadofx.*
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class BootstrapperView : View("Network Bootstrapper") {
|
||||
|
||||
val YAML_MAPPER = Constants.getContextMapper()
|
||||
|
||||
|
||||
val controller: State by inject()
|
||||
|
||||
val textarea = textarea {
|
||||
maxWidth = Double.MAX_VALUE
|
||||
maxHeight = Double.MAX_VALUE
|
||||
}
|
||||
|
||||
override val root = vbox {
|
||||
|
||||
menubar {
|
||||
menu("File") {
|
||||
item("Open") {
|
||||
action {
|
||||
selectNodeDirectory().thenAcceptAsync({ (notaries: List<FoundNode>, nodes: List<FoundNode>) ->
|
||||
controller.nodes(nodes)
|
||||
controller.notaries(notaries)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
item("Build") {
|
||||
enableWhen(controller.baseDir.isNotNull)
|
||||
action {
|
||||
controller.clear()
|
||||
val availableBackends = getAvailableBackends()
|
||||
val backend = ChoiceDialog<Backend.BackendType>(availableBackends.first(), availableBackends).showAndWait()
|
||||
var networkName = "gui-network"
|
||||
backend.ifPresent { selectedBackEnd ->
|
||||
|
||||
val backendParams = when (selectedBackEnd) {
|
||||
Backend.BackendType.LOCAL_DOCKER -> {
|
||||
|
||||
emptyMap<String, String>()
|
||||
}
|
||||
Backend.BackendType.AZURE -> {
|
||||
val defaultName = RandomStringUtils.randomAlphabetic(4) + "-network"
|
||||
val textInputDialog = TextInputDialog(defaultName)
|
||||
textInputDialog.title = "Choose Network Name"
|
||||
networkName = textInputDialog.showAndWait().orElseGet { defaultName }
|
||||
mapOf(Constants.REGION_ARG_NAME to ChoiceDialog<Region>(Region.EUROPE_WEST, Region.values().toList().sortedBy { it.name() }).showAndWait().get().name())
|
||||
}
|
||||
}
|
||||
|
||||
val nodeCount = controller.foundNodes.map { it.id to it.count }.toMap()
|
||||
val result = NetworkBuilder.instance()
|
||||
.withBasedir(controller.baseDir.get())
|
||||
.withNetworkName(networkName)
|
||||
.onNodeBuild(controller::addBuiltNode)
|
||||
.onNodePushed(controller::addPushedNode)
|
||||
.onNodeInstance(controller::addInstance)
|
||||
.withBackend(selectedBackEnd)
|
||||
.withNodeCounts(nodeCount)
|
||||
.withBackendOptions(backendParams)
|
||||
.build()
|
||||
result.handle { v, t ->
|
||||
runLater {
|
||||
if (t != null) {
|
||||
GuiUtils.showException("Failed to build network", "Failure due to", t)
|
||||
} else {
|
||||
controller.networkContext.set(v.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item("Add Node") {
|
||||
enableWhen(controller.networkContext.isNotNull)
|
||||
action {
|
||||
val foundNodes = controller.foundNodes.map { it.id }
|
||||
val nodeToAdd = ChoiceDialog<String>(foundNodes.first(), *foundNodes.toTypedArray()).showAndWait()
|
||||
val context = controller.networkContext.value
|
||||
nodeToAdd.ifPresent { node ->
|
||||
runLater {
|
||||
val (_, instantiator, _) = Backend.fromContext(
|
||||
context,
|
||||
File(controller.baseDir.get(), Constants.BOOTSTRAPPER_DIR_NAME))
|
||||
val nodeAdder = NodeAdder(context, NodeInstantiator(instantiator, context))
|
||||
nodeAdder.addNode(context, node).handleAsync { instanceInfo, t ->
|
||||
t?.let {
|
||||
GuiUtils.showException("Failed", "Failed to add node", it)
|
||||
}
|
||||
instanceInfo?.let {
|
||||
runLater {
|
||||
controller.addInstance(NodeInstanceTableEntry(
|
||||
it.groupId,
|
||||
it.instanceName,
|
||||
it.instanceAddress,
|
||||
it.reachableAddress,
|
||||
it.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
|
||||
it.portMapping[Constants.NODE_SSHD_PORT]
|
||||
?: Constants.NODE_SSHD_PORT))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hbox {
|
||||
vbox {
|
||||
label("Nodes to build")
|
||||
val foundNodesTable = tableview(controller.foundNodes) {
|
||||
readonlyColumn("ID", FoundNodeTableEntry::id)
|
||||
column("Count", FoundNodeTableEntry::count).makeEditable()
|
||||
vgrow = Priority.ALWAYS
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
foundNodesTable.columnResizePolicy = CONSTRAINED_RESIZE_POLICY
|
||||
label("Notaries to build")
|
||||
val notaryListView = listview(controller.foundNotaries) {
|
||||
vgrow = Priority.ALWAYS
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
notaryListView.cellFormat { text = it.name }
|
||||
vgrow = Priority.ALWAYS
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
|
||||
vbox {
|
||||
|
||||
label("Built Nodes")
|
||||
tableview(controller.builtNodes) {
|
||||
readonlyColumn("ID", BuiltNodeTableEntry::id)
|
||||
readonlyColumn("LocalImageId", BuiltNodeTableEntry::localImageId)
|
||||
columnResizePolicy = CONSTRAINED_RESIZE_POLICY
|
||||
vgrow = Priority.ALWAYS
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
|
||||
|
||||
label("Pushed Nodes")
|
||||
tableview(controller.pushedNodes) {
|
||||
readonlyColumn("ID", PushedNode::name)
|
||||
readonlyColumn("RemoteImageId", PushedNode::remoteImageName)
|
||||
columnResizePolicy = CONSTRAINED_RESIZE_POLICY
|
||||
vgrow = Priority.ALWAYS
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
vgrow = Priority.ALWAYS
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
|
||||
borderpane {
|
||||
top = vbox {
|
||||
label("Instances")
|
||||
tableview(controller.nodeInstances) {
|
||||
onMouseClicked = EventHandler<MouseEvent> { _ ->
|
||||
textarea.text = YAML_MAPPER.writeValueAsString(selectionModel.selectedItem)
|
||||
}
|
||||
readonlyColumn("ID", NodeInstanceTableEntry::id)
|
||||
readonlyColumn("InstanceId", NodeInstanceTableEntry::nodeInstanceName)
|
||||
readonlyColumn("Address", NodeInstanceTableEntry::address)
|
||||
columnResizePolicy = CONSTRAINED_RESIZE_POLICY
|
||||
}
|
||||
}
|
||||
center = textarea
|
||||
vgrow = Priority.ALWAYS
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
|
||||
vgrow = Priority.ALWAYS
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getAvailableBackends(): List<Backend.BackendType> {
|
||||
return Backend.BackendType.values().toMutableList();
|
||||
}
|
||||
|
||||
|
||||
fun selectNodeDirectory(): CompletableFuture<Pair<List<FoundNode>, List<FoundNode>>> {
|
||||
val fileChooser = DirectoryChooser();
|
||||
fileChooser.initialDirectory = File(System.getProperty("user.home"))
|
||||
val file = fileChooser.showDialog(null)
|
||||
controller.baseDir.set(file)
|
||||
return processSelectedDirectory(file)
|
||||
}
|
||||
|
||||
|
||||
fun processSelectedDirectory(dir: File): CompletableFuture<Pair<List<FoundNode>, List<FoundNode>>> {
|
||||
val foundNodes = CompletableFuture.supplyAsync {
|
||||
val nodeFinder = NodeFinder(dir)
|
||||
nodeFinder.findNodes()
|
||||
}
|
||||
val foundNotaries = CompletableFuture.supplyAsync {
|
||||
val notaryFinder = NotaryFinder(dir)
|
||||
notaryFinder.findNotaries()
|
||||
}
|
||||
return foundNodes.thenCombine(foundNotaries) { nodes, notaries ->
|
||||
notaries to nodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class State : Controller() {
|
||||
|
||||
val foundNodes = Collections.synchronizedList(ArrayList<FoundNodeTableEntry>()).observable()
|
||||
val builtNodes = Collections.synchronizedList(ArrayList<BuiltNodeTableEntry>()).observable()
|
||||
val pushedNodes = Collections.synchronizedList(ArrayList<PushedNode>()).observable()
|
||||
|
||||
private val backingUnsortedInstances = Collections.synchronizedList(ArrayList<NodeInstanceTableEntry>()).observable()
|
||||
val nodeInstances = SortedList(backingUnsortedInstances, COMPARATOR)
|
||||
|
||||
val foundNotaries = Collections.synchronizedList(ArrayList<FoundNode>()).observable()
|
||||
val networkContext = SimpleObjectProperty<Context>(null)
|
||||
|
||||
fun clear() {
|
||||
builtNodes.clear()
|
||||
pushedNodes.clear()
|
||||
backingUnsortedInstances.clear()
|
||||
networkContext.set(null)
|
||||
}
|
||||
|
||||
fun nodes(nodes: List<FoundNode>) {
|
||||
foundNodes.clear()
|
||||
nodes.forEach { addFoundNode(it) }
|
||||
}
|
||||
|
||||
fun notaries(notaries: List<FoundNode>) {
|
||||
foundNotaries.clear()
|
||||
notaries.forEach { runLater { foundNotaries.add(it) } }
|
||||
}
|
||||
|
||||
var baseDir = SimpleObjectProperty<File>(null)
|
||||
|
||||
|
||||
fun addFoundNode(foundNode: FoundNode) {
|
||||
runLater {
|
||||
foundNodes.add(FoundNodeTableEntry(foundNode.name))
|
||||
}
|
||||
}
|
||||
|
||||
fun addBuiltNode(builtNode: BuiltNode) {
|
||||
runLater {
|
||||
builtNodes.add(BuiltNodeTableEntry(builtNode.name, builtNode.localImageId))
|
||||
}
|
||||
}
|
||||
|
||||
fun addPushedNode(pushedNode: PushedNode) {
|
||||
runLater {
|
||||
pushedNodes.add(pushedNode)
|
||||
}
|
||||
}
|
||||
|
||||
fun addInstance(nodeInstance: NodeInstance) {
|
||||
runLater {
|
||||
backingUnsortedInstances.add(NodeInstanceTableEntry(
|
||||
nodeInstance.name,
|
||||
nodeInstance.nodeInstanceName,
|
||||
nodeInstance.expectedFqName,
|
||||
nodeInstance.reachableAddress,
|
||||
nodeInstance.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
|
||||
nodeInstance.portMapping[Constants.NODE_SSHD_PORT] ?: Constants.NODE_SSHD_PORT))
|
||||
}
|
||||
}
|
||||
|
||||
fun addInstance(nodeInstance: NodeInstanceTableEntry) {
|
||||
runLater {
|
||||
backingUnsortedInstances.add(nodeInstance)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val COMPARATOR: (NodeInstanceTableEntry, NodeInstanceTableEntry) -> Int = { o1, o2 ->
|
||||
if (o1.id == (o2.id)) {
|
||||
o1.nodeInstanceName.compareTo(o2.nodeInstanceName)
|
||||
} else {
|
||||
o1.id.compareTo(o2.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
data class FoundNodeTableEntry(val id: String,
|
||||
@Volatile var count: Int = 1)
|
||||
|
||||
data class BuiltNodeTableEntry(val id: String, val localImageId: String)
|
||||
|
||||
data class NodeInstanceTableEntry(val id: String,
|
||||
val nodeInstanceName: String,
|
||||
val address: String,
|
||||
val locallyReachableAddress: String,
|
||||
val rpcPort: Int,
|
||||
val sshPort: Int)
|
@ -0,0 +1,11 @@
|
||||
package net.corda.bootstrapper.gui
|
||||
|
||||
import javafx.application.Application
|
||||
import tornadofx.App
|
||||
|
||||
class Gui : App(BootstrapperView::class) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) = Application.launch(Gui::class.java, *args)
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import java.io.File
|
||||
|
||||
open class BuiltNode(configFile: File, baseDirectory: File,
|
||||
copiedNodeConfig: File, copiedNodeDir: File,
|
||||
val nodeConfig: NodeConfiguration, val localImageId: String) : CopiedNode(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir) {
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "BuiltNode(" +
|
||||
"nodeConfig=$nodeConfig," +
|
||||
"localImageId='$localImageId'" +
|
||||
")" +
|
||||
" ${super.toString()}"
|
||||
}
|
||||
|
||||
fun toPushedNode(remoteImageName: String): PushedNode {
|
||||
return PushedNode(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, localImageId, remoteImageName)
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import java.io.File
|
||||
|
||||
open class CopiedNode(configFile: File, baseDirectory: File,
|
||||
open val copiedNodeConfig: File, open val copiedNodeDir: File) :
|
||||
FoundNode(configFile, baseDirectory) {
|
||||
|
||||
constructor(foundNode: FoundNode, copiedNodeConfig: File, copiedNodeDir: File) : this(
|
||||
foundNode.configFile, foundNode.baseDirectory, copiedNodeConfig, copiedNodeDir
|
||||
)
|
||||
|
||||
operator fun component4(): File {
|
||||
return copiedNodeDir;
|
||||
}
|
||||
|
||||
operator fun component5(): File {
|
||||
return copiedNodeConfig;
|
||||
}
|
||||
|
||||
|
||||
fun builtNode(nodeConfig: NodeConfiguration, imageId: String): BuiltNode {
|
||||
return BuiltNode(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, imageId)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "CopiedNode(" +
|
||||
"copiedNodeConfig=$copiedNodeConfig," +
|
||||
"copiedNodeDir=$copiedNodeDir" +
|
||||
")" +
|
||||
" ${super.toString()}"
|
||||
}
|
||||
|
||||
fun toBuiltNode(nodeConfig: NodeConfiguration, localImageId: String): BuiltNode {
|
||||
return BuiltNode(this.configFile, this.baseDirectory, this.copiedNodeConfig, this.copiedNodeDir, nodeConfig, localImageId)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import java.io.File
|
||||
|
||||
open class FoundNode(open val configFile: File,
|
||||
open val baseDirectory: File = configFile.parentFile,
|
||||
val name: String = configFile.parentFile.name.toLowerCase().replace(net.corda.bootstrapper.Constants.ALPHA_NUMERIC_ONLY_REGEX, "")) {
|
||||
|
||||
|
||||
operator fun component1(): File {
|
||||
return baseDirectory;
|
||||
}
|
||||
|
||||
operator fun component2(): File {
|
||||
return configFile;
|
||||
}
|
||||
|
||||
operator fun component3(): String {
|
||||
return name;
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FoundNode
|
||||
|
||||
if (configFile != other.configFile) return false
|
||||
if (baseDirectory != other.baseDirectory) return false
|
||||
if (name != other.name) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = configFile.hashCode()
|
||||
result = 31 * result + baseDirectory.hashCode()
|
||||
result = 31 * result + name.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "FoundNode(name='$name', configFile=$configFile, baseDirectory=$baseDirectory)"
|
||||
}
|
||||
|
||||
fun toCopiedNode(copiedNodeConfig: File, copiedNodeDir: File): CopiedNode {
|
||||
return CopiedNode(this.configFile, this.baseDirectory, copiedNodeConfig, copiedNodeDir)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import net.corda.bootstrapper.containers.instance.InstanceInfo
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class NodeAdder(val context: Context,
|
||||
val nodeInstantiator: NodeInstantiator) {
|
||||
|
||||
fun addNode(context: Context, nodeGroupName: String): CompletableFuture<InstanceInfo> {
|
||||
return synchronized(context) {
|
||||
val nodeGroup = context.nodes[nodeGroupName]!!
|
||||
val nodeInfo = nodeGroup.iterator().next()
|
||||
val currentNodeSize = nodeGroup.size
|
||||
val newInstanceX500 = nodeInfo.groupX500!!.copy(commonName = nodeInfo.groupX500.commonName + (currentNodeSize)).toString()
|
||||
val newInstanceName = nodeGroupName + (currentNodeSize)
|
||||
val nextNodeInfo = nodeInfo.copy(
|
||||
instanceX500 = newInstanceX500,
|
||||
instanceName = newInstanceName,
|
||||
fqdn = nodeInstantiator.expectedFqdn(newInstanceName)
|
||||
)
|
||||
nodeGroup.add(nextNodeInfo)
|
||||
nodeInstantiator.instantiateNodeInstance(nextNodeInfo)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import com.github.dockerjava.core.command.BuildImageResultCallback
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import net.corda.bootstrapper.docker.DockerUtils
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.parseAsNodeConfiguration
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
open class NodeBuilder {
|
||||
|
||||
companion object {
|
||||
val LOG = LoggerFactory.getLogger(NodeBuilder::class.java)
|
||||
}
|
||||
|
||||
fun buildNode(copiedNode: CopiedNode): BuiltNode {
|
||||
val localDockerClient = DockerUtils.createLocalDockerClient()
|
||||
val copiedNodeConfig = copiedNode.copiedNodeConfig
|
||||
val nodeDir = copiedNodeConfig.parentFile
|
||||
if (!copiedNodeConfig.exists()) {
|
||||
throw IllegalStateException("There is no nodeConfig for dir: " + copiedNodeConfig)
|
||||
}
|
||||
val nodeConfig = ConfigFactory.parseFile(copiedNodeConfig)
|
||||
LOG.info("starting to build docker image for: " + nodeDir)
|
||||
val nodeImageId = localDockerClient.buildImageCmd()
|
||||
.withDockerfile(File(nodeDir, "Dockerfile"))
|
||||
.withBaseDirectory(nodeDir)
|
||||
.exec(BuildImageResultCallback()).awaitImageId()
|
||||
LOG.info("finished building docker image for: $nodeDir with id: $nodeImageId")
|
||||
val config = nodeConfig.parseAsNodeConfigWithFallback(ConfigFactory.parseFile(copiedNode.configFile))
|
||||
return copiedNode.builtNode(config, nodeImageId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun Config.parseAsNodeConfigWithFallback(preCopyConfig: Config): NodeConfiguration {
|
||||
val nodeConfig = this
|
||||
.withValue("baseDirectory", ConfigValueFactory.fromAnyRef(""))
|
||||
.withFallback(ConfigFactory.parseResources("reference.conf"))
|
||||
.withFallback(preCopyConfig)
|
||||
.resolve()
|
||||
return nodeConfig.parseAsNodeConfiguration()
|
||||
}
|
||||
|
@ -0,0 +1,101 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import com.typesafe.config.ConfigValue
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
open class NodeCopier(private val cacheDir: File) {
|
||||
|
||||
|
||||
fun copyNode(foundNode: FoundNode): CopiedNode {
|
||||
val nodeCacheDir = File(cacheDir, foundNode.baseDirectory.name)
|
||||
nodeCacheDir.deleteRecursively()
|
||||
LOG.info("copying: ${foundNode.baseDirectory} to $nodeCacheDir")
|
||||
foundNode.baseDirectory.copyRecursively(nodeCacheDir, overwrite = true)
|
||||
copyBootstrapperFiles(nodeCacheDir)
|
||||
val configInCacheDir = File(nodeCacheDir, "node.conf")
|
||||
LOG.info("Applying precanned config " + configInCacheDir)
|
||||
val rpcSettings = getDefaultRpcSettings()
|
||||
val sshSettings = getDefaultSshSettings();
|
||||
mergeConfigs(configInCacheDir, rpcSettings, sshSettings)
|
||||
return CopiedNode(foundNode, configInCacheDir, nodeCacheDir)
|
||||
}
|
||||
|
||||
|
||||
fun copyBootstrapperFiles(nodeCacheDir: File) {
|
||||
this.javaClass.classLoader.getResourceAsStream("node-Dockerfile").use { nodeDockerFileInStream ->
|
||||
val nodeDockerFile = File(nodeCacheDir, "Dockerfile")
|
||||
nodeDockerFile.outputStream().use { nodeDockerFileOutStream ->
|
||||
nodeDockerFileInStream.copyTo(nodeDockerFileOutStream)
|
||||
}
|
||||
}
|
||||
|
||||
this.javaClass.classLoader.getResourceAsStream("run-corda-node.sh").use { nodeRunScriptInStream ->
|
||||
val nodeRunScriptFile = File(nodeCacheDir, "run-corda.sh")
|
||||
nodeRunScriptFile.outputStream().use { nodeDockerFileOutStream ->
|
||||
nodeRunScriptInStream.copyTo(nodeDockerFileOutStream)
|
||||
}
|
||||
}
|
||||
|
||||
this.javaClass.classLoader.getResourceAsStream("node_info_watcher.sh").use { nodeRunScriptInStream ->
|
||||
val nodeInfoWatcherFile = File(nodeCacheDir, "node_info_watcher.sh")
|
||||
nodeInfoWatcherFile.outputStream().use { nodeDockerFileOutStream ->
|
||||
nodeRunScriptInStream.copyTo(nodeDockerFileOutStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getDefaultRpcSettings(): ConfigValue {
|
||||
return javaClass
|
||||
.classLoader
|
||||
.getResourceAsStream("rpc-settings.conf")
|
||||
.reader().use {
|
||||
ConfigFactory.parseReader(it)
|
||||
}.getValue("rpcSettings")
|
||||
}
|
||||
|
||||
internal fun getDefaultSshSettings(): ConfigValue {
|
||||
return javaClass
|
||||
.classLoader
|
||||
.getResourceAsStream("ssh.conf")
|
||||
.reader().use {
|
||||
ConfigFactory.parseReader(it)
|
||||
}.getValue("sshd")
|
||||
}
|
||||
|
||||
internal fun mergeConfigs(configInCacheDir: File,
|
||||
rpcSettings: ConfigValue,
|
||||
sshSettings: ConfigValue,
|
||||
mergeMode: Mode = Mode.NODE) {
|
||||
var trimmedConfig = ConfigFactory.parseFile(configInCacheDir)
|
||||
.withoutPath("compatibilityZoneURL")
|
||||
.withValue("rpcSettings", rpcSettings)
|
||||
.withValue("sshd", sshSettings)
|
||||
|
||||
if (mergeMode == Mode.NODE) {
|
||||
trimmedConfig = trimmedConfig.withoutPath("p2pAddress")
|
||||
}
|
||||
|
||||
configInCacheDir.outputStream().use {
|
||||
trimmedConfig.root().render(ConfigRenderOptions
|
||||
.defaults()
|
||||
.setOriginComments(false)
|
||||
.setComments(false)
|
||||
.setFormatted(true)
|
||||
.setJson(false)).byteInputStream().copyTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal enum class Mode {
|
||||
NOTARY, NODE
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
val LOG = LoggerFactory.getLogger(NodeCopier::class.java)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.bootstrapper.Constants
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
class NodeFinder(private val scratchDir: File) {
|
||||
|
||||
|
||||
fun findNodes(): List<FoundNode> {
|
||||
return scratchDir.walkBottomUp().filter { it.name == "node.conf" && !it.absolutePath.contains(Constants.BOOTSTRAPPER_DIR_NAME) }.map {
|
||||
try {
|
||||
ConfigFactory.parseFile(it) to it
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
.filter { !it.first.hasPath("notary") }
|
||||
.map { (nodeConfig, nodeConfigFile) ->
|
||||
LOG.info("We've found a node with name: ${nodeConfigFile.parentFile.name}")
|
||||
FoundNode(nodeConfigFile, nodeConfigFile.parentFile)
|
||||
}.toList()
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = LoggerFactory.getLogger(NodeFinder::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import java.io.File
|
||||
|
||||
class NodeInstance(configFile: File,
|
||||
baseDirectory: File,
|
||||
copiedNodeConfig: File,
|
||||
copiedNodeDir: File,
|
||||
nodeConfig: NodeConfiguration,
|
||||
localImageId: String,
|
||||
remoteImageName: String,
|
||||
nodeInstanceName: String,
|
||||
actualX500: String,
|
||||
expectedFqName: String,
|
||||
val reachableAddress: String,
|
||||
val portMapping: Map<Int, Int>) :
|
||||
NodeInstanceRequest(
|
||||
configFile,
|
||||
baseDirectory,
|
||||
copiedNodeConfig,
|
||||
copiedNodeDir,
|
||||
nodeConfig,
|
||||
localImageId,
|
||||
remoteImageName,
|
||||
nodeInstanceName,
|
||||
actualX500,
|
||||
expectedFqName
|
||||
) {
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import java.io.File
|
||||
|
||||
open class NodeInstanceRequest(configFile: File, baseDirectory: File,
|
||||
copiedNodeConfig: File, copiedNodeDir: File,
|
||||
nodeConfig: NodeConfiguration, localImageId: String, remoteImageName: String,
|
||||
internal val nodeInstanceName: String,
|
||||
internal val actualX500: String,
|
||||
internal val expectedFqName: String) : PushedNode(
|
||||
configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, localImageId, remoteImageName
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
return "NodeInstanceRequest(nodeInstanceName='$nodeInstanceName', actualX500='$actualX500', expectedFqName='$expectedFqName') ${super.toString()}"
|
||||
}
|
||||
|
||||
fun toNodeInstance(reachableAddress: String, portMapping: Map<Int, Int>): NodeInstance {
|
||||
return NodeInstance(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, localImageId, remoteImageName, nodeInstanceName, actualX500, expectedFqName, reachableAddress, portMapping)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import net.corda.bootstrapper.Constants
|
||||
import net.corda.bootstrapper.containers.instance.InstanceInfo
|
||||
import net.corda.bootstrapper.containers.instance.Instantiator
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class NodeInstantiator(val instantiator: Instantiator,
|
||||
val context: Context) {
|
||||
|
||||
|
||||
fun createInstanceRequests(pushedNode: PushedNode, nodeCount: Map<FoundNode, Int>): List<NodeInstanceRequest> {
|
||||
|
||||
val namedMap = nodeCount.map { it.key.name.toLowerCase() to it.value }.toMap()
|
||||
|
||||
return (0 until (namedMap[pushedNode.name.toLowerCase()] ?: 1)).map { i ->
|
||||
createInstanceRequest(pushedNode, i)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInstanceRequest(node: PushedNode, i: Int): NodeInstanceRequest {
|
||||
val nodeInstanceName = node.name + i
|
||||
val expectedName = instantiator.getExpectedFQDN(nodeInstanceName)
|
||||
return node.toNodeInstanceRequest(nodeInstanceName, buildX500(node.nodeConfig.myLegalName, i), expectedName)
|
||||
}
|
||||
|
||||
fun createInstanceRequest(node: PushedNode): NodeInstanceRequest {
|
||||
return createInstanceRequest(node, 0)
|
||||
}
|
||||
|
||||
|
||||
private fun buildX500(baseX500: CordaX500Name, i: Int): String {
|
||||
if (i == 0) {
|
||||
return baseX500.toString()
|
||||
}
|
||||
return baseX500.copy(commonName = (baseX500.commonName ?: "") + i).toString()
|
||||
}
|
||||
|
||||
fun instantiateNodeInstance(request: Context.PersistableNodeInstance): CompletableFuture<InstanceInfo> {
|
||||
return instantiateNodeInstance(request.remoteImageName, request.rpcPort!!, request.instanceName, request.fqdn, request.instanceX500).thenApplyAsync {
|
||||
InstanceInfo(request.groupName, request.instanceName, request.fqdn, it.first, it.second)
|
||||
}
|
||||
}
|
||||
|
||||
fun instantiateNodeInstance(request: NodeInstanceRequest): CompletableFuture<NodeInstance> {
|
||||
return instantiateNodeInstance(request.remoteImageName, request.nodeConfig.rpcOptions.address!!.port, request.nodeInstanceName, request.expectedFqName, request.actualX500)
|
||||
.thenApplyAsync { (reachableName, portMapping) ->
|
||||
request.toNodeInstance(reachableName, portMapping)
|
||||
}
|
||||
}
|
||||
|
||||
fun instantiateNotaryInstance(request: NodeInstanceRequest): CompletableFuture<NodeInstance> {
|
||||
return instantiateNotaryInstance(request.remoteImageName, request.nodeConfig.rpcOptions.address!!.port, request.nodeInstanceName, request.expectedFqName)
|
||||
.thenApplyAsync { (reachableName, portMapping) ->
|
||||
request.toNodeInstance(reachableName, portMapping)
|
||||
}
|
||||
}
|
||||
|
||||
private fun instantiateNotaryInstance(remoteImageName: String,
|
||||
rpcPort: Int,
|
||||
nodeInstanceName: String,
|
||||
expectedFqName: String): CompletableFuture<Pair<String, Map<Int, Int>>> {
|
||||
return instantiator.instantiateContainer(
|
||||
remoteImageName,
|
||||
listOf(Constants.NODE_P2P_PORT, Constants.NODE_RPC_PORT, Constants.NODE_SSHD_PORT),
|
||||
nodeInstanceName,
|
||||
mapOf("OUR_NAME" to expectedFqName,
|
||||
"OUR_PORT" to Constants.NODE_P2P_PORT.toString())
|
||||
)
|
||||
}
|
||||
|
||||
private fun instantiateNodeInstance(remoteImageName: String,
|
||||
rpcPort: Int,
|
||||
nodeInstanceName: String,
|
||||
expectedFqName: String,
|
||||
actualX500: String): CompletableFuture<Pair<String, Map<Int, Int>>> {
|
||||
|
||||
return instantiator.instantiateContainer(
|
||||
remoteImageName,
|
||||
listOf(Constants.NODE_P2P_PORT, Constants.NODE_RPC_PORT, Constants.NODE_SSHD_PORT),
|
||||
nodeInstanceName,
|
||||
mapOf("OUR_NAME" to expectedFqName,
|
||||
"OUR_PORT" to Constants.NODE_P2P_PORT.toString(),
|
||||
"X500" to actualX500)
|
||||
)
|
||||
}
|
||||
|
||||
fun expectedFqdn(newInstanceName: String): String {
|
||||
return instantiator.getExpectedFQDN(newInstanceName)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import net.corda.bootstrapper.containers.push.ContainerPusher
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class NodePusher(private val containerPusher: ContainerPusher,
|
||||
private val context: Context) {
|
||||
|
||||
|
||||
fun pushNode(builtNode: BuiltNode): CompletableFuture<PushedNode> {
|
||||
|
||||
val localImageId = builtNode.localImageId
|
||||
val nodeImageIdentifier = "node-${builtNode.name}"
|
||||
val nodeImageNameFuture = containerPusher.pushContainerToImageRepository(localImageId,
|
||||
nodeImageIdentifier, context.networkName)
|
||||
return nodeImageNameFuture.thenApplyAsync { imageName -> builtNode.toPushedNode(imageName) }
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package net.corda.bootstrapper.nodes
|
||||
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import java.io.File
|
||||
|
||||
open class PushedNode(configFile: File, baseDirectory: File,
|
||||
copiedNodeConfig: File, copiedNodeDir: File,
|
||||
nodeConfig: NodeConfiguration, localImageId: String, val remoteImageName: String) : BuiltNode(
|
||||
configFile,
|
||||
baseDirectory,
|
||||
copiedNodeConfig,
|
||||
copiedNodeDir,
|
||||
nodeConfig,
|
||||
localImageId
|
||||
) {
|
||||
fun toNodeInstanceRequest(nodeInstanceName: String, actualX500: String, expectedFqName: String): NodeInstanceRequest {
|
||||
return NodeInstanceRequest(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir, nodeConfig, localImageId, remoteImageName, nodeInstanceName, actualX500, expectedFqName)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "PushedNode(remoteImageName='$remoteImageName') ${super.toString()}"
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package net.corda.bootstrapper.notaries
|
||||
|
||||
import net.corda.bootstrapper.nodes.CopiedNode
|
||||
import java.io.File
|
||||
|
||||
class CopiedNotary(configFile: File, baseDirectory: File,
|
||||
copiedNodeConfig: File, copiedNodeDir: File, val nodeInfoFile: File) :
|
||||
CopiedNode(configFile, baseDirectory, copiedNodeConfig, copiedNodeDir) {
|
||||
}
|
||||
|
||||
|
||||
fun CopiedNode.toNotary(nodeInfoFile: File): CopiedNotary {
|
||||
return CopiedNotary(this.configFile, this.baseDirectory, this.copiedNodeConfig, this.copiedNodeDir, nodeInfoFile)
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.corda.bootstrapper.notaries
|
||||
|
||||
import net.corda.bootstrapper.nodes.CopiedNode
|
||||
import net.corda.bootstrapper.nodes.FoundNode
|
||||
import net.corda.bootstrapper.nodes.NodeCopier
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
class NotaryCopier(val cacheDir: File) : NodeCopier(cacheDir) {
|
||||
|
||||
fun copyNotary(foundNotary: FoundNode): CopiedNotary {
|
||||
val nodeCacheDir = File(cacheDir, foundNotary.baseDirectory.name)
|
||||
nodeCacheDir.deleteRecursively()
|
||||
LOG.info("copying: ${foundNotary.baseDirectory} to $nodeCacheDir")
|
||||
foundNotary.baseDirectory.copyRecursively(nodeCacheDir, overwrite = true)
|
||||
copyNotaryBootstrapperFiles(nodeCacheDir)
|
||||
val configInCacheDir = File(nodeCacheDir, "node.conf")
|
||||
LOG.info("Applying precanned config " + configInCacheDir)
|
||||
val rpcSettings = getDefaultRpcSettings()
|
||||
val sshSettings = getDefaultSshSettings();
|
||||
mergeConfigs(configInCacheDir, rpcSettings, sshSettings, Mode.NOTARY)
|
||||
val generatedNodeInfo = generateNodeInfo(nodeCacheDir)
|
||||
return CopiedNode(foundNotary, configInCacheDir, nodeCacheDir).toNotary(generatedNodeInfo)
|
||||
}
|
||||
|
||||
fun generateNodeInfo(dirToGenerateFrom: File): File {
|
||||
val nodeInfoGeneratorProcess = ProcessBuilder()
|
||||
.command(listOf("java", "-jar", "corda.jar", "--just-generate-node-info"))
|
||||
.directory(dirToGenerateFrom)
|
||||
.inheritIO()
|
||||
.start()
|
||||
|
||||
val exitCode = nodeInfoGeneratorProcess.waitFor()
|
||||
if (exitCode != 0) {
|
||||
throw IllegalStateException("Failed to generate nodeInfo for notary: " + dirToGenerateFrom)
|
||||
}
|
||||
val nodeInfoFile = dirToGenerateFrom.listFiles().filter { it.name.startsWith("nodeInfo") }.single()
|
||||
return nodeInfoFile;
|
||||
}
|
||||
|
||||
private fun copyNotaryBootstrapperFiles(nodeCacheDir: File) {
|
||||
this.javaClass.classLoader.getResourceAsStream("notary-Dockerfile").use { nodeDockerFileInStream ->
|
||||
val nodeDockerFile = File(nodeCacheDir, "Dockerfile")
|
||||
nodeDockerFile.outputStream().use { nodeDockerFileOutStream ->
|
||||
nodeDockerFileInStream.copyTo(nodeDockerFileOutStream)
|
||||
}
|
||||
}
|
||||
|
||||
this.javaClass.classLoader.getResourceAsStream("run-corda-notary.sh").use { nodeRunScriptInStream ->
|
||||
val nodeRunScriptFile = File(nodeCacheDir, "run-corda.sh")
|
||||
nodeRunScriptFile.outputStream().use { nodeDockerFileOutStream ->
|
||||
nodeRunScriptInStream.copyTo(nodeDockerFileOutStream)
|
||||
}
|
||||
}
|
||||
|
||||
this.javaClass.classLoader.getResourceAsStream("node_info_watcher.sh").use { nodeRunScriptInStream ->
|
||||
val nodeInfoWatcherFile = File(nodeCacheDir, "node_info_watcher.sh")
|
||||
nodeInfoWatcherFile.outputStream().use { nodeDockerFileOutStream ->
|
||||
nodeRunScriptInStream.copyTo(nodeDockerFileOutStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = LoggerFactory.getLogger(NotaryCopier::class.java)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package net.corda.bootstrapper.notaries
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.bootstrapper.Constants
|
||||
import net.corda.bootstrapper.nodes.FoundNode
|
||||
import java.io.File
|
||||
|
||||
class NotaryFinder(private val dirToSearch: File) {
|
||||
|
||||
fun findNotaries(): List<FoundNode> {
|
||||
return dirToSearch.walkBottomUp().filter { it.name == "node.conf" && !it.absolutePath.contains(Constants.BOOTSTRAPPER_DIR_NAME) }
|
||||
.map {
|
||||
try {
|
||||
ConfigFactory.parseFile(it) to it
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
.filter { it.first.hasPath("notary") }
|
||||
.map { (_, nodeConfigFile) ->
|
||||
FoundNode(nodeConfigFile)
|
||||
}.toList()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
package net.corda.bootstrapper.serialization
|
||||
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.serialization.internal.AMQP_P2P_CONTEXT
|
||||
import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT
|
||||
import net.corda.serialization.internal.SerializationFactoryImpl
|
||||
|
||||
|
||||
class SerializationEngine {
|
||||
companion object {
|
||||
fun init() {
|
||||
synchronized(this) {
|
||||
if (nodeSerializationEnv == null) {
|
||||
val classloader = this.javaClass.classLoader
|
||||
nodeSerializationEnv = SerializationEnvironmentImpl(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(AMQPServerSerializationScheme(emptyList()))
|
||||
},
|
||||
p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
|
||||
rpcServerContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
|
||||
storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader),
|
||||
checkpointContext = AMQP_P2P_CONTEXT.withClassLoader(classloader)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package net.corda.bootstrapper.volumes
|
||||
|
||||
import com.microsoft.azure.storage.file.CloudFile
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.bootstrapper.notaries.CopiedNotary
|
||||
import net.corda.bootstrapper.serialization.SerializationEngine
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||
import java.io.File
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
|
||||
interface Volume {
|
||||
fun notariesForNetworkParams(notaries: List<CopiedNotary>)
|
||||
|
||||
companion object {
|
||||
init {
|
||||
SerializationEngine.init()
|
||||
}
|
||||
|
||||
internal val networkMapCa = createDevNetworkMapCa(DEV_ROOT_CA)
|
||||
internal val networkMapCert: X509Certificate = networkMapCa.certificate
|
||||
internal val keyPair = networkMapCa.keyPair
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun CloudFile.uploadFromByteArray(array: ByteArray) {
|
||||
this.uploadFromByteArray(array, 0, array.size)
|
||||
}
|
||||
|
||||
|
||||
fun convertNodeIntoToNetworkParams(notaryFiles: List<Pair<File, File>>): NetworkParameters {
|
||||
val notaryInfos = notaryFiles.map { (configFile, nodeInfoFile) ->
|
||||
val validating = ConfigFactory.parseFile(configFile).getConfig("notary").getBoolean("validating")
|
||||
nodeInfoFile.readBytes().deserialize<SignedNodeInfo>().verified().let { NotaryInfo(it.legalIdentities.first(), validating) }
|
||||
}
|
||||
|
||||
return notaryInfos.let {
|
||||
NetworkParameters(
|
||||
minimumPlatformVersion = 1,
|
||||
notaries = it,
|
||||
maxMessageSize = 10485760,
|
||||
maxTransactionSize = Int.MAX_VALUE,
|
||||
modifiedTime = Instant.now(),
|
||||
epoch = 10,
|
||||
whitelistedContractImplementations = emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package net.corda.bootstrapper.volumes.azure
|
||||
|
||||
import com.microsoft.azure.management.Azure
|
||||
import com.microsoft.azure.management.resources.ResourceGroup
|
||||
import com.microsoft.azure.management.storage.StorageAccount
|
||||
import com.microsoft.azure.storage.CloudStorageAccount
|
||||
import net.corda.bootstrapper.Constants.Companion.restFriendlyName
|
||||
import net.corda.bootstrapper.notaries.CopiedNotary
|
||||
import net.corda.bootstrapper.volumes.Volume
|
||||
import net.corda.bootstrapper.volumes.Volume.Companion.keyPair
|
||||
import net.corda.bootstrapper.volumes.Volume.Companion.networkMapCert
|
||||
import net.corda.core.internal.signWithCert
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
|
||||
class AzureSmbVolume(private val azure: Azure, private val resourceGroup: ResourceGroup) : Volume {
|
||||
|
||||
private val storageAccount = getStorageAccount()
|
||||
|
||||
private val accKeys = storageAccount.keys[0]
|
||||
|
||||
|
||||
private val cloudFileShare = CloudStorageAccount.parse(
|
||||
"DefaultEndpointsProtocol=https;" +
|
||||
"AccountName=${storageAccount.name()};" +
|
||||
"AccountKey=${accKeys.value()};" +
|
||||
"EndpointSuffix=core.windows.net"
|
||||
)
|
||||
.createCloudFileClient()
|
||||
.getShareReference("nodeinfos")
|
||||
|
||||
val networkParamsFolder = cloudFileShare.rootDirectoryReference.getDirectoryReference("network-params")
|
||||
val shareName: String = cloudFileShare.name
|
||||
val storageAccountName: String
|
||||
get() = resourceGroup.restFriendlyName()
|
||||
val storageAccountKey: String
|
||||
get() = accKeys.value()
|
||||
|
||||
|
||||
init {
|
||||
while (true) {
|
||||
try {
|
||||
cloudFileShare.createIfNotExists()
|
||||
networkParamsFolder.createIfNotExists()
|
||||
break
|
||||
} catch (e: Throwable) {
|
||||
LOG.debug("storage account not ready, waiting")
|
||||
Thread.sleep(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStorageAccount(): StorageAccount {
|
||||
return azure.storageAccounts().getByResourceGroup(resourceGroup.name(), resourceGroup.restFriendlyName())
|
||||
?: azure.storageAccounts().define(resourceGroup.restFriendlyName())
|
||||
.withRegion(resourceGroup.region())
|
||||
.withExistingResourceGroup(resourceGroup)
|
||||
.withAccessFromAllNetworks()
|
||||
.create()
|
||||
}
|
||||
|
||||
override fun notariesForNetworkParams(notaries: List<CopiedNotary>) {
|
||||
val networkParamsFile = networkParamsFolder.getFileReference(NETWORK_PARAMS_FILE_NAME)
|
||||
networkParamsFile.deleteIfExists()
|
||||
LOG.info("Storing network-params in AzureFile location: " + networkParamsFile.uri)
|
||||
val networkParameters = convertNodeIntoToNetworkParams(notaries.map { it.configFile to it.nodeInfoFile })
|
||||
networkParamsFile.uploadFromByteArray(networkParameters.signWithCert(keyPair.private, networkMapCert).serialize().bytes)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
val LOG = LoggerFactory.getLogger(AzureSmbVolume::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,35 @@
|
||||
package net.corda.bootstrapper.volumes.docker
|
||||
|
||||
import net.corda.bootstrapper.context.Context
|
||||
import net.corda.bootstrapper.notaries.CopiedNotary
|
||||
import net.corda.bootstrapper.volumes.Volume
|
||||
import net.corda.core.internal.signWithCert
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
class LocalVolume(scratchDir: File, context: Context) : Volume {
|
||||
|
||||
private val networkDir = File(scratchDir, context.safeNetworkName)
|
||||
private val volumeDir = File(networkDir, "nodeinfos")
|
||||
private val networkParamsDir = File(volumeDir, "network-params")
|
||||
|
||||
override fun notariesForNetworkParams(notaries: List<CopiedNotary>) {
|
||||
volumeDir.deleteRecursively()
|
||||
networkParamsDir.mkdirs()
|
||||
val networkParameters = convertNodeIntoToNetworkParams(notaries.map { it.configFile to it.nodeInfoFile })
|
||||
val networkParamsFile = File(networkParamsDir, NETWORK_PARAMS_FILE_NAME)
|
||||
networkParamsFile.outputStream().use { networkParameters.signWithCert(Volume.keyPair.private, Volume.networkMapCert).serialize().writeTo(it) }
|
||||
LOG.info("wrote network params to local file: ${networkParamsFile.absolutePath}")
|
||||
}
|
||||
|
||||
|
||||
fun getPath(): String {
|
||||
return volumeDir.absolutePath
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = LoggerFactory.getLogger(LocalVolume::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
# Base image from (http://phusion.github.io/baseimage-docker)
|
||||
FROM openjdk:8u151-jre-alpine
|
||||
|
||||
RUN apk upgrade --update && \
|
||||
apk add --update --no-cache bash iputils && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
# Add user to run the app && \
|
||||
addgroup corda && \
|
||||
adduser -G corda -D -s /bin/bash corda && \
|
||||
# Create /opt/corda directory && \
|
||||
mkdir -p /opt/corda/plugins && \
|
||||
mkdir -p /opt/corda/logs && \
|
||||
mkdir -p /opt/corda/additional-node-infos && \
|
||||
mkdir -p /opt/node-setup
|
||||
|
||||
# Copy corda files
|
||||
ADD --chown=corda:corda corda.jar /opt/corda/corda.jar
|
||||
ADD --chown=corda:corda node.conf /opt/corda/node.conf
|
||||
ADD --chown=corda:corda cordapps/ /opt/corda/cordapps
|
||||
|
||||
# Copy node info watcher script
|
||||
ADD --chown=corda:corda node_info_watcher.sh /opt/corda/
|
||||
|
||||
COPY run-corda.sh /run-corda.sh
|
||||
|
||||
RUN chmod +x /run-corda.sh && \
|
||||
chmod +x /opt/corda/node_info_watcher.sh && \
|
||||
sync && \
|
||||
chown -R corda:corda /opt/corda
|
||||
|
||||
# Working directory for Corda
|
||||
WORKDIR /opt/corda
|
||||
ENV HOME=/opt/corda
|
||||
USER corda
|
||||
|
||||
# Start it
|
||||
CMD ["/run-corda.sh"]
|
12
tools/network-bootstrapper/src/main/resources/node_info_watcher.sh
Executable file
12
tools/network-bootstrapper/src/main/resources/node_info_watcher.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
while [ 1 -lt 2 ]; do
|
||||
NODE_INFO=$(ls | grep nodeInfo)
|
||||
if [ ${#NODE_INFO} -ge 5 ]; then
|
||||
echo "found nodeInfo copying to additional node node info folder"
|
||||
cp ${NODE_INFO} additional-node-infos/
|
||||
exit 0
|
||||
else
|
||||
echo "no node info found waiting"
|
||||
fi
|
||||
sleep 5
|
||||
done
|
3
tools/network-bootstrapper/src/main/resources/node_killer.sh
Executable file
3
tools/network-bootstrapper/src/main/resources/node_killer.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
sleep $1
|
@ -0,0 +1,39 @@
|
||||
# Base image from (http://phusion.github.io/baseimage-docker)
|
||||
FROM openjdk:8u151-jre-alpine
|
||||
|
||||
RUN apk upgrade --update && \
|
||||
apk add --update --no-cache bash iputils && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
# Add user to run the app && \
|
||||
addgroup corda && \
|
||||
adduser -G corda -D -s /bin/bash corda && \
|
||||
# Create /opt/corda directory && \
|
||||
mkdir -p /opt/corda/plugins && \
|
||||
mkdir -p /opt/corda/logs && \
|
||||
mkdir -p /opt/corda/additional-node-infos && \
|
||||
mkdir -p /opt/node-setup
|
||||
|
||||
# Copy corda files
|
||||
ADD --chown=corda:corda corda.jar /opt/corda/corda.jar
|
||||
ADD --chown=corda:corda node.conf /opt/corda/node.conf
|
||||
ADD --chown=corda:corda cordapps/ /opt/corda/cordapps
|
||||
ADD --chown=corda:corda certificates/ /opt/corda/certificates
|
||||
#ADD --chown=corda:corda nodeInfo-* /opt/corda/
|
||||
|
||||
# Copy node info watcher script
|
||||
ADD --chown=corda:corda node_info_watcher.sh /opt/corda/
|
||||
|
||||
COPY run-corda.sh /run-corda.sh
|
||||
|
||||
RUN chmod +x /run-corda.sh && \
|
||||
chmod +x /opt/corda/node_info_watcher.sh && \
|
||||
sync && \
|
||||
chown -R corda:corda /opt/corda
|
||||
|
||||
# Working directory for Corda
|
||||
WORKDIR /opt/corda
|
||||
ENV HOME=/opt/corda
|
||||
USER corda
|
||||
|
||||
# Start it
|
||||
CMD ["/run-corda.sh"]
|
@ -0,0 +1,4 @@
|
||||
rpcSettings {
|
||||
address="0.0.0.0:10003"
|
||||
adminAddress="127.0.0.1:10005"
|
||||
}
|
26
tools/network-bootstrapper/src/main/resources/run-corda-node.sh
Executable file
26
tools/network-bootstrapper/src/main/resources/run-corda-node.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
: ${CORDA_HOME:=/opt/corda}
|
||||
: ${JAVA_OPTIONS:=-Xmx512m}
|
||||
: ${X500? Need a value for the x500 name of this node}
|
||||
: ${OUR_NAME? Need a value for the expected FQDN of this node}
|
||||
: ${OUR_PORT? Need a value for the port to bind to}
|
||||
|
||||
export CORDA_HOME JAVA_OPTIONS
|
||||
|
||||
sed -i "/myLegalName/d" node.conf
|
||||
echo "myLegalName=\"${X500}\"" >> node.conf
|
||||
echo "p2pAddress=\"${OUR_NAME}:${OUR_PORT}\"" >> node.conf
|
||||
|
||||
cp additional-node-infos/network-params/network-parameters .
|
||||
|
||||
bash node_info_watcher.sh &
|
||||
|
||||
cd ${CORDA_HOME}
|
||||
|
||||
if java ${JAVA_OPTIONS} -jar ${CORDA_HOME}/corda.jar 2>&1 ; then
|
||||
echo "Corda exited with zero exit code"
|
||||
else
|
||||
echo "Corda exited with nonzero exit code, sleeping to allow log collection"
|
||||
sleep 10000
|
||||
fi
|
25
tools/network-bootstrapper/src/main/resources/run-corda-notary.sh
Executable file
25
tools/network-bootstrapper/src/main/resources/run-corda-notary.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
: ${CORDA_HOME:=/opt/corda}
|
||||
: ${JAVA_OPTIONS:=-Xmx512m}
|
||||
: ${OUR_NAME? Need a value for the expected FQDN of this node}
|
||||
: ${OUR_PORT? Need a value for the port to bind to}
|
||||
|
||||
export CORDA_HOME JAVA_OPTIONS
|
||||
echo "p2pAddress=\"${OUR_NAME}:${OUR_PORT}\"" >> node.conf
|
||||
cp additional-node-infos/network-params/network-parameters .
|
||||
|
||||
bash node_info_watcher.sh &
|
||||
|
||||
cd ${CORDA_HOME}
|
||||
|
||||
|
||||
if java ${JAVA_OPTIONS} -jar ${CORDA_HOME}/corda.jar 2>&1 ; then
|
||||
echo "Corda exited with zero exit code"
|
||||
else
|
||||
echo "Corda exited with nonzero exit code, sleeping to allow log collection"
|
||||
sleep 10000
|
||||
fi
|
||||
|
||||
|
||||
|
3
tools/network-bootstrapper/src/main/resources/ssh.conf
Normal file
3
tools/network-bootstrapper/src/main/resources/ssh.conf
Normal file
@ -0,0 +1,3 @@
|
||||
sshd {
|
||||
port = 12222
|
||||
}
|
Loading…
Reference in New Issue
Block a user