Skip to content

Commit

Permalink
Merge branch 'enhancement/log-viewer-widget' into feature/381
Browse files Browse the repository at this point in the history
  • Loading branch information
mprzypasniak99 authored Oct 17, 2021
2 parents 954f9af + 2c433cd commit e28ceab
Show file tree
Hide file tree
Showing 33 changed files with 995 additions and 4 deletions.
1 change: 1 addition & 0 deletions cogboard-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
}
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.0")
implementation(kotlin("stdlib-jdk8"))
implementation("com.jcraft:jsch:0.1.55")

testImplementation("org.assertj:assertj-core:3.12.2")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.4.2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ class CogboardConstants {
const val SCHEDULE_PERIOD = "schedulePeriod"
const val SCHEDULE_PERIOD_DEFAULT = 120L // 120 seconds
const val SCHEDULE_DELAY_DEFAULT = 10L // 10 seconds
const val SSH_TIMEOUT = 5000 // 5000ms -> 5s
const val SSH_HOST = "sshAddress"
const val SSH_KEY = "sshKey"
const val SSH_KEY_PASSPHRASE = "sshKeyPassphrase"
const val URL = "url"

const val LOG_LINES = "logLines"
const val LOG_FILE_PATH = "logFilePath"
const val REQUEST_ID = "requestId"
const val PUBLIC_URL = "publicUrl"
const val USER = "user"
Expand Down Expand Up @@ -109,6 +113,7 @@ class CogboardConstants {
const val HTTP_POST = "cogboard.httpclient.post"
const val HTTP_PUT = "cogboard.httpclient.put"
const val HTTP_DELETE = "cogboard.httpclient.delete"
const val SSH_COMMAND = "cogboard.sshclient.ssh"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.cognifide.cogboard.ssh

import com.cognifide.cogboard.CogboardConstants
import com.cognifide.cogboard.ssh.auth.SSHAuthData
import com.cognifide.cogboard.ssh.session.SessionStrategyFactory
import com.jcraft.jsch.ChannelExec
import com.jcraft.jsch.JSch
import com.jcraft.jsch.JSchException
import com.jcraft.jsch.Session
import io.vertx.core.AbstractVerticle
import io.vertx.core.buffer.Buffer
import io.vertx.core.eventbus.MessageConsumer
import io.vertx.core.json.JsonObject
import io.vertx.core.logging.Logger
import io.vertx.core.logging.LoggerFactory
import java.io.InputStream

class SSHClient : AbstractVerticle() {
private lateinit var session: Session
private lateinit var jsch: JSch
private lateinit var channel: ChannelExec
private lateinit var sshInputStream: InputStream
private lateinit var consumer: MessageConsumer<JsonObject>
override fun start() {
registerSSHCommand()
super.start()
}

override fun stop() {
consumer.unregister()
super.stop()
}

private fun registerSSHCommand() {
consumer = vertx.eventBus()
.consumer<JsonObject>(CogboardConstants.Event.SSH_COMMAND)
.handler { message ->
message.body()?.let {
tryToConnect(it)
}
}
}

private fun tryToConnect(config: JsonObject) {
val eventBusAddress = config.getString(CogboardConstants.Props.EVENT_ADDRESS)
try {
connect(config)
} catch (e: JSchException) {
LOGGER.error(e.message)
vertx.eventBus().send(eventBusAddress, e)
}
}

private fun connect(config: JsonObject) {
val authData = SSHAuthData(config)
createSSHChannel(authData)
executeCommandAndSendResult(config)
}

private fun createSSHChannel(authData: SSHAuthData) {
with(authData) {
initSSHSession(authData)
if (session.isConnected) {
createChannel(createCommand())
}
}
}

private fun initSSHSession(authData: SSHAuthData) {
jsch = JSch()
jsch.setKnownHosts("~/.ssh/known_hosts")
val session = SessionStrategyFactory(jsch).create(authData).initSession()
session.connect(CogboardConstants.Props.SSH_TIMEOUT)
}

private fun createChannel(command: String) {
channel = session.openChannel("exec") as ChannelExec
channel.setCommand(command)
channel.inputStream = null
sshInputStream = channel.inputStream
channel.connect(CogboardConstants.Props.SSH_TIMEOUT)
}

private fun executeCommandAndSendResult(config: JsonObject) {
val eventBusAddress = config.getString(CogboardConstants.Props.EVENT_ADDRESS)
val responseBuffer = Buffer.buffer()
responseBuffer.appendBytes(sshInputStream.readAllBytes())
vertx.eventBus().send(eventBusAddress, responseBuffer)
channel.disconnect()
session.disconnect()
}

companion object {
val LOGGER: Logger = LoggerFactory.getLogger(SSHClient::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.cognifide.cogboard.ssh.auth

enum class AuthenticationType {
BASIC,
TOKEN,
SSH_KEY
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.cognifide.cogboard.ssh.auth

import com.cognifide.cogboard.CogboardConstants
import com.cognifide.cogboard.ssh.auth.AuthenticationType.BASIC
import com.cognifide.cogboard.ssh.auth.AuthenticationType.TOKEN
import com.cognifide.cogboard.ssh.auth.AuthenticationType.SSH_KEY
import io.vertx.core.json.Json
import io.vertx.core.json.JsonArray
import io.vertx.core.json.JsonObject

class SSHAuthData(private val config: JsonObject) {
val user = config.getString(CogboardConstants.Props.USER) ?: ""
val password = config.getString(CogboardConstants.Props.PASSWORD) ?: ""
val token = config.getString(CogboardConstants.Props.TOKEN) ?: ""
val key = config.getString(CogboardConstants.Props.SSH_KEY) ?: ""
val host = config.getString(CogboardConstants.Props.SSH_HOST) ?: ""
val authenticationType = fromConfigAuthenticationType()

private fun fromConfigAuthenticationType(): AuthenticationType {
val authTypesString = config.getString(CogboardConstants.Props.AUTHENTICATION_TYPES)

val authTypes = authTypesString?.let { Json.decodeValue(authTypesString) } ?: JsonArray()

return (authTypes as JsonArray)
.map { AuthenticationType.valueOf(it.toString()) }
.firstOrNull { hasAuthTypeCorrectCredentials(it) } ?: BASIC
}

private fun hasAuthTypeCorrectCredentials(authType: AuthenticationType): Boolean =
when {
authType == TOKEN && user.isNotBlank() && token.isNotBlank() -> true
authType == SSH_KEY && key.isNotBlank() -> true
else -> authType == BASIC && user.isNotBlank() && password.isNotBlank()
}

fun getAuthenticationString(): String =
when (authenticationType) {
BASIC -> config.getString(CogboardConstants.Props.PASSWORD)
TOKEN -> config.getString(CogboardConstants.Props.TOKEN)
SSH_KEY -> config.getString(CogboardConstants.Props.SSH_KEY)
}

fun createCommand(): String {
val logLines = config.getString(CogboardConstants.Props.LOG_LINES) ?: "0"
val logFilePath = config.getString(CogboardConstants.Props.LOG_FILE_PATH) ?: ""

return "cat $logFilePath | tail -$logLines"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.cognifide.cogboard.ssh.session

import com.cognifide.cogboard.ssh.auth.AuthenticationType.BASIC
import com.cognifide.cogboard.ssh.auth.AuthenticationType.TOKEN
import com.cognifide.cogboard.ssh.auth.AuthenticationType.SSH_KEY
import com.cognifide.cogboard.ssh.auth.SSHAuthData
import com.cognifide.cogboard.ssh.session.strategy.BasicAuthSessionStrategy
import com.cognifide.cogboard.ssh.session.strategy.SSHKeyAuthSessionStrategy
import com.cognifide.cogboard.ssh.session.strategy.SessionStrategy
import com.jcraft.jsch.JSch

class SessionStrategyFactory(private val jsch: JSch) {
fun create(authData: SSHAuthData): SessionStrategy =
when (authData.authenticationType) {
BASIC, TOKEN -> {
BasicAuthSessionStrategy(jsch, authData)
}
SSH_KEY -> {
SSHKeyAuthSessionStrategy(jsch, authData)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.cognifide.cogboard.ssh.session.strategy

import com.cognifide.cogboard.ssh.auth.SSHAuthData
import com.jcraft.jsch.JSch
import com.jcraft.jsch.Session

class BasicAuthSessionStrategy(jsch: JSch, authData: SSHAuthData) : SessionStrategy(jsch, authData) {

override fun initSession(): Session {
val session = jsch.getSession(authData.user, authData.host)
session.setPassword(securityString)

return session
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.cognifide.cogboard.ssh.session.strategy

import com.cognifide.cogboard.ssh.auth.SSHAuthData
import com.jcraft.jsch.JSch
import com.jcraft.jsch.Session

class SSHKeyAuthSessionStrategy(jSch: JSch, authData: SSHAuthData) : SessionStrategy(jSch, authData) {
override fun initSession(): Session {
if (authData.password == "") {
jsch.addIdentity(securityString)
} else {
jsch.addIdentity(securityString, authData.password)
}
val session = jsch.getSession(authData.user, authData.host)
session.setConfig("PreferredAuthentications", "publickey")

return session
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.cognifide.cogboard.ssh.session.strategy

import com.cognifide.cogboard.ssh.auth.SSHAuthData
import com.jcraft.jsch.JSch
import com.jcraft.jsch.Session

abstract class SessionStrategy(protected val jsch: JSch, protected val authData: SSHAuthData) {
protected val securityString: String
get() = authData.getAuthenticationString()

abstract fun initSession(): Session
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.cognifide.cogboard.widget

import com.cognifide.cogboard.CogboardConstants.Props
import com.cognifide.cogboard.CogboardConstants.Event
import com.cognifide.cogboard.config.service.BoardsConfigService
import io.vertx.core.Vertx
import io.vertx.core.eventbus.MessageConsumer
import io.vertx.core.json.JsonObject
import java.nio.Buffer

abstract class SSHWidget(
vertx: Vertx,
config: JsonObject,
serv: BoardsConfigService
) : AsyncWidget(vertx, config, serv) {
val sshKey: String = config.endpointProp(Props.SSH_KEY)
val host: String = config.endpointProp(Props.SSH_HOST)
val logPath: String = config.endpointProp(Props.LOG_FILE_PATH)
val logLines: String = config.endpointProp(Props.LOG_LINES)
private lateinit var sshConsumer: MessageConsumer<Buffer>

fun registerForSSH(eventBusAddress: String) {
sshConsumer = vertx.eventBus()
.consumer<Buffer>(eventBusAddress)
.handler {
handleSSHResponse(it.body())
}
}

abstract fun handleSSHResponse(body: Buffer?)

fun unregisterFromSSH() {
if (::sshConsumer.isInitialized) {
sshConsumer.unregister()
}
}

fun sendRequestForLogs(config: JsonObject) {
ensureConfigIsPrepared(config)
vertx.eventBus().send(Event.SSH_COMMAND, config)
}

private fun ensureConfigIsPrepared(config: JsonObject) {
config.getString(Props.USER) ?: config.put(Props.USER, user)
config.getString(Props.PASSWORD) ?: config.put(Props.PASSWORD, password)
config.getString(Props.TOKEN) ?: config.put(Props.TOKEN, token)
config.getString(Props.SSH_KEY) ?: config.put(Props.SSH_KEY, sshKey)
config.getString(Props.SSH_HOST) ?: config.put(Props.SSH_HOST, host)
config.getString(Props.LOG_FILE_PATH) ?: config.put(Props.LOG_FILE_PATH, logPath)
config.getString(Props.LOG_LINES) ?: config.put(Props.LOG_LINES, logLines)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.cognifide.cogboard.widget.type.*
import com.cognifide.cogboard.widget.type.randompicker.RandomPickerWidget
import com.cognifide.cogboard.widget.type.sonarqube.SonarQubeWidget
import com.cognifide.cogboard.widget.type.zabbix.ZabbixWidget
import com.cognifide.cogboard.widget.type.LogViewerWidget
import io.vertx.core.Vertx
import io.vertx.core.json.JsonArray
import io.vertx.core.json.JsonObject
Expand Down Expand Up @@ -36,7 +37,7 @@ class WidgetIndex {
"Service Check" to ServiceCheckWidget::class.java,
"SonarQube" to SonarQubeWidget::class.java,
"White Space" to WhiteSpaceWidget::class.java,
"Log Viewer" to LogViewerWidget::class.java
"Log Viewer" to LogViewerWidget::class.java,
)

fun availableWidgets(): JsonArray {
Expand Down
Loading

0 comments on commit e28ceab

Please sign in to comment.