diff --git a/.editorconfig b/.editorconfig
index 2b991f50..6fd134e2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,2 +1,3 @@
-[*.{kt,kts}]
-indent_size = 4
\ No newline at end of file
+[*.{kt, kts}]
+indent_size = 4
+insert_final_newline = true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6f0b8a7..1f5f92da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,9 @@ Dropping a requirement of a major version of a dependency is a new contract.
## [Unreleased]
[Unreleased]: https://github.com/atlassian/infrastructure/compare/release-4.17.5...master
+### Fixed
+- Increase network-level retries for Jira/browser downloads. Decrease flakiness of such downloads on Ubuntu on WSL2.
+
## [4.17.5] - 2020-12-15
[4.17.5]: https://github.com/atlassian/infrastructure/compare/release-4.17.4...release-4.17.5
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/HttpResource.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/HttpResource.kt
index f4326c94..01edbfae 100644
--- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/HttpResource.kt
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/HttpResource.kt
@@ -27,7 +27,7 @@ class HttpResource(
) {
Ubuntu().install(ssh, listOf("lftp"), Duration.ofMinutes(2))
ssh.execute(
- """lftp -c 'set net:timeout 15; set net:max-retries 10; pget -n 32 -c "$uri" -o $destination'""",
+ """lftp -c 'set net:timeout 15; set net:max-retries 50; pget -n 32 -c "$uri" -o $destination'""",
timeout
)
}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/Iostat.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/Iostat.kt
index 7a7ed2c9..c82e0790 100644
--- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/Iostat.kt
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/Iostat.kt
@@ -10,7 +10,7 @@ import java.time.temporal.ChronoUnit
internal class Iostat : OsMetric {
companion object {
private val DELAY: Duration = Duration.ofSeconds(2)
- private val LOG_PATH: String = "~/jpt-iostat.log"
+ private val LOG_PATH: String = "./jpt-iostat.log"
private val TIME = "date -u \"+%d-%m-%Y %H:%M:%S UTC\""
private val ADD_TIME =
@@ -32,4 +32,4 @@ internal class Iostat : OsMetric {
val process = connection.startProcess("iostat -d $seconds -x | $ADD_TIME > $LOG_PATH")
return MonitoringProcess(process, LOG_PATH)
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/DatabaseIpConfig.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/DatabaseIpConfig.kt
new file mode 100644
index 00000000..40c12af2
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/DatabaseIpConfig.kt
@@ -0,0 +1,25 @@
+package com.atlassian.performance.tools.infrastructure.api.database
+
+import com.atlassian.performance.tools.infrastructure.api.Sed
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PostInstallHooks
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PostInstallHook
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class DatabaseIpConfig(
+ private val databaseIp: String
+) : PostInstallHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PostInstallHooks
+ ) {
+ Sed().replace(
+ connection = ssh,
+ expression = "(.*(@(//)?|//))" + "([^:/]+)" + "(.*)",
+ output = """\1$databaseIp\5""",
+ file = "${jira.home}/dbconfig.xml"
+ )
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabase.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabase.kt
index 7746d94e..965ef55c 100644
--- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabase.kt
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabase.kt
@@ -61,4 +61,4 @@ class MySqlDatabase(
Thread.sleep(Duration.ofSeconds(10).toMillis())
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MysqlConnector.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MysqlConnector.kt
new file mode 100644
index 00000000..f8ecf19c
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MysqlConnector.kt
@@ -0,0 +1,35 @@
+package com.atlassian.performance.tools.infrastructure.api.database
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PostInstallHooks
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PostInstallHook
+import com.atlassian.performance.tools.jvmtasks.api.Backoff
+import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.time.Duration
+
+class MysqlConnector : PostInstallHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PostInstallHooks
+ ) {
+ val connector = "https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.40.tar.gz"
+ IdempotentAction(
+ description = "Download MySQL connector",
+ action = { ssh.execute("wget -q $connector") }
+ ).retry(
+ maxAttempts = 3,
+ backoff = StaticBackoff(Duration.ofSeconds(5))
+ )
+ ssh.execute("tar -xzf mysql-connector-java-5.1.40.tar.gz")
+ ssh.execute("cp mysql-connector-java-5.1.40/mysql-connector-java-5.1.40-bin.jar ${jira.installation}/lib")
+ }
+}
+
+private class StaticBackoff(
+ private val backOff: Duration
+) : Backoff {
+ override fun backOff(attempt: Int): Duration = backOff
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/PostgresDatabase.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/PostgresDatabase.kt
new file mode 100644
index 00000000..602889df
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/PostgresDatabase.kt
@@ -0,0 +1,51 @@
+package com.atlassian.performance.tools.infrastructure.api.database
+
+import com.atlassian.performance.tools.infrastructure.DockerImage
+import com.atlassian.performance.tools.infrastructure.api.dataset.DatasetPackage
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import org.apache.logging.log4j.LogManager
+import org.apache.logging.log4j.Logger
+import java.net.URI
+import java.time.Duration
+
+class PostgresDatabase(
+ private val source: DatasetPackage,
+ private val maxConnections: Int
+) : Database {
+ private val logger: Logger = LogManager.getLogger(this::class.java)
+
+ private val image: DockerImage = DockerImage(
+ name = "postgres:9.6.15",
+ pullTimeout = Duration.ofMinutes(5)
+ )
+
+ constructor(
+ source: DatasetPackage
+ ) : this(
+ source = source,
+ maxConnections = 200
+ )
+
+ override fun setup(ssh: SshConnection): String {
+ val data = source.download(ssh)
+ val containerName = image.run(
+ ssh = ssh,
+// TODO Dataset for Postgres
+// parameters = "-p 5432:5432 -v `realpath $data`:/",
+ parameters = "-p 5432:5432",
+ arguments = "-c 'listen_addresses='*'' -c 'max_connections=$maxConnections'"
+ )
+ Thread.sleep(Duration.ofSeconds(15).toMillis())
+ logger.debug("Postgres - creating jira user and database")
+ ssh.execute("sudo docker exec -u postgres $containerName psql --command \"CREATE USER jira WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN PASSWORD 'jira';\"")
+ ssh.execute("sudo docker exec -u postgres $containerName createdb -E UNICODE -l C -T template0 -O jira jira")
+ return data
+ }
+
+ override fun start(jira: URI, ssh: SshConnection) {
+ // TODO Check logs for the following entry
+ // LOG: database system is ready to accept connections
+ Thread.sleep(Duration.ofSeconds(15).toMillis())
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/EmptyJiraHome.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/EmptyJiraHome.kt
new file mode 100644
index 00000000..0ea7f794
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/EmptyJiraHome.kt
@@ -0,0 +1,11 @@
+package com.atlassian.performance.tools.infrastructure.api.jira
+
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class EmptyJiraHome : JiraHomeSource {
+ override fun download(ssh: SshConnection): String {
+ val jiraHome = "jira-home"
+ ssh.execute("mkdir $jiraHome")
+ return jiraHome
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/JiraNodeConfig.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/JiraNodeConfig.kt
index a759d080..1a0877cf 100644
--- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/JiraNodeConfig.kt
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/JiraNodeConfig.kt
@@ -1,10 +1,6 @@
package com.atlassian.performance.tools.infrastructure.api.jira
-import com.atlassian.performance.tools.infrastructure.api.jvm.DisabledJvmDebug
-import com.atlassian.performance.tools.infrastructure.api.jvm.JavaDevelopmentKit
-import com.atlassian.performance.tools.infrastructure.api.jvm.VersionedJavaDevelopmentKit
-import com.atlassian.performance.tools.infrastructure.api.jvm.JvmDebug
-import com.atlassian.performance.tools.infrastructure.api.jvm.OracleJDK
+import com.atlassian.performance.tools.infrastructure.api.jvm.*
import com.atlassian.performance.tools.infrastructure.api.jvm.jmx.DisabledRemoteJmx
import com.atlassian.performance.tools.infrastructure.api.jvm.jmx.RemoteJmx
import com.atlassian.performance.tools.infrastructure.api.profiler.Profiler
@@ -157,4 +153,4 @@ class JiraNodeConfig private constructor(
JiraNodeConfig::class.java.getResource("/collectd/conf/jira-default.conf").toURI()
)
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/InstalledJira.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/InstalledJira.kt
new file mode 100644
index 00000000..a796b32a
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/InstalledJira.kt
@@ -0,0 +1,16 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install
+
+import com.atlassian.performance.tools.infrastructure.api.jvm.JavaDevelopmentKit
+
+class InstalledJira(
+ /**
+ * E.g. it contains `./dbconfig.xml`
+ */
+ val home: String,
+ /**
+ * E.g. it contains `./conf/server.xml`
+ */
+ val installation: String,
+ val jdk: JavaDevelopmentKit,
+ val server: TcpServer
+)
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/JiraInstallation.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/JiraInstallation.kt
new file mode 100644
index 00000000..d9d96b83
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/JiraInstallation.kt
@@ -0,0 +1,13 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install
+
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import net.jcip.annotations.ThreadSafe
+
+@ThreadSafe
+interface JiraInstallation {
+
+ fun install(
+ ssh: SshConnection,
+ server: TcpServer
+ ): InstalledJira
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/ParallelInstallation.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/ParallelInstallation.kt
new file mode 100644
index 00000000..72fefb17
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/ParallelInstallation.kt
@@ -0,0 +1,36 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install
+
+import com.atlassian.performance.tools.concurrency.api.submitWithLogContext
+import com.atlassian.performance.tools.infrastructure.api.distribution.ProductDistribution
+import com.atlassian.performance.tools.infrastructure.api.jira.JiraHomeSource
+import com.atlassian.performance.tools.infrastructure.api.jvm.JavaDevelopmentKit
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.util.concurrent.Executors
+
+class ParallelInstallation(
+ private val jiraHomeSource: JiraHomeSource,
+ private val productDistribution: ProductDistribution,
+ private val jdk: JavaDevelopmentKit
+) : JiraInstallation {
+
+ override fun install(
+ ssh: SshConnection,
+ server: TcpServer
+ ): InstalledJira {
+ val pool = Executors.newCachedThreadPool { runnable ->
+ Thread(runnable, "jira-installation-${runnable.hashCode()}")
+ }
+ val product = pool.submitWithLogContext("product") {
+ productDistribution.install(ssh, ".")
+ }
+ val home = pool.submitWithLogContext("home") {
+ jiraHomeSource.download(ssh)
+ }
+ val java = pool.submitWithLogContext("java") {
+ jdk.also { it.install(ssh) }
+ }
+ val jira = InstalledJira(home.get(), product.get(), java.get(), server)
+ pool.shutdownNow()
+ return jira
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/SequentialInstallation.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/SequentialInstallation.kt
new file mode 100644
index 00000000..dbd7367a
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/SequentialInstallation.kt
@@ -0,0 +1,23 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install
+
+import com.atlassian.performance.tools.infrastructure.api.distribution.ProductDistribution
+import com.atlassian.performance.tools.infrastructure.api.jira.JiraHomeSource
+import com.atlassian.performance.tools.infrastructure.api.jvm.JavaDevelopmentKit
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class SequentialInstallation(
+ private val jiraHomeSource: JiraHomeSource,
+ private val productDistribution: ProductDistribution,
+ private val jdk: JavaDevelopmentKit
+) : JiraInstallation {
+
+ override fun install(
+ ssh: SshConnection,
+ server: TcpServer
+ ): InstalledJira {
+ val installation = productDistribution.install(ssh, ".")
+ val home = jiraHomeSource.download(ssh)
+ jdk.install(ssh)
+ return InstalledJira(home, installation, jdk, server)
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/TcpServer.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/TcpServer.kt
new file mode 100644
index 00000000..9997f8a4
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/TcpServer.kt
@@ -0,0 +1,19 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install
+
+class TcpServer(
+ val ip: String,
+ val publicPort: Int,
+ val privatePort: Int,
+ val name: String
+) {
+ constructor(
+ ip: String,
+ port: Int,
+ name: String
+ ) : this(
+ ip,
+ port,
+ port,
+ name
+ )
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/AsyncProfilerHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/AsyncProfilerHook.kt
new file mode 100644
index 00000000..32457b84
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/AsyncProfilerHook.kt
@@ -0,0 +1,57 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpServer
+import com.atlassian.performance.tools.infrastructure.api.jira.report.Report
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PostStartHook
+import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PostStartHooks
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.net.URI
+
+class AsyncProfilerHook : PreInstallHook {
+
+ override fun call(
+ ssh: SshConnection,
+ server: TcpServer,
+ hooks: PreInstallHooks
+ ) {
+ val directory = "async-profiler"
+ val downloads = URI("https://github.com/jvm-profiling-tools/async-profiler/releases/download/")
+ val distribution = downloads.resolve("v1.4/async-profiler-1.4-linux-x64.tar.gz")
+ ssh.execute("wget -q $distribution")
+ ssh.execute("mkdir $directory")
+ ssh.execute("tar -xzf async-profiler-1.4-linux-x64.tar.gz -C $directory")
+ ssh.execute("sudo sh -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid'")
+ ssh.execute("sudo sh -c 'echo 0 > /proc/sys/kernel/kptr_restrict'")
+ val profilerPath = "./$directory/profiler.sh"
+ val profiler = InstalledAsyncProfiler(profilerPath)
+ hooks.postStart.insert(profiler)
+ }
+}
+
+private class InstalledAsyncProfiler(
+ private val profilerPath: String
+) : PostStartHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: StartedJira,
+ hooks: PostStartHooks
+ ) {
+ ssh.execute("$profilerPath -b 20000000 start ${jira.pid}")
+ val profiler = StartedAsyncProfiler(jira.pid, profilerPath)
+ hooks.reports.add(profiler)
+ }
+}
+
+private class StartedAsyncProfiler(
+ private val pid: Int,
+ private val profilerPath: String
+) : Report {
+
+ override fun locate(ssh: SshConnection): List {
+ val flameGraphFile = "flamegraph.svg"
+ ssh.execute("$profilerPath stop $pid -o svg > $flameGraphFile")
+ return listOf(flameGraphFile)
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/DataCenterHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/DataCenterHook.kt
new file mode 100644
index 00000000..802a6a18
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/DataCenterHook.kt
@@ -0,0 +1,24 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.SharedHome
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class DataCenterHook(
+ private val nodeId: String,
+ private val sharedHome: SharedHome
+) : PostInstallHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PostInstallHooks
+ ) {
+ val localSharedHome = sharedHome.localSharedHome
+ sharedHome.mount(ssh)
+ val jiraHome = jira.home // TODO what's the difference between localSharedHome and jiraHome? should both be hookable?
+ ssh.execute("echo ehcache.object.port = 40011 >> $jiraHome/cluster.properties")
+ ssh.execute("echo jira.node.id = $nodeId >> $jiraHome/cluster.properties")
+ ssh.execute("echo jira.shared.home = `realpath $localSharedHome` >> $jiraHome/cluster.properties")
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/DisabledAutoBackup.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/DisabledAutoBackup.kt
new file mode 100644
index 00000000..dd482d1e
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/DisabledAutoBackup.kt
@@ -0,0 +1,15 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class DisabledAutoBackup : PostInstallHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PostInstallHooks
+ ) {
+ ssh.execute("echo jira.autoexport=false > ${jira.home}/jira-config.properties")
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/HookedJiraInstallation.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/HookedJiraInstallation.kt
new file mode 100644
index 00000000..06d40ba2
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/HookedJiraInstallation.kt
@@ -0,0 +1,22 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.install.JiraInstallation
+import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpServer
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class HookedJiraInstallation(
+ private val installation: JiraInstallation,
+ private val hooks: PreInstallHooks
+) : JiraInstallation {
+
+ override fun install(
+ ssh: SshConnection,
+ server: TcpServer
+ ): InstalledJira {
+ hooks.call(ssh, server)
+ val installed = installation.install(ssh, server)
+ hooks.postInstall.call(ssh, installed)
+ return installed
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/JiraHomeProperty.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/JiraHomeProperty.kt
new file mode 100644
index 00000000..a95498cf
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/JiraHomeProperty.kt
@@ -0,0 +1,16 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class JiraHomeProperty : PostInstallHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PostInstallHooks
+ ) {
+ val properties = "${jira.installation}/atlassian-jira/WEB-INF/classes/jira-application.properties"
+ ssh.execute("echo jira.home=`realpath ${jira.home}` > $properties")
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/JiraLogs.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/JiraLogs.kt
new file mode 100644
index 00000000..20e94ed1
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/JiraLogs.kt
@@ -0,0 +1,28 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.report.StaticReport
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.nio.file.Path
+import java.nio.file.Paths
+
+class JiraLogs : PostInstallHook {
+
+ override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PostInstallHooks) {
+ listOf(
+ "${jira.home}/log/atlassian-jira.log",
+ "${jira.installation}/logs/catalina.out"
+ )
+ .onEach { ensureFile(Paths.get(it), ssh) }
+ .map { StaticReport(it) }
+ .forEach { hooks.reports.add(it) }
+ }
+
+ private fun ensureFile(
+ path: Path,
+ ssh: SshConnection
+ ) {
+ ssh.execute("mkdir -p ${path.parent!!}")
+ ssh.execute("touch $path")
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/JvmConfig.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/JvmConfig.kt
new file mode 100644
index 00000000..e772aa13
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/JvmConfig.kt
@@ -0,0 +1,29 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.JiraGcLog
+import com.atlassian.performance.tools.infrastructure.api.jira.JiraNodeConfig
+import com.atlassian.performance.tools.infrastructure.api.jira.SetenvSh
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.report.FileListing
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class JvmConfig(
+ private val config: JiraNodeConfig
+) : PostInstallHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PostInstallHooks
+ ) {
+ val gcLog = JiraGcLog(jira.installation)
+ SetenvSh(jira.installation).setup(
+ connection = ssh,
+ config = config,
+ gcLog = gcLog,
+ jiraIp = jira.server.ip
+ )
+ val report = FileListing(gcLog.path("*"))
+ hooks.reports.add(report)
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/LateUbuntuSysstat.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/LateUbuntuSysstat.kt
new file mode 100644
index 00000000..3a6d2222
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/LateUbuntuSysstat.kt
@@ -0,0 +1,35 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.Iostat
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PostStartHook
+import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PostStartHooks
+import com.atlassian.performance.tools.infrastructure.api.os.OsMetric
+import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu
+import com.atlassian.performance.tools.infrastructure.api.os.Vmstat
+import com.atlassian.performance.tools.infrastructure.jira.hook.RemoteMonitoringProcessReport
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class LateUbuntuSysstat : PostInstallHook {
+ override fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PostInstallHooks
+ ) {
+ val ubuntu = Ubuntu()
+ ubuntu.install(ssh, listOf("sysstat"))
+ listOf(Vmstat(), Iostat())
+ .map { PostStartOsMetric(it) }
+ .forEach { hooks.postStart.insert(it) }
+ }
+}
+
+private class PostStartOsMetric(
+ private val metric: OsMetric
+) : PostStartHook {
+ override fun call(ssh: SshConnection, jira: StartedJira, hooks: PostStartHooks) {
+ val process = metric.start(ssh)
+ hooks.reports.add(RemoteMonitoringProcessReport(process))
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PostInstallHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PostInstallHook.kt
new file mode 100644
index 00000000..0fe19b0a
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PostInstallHook.kt
@@ -0,0 +1,21 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+/**
+ * Intercepts a call after Jira is installed.
+ */
+interface PostInstallHook {
+
+ /**
+ * @param [ssh] connects to the [jira]
+ * @param [jira] points to the installed Jira
+ * @param [hooks] inserts future hooks and reports
+ */
+ fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PostInstallHooks
+ )
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PostInstallHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PostInstallHooks.kt
new file mode 100644
index 00000000..155dcc63
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PostInstallHooks.kt
@@ -0,0 +1,54 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.JiraNodeConfig
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PreStartHooks
+import com.atlassian.performance.tools.infrastructure.jira.hook.install.ProfilerHook
+import com.atlassian.performance.tools.infrastructure.jira.hook.install.SplunkForwarderHook
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.util.*
+import java.util.concurrent.ConcurrentLinkedQueue
+
+class PostInstallHooks private constructor(
+ val preStart: PreStartHooks
+) {
+
+ private val hooks: Queue = ConcurrentLinkedQueue()
+ val postStart = preStart.postStart
+ val reports = postStart.reports
+
+ fun insert(
+ hook: PostInstallHook
+ ) {
+ hooks.add(hook)
+ }
+
+ internal fun call(
+ ssh: SshConnection,
+ jira: InstalledJira
+ ) {
+ while (true) {
+ hooks
+ .poll()
+ ?.call(ssh, jira, this)
+ ?: break
+ }
+ }
+
+ companion object Factory {
+ fun default(): PostInstallHooks = PostInstallHooks(PreStartHooks.default()).apply {
+ val config = JiraNodeConfig.Builder().build()
+ listOf(
+ JiraHomeProperty(),
+ DisabledAutoBackup(),
+ JvmConfig(config),
+ ProfilerHook(config.profiler),
+ SplunkForwarderHook(config.splunkForwarder),
+ JiraLogs(),
+ LateUbuntuSysstat()
+ ).forEach { insert(it) }
+ }
+
+ fun empty(): PostInstallHooks = PostInstallHooks(PreStartHooks.empty())
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHook.kt
new file mode 100644
index 00000000..1e58b1af
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHook.kt
@@ -0,0 +1,21 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpServer
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+/**
+ * Intercepts a call before Jira is installed.
+ */
+interface PreInstallHook {
+
+ /**
+ * @param [ssh] connects to the [server]
+ * @param [server] will install Jira
+ * @param [hooks] inserts future hooks and reports
+ */
+ fun call(
+ ssh: SshConnection,
+ server: TcpServer,
+ hooks: PreInstallHooks
+ )
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHooks.kt
new file mode 100644
index 00000000..5506de0d
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHooks.kt
@@ -0,0 +1,40 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpServer
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.util.*
+import java.util.concurrent.ConcurrentLinkedQueue
+
+class PreInstallHooks private constructor(
+ val postInstall: PostInstallHooks
+) {
+
+ private val hooks: Queue = ConcurrentLinkedQueue()
+ val preStart = postInstall.preStart
+ val postStart = preStart.postStart
+ val reports = postStart.reports
+
+ fun insert(
+ hook: PreInstallHook
+ ) {
+ hooks.add(hook)
+ }
+
+ internal fun call(
+ ssh: SshConnection,
+ server: TcpServer
+ ) {
+ while (true) {
+ hooks
+ .poll()
+ ?.call(ssh, server, this)
+ ?: break
+ }
+ }
+
+ companion object Factory {
+ fun default(): PreInstallHooks = PreInstallHooks(PostInstallHooks.default())
+
+ fun empty(): PreInstallHooks = PreInstallHooks(PostInstallHooks.empty())
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/SystemLog.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/SystemLog.kt
new file mode 100644
index 00000000..cb51739d
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/SystemLog.kt
@@ -0,0 +1,12 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.report.Report
+import com.atlassian.performance.tools.infrastructure.api.jira.report.StaticReport
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class SystemLog : Report {
+
+ override fun locate(ssh: SshConnection): List {
+ return StaticReport("/var/log/syslog").locate(ssh)
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/FileListing.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/FileListing.kt
new file mode 100644
index 00000000..e1736a7d
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/FileListing.kt
@@ -0,0 +1,15 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.report
+
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class FileListing(
+ private val pattern: String
+) : Report {
+ override fun locate(
+ ssh: SshConnection
+ ): List = ssh
+ .execute("ls $pattern")
+ .output
+ .lines()
+ .filter { it.isNotBlank() }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Report.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Report.kt
new file mode 100644
index 00000000..1da33c75
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Report.kt
@@ -0,0 +1,15 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.report
+
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+/**
+ * Reports back about remote events. E.g. points to interesting logs, dumps, charts.
+ */
+interface Report {
+
+ /**
+ * @param [ssh] connects to the server, which holds interesting data
+ * @return list of interesting file paths to be downloaded
+ */
+ fun locate(ssh: SshConnection): List
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Reports.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Reports.kt
new file mode 100644
index 00000000..bfc8b1cd
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Reports.kt
@@ -0,0 +1,18 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.report
+
+import java.util.*
+import java.util.concurrent.ConcurrentLinkedQueue
+
+class Reports {
+ private val reports: Queue = ConcurrentLinkedQueue()
+
+ fun add(
+ report: Report
+ ) {
+ reports.add(report)
+ }
+
+ fun list(): Iterable {
+ return reports.toList()
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/StaticReport.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/StaticReport.kt
new file mode 100644
index 00000000..da90e4d1
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/StaticReport.kt
@@ -0,0 +1,10 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.report
+
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class StaticReport(
+ private val remotePath: String
+) : Report {
+
+ override fun locate(ssh: SshConnection): List = listOf(remotePath)
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraLaunchScript.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraLaunchScript.kt
new file mode 100644
index 00000000..32673528
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraLaunchScript.kt
@@ -0,0 +1,24 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.time.Duration
+
+class JiraLaunchScript : JiraStart {
+
+ override fun start(
+ ssh: SshConnection,
+ installed: InstalledJira
+ ): StartedJira {
+ ssh.execute(
+ "${installed.jdk.use()}; ${installed.installation}/bin/start-jira.sh",
+ Duration.ofMinutes(1)
+ )
+ val pid = ssh
+ .execute("cat ${installed.installation}/work/catalina.pid")
+ .output
+ .trim()
+ .toInt()
+ return StartedJira(installed, pid)
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraStart.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraStart.kt
new file mode 100644
index 00000000..dabddc38
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraStart.kt
@@ -0,0 +1,14 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import net.jcip.annotations.ThreadSafe
+
+@ThreadSafe
+interface JiraStart {
+
+ fun start(
+ ssh: SshConnection,
+ installed: InstalledJira
+ ): StartedJira
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/StartedJira.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/StartedJira.kt
new file mode 100644
index 00000000..da6f4955
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/StartedJira.kt
@@ -0,0 +1,8 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+
+class StartedJira(
+ val installed: InstalledJira,
+ val pid: Int
+)
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/AccessLogs.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/AccessLogs.kt
new file mode 100644
index 00000000..cccd0da5
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/AccessLogs.kt
@@ -0,0 +1,12 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.report.FileListing
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class AccessLogs : PostStartHook {
+
+ override fun call(ssh: SshConnection, jira: StartedJira, hooks: PostStartHooks) {
+ hooks.reports.add(FileListing("${jira.installed.installation}/logs/*access*"))
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/HookedJiraStart.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/HookedJiraStart.kt
new file mode 100644
index 00000000..20634150
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/HookedJiraStart.kt
@@ -0,0 +1,22 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.start.JiraStart
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class HookedJiraStart(
+ private val start: JiraStart,
+ private val hooks: PreStartHooks
+) : JiraStart {
+
+ override fun start(
+ ssh: SshConnection,
+ installed: InstalledJira
+ ): StartedJira {
+ hooks.call(ssh, installed)
+ val started = start.start(ssh, installed)
+ hooks.postStart.call(ssh, started)
+ return started
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/JstatHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/JstatHook.kt
new file mode 100644
index 00000000..27b23757
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/JstatHook.kt
@@ -0,0 +1,17 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.infrastructure.jira.hook.RemoteMonitoringProcessReport
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class JstatHook : PostStartHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: StartedJira,
+ hooks: PostStartHooks
+ ) {
+ val process = jira.installed.jdk.jstatMonitoring.start(ssh, jira.pid)
+ hooks.reports.add(RemoteMonitoringProcessReport(process))
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PostStartHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PostStartHook.kt
new file mode 100644
index 00000000..c3c47123
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PostStartHook.kt
@@ -0,0 +1,21 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+/**
+ * Intercepts a call after Jira is started.
+ */
+interface PostStartHook {
+
+ /**
+ * @param [ssh] connects to the [jira]
+ * @param [jira] points to the started Jira
+ * @param [hooks] inserts future reports
+ */
+ fun call(
+ ssh: SshConnection,
+ jira: StartedJira,
+ hooks: PostStartHooks
+ )
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PostStartHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PostStartHooks.kt
new file mode 100644
index 00000000..21b0b46a
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PostStartHooks.kt
@@ -0,0 +1,39 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.util.*
+import java.util.concurrent.ConcurrentLinkedQueue
+
+class PostStartHooks private constructor() {
+
+ private val hooks: Queue = ConcurrentLinkedQueue()
+ val reports = Reports()
+
+ fun insert(
+ hook: PostStartHook
+ ) {
+ hooks.add(hook)
+ }
+
+ internal fun call(
+ ssh: SshConnection,
+ jira: StartedJira
+ ) {
+ while (true) {
+ hooks
+ .poll()
+ ?.call(ssh, jira, this)
+ ?: break
+ }
+ }
+
+ companion object Factory {
+ fun default(): PostStartHooks = empty().apply {
+ insert(JstatHook())
+ }
+
+ fun empty(): PostStartHooks = PostStartHooks()
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PreStartHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PreStartHook.kt
new file mode 100644
index 00000000..d0fa8326
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PreStartHook.kt
@@ -0,0 +1,21 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+/**
+ * Intercepts a call before Jira is started.
+ */
+interface PreStartHook {
+
+ /**
+ * @param [ssh] connects to the [jira]
+ * @param [jira] points to the installed Jira
+ * @param [hooks] inserts future hooks and reports
+ */
+ fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PreStartHooks
+ )
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PreStartHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PreStartHooks.kt
new file mode 100644
index 00000000..6ce16b42
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/PreStartHooks.kt
@@ -0,0 +1,38 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.util.*
+import java.util.concurrent.ConcurrentLinkedQueue
+
+class PreStartHooks private constructor(
+ val postStart: PostStartHooks
+) {
+
+ private val hooks: Queue = ConcurrentLinkedQueue()
+ val reports = postStart.reports
+
+ fun insert(
+ hook: PreStartHook
+ ) {
+ hooks.add(hook)
+ }
+
+ internal fun call(
+ ssh: SshConnection,
+ jira: InstalledJira
+ ) {
+ while (true) {
+ hooks
+ .poll()
+ ?.call(ssh, jira, this)
+ ?: break
+ }
+ }
+
+ companion object Factory {
+ fun default() = PreStartHooks(PostStartHooks.default())
+
+ fun empty() = PreStartHooks(PostStartHooks.empty())
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/RestUpgrade.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/RestUpgrade.kt
new file mode 100644
index 00000000..84aa39f4
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/RestUpgrade.kt
@@ -0,0 +1,74 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.JiraLaunchTimeouts
+import com.atlassian.performance.tools.infrastructure.api.jira.report.StaticReport
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.infrastructure.api.jvm.ThreadDump
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.net.URI
+import java.time.Duration
+import java.time.Instant
+
+class RestUpgrade(
+ private val timeouts: JiraLaunchTimeouts,
+ private val adminUsername: String,
+ private val adminPassword: String
+) : PostStartHook {
+
+ override fun call(ssh: SshConnection, jira: StartedJira, hooks: PostStartHooks) {
+ val threadDump = ThreadDump(jira.pid, jira.installed.jdk)
+ val privatePort = jira.installed.server.privatePort
+ val upgradesEndpoint = URI("http://$adminUsername:$adminPassword@localhost:$privatePort/rest/api/2/upgrade")
+ hooks.reports.add(StaticReport("thread-dumps"))
+ waitForStatusToChange(
+ statusQuo = "000",
+ timeout = timeouts.offlineTimeout,
+ ssh = ssh,
+ uri = upgradesEndpoint,
+ threadDump = threadDump
+ )
+ waitForStatusToChange(
+ statusQuo = "503",
+ timeout = timeouts.initTimeout,
+ ssh = ssh,
+ uri = upgradesEndpoint,
+ threadDump = threadDump
+ )
+ ssh.execute(
+ cmd = "curl --silent --retry 6 -X POST $upgradesEndpoint",
+ timeout = Duration.ofSeconds(15)
+ )
+ waitForStatusToChange(
+ statusQuo = "303",
+ timeout = timeouts.upgradeTimeout,
+ ssh = ssh,
+ uri = upgradesEndpoint,
+ threadDump = threadDump
+ )
+ }
+
+ private fun waitForStatusToChange(
+ statusQuo: String,
+ uri: URI,
+ timeout: Duration,
+ ssh: SshConnection,
+ threadDump: ThreadDump
+ ) {
+ val backoff = Duration.ofSeconds(10)
+ val deadline = Instant.now() + timeout
+ while (true) {
+ val currentStatus = ssh.safeExecute(
+ cmd = "curl --silent --write-out '%{http_code}' --output /dev/null -X GET $uri",
+ timeout = timeouts.unresponsivenessTimeout
+ ).output
+ if (currentStatus != statusQuo) {
+ break
+ }
+ if (deadline < Instant.now()) {
+ throw Exception("$uri failed to get out of $statusQuo status within $timeout")
+ }
+ threadDump.gather(ssh, "thread-dumps")
+ Thread.sleep(backoff.toMillis())
+ }
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/Jstat.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/Jstat.kt
index f0d01e0c..34a3e467 100644
--- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/Jstat.kt
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/Jstat.kt
@@ -11,7 +11,7 @@ class Jstat(
companion object {
val LOG_FILE_NAME = "jpt-jstat.log"
private val INTERVAL: Duration = Duration.ofSeconds(2)
- private val LOG_PATH: String = "~/$LOG_FILE_NAME"
+ private val LOG_PATH: String = "./$LOG_FILE_NAME"
private val TIME = "date -Iseconds"
private val ADD_TIME =
@@ -48,4 +48,4 @@ class Jstat(
val process = connection.startProcess("${jvmBin}jstat $option -t $pid $interval | $ADD_TIME > $LOG_PATH")
return MonitoringProcess(process, LOG_PATH)
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/ThreadDump.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/ThreadDump.kt
index 18a6d361..8932d469 100644
--- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/ThreadDump.kt
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/ThreadDump.kt
@@ -26,7 +26,7 @@ class ThreadDump(
*/
fun gather(connection: SshConnection, destination: String) {
val threadDumpName = Instant.now().toEpochMilli()
- val command = "${jdk.use()}; jcmd $pid Thread.print > $destination/${threadDumpName}"
+ val command = "${jdk.use()}; jcmd $pid Thread.print > $destination/$threadDumpName"
connection.execute("mkdir -p $destination")
connection.execute(command)
}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/os/Vmstat.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/os/Vmstat.kt
index 2d8f8005..84c6aaab 100644
--- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/os/Vmstat.kt
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/os/Vmstat.kt
@@ -9,7 +9,7 @@ class Vmstat : OsMetric {
companion object {
val LOG_FILE_NAME = "jpt-vmstat.log"
- private val LOG_PATH: String = "~/$LOG_FILE_NAME"
+ private val LOG_PATH: String = "./$LOG_FILE_NAME"
private val DELAY: Duration = Duration.ofSeconds(2)
}
@@ -26,4 +26,4 @@ class Vmstat : OsMetric {
val process = connection.startProcess("vmstat -t $delayInSeconds > $LOG_PATH")
return MonitoringProcess(process, LOG_PATH)
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfiler.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfiler.kt
index e2f3f23e..c2850884 100644
--- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfiler.kt
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfiler.kt
@@ -6,6 +6,7 @@ import com.atlassian.performance.tools.ssh.api.SshConnection
/**
* Asynchronous profiler. See https://github.com/jvm-profiling-tools/async-profiler#basic-usage
*/
+@Deprecated("Use AsyncProfilerHook")
class AsyncProfiler : Profiler {
override fun install(ssh: SshConnection) {
ssh.execute("wget -q https://github.com/jvm-profiling-tools/async-profiler/releases/download/v1.4/async-profiler-1.4-linux-x64.tar.gz")
@@ -34,4 +35,4 @@ class AsyncProfiler : Profiler {
return flameGraphFile
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/virtualusers/SshVirtualUsers.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/virtualusers/SshVirtualUsers.kt
index e5589fe1..5e0da2aa 100644
--- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/virtualusers/SshVirtualUsers.kt
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/virtualusers/SshVirtualUsers.kt
@@ -89,8 +89,4 @@ class SshVirtualUsers(
}
}
}
-
- override fun toString(): String {
- return "SshVirtualUsers(name='$name', nodeOrder=$nodeOrder, resultsTransport=$resultsTransport, jarName='$jarName', ssh=$ssh, logger=$logger, jdk=$jdk)"
- }
}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/hook/RemoteMonitoringProcessReport.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/hook/RemoteMonitoringProcessReport.kt
new file mode 100644
index 00000000..674a03e6
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/hook/RemoteMonitoringProcessReport.kt
@@ -0,0 +1,14 @@
+package com.atlassian.performance.tools.infrastructure.jira.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.report.Report
+import com.atlassian.performance.tools.infrastructure.api.process.RemoteMonitoringProcess
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+internal class RemoteMonitoringProcessReport(
+ private val process: RemoteMonitoringProcess
+) : Report {
+ override fun locate(ssh: SshConnection): List {
+ process.stop(ssh)
+ return listOf(process.getResultPath())
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/hook/install/ProfilerHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/hook/install/ProfilerHook.kt
new file mode 100644
index 00000000..220fdbd4
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/hook/install/ProfilerHook.kt
@@ -0,0 +1,40 @@
+package com.atlassian.performance.tools.infrastructure.jira.hook.install
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PostInstallHooks
+import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PostStartHooks
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PostInstallHook
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PostStartHook
+import com.atlassian.performance.tools.infrastructure.api.profiler.Profiler
+import com.atlassian.performance.tools.infrastructure.jira.hook.RemoteMonitoringProcessReport
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+/**
+ * Bridges the [Profiler] SPI with the [PostInstallHook] SPI.
+ * In general any [Profiler] can be rewritten as an [PostInstallHook] or any other hookPreStart without this bridge.
+ */
+class ProfilerHook(
+ private val profiler: Profiler
+) : PostInstallHook {
+ override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PostInstallHooks) {
+ profiler.install(ssh)
+ hooks.preStart.postStart.insert(InstalledProfiler(profiler))
+ }
+}
+
+private class InstalledProfiler(
+ private val profiler: Profiler
+) : PostStartHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: StartedJira,
+ hooks: PostStartHooks
+ ) {
+ val process = profiler.start(ssh, jira.pid)
+ if (process != null) {
+ hooks.reports.add(RemoteMonitoringProcessReport(process))
+ }
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/hook/install/SplunkForwarderHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/hook/install/SplunkForwarderHook.kt
new file mode 100644
index 00000000..2112e483
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/hook/install/SplunkForwarderHook.kt
@@ -0,0 +1,21 @@
+package com.atlassian.performance.tools.infrastructure.jira.hook.install
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PostInstallHooks
+import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PostInstallHook
+import com.atlassian.performance.tools.infrastructure.api.splunk.SplunkForwarder
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+internal class SplunkForwarderHook(
+ private val splunk: SplunkForwarder
+) : PostInstallHook {
+
+ override fun call(
+ ssh: SshConnection,
+ jira: InstalledJira,
+ hooks: PostInstallHooks
+ ) {
+ splunk.jsonifyLog4j(ssh, "${jira.installation}/atlassian-jira/WEB-INF/classes/log4j.properties")
+ splunk.run(ssh, jira.server.name, "/home/ubuntu/jirahome/log")
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/ubuntu/EarlyUbuntuSysstat.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/ubuntu/EarlyUbuntuSysstat.kt
new file mode 100644
index 00000000..9bc25df4
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/ubuntu/EarlyUbuntuSysstat.kt
@@ -0,0 +1,19 @@
+package com.atlassian.performance.tools.infrastructure.ubuntu
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpServer
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PreInstallHooks
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PreInstallHook
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+class EarlyUbuntuSysstat : PreInstallHook {
+
+ override fun call(
+ ssh: SshConnection,
+ server: TcpServer,
+ hooks: PreInstallHooks
+ ) {
+ UbuntuSysstat()
+ .install(ssh)
+ .forEach { hooks.insert(it) }
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/ubuntu/InstalledOsMetric.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/ubuntu/InstalledOsMetric.kt
new file mode 100644
index 00000000..515b806f
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/ubuntu/InstalledOsMetric.kt
@@ -0,0 +1,18 @@
+package com.atlassian.performance.tools.infrastructure.ubuntu
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpServer
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PreInstallHooks
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PreInstallHook
+import com.atlassian.performance.tools.infrastructure.api.os.OsMetric
+import com.atlassian.performance.tools.infrastructure.jira.hook.RemoteMonitoringProcessReport
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+internal class InstalledOsMetric(
+ private val metric: OsMetric
+) : PreInstallHook {
+
+ override fun call(ssh: SshConnection, server: TcpServer, hooks: PreInstallHooks) {
+ val process = metric.start(ssh)
+ hooks.reports.add(RemoteMonitoringProcessReport(process))
+ }
+}
diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/ubuntu/UbuntuSysstat.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/ubuntu/UbuntuSysstat.kt
new file mode 100644
index 00000000..8a76c645
--- /dev/null
+++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/ubuntu/UbuntuSysstat.kt
@@ -0,0 +1,17 @@
+package com.atlassian.performance.tools.infrastructure.ubuntu
+
+import com.atlassian.performance.tools.infrastructure.Iostat
+import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu
+import com.atlassian.performance.tools.infrastructure.api.os.Vmstat
+import com.atlassian.performance.tools.ssh.api.SshConnection
+
+internal class UbuntuSysstat {
+
+ fun install(
+ ssh: SshConnection
+ ): List {
+ val ubuntu = Ubuntu()
+ ubuntu.install(ssh, listOf("sysstat"))
+ return listOf(Vmstat(), Iostat()).map { InstalledOsMetric(it) }
+ }
+}
diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshUbuntuExtensions.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshUbuntuExtensions.kt
index 73d3a946..03890f04 100644
--- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshUbuntuExtensions.kt
+++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshUbuntuExtensions.kt
@@ -1,20 +1,22 @@
package com.atlassian.performance.tools.infrastructure
import com.atlassian.performance.tools.ssh.api.Ssh
+import com.atlassian.performance.tools.ssh.api.SshHost
+import com.atlassian.performance.tools.ssh.api.auth.PublicKeyAuthentication
import com.atlassian.performance.tools.sshubuntu.api.SshUbuntu
import java.time.Duration
internal fun SshUbuntu.toSsh(): Ssh {
val ssh = Ssh(with(this.ssh) {
- com.atlassian.performance.tools.ssh.api.SshHost(
+ SshHost(
ipAddress = ipAddress,
userName = userName,
- authentication = com.atlassian.performance.tools.ssh.api.auth.PublicKeyAuthentication(privateKey),
+ authentication = PublicKeyAuthentication(privateKey),
port = port
)
})
ssh.newConnection().use { connection ->
- connection.execute("apt-get update -qq", Duration.ofMinutes(3))
+ connection.execute("apt-get update -qq", Duration.ofMinutes(7))
connection.execute("export DEBIAN_FRONTEND=noninteractive; apt-get install sudo curl screen gnupg2 -y -qq", Duration.ofMinutes(10))
}
return ssh
diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHooksTest.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHooksTest.kt
new file mode 100644
index 00000000..c5fb692f
--- /dev/null
+++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHooksTest.kt
@@ -0,0 +1,57 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.install.hook
+
+import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpServer
+import com.atlassian.performance.tools.infrastructure.mock.UnimplementedSshConnection
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+
+class JiraNodeHooksTest {
+
+ @Test
+ fun shouldHookDuringListing() {
+ val counter = CountingHook()
+ val hooks = PreInstallHooks.empty().apply {
+ insert(counter)
+ insert(HookingHook(counter))
+ insert(counter)
+ }
+ val server = TcpServer("doesn't matter", 123, "fake-server")
+
+ hooks.call(UnimplementedSshConnection(), server)
+
+ assertThat(counter.count).isEqualTo(3)
+ }
+
+ @Test
+ fun shouldHookToTheTailDuringListing() {
+ val counter = CountingHook()
+ val hooks = PreInstallHooks.empty().apply {
+ insert(counter)
+ insert(counter)
+ insert(HookingHook(counter))
+ }
+ val server = TcpServer("doesn't matter", 123, "fake-server")
+
+ hooks.call(UnimplementedSshConnection(), server)
+
+ assertThat(counter.count).isEqualTo(3)
+ }
+}
+
+private class CountingHook : PreInstallHook {
+
+ var count = 0
+
+ override fun call(ssh: SshConnection, server: TcpServer, hooks: PreInstallHooks) {
+ count++
+ }
+}
+
+private class HookingHook(
+ private val hook: PreInstallHook
+) : PreInstallHook {
+ override fun call(ssh: SshConnection, server: TcpServer, hooks: PreInstallHooks) {
+ hooks.insert(hook)
+ }
+}
diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/HookedJiraStartIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/HookedJiraStartIT.kt
new file mode 100644
index 00000000..d9088d51
--- /dev/null
+++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/hook/HookedJiraStartIT.kt
@@ -0,0 +1,141 @@
+package com.atlassian.performance.tools.infrastructure.api.jira.start.hook
+
+import com.atlassian.performance.tools.infrastructure.api.distribution.PublicJiraSoftwareDistribution
+import com.atlassian.performance.tools.infrastructure.api.jira.EmptyJiraHome
+import com.atlassian.performance.tools.infrastructure.api.jira.install.ParallelInstallation
+import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpServer
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.HookedJiraInstallation
+import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PreInstallHooks
+import com.atlassian.performance.tools.infrastructure.api.jira.start.JiraLaunchScript
+import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira
+import com.atlassian.performance.tools.infrastructure.api.jvm.S3HostedJdk
+import com.atlassian.performance.tools.infrastructure.toSsh
+import com.atlassian.performance.tools.infrastructure.ubuntu.EarlyUbuntuSysstat
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import java.io.File
+import java.nio.file.Files
+import java.util.function.Consumer
+
+class HookedJiraStartIT {
+
+ @Test
+ fun shouldStartJiraWithDefaultHooks() {
+ // given
+ val hooks = PreInstallHooks.default()
+ val jiraInstallation = HookedJiraInstallation(
+ ParallelInstallation(
+ jiraHomeSource = EmptyJiraHome(),
+ productDistribution = PublicJiraSoftwareDistribution("7.13.0"),
+ jdk = S3HostedJdk()
+ ),
+ hooks
+ )
+ val jiraStart = HookedJiraStart(JiraLaunchScript(), hooks.preStart)
+ val privatePort = 8080
+ val container = SshUbuntuContainer(Consumer {
+ it.addExposedPort(privatePort)
+ })
+ val remoteReports = container.start().use { sshUbuntu ->
+ val server = TcpServer(
+ "localhost",
+ sshUbuntu.container.getMappedPort(privatePort),
+ privatePort,
+ "my-jira"
+ )
+ val remoteReports = sshUbuntu.toSsh().newConnection().use { ssh ->
+ // when
+ val installed = jiraInstallation.install(ssh, server)
+ val started = jiraStart.start(ssh, installed)
+ stop(started, ssh)
+ hooks.reports.list().flatMap { it.locate(ssh) }
+ }
+
+ // then
+ sshUbuntu.toSsh().newConnection().use { ssh ->
+ download(remoteReports, ssh)
+ }
+ return@use remoteReports
+ }
+
+ assertThat(remoteReports).contains(
+ "./jpt-vmstat.log",
+ "./jpt-iostat.log",
+ "jira-home/log/atlassian-jira.log",
+ "./atlassian-jira-software-7.13.0-standalone/logs/catalina.out",
+ "./jpt-jstat.log"
+ )
+ }
+
+ @Test
+ fun shouldDownloadPartialReportsInCaseOfFailure() {
+ // given
+ val hooks = PreInstallHooks.empty().apply {
+ insert(EarlyUbuntuSysstat())
+ postStart.insert(FailingHook())
+ }
+ val jiraInstallation = HookedJiraInstallation(
+ ParallelInstallation(
+ jiraHomeSource = EmptyJiraHome(),
+ productDistribution = PublicJiraSoftwareDistribution("7.13.0"),
+ jdk = S3HostedJdk()
+ ),
+ hooks = hooks
+ )
+ val privatePort = 8080
+ val container = SshUbuntuContainer(Consumer {
+ it.addExposedPort(privatePort)
+ })
+ val remoteReports = container.start().use { sshUbuntu ->
+ val server = TcpServer(
+ "localhost",
+ sshUbuntu.container.getMappedPort(privatePort),
+ privatePort,
+ "my-jira"
+ )
+ return@use sshUbuntu.toSsh().newConnection().use useSsh@{ ssh ->
+ // when
+ try {
+ jiraInstallation.install(ssh, server)
+ } catch (e: Exception) {
+ println("Failed: ${e.message}")
+ }
+ return@useSsh hooks.reports.list().flatMap { it.locate(ssh) }
+ }
+ }
+
+ // then
+ assertThat(remoteReports).contains(
+ "./jpt-vmstat.log",
+ "./jpt-iostat.log"
+ )
+ }
+
+ private fun stop(
+ started: StartedJira,
+ ssh: SshConnection
+ ) {
+ val installed = started.installed
+ ssh.execute("${installed.jdk.use()}; ${installed.installation}/bin/stop-jira.sh")
+ }
+
+ private fun download(
+ remotes: List,
+ ssh: SshConnection
+ ): List {
+ val downloads = Files.createTempDirectory("apt-infra-test")
+ return remotes.map { remote ->
+ val local = downloads.resolve("./$remote")
+ ssh.download(remote, local)
+ return@map local.toFile()
+ }
+ }
+}
+
+private class FailingHook : PostStartHook {
+ override fun call(ssh: SshConnection, jira: StartedJira, hooks: PostStartHooks) {
+ throw Exception("Expected failure")
+ }
+}
diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/S3HostedJdk.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/S3HostedJdk.kt
new file mode 100644
index 00000000..81188113
--- /dev/null
+++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/S3HostedJdk.kt
@@ -0,0 +1,44 @@
+package com.atlassian.performance.tools.infrastructure.api.jvm
+
+import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction
+import com.atlassian.performance.tools.jvmtasks.api.StaticBackoff
+import com.atlassian.performance.tools.ssh.api.SshConnection
+import java.net.URI
+import java.time.Duration
+
+/**
+ * Harvested from https://stash.atlassian.com/projects/JIRASERVER/repos/jira-performance-tests/pull-requests/630
+ */
+class S3HostedJdk : VersionedJavaDevelopmentKit {
+ private val jdkVersion = "1.8.0"
+ private val jdkUpdate = 131
+ private val jdkArchive = "jdk${jdkVersion}_$jdkUpdate-linux-x64.tar.gz"
+ private val jdkUrl = URI.create("https://s3.amazonaws.com/packages_java/$jdkArchive")
+ private val jdkBin = "~/jdk${jdkVersion}_$jdkUpdate/jre/bin/"
+ private val bin = "~/jdk${jdkVersion}_$jdkUpdate/bin/"
+ override val jstatMonitoring = Jstat(bin)
+
+ override fun getMajorVersion() = 8
+
+ override fun install(connection: SshConnection) {
+ download(connection)
+ connection.execute("tar -xzf $jdkArchive")
+ connection.execute("echo '${use()}' >> ~/.bashrc")
+ }
+
+ private fun download(connection: SshConnection) {
+ IdempotentAction("download JDK") {
+ connection.execute(
+ cmd = "curl -s -L -O -k $jdkUrl",
+ timeout = Duration.ofMinutes(4)
+ )
+ }.retry(
+ maxAttempts = 3,
+ backoff = StaticBackoff(Duration.ofSeconds(4))
+ )
+ }
+
+ override fun use(): String = "export PATH=$jdkBin:$bin:${'$'}PATH"
+
+ override fun command(options: String) = "${jdkBin}java $options"
+}