From 1c9e7cdb83e2dfc4726384c027f6414c517549bd Mon Sep 17 00:00:00 2001 From: Zhaofeng Li Date: Tue, 9 Feb 2021 21:02:00 -0800 Subject: [PATCH] Allow customization of SSH configurations --- README.md | 10 +++++++- src/command/apply.rs | 13 +++++++++-- src/nix/eval.nix | 10 ++++++++ src/nix/host/ssh.rs | 54 +++++++++++++++++++++++++++++++++++++++----- src/nix/mod.rs | 13 ++++++++--- 5 files changed, 88 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6ba08ff..fc06a78 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,11 @@ Here is a sample `hive.nix` with two nodes, with some common configurations appl # can override it like: deployment.targetHost = "host-b.mydomain.tld"; + # It's also possible to override the target SSH port. + # For further customization, use the SSH_CONFIG_FILE + # environment variable to specify a ssh_config file. + deployment.targetPort = 1234; + time.timeZone = "America/Los_Angeles"; boot.loader.grub.device = "/dev/sda"; @@ -179,10 +184,13 @@ For example, to deploy ACME credentials for use with `security.acme`: Take note that if you use the default path (`/run/keys`), the secret files are only stored in-memory and will not survive reboots. To upload your secrets without performing a full deployment, use `colmena upload-keys`. +## Environment variables + +- `SSH_CONFIG_FILE`: Path to a `ssh_config` file + ## Current limitations - It's required to use SSH keys to log into the remote hosts, and interactive authentication will not work. -- There is no option to override SSH or `nix-copy-closure` options. - Error reporting is lacking. ## Licensing diff --git a/src/command/apply.rs b/src/command/apply.rs index c5456f1..4bb9383 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; +use std::env; +use std::path::PathBuf; use std::sync::Arc; use clap::{Arg, App, SubCommand, ArgMatches}; @@ -131,6 +133,9 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { quit::with_code(2); } + let ssh_config = env::var("SSH_CONFIG_FILE") + .ok().map(PathBuf::from); + // FIXME: This is ugly :/ Make an enum wrapper for this fake "keys" goal let goal_arg = local_args.value_of("goal").unwrap(); let goal = if goal_arg == "keys" { @@ -146,10 +151,14 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { let config = all_nodes.get(node).unwrap(); let host = config.to_ssh_host(); match host { - Some(host) => { + Some(mut host) => { + if let Some(ssh_config) = ssh_config.as_ref() { + host.set_ssh_config(ssh_config.clone()); + } + targets.insert( node.clone(), - Target::new(host, config.clone()), + Target::new(host.upcast(), config.clone()), ); } None => { diff --git a/src/nix/eval.nix b/src/nix/eval.nix index c4196d5..71863d1 100644 --- a/src/nix/eval.nix +++ b/src/nix/eval.nix @@ -41,6 +41,16 @@ let type = types.nullOr types.str; default = name; }; + targetPort = lib.mkOption { + description = '' + The target SSH port for deployment. + + By default, the port is the standard port (22) or taken + from your ssh_config. + ''; + type = types.nullOr types.ints.unsigned; + default = null; + }; targetUser = lib.mkOption { description = '' The user to use to log into the remote node. diff --git a/src/nix/host/ssh.rs b/src/nix/host/ssh.rs index 3ab7d90..5d15d8b 100644 --- a/src/nix/host/ssh.rs +++ b/src/nix/host/ssh.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::convert::TryInto; +use std::path::PathBuf; use std::process::Stdio; use async_trait::async_trait; @@ -23,6 +24,12 @@ pub struct Ssh { /// The hostname or IP address to connect to. host: String, + /// The port to connect to. + port: Option, + + /// Local path to a ssh_config file. + ssh_config: Option, + friendly_name: String, path_cache: HashSet, progress_bar: ProcessProgress, @@ -81,6 +88,8 @@ impl Ssh { Self { user, host, + port: None, + ssh_config: None, friendly_name, path_cache: HashSet::new(), progress_bar: ProcessProgress::default(), @@ -88,6 +97,18 @@ impl Ssh { } } + pub fn set_port(&mut self, port: u16) { + self.port = Some(port); + } + + pub fn set_ssh_config(&mut self, ssh_config: PathBuf) { + self.ssh_config = Some(ssh_config); + } + + pub fn upcast(self) -> Box { + Box::new(self) + } + async fn run_command(&mut self, command: Command) -> NixResult<()> { let mut execution = CommandExecution::new(command); @@ -136,20 +157,41 @@ impl Ssh { command } - fn ssh(&self, command: &[&str]) -> Command { + fn ssh_options(&self) -> Vec { // TODO: Allow configuation of SSH parameters + let mut options: Vec = ["-o", "StrictHostKeyChecking=accept-new", "-T"] + .iter().map(|s| s.to_string()).collect(); + + if let Some(port) = self.port { + options.push("-p".to_string()); + options.push(port.to_string()); + } + + if let Some(ssh_config) = self.ssh_config.as_ref() { + options.push("-F".to_string()); + options.push(ssh_config.to_str().unwrap().to_string()); + } + + options + } + + fn ssh(&self, command: &[&str]) -> Command { + let options = self.ssh_options(); + let options_str = options.join(" "); + let mut cmd = Command::new("ssh"); - cmd.arg(self.ssh_target()) - .args(&["-o", "StrictHostKeyChecking=accept-new", "-T"]) + + cmd + .arg(self.ssh_target()) + .args(&options) .arg("--") - .args(command); + .args(command) + .env("NIX_SSHOPTS", options_str); cmd } -} -impl Ssh { /// Uploads a single key. async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> { self.progress_bar.log(&format!("Deploying key {}", name)); diff --git a/src/nix/mod.rs b/src/nix/mod.rs index 8298fea..20d110d 100644 --- a/src/nix/mod.rs +++ b/src/nix/mod.rs @@ -86,6 +86,9 @@ pub struct NodeConfig { #[serde(rename = "targetUser")] target_user: String, + #[serde(rename = "targetPort")] + target_port: Option, + #[serde(rename = "allowLocalDeployment")] allow_local_deployment: bool, tags: Vec, @@ -98,10 +101,14 @@ impl NodeConfig { pub fn tags(&self) -> &[String] { &self.tags } pub fn allows_local_deployment(&self) -> bool { self.allow_local_deployment } - pub fn to_ssh_host(&self) -> Option> { + pub fn to_ssh_host(&self) -> Option { self.target_host.as_ref().map(|target_host| { - let host = Ssh::new(self.target_user.clone(), target_host.clone()); - let host: Box = Box::new(host); + let mut host = Ssh::new(self.target_user.clone(), target_host.clone()); + + if let Some(target_port) = self.target_port { + host.set_port(target_port); + } + host }) }