Groovy/Expect Text-Based Interaction

Last updated on 20 March, 2023

Overview

One of most powerful features in the LogicMonitor Collector’s kitchen sink of Groovy tricks is our Expect-based new helper classes. Many network devices require that you connect via SSH/Telnet and run commands on their CLI. With our Expect methods, you can write Groovy scripts that will interact with processes and devices remotely as though you were typing commands and reading the output yourself.

Collector SSH Communication

The Collector uses an SSH connection to communicate with other devices and resources to collect data. Some core LogicModules call JSch directly to establish an SSH connection, but most LogicModules written in Groovy script use an Expect framework in which you can specify either the JSch library or an SSHJ library for establishing an SSH connection. 

If you’re using Expect, you can pass the library to the Expect.open() method:

Expect.open(host, user, password, "lib=jsch");
Expect.open(host, user, password, "lib=sshj");

If you don’t pass a specific protocol, JSch will be used by default. You can override the default configuration in the Collector’s agent.conf by setting ssh.preferredlibrary = sshj.

Interaction Examples

Next are a few examples that demonstrate how this feature works.

SSH Connectivity: Check Tomcat log file size on a Remote Server

In this first example, we’re using Groovy/Expect to connect to a remote server via ssh, run a command from the prompt, and parse the output to get a datapoint value.

// import the logicmonitor expect helper class
import com.santaba.agent.groovyapi.expect.Expect;

// get the hostname and credentials from the device property table
hostname = hostProps.get("system.hostname");
userid = hostProps.get("ssh.user");
passwd = hostProps.get("ssh.pass");

// initiate an ssh connection to the host using the provided credentials
ssh_connection = Expect.open(hostname, userid, passwd);

// wait for the cli prompt, which indicates we've connected
ssh_connection.expect("# ");

// send a command to show the tomcat log file size, along with the newline [enter] character
ssh_connection.send("ls -l /usr/local/tomcat/logs/catalina.out\n");

// wait for the cli prompt to return, which indicates the command has completed
ssh_connection.expect("# ");

// capture all the text up to the expected string. this should look something like
// -rw-r--r-- 1 root root 330885412 Jan 11 20:40 /usr/local/tomcat/logs/catalina.out
cmd_output = ssh_connection.before();

// now that we've capture the data we care about lets exit from the cli
ssh_connection.send("exit\n");

// wait until the external process finishes then close the connection
ssh_connection.expectClose();

// now let's iterate over each line of the we collected
cmd_output.eachLine
{ line ->

    // does this line contain the characters "-rw"
    if ( line =~ /\-rw/ )
    {
        // yes -- this is the line containing the output of our ls command
        // tokenize the cmd output on one-or-more whitespace characters
        tokens = line.split(/\s+/);

        // print the 5th element in the array, which is the size
        println tokens[4];
    }
}

// return with a response code that indicates we ran successfully
return (0);

To use this code in a DataSource, we’d create a single datapoint that uses the script output as the data value.

Telnet/Port Connectivity: Test FTP Server Availability

For this next example we’ll use Groovy/Expect to connect to a standard port on a remote server, login, and then exit. You’d do something like this to test the response time of the service so you could alert when it exceeds a particular threshold.

// import the logicmonitor expect helper class
import com.santaba.agent.groovyapi.expect.Expect;

// get the hostname and credentials from the device property table
hostname = hostProps.get("system.hostname");
userid = hostProps.get("ftp.user");
passwd = hostProps.get("ftp.pass");

// specify port 21 and a 5 second connection timeout
port = 21;
timeout = 5;

// initiate an connection to the host:port
ftp_connection = Expect.open(hostname, port, timeout);

// wait for the 220 prompt from the FTP server, then send the userid and a newline [enter] character
ftp_connection.expect("220 ");
ftp_connection.send("user " + userid + "\n");

// wait for the 331 prompt from the FTP server, then send the passwd and a newline [enter] character
ftp_connection.expect("331 ");
ftp_connection.send(passwd + "\n");

// wait for the 230 prompt from the FTP server, then send quit and a newline [enter] character
ftp_connection.expect("230 ");
ftp_connection.send("quit\n");

// return with a response code that indicates we ran successfully
return (0);

To measure the response time of this interaction, you’d create a datapoint of type “execution time”.

External Process Interaction: Using nslookup to test a DNS Server

Consider this following interaction with the “nslookup” command, In which we test that the DNS server at 8.8.4.4. can resolve the name “www.yahoo.com”.

We can conduct this same interaction in Groovy/Expect like this:

// import the logicmonitor expect helper class
import com.santaba.agent.groovyapi.expect.Expect;

// open a handle to the nslookup process and wait for the prompt
nslookup = Expect.open("nslookup.exe");
nslookup.expect( '> ');

// send the server command to change dns source
nslookup.send("server 8.8.4.4\n");
nslookup.expect('> ');

// send the hostname we want to resolve and wait for the prompt
nslookup.send("www.yahoo.com\n");
nslookup.expect('> ');

// capture the output up to the "> " prompt, and then exit the nslookup prompt
nslookup_output = nslookup.before();
nslookup.send("exit\n");

