Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ihmc-cd functionality #80

Merged
merged 5 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies {
}
api("com.dorongold.plugins:task-tree:2.1.0")
api("guru.nidi:graphviz-kotlin:0.18.1")
api("com.hierynomus:sshj:0.38.0")

testApi("org.junit.jupiter:junit-jupiter-api:5.8.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/us/ihmc/build/IHMCBuildPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ import org.gradle.api.tasks.Delete
import org.gradle.kotlin.dsl.create
import org.gradle.plugins.ide.eclipse.EclipsePlugin
import org.gradle.plugins.ide.idea.IdeaPlugin
import us.ihmc.cd.AppExtension
import us.ihmc.cd.RemoteExtension

class IHMCBuildPlugin : Plugin<Project>
{
override fun apply(project: Project)
{
// add deploy task
ds58 marked this conversation as resolved.
Show resolved Hide resolved
project.extensions.add("app", AppExtension(project))
// add SFTP extension
project.extensions.add("remote", RemoteExtension())

LogTools = IHMCBuildLogTools(project.logger)

if (project.hasProperty("isProjectGroup") &&
Expand Down
53 changes: 53 additions & 0 deletions src/main/kotlin/us/ihmc/cd/AppExtension.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package us.ihmc.cd

import org.gradle.api.Project
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.plugins.ApplicationPlugin
import org.gradle.api.plugins.JavaApplication
import org.gradle.api.tasks.application.CreateStartScripts
import org.gradle.api.tasks.bundling.Jar
import org.gradle.kotlin.dsl.getByName
import java.io.File

class AppExtension(val project: Project)
{
val javaApplicationMap = hashMapOf<Project, JavaApplication>()

init
{
project.allprojects {
pluginManager.apply(ApplicationPlugin::class.java)
val javaApplication = extensions.getByType(JavaApplication::class.java)
javaApplicationMap.put(this, javaApplication)
javaApplication.mainClass.set("") // make user experience so N start scripts is natural
}
}

fun entrypoint(applicationName: String, mainClassName: String)
{
entrypoint(applicationName, mainClassName, null)
}

fun entrypoint(applicationName: String, mainClassName: String, defaultJvmOpts: Iterable<String>? = null)
{
entrypoint(project, applicationName, mainClassName, defaultJvmOpts)
}

fun entrypoint(project: Project, applicationName: String, mainClassName: String, defaultJvmOpts: Iterable<String>? = null)
{
val entrypoint = project.tasks.create(applicationName.decapitalize(), CreateStartScripts::class.java) {
this.mainClass.set(mainClassName)
this.applicationName = applicationName
if (defaultJvmOpts != null)
{
this.defaultJvmOpts = defaultJvmOpts
}
this.outputDir = File(project.buildDir, "scripts")
this.classpath = project.tasks.getByName<Jar>("jar").outputs.files + project.configurations.getByName("runtimeClasspath")
}
javaApplicationMap[project]!!.applicationDistribution.into("bin") {
from(entrypoint)
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
}
}
136 changes: 136 additions & 0 deletions src/main/kotlin/us/ihmc/cd/RemoteExtension.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package us.ihmc.cd

import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.common.IOUtils
import net.schmizz.sshj.connection.channel.direct.Session
import net.schmizz.sshj.sftp.SFTPClient
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts
import net.schmizz.sshj.transport.verification.PromiscuousVerifier
import org.gradle.api.Action
import us.ihmc.build.LogTools
import java.io.IOException
import java.nio.file.Files
import java.util.concurrent.TimeUnit

open class RemoteExtension
{
fun session(address: String, username: String, password: String, action: Action<RemoteConnection>)
{
session(address, {sshClient -> sshClient.authPassword(username, password)} , action)
}

fun session(address: String, username: String, action: Action<RemoteConnection>)
{
session(address, {sshClient -> authWithSSHKey(username, sshClient)} , action)
}

/**
* Replicate OpenSSH functionality where users can name private keys whatever they want.
*/
private fun authWithSSHKey(username: String, sshClient: SSHClient)
{
val sshDir = OpenSSHKnownHosts.detectSSHDir()

if (sshDir.isDirectory)
{
val list = Files.list(sshDir.toPath())

val privateKeyFiles = arrayListOf<String>()
for (path in list)
{
if (Files.isRegularFile(path)
&& path.fileName.toString() != "config"
&& path.fileName.toString() != "known_hosts"
&& !path.fileName.toString().endsWith(".pub"))
{
val absoluteNormalizedString = path.toAbsolutePath().normalize().toString()
privateKeyFiles.add(absoluteNormalizedString)
}
}

LogTools.info("Passing keys to authPublicKey: $privateKeyFiles")

sshClient.authPublickey(username, *privateKeyFiles.toTypedArray())
}
}

class RemoteConnection(val ssh: SSHClient, val sftp: SFTPClient)
{
fun exec(command: String, timeout: Double = 5.0)
{
var session: Session? = null
try
{
session = ssh.startSession()

LogTools.quiet("Executing on ${ssh.remoteHostname}: \"$command\"")
val sshjCommand = session.exec(command)
LogTools.quiet(IOUtils.readFully(sshjCommand.inputStream).toString())
sshjCommand.join((timeout * 1e9).toLong(), TimeUnit.NANOSECONDS)
LogTools.quiet("** exit status: " + sshjCommand.exitStatus)
}
finally
{
try
{
session?.close()
}
catch (e: IOException)
{
// do nothing
}
}
}

fun put(source: String, dest: String)
{
LogTools.quiet("Putting $source to ${ssh.remoteHostname}:$dest")
sftp.put(source, dest)
}

fun get(source: String, dest: String)
{
LogTools.quiet("Getting ${ssh.remoteHostname}:$source to $dest")
sftp.get(source, dest)
}
}

fun session(address: String, authenticate: (SSHClient) -> Unit, action: Action<RemoteConnection>)
{
val sshClient = SSHClient()

val sshDir = OpenSSHKnownHosts.detectSSHDir()

if (sshDir.resolve("known_hosts").isFile)
{
sshClient.loadKnownHosts()
}
else
{
LogTools.warn("Could not find known_hosts file. Disabling host key verification.")
ds58 marked this conversation as resolved.
Show resolved Hide resolved

sshClient.addHostKeyVerifier(PromiscuousVerifier())
}

sshClient.connect(address)

try
{
authenticate(sshClient)

val sftpClient: SFTPClient = sshClient.newSFTPClient()
try
{
action.execute(RemoteConnection(sshClient, sftpClient))
}
finally
{
sftpClient.close()
}
}
finally
{
sshClient.disconnect()
}
}
}