camilosampedro/Aton

View on GitHub
app/services/impl/SSHOrderServiceImpl.scala

Summary

Maintainability
C
1 day
Test Coverage
package services.impl

import java.io.{BufferedReader, IOException, InputStreamReader}
import java.net.NoRouteToHostException
import java.sql.Timestamp
import java.util.Calendar

import com.google.inject.{Inject, Singleton}
import com.jcraft.jsch.{ChannelExec, JSch, JSchException}
import dao.{SSHOrderDAO, SSHOrderToComputerDAO}
import fr.janalyse.ssh.{Expect, SSHCommand, SSHOptions}
import model._
import services.SSHOrderService
import services.exec.SSHFunction._
import services.state.ActionState
import services.state

import scala.concurrent.{Await, ExecutionContext, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.duration._

/**
  * @author Camilo Sampedro <camilo.sampedro@udea.edu.co>
  */
@Singleton
class SSHOrderServiceImpl @Inject()(sSHOrderDAO: SSHOrderDAO, sSHOrderToComputerDAO: SSHOrderToComputerDAO)(implicit executionContext: ExecutionContext) extends SSHOrderService {

  private val connectTimeout = 15000

  @throws(classOf[JSchException])
  override def execute(computer: Computer, sshOrder: SSHOrder): (String, Int) = {
    play.Logger.debug(s"""Executing: $sshOrder into: $computer""")
    // Sudofy ssh order if it is intended to be executed by super user
    val newSSHOrder = if(sshOrder.superUser){
      play.Logger.debug("sudofying")
      sshOrder.copy(command = sudofy(sshOrder.command))
    } else {
      sshOrder
    }

    // Create the order on the database (Before execution state)
    val future = sSHOrderDAO.add(newSSHOrder).map {
      case state.ActionCompletedWithId(id) =>
        // ID will be the identifier generated by the database engine
        val settings = generateSSHSettings(computer, newSSHOrder)
        // Save result and exit code of the execution
        val (result, exitCode) = if (newSSHOrder.superUser) {
          // Execute with a workaround
          executeWithSudoWorkaround(newSSHOrder, settings)
          //jassh.SSH.once(settings)(_.executeWithStatus("sudo " + sshOrder.command))
        } else {
          // Execute directly with JASSH library
          jassh.SSH.once(settings)(_.executeWithStatus(newSSHOrder.command))
        }
        play.Logger.debug(s"${sshOrder.command} - Execution result: ($result, $exitCode)")
        // Pack results into a SSHOrderToComputer object and then save it on the database (After execution state)
        val resultSSHOrder = SSHOrderToComputer(computer.ip, id, now, Some(result), Some(exitCode))
        sSHOrderToComputerDAO.add(resultSSHOrder)

        // Return the results on this future
        (result, exitCode)
      case _ =>
        ("", 0)
    }

    // Wait for the previously created "future" task
    Await.result(future, Duration.Inf)
  }


  def executeUntilResult(computer: Computer, sshOrders: Seq[SSHOrder]): (String, Int) = {
    play.Logger.debug(s"""Executing: $sshOrders into: $computer""")
    val joinedSSHOrder = sshOrders.headOption match {
      case Some(sshOrder) =>
        sshOrder.copy(command = sshOrders.map(_.command).mkString(" & "))
      case _ =>
        return ("", 1)
    }
    val future = sSHOrderDAO.add(joinedSSHOrder).map {
      case state.ActionCompletedWithId(id) =>
        val settings = generateSSHSettings(computer, joinedSSHOrder)
        val (result, exitCode) = if (joinedSSHOrder.superUser) {
          executeWithSudoWorkaround(joinedSSHOrder, settings)
          jassh.SSH.shell(settings) { ssh =>
            /*ssh.executeWithExpects("sudo -S su", List(new Expect(_.contains("password"), settings.password.password.getOrElse(""))))
            ssh.become("root", settings.password.password)*/

            ssh.executeWithExpects("""SUDO_PROMPT="prompt" sudo -S su -""", List(Expect(_.endsWith("prompt"), settings.password.password.getOrElse(""))))
            val (result, exitCode) = ssh.executeWithStatus(joinedSSHOrder.command)
            ssh.execute("exit")
            (result, exitCode)
          }
          //jassh.SSH.once(settings)(_.executeWithStatus("sudo " + sshOrder.command))
        } else {
          jassh.SSH.once(settings) { ssh =>
            var (result, exitStatus) = ("", 1)
            val commands = sshOrders.map(_.command)
            var i = 0
            while (i < commands.size && result == "" && exitStatus != 0) {
              val command = commands(i)
              val (newResult, newExit) = ssh.executeWithStatus(SSHCommand.stringToCommand(command))
              result = newResult
              exitStatus = newExit
              i += 1
            }
            (result, exitStatus)
          }
        }
        val resultSSHOrder = SSHOrderToComputer(computer.ip, id, now, Some(result), Some(exitCode))
        sSHOrderToComputerDAO.add(resultSSHOrder)
        (result, exitCode)
      case _ =>
        ("", 0)
    }
    Await.result(future, 30.seconds)
  }

  override def getMac(computer: Computer, operatingSystem: Option[String])(implicit username: String): Option[String] = {
    //play.Logger.debug(s"""Looking for mac of "${computer.ip}"""")
    val orders = operatingSystem match {
      case Some(os) => macOrders(translateOS(os))
      case _ => macOrders("")
    }
    //play.Logger.debug(s"""Trying "${order}"""")
    try {
      val (result, a) = executeUntilResult(computer, orders.map(new SSHOrder(now, _, username)))
      //play.Logger.debug(s"""Result: $result""")
      if (result != "") {
        Some(result)
      } else {
        None
      }
    } catch {
      case e: JSchException => None
      case e: Exception => play.Logger.error("An error occurred while looking for computer's mac: " + computer, e)
        None
    }

  }

  @throws[JSchException]
  override def shutdown(computer: Computer)(implicit username: String): ActionState = {
    execute(computer, new SSHOrder(now, superUser = true, interrupt = false, command = shutdownOrder, username = username))
    state.ActionCompleted
  }

  private def now = new Timestamp(Calendar.getInstance().getTime.getTime)

  @throws[JSchException]
  override def upgrade(computer: Computer,computerState: ComputerState)(implicit username: String): ActionState = {
    val order = upgradeOrder(translateOS(computerState.operatingSystem.getOrElse("")))
    val (result, exitCode) = execute(computer, new SSHOrder(now, superUser = true, interrupt = false, command = order, username = username))
    if (exitCode == 0) {
      state.ActionCompleted
    } else {
      state.OrderFailed(result, exitCode)
    }
  }

  @throws[JSchException]
  override def unfreeze(computer: Computer)(implicit username: String): ActionState = ???

  @throws[JSchException]
  override def getOperatingSystem(computer: Computer)(implicit username: String): Option[String] = {
    try {
      val (result, exitCode) = execute(computer, new SSHOrder(now, superUser = false, interrupt = true, command = operatingSystemCheck, username = username))
      if (exitCode == 0) Some(result) else None
    }
    catch {
      case e: Exception => None
    }
  }


  override def check(computer: Computer)(implicit username: String): (ComputerState, Seq[ConnectedUser]) = {
    try {
      val state = checkState(computer)
      play.Logger.debug(s"""$computer is  $state""")
      val date = now
      val (operatingSystem, mac, whoIsUsing) = if (state != Connected()) {
        (None, None, Seq.empty)
      } else {
        val os = getOperatingSystem(computer)
        (os, getMac(computer, os), whoAreUsing(computer).map { username => ConnectedUser(0, username, computer.ip, date) })
      }
      (ComputerState(computer.ip, date, state.id, operatingSystem, mac), whoIsUsing)
    } catch {
      case e: Exception => play.Logger.error(s"There was an error checking $computer's state")
        (ComputerState(computer.ip, now, NotConnected().id, None, None), Seq.empty)
    }

  }

  override def checkState(computer: Computer)(implicit username: String): StateRef = {
    val sSHOrder = new SSHOrder(now, false, false, dummy, username)
    val settings = generateSSHSettings(computer, sSHOrder)
    try {
      val result = jassh.SSH.shell(settings){ shell =>
        if(shell.executeWithStatus(sSHOrder.command)._1 == "Ping from Aton"){
          if (shell.sudoSuMinusWithCommandTest()){
            0
          } else {
            1
          }
        } else {
          2
        }
      }
      result match {
        case 0 => Connected()
        case 1 => WithoutSudoRights()
        case _ => NotConnected()
      }
    } catch {
      case ex: JSchException =>
        ex.getMessage match {
          case "Auth fail" => AuthFailed()
          case e if e.contains("Too many authentication failures") => AuthFailed()
          case "timeout: socket is not established" => NotConnected()
          case "Session.connect: java.net.SocketTimeoutException: Read timed out" => NotConnected()
          case "java.net.NoRouteToHostException: No route to host" => NotConnected()
          case e => play.Logger.error(s"The error checking $computer was : " + e)
            UnknownError()
        }
      case e: NoRouteToHostException =>play.Logger.error("There was an error checking for " + computer + "'s state", e)
        NotConnected()
      case e: Exception => play.Logger.error("There was an error checking for " + computer + "'s state", e)
        UnknownError()
    }
  }



  private def generateSSHSettings(computer: Computer, sSHOrder: SSHOrder) = SSHOptions(host = computer.ip, username = computer.SSHUser, password = computer.SSHPassword, connectTimeout = connectTimeout, prompt = Some("prompt"))

  @throws[JSchException]
  override def whoAreUsing(computer: Computer)(implicit username: String): Seq[String] = {
    try {
      val (result, _) = execute(computer, new SSHOrder(now, false, false, userListOrder, username))
      for (user <- result.split("\n") if user != "") yield user
    } catch {
      case e: Exception => Seq()
    }
  }

  private def executeWithSudoWorkaround(sshOrder: SSHOrder, settings: SSHOptions): (String,Int) = {
    play.Logger.debug(s"""Trying sudo workaround with $sshOrder""")
    settings.password.password match {
      case Some(password) =>
        val jsch = new JSch()
        val sshSession = jsch.getSession(settings.username, settings.host, settings.port)
        sshSession.setConfig("StrictHostKeyChecking", "no")
        sshSession.setPassword(password)
        sshSession.connect()
        play.Logger.debug("Connected!")
        val channel: ChannelExec = sshSession.openChannel("exec").asInstanceOf[ChannelExec]
        play.Logger.debug("Channel created")
        val (result, exitCode) = try {
          val stream = channel.getInputStream
          val out = channel.getOutputStream
          val err = channel.getErrStream
          channel.setCommand(sshOrder.command)
          play.Logger.debug("Command sent")
          channel.connect()
          play.Logger.debug("Channel connected")
          out.write((password + "\n").getBytes())
          play.Logger.debug("Wrote seccond password")
          out.flush()
          play.Logger.debug("Everything sent")
          if (sshOrder.interrupt) {
            Thread.sleep(1000)
            out.write("~.\n".getBytes())
            out.flush()
            play.Logger.debug("Interrupted succesfully")
            ("Interrupted succesfully", 0)
          } else {
            play.Logger.debug("Reading output")
            val readerInput = new BufferedReader(new InputStreamReader(stream))
            val readerErr = new BufferedReader(new InputStreamReader(err))
            val lines = Stream.continually(readerInput.readLine()).takeWhile{ line=>
                play.Logger.debug(line)
                line != null
            }.toList:::Stream.continually(readerErr.readLine()).takeWhile(_ != null).toList
            play.Logger.debug(s"Lines gotten: $lines")
            (lines.mkString("\n"), 0)
          }
        } catch {
          case e: IOException =>
            play.Logger.error("An exception occurred while creating stream executing a sudo action", e)
            ("Error, check aton log", 1)
          case e: JSchException =>
            play.Logger.error("SSH Exception ocurred while executing ssh sudo action", e)
            ("Error, check aton log", 1)
          case e: Exception =>
            play.Logger.error("Something unexpected happened executing with sudo", e)
            ("Error, check aton log", 1)
        }
        val exitStatus = channel.getExitStatus
        play.Logger.debug(s"Exit status: $exitStatus")
        if (channel != null) {
          channel.disconnect()
          play.Logger.debug("Channel disconnected")
        }
        if (sshSession != null) {
          play.Logger.debug("Session disconnected")
          sshSession.disconnect()
        }
        (result, exitStatus)
      case _ =>
        play.Logger.error("There is not a password for executing")
        ("There is not a password for executing",1)
    }

  }

  override def execute(computer: Computer, superUser: Boolean, interrupt: Boolean, command: String)(implicit username: String): (String, Int) = {
    execute(computer, new SSHOrder(now, superUser, interrupt, command, username))
  }

  override def blockPage(computer: Computer, page: String)(implicit username: String): ActionState = {
    if (execute(computer, new SSHOrder(now,superUser = true,interrupt= false,blockPageOrder(page),username ))._2 == 0) {
      state.ActionCompleted
    } else {
      state.Failed
    }
  }

  override def sendMessage(computer: Computer, message: String, users: Seq[ConnectedUser])(implicit username: String): ActionState = {

    val actionStates = users.map{user=>
      try {
        val (result,exitCode) = execute(computer, new SSHOrder(now,superUser = false, interrupt= false, notificationOrder(user.username,message),username))
        state.OrderCompleted(result, exitCode)
      } catch {
        case e: JSchException => play.Logger.error(s"There was a SSH error sending messages to the $computer",e)
          state.Failed
        case e: Exception =>play.Logger.error(s"There was a non SSH error sending messages to the $computer",e)
          state.Failed
      }

    }
    if (actionStates.exists(!_.isInstanceOf[state.OrderCompleted])){
      state.Failed
    } else {
      state.ActionCompleted
    }
  }

  override def installAPackage(computer: Computer, programs: List[String]): ActionState = ???

  override def listAll: Future[Seq[SSHOrder]] = sSHOrderDAO.listAll

  override def get(id: Long): Future[Option[SSHOrder]] = sSHOrderDAO.get(id)

  override def delete(id: Long): Future[ActionState] = sSHOrderDAO.delete(id)
}