My First Simple Rust Program

· 7min · Dan F.

Following up on my previous posting about the programming language rust, I figured I may as well release the code I have written so far. It's really been a fun journey, and I don't claim at all to be a rust expert by any means, but here you go.

This small program builds on what is taught in the first few chapters of the rust book. This is a simple program that can be created by running the following commands, once rust has been installed. You will need at least version 1.31.0 or newer for this to work properly.

First, initialize a new project with cargo new vantage. You don't have to call it vantage; that is simply what I named my executable. If you decide to call it something else, you will need to find the couple of instances of vantage in the code and switch around the project names.

vantage/Cargo.toml

[package]
name = "vantage"
version = "0.2.0"
authors = ["My Name <my_email@gmail.com>"]
edition = "2018"

[dependencies]
chrono = "0.4"
getopts = "0.2.18"

vantage/main.rs

extern crate getopts;

use getopts::Options;
use std::env;
use std::string::ToString;
use vantage::*;

// Main Body //

fn main() {

    // 
    // GETOPTS //
    //

    let args: Vec<String> = env::args().collect();          // Get script arguments
    let program = args[0].clone();                          // This is the script as it was called
    let mut opts = Options::new();

    opts.optopt("l", "", "set output log name", "LOG");     // Set output log flag
    opts.optopt("e", "email", "set email", "EMAIL");        // Set email flag
    opts.optflag("v", "verbose", "use verbose");            // Set v option, for increased verbosity
    opts.optflag("h", "help", "print this help menu");      // Set h flag for help

    let matches = match opts.parse(&args[1..]) {            // Panic arg is passed that does not exist
        Ok(m) => { m }
        Err(f) => { panic!(f.to_string()) }
    };

    let server = if !matches.free.is_empty() {              // Use the free "input" parameter as servername
        matches.free[0].clone()
    } else {
        print_usage(&program, opts);                        // Run help if no parameters are passed
        return;
    };

    if matches.opt_present("h") {                           // If -h or --help is passed, print usage
        print_usage(&program, opts);
        return;
    }

    let logpath = matches.opt_str("l");                     // Set logpath 
    let log = get_log(logpath);                             // Ensure that if logpath is passed, that

    let email = if matches.opt_present("e") {               // Get email
        matches.opt_str("e").unwrap()                       // If -e is passed, unwrap and set email var
    } else {
        "None".to_string()                                  // If no -e is passed, set email to None
    };

    if !matches.opt_present("v") &&                         // Fail if -v and -l are not passed,
        !matches.opt_present("l") {                         // as then vantage would log nothing visible
            println!("Missing either -v or -l");
            return;
    }

    let verbose = matches.opt_present("v");                 // Get email, per getopts

    let hostname = get_info::hostname();                    // Get local hostname

    //
    // END GETOPTS - FILL IN STRUCT //
    //

    let info = RunData { 
        remotehost: server,
        email: email,
        hostname: hostname,
        log: log,
        verbose: verbose,
    };

    //
    // START MAIN //
    //

    check_dns(&info);                                       // Check if remotehost is resolvable

    match ping(&info) {                                     // Start ping cycle
        true => state_change::online(&info),                // Set state to online if ping succeeds
        false => state_change::degraded(&info),             // Set state to degraded if ping fails
    }
}

vantage/lib.rs

extern crate chrono; 
extern crate getopts;

use std::process;
use std::process::{Command, Stdio};
use std::string::ToString;
use std::{thread, time};
use std::io::Write;
use std::str::from_utf8;
use std::fs::OpenOptions;
use std::path::Path;
use chrono::prelude::*;
use getopts::Options;

pub const TIME_WAIT: u32 = 5;
pub const PROGNAME: &str = "vantage-v0.2";

// We need a way to pass info between functions easily
pub struct RunData {
    pub remotehost: String,
    pub email: String,
    pub hostname: String,
    pub log: String,
    pub verbose: bool,
}

// Logger logs things to log path provided
fn logger(run_data: &RunData, message: &str) {
    let localtime: DateTime<Local> = Local::now(); 
    if run_data.log != "None" {
        let mut file = OpenOptions::new()
            .write(true)
            .append(true)
            .create(true)
            .open(&run_data.log)
            .unwrap();
        write!(&mut file, "{}: {} - {}\n", localtime, PROGNAME, message)
            .expect("Failed to write to log");
    }
}

// Simple echo fn, that prints message
fn cecho(run_data: &RunData, message: &String) {
    if run_data.verbose {
        println!("{}", &message);
    }
}

// Notify is a combo of cecho and logger
fn notify(run_data: &RunData, message: &String) {
    logger(&run_data, &message);
    cecho(&run_data, &message);
}

