tricorder 0.3.0

Automation, the KISS way, no YAML involved.
Documentation
//! Command Line Interface to **tricorder** capabilities.
//!
//! **tricorder** is distributed as a static command line tool. It is agent-less
//! and connects to remote hosts via SSH (authentication is done via `ssh-agent`
//! on the local host).
//!
//! It requires an [[Inventory]] and a selection of hosts to perform a task:
//!
//! | Global flag | Description |
//! | --- | --- |
//! | `-i, --inventory <PATH>` | Path to a TOML inventory file or an executable producing a JSON inventory |
//! | `-H, --host_id <STR>` | Specific host on which to perform the task |
//! | `-t, --host_tags <STR>` | Comma-separated list of tags to select the hosts |
//!
//! > **NB:**
//! >   - If `-H` is provided, `-t` will be ignored.
//! >   - If `-i` is omitted, we assume an inventory with only `root@localhost:22`
//! >   - The host needs only one tag from the list to match in order to be selected (boolean OR)

pub mod external;
pub mod info;
pub mod exec;
pub mod upload;

use crate::{Result, Inventory, Host};

use clap::ArgMatches;
use is_executable::IsExecutable;
use std::{
  path::Path,
  process::Command,
  fs
};

pub fn run(matches: ArgMatches) -> Result<()> {
  let inventory_arg = matches.value_of("inventory");
  let host_id_arg = matches.value_of("host_id");
  let host_tags_arg = matches.value_of("host_tags");

  match matches.subcommand() {
    Some(("info", sub_matches)) => {
      let inventory = get_inventory(inventory_arg)?;
      let hosts = get_host_list(inventory, host_id_arg, host_tags_arg);
      info::run(hosts, sub_matches)
    },
    Some(("do", sub_matches)) => {
      let inventory = get_inventory(inventory_arg)?;
      let hosts = get_host_list(inventory, host_id_arg, host_tags_arg);
      exec::run(hosts, sub_matches)
    },
    Some(("upload", sub_matches)) => {
      let inventory = get_inventory(inventory_arg)?;
      let hosts = get_host_list(inventory, host_id_arg, host_tags_arg);
      upload::run(hosts, sub_matches)
    },
    Some((cmd, sub_matches)) => {
      external::run(cmd, inventory_arg, host_id_arg, host_tags_arg, sub_matches)
    },
    _ => {
      unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`")
    }
  }
}

fn get_inventory(arg: Option<&str>) -> Result<Inventory> {
  match arg {
    Some(path) => {
      let inventory_path = Path::new(path);

      if inventory_path.exists() {
        if inventory_path.is_executable() {
          let result = Command::new(path).output()?;

          if !result.status.success() {
            eprintln!("Failed to execute inventory {}: {}", path, result.status);
            eprintln!("Ignoring...");
            Ok(Inventory::new())
          }
          else {
            let content = String::from_utf8(result.stdout)?;
            Ok(Inventory::from_json(&content)?)
          }
        }
        else {
          let content = fs::read_to_string(path)?;
          Ok(Inventory::from_toml(&content)?)
        }
      }
      else {
        eprintln!("Inventory '{}' does not exist, ignoring...", path);
        Ok(Inventory::new())
      }
    },
    None => {
      eprintln!("No inventory provided, using localhost...");
      Ok(Inventory::new())
    }
  }
}

fn get_host_list(
  inventory: Inventory,
  host_id_arg: Option<&str>,
  host_tags_arg: Option<&str>
) -> Vec<Host> {
  if let Some(host_id) = host_id_arg {
    match inventory.get_host_by_id(host_id.to_string()) {
      Some(host) => {
        return vec![host];
      },
      None => {
        eprintln!("Host '{}' not found in inventory, ignoring...", host_id);
        return vec![];
      }
    }
  }

  if let Some(host_tags) = host_tags_arg {
    let tags: Vec<String> = host_tags.split(",")
      .map(|tag| String::from(tag.trim()))
      .collect();

    return inventory.get_hosts_by_tags(tags);
  }

  return inventory.hosts.clone();
}