Traverse up to find hive.nix by default, and other CLI ergonomics fixes

This commit is contained in:
Zhaofeng Li 2020-12-29 11:31:19 -08:00
parent 9c8e3034f7
commit 60d6475897
11 changed files with 212 additions and 95 deletions

View file

@ -1,6 +1,6 @@
use clap::{Arg, App, SubCommand, ArgMatches};
use crate::nix::{Hive, DeploymentTask, DeploymentGoal};
use crate::nix::{DeploymentTask, DeploymentGoal};
use crate::deployment::deploy;
use crate::util;
@ -37,9 +37,9 @@ pub fn subcommand() -> App<'static, 'static> {
}
pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
let mut hive = Hive::from_args(local_args).unwrap();
let mut hive = util::hive_from_args(local_args).unwrap();
println!("Enumerating nodes...");
log::info!("Enumerating nodes...");
let all_nodes = hive.deployment_info().await.unwrap();
let selected_nodes = match local_args.value_of("on") {
@ -50,14 +50,14 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
};
if selected_nodes.len() == 0 {
println!("No hosts matched. Exiting...");
log::warn!("No hosts matched. Exiting...");
quit::with_code(2);
}
if selected_nodes.len() == all_nodes.len() {
println!("Building all node configurations...");
log::info!("Building all node configurations...");
} else {
println!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), all_nodes.len());
log::info!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), all_nodes.len());
}
// Some ugly argument mangling :/
@ -88,9 +88,9 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
}
if skip_list.len() != 0 {
println!("Applying configurations ({} skipped)...", skip_list.len());
log::info!("Applying configurations ({} skipped)...", skip_list.len());
} else {
println!("Applying configurations...");
log::info!("Applying configurations...");
}
deploy(task_list, max_parallelism, !verbose).await;

View file

@ -4,12 +4,12 @@ use clap::{Arg, App, SubCommand, ArgMatches};
use tokio::fs;
use tokio::process::Command;
use crate::nix::{Hive, DeploymentTask, DeploymentGoal, Host};
use crate::nix::{DeploymentTask, DeploymentGoal, Host};
use crate::nix::host;
use crate::util;
pub fn subcommand() -> App<'static, 'static> {
let command = SubCommand::with_name("apply-local")
SubCommand::with_name("apply-local")
.about("Apply configurations on the local machine")
.arg(Arg::with_name("goal")
.help("Deployment goal")
@ -17,12 +17,6 @@ pub fn subcommand() -> App<'static, 'static> {
.default_value("switch")
.index(1)
.possible_values(&["push", "switch", "boot", "test", "dry-activate"]))
.arg(Arg::with_name("config")
.short("f")
.long("config")
.help("Path to a Hive expression")
.default_value("hive.nix")
.required(true))
.arg(Arg::with_name("sudo")
.long("sudo")
.help("Attempt to escalate privileges if not run as root")
@ -30,20 +24,18 @@ pub fn subcommand() -> App<'static, 'static> {
.arg(Arg::with_name("we-are-launched-by-sudo")
.long("we-are-launched-by-sudo")
.hidden(true)
.takes_value(false));
util::register_common_args(command)
.takes_value(false))
}
pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
// Sanity check: Are we running NixOS?
if let Ok(os_release) = fs::read_to_string("/etc/os-release").await {
if !os_release.contains("ID=nixos\n") {
eprintln!("\"apply-local\" only works on NixOS machines.");
log::error!("\"apply-local\" only works on NixOS machines.");
quit::with_code(5);
}
} else {
eprintln!("Coult not detect the OS version from /etc/os-release.");
log::error!("Coult not detect the OS version from /etc/os-release.");
quit::with_code(5);
}
@ -52,42 +44,42 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
let euid: u32 = unsafe { libc::geteuid() };
if euid != 0 {
if local_args.is_present("we-are-launched-by-sudo") {
eprintln!("Failed to escalate privileges. We are still not root despite a successful sudo invocation.");
log::error!("Failed to escalate privileges. We are still not root despite a successful sudo invocation.");
quit::with_code(3);
}
if local_args.is_present("sudo") {
escalate().await;
} else {
eprintln!("Colmena was not started by root. This is probably not going to work.");
eprintln!("Hint: Add the --sudo flag.");
log::warn!("Colmena was not started by root. This is probably not going to work.");
log::warn!("Hint: Add the --sudo flag.");
}
}
}
let mut hive = Hive::from_args(local_args).unwrap();
let mut hive = util::hive_from_args(local_args).unwrap();
let hostname = hostname::get().expect("Could not get hostname")
.to_string_lossy().into_owned();
let goal = DeploymentGoal::from_str(local_args.value_of("goal").unwrap()).unwrap();
println!("Enumerating nodes...");
log::info!("Enumerating nodes...");
let all_nodes = hive.deployment_info().await.unwrap();
let target: Box<dyn Host> = {
if let Some(info) = all_nodes.get(&hostname) {
if !info.allows_local_deployment() {
eprintln!("Local deployment is not enabled for host {}.", hostname);
eprintln!("Hint: Set deployment.allowLocalDeployment to true.");
log::error!("Local deployment is not enabled for host {}.", hostname);
log::error!("Hint: Set deployment.allowLocalDeployment to true.");
quit::with_code(2);
}
host::local()
} else {
eprintln!("Host {} is not present in the Hive configuration.", hostname);
log::error!("Host {} is not present in the Hive configuration.", hostname);
quit::with_code(2);
}
};
println!("Building local node configuration...");
log::info!("Building local node configuration...");
let profile = {
let selected_nodes: Vec<String> = vec![hostname.clone()];
let mut profiles = hive.build_selected(selected_nodes).await

View file

@ -1,6 +1,5 @@
use clap::{Arg, App, SubCommand, ArgMatches};
use crate::nix::Hive;
use crate::util;
pub fn subcommand() -> App<'static, 'static> {
@ -17,9 +16,9 @@ pub fn subcommand() -> App<'static, 'static> {
}
pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
let mut hive = Hive::from_args(local_args).unwrap();
let mut hive = util::hive_from_args(local_args).unwrap();
println!("Enumerating nodes...");
log::info!("Enumerating nodes...");
let all_nodes = hive.deployment_info().await.unwrap();
let selected_nodes = match local_args.value_of("on") {
@ -30,17 +29,17 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
};
if selected_nodes.len() == 0 {
println!("No hosts matched. Exiting...");
log::warn!("No hosts matched. Exiting...");
quit::with_code(2);
}
if selected_nodes.len() == all_nodes.len() {
println!("Building all node configurations...");
log::info!("Building all node configurations...");
} else {
println!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), all_nodes.len());
log::info!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), all_nodes.len());
}
hive.build_selected(selected_nodes).await.unwrap();
println!("Success!");
log::info!("Success!");
}