// This uses the OS's mail command to send emails
fn send_email(run_data: &RunData, state: String) {

    if run_data.email != "None" {
        let mut output = {
            Command::new("mail")
            .stdin(Stdio::piped())
            .args(&["-s", "state change alert"])
            .arg(&run_data.email)
            .spawn()
            .expect("failed to execute process")
        };

        let alert_string = format!("Alert from: {}{} is now {}", &run_data.hostname, &run_data.remotehost, &state);

        let stdin = output.stdin.as_mut().expect("Failed to open stdin");
        stdin.write_all(alert_string.as_bytes()).expect("Failed to write to stdin");
    } else {
        let message: String = "Not sending alert, email is empty".to_string();
        cecho(&run_data, &message);
    }
}

// Just wanted to figure out how to create a module
pub mod get_info {
    pub use crate::*;
    
    pub fn server(args: &Vec<String>) -> String {
        if args.len() == 1 {
            println!("Please specify first argument as target");
            process::exit(1);
        } else {
            return args[1].to_string()
        }
    }
    
    pub fn hostname() -> String {
        let output = {
            Command::new("hostname")
            .arg("-s")
            .output()
            .expect("failed to run hostname command")
        };
    
        from_utf8(&output.stdout).unwrap().to_string()
    }
}

// Again, my second attempt at creating a module
// These are the "states" a remotehost can be in
// online/offline/degraded
pub mod state_change {
    pub use crate::*;
    
    pub fn online(run_data: &RunData) {
        let state = " online ".to_string();
        let message = format!("state change: {}{}", &run_data.remotehost.to_string(), &state);
       
        notify(&run_data, &message); 
        send_email(&run_data,state);
    
        loop {
            match ping(&run_data) {
                true => thread::sleep(time::Duration::from_secs(TIME_WAIT.into())),
                false => break,
            }        
        }
        degraded(&run_data);
    }
    
    pub fn offline(run_data: &RunData) {
        let state = " offline ".to_string();
        let message = format!("state change: {}{}", &run_data.remotehost.to_string(), &state);
        
        notify(&run_data, &message); 
    
        send_email(&run_data,state);
        loop {
            match ping(&run_data) {
                true => break,
                false => thread::sleep(time::Duration::from_secs(TIME_WAIT.into())),
            }
        }
        online(&run_data);
    }
    
    pub fn degraded(run_data: &RunData) {
        let state = " degraded ".to_string();
        let message = format!("state change: {}{}", &run_data.remotehost.to_string(), &state);
        
        for count in 0..2 {
            thread::sleep(time::Duration::from_secs(TIME_WAIT.into()));
            let ping_rc = ping(&run_data);
    
            if count == 0 {
                match ping_rc {
                    true => online(&run_data),
                    false => notify(&run_data, &message),
                }
            } else if count == 1 {
                match ping_rc {
                    true => online(&run_data),
                    false => offline(&run_data),
                }
            }
        }        
    }
}

// Ping fn that uses the OS's ping command
pub fn ping(run_data: &RunData) -> bool {
    let output = {
        if cfg!(target_os = "openbsd") {
            Command::new("ping")
                .args(&["-c2","-w5"])
                .arg(&run_data.remotehost)
                .output()
                .expect("failed to execute process")
        } else {
            Command::new("ping")
                .args(&["-c2","-W5"])
                .arg(&run_data.remotehost)
                .output()
                .expect("failed to execute process")
        }
    };

    if output.status.success() {
        return true;    // Ping succeeded
    } else {
        return false;   // Ping failed
    }
}

// Usage function
pub fn print_usage(program: &str, opts: Options) {
    let brief = format!("Usage: {} remotehost [options]", program);
    print!("{}", opts.usage(&brief));
}

// Get log checks to see if the logpath passed can be accessed
pub fn get_log(logpath: Option<String>) -> String {
    
    let log = if logpath.is_some() {
        logpath.unwrap()
    } else {
        "None".to_string()
    };

    if &log != "None" {
        if ! Path::new(&log).exists() {
            println!("error: {} could not be opened",&log);
            process::exit(1);
        } else {
            log
        }
    } else {
        "None".to_string()
    }
}

// This function checks to see if dns can resolve remotehost passed
pub fn check_dns(run_data: &RunData) {
    let output = {
        Command::new("host")
            .arg(&run_data.remotehost)
            .output()
            .expect("failed to execute host command")
        }; 

    if !output.status.success() {
        println!("Error: Could not resolve {}",run_data.remotehost);
        process::exit(1);
    }

}

To compile the code with debugging extras, simply run cargo build. If you want to compile the binary without extra junk, simply run cargo build --release. The binary will show up in either vantage/target/debug/vantage, or vantage/target/release/vantage, depending on what method of compilation was used.

All this program does is ping a server every five seconds, and either logging to stdout, or a log, or both, the status of specified server. There is a -h "help" flag, that will show basic usage. I will continue working on this code to clean out any bugs or inconsistencies, but should work fairly well as-is.

I already am using this code to watch my remote routers, to ensure that they are online. The program can also be passed an email with -e, which will send a notification to the email passed when the remote host goes either online or offline.

Has been tested on OpenBSD 6.5

Edited 04/10/2019: Updated src.lib