colmena/src/command/apply.rs
2022-06-03 23:51:32 -07:00

232 lines
8.7 KiB
Rust

use std::env;
use std::path::PathBuf;
use clap::{Arg, Command as ClapCommand, ArgMatches};
use crate::error::ColmenaError;
use crate::nix::deployment::{
Deployment,
Goal,
Options,
EvaluationNodeLimit,
Evaluator,
ParallelismLimit,
};
use crate::progress::SimpleProgressOutput;
use crate::nix::NodeFilter;
use crate::util;
pub fn register_deploy_args(command: ClapCommand) -> ClapCommand {
command
.arg(Arg::new("eval-node-limit")
.long("eval-node-limit")
.value_name("LIMIT")
.help("Evaluation node limit")
.long_help(r#"Limits the maximum number of hosts to be evaluated at once.
The evaluation process is RAM-intensive. The default behavior is to limit the maximum number of host evaluated at the same time based on naive heuristics.
Set to 0 to disable the limit.
"#)
.default_value("auto")
.takes_value(true)
.validator(|s| {
if s == "auto" {
return Ok(());
}
match s.parse::<usize>() {
Ok(_) => Ok(()),
Err(_) => Err(String::from("The value must be a valid number")),
}
}))
.arg(Arg::new("parallel")
.short('p')
.long("parallel")
.value_name("LIMIT")
.help("Deploy parallelism limit")
.long_help(r#"Limits the maximum number of hosts to be deployed in parallel.
Set to 0 to disable parallemism limit.
"#)
.default_value("10")
.takes_value(true)
.validator(|s| {
match s.parse::<usize>() {
Ok(_) => Ok(()),
Err(_) => Err(String::from("The value must be a valid number")),
}
}))
.arg(Arg::new("keep-result")
.long("keep-result")
.help("Create GC roots for built profiles")
.long_help(r#"Create GC roots for built profiles.
The built system profiles will be added as GC roots so that they will not be removed by the garbage collector.
The links will be created under .gcroots in the directory the Hive configuration is located.
"#)
.takes_value(false))
.arg(Arg::new("verbose")
.short('v')
.long("verbose")
.help("Be verbose")
.long_help("Deactivates the progress spinner and prints every line of output.")
.takes_value(false))
.arg(Arg::new("no-keys")
.long("no-keys")
.help("Do not upload keys")
.long_help(r#"Do not upload secret keys set in `deployment.keys`.
By default, Colmena will upload keys set in `deployment.keys` before deploying the new profile on a node.
To upload keys without building or deploying the rest of the configuration, use `colmena upload-keys`.
"#)
.takes_value(false))
.arg(Arg::new("reboot")
.long("reboot")
.help("Reboot nodes after activation")
.long_help("Reboots nodes after activation and waits for them to come back up.")
.takes_value(false))
.arg(Arg::new("no-substitutes")
.long("no-substitutes")
.help("Do not use substitutes")
.long_help("Disables the use of substituters when copying closures to the remote host.")
.takes_value(false))
.arg(Arg::new("no-gzip")
.long("no-gzip")
.help("Do not use gzip")
.long_help("Disables the use of gzip when copying closures to the remote host.")
.takes_value(false))
.arg(Arg::new("build-on-target")
.long("build-on-target")
.help("Build the system profiles on the target nodes")
.long_help(r#"Build the system profiles on the target nodes themselves.
If enabled, the system profiles will be built on the target nodes themselves, not on the host running Colmena itself.
This overrides per-node perferences set in `deployment.buildOnTarget`.
To temporarily disable remote build on all nodes, use `--no-build-on-target`.
"#)
.takes_value(false))
.arg(Arg::new("no-build-on-target")
.long("no-build-on-target")
.hide(true)
.takes_value(false))
.arg(Arg::new("force-replace-unknown-profiles")
.long("force-replace-unknown-profiles")
.help("Ignore all targeted nodes deployment.replaceUnknownProfiles setting")
.long_help(r#"If `deployment.replaceUnknownProfiles` is set for a target, using this switch
will treat deployment.replaceUnknownProfiles as though it was set true and perform unknown profile replacement."#)
.takes_value(false))
.arg(Arg::new("evaluator")
.long("evaluator")
.help("The evaluator to use (experimental)")
.long_help(r#"If set to `chunked` (default), evaluation of nodes will happen in batches. If set to `streaming`, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel.
This is an experimental feature."#)
.default_value("chunked")
.possible_values(Evaluator::possible_values()))
}
pub fn subcommand() -> ClapCommand<'static> {
let command = ClapCommand::new("apply")
.about("Apply configurations on remote machines")
.arg(Arg::new("goal")
.help("Deployment goal")
.long_help("Same as the targets for switch-to-configuration.\n\"push\" means only copying the closures to remote nodes.")
.default_value("switch")
.index(1)
.possible_values(&["build", "push", "switch", "boot", "test", "dry-activate", "keys"]))
;
let command = register_deploy_args(command);
util::register_selector_args(command)
}
pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> {
let hive = util::hive_from_args(local_args).await?;
let ssh_config = env::var("SSH_CONFIG_FILE")
.ok().map(PathBuf::from);
let filter = local_args.value_of("on")
.map(NodeFilter::new)
.transpose()?;
let goal_arg = local_args.value_of("goal").unwrap();
let goal = Goal::from_str(goal_arg).unwrap();
let targets = hive.select_nodes(filter, ssh_config, goal.requires_target_host()).await?;
let n_targets = targets.len();
let verbose = local_args.is_present("verbose") || goal == Goal::DryActivate;
let mut output = SimpleProgressOutput::new(verbose);
let progress = output.get_sender();
let mut deployment = Deployment::new(hive, targets, goal, progress);
// FIXME: Configure limits
let options = {
let mut options = Options::default();
options.set_substituters_push(!local_args.is_present("no-substitutes"));
options.set_gzip(!local_args.is_present("no-gzip"));
options.set_upload_keys(!local_args.is_present("no-keys"));
options.set_reboot(local_args.is_present("reboot"));
options.set_force_replace_unknown_profiles(local_args.is_present("force-replace-unknown-profiles"));
options.set_evaluator(local_args.value_of_t("evaluator").unwrap());
if local_args.is_present("keep-result") {
options.set_create_gc_roots(true);
}
if local_args.is_present("no-build-on-target") {
options.set_force_build_on_target(false);
} else if local_args.is_present("build-on-target") {
options.set_force_build_on_target(true);
}
options
};
deployment.set_options(options);
if local_args.is_present("no-keys") && goal == Goal::UploadKeys {
log::error!("--no-keys cannot be used when the goal is to upload keys");
quit::with_code(1);
}
let parallelism_limit = {
let mut limit = ParallelismLimit::default();
limit.set_apply_limit({
let limit = local_args.value_of("parallel").unwrap().parse::<usize>().unwrap();
if limit == 0 {
n_targets
} else {
limit
}
});
limit
};
let evaluation_node_limit = match local_args.value_of("eval-node-limit").unwrap() {
"auto" => EvaluationNodeLimit::Heuristic,
number => {
let number = number.parse::<usize>().unwrap();
if number == 0 {
EvaluationNodeLimit::None
} else {
EvaluationNodeLimit::Manual(number)
}
}
};
deployment.set_parallelism_limit(parallelism_limit);
deployment.set_evaluation_node_limit(evaluation_node_limit);
let (deployment, output) = tokio::join!(
deployment.execute(),
output.run_until_completion(),
);
deployment?; output?;
Ok(())
}