Allow customization of SSH configurations

This commit is contained in:
Zhaofeng Li 2021-02-09 21:02:00 -08:00
parent a2fa8f1da7
commit 1c9e7cdb83
5 changed files with 88 additions and 12 deletions

View file

@ -66,6 +66,11 @@ Here is a sample `hive.nix` with two nodes, with some common configurations appl
# can override it like: # can override it like:
deployment.targetHost = "host-b.mydomain.tld"; 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"; time.timeZone = "America/Los_Angeles";
boot.loader.grub.device = "/dev/sda"; 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. 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`. 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 ## Current limitations
- It's required to use SSH keys to log into the remote hosts, and interactive authentication will not work. - 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. - Error reporting is lacking.
## Licensing ## Licensing

View file

@ -1,4 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use clap::{Arg, App, SubCommand, ArgMatches}; use clap::{Arg, App, SubCommand, ArgMatches};
@ -131,6 +133,9 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
quit::with_code(2); 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 // FIXME: This is ugly :/ Make an enum wrapper for this fake "keys" goal
let goal_arg = local_args.value_of("goal").unwrap(); let goal_arg = local_args.value_of("goal").unwrap();
let goal = if goal_arg == "keys" { 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 config = all_nodes.get(node).unwrap();
let host = config.to_ssh_host(); let host = config.to_ssh_host();
match 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( targets.insert(
node.clone(), node.clone(),
Target::new(host, config.clone()), Target::new(host.upcast(), config.clone()),
); );
} }
None => { None => {

View file

@ -41,6 +41,16 @@ let
type = types.nullOr types.str; type = types.nullOr types.str;
default = name; 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 { targetUser = lib.mkOption {
description = '' description = ''
The user to use to log into the remote node. The user to use to log into the remote node.

View file

@ -1,5 +1,6 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::convert::TryInto; use std::convert::TryInto;
use std::path::PathBuf;
use std::process::Stdio; use std::process::Stdio;
use async_trait::async_trait; use async_trait::async_trait;
@ -23,6 +24,12 @@ pub struct Ssh {
/// The hostname or IP address to connect to. /// The hostname or IP address to connect to.
host: String, host: String,
/// The port to connect to.
port: Option<u16>,
/// Local path to a ssh_config file.
ssh_config: Option<PathBuf>,
friendly_name: String, friendly_name: String,
path_cache: HashSet<StorePath>, path_cache: HashSet<StorePath>,
progress_bar: ProcessProgress, progress_bar: ProcessProgress,
@ -81,6 +88,8 @@ impl Ssh {
Self { Self {
user, user,
host, host,
port: None,
ssh_config: None,
friendly_name, friendly_name,
path_cache: HashSet::new(), path_cache: HashSet::new(),
progress_bar: ProcessProgress::default(), 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<dyn Host> {
Box::new(self)
}
async fn run_command(&mut self, command: Command) -> NixResult<()> { async fn run_command(&mut self, command: Command) -> NixResult<()> {
let mut execution = CommandExecution::new(command); let mut execution = CommandExecution::new(command);
@ -136,20 +157,41 @@ impl Ssh {
command command
} }
fn ssh(&self, command: &[&str]) -> Command { fn ssh_options(&self) -> Vec<String> {
// TODO: Allow configuation of SSH parameters // TODO: Allow configuation of SSH parameters
let mut options: Vec<String> = ["-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"); 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("--") .arg("--")
.args(command); .args(command)
.env("NIX_SSHOPTS", options_str);
cmd cmd
} }
}
impl Ssh {
/// Uploads a single key. /// Uploads a single key.
async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> { async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> {
self.progress_bar.log(&format!("Deploying key {}", name)); self.progress_bar.log(&format!("Deploying key {}", name));

View file

@ -86,6 +86,9 @@ pub struct NodeConfig {
#[serde(rename = "targetUser")] #[serde(rename = "targetUser")]
target_user: String, target_user: String,
#[serde(rename = "targetPort")]
target_port: Option<u16>,
#[serde(rename = "allowLocalDeployment")] #[serde(rename = "allowLocalDeployment")]
allow_local_deployment: bool, allow_local_deployment: bool,
tags: Vec<String>, tags: Vec<String>,
@ -98,10 +101,14 @@ impl NodeConfig {
pub fn tags(&self) -> &[String] { &self.tags } pub fn tags(&self) -> &[String] { &self.tags }
pub fn allows_local_deployment(&self) -> bool { self.allow_local_deployment } pub fn allows_local_deployment(&self) -> bool { self.allow_local_deployment }
pub fn to_ssh_host(&self) -> Option<Box<dyn Host>> { pub fn to_ssh_host(&self) -> Option<Ssh> {
self.target_host.as_ref().map(|target_host| { self.target_host.as_ref().map(|target_host| {
let host = Ssh::new(self.target_user.clone(), target_host.clone()); let mut host = Ssh::new(self.target_user.clone(), target_host.clone());
let host: Box<dyn Host> = Box::new(host);
if let Some(target_port) = self.target_port {
host.set_port(target_port);
}
host host
}) })
} }