From 210cc0ab1bdff78fcefd80ad59e340635a21b063 Mon Sep 17 00:00:00 2001 From: Steve Siani Date: Tue, 19 Nov 2019 22:44:58 -0500 Subject: Cli executor doesn't keep/support execution context Issue-ID: CCSDK-1927 Signed-off-by: Steve Siani Change-Id: Ib417bfd62662676fe7520a5500df82ade716f66c --- .../ssh/service/BasicAuthSshClientService.kt | 133 ++++++++++++++++----- .../ssh/service/BlueprintSshClientService.kt | 10 +- .../ssh/service/BlueprintSshClientServiceTest.kt | 92 +++++++++++--- .../ssh/service/echoShell/EchoShellFactory.kt | 102 ++++++++++++++++ 4 files changed, 284 insertions(+), 53 deletions(-) create mode 100644 ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/echoShell/EchoShellFactory.kt (limited to 'ms/blueprintsprocessor/modules/commons/ssh-lib') diff --git a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt index 61baaa1ef..2885d6528 100644 --- a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt +++ b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt @@ -1,6 +1,8 @@ /* * Copyright © 2019 IBM. * + * Modifications Copyright © 2018-2019 IBM, Bell Canada + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -16,9 +18,9 @@ package org.onap.ccsdk.cds.blueprintsprocessor.ssh.service +import org.apache.commons.io.output.TeeOutputStream import org.apache.sshd.client.SshClient -import org.apache.sshd.client.channel.ChannelExec -import org.apache.sshd.client.channel.ClientChannel +import org.apache.sshd.client.channel.ChannelShell import org.apache.sshd.client.channel.ClientChannelEvent import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier import org.apache.sshd.client.session.ClientSession @@ -26,75 +28,142 @@ import org.onap.ccsdk.cds.blueprintsprocessor.ssh.BasicAuthSshClientProperties import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException import org.slf4j.LoggerFactory import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.PipedInputStream +import java.io.PipedOutputStream import java.util.Collections import java.util.EnumSet +import java.util.Scanner +import java.util.ArrayList open class BasicAuthSshClientService(private val basicAuthSshClientProperties: BasicAuthSshClientProperties) : - BlueprintSshClientService { + BlueprintSshClientService { private val log = LoggerFactory.getLogger(BasicAuthSshClientService::class.java)!! + private val newLine = "\n".toByteArray() + private var channel: ChannelShell? = null + private var teeOutput: TeeOutputStream? = null private lateinit var sshClient: SshClient private lateinit var clientSession: ClientSession - var channel: ChannelExec? = null override suspend fun startSessionNB(): ClientSession { sshClient = SshClient.setUpDefaultClient() sshClient.serverKeyVerifier = AcceptAllServerKeyVerifier.INSTANCE sshClient.start() log.debug("SSH Client Service started successfully") + clientSession = sshClient.connect( - basicAuthSshClientProperties.username, basicAuthSshClientProperties.host, - basicAuthSshClientProperties.port - ) - .verify(basicAuthSshClientProperties.connectionTimeOut) - .session + basicAuthSshClientProperties.username, basicAuthSshClientProperties.host, + basicAuthSshClientProperties.port).verify(basicAuthSshClientProperties.connectionTimeOut).session clientSession.addPasswordIdentity(basicAuthSshClientProperties.password) clientSession.auth().verify(basicAuthSshClientProperties.connectionTimeOut) + startChannel() + log.info("SSH client session($clientSession) created") return clientSession } - override suspend fun executeCommandsNB(commands: List, timeOut: Long): String { - val buffer = StringBuffer() + private fun startChannel() { try { - commands.forEach { command -> - buffer.append("\nCommand : $command") - buffer.append("\n" + executeCommandNB(command, timeOut)) + channel = clientSession.createShellChannel() + val pipedIn = PipedOutputStream() + channel!!.setIn(PipedInputStream(pipedIn)) + teeOutput = TeeOutputStream(ByteArrayOutputStream(), pipedIn) + channel!!.out = ByteArrayOutputStream() + channel!!.err = ByteArrayOutputStream() + channel!!.open() + } catch (e: Exception) { + throw BluePrintProcessorException("Failed to start Shell channel: ${e.message}") + } + } + + override suspend fun executeCommandsNB(commands: List , timeOut: Long): List { + val response = ArrayList() + try { + var stopLoop = false + val commandsIterator = commands.iterator() + while (commandsIterator.hasNext() && !stopLoop) { + val command = commandsIterator.next() + log.debug("Executing host command($command) \n") + val result = executeCommand(command, timeOut) + response.add(result) + // Once a command in the template has failed break out of the loop to stop executing further commands + if (!result.successful) { + log.debug("Template execution will stop because command ({}) has failed.", command) + stopLoop = true + } } } catch (e: Exception) { - throw BluePrintProcessorException("Failed to execute commands, below the output : $buffer") + throw BluePrintProcessorException("Failed to execute commands, below the error message : ${e.message}") } - return buffer.toString() + return response } - override suspend fun executeCommandNB(command: String, timeOut: Long): String { - log.debug("Executing host($clientSession) command($command)") + override suspend fun executeCommandNB(command: String, timeOut: Long): CommandResult { + val deviceOutput: String + var isSuccessful = true + try { + teeOutput!!.write(command.toByteArray()) + teeOutput!!.write(newLine) + teeOutput!!.flush() + deviceOutput = waitForPrompt(timeOut) + } catch (e: IOException) { + throw BluePrintProcessorException("Exception during command execution: ${e.message}", e) + } + + if (detectFailure(deviceOutput)) { + isSuccessful = false + } - channel = clientSession.createExecChannel(command) - checkNotNull(channel) { "failed to create Channel for the command : $command" } + val commandResult = CommandResult(command, deviceOutput, isSuccessful) + log.info("Command Response: ({}) $newLine", commandResult) + return commandResult + } - // TODO("Convert to streaming ") - val outputStream = ByteArrayOutputStream() - channel!!.out = outputStream - channel!!.err = outputStream - channel!!.open().await() - val waitMask = channel!!.waitFor(Collections.unmodifiableSet(EnumSet.of(ClientChannelEvent.CLOSED)), timeOut) - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - throw BluePrintProcessorException("Failed to retrieve command result in time: $command") + private fun waitForPrompt(timeOut: Long): String { + val waitMask = channel!!.waitFor( + Collections.unmodifiableSet(EnumSet.of(ClientChannelEvent.CLOSED)), timeOut) + if (channel!!.out.toString().indexOfAny(arrayListOf("$", ">", "#")) <= 0 && waitMask.contains(ClientChannelEvent.TIMEOUT)) { + throw BluePrintProcessorException("Timeout: Failed to retrieve commands result in $timeOut ms") } - val exitStatus = channel!!.exitStatus - ClientChannel.validateCommandExitStatusCode(command, exitStatus!!) - return outputStream.toString() + val outputResult = channel!!.out.toString() + channel!!.out.flush() + return outputResult } override suspend fun closeSessionNB() { - if (channel != null) + if (channel != null) { channel!!.close() + } + + if (clientSession.isOpen && !clientSession.isClosing) { + clientSession.close() + } + if (sshClient.isStarted) { sshClient.stop() } log.debug("SSH Client Service stopped successfully") } + + // TODO filter output to check error message + private fun detectFailure(output: String): Boolean { + if (output.isNotBlank()) { + // Output can be multiline, need to check if any of the line starts with % + Scanner(output).use { scanner -> + while (scanner.hasNextLine()) { + val temp = scanner.nextLine() + if (temp.isNotBlank() && (temp.trim { it <= ' ' }.startsWith("%") || + temp.trim { it <= ' ' }.startsWith("syntax error"))) { + return true + } + } + } + } + return false + } } + +data class CommandResult(val command: String, val deviceOutput: String, val successful: Boolean) diff --git a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt index 724c4277d..27ebf50bc 100644 --- a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt +++ b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt @@ -1,6 +1,8 @@ /* * Copyright © 2019 IBM. * + * Modifications Copyright © 2018-2019 IBM, Bell Canada + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -25,11 +27,11 @@ interface BlueprintSshClientService { startSessionNB() } - fun executeCommands(commands: List, timeOut: Long): String = runBlocking { + fun executeCommands(commands: List, timeOut: Long): List = runBlocking { executeCommandsNB(commands, timeOut) } - fun executeCommand(command: String, timeOut: Long): String = runBlocking { + fun executeCommand(command: String, timeOut: Long): CommandResult = runBlocking { executeCommandNB(command, timeOut) } @@ -39,9 +41,9 @@ interface BlueprintSshClientService { suspend fun startSessionNB(): ClientSession - suspend fun executeCommandsNB(commands: List, timeOut: Long): String + suspend fun executeCommandsNB(commands: List, timeOut: Long): List - suspend fun executeCommandNB(command: String, timeOut: Long): String + suspend fun executeCommandNB(command: String, timeOut: Long): CommandResult suspend fun closeSessionNB() } diff --git a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt index 683816f7f..3785a21b2 100644 --- a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt +++ b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt @@ -1,6 +1,8 @@ /* * Copyright © 2019 IBM. * + * Modifications Copyright © 2018-2019 IBM, Bell Canada + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -25,18 +27,22 @@ import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider import org.apache.sshd.server.session.ServerSession import org.apache.sshd.server.shell.ProcessShellCommandFactory -import org.junit.runner.RunWith import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintPropertiesService import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintPropertyConfiguration import org.onap.ccsdk.cds.blueprintsprocessor.ssh.BluePrintSshLibConfiguration +import org.onap.ccsdk.cds.blueprintsprocessor.ssh.service.echoShell.EchoShellFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource import org.springframework.test.context.junit4.SpringRunner import java.nio.file.Paths +import org.junit.runner.RunWith +import kotlin.test.BeforeTest +import kotlin.test.AfterTest import kotlin.test.Test -import kotlin.test.assertEquals +import kotlin.test.assertTrue import kotlin.test.assertNotNull +import kotlin.test.assertEquals @RunWith(SpringRunner::class) @ContextConfiguration( @@ -57,29 +63,76 @@ class BlueprintSshClientServiceTest { @Autowired lateinit var bluePrintSshLibPropertyService: BluePrintSshLibPropertyService - @Test - fun testBasicAuthSshClientService() { + lateinit var bluePrintSshLibPropertyServiceMock: BluePrintSshLibPropertyService + + private lateinit var sshServer: SshServer + + @BeforeTest + fun startShellServer() { runBlocking { - val sshServer = setupTestServer("localhost", 52815, "root", "dummyps") + println("Start local Shell server") + sshServer = setupTestServer("localhost", 52815, "root", "dummyps") sshServer.start() println(sshServer) - val bluePrintSshLibPropertyService = bluePrintSshLibPropertyService.blueprintSshClientService("sample") - val sshSession = bluePrintSshLibPropertyService.startSession() - val response = bluePrintSshLibPropertyService.executeCommandsNB(arrayListOf("echo '1'", "echo '2'"), 2000) - assertNotNull(response, "failed to get command response") - bluePrintSshLibPropertyService.closeSession() - sshServer.stop(true) } } - private fun setupTestServer(host: String, port: Int, userName: String, password: String): SshServer { + @AfterTest + fun stopShellServer() { + println("End the Shell server") + sshServer.stop(true) + } + + @Test + fun testStartSessionNB() { + val clientSession = getSshClientService().startSession() + assertNotNull(clientSession, "Failed to start ssh session with server") + } + + @Test + fun testBasicAuthSshClientService() { + runBlocking { + val blueprintSshClientService = getSshClientService() + blueprintSshClientService.startSession() + // Preparing response + val commandResults = arrayListOf() + commandResults.add(CommandResult("echo 1", "echo 1\n#", true)) + commandResults.add(CommandResult("echo 2", "echo 1\n#echo 2\n#", true)) + val response = blueprintSshClientService.executeCommands(arrayListOf("echo 1", "echo 2"), 2000) + blueprintSshClientService.closeSession() + + assertEquals(response, commandResults, "failed to get command responses") + } + } + + @Test + fun `testBasicAuthSshClientService single execution command`() { + runBlocking { + val blueprintSshClientService = getSshClientService() + blueprintSshClientService.startSession() + val response = blueprintSshClientService.executeCommand("echo 1", 2000) + blueprintSshClientService.closeSession() + + assertEquals(response, CommandResult("echo 1", "echo 1\n#", true), "failed to get command response") + } + } + + @Test + fun testCloseSessionNB() { + val bluePrintSshLibPropertyService = bluePrintSshLibPropertyService.blueprintSshClientService("sample") + val clientSession = bluePrintSshLibPropertyService.startSession() + bluePrintSshLibPropertyService.closeSession() + assertTrue(clientSession.isClosed, "Failed to close ssh session with server") + } + + private fun setupTestServer(host: String, port: Int, username: String, password: String): SshServer { val sshd = SshServer.setUpDefaultServer() sshd.port = port sshd.host = host sshd.keyPairProvider = createTestHostKeyProvider() - sshd.passwordAuthenticator = BogusPasswordAuthenticator(userName, password) + sshd.passwordAuthenticator = BogusPasswordAuthenticator(username, password) sshd.publickeyAuthenticator = AcceptAllPublickeyAuthenticator.INSTANCE - // sshd.shellFactory = EchoShellFactory() + sshd.shellFactory = EchoShellFactory.INSTANCE sshd.commandFactory = ProcessShellCommandFactory.INSTANCE return sshd } @@ -90,12 +143,17 @@ class BlueprintSshClientServiceTest { keyProvider.algorithm = RSA_ALGORITHM return keyProvider } + + private fun getSshClientService(): BlueprintSshClientService { + return bluePrintSshLibPropertyService.blueprintSshClientService("sample") + } } -class BogusPasswordAuthenticator(userName: String, password: String) : PasswordAuthenticator { +class BogusPasswordAuthenticator(private val usr: String, private val pwd: String) : PasswordAuthenticator { + override fun authenticate(username: String, password: String, serverSession: ServerSession): Boolean { - assertEquals(username, "root", "failed to match username") - assertEquals(password, "dummyps", "failed to match password") + assertEquals(username, usr, "failed to match username") + assertEquals(password, pwd, "failed to match password") return true } } diff --git a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/echoShell/EchoShellFactory.kt b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/echoShell/EchoShellFactory.kt new file mode 100644 index 000000000..9d308202f --- /dev/null +++ b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/echoShell/EchoShellFactory.kt @@ -0,0 +1,102 @@ +/* + * Copyright © 2019 IBM. Bell Canada + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onap.ccsdk.cds.blueprintsprocessor.ssh.service.echoShell + +import org.apache.sshd.common.Factory +import org.apache.sshd.server.Environment +import org.apache.sshd.server.command.Command +import org.apache.sshd.server.ExitCallback +import java.io.InputStream +import java.io.OutputStream +import java.io.IOException +import java.io.BufferedReader +import java.io.InputStreamReader +import java.io.InterruptedIOException + +class EchoShellFactory : Factory { + + override fun create(): Command { + return EchoShell() + } + + companion object { + val INSTANCE = EchoShellFactory() + } +} + +class EchoShell : Command, Runnable { + + var `in`: InputStream? = null + private set + var out: OutputStream? = null + private set + var err: OutputStream? = null + private set + private var callback: ExitCallback? = null + var environment: Environment? = null + private set + private var thread: Thread? = null + + override fun setInputStream(`in`: InputStream) { + this.`in` = `in` + } + + override fun setOutputStream(out: OutputStream) { + this.out = out + } + + override fun setErrorStream(err: OutputStream) { + this.err = err + } + + override fun setExitCallback(callback: ExitCallback) { + this.callback = callback + } + + @Throws(IOException::class) + override fun start(env: Environment) { + environment = env + thread = Thread(this, "EchoShell") + thread!!.isDaemon = true + thread!!.start() + } + + override fun destroy() { + thread!!.interrupt() + } + + override fun run() { + val r = BufferedReader(InputStreamReader(`in`)) + try { + while (true) { + val s = r.readLine() ?: return + out!!.write((s + "\n").toByteArray()) + out!!.write("#".toByteArray()) + out!!.flush() + if ("exit" == s) { + return + } + } + } catch (e: InterruptedIOException) { + // Ignore + } catch (e: Exception) { + e.printStackTrace() + } finally { + callback!!.onExit(0) + } + } +} -- cgit 1.2.3-korg