git.delta.rocks / jrsonnet / refs/commits / a9e03a39aec5

difftreelog

refactor move main command to subdirectory

Yaroslav Bolyukin2021-09-30parent: #bb34d42.patch.diff
in: trunk

21 files changed

modifiedCargo.lockdiffbeforeafterboth
456 "serde_json",456 "serde_json",
457 "structopt",457 "structopt",
458 "tempfile",458 "tempfile",
459 "time 0.3.2",459 "time 0.3.3",
460 "z85",460 "z85",
461]461]
462462
15211521
1522[[package]]1522[[package]]
1523name = "time"1523name = "time"
1524version = "0.3.2"1524version = "0.3.3"
1525source = "registry+https://github.com/rust-lang/crates.io-index"1525source = "registry+https://github.com/rust-lang/crates.io-index"
1526checksum = "3e0a10c9a9fb3a5dce8c2239ed670f1a2569fcf42da035f5face1b19860d52b0"1526checksum = "cde1cf55178e0293453ba2cca0d5f8392a922e52aa958aee9c28ed02becc6d03"
1527dependencies = [1527dependencies = [
1528 "libc",1528 "libc",
1529 "serde",1529 "serde",
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,28 +1,2 @@
-[package]
-name = "fleet"
-description = "NixOS configuration management"
-version = "0.1.0"
-authors = ["Yaroslav Bolyukin <iam@lach.pw>"]
-edition = "2018"
-
-[dependencies]
-anyhow = "1.0"
-log = "0.4.14"
-env_logger = "0.9.0"
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-time = { version = "0.3.2", features = ["serde"] }
-tempfile = "3.2"
-once_cell = "1.5"
-hostname = "0.3.1"
-age-core = "0.6.0"
-peg = "0.7.0"
-nixlike = {path = "crates/nixlike"}
-age = { version = "0.6.0", features = ["ssh", "armor"] }
-base64 = "0.13.0"
-chrono = { version = "0.4.19", features = ["serde"] }
-z85 = "3.0.3"
-structopt = "0.3.23"
-
 [workspace]
-members = ["crates/nixlike", "cmds/install-secrets"]
+members = ["crates/*", "cmds/*"]
addedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "fleet"
+description = "NixOS configuration management"
+version = "0.1.0"
+authors = ["Yaroslav Bolyukin <iam@lach.pw>"]
+edition = "2018"
+
+[dependencies]
+anyhow = "1.0"
+log = "0.4.14"
+env_logger = "0.9.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+time = { version = "0.3.2", features = ["serde"] }
+tempfile = "3.2"
+once_cell = "1.5"
+hostname = "0.3.1"
+age-core = "0.6.0"
+peg = "0.7.0"
+nixlike = {path = "../../crates/nixlike"}
+age = { version = "0.6.0", features = ["ssh", "armor"] }
+base64 = "0.13.0"
+chrono = { version = "0.4.19", features = ["serde"] }
+z85 = "3.0.3"
+structopt = "0.3.23"
addedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -0,0 +1,122 @@
+use std::process::Command;
+
+use crate::{command::CommandExt, host::Config, nix::SYSTEMS_ATTRIBUTE};
+use anyhow::Result;
+use log::info;
+use structopt::StructOpt;
+
+#[derive(StructOpt)]
+pub struct BuildSystems {
+	/// --builders arg for nix
+	#[structopt(long)]
+	builders: Option<String>,
+	/// Jobs to run locally
+	#[structopt(long)]
+	jobs: Option<usize>,
+	/// Do not continue on error
+	#[structopt(long)]
+	fail_fast: bool,
+	#[structopt(long)]
+	privileged_build: bool,
+	#[structopt(subcommand)]
+	subcommand: Option<Subcommand>,
+}
+
+#[derive(StructOpt)]
+enum Subcommand {
+	/// Switch to built system until reboot
+	Test,
+	/// Switch to built system after reboot
+	Boot,
+	/// test + boot
+	Switch,
+}
+impl Subcommand {
+	fn should_switch_profile(&self) -> bool {
+		matches!(self, Self::Test | Self::Switch)
+	}
+	fn name(&self) -> &'static str {
+		match self {
+			Self::Test => "test",
+			Self::Boot => "boot",
+			Self::Switch => "switch",
+		}
+	}
+}
+
+impl BuildSystems {
+	pub fn run(self, config: &Config) -> Result<()> {
+		let hosts = config.list_hosts()?;
+
+		for host in hosts.iter() {
+			if config.should_skip(host) {
+				continue;
+			}
+			info!("Building host {}", host);
+			let built = {
+				let dir = tempfile::tempdir()?;
+				dir.path().to_owned()
+			};
+
+			let mut nix_build = if self.privileged_build {
+				let mut out = Command::new("sudo");
+				out.arg("nix");
+				out
+			} else {
+				Command::new("nix")
+			};
+			nix_build
+				.args(&["build", "--impure", "--no-link", "--out-link"])
+				.arg(&built)
+				.arg(format!(
+					"{}.{}.config.system.build.toplevel",
+					SYSTEMS_ATTRIBUTE, host,
+				));
+
+			if let Some(builders) = &self.builders {
+				nix_build.arg("--builders").arg(builders);
+			}
+			if let Some(jobs) = &self.jobs {
+				nix_build.arg("--max-jobs");
+				nix_build.arg(format!("{}", jobs));
+			}
+			if !self.fail_fast {
+				nix_build.arg("--keep-going");
+			}
+
+			nix_build.inherit_stdio().run()?;
+			let built = std::fs::canonicalize(built)?;
+			info!("Built closure: {:?}", built);
+			if !config.is_local(host) {
+				info!("Uploading system closure");
+				Command::new("nix")
+					.args(&["copy", "--to"])
+					.arg(format!("ssh://root@{}", host))
+					.arg(&built)
+					.inherit_stdio()
+					.run()?;
+			}
+			if let Some(subcommand) = &self.subcommand {
+				if subcommand.should_switch_profile() {
+					info!("Switching generation");
+					config
+						.command_on(host, "nix-env", true)
+						.args(&["-p", "/nix/var/nix/profiles/system", "--set"])
+						.arg(&built)
+						.inherit_stdio()
+						.run()?;
+				}
+				info!("Executing activation script");
+				let mut switch_script = built.clone();
+				switch_script.push("bin");
+				switch_script.push("switch-to-configuration");
+				config
+					.command_on(host, switch_script, true)
+					.arg(subcommand.name())
+					.inherit_stdio()
+					.run()?;
+			}
+		}
+		Ok(())
+	}
+}
addedcmds/fleet/src/cmds/mod.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/cmds/mod.rs
@@ -0,0 +1,2 @@
+pub mod build_systems;
+pub mod secrets;
addedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -0,0 +1,80 @@
+use crate::{fleetdata::FleetSecret, host::Config};
+use anyhow::{bail, Result};
+use std::io::{self, Cursor, Read};
+use structopt::StructOpt;
+
+#[derive(StructOpt)]
+pub enum Secrets {
+	/// Force load keys for all defined hosts
+	ForceKeys,
+	/// Add secret, data should be provided in stdin
+	Add {
+		/// Secret name
+		name: String,
+		/// Secret owners
+		machines: Vec<String>,
+		/// Override secret if already present
+		#[structopt(long)]
+		force: bool,
+		#[structopt(long)]
+		public: Option<String>,
+	},
+}
+
+impl Secrets {
+	pub fn run(self, config: &Config) -> Result<()> {
+		match self {
+			Secrets::ForceKeys => {
+				for host in config.list_hosts()? {
+					if config.should_skip(&host) {
+						continue;
+					}
+					config.key(&host)?;
+				}
+			}
+			Secrets::Add {
+				machines,
+				name,
+				force,
+				public,
+			} => {
+				let recipients = machines
+					.iter()
+					.map(|m| config.recipient(m))
+					.collect::<Result<Vec<_>>>()?;
+
+				let secret = {
+					let mut input = vec![];
+					io::stdin().read_to_end(&mut input)?;
+
+					let mut encrypted = vec![];
+					let recipients = recipients
+						.iter()
+						.cloned()
+						.map(|r| Box::new(r) as Box<dyn age::Recipient>)
+						.collect();
+					let mut encryptor =
+						age::Encryptor::with_recipients(recipients).wrap_output(&mut encrypted)?;
+					io::copy(&mut Cursor::new(input), &mut encryptor)?;
+					encryptor.finish()?;
+					encrypted
+				};
+
+				let mut data = config.data_mut();
+				if data.secrets.contains_key(&name) && !force {
+					bail!("secret already defined");
+				}
+				data.secrets.insert(
+					name,
+					FleetSecret {
+						owners: machines,
+						expire_at: None,
+						secret,
+						public,
+					},
+				);
+			}
+		}
+		Ok(())
+	}
+}
addedcmds/fleet/src/command.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/command.rs
@@ -0,0 +1,51 @@
+use std::{
+	ffi::OsStr,
+	process::{Command, Stdio},
+};
+
+use anyhow::{Context, Result};
+use serde::de::DeserializeOwned;
+
+pub trait CommandExt {
+	fn run(&mut self) -> Result<()>;
+	fn run_json<T: DeserializeOwned>(&mut self) -> Result<T>;
+	fn run_string(&mut self) -> Result<String>;
+	fn inherit_stdio(&mut self) -> &mut Self;
+	fn ssh_on(host: impl AsRef<OsStr>, command: impl AsRef<OsStr>) -> Self;
+}
+
+impl CommandExt for Command {
+	fn inherit_stdio(&mut self) -> &mut Self {
+		self.stderr(Stdio::inherit());
+		self
+	}
+
+	fn run(&mut self) -> Result<()> {
+		self.inherit_stdio();
+		let out = self.output()?;
+		if !out.status.success() {
+			anyhow::bail!("command ({:?}) failed with status {}", self, out.status);
+		}
+		Ok(())
+	}
+
+	fn run_json<T: DeserializeOwned>(&mut self) -> Result<T> {
+		let str = self.run_string()?;
+		serde_json::from_str(&str).with_context(|| format!("{:?}", str))
+	}
+
+	fn run_string(&mut self) -> Result<String> {
+		self.inherit_stdio();
+		let out = self.output()?;
+		if !out.status.success() {
+			anyhow::bail!("command ({:?}) failed with status {}", self, out.status);
+		}
+		Ok(String::from_utf8(out.stdout)?)
+	}
+
+	fn ssh_on(host: impl AsRef<OsStr>, command: impl AsRef<OsStr>) -> Self {
+		let mut cmd = Command::new("ssh");
+		cmd.arg(host).arg("--").arg(command);
+		cmd
+	}
+}
addedcmds/fleet/src/fleetdata.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/fleetdata.rs
@@ -0,0 +1,49 @@
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use std::collections::BTreeMap;
+
+#[derive(Serialize, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct HostData {
+	#[serde(default)]
+	#[serde(skip_serializing_if = "String::is_empty")]
+	pub encryption_key: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct FleetData {
+	#[serde(default)]
+	pub hosts: BTreeMap<String, HostData>,
+	#[serde(default)]
+	#[serde(skip_serializing_if = "BTreeMap::is_empty")]
+	pub secrets: BTreeMap<String, FleetSecret>,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct FleetSecret {
+	pub owners: Vec<String>,
+	#[serde(default)]
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub expire_at: Option<DateTime<Utc>>,
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub public: Option<String>,
+	#[serde(serialize_with = "as_z85", deserialize_with = "from_z85")]
+	pub secret: Vec<u8>,
+}
+
+fn as_z85<S>(key: &[u8], serializer: S) -> Result<S::Ok, S::Error>
+where
+	S: Serializer,
+{
+	serializer.serialize_str(&z85::encode(&key))
+}
+
+fn from_z85<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
+where
+	D: Deserializer<'de>,
+{
+	use serde::de::Error;
+	String::deserialize(deserializer)
+		.and_then(|string| z85::decode(&string).map_err(|err| Error::custom(err.to_string())))
+}
addedcmds/fleet/src/host.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/host.rs
@@ -0,0 +1,141 @@
+use std::{
+	cell::{Ref, RefCell, RefMut},
+	env::current_dir,
+	ffi::{OsStr, OsString},
+	ops::Deref,
+	path::PathBuf,
+	process::Command,
+	sync::Arc,
+};
+
+use anyhow::Result;
+use structopt::clap::ArgGroup;
+use structopt::StructOpt;
+
+use crate::{command::CommandExt, fleetdata::FleetData};
+
+pub struct FleetConfigInternals {
+	pub directory: PathBuf,
+	pub opts: FleetOpts,
+	pub data: RefCell<FleetData>,
+}
+
+#[derive(Clone)]
+pub struct Config(Arc<FleetConfigInternals>);
+
+impl Deref for Config {
+	type Target = FleetConfigInternals;
+
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+
+impl Config {
+	pub fn should_skip(&self, host: &str) -> bool {
+		if !self.opts.skip.is_empty() {
+			self.opts.skip.iter().any(|h| h as &str == host)
+		} else if !self.opts.only.is_empty() {
+			!self.opts.only.iter().any(|h| h as &str == host)
+		} else {
+			false
+		}
+	}
+	pub fn is_local(&self, host: &str) -> bool {
+		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)
+	}
+
+	pub fn command_on(&self, host: &str, program: impl AsRef<OsStr>, sudo: bool) -> Command {
+		if self.is_local(host) {
+			if sudo {
+				let mut cmd = Command::new("sudo");
+				cmd.arg(program);
+				cmd
+			} else {
+				Command::new(program)
+			}
+		} else {
+			let mut cmd = Command::new("ssh");
+			cmd.arg(host).arg("--");
+			if sudo {
+				cmd.arg("sudo");
+			}
+			cmd.arg(program);
+			cmd
+		}
+	}
+
+	pub fn full_attr_name(&self, attr_name: &str) -> OsString {
+		let mut str = self.directory.as_os_str().to_owned();
+		str.push("#");
+		str.push(attr_name);
+		str
+	}
+
+	pub fn list_hosts(&self) -> Result<Vec<String>> {
+		Command::new("nix")
+			.arg("eval")
+			.arg(self.full_attr_name("fleetConfigurations.default.configuredHosts"))
+			.args(&["--apply", "builtins.attrNames", "--json", "--show-trace"])
+			.inherit_stdio()
+			.run_json()
+	}
+
+	pub fn data(&self) -> Ref<FleetData> {
+		self.data.borrow()
+	}
+	pub fn data_mut(&self) -> RefMut<FleetData> {
+		self.data.borrow_mut()
+	}
+
+	pub fn save(&self) -> Result<()> {
+		let mut fleet_data_path = self.directory.clone();
+		fleet_data_path.push("fleet.nix");
+		let data = nixlike::serialize(&self.data() as &FleetData)?;
+		std::fs::write(
+			fleet_data_path,
+			format!(
+				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",
+				data
+			),
+		)?;
+		Ok(())
+	}
+}
+
+#[derive(StructOpt, Clone)]
+#[structopt(group = ArgGroup::with_name("target_hosts"))]
+pub struct FleetOpts {
+	/// All hosts except those would be skipped
+	#[structopt(long, number_of_values = 1, group = "target_hosts")]
+	only: Vec<String>,
+
+	/// Hosts to skip
+	#[structopt(long, number_of_values = 1, group = "target_hosts")]
+	skip: Vec<String>,
+
+	/// Host, which should be threaten as current machine
+	#[structopt(long)]
+	pub localhost: Option<String>,
+}
+
+impl FleetOpts {
+	pub fn build(mut self) -> Result<Config> {
+		if self.localhost.is_none() {
+			self.localhost
+				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());
+		}
+		let directory = current_dir()?;
+
+		let mut fleet_data_path = directory.clone();
+		fleet_data_path.push("fleet.nix");
+		let bytes = std::fs::read_to_string(fleet_data_path)?;
+		let data = nixlike::parse_str(&bytes)?;
+
+		Ok(Config(Arc::new(FleetConfigInternals {
+			opts: self,
+			directory,
+			data,
+		})))
+	}
+}
addedcmds/fleet/src/keys.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/keys.rs
@@ -0,0 +1,59 @@
+use std::str::FromStr;
+
+use crate::{command::CommandExt, host::Config};
+use anyhow::{anyhow, Result};
+use log::warn;
+
+impl Config {
+	pub fn cached_key(&self, host: &str) -> Option<String> {
+		let data = self.data();
+		let key = data.hosts.get(host).map(|h| &h.encryption_key);
+		if let Some(key) = key {
+			if key.is_empty() {
+				return None;
+			}
+		}
+		key.cloned()
+	}
+	pub fn update_key(&self, host: &str, key: String) {
+		let mut data = self.data_mut();
+		let host = data.hosts.entry(host.to_string()).or_default();
+		host.encryption_key = key.trim().to_string();
+	}
+
+	pub fn key(&self, host: &str) -> anyhow::Result<String> {
+		if let Some(key) = self.cached_key(host) {
+			Ok(key)
+		} else {
+			warn!("Loading key for {}", host);
+			let key = self
+				.command_on(host, "cat", false)
+				.arg("/etc/ssh/ssh_host_ed25519_key.pub")
+				.run_string()?;
+			self.update_key(host, key.clone());
+			Ok(key)
+		}
+	}
+	pub fn recipient(&self, host: &str) -> anyhow::Result<age::ssh::Recipient> {
+		let key = self.key(host)?;
+		age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
+	}
+
+	pub fn orphaned_data(&self) -> Result<Vec<String>> {
+		let mut out = Vec::new();
+		let host_names = self.list_hosts()?;
+		for hostname in self
+			.data()
+			.hosts
+			.iter()
+			.filter(|(_, host)| !host.encryption_key.is_empty())
+			.map(|(n, _)| n)
+		{
+			if !host_names.contains(&hostname.to_owned()) {
+				out.push(hostname.to_owned())
+			}
+		}
+
+		Ok(out)
+	}
+}
addedcmds/fleet/src/main.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/main.rs
@@ -0,0 +1,64 @@
+pub mod command;
+pub mod host;
+pub mod keys;
+
+pub mod cmds;
+pub mod nix;
+
+mod fleetdata;
+
+use anyhow::Result;
+use structopt::clap::AppSettings::*;
+use structopt::StructOpt;
+
+use cmds::{build_systems::BuildSystems, secrets::Secrets};
+use host::{Config, FleetOpts};
+
+#[derive(StructOpt)]
+enum Opts {
+	/// Prepare systems for deployments
+	BuildSystems(BuildSystems),
+	/// Secret management
+	Secrets(Secrets),
+}
+
+#[derive(StructOpt)]
+#[structopt(
+	version = "1.0",
+	author,
+	global_setting(ColorAuto),
+	global_setting(ColoredHelp)
+)]
+struct RootOpts {
+	#[structopt(flatten)]
+	fleet_opts: FleetOpts,
+	#[structopt(subcommand)]
+	command: Opts,
+}
+
+fn run_command(config: &Config, command: Opts) -> Result<()> {
+	match command {
+		Opts::BuildSystems(c) => c.run(config)?,
+		Opts::Secrets(s) => s.run(config)?,
+	};
+	Ok(())
+}
+
+fn main() -> Result<()> {
+	env_logger::Builder::new()
+		.filter_level(log::LevelFilter::Info)
+		.init();
+	let opts = RootOpts::from_args();
+	let config = opts.fleet_opts.build()?;
+
+	match run_command(&config, opts.command) {
+		Ok(()) => {
+			config.save()?;
+			Ok(())
+		}
+		Err(e) => {
+			let _ = config.save();
+			Err(e)
+		}
+	}
+}
addedcmds/fleet/src/nix.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/nix.rs
@@ -0,0 +1,3 @@
+pub const HOSTS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredHosts";
+pub const SECRETS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSecrets";
+pub const SYSTEMS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSystems";
deletedsrc/cmds/build_systems.rsdiffbeforeafterboth
--- a/src/cmds/build_systems.rs
+++ /dev/null
@@ -1,122 +0,0 @@
-use std::process::Command;
-
-use crate::{command::CommandExt, host::Config, nix::SYSTEMS_ATTRIBUTE};
-use anyhow::Result;
-use log::info;
-use structopt::StructOpt;
-
-#[derive(StructOpt)]
-pub struct BuildSystems {
-	/// --builders arg for nix
-	#[structopt(long)]
-	builders: Option<String>,
-	/// Jobs to run locally
-	#[structopt(long)]
-	jobs: Option<usize>,
-	/// Do not continue on error
-	#[structopt(long)]
-	fail_fast: bool,
-	#[structopt(long)]
-	privileged_build: bool,
-	#[structopt(subcommand)]
-	subcommand: Option<Subcommand>,
-}
-
-#[derive(StructOpt)]
-enum Subcommand {
-	/// Switch to built system until reboot
-	Test,
-	/// Switch to built system after reboot
-	Boot,
-	/// test + boot
-	Switch,
-}
-impl Subcommand {
-	fn should_switch_profile(&self) -> bool {
-		matches!(self, Self::Test | Self::Switch)
-	}
-	fn name(&self) -> &'static str {
-		match self {
-			Self::Test => "test",
-			Self::Boot => "boot",
-			Self::Switch => "switch",
-		}
-	}
-}
-
-impl BuildSystems {
-	pub fn run(self, config: &Config) -> Result<()> {
-		let hosts = config.list_hosts()?;
-
-		for host in hosts.iter() {
-			if config.should_skip(host) {
-				continue;
-			}
-			info!("Building host {}", host);
-			let built = {
-				let dir = tempfile::tempdir()?;
-				dir.path().to_owned()
-			};
-
-			let mut nix_build = if self.privileged_build {
-				let mut out = Command::new("sudo");
-				out.arg("nix");
-				out
-			} else {
-				Command::new("nix")
-			};
-			nix_build
-				.args(&["build", "--impure", "--no-link", "--out-link"])
-				.arg(&built)
-				.arg(format!(
-					"{}.{}.config.system.build.toplevel",
-					SYSTEMS_ATTRIBUTE, host,
-				));
-
-			if let Some(builders) = &self.builders {
-				nix_build.arg("--builders").arg(builders);
-			}
-			if let Some(jobs) = &self.jobs {
-				nix_build.arg("--max-jobs");
-				nix_build.arg(format!("{}", jobs));
-			}
-			if !self.fail_fast {
-				nix_build.arg("--keep-going");
-			}
-
-			nix_build.inherit_stdio().run()?;
-			let built = std::fs::canonicalize(built)?;
-			info!("Built closure: {:?}", built);
-			if !config.is_local(host) {
-				info!("Uploading system closure");
-				Command::new("nix")
-					.args(&["copy", "--to"])
-					.arg(format!("ssh://root@{}", host))
-					.arg(&built)
-					.inherit_stdio()
-					.run()?;
-			}
-			if let Some(subcommand) = &self.subcommand {
-				if subcommand.should_switch_profile() {
-					info!("Switching generation");
-					config
-						.command_on(host, "nix-env", true)
-						.args(&["-p", "/nix/var/nix/profiles/system", "--set"])
-						.arg(&built)
-						.inherit_stdio()
-						.run()?;
-				}
-				info!("Executing activation script");
-				let mut switch_script = built.clone();
-				switch_script.push("bin");
-				switch_script.push("switch-to-configuration");
-				config
-					.command_on(host, switch_script, true)
-					.arg(subcommand.name())
-					.inherit_stdio()
-					.run()?;
-			}
-		}
-		Ok(())
-	}
-}
deletedsrc/cmds/mod.rsdiffbeforeafterboth
--- a/src/cmds/mod.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod build_systems;
-pub mod secrets;
deletedsrc/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/src/cmds/secrets/mod.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use crate::{fleetdata::FleetSecret, host::Config};
-use anyhow::{bail, Result};
-use std::io::{self, Cursor, Read};
-use structopt::StructOpt;
-
-#[derive(StructOpt)]
-pub enum Secrets {
-	/// Force load keys for all defined hosts
-	ForceKeys,
-	/// Add secret, data should be provided in stdin
-	Add {
-		/// Secret name
-		name: String,
-		/// Secret owners
-		machines: Vec<String>,
-		/// Override secret if already present
-		#[structopt(long)]
-		force: bool,
-		#[structopt(long)]
-		public: Option<String>,
-	},
-}
-
-impl Secrets {
-	pub fn run(self, config: &Config) -> Result<()> {
-		match self {
-			Secrets::ForceKeys => {
-				for host in config.list_hosts()? {
-					if config.should_skip(&host) {
-						continue;
-					}
-					config.key(&host)?;
-				}
-			}
-			Secrets::Add {
-				machines,
-				name,
-				force,
-				public,
-			} => {
-				let recipients = machines
-					.iter()
-					.map(|m| config.recipient(m))
-					.collect::<Result<Vec<_>>>()?;
-
-				let secret = {
-					let mut input = vec![];
-					io::stdin().read_to_end(&mut input)?;
-
-					let mut encrypted = vec![];
-					let recipients = recipients
-						.iter()
-						.cloned()
-						.map(|r| Box::new(r) as Box<dyn age::Recipient>)
-						.collect();
-					let mut encryptor =
-						age::Encryptor::with_recipients(recipients).wrap_output(&mut encrypted)?;
-					io::copy(&mut Cursor::new(input), &mut encryptor)?;
-					encryptor.finish()?;
-					encrypted
-				};
-
-				let mut data = config.data_mut();
-				if data.secrets.contains_key(&name) && !force {
-					bail!("secret already defined");
-				}
-				data.secrets.insert(
-					name,
-					FleetSecret {
-						owners: machines,
-						expire_at: None,
-						secret,
-						public,
-					},
-				);
-			}
-		}
-		Ok(())
-	}
-}
deletedsrc/command.rsdiffbeforeafterboth
--- a/src/command.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-use std::{
-	ffi::OsStr,
-	process::{Command, Stdio},
-};
-
-use anyhow::{Context, Result};
-use serde::de::DeserializeOwned;
-
-pub trait CommandExt {
-	fn run(&mut self) -> Result<()>;
-	fn run_json<T: DeserializeOwned>(&mut self) -> Result<T>;
-	fn run_string(&mut self) -> Result<String>;
-	fn inherit_stdio(&mut self) -> &mut Self;
-	fn ssh_on(host: impl AsRef<OsStr>, command: impl AsRef<OsStr>) -> Self;
-}
-
-impl CommandExt for Command {
-	fn inherit_stdio(&mut self) -> &mut Self {
-		self.stderr(Stdio::inherit());
-		self
-	}
-
-	fn run(&mut self) -> Result<()> {
-		self.inherit_stdio();
-		let out = self.output()?;
-		if !out.status.success() {
-			anyhow::bail!("command ({:?}) failed with status {}", self, out.status);
-		}
-		Ok(())
-	}
-
-	fn run_json<T: DeserializeOwned>(&mut self) -> Result<T> {
-		let str = self.run_string()?;
-		serde_json::from_str(&str).with_context(|| format!("{:?}", str))
-	}
-
-	fn run_string(&mut self) -> Result<String> {
-		self.inherit_stdio();
-		let out = self.output()?;
-		if !out.status.success() {
-			anyhow::bail!("command ({:?}) failed with status {}", self, out.status);
-		}
-		Ok(String::from_utf8(out.stdout)?)
-	}
-
-	fn ssh_on(host: impl AsRef<OsStr>, command: impl AsRef<OsStr>) -> Self {
-		let mut cmd = Command::new("ssh");
-		cmd.arg(host).arg("--").arg(command);
-		cmd
-	}
-}
deletedsrc/fleetdata.rsdiffbeforeafterboth
--- a/src/fleetdata.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use chrono::{DateTime, Utc};
-use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use std::collections::BTreeMap;
-
-#[derive(Serialize, Deserialize, Default)]
-#[serde(rename_all = "camelCase")]
-pub struct HostData {
-	#[serde(default)]
-	#[serde(skip_serializing_if = "String::is_empty")]
-	pub encryption_key: String,
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct FleetData {
-	#[serde(default)]
-	pub hosts: BTreeMap<String, HostData>,
-	#[serde(default)]
-	#[serde(skip_serializing_if = "BTreeMap::is_empty")]
-	pub secrets: BTreeMap<String, FleetSecret>,
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct FleetSecret {
-	pub owners: Vec<String>,
-	#[serde(default)]
-	#[serde(skip_serializing_if = "Option::is_none")]
-	pub expire_at: Option<DateTime<Utc>>,
-	#[serde(skip_serializing_if = "Option::is_none")]
-	pub public: Option<String>,
-	#[serde(serialize_with = "as_z85", deserialize_with = "from_z85")]
-	pub secret: Vec<u8>,
-}
-
-fn as_z85<S>(key: &[u8], serializer: S) -> Result<S::Ok, S::Error>
-where
-	S: Serializer,
-{
-	serializer.serialize_str(&z85::encode(&key))
-}
-
-fn from_z85<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
-where
-	D: Deserializer<'de>,
-{
-	use serde::de::Error;
-	String::deserialize(deserializer)
-		.and_then(|string| z85::decode(&string).map_err(|err| Error::custom(err.to_string())))
-}
deletedsrc/host.rsdiffbeforeafterboth
--- a/src/host.rs
+++ /dev/null
@@ -1,141 +0,0 @@
-use std::{
-	cell::{Ref, RefCell, RefMut},
-	env::current_dir,
-	ffi::{OsStr, OsString},
-	ops::Deref,
-	path::PathBuf,
-	process::Command,
-	sync::Arc,
-};
-
-use anyhow::Result;
-use structopt::clap::ArgGroup;
-use structopt::StructOpt;
-
-use crate::{command::CommandExt, fleetdata::FleetData};
-
-pub struct FleetConfigInternals {
-	pub directory: PathBuf,
-	pub opts: FleetOpts,
-	pub data: RefCell<FleetData>,
-}
-
-#[derive(Clone)]
-pub struct Config(Arc<FleetConfigInternals>);
-
-impl Deref for Config {
-	type Target = FleetConfigInternals;
-
-	fn deref(&self) -> &Self::Target {
-		&self.0
-	}
-}
-
-impl Config {
-	pub fn should_skip(&self, host: &str) -> bool {
-		if !self.opts.skip.is_empty() {
-			self.opts.skip.iter().any(|h| h as &str == host)
-		} else if !self.opts.only.is_empty() {
-			!self.opts.only.iter().any(|h| h as &str == host)
-		} else {
-			false
-		}
-	}
-	pub fn is_local(&self, host: &str) -> bool {
-		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)
-	}
-
-	pub fn command_on(&self, host: &str, program: impl AsRef<OsStr>, sudo: bool) -> Command {
-		if self.is_local(host) {
-			if sudo {
-				let mut cmd = Command::new("sudo");
-				cmd.arg(program);
-				cmd
-			} else {
-				Command::new(program)
-			}
-		} else {
-			let mut cmd = Command::new("ssh");
-			cmd.arg(host).arg("--");
-			if sudo {
-				cmd.arg("sudo");
-			}
-			cmd.arg(program);
-			cmd
-		}
-	}
-
-	pub fn full_attr_name(&self, attr_name: &str) -> OsString {
-		let mut str = self.directory.as_os_str().to_owned();
-		str.push("#");
-		str.push(attr_name);
-		str
-	}
-
-	pub fn list_hosts(&self) -> Result<Vec<String>> {
-		Command::new("nix")
-			.arg("eval")
-			.arg(self.full_attr_name("fleetConfigurations.default.configuredHosts"))
-			.args(&["--apply", "builtins.attrNames", "--json", "--show-trace"])
-			.inherit_stdio()
-			.run_json()
-	}
-
-	pub fn data(&self) -> Ref<FleetData> {
-		self.data.borrow()
-	}
-	pub fn data_mut(&self) -> RefMut<FleetData> {
-		self.data.borrow_mut()
-	}
-
-	pub fn save(&self) -> Result<()> {
-		let mut fleet_data_path = self.directory.clone();
-		fleet_data_path.push("fleet.nix");
-		let data = nixlike::serialize(&self.data() as &FleetData)?;
-		std::fs::write(
-			fleet_data_path,
-			format!(
-				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",
-				data
-			),
-		)?;
-		Ok(())
-	}
-}
-
-#[derive(StructOpt, Clone)]
-#[structopt(group = ArgGroup::with_name("target_hosts"))]
-pub struct FleetOpts {
-	/// All hosts except those would be skipped
-	#[structopt(long, number_of_values = 1, group = "target_hosts")]
-	only: Vec<String>,
-
-	/// Hosts to skip
-	#[structopt(long, number_of_values = 1, group = "target_hosts")]
-	skip: Vec<String>,
-
-	/// Host, which should be threaten as current machine
-	#[structopt(long)]
-	pub localhost: Option<String>,
-}
-
-impl FleetOpts {
-	pub fn build(mut self) -> Result<Config> {
-		if self.localhost.is_none() {
-			self.localhost
-				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());
-		}
-		let directory = current_dir()?;
-
-		let mut fleet_data_path = directory.clone();
-		fleet_data_path.push("fleet.nix");
-		let bytes = std::fs::read_to_string(fleet_data_path)?;
-		let data = nixlike::parse_str(&bytes)?;
-
-		Ok(Config(Arc::new(FleetConfigInternals {
-			opts: self,
-			directory,
-			data,
-		})))
-	}
-}
deletedsrc/keys.rsdiffbeforeafterboth
--- a/src/keys.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use std::str::FromStr;
-
-use crate::{command::CommandExt, host::Config};
-use anyhow::{anyhow, Result};
-use log::warn;
-
-impl Config {
-	pub fn cached_key(&self, host: &str) -> Option<String> {
-		let data = self.data();
-		let key = data.hosts.get(host).map(|h| &h.encryption_key);
-		if let Some(key) = key {
-			if key.is_empty() {
-				return None;
-			}
-		}
-		key.cloned()
-	}
-	pub fn update_key(&self, host: &str, key: String) {
-		let mut data = self.data_mut();
-		let host = data.hosts.entry(host.to_string()).or_default();
-		host.encryption_key = key.trim().to_string();
-	}
-
-	pub fn key(&self, host: &str) -> anyhow::Result<String> {
-		if let Some(key) = self.cached_key(host) {
-			Ok(key)
-		} else {
-			warn!("Loading key for {}", host);
-			let key = self
-				.command_on(host, "cat", false)
-				.arg("/etc/ssh/ssh_host_ed25519_key.pub")
-				.run_string()?;
-			self.update_key(host, key.clone());
-			Ok(key)
-		}
-	}
-	pub fn recipient(&self, host: &str) -> anyhow::Result<age::ssh::Recipient> {
-		let key = self.key(host)?;
-		age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
-	}
-
-	pub fn orphaned_data(&self) -> Result<Vec<String>> {
-		let mut out = Vec::new();
-		let host_names = self.list_hosts()?;
-		for hostname in self
-			.data()
-			.hosts
-			.iter()
-			.filter(|(_, host)| !host.encryption_key.is_empty())
-			.map(|(n, _)| n)
-		{
-			if !host_names.contains(&hostname.to_owned()) {
-				out.push(hostname.to_owned())
-			}
-		}
-
-		Ok(out)
-	}
-}
deletedsrc/main.rsdiffbeforeafterboth
--- a/src/main.rs
+++ /dev/null
@@ -1,58 +0,0 @@
-pub mod command;
-pub mod host;
-pub mod keys;
-
-pub mod cmds;
-pub mod nix;
-
-mod fleetdata;
-
-use anyhow::Result;
-use structopt::StructOpt;
-
-use cmds::{build_systems::BuildSystems, secrets::Secrets};
-use host::{Config, FleetOpts};
-
-#[derive(StructOpt)]
-#[structopt(version = "1.0", author = "CertainLach <iam@lach.pw>")]
-enum Opts {
-	/// Prepare systems for deployments
-	BuildSystems(BuildSystems),
-	/// Secret management
-	Secrets(Secrets),
-}
-
-#[derive(StructOpt)]
-struct RootOpts {
-	#[structopt(flatten)]
-	fleet_opts: FleetOpts,
-	#[structopt(subcommand)]
-	command: Opts,
-}
-
-fn run_command(config: &Config, command: Opts) -> Result<()> {
-	match command {
-		Opts::BuildSystems(c) => c.run(config)?,
-		Opts::Secrets(s) => s.run(config)?,
-	};
-	Ok(())
-}
-
-fn main() -> Result<()> {
-	env_logger::Builder::new()
-		.filter_level(log::LevelFilter::Info)
-		.init();
-	let opts = RootOpts::from_args();
-	let config = opts.fleet_opts.build()?;
-
-	match run_command(&config, opts.command) {
-		Ok(()) => {
-			config.save()?;
-			Ok(())
-		}
-		Err(e) => {
-			let _ = config.save();
-			Err(e)
-		}
-	}
-}
deletedsrc/nix.rsdiffbeforeafterboth
--- a/src/nix.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub const HOSTS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredHosts";
-pub const SECRETS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSecrets";
-pub const SYSTEMS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSystems";