From d8ed8089bb8ef4642abb6a98c2ade2716330621a Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 24 Nov 2016 22:13:19 +0100 Subject: [PATCH] Add support for run constraints (closes #45) --- Cargo.toml | 3 ++ src/main.rs | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6e639b6..9e6d3d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ rand = "0.3" rust-crypto = "^0.2" uuid = { version = "0.2", features = ["v4"] } hyper = "0.9" +libc = "0.2.17" +ifaces = "0.0.3" +dns-lookup = "0.2.1" diff --git a/src/main.rs b/src/main.rs index 044f591..5ce5a53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,9 @@ extern crate rand; extern crate crypto; extern crate uuid; extern crate hyper; +extern crate libc; +extern crate ifaces; +extern crate dns_lookup; use docopt::Docopt; use std::fs; @@ -45,6 +48,7 @@ use std::fs::OpenOptions; use std::env; use hyper::Url; use std::sync::mpsc; +use std::net; #[cfg(test)] use std::fs::File; use std::collections::HashMap; @@ -56,13 +60,15 @@ const PROC_PARSE_ERROR: i32 = 1; const PROC_EXEC_ERROR: i32 = 2; const PROC_OTHER_ERROR: i32 = 3; +const CONSTRAINT_HOST: &'static str = "host"; + const VERSION: &'static str = env!("CARGO_PKG_VERSION"); const USAGE: &'static str = " Factotum. Usage: - factotum run [--start=] [--env=] [--dry-run] [--no-colour] [--webhook=] [--tag=]... + factotum run [--start=] [--env=] [--dry-run] [--no-colour] [--webhook=] [--tag=]... [--constraint=]... factotum validate [--no-colour] factotum dot [--start=] [--output=] [--overwrite] [--no-colour] factotum (-h | --help) [--no-colour] @@ -79,6 +85,7 @@ Options: --no-colour Turn off ANSI terminal colours/formatting in output. --webhook= Post updates on job execution to the specified URL. --tag= Add job metadata (tags). + --constraint= Checks for an external constraint that will prevent execution; allowed constraints (host). "; #[derive(Debug, RustcDecodable)] @@ -91,6 +98,7 @@ struct Args { flag_dry_run: bool, flag_no_colour: bool, flag_tag: Option>, + flag_constraint: Option>, arg_factfile: String, flag_version: bool, cmd_run: bool, @@ -510,6 +518,92 @@ fn is_valid_url(url: &str) -> Result<(), String> { } } +fn get_constraint_map(constraints: &Vec) -> HashMap { + get_tag_map(constraints) +} + +fn is_valid_host(host: &str) -> Result<(), String> { + if host == "*" { + return Ok(()) + } + + let os_hostname = try!(gethostname_safe().map_err(|e| e.to_string())); + + if host == os_hostname { + return Ok(()) + } + + let external_addrs = try!(get_external_addrs().map_err(|e| e.to_string())); + let host_addrs = try!(dns_lookup::lookup_host(&host).map_err( + |_| "could not find any IPv4 addresses for the supplied hostname" + )); + + for host_addr in host_addrs { + if let Ok(good_host_addr) = host_addr { + if external_addrs.iter().any(|external_addr| external_addr.ip() == good_host_addr) { + return Ok(()) + } + } + } + + Err("failed to match any of the interface addresses to the found host addresses".into()) +} + +extern { + pub fn gethostname(name: *mut libc::c_char, size: libc::size_t) -> libc::c_int; +} + +fn gethostname_safe() -> Result { + let len = 255; + let mut buf = Vec::::with_capacity(len); + + let ptr = buf.as_mut_slice().as_mut_ptr(); + + let err = unsafe { + gethostname(ptr as *mut libc::c_char, len as libc::size_t) + } as libc::c_int; + + match err { + 0 => { + let mut _real_len = len; + let mut i = 0; + loop { + let byte = unsafe { *(((ptr as u64) + (i as u64)) as *const u8) }; + if byte == 0 { + _real_len = i; + break; + } + i += 1; + } + unsafe { buf.set_len(_real_len) } + Ok(String::from_utf8_lossy(buf.as_slice()).into_owned()) + }, + _ => { + Err("could not get hostname from system; cannot compare against supplied hostname".into()) + } + } +} + +fn get_external_addrs() -> Result, String> { + let mut external_addrs = vec![]; + + for iface in ifaces::Interface::get_all().unwrap().into_iter() { + if iface.kind == ifaces::Kind::Ipv4 { + if let Some(addr) = iface.addr { + if !addr.ip().is_loopback() { + external_addrs.push(addr) + } + } + } + } + + if external_addrs.len() == 0 { + Err("could not find any non-loopback IPv4 addresses in the network interfaces; do you have a working network interface card?".into()) + } else { + Ok(external_addrs) + } +} + fn get_tag_map(args: &Vec) -> HashMap { let mut arg_map: HashMap = HashMap::new(); @@ -622,6 +716,21 @@ fn factotum() -> i32 { } if args.cmd_run { + if let Some(constraints) = args.flag_constraint { + let c_map = get_constraint_map(&constraints); + + if let Some(host_value) = c_map.get(CONSTRAINT_HOST) { + if let Err(msg) = is_valid_host(host_value) { + println!("{}", + format!("Info: the specifed host constraint \"{}\" did not match. Reason: {}", + host_value, + msg) + .red()); + return PROC_SUCCESS; + } + } + } + if !args.flag_dry_run { parse_file_and_execute(&args.arg_factfile, args.flag_env, @@ -1135,3 +1244,35 @@ fn test_start_task_cycles() { _ => unreachable!("the task validated when it shouldn't have"), } } + +#[test] +fn test_gethostname_safe() { + let hostname = gethostname_safe(); + if let Ok(ok_hostname) = hostname { + assert!(!ok_hostname.is_empty()); + } else { + panic!("gethostname_safe() must return a Ok()"); + } +} + +#[test] +fn test_get_external_addrs() { + let external_addrs = get_external_addrs(); + if let Ok(ok_external_addrs) = external_addrs { + assert!(ok_external_addrs.len() > 0); + } else { + panic!("get_external_addrs() must return a Ok(Vec) that is non-empty"); + } +} + +#[test] +fn test_is_valid_host() { + is_valid_host("*").expect("must be Ok() for wildcard"); + + // Test each external addr is_valid_host + let external_addrs = get_external_addrs().expect("get_external_addrs() must return a Ok(Vec) that is non-empty"); + for external_addr in external_addrs { + let ip_str = external_addr.ip().to_string(); + is_valid_host(&ip_str).expect(&format!("must be Ok() for IP {}", &ip_str)); + } +}