Cli executor doesn't keep/support execution context 80/98680/12
authorSteve Siani <alphonse.steve.siani.djissitchi@ibm.com>
Wed, 20 Nov 2019 03:44:58 +0000 (22:44 -0500)
committerSteve Siani <alphonse.steve.siani.djissitchi@ibm.com>
Tue, 10 Dec 2019 02:44:01 +0000 (21:44 -0500)
Issue-ID: CCSDK-1927
Signed-off-by: Steve Siani <alphonse.steve.siani.djissitchi@ibm.com>
Change-Id: Ib417bfd62662676fe7520a5500df82ade716f66c

ms/blueprintsprocessor/functions/cli-executor/src/main/kotlin/internal/scripts/InternalSimpleCli.kt
ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt
ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt
ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt
ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/echoShell/EchoShellFactory.kt [new file with mode: 0644]

index b3708a0..a0dfb8b 100644 (file)
@@ -1,6 +1,8 @@
 /*
  *  Copyright © 2019 IBM.
  *
+ *  Modifications 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
@@ -23,7 +25,7 @@ import org.onap.ccsdk.cds.blueprintsprocessor.functions.cli.executor.cliDeviceIn
 import org.onap.ccsdk.cds.blueprintsprocessor.functions.cli.executor.getSshClientService
 import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractScriptComponentFunction
 import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.ComponentScriptExecutor
-import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonType
 import org.slf4j.LoggerFactory
 
 open class TestCliScriptFunction : AbstractScriptComponentFunction() {
@@ -70,7 +72,7 @@ open class Check : AbstractScriptComponentFunction() {
         sshClientService.closeSessionNB()
 
         // Set the Response Data
-        setAttribute(ComponentScriptExecutor.ATTRIBUTE_RESPONSE_DATA, responseLog.asJsonPrimitive())
+        setAttribute(ComponentScriptExecutor.ATTRIBUTE_RESPONSE_DATA, responseLog.asJsonType())
 
         log.info("Executing process")
     }
index 61baaa1..2885d65 100644 (file)
@@ -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<String>, 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 <String>, timeOut: Long): List<CommandResult> {
+        val response = ArrayList<CommandResult>()
+        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)
index 724c427..27ebf50 100644 (file)
@@ -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<String>, timeOut: Long): String = runBlocking {
+    fun executeCommands(commands: List<String>, timeOut: Long): List<CommandResult> = 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<String>, timeOut: Long): String
+    suspend fun executeCommandsNB(commands: List<String>, timeOut: Long): List<CommandResult>
 
-    suspend fun executeCommandNB(command: String, timeOut: Long): String
+    suspend fun executeCommandNB(command: String, timeOut: Long): CommandResult
 
     suspend fun closeSessionNB()
 }
index 683816f..3785a21 100644 (file)
@@ -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<CommandResult>()
+            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 (file)
index 0000000..9d30820
--- /dev/null
@@ -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<Command> {
+
+    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)
+        }
+    }
+}