As my learning path in Rust continues, I decided to write a little application that can help me at work. Currently I’m working on building administrative tools for HPC Clusters where it’s common to have development environments where a set of nodes is involved (we use vagrant to build a micro-cluster in a devbox).

In this environments is also common to have the need to send a command to a set of nodes at the same time, like change a field in a configuration file or start a program in all nodes at the same time. The pdsh is one the most used tools to issue commands in hosts concurrently. So, as a learning task, I started to create my own version of pdsh in Rust.

The plan is :

* To have a tool that solves the same problem as `pdsh`. It's not a clone, at least for now.
* Send commands through ssh to the hosts. Asumes that the ssh key setup was done before, there's no support for setting ssh passwords.
* It shall be capable to copy files with scp.
* Shall support the hostlist format.
* It would be nice to print results in JSON format.

Jaibon

I called the project jaibon, not particular reason, and at this point this the usage supported.

jaibon 0.1.0

USAGE:
    jaibon [OPTIONS] --command <command> --nodes <nodes>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -c, --command <command>    The command to be executed in the remote hosts.
    -n, --nodes <nodes>        Specify the node or list of nodes where the command
                               will be executed. The list of nodes should be set in a
                               separated comma list format.
    -u, --user <user>          Set the username used to perform the SSH connection to
                               the specified hosts. If is not set, the user will be
                               retrieved from the $USER environmental variable.

Basically only needs a command and a set of nodes in order to work. The user parameter is optional, if is not provided then the $USER environment variable will be used. An example of the usage will be:

Using jaibon

How it works

The idea behind this is just to create ssh commands with the parameters provided. For instance, if you want to run the uptime command in a remote node, you can do the following:

ssh myuser@host uptime

And the process will return the output of the remote execution in your terminal.

The command type

I created the command type to store all the information regarding the ongoing command.

pub struct Command {
    command: String,
    pub stdout: String,
    pub stderr: String,
    pub node: String,
    user: String,
    pub result: CommandResult,
    pub printer: CommandPrinter,
}

The structure is very straigthforward, it should store the command, the user, the stdout and stderr of the process and the overall result execution. Also is possible to define a printer for the command (planning to support JSON in the future).

Launching commands

A vector stores the list of nodes, so a loop is used to spawn threads for every node.

    for i in nodes_vec {
        let theuser = user.clone();
        let thecmd = command.clone();
        thread_handlers.push(thread::spawn(move || {
                                               println!("Launching command on node {}", i);
                                               let mut cmd = Command::new(theuser.to_owned(),
                                                                          &i,
                                                                          thecmd.to_owned());
                                               cmd.run();
                                               cmd
                                           }));
    }

On every thread a command structure is created and then the run() method is executed. All the thread handlers are stored in the thread_handlers vector.

Then, to wait for results only the following code is needed.

    for t in thread_handlers {
        let cmd = t.join().unwrap();
        println!("{}", cmd);
    }

In order to use println!("{}", cmd), the Display trait was implemented in the command structure.

impl fmt::Display for Command {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.printer {
            CommandPrinter::DefaultPrinter => self.default_formatter(f),
        }
    }
}

Here the CommandPrinter enum is in charge of dynamically dispatch the needed printer. At this point only the default formatter is enabled.

Conclusion

These are the things that I’ve learn in this first version:

* If the compiler complains a lot, probably the design is wrong.
* The `Command::new` method works but I'm not sure if that is the best way to create a new `command` structure. Should I use `&str` instead of `String`?
* I won a battle against the compiler, but definitely I'm far to win the war.
* Not sure if I should use the [ssh2](https://crates.io/crates/ssh2) crate instead of launching commands.
* I'm starting to love traits!

I’ll continue with the support of scp and the hostlist support.