difftreelog
feat libssh preparation
in: trunk
7 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -651,6 +651,27 @@
]
[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
name = "displaydoc"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -731,13 +752,16 @@
"clap",
"futures",
"hostname",
+ "human-repr",
"indicatif",
"itertools",
"nixlike",
"once_cell",
+ "openssh",
"owo-colors",
"peg",
"r2d2",
+ "regex",
"serde",
"serde_json",
"shlex",
@@ -1019,6 +1043,12 @@
]
[[package]]
+name = "human-repr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f58b778a5761513caf593693f8951c97a5b610841e754788400f32102eefdff1"
+
+[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1257,6 +1287,17 @@
]
[[package]]
+name = "libredox"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
+dependencies = [
+ "bitflags 2.4.1",
+ "libc",
+ "redox_syscall 0.4.1",
+]
+
+[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1480,6 +1521,28 @@
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
+name = "openssh"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfe68c42d6ee6bd9de175b7a5d9bb86aa99d4e2fa7cf2f2a44e97f60b6d2759"
+dependencies = [
+ "dirs",
+ "libc",
+ "once_cell",
+ "shell-escape",
+ "tempfile",
+ "thiserror",
+ "tokio",
+ "tokio-pipe",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1804,15 +1867,26 @@
]
[[package]]
+name = "redox_users"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
+dependencies = [
+ "getrandom 0.2.10",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
name = "regex"
-version = "1.9.5"
+version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
+checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.3.8",
- "regex-syntax 0.7.5",
+ "regex-automata 0.4.3",
+ "regex-syntax 0.8.2",
]
[[package]]
@@ -1826,13 +1900,13 @@
[[package]]
name = "regex-automata"
-version = "0.3.8"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
+checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.7.5",
+ "regex-syntax 0.8.2",
]
[[package]]
@@ -1843,9 +1917,9 @@
[[package]]
name = "regex-syntax"
-version = "0.7.5"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "rnix"
@@ -2099,6 +2173,12 @@
]
[[package]]
+name = "shell-escape"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
+
+[[package]]
name = "shlex"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2379,6 +2459,16 @@
]
[[package]]
+name = "tokio-pipe"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f213a84bffbd61b8fa0ba8a044b4bbe35d471d0b518867181e82bd5c15542784"
+dependencies = [
+ "libc",
+ "tokio",
+]
+
+[[package]]
name = "tokio-util"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
cmds/fleet/Cargo.tomldiffbeforeafterboth--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -11,7 +11,7 @@
serde_json = "1.0"
time = { version = "0.3.30", features = ["serde"] }
tempfile = "3.8"
-once_cell = "1.18"
+once_cell = "1.18.0"
hostname = "0.3.1"
age-core = "0.9.0"
peg = "0.8.2"
@@ -41,3 +41,6 @@
r2d2 = "0.8.10"
abort-on-drop = "0.2.2"
unindent = "0.2.3"
+regex = "1.10.2"
+openssh = "0.10.1"
+human-repr = "1.1.0"
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -336,15 +336,14 @@
if !config.is_local(&host) {
info!("uploading system closure");
{
- let mut sign = MyCommand::new("sudo");
+ let mut sign = MyCommand::new("nix");
// Private key for host machine is registered in nix-sign.nix
- sign.arg("nix")
- .arg("store")
+ sign.arg("store")
.arg("sign")
.comparg("--key-file", "/etc/nix/private-key")
.arg("-r")
.arg(&built);
- if let Err(e) = sign.run_nix().await {
+ if let Err(e) = sign.sudo().run_nix().await {
warn!("Failed to sign store paths: {e}");
};
}
cmds/fleet/src/command.rsdiffbeforeafterboth--- a/cmds/fleet/src/command.rs
+++ b/cmds/fleet/src/command.rs
@@ -1,4 +1,5 @@
use std::{
+ borrow::Cow,
collections::HashMap,
ffi::OsStr,
process::Stdio,
@@ -6,8 +7,12 @@
task::Poll,
};
-use anyhow::Result;
+use anyhow::{anyhow, Result};
use futures::StreamExt;
+use itertools::Either;
+use once_cell::sync::Lazy;
+use openssh::{OverSsh, Session};
+use regex::Regex;
use serde::{de::Visitor, Deserialize};
use tokio::{io::AsyncRead, process::Command, select};
use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};
@@ -37,6 +42,7 @@
command: String,
args: Vec<String>,
env: Vec<(String, String)>,
+ ssh_session: Option<Arc<Session>>,
}
impl MyCommand {
pub fn new(cmd: impl AsRef<OsStr>) -> Self {
@@ -45,6 +51,7 @@
command: ostoutf8(cmd),
args: vec![],
env: vec![],
+ ssh_session: None,
}
}
fn into_args(self) -> Vec<String> {
@@ -90,6 +97,18 @@
}
out
}
+ fn into_command_new(self) -> Result<Either<Command, openssh::OwningCommand<Arc<Session>>>> {
+ Ok(if let Some(session) = self.ssh_session.clone() {
+ let cmd = self.into_command();
+ Either::Right(
+ cmd.over_ssh(session)
+ .map_err(|e| anyhow!("ssh error: {e}"))?,
+ )
+ } else {
+ let cmd = self.into_command();
+ Either::Left(cmd)
+ })
+ }
pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
let arg = arg.as_ref();
self.args.push(ostoutf8(arg));
@@ -116,9 +135,15 @@
self
}
pub fn sudo(self) -> Self {
- let mut out = Self::new("sudo");
- out.args(self.into_args());
- out
+ if std::env::var_os("NO_SUDO").is_some() {
+ let mut out = Self::new("su");
+ out.arg("-c").arg(self.into_string());
+ out
+ } else {
+ let mut out = Self::new("sudo");
+ out.args(self.into_args());
+ out
+ }
}
pub fn ssh(self, on: impl AsRef<OsStr>) -> Self {
let mut out = Self::new("ssh");
@@ -126,6 +151,10 @@
out.arg(self.into_string());
out
}
+ pub fn over_ssh(mut self, session: Arc<Session>) -> Self {
+ self.ssh_session = Some(session);
+ self
+ }
pub async fn run(self) -> Result<()> {
let str = self.clone().into_string();
@@ -218,6 +247,11 @@
pub struct NixHandler {
spans: HashMap<u64, Span>,
}
+fn process_message(m: &str) -> Cow<'_, str> {
+ static OSC_CLEANER: Lazy<Regex> =
+ Lazy::new(|| Regex::new(r"\x1B\]([^\x07\x1C]*[\x07\x1C])?|\r").unwrap());
+ OSC_CLEANER.replace_all(m, "")
+}
impl Handler for NixHandler {
fn handle_line(&mut self, e: &str) {
if let Some(e) = e.strip_prefix("@nix ") {
@@ -303,7 +337,7 @@
{
let span = info_span!("job");
span.pb_start();
- span.pb_set_message(text.trim());
+ span.pb_set_message(&process_message(text.trim()));
self.spans.insert(id, span);
info!(target: "nix", "{}", text);
}
@@ -383,7 +417,7 @@
NixLog::Result { fields, id, typ } if typ == 101 && !fields.is_empty() => {
if let Some(span) = self.spans.get(&id) {
if let LogField::String(s) = &fields[0] {
- span.pb_set_message(s.trim());
+ span.pb_set_message(&process_message(s.trim()));
} else {
warn!("bad fields: {fields:?}");
}
cmds/fleet/src/host.rsdiffbeforeafterboth1use std::{2 env::current_dir,3 ffi::OsString,4 io::Write,5 ops::Deref,6 path::PathBuf,7 sync::{Arc, Mutex, MutexGuard},8};910use anyhow::{bail, Context, Result};11use clap::{ArgGroup, Parser};12use tempfile::NamedTempFile;1314use crate::{15 better_nix_eval::{Field, NixSessionPool},16 command::MyCommand,17 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},18};1920pub struct FleetConfigInternals {21 pub local_system: String,22 pub directory: PathBuf,23 pub opts: FleetOpts,24 pub data: Mutex<FleetData>,25 pub nix_args: Vec<OsString>,26 // fleetConfigurations.<name>27 pub fleet_field: Field,28 // fleet_config.configUnchecked29 pub config_field: Field,30}3132#[derive(Clone)]33pub struct Config(Arc<FleetConfigInternals>);3435impl Deref for Config {36 type Target = FleetConfigInternals;3738 fn deref(&self) -> &Self::Target {39 &self.040 }41}4243pub struct ConfigHost {44 pub name: String,45}4647impl Config {48 pub fn should_skip(&self, host: &str) -> bool {49 if !self.opts.skip.is_empty() {50 self.opts.skip.iter().any(|h| h as &str == host)51 } else if !self.opts.only.is_empty() {52 !self.opts.only.iter().any(|h| h as &str == host)53 } else {54 false55 }56 }57 pub fn is_local(&self, host: &str) -> bool {58 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)59 }6061 pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {62 if sudo {63 command = command.sudo();64 }65 if !self.is_local(host) {66 command = command.ssh(host);67 }68 command.run().await69 }70 pub async fn run_string_on(71 &self,72 host: &str,73 mut command: MyCommand,74 sudo: bool,75 ) -> Result<String> {76 if sudo {77 command = command.sudo();78 }79 if !self.is_local(host) {80 command = command.ssh(host);81 }82 command.run_string().await83 }8485 pub fn configuration_attr_name(&self, name: &str) -> OsString {86 let mut str = self.directory.as_os_str().to_owned();87 str.push("#");88 str.push(&format!(89 "fleetConfigurations.default.{}.{}",90 self.local_system, name91 ));92 str93 }9495 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {96 let names = self.fleet_field97 .get_field_deep(["configuredHosts"])98 .await?99 .list_fields()100 .await?;101 let mut out = vec![];102 for name in names {103 out.push(ConfigHost {104 name,105 })106 }107 Ok(out)108 }109 pub async fn system_config(&self, host: &str) -> Result<Field> {110 self.fleet_field.get_field_deep(["configuredSystems", host, "config"]).await111 }112113 pub(super) fn data(&self) -> MutexGuard<FleetData> {114 self.data.lock().unwrap()115 }116 pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {117 self.data.lock().unwrap()118 }119 /// Shared secrets configured in fleet.nix or in flake120 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {121 self.config_field122 .get_field("sharedSecrets")123 .await?124 .list_fields()125 .await126 }127 /// Shared secrets configured in fleet.nix128 pub fn list_shared(&self) -> Vec<String> {129 let data = self.data();130 data.shared_secrets.keys().cloned().collect()131 }132 pub fn has_shared(&self, name: &str) -> bool {133 let data = self.data();134 data.shared_secrets.contains_key(name)135 }136 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {137 let mut data = self.data_mut();138 data.shared_secrets.insert(name.to_owned(), shared);139 }140 pub fn remove_shared(&self, secret: &str) {141 let mut data = self.data_mut();142 data.shared_secrets.remove(secret);143 }144145 pub fn has_secret(&self, host: &str, secret: &str) -> bool {146 let data = self.data();147 let Some(host_secrets) = data.host_secrets.get(host) else {148 return false;149 };150 host_secrets.contains_key(secret)151 }152 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {153 let mut data = self.data_mut();154 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();155 host_secrets.insert(secret, value);156 }157158 pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>> {159 let data = z85::encode(&data);160 let mut cmd = MyCommand::new("fleet-install-secrets");161 cmd.arg("decrypt").eqarg("--secret", data);162 cmd = cmd.sudo().ssh(host);163 let encoded = cmd164 .run_string()165 .await166 .context("failed to call remote host for decrypt")?167 .trim()168 .to_owned();169 z85::decode(encoded).context("bad encoded data? outdated host?")170 }171 pub async fn reencrypt_on_host(172 &self,173 host: &str,174 data: Vec<u8>,175 targets: Vec<String>,176 ) -> Result<Vec<u8>> {177 let data = z85::encode(&data);178 let mut recmd = MyCommand::new("fleet-install-secrets");179 recmd.arg("reencrypt").eqarg("--secret", data);180 for target in targets {181 recmd.eqarg("--targets", target);182 }183 recmd = recmd.sudo().ssh(host);184 let encoded = recmd185 .run_string()186 .await187 .context("failed to call remote host for decrypt")?188 .trim()189 .to_owned();190 z85::decode(encoded).context("bad encoded data? outdated host?")191 }192193 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {194 let data = self.data();195 let Some(host_secrets) = data.host_secrets.get(host) else {196 bail!("no secrets for machine {host}");197 };198 let Some(secret) = host_secrets.get(secret) else {199 bail!("machine {host} has no secret {secret}");200 };201 Ok(secret.clone())202 }203 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {204 let data = self.data();205 let Some(secret) = data.shared_secrets.get(secret) else {206 bail!("no shared secret {secret}");207 };208 Ok(secret.clone())209 }210 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {211 self.config_field212 .get_field_deep(["sharedSecrets", secret, "expectedOwners"])213 .await?214 .as_json()215 .await216 }217218 pub fn save(&self) -> Result<()> {219 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;220 let data = nixlike::serialize(&self.data() as &FleetData)?;221 tempfile.write_all(222 format!(223 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",224 data225 )226 .as_bytes(),227 )?;228 let mut fleet_data_path = self.directory.clone();229 fleet_data_path.push("fleet.nix");230 tempfile.persist(fleet_data_path)?;231 Ok(())232 }233}234235#[derive(Parser, Clone)]236#[clap(group = ArgGroup::new("target_hosts"))]237pub struct FleetOpts {238 /// All hosts except those would be skipped239 #[clap(long, number_of_values = 1, group = "target_hosts")]240 only: Vec<String>,241242 /// Hosts to skip243 #[clap(long, number_of_values = 1, group = "target_hosts")]244 skip: Vec<String>,245246 /// Host, which should be threaten as current machine247 #[clap(long)]248 pub localhost: Option<String>,249250 // TODO: unhardcode x86_64-linux251 /// Override detected system for host, to perform builds via252 /// binfmt-declared qemu instead of trying to crosscompile253 #[clap(long, default_value = "detect")]254 pub local_system: String,255}256257impl FleetOpts {258 pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {259 if self.localhost.is_none() {260 self.localhost261 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());262 }263 let directory = current_dir()?;264265 let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;266 let root_field = pool.get().await?;267268 if self.local_system == "detect" {269 let builtins_field = Field::field(root_field.clone(), "builtins").await?;270 let system = builtins_field.get_field("currentSystem").await?;271 self.local_system = system.as_json().await?;272 }273 let local_system = self.local_system.clone();274275 let fleet_root = Field::field(root_field, "fleetConfigurations").await?;276277 let fleet_field = fleet_root278 .get_field_deep(["default", &local_system])279 .await?;280 let config_field = fleet_field.get_field("configUnchecked").await?;281282 let mut fleet_data_path = directory.clone();283 fleet_data_path.push("fleet.nix");284 let bytes = std::fs::read_to_string(fleet_data_path)?;285 let data = nixlike::parse_str(&bytes)?;286287 Ok(Config(Arc::new(FleetConfigInternals {288 opts: self,289 directory,290 data,291 local_system,292 nix_args,293 fleet_field,294 config_field,295 })))296 }297}1use std::{2 env::current_dir,3 ffi::OsString,4 io::Write,5 ops::Deref,6 path::PathBuf,7 sync::{Arc, Mutex, MutexGuard},8};910use anyhow::{anyhow, bail, Context, Result};11use clap::{ArgGroup, Parser};12use openssh::SessionBuilder;13use tempfile::NamedTempFile;1415use crate::{16 better_nix_eval::{Field, NixSessionPool},17 command::MyCommand,18 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},19};2021pub struct FleetConfigInternals {22 pub local_system: String,23 pub directory: PathBuf,24 pub opts: FleetOpts,25 pub data: Mutex<FleetData>,26 pub nix_args: Vec<OsString>,27 // fleetConfigurations.<name>28 pub fleet_field: Field,29 // fleet_config.configUnchecked30 pub config_field: Field,31}3233#[derive(Clone)]34pub struct Config(Arc<FleetConfigInternals>);3536impl Deref for Config {37 type Target = FleetConfigInternals;3839 fn deref(&self) -> &Self::Target {40 &self.041 }42}4344pub struct ConfigHost {45 pub name: String,46}47impl ConfigHost {48 async fn open_session(&self) -> Result<openssh::Session> {49 let mut session = SessionBuilder::default();5051 session52 .connect(&self.name)53 .await54 .map_err(|e| anyhow!("ssh error: {e}"))55 }56}5758impl Config {59 pub fn should_skip(&self, host: &str) -> bool {60 if !self.opts.skip.is_empty() {61 self.opts.skip.iter().any(|h| h as &str == host)62 } else if !self.opts.only.is_empty() {63 !self.opts.only.iter().any(|h| h as &str == host)64 } else {65 false66 }67 }68 pub fn is_local(&self, host: &str) -> bool {69 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)70 }7172 pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {73 if sudo {74 command = command.sudo();75 }76 if !self.is_local(host) {77 command = command.ssh(host);78 }79 command.run().await80 }81 pub async fn run_string_on(82 &self,83 host: &str,84 mut command: MyCommand,85 sudo: bool,86 ) -> Result<String> {87 if sudo {88 command = command.sudo();89 }90 if !self.is_local(host) {91 command = command.ssh(host);92 }93 command.run_string().await94 }9596 pub fn configuration_attr_name(&self, name: &str) -> OsString {97 let mut str = self.directory.as_os_str().to_owned();98 str.push("#");99 str.push(&format!(100 "fleetConfigurations.default.{}.{}",101 self.local_system, name102 ));103 str104 }105106 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {107 let names = self108 .fleet_field109 .get_field_deep(["configuredHosts"])110 .await?111 .list_fields()112 .await?;113 let mut out = vec![];114 for name in names {115 out.push(ConfigHost { name })116 }117 Ok(out)118 }119 pub async fn system_config(&self, host: &str) -> Result<Field> {120 self.fleet_field121 .get_field_deep(["configuredSystems", host, "config"])122 .await123 }124125 pub(super) fn data(&self) -> MutexGuard<FleetData> {126 self.data.lock().unwrap()127 }128 pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {129 self.data.lock().unwrap()130 }131 /// Shared secrets configured in fleet.nix or in flake132 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {133 self.config_field134 .get_field("sharedSecrets")135 .await?136 .list_fields()137 .await138 }139 /// Shared secrets configured in fleet.nix140 pub fn list_shared(&self) -> Vec<String> {141 let data = self.data();142 data.shared_secrets.keys().cloned().collect()143 }144 pub fn has_shared(&self, name: &str) -> bool {145 let data = self.data();146 data.shared_secrets.contains_key(name)147 }148 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {149 let mut data = self.data_mut();150 data.shared_secrets.insert(name.to_owned(), shared);151 }152 pub fn remove_shared(&self, secret: &str) {153 let mut data = self.data_mut();154 data.shared_secrets.remove(secret);155 }156157 pub fn has_secret(&self, host: &str, secret: &str) -> bool {158 let data = self.data();159 let Some(host_secrets) = data.host_secrets.get(host) else {160 return false;161 };162 host_secrets.contains_key(secret)163 }164 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {165 let mut data = self.data_mut();166 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();167 host_secrets.insert(secret, value);168 }169170 pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>> {171 let data = z85::encode(&data);172 let mut cmd = MyCommand::new("fleet-install-secrets");173 cmd.arg("decrypt").eqarg("--secret", data);174 cmd = cmd.sudo().ssh(host);175 let encoded = cmd176 .run_string()177 .await178 .context("failed to call remote host for decrypt")?179 .trim()180 .to_owned();181 z85::decode(encoded).context("bad encoded data? outdated host?")182 }183 pub async fn reencrypt_on_host(184 &self,185 host: &str,186 data: Vec<u8>,187 targets: Vec<String>,188 ) -> Result<Vec<u8>> {189 let data = z85::encode(&data);190 let mut recmd = MyCommand::new("fleet-install-secrets");191 recmd.arg("reencrypt").eqarg("--secret", data);192 for target in targets {193 recmd.eqarg("--targets", target);194 }195 recmd = recmd.sudo().ssh(host);196 let encoded = recmd197 .run_string()198 .await199 .context("failed to call remote host for decrypt")?200 .trim()201 .to_owned();202 z85::decode(encoded).context("bad encoded data? outdated host?")203 }204205 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {206 let data = self.data();207 let Some(host_secrets) = data.host_secrets.get(host) else {208 bail!("no secrets for machine {host}");209 };210 let Some(secret) = host_secrets.get(secret) else {211 bail!("machine {host} has no secret {secret}");212 };213 Ok(secret.clone())214 }215 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {216 let data = self.data();217 let Some(secret) = data.shared_secrets.get(secret) else {218 bail!("no shared secret {secret}");219 };220 Ok(secret.clone())221 }222 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {223 self.config_field224 .get_field_deep(["sharedSecrets", secret, "expectedOwners"])225 .await?226 .as_json()227 .await228 }229230 pub fn save(&self) -> Result<()> {231 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;232 let data = nixlike::serialize(&self.data() as &FleetData)?;233 tempfile.write_all(234 format!(235 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",236 data237 )238 .as_bytes(),239 )?;240 let mut fleet_data_path = self.directory.clone();241 fleet_data_path.push("fleet.nix");242 tempfile.persist(fleet_data_path)?;243 Ok(())244 }245}246247#[derive(Parser, Clone)]248#[clap(group = ArgGroup::new("target_hosts"))]249pub struct FleetOpts {250 /// All hosts except those would be skipped251 #[clap(long, number_of_values = 1, group = "target_hosts")]252 only: Vec<String>,253254 /// Hosts to skip255 #[clap(long, number_of_values = 1, group = "target_hosts")]256 skip: Vec<String>,257258 /// Host, which should be threaten as current machine259 #[clap(long)]260 pub localhost: Option<String>,261262 // TODO: unhardcode x86_64-linux263 /// Override detected system for host, to perform builds via264 /// binfmt-declared qemu instead of trying to crosscompile265 #[clap(long, default_value = "detect")]266 pub local_system: String,267}268269impl FleetOpts {270 pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {271 if self.localhost.is_none() {272 self.localhost273 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());274 }275 let directory = current_dir()?;276277 let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;278 let root_field = pool.get().await?;279280 if self.local_system == "detect" {281 let builtins_field = Field::field(root_field.clone(), "builtins").await?;282 let system = builtins_field.get_field("currentSystem").await?;283 self.local_system = system.as_json().await?;284 }285 let local_system = self.local_system.clone();286287 let fleet_root = Field::field(root_field, "fleetConfigurations").await?;288289 let fleet_field = fleet_root290 .get_field_deep(["default", &local_system])291 .await?;292 let config_field = fleet_field.get_field("configUnchecked").await?;293294 let mut fleet_data_path = directory.clone();295 fleet_data_path.push("fleet.nix");296 let bytes = std::fs::read_to_string(fleet_data_path)?;297 let data = nixlike::parse_str(&bytes)?;298299 Ok(Config(Arc::new(FleetConfigInternals {300 opts: self,301 directory,302 data,303 local_system,304 nix_args,305 fleet_field,306 config_field,307 })))308 }309}cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -5,8 +5,8 @@
pub(crate) mod host;
pub(crate) mod keys;
+pub(crate) mod better_nix_eval;
pub(crate) mod extra_args;
-pub(crate) mod better_nix_eval;
mod fleetdata;
@@ -21,6 +21,7 @@
use futures::stream::FuturesUnordered;
use futures::TryStreamExt;
use host::{Config, FleetOpts};
+use human_repr::HumanCount;
use indicatif::{ProgressState, ProgressStyle};
use tracing::{info, metadata::LevelFilter};
use tracing::{info_span, Instrument};
@@ -121,9 +122,16 @@
fn setup_logging() {
let indicatif_layer = IndicatifLayer::new().with_progress_style(
ProgressStyle::with_template(
- "{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{pos:>7}/{len:7}{elapsed}{color_end}",
+ "{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{download_progress} {elapsed}{color_end}",
)
.unwrap()
+ .with_key("download_progress", |state: &ProgressState, writer: &mut dyn std::fmt::Write| {
+ let Some(len) = state.len() else {
+ return;
+ };
+ let pos = state.pos();
+ let _ = write!(writer, "{} / {}", pos.human_count_bare(), len.human_count_bare());
+ })
.with_key(
"color_start",
|state: &ProgressState, writer: &mut dyn std::fmt::Write| {
lib/default.nixdiffbeforeafterboth--- a/lib/default.nix
+++ b/lib/default.nix
@@ -1,44 +1,56 @@
-{ flake-utils }: {
- fleetConfiguration = { data, nixpkgs, hosts, ... }@allConfig:
- let
- hostNames = nixpkgs.lib.attrNames hosts;
- config = builtins.removeAttrs allConfig [ "nixpkgs" "data" ];
- fleetLib = import ./fleetLib.nix {
- inherit nixpkgs hostNames;
- };
- in
- nixpkgs.lib.genAttrs flake-utils.lib.defaultSystems (system:
- let
+{flake-utils}: {
+ fleetConfiguration = {
+ data,
+ nixpkgs,
+ hosts,
+ ...
+ } @ allConfig: let
+ hostNames = nixpkgs.lib.attrNames hosts;
+ config = builtins.removeAttrs allConfig ["nixpkgs" "data"];
+ fleetLib = import ./fleetLib.nix {
+ inherit nixpkgs hostNames;
+ };
+ in
+ # Top-level arg is the builder system (not the target system!)
+ nixpkgs.lib.genAttrs flake-utils.lib.defaultSystems (system: let
+ withData = data: rec {
root = nixpkgs.lib.evalModules {
- modules = (import ../modules/fleet/_modules.nix) ++ [ config data ];
+ modules = (import ../modules/fleet/_modules.nix) ++ [config data];
specialArgs = {
inherit nixpkgs fleetLib;
};
};
failedAssertions = map (x: x.message) (nixpkgs.lib.filter (x: !x.assertion) root.config.assertions);
rootAssertWarn =
- if failedAssertions != [ ]
+ if failedAssertions != []
then throw "Failed assertions:\n${nixpkgs.lib.concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
else nixpkgs.lib.showWarnings root.config.warnings root;
configuredHosts = rootAssertWarn.config.hosts;
configuredSecrets = rootAssertWarn.config.secrets;
- configuredSystems = configuredSystemsWithExtraModules [ ];
- configuredSystemsWithExtraModules = extraModules: nixpkgs.lib.listToAttrs (
- map
+ configuredSystems = configuredSystemsWithExtraModules [];
+ configuredSystemsWithExtraModules = extraModules:
+ nixpkgs.lib.listToAttrs (
+ map
(
name: {
inherit name;
value = nixpkgs.lib.nixosSystem {
system = configuredHosts.${name}.system;
- modules = configuredHosts.${name}.modules ++ extraModules ++ [
- ({ ... }: {
- nixpkgs.system = system;
- nixpkgs.localSystem.system = system;
- nixpkgs.crossSystem = if system == configuredHosts.${name}.system then null else {
- system = configuredHosts.${name}.system;
- };
- })
- ];
+ modules =
+ configuredHosts.${name}.modules
+ ++ extraModules
+ ++ [
+ ({...}: {
+ nixpkgs.system = system;
+ nixpkgs.localSystem.system = system;
+ nixpkgs.crossSystem =
+ if system == configuredHosts.${name}.system
+ then null
+ else {
+ system = configuredHosts.${name}.system;
+ };
+ })
+ ];
specialArgs = {
inherit fleetLib;
fleet = fleetLib.hostsToAttrs (host: configuredSystems.${host}.config);
@@ -47,11 +59,7 @@
}
)
(builtins.attrNames rootAssertWarn.config.hosts)
- );
- in
- rec {
- inherit configuredHosts configuredSecrets configuredSystems;
- configUnchecked = root.config;
+ );
buildSystems = {
toplevel = builtins.mapAttrs (_name: value: value.config.system.build.toplevel) (configuredSystemsWithExtraModules [
({...}: {
@@ -66,12 +74,22 @@
]);
installationCd = builtins.mapAttrs (_name: value: value.config.system.build.isoImage) (configuredSystemsWithExtraModules [
(nixpkgs + "/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix")
- ({ lib, ... }: {
+ ({lib, ...}: {
buildTarget = "installation-cd";
# Needed for https://github.com/NixOS/nixpkgs/issues/58959
- boot.supportedFilesystems = lib.mkForce [ "btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs" ];
+ boot.supportedFilesystems = lib.mkForce ["btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs"];
})
]);
};
- });
+ configUnchecked = root.config;
+ };
+ defaultData = withData data;
+ in rec {
+ inherit (defaultData) configuredHosts configuredSecrets configuredSystems buildSystems configUnchecked;
+ injectData = data: let
+ injectedData = withData data;
+ in {
+ inherit (injectedData) configuredHosts configuredSecrets configuredSystems buildSystems configUnchecked;
+ };
+ });
}