// iterate over each line we collected from the output
nslookup_output.eachLine
{ line ->

    // does this line match "Addresses: x.x.x.x" ?
    if ( line =~ /Addresses:\s+\d+\.\d+\.\d+\.\d+/ )
    {
        // yes -- this is the line containing the address resolution information
        // tokenize the nslookup output on ': '
        tokens = line.split(/:\s+/);

        // print the address that was resolved
        println tokens[1];
    }
}

// return with a response code that indicates we ran successfully
return (0);

SSH Connectivity: Retrieve a Device Config

Here we’ll use Groovy/Expect to connect to a network device and retrieve its configuration for use in a ConfigSource:

// import the logicmonitor expect helper class
import com.santaba.agent.groovyapi.expect.Expect;

// get the hostname and credentials from the device property table
hostname = hostProps.get("system.hostname");
userid = hostProps.get("ssh.user");
passwd = hostProps.get("ssh.pass");

// open an ssh connection and wait for the prompt
ssh_connection = Expect.open(hostname, userid, passwd);
ssh_connection.expect(">");

// enter enable mode
ssh_connection.send("enable\n");
ssh_connection.expect(":");
ssh_connection.send(passwd + "\n")
ssh_connection.expect("#");

// ensure the page-by-page view doesn't foul the config output
ssh_connection.send("terminal pager 0\n");
ssh_connection.expect("#");

// display the config
ssh_connection.send("show running-config \n");

// logout from the device
ssh_connection.send("exit\n");
ssh_connection.expect("# exit");

// collect the output, then close the ssh connection
config=ssh_connection.before();
ssh_connection.expectClose();

// strip the "ntp clock-period" value and print the config
config = config.replaceAll(/ntp clock-period \d+/,"ntp clock-period ")
println config;

// return with a response code that indicates we ran successfully
return(0);

Expect Method Reference

Object Instantiation

Expect.open(host, user, passwd, [timeout]) – instantiate an SSH connection object to a remote CLI

  • @param string host – hostname to which we should initiate an ssh connection
  • @param string user – username credential
  • @param string passwd – password credential
  • @param int timeout – connection timeout in seconds
  • @return object connection an expect object for use with subsequent method calls

Expect.open(host, port, [timeout]– instantiate an TCP connection object to a remote CLI

  • @param string host – hostname to which we we should connect
  • @param int port – tcp port on which we should connect
  • @param int timeout – connection timeout in seconds
  • @return object connection an expect object for use with subsequent method calls

Expect.open(command, [timeout]) – instantiate a shell command connection object

  • @param string command – full path of command to execute
  • @param int timeout – connection timeout in seconds
  • @return object connection an expect object for use with subsequent method calls

Expect open(host, int port, user, pass, [timeout]) –  Open a specified port for SSH connection

  • @param string host – hostname to which we we should connect
  • @param int port – designated port to open
  • @param string user – username credential
  • @param string pass – password credential
  • @param int timeout – connection timeout in seconds

Expect open(host, int port, user, pass) – Open a specified port for SSH connection

  • @param string host – hostname to which we we should connect
  • @param int port – designated port to open
  • @param string user – username credential
  • @param string pass – password credential

Object Methods

send(command) – send a string to the Expect connection

  • @param string command – full path of command to execute
  • @return void

expect(match_regex) – reads text from the connection until it finds a match in the provided regular expression

  • @param string|array match_regex – either a regular expression string or an array of regular expression strings against which the remote text should match
  • @return void

before() – get the output between the current matched expect() and the previous matched expect()

  • @return string before_match – the text before the most recent expect regex

matched() – get the current matched string

  • @return string current_match – the text matched by the most recent expect regex

expectClose() – wait until the external process has completed, then close it

  • @return void

stdout() – get the external process’s standard output

  • @return string stdout – process stdout

stderr() – get the external process’s standard error

  • @return string stderr – process stdout

exitValue() – get the exit code from the external process

  • @return int exitValue – process exit code

Expect Usage Notes

Regex Selection

As noted, cli.expect(regex) will tell the process to read all content until it hits regex and then cli.before() will return all output before that regex. It is common to use the CLI prompt as the cli.expect(regex) parameter, but if the output produced also contains that same prompt character the output capture will stop at that point. For this reason, care must be taken when choosing a regex to expect. If possible you should make the expected string unique.

For example, if you wanted to “cat” the content of the snmpd.conf file — which may contain the “#” symbol to comment out lines — you cannot use a simple “#” to denote the return to the prompt. Instead, you could capture the host’s full ssh prompt:

// initialize a variable to contain the actual host prompt
def actualPrompt = "";

// initiate an ssh connection to the host and wait for the "# " prompt
cli = Expect.open("192.168.211.129", "root", "xxxxxxx");
cli.expect("# ");

// capture the text preceding the initial occurrence of the  "# " string, and use that as the actual prompt
// e.g. if the prompt is "bash-3.2 # ", we'll set actualPrompt to "bash-3.2"
cli.before().eachLine
{ line ->
    actualPrompt = line;
}

// concatenate the snmpd.conf file,  and use the actual prompt as your expect regex
cli.send("cat /etc/snmp/snmpd.conf\n");

// now we expect the actual prompt character, and won't get tripped up by comment characters in the config file
cli.expect(actualPrompt + "# ");

Escape your Prompt

If the prompt character of an ssh host is $, this character must be escaped and passed within single quotes in the script, i.e.:

cli.expect('\
);
In This Article