View file

@ -2,11 +2,10 @@ use std::path::PathBuf;
use clap::{Arg, App, SubCommand, ArgMatches};
use crate::nix::Hive;
use crate::util;
pub fn subcommand() -> App<'static, 'static> {
let command = SubCommand::with_name("introspect")
SubCommand::with_name("introspect")
.about("Evaluate expressions using the complete configuration.")
.long_about(r#"Your expression should take an attribute set with keys `pkgs`, `lib` and `nodes` (like a NixOS module) and return a JSON-serializable value.
@ -22,22 +21,13 @@ For example, to retrieve the configuration of one node, you may write something
.short("E")
.help("The Nix expression")
.takes_value(true))
.arg(Arg::with_name("config")
.short("f")
.long("config")
.help("Path to a Hive expression")
.default_value("hive.nix")
.required(true))
;
util::register_common_args(command)
}
pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
let mut hive = Hive::from_args(local_args).unwrap();
let mut hive = util::hive_from_args(local_args).unwrap();
if !(local_args.is_present("expression") ^ local_args.is_present("expression_file")) {
eprintln!("Either an expression (-E) xor a .nix file containing an expression should be specified, not both.");
log::error!("Either an expression (-E) xor a .nix file containing an expression should be specified, not both.");
quit::with_code(1);
}

View file

@ -1,4 +1,5 @@
use clap::{App, AppSettings};
use std::env;
use clap::{App, AppSettings, Arg};
mod nix;
mod command;
@ -29,22 +30,60 @@ macro_rules! bind_command {
#[tokio::main(flavor = "multi_thread")]
async fn main() {
init_logging();
let mut app = App::new("Colmena")
.version("0.1.0")
.author("Zhaofeng Li <hello@zhaofeng.li>")
.about("NixOS deployment tool")
.global_setting(AppSettings::ColoredHelp)
.setting(AppSettings::ArgRequiredElseHelp);
.setting(AppSettings::ArgRequiredElseHelp)
.arg(Arg::with_name("config")
.short("f")
.long("config")
.help("Path to a Hive expression")
// The default value is a lie (sort of)!
//
// The default behavior is to search upwards from the
// current working directory for a file named "hive.nix".
// This behavior is disabled if --config/-f is explicitly
// supplied by the user (occurrences_of > 0).
.default_value("hive.nix")
.long_help(r#"If this argument is not specified, Colmena will search upwards from the current working directory for a file named "hive.nix". This behavior is disabled if --config/-f is given explicitly.
For a sample configuration, see <https://github.com/zhaofengli/colmena>.
"#)
.global(true))
.arg(Arg::with_name("show-trace")
.long("show-trace")
.help("Show debug information for Nix commands")
.long_help("Passes --show-trace to Nix commands")
.global(true)
.takes_value(false));
bind_command!(apply, app);
bind_command!(apply_local, app);
bind_command!(build, app);
bind_command!(introspect, app);
let matches = app.get_matches();
let matches = app.clone().get_matches();
command!(apply, matches);
command!("apply-local", apply_local, matches);
command!(build, matches);
command!(introspect, matches);
app.print_long_help().unwrap();
}
fn init_logging() {
if env::var("RUST_LOG").is_err() {
// HACK
env::set_var("RUST_LOG", "info")
}
env_logger::builder()
.format_timestamp(None)
.format_module_path(false)
.init();
}

View file

@ -187,7 +187,7 @@ impl SSH {
progress.set_message(trimmed);
progress.inc(0);
} else {
println!("{} | {}", style(&self.friendly_name).cyan(), trimmed);
eprintln!("{} | {}", style(&self.friendly_name).cyan(), trimmed);
}
self.logs.push(line);
}

View file

@ -6,7 +6,6 @@ use std::collections::HashMap;
use std::fs;
use async_trait::async_trait;
use clap::ArgMatches;
use indicatif::ProgressBar;
use serde::de::DeserializeOwned;
use serde::{Serialize, Deserialize};
@ -72,17 +71,8 @@ impl Hive {
})
}
pub fn from_args(args: &ArgMatches<'_>) -> NixResult<Self> {
let path = args.value_of("config").expect("The config arg should exist").to_owned();
let path = canonicalize_path(path);
let mut hive = Self::new(path)?;
if args.is_present("show-trace") {
hive.show_trace = true;
}
Ok(hive)
pub fn show_trace(&mut self, value: bool) {
self.show_trace = value;
}
/// Retrieve deployment info for all nodes
@ -443,12 +433,3 @@ impl DeploymentTask {
}
}
}
fn canonicalize_path(path: String) -> PathBuf {
if !path.starts_with("/") {
format!("./{}", path).into()
} else {
path.into()
}
}

