My First Simple Rust Program
Table of Contents
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