Using expect script C/C++ library to manage Linux hosts

Saturday November 17, 2012 ()

The expect C library is an altenative to using plain expect scripts, specially useful if you are a C or C++ programmer who also mange Linux hosts.  The program that follows was built in Debian, it is a simple demonstration on how to use the library.

Let us start by installing the library and other tools we need.

apt-get update
apt-get install build-essential libgoogle-glog-dev expect-dev tcl8.5 dev

The function below executes a single command and exit after successful ssh login.  Please refer to the inline comments and the description that follows for guidance.


#include <sys/wait.h>
#include <tcl8.5/expect.h>
#include <glog/logging.h>

#include <iostream>
#include <fstream>
#include <string>
#include <iterator>
#include <sstream>
#include <vector>

using namespace std;

int task(const string& host, const string& password,
        const string& command) {

    exp_is_debugging = 0;
    exp_timeout = 20;

    // exp_open spawn our ssh/program and returns a stream.	
    FILE *expect = exp_popen((char *) host.c_str());
    if (expect == 0) {
        return 1;
    }

    enum { denied, invalid, command_not_found, 
            command_failed, prompt };

    // exp_fexpectl takes variable parameters terminated by
    // exp_end.  A parameter takes the form 
    // {type, pattern, and return value}  corresponding to
    // exp_glob, "password", prompt,  in exp_fexpectl call below.
    // If a pattern matches, exp_fexpectl returns with the value
    // corresponding to the pattern.  

    switch (exp_fexpectl(expect,
                exp_glob, "password: ", prompt,
                exp_end)) {
        case prompt:
                // continue 
                break;
        case EXP_TIMEOUT:
                LOG(ERROR) << "Timeout,  may be invalid host" << endl;
                return 1;
    }

    bool success = true

    // In sending command the return character \r is required.
    fprintf(expect, "%s\r", password.c_str());

    switch (exp_fexpectl(expect,
                exp_glob, "denied", denied, // 1 case		
                exp_glob, "invalid password", invalid, // another case
                exp_glob, "$ ", prompt, // third case			
                exp_end)) {
        case denied:
                LOG(ERROR) << "Access denied" << endl;
                success = false;
                break;
        case invalid:
                success = false;
                LOG(ERROR) << "Invalid password" << endl;
                break;
        case EXP_TIMEOUT:
                LOG(ERROR) << "Login timeout" << endl;
                break;
        case prompt:
                // continue;		
                break;
        default:
                break;
    }

    if (!success) {
        fclose(expect);
        waitpid(exp_pid, 0, 0);
        return 1;
    }

    // Note that the pattern used below are dependent
    // on the command.  Adding more cases to cover
    // various command will help.

    fprintf(expect, "%s\r", command.c_str());

    switch (exp_fexpectl(expect,
        exp_glob, "cannot create", command_failed,
        exp_glob, "command not found", command_not_found,
        exp_glob, "$ ", prompt,
        exp_end)) {
    case command_failed:
        LOG(ERROR) << "Could not create directory" << endl;
        success = false;;
        break;
    case command_not_found:
        LOG(ERROR) << "Command not found" << endl;
        success = false;
        break;
    case prompt:
        fprintf(expect, "%s\r", "exit");
        LOG(INFO) << "Task completed." << endl;
        break;
    }

    // You should always do this.
    // exp_popen spawns a process with pid exp_pid and 
    // return a FILE object.  

    fclose(expect);
    waitpid(exp_pid, 0, 0);

    if (success)
        return 0;
    else
        return 1;
}

Note that exp_fexpectl will not return and will wait for the pattern to arrive until exp_timeout.   Make sure you have all the expected string patterns covered.

The pattern type exp_glob is an enum which determines how the pattern is interpreted.  Use exp_regex if you want to use reqular expressions in the case string pattern.  Please see expect.h in your include directory for more options.

exp_fexpectv can be used instead of exp_fexpectl. exp_fexpectv takes struct exp_case as parameter.  The code fragment below corresponds to the section of code above beginning at line 52.

struct exp_case cases [] = { 
        { "denied", NULL, exp_glob, denied},
        { "invalid password", NULL,exp_glob,invalid},
        { "$ ", NULL, exp_glob, prompt},
        0
};

switch (exp_fexpectv (expect, cases)) {	
case denied:
        LOG(ERROR) << "Access denied" << endl;
        retval=error;
        break; 
case invalid:
        retval=failure;
        LOG(ERROR) << "Invalid password" << endl;
        break;
case prompt:
        // continue
        break;
case EXP_TIMEOUT:
        LOG(ERROR) << "Login timeout" << endl;
        break;
}

The terminating null character in exp_case cases above is required.

We have 3 switch blocks above which makes the code a little bit longer.  If preferred, all the 3 switch blocks can be placed in a loop and then expand the case patters to cover all possible prompts.

The main function.


int main(int argc, char *argv[]) {
    // Initialize logging
    google::InitGoogleLogging("exp");
    ifstream tasks("tasks.txt", ifstream::in);

    // This is how we read our tasks.txt file.  You may
    // want to employ a way you are comfortable with.

    if (tasks.is_open()) {
        for (string line; getline(tasks, line);) {
            stringstream str(line);
            istream_iterator<string> it(str);
            istream_iterator<string> end;
            vector<string> result(it, end);

            string password;
            ostringstream command, host;
            host << "ssh ";

            for (vector<int>::size_type i = 0;
                    i != result.size(); i++) {
                    switch (i) {
                    case 0:
                            host << result [i];
                            break;
                    case 1:
                            password = result [i];
                            break;
                    default:
                            command << result [i] << " ";
                            break;
                    }
            }

            if (task(host.str(), password, command.str()) == 1) {
                // Error occurred.  You may want to stop
                // at this point if the next task is dependent 
                // on the previous task.
            }
        }
    }

    return 0;
}

Our programs takes a text file,  tasks.txt as input.   Each line is separated by spaces with the first to being use@host and password.  The third and beyond is the command and its parameters.

To build the program above use the following from your command line.

g++ exp.cpp -Wall -std=c++0x -lexpect -ltcl8.5 -lglog -o exp

That's it Good Luck.


9,759

Comments (Using expect script C/C++ library to manage Linux hosts)