View file

@ -1,15 +1,89 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::fs;
use clap::{Arg, App};
use clap::{App, Arg, ArgMatches};
use glob::Pattern as GlobPattern;
use super::nix::DeploymentConfig;
use super::nix::{DeploymentConfig, Hive, NixResult};
enum NodeFilter {
NameFilter(GlobPattern),
TagFilter(GlobPattern),
}
pub fn hive_from_args(args: &ArgMatches<'_>) -> NixResult<Hive> {
let path = match args.occurrences_of("config") {
0 => {
// traverse upwards until we find hive.nix
let mut cur = std::env::current_dir()?;
let mut hive_path = None;
loop {
let mut listing = match fs::read_dir(&cur) {
Ok(listing) => listing,
Err(e) => {
// This can very likely fail in shared environments
// where users aren't able to list /home. It's not
// unexpected.
//
// It may not be immediately obvious to the user that
// we are traversing upwards to find hive.nix.
log::warn!("Could not traverse up ({:?}) to find hive.nix: {}", cur, e);
break;
},
};
let found = listing.find_map(|rdirent| {
match rdirent {
Err(e) => Some(Err(e)),
Ok(f) => {
if f.file_name() == "hive.nix" {
Some(Ok(f))
} else {
None
}
}
}
});
if let Some(rdirent) = found {
let dirent = rdirent?;
hive_path = Some(dirent.path());
break;
}
match cur.parent() {
Some(parent) => {
cur = parent.to_owned();
}
None => {
break;
}
}
}
if hive_path.is_none() {
log::error!("Could not find `hive.nix` in {:?} or any parent directory", std::env::current_dir()?);
}
hive_path.unwrap()
}
_ => {
let path = args.value_of("config").expect("The config arg should exist").to_owned();
canonicalize_cli_path(path)
}
};
let mut hive = Hive::new(path)?;
if args.is_present("show-trace") {
hive.show_trace(true);
}
Ok(hive)
}
pub fn filter_nodes(nodes: &HashMap<String, DeploymentConfig>, filter: &str) -> Vec<String> {
let filters: Vec<NodeFilter> = filter.split(",").map(|pattern| {
use NodeFilter::*;
@ -48,24 +122,7 @@ pub fn filter_nodes(nodes: &HashMap<String, DeploymentConfig>, filter: &str) ->
}
}
pub fn register_common_args<'a, 'b>(command: App<'a, 'b>) -> App<'a, 'b> {
command
.arg(Arg::with_name("config")
.short("f")
.long("config")
.help("Path to a Hive expression")
.default_value("hive.nix")
.required(true))
.arg(Arg::with_name("show-trace")
.long("show-trace")
.help("Show debug information for Nix commands")
.long_help("Passes --show-trace to Nix commands")
.takes_value(false))
}
pub fn register_selector_args<'a, 'b>(command: App<'a, 'b>) -> App<'a, 'b> {
let command = register_common_args(command);
command
.arg(Arg::with_name("on")
.long("on")
@ -79,3 +136,11 @@ Valid examples:
- @a-tag,@tags-can-have-*"#)
.takes_value(true))
}
fn canonicalize_cli_path(path: String) -> PathBuf {
if !path.starts_with("/") {
format!("./{}", path).into()
} else {
path.into()
}
}