DSL for executing commands over SSH

DSL is quite a popular technique to help a developer to define program or business logic in a more readable and concise way compared to using the general-purpose language features. There are two types of DSLs: internal and external. Internal (or embedded) DSLs exploit host language features to build a fluent library API that makes certain concepts more readable in the host language itself. External DSLs call for a specifically designed language that is not bound to host language and usually requires a separately developed DSL parser.

With the help of Groovy, you can create both DSL types with ease. In this recipe, we will define an internal DSL for executing remote SSH commands.

Getting ready

We are going to use the JSch library (http://www.jcraft.com/jsch/), which is used by many other Java libraries that require SSH connectivity.

The following Gradle script (see the Integrating Groovy into the build process using Gradle recipe in Chapter 2, Using Groovy Ecosystem) will help us to build the source code of this recipe:

apply plugin: 'groovy'

repositories {
  mavenCentral()
}

dependencies {
  compile localGroovy()
  compile 'ch.qos.logback:logback-classic:1.+'
  compile 'org.slf4j:slf4j-api:1.7.4'
  compile 'commons-io:commons-io:1.4'
  compile 'com.jcraft:jsch:0.1.49'
  testCompile 'junit:junit:4.+'
}

For more information on Gradle, you can take a look into the Integrating Groovy into the build process using Gradle recipe in Chapter 2, Using Groovy Ecosystem.

How to do it...

Let's start by creating Groovy classes needed to define elements of our DSL:

  1. First of all, we will define a data structure to hold remote command result details such as exit codes, standard output data, and if any, exceptions thrown by the processing code:
    class CommandOutput {
    
      int exitStatus
      String output
      Throwable exception
    
      CommandOutput(int exitStatus, String output) {
        this.exitStatus = exitStatus
        this.output = output
      }
    
      CommandOutput(int exitStatus,
                    String output,
                    Throwable exception) {
        this.exitStatus = exitStatus
        this.output = output
        this.exception = exception
      }
    }
  2. We define another structure for holding SSH connectivity details:
    import java.util.regex.Pattern
    
    class RemoteSessionData {
    
      static final int DEFAULT_SSH_PORT = 22
    
      static final Pattern SSH_URL =
             ~/^(([^:@]+)(:([^@]+))?@)?([^:]+)(:(d+))?$/
    
      String     host           = null
      int        port           = DEFAULT_SSH_PORT
      String     username       = null
      String     password       = null
    
      def setUrl(String url) {
        def matcher = SSH_URL.matcher(url)
        if (matcher.matches()) {
          host = matcher.group(5)
          port = matcher.group(7).toInteger()
          username = matcher.group(2)
          password = matcher.group(4)
        } else {
          throw new RuntimeException("Unknown URL format: $url")
        }
      }
    }
  3. The next step defines the core of our DSL: a remote SSH session implementation that will hold our DSL verbs implemented as methods such as connect, disconnect, reconnect, and exec:
    import org.apache.commons.io.output.CloseShieldOutputStream
    import org.apache.commons.io.output.TeeOutputStream
    import com.jcraft.jsch.Channel
    import com.jcraft.jsch.ChannelExec
    import com.jcraft.jsch.JSch
    import com.jcraft.jsch.JSchException
    import com.jcraft.jsch.Session
    
    class RemoteSession extends RemoteSessionData {
    
      private Session          session  = null
      private final JSch       jsch     = null
    
      RemoteSession(JSch jsch) {
        this.jsch = jsch
      }
    
      def connect() {
        if (session == null || !session.connected) {
          disconnect()
          if (host == null) {
            throw new RuntimeException('Host is required.')
          }
          if (username == null) {
            throw new RuntimeException('Username is required.')
          }
          if (password == null) {
            throw new RuntimeException('Password is required.')
          }
          session = jsch.getSession(username, host, port)
          session.password = password
          println ">>> Connecting to $host"
          session.connect()
        }
      }
    
      def disconnect() {
        if (session?.connected) {
          try {
            session.disconnect()
          } catch (Exception e) {
          } finally {
            println "<<< Disconnected from $host"
          }
        }
      }
      def reconnect() {
        disconnect()
        connect()
      }
    
      CommandOutput exec(String cmd) {
        connect()
        catchExceptions {
          awaitTermination(executeCommand(cmd))
        }
      }
    
      private ChannelData executeCommand(String cmd) {
        println "> $cmd"
        def channel = session.openChannel('exec')
        def savedOutput = new ByteArrayOutputStream()
        def systemOutput =
          new CloseShieldOutputStream(System.out)
        def output =
          new TeeOutputStream(savedOutput, systemOutput)
        channel.command = cmd
        channel.outputStream = output
        channel.extOutputStream = output
        channel.setPty(true)
        channel.connect()
        new ChannelData(channel: channel,
                        output: savedOutput)
      }
    
      class ChannelData {
        ByteArrayOutputStream output
        Channel channel
      }
    
      private CommandOutput awaitTermination(
                              ChannelData channelData) {
        Channel channel = channelData.channel
        try {
          def thread = null
          thread =
              new Thread() {
                void run() {
                  while (!channel.isClosed()) {
                    if (thread == null) {
                      return
                    }
                    try {
                      sleep(1000)
                    } catch (Exception e) {
                      // ignored
                    }
                  }
                }
              }
          thread.start()
          thread.join(0)
          if (thread.isAlive()) {
            thread = null
            return failWithTimeout()
          } else {
            int ec = channel.exitStatus
            return new CommandOutput(
                     ec,
                     channelData.output.toString()
                   )
          }
        } finally {
          channel.disconnect()
        }
      }
    
      private CommandOutput catchExceptions(Closure cl) {
        try {
          return cl()
        } catch (JSchException e) {
          return failWithException(e)
        }
      }
    
      private CommandOutput failWithTimeout() {
        println 'Session timeout!'
        new CommandOutput(-1, 'Session timeout!')
      }
    
      private CommandOutput failWithException(Throwable e) {
        println "Caught exception: ${e.message}"
        new CommandOutput(-1, e.message, e)
      }
    }
  4. Now, we are ready to create an entry point to our DSL in the form of an engine class:
    import com.jcraft.jsch.JSch
    
    class SshDslEngine {
    
      private final JSch jsch
      private RemoteSession delegate
    
      SshDslEngine()  {
        JSch.setConfig('HashKnownHosts',  'yes')
        JSch.setConfig('StrictHostKeyChecking', 'no')
        this.jsch = new JSch()
      }
    
      def remoteSession(Closure cl) {
        if (cl != null) {
          delegate = new RemoteSession(jsch)
          cl.delegate = delegate
          cl.resolveStrategy = Closure.DELEGATE_FIRST
          cl()
          if (delegate?.session?.connected) {
            try {
              delegate.session.disconnect()
            } catch (Exception e) {
            }
          }
        }
      }
    }
  5. Now, it is time to try the DSL out:
    new SshDslEngine().remoteSession {
    
      url = 'root:secret123@localhost:3223'
    
      exec 'yum --assumeyes install groovy'
      exec 'groovy -e "println 'Hello, Remote!'"'
    
    }

How it works...

In the previous code example, we construct a DSL engine object and call the remoteSession method to which we pass a closure with our DSL code. The snippet, after connecting to a remote server, installs Groovy through the Yum package manager and runs a simple Groovy script through the command line, which just prints the message: Hello, Remote!.

The principal stratagems employed by Groovy for the definition of a DSL are the closure delegates. A closure delegate is basically an object that is dynamically queried for methods/fields from within the closure's code. By default, delegate equals to the object that contains the closure; for example, enclosing a class or surrounding a closure.

As you can notice in the remoteSession method, the closure input parameter is given a delegate object (cl.delegate = delegate) that represents the RemoteSession implementation. Also, the closure's resolveStrategy is set to DELEGATE_FIRST; this means the closure's code will first call a method from the given RemoteSession instance, and only after that will it call methods from other available contexts. This is the reason why, in the DSL usage example, the closure that we pass to the remoteSession method has access to setUrl and exec methods.

The remote connection is automatically started upon the first command execution; but the connection logic can be controlled explicitly since it is defined by our DSL. Additionally, normal Groovy code can be added around methods of the RemoteSession class, like the following code snippet:

new SshDslEngine().remoteSession {

  url = 'root:secret123@localhost:3223'

  connect()
  if (exec('rpm -qa | grep groovy').exitStatus != 0) {
    exec 'yum --assumeyes install groovy'
  }
  disconnect()

  connect()
  def result = exec 'groovy -e "println 'Hello, Remote!'"'
  if (!result.contains('Hello')) {
    throw new RuntimeException('Command failed!')
  }
  disconnect()

}

See also

  • A more sophisticated implementation of the previous DSL is put into practice by the Groovy SSH DSL project available at https://github.com/aestasit/groovy-ssh-dsl. It supports SCP operations, tunneling, key-based authentication, and many other useful features.
  • Specifics of the SSH implementation are not covered in this recipe since most of the functionality is delegated to the JSch library, which can be found at http://www.jcraft.com/jsch/.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.7.208