ALPHA: wire is a tool to deploy nixos systems wire.althaea.zone/

allow the agent to be ran locally (#175)

Signed-off-by: marshmallow <github@althaea.zone>

authored by

marshmallow and committed by
GitHub
bcacfd6b a8d83e6e

+228 -151
+1
CHANGELOG.md
··· 26 26 ### Fixed 27 27 28 28 - A non-existant key owner user/group would not default to gid/uid `0`. 29 + - Keys can now be deployed to localhost. 29 30 30 31 ## [0.2.0] - 2025-04-21 31 32
+1
Cargo.lock
··· 587 587 "gethostname", 588 588 "im", 589 589 "key_agent", 590 + "nix", 590 591 "prost", 591 592 "regex", 592 593 "serde",
+1
Cargo.toml
··· 20 20 im = { version = "15.1.0", features = ["serde"] } 21 21 anyhow = "1.0.98" 22 22 prost = "0.14.1" 23 + nix = { version = "0.30.1", features = ["user"] }
+1
tests/nix/default.nix
··· 25 25 imports = [ 26 26 ./suite/test_remote_deploy 27 27 ./suite/test_local_deploy 28 + ./suite/test_keys 28 29 ]; 29 30 options.wire.testing = mkOption { 30 31 type = attrsOf (
+69
tests/nix/suite/test_keys/default.nix
··· 1 + { 2 + wire.testing.test_keys = { 3 + nodes.deployer = { 4 + _wire.deployer = true; 5 + _wire.receiver = true; 6 + }; 7 + nodes.receiver = { 8 + _wire.receiver = true; 9 + }; 10 + testScript = '' 11 + deployer_so = collect_store_objects(deployer) 12 + receiver_so = collect_store_objects(receiver) 13 + 14 + # build all nodes without any keys 15 + deployer.succeed(f"wire apply --no-progress --on receiver --path {TEST_DIR}/hive.nix --no-keys -vvv >&2") 16 + 17 + receiver.wait_for_unit("sshd.service") 18 + 19 + # --no-keys should never push a key 20 + receiver.fail("test -f /run/keys/source_string") 21 + deployer.fail("test -f /run/keys/source_string") 22 + 23 + def test_keys(target, target_object): 24 + deployer.succeed(f"wire apply keys --on {target} --no-progress --path {TEST_DIR}/hive.nix -vvv >&2") 25 + 26 + keys = [ 27 + ("/run/keys/source_string", "hello_world_source", "root root 600"), 28 + ("/etc/keys/file", "hello_world_file", "root root 644"), 29 + ("/home/owner/some/deep/path/command", "hello_world_command", "owner owner 644"), 30 + ] 31 + 32 + for path, value, permissions in keys: 33 + # test existence & value 34 + source_string = target_object.succeed(f"cat {path}") 35 + assert value in source_string, f"{path} has correct contents ({target})" 36 + 37 + stat = target_object.succeed(f"stat -c '%U %G %a' {path}").rstrip() 38 + assert permissions == stat, f"{path} has correct permissions ({target})" 39 + 40 + def perform_routine(target, target_object): 41 + test_keys(target, target_object) 42 + 43 + # Mess with the keys to make sure that every push refreshes the permissions 44 + target_object.succeed("echo 'incorrect_value' > /run/keys/source_string") 45 + target_object.succeed("chown 600 /etc/keys/file") 46 + # Test having a key that doesn't exist mixed with keys that do 47 + target_object.succeed("rm /home/owner/some/deep/path/command") 48 + 49 + # Test keys twice to ensure the operation is idempotent, 50 + # especially around directory creation. 51 + test_keys(target, target_object) 52 + 53 + perform_routine("receiver", receiver) 54 + perform_routine("deployer", deployer) 55 + 56 + new_deployer_store_objects = collect_store_objects(deployer).difference(deployer_so) 57 + new_receiver_store_objects = collect_store_objects(receiver).difference(receiver_so) 58 + 59 + # no one should have any keys introduced by the operation 60 + for node, objects in [ 61 + (deployer, new_deployer_store_objects), 62 + (receiver, new_receiver_store_objects), 63 + ]: 64 + assert_store_not_posioned(node, "hello_world_source", objects) 65 + assert_store_not_posioned(node, "hello_world_file", objects) 66 + assert_store_not_posioned(node, "hello_world_command", objects) 67 + ''; 68 + }; 69 + }
+51
tests/nix/suite/test_keys/hive.nix
··· 1 + let 2 + mkHiveNode = import ../utils.nix { testName = "test_keys-@IDENT@"; }; 3 + in 4 + { 5 + meta.nixpkgs = import <nixpkgs> { system = "x86_64-linux"; }; 6 + defaults = { 7 + deployment.keys = { 8 + source_string = { 9 + source = '' 10 + hello_world_source 11 + ''; 12 + }; 13 + file = { 14 + source = ./file.txt; 15 + destDir = "/etc/keys/"; 16 + permissions = "644"; 17 + # Test defaulting to root when user or group does not exist 18 + user = "USERDOESNOTEXIST"; 19 + group = "USERDOESNOTEXIST"; 20 + }; 21 + command = { 22 + source = [ 23 + "echo" 24 + "hello_world_command" 25 + ]; 26 + permissions = "644"; 27 + user = "owner"; 28 + group = "owner"; 29 + destDir = "/home/owner/some/deep/path"; 30 + }; 31 + }; 32 + 33 + users.groups."owner" = { }; 34 + users.users."owner" = { 35 + group = "owner"; 36 + isNormalUser = true; 37 + }; 38 + }; 39 + 40 + receiver = mkHiveNode { hostname = "receiver"; } ( 41 + { pkgs, ... }: 42 + { 43 + environment.etc."a".text = "b"; 44 + environment.systemPackages = [ pkgs.ripgrep ]; 45 + } 46 + ); 47 + 48 + deployer = mkHiveNode { hostname = "deployer"; } { 49 + environment.etc."a".text = "b"; 50 + }; 51 + }
-44
tests/nix/suite/test_remote_deploy/default.nix
··· 15 15 receiver.wait_for_unit("sshd.service") 16 16 17 17 receiver.succeed("test -f /etc/a") 18 - 19 - # --no-keys should never push a key 20 - receiver.fail("test -f /run/keys/source_string") 21 - 22 - def test_keys(): 23 - deployer.succeed(f"wire apply keys --on receiver --no-progress --path {TEST_DIR}/hive.nix -vvv >&2") 24 - 25 - keys = [ 26 - ("/run/keys/source_string", "hello_world_source", "root root 600"), 27 - ("/etc/keys/file", "hello_world_file", "root root 644"), 28 - ("/home/owner/some/deep/path/command", "hello_world_command", "owner owner 644"), 29 - ] 30 - 31 - for path, value, permissions in keys: 32 - # test existence & value 33 - source_string = receiver.succeed(f"cat {path}") 34 - assert value in source_string, f"{path} has correct contents" 35 - 36 - stat = receiver.succeed(f"stat -c '%U %G %a' {path}").rstrip() 37 - assert permissions == stat, f"{path} has correct permissions" 38 - 39 - test_keys() 40 - 41 - # Mess with the keys to make sure that every push refreshes the permissions 42 - receiver.succeed("echo 'incorrect_value' > /run/keys/source_string") 43 - receiver.succeed("chown 600 /etc/keys/file") 44 - # Test having a key that doesn't exist mixed with keys that do 45 - receiver.succeed("rm /home/owner/some/deep/path/command") 46 - 47 - # Test keys twice to ensure the operation is idempotent, 48 - # especially around directory creation. 49 - test_keys() 50 - 51 - new_deployer_store_objects = collect_store_objects(deployer).difference(deployer_so) 52 - new_receiver_store_objects = collect_store_objects(receiver).difference(receiver_so) 53 - 54 - # no one should have any keys introduced by the operation 55 - for node, objects in [ 56 - (deployer, new_deployer_store_objects), 57 - (receiver, new_receiver_store_objects), 58 - ]: 59 - assert_store_not_posioned(node, "hello_world_source", objects) 60 - assert_store_not_posioned(node, "hello_world_file", objects) 61 - assert_store_not_posioned(node, "hello_world_command", objects) 62 18 ''; 63 19 }; 64 20 }
tests/nix/suite/test_remote_deploy/file.txt tests/nix/suite/test_keys/file.txt
-29
tests/nix/suite/test_remote_deploy/hive.nix
··· 5 5 meta.nixpkgs = import <nixpkgs> { system = "x86_64-linux"; }; 6 6 receiver = mkHiveNode { hostname = "receiver"; } { 7 7 environment.etc."a".text = "b"; 8 - 9 - users.groups."owner" = { }; 10 - users.users."owner" = { 11 - group = "owner"; 12 - isNormalUser = true; 13 - }; 14 - 15 - deployment.keys = { 16 - source_string = { 17 - source = '' 18 - hello_world_source 19 - ''; 20 - }; 21 - file = { 22 - source = ./file.txt; 23 - destDir = "/etc/keys/"; 24 - permissions = "644"; 25 - }; 26 - command = { 27 - source = [ 28 - "echo" 29 - "hello_world_command" 30 - ]; 31 - permissions = "644"; 32 - user = "owner"; 33 - group = "owner"; 34 - destDir = "/home/owner/some/deep/path"; 35 - }; 36 - }; 37 8 }; 38 9 }
+2
tests/nix/suite/utils.nix
··· 39 39 40 40 services.openssh.enable = true; 41 41 users.users.root.openssh.authorizedKeys.keys = [ snakeOil.snakeOilEd25519PublicKey ]; 42 + 43 + environment.systemPackages = [ pkgs.ripgrep ]; 42 44 } 43 45 ) 44 46 ];
+13 -1
tests/nix/test-opts.nix
··· 3 3 snakeOil, 4 4 wire-small, 5 5 config, 6 + pkgs, 6 7 ... 7 8 }: 8 9 let ··· 31 32 systemd.tmpfiles.rules = [ 32 33 "C+ /root/.ssh/id_ed25519 600 - - - ${snakeOil.snakeOilEd25519PrivateKey}" 33 34 ]; 34 - environment.systemPackages = [ wire-small ]; 35 + environment.systemPackages = [ 36 + wire-small 37 + pkgs.ripgrep 38 + ]; 35 39 # It's important to note that you should never ever use this configuration 36 40 # for production. You are risking a MITM attack with this! 37 41 programs.ssh.extraConfig = '' ··· 40 44 UserKnownHostsFile /dev/null 41 45 ''; 42 46 47 + # owner user used to test keys on the deployer. 48 + # here instead of in the test case hive because we lose the wire binary when 49 + # applying to deployer. 50 + users.groups."owner" = { }; 51 + users.users."owner" = { 52 + group = "owner"; 53 + isNormalUser = true; 54 + }; 43 55 }) 44 56 (mkIf cfg.receiver { 45 57 services.openssh.enable = true;
+1
tests/nix/tools.py
··· 5 5 def assert_store_not_posioned(machine: Machine, poison: str, objects: set[str]): 6 6 paths = list(map(lambda n: f"/nix/store/{n}", objects)) 7 7 8 + machine.succeed("which rg") 8 9 machine.fail(f"rg '{poison}' {" ".join(paths)}")
+1 -1
wire/key_agent/Cargo.toml
··· 7 7 tokio = { workspace = true } 8 8 anyhow = { workspace = true } 9 9 prost = { workspace = true } 10 - nix = { version = "0.30.1", features = ["user"] } 10 + nix = { workspace = true } 11 11 12 12 [build-dependencies] 13 13 prost-build = "0.14"
+1
wire/lib/Cargo.toml
··· 24 24 prost = { workspace = true } 25 25 gethostname = "1.0.2" 26 26 async-trait = "0.1.88" 27 + nix = { workspace = true } 27 28 28 29 [dev-dependencies] 29 30 tempdir = "0.3"
+63 -57
wire/lib/src/hive/key.rs wire/lib/src/hive/steps/keys.rs
··· 4 4 use serde::{Deserialize, Serialize}; 5 5 use std::env; 6 6 use std::fmt::Display; 7 + use std::io::Cursor; 7 8 use std::pin::Pin; 8 9 use std::process::{ExitStatus, Stdio}; 9 10 use std::str::from_utf8; 10 - use std::{io::Cursor, path::PathBuf}; 11 + use std::{num::ParseIntError, path::PathBuf}; 11 12 use thiserror::Error; 12 - use tokio::io::{AsyncReadExt, AsyncWriteExt}; 13 + use tokio::io::{AsyncReadExt as _, AsyncWriteExt}; 13 14 use tokio::process::Command; 14 15 use tokio::{fs::File, io::AsyncRead}; 15 - use tracing::{Span, debug, info, trace, warn}; 16 + use tracing::{debug, info, trace, warn}; 16 17 17 - use crate::hive::node::{Push, should_apply_locally}; 18 + use crate::hive::node::{ 19 + Context, ExecuteStep, Goal, Push, SwitchToConfigurationGoal, push, should_apply_locally, 20 + }; 21 + use crate::hive::steps::activate::get_elevation; 18 22 use crate::{HiveLibError, create_ssh_command}; 19 23 20 - use super::node::{Context, ExecuteStep, Goal, push}; 21 - 22 24 #[derive(Debug, Error)] 23 - pub enum Error { 25 + pub enum KeyError { 24 26 #[error("error reading file")] 25 27 File(#[source] std::io::Error), 26 28 ··· 32 34 33 35 #[error("Command list empty")] 34 36 Empty, 37 + 38 + #[error("Failed to parse key permissions")] 39 + ParseKeyPermissions(#[source] ParseIntError), 35 40 } 36 41 37 42 #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] ··· 49 54 #[serde(rename = "post-activation")] 50 55 PostActivation, 51 56 #[serde(skip)] 52 - AnyOpportunity, 57 + NoFilter, 53 58 } 54 59 55 60 #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] ··· 66 71 pub upload_at: UploadKeyAt, 67 72 } 68 73 69 - pub trait PushKeys { 70 - fn push_keys( 71 - self, 72 - target: UploadKeyAt, 73 - span: &Span, 74 - ) -> impl std::future::Future<Output = Result<(), HiveLibError>> + Send; 74 + fn should_execute(filter: &UploadKeyAt, ctx: &crate::hive::node::Context) -> bool { 75 + if ctx.no_keys { 76 + return false; 77 + } 78 + 79 + // should execute if no filter, and the goal is keys. 80 + // otherwise, only execute if the goal is switch 81 + matches!( 82 + (filter, &ctx.goal), 83 + (UploadKeyAt::NoFilter, Goal::Keys) 84 + | ( 85 + _, 86 + Goal::SwitchToConfiguration(SwitchToConfigurationGoal::Switch) 87 + ) 88 + ) 75 89 } 76 90 77 - async fn create_reader(source: &'_ Source) -> Result<Pin<Box<dyn AsyncRead + Send + '_>>, Error> { 91 + fn get_u32_permission(key: &Key) -> Result<u32, KeyError> { 92 + u32::from_str_radix(&key.permissions, 8).map_err(KeyError::ParseKeyPermissions) 93 + } 94 + 95 + async fn create_reader( 96 + source: &'_ Source, 97 + ) -> Result<Pin<Box<dyn AsyncRead + Send + '_>>, KeyError> { 78 98 match source { 79 - Source::Path(path) => Ok(Box::pin(File::open(path).await.map_err(Error::File)?)), 99 + Source::Path(path) => Ok(Box::pin(File::open(path).await.map_err(KeyError::File)?)), 80 100 Source::String(string) => Ok(Box::pin(Cursor::new(string))), 81 101 Source::Command(args) => { 82 - let output = Command::new(args.first().ok_or(Error::Empty)?) 102 + let output = Command::new(args.first().ok_or(KeyError::Empty)?) 83 103 .args(&args[1..]) 84 104 .stdin(Stdio::null()) 85 105 .stdout(Stdio::piped()) 86 106 .stderr(Stdio::piped()) 87 107 .spawn() 88 - .map_err(Error::CommandSpawnError)? 108 + .map_err(KeyError::CommandSpawnError)? 89 109 .wait_with_output() 90 110 .await 91 - .map_err(Error::CommandSpawnError)?; 111 + .map_err(KeyError::CommandSpawnError)?; 92 112 93 113 if output.status.success() { 94 114 return Ok(Box::pin(Cursor::new(output.stdout))); 95 115 } 96 116 97 - Err(Error::CommandError( 117 + Err(KeyError::CommandError( 98 118 output.status, 99 119 from_utf8(&output.stderr).unwrap().to_string(), 100 120 )) ··· 128 148 Ok(()) 129 149 } 130 150 131 - async fn process_key(key: &Key) -> Result<(key_agent::keys::Key, Vec<u8>), Error> { 151 + async fn process_key(key: &Key) -> Result<(key_agent::keys::Key, Vec<u8>), KeyError> { 132 152 let mut reader = create_reader(&key.source).await?; 133 153 134 154 let mut buf = Vec::new(); ··· 153 173 .expect("Failed to conver usize buf length to i32"), 154 174 user: key.user.clone(), 155 175 group: key.group.clone(), 156 - permissions: u32::from_str_radix(&key.permissions, 8) 157 - .expect("Failed to convert octal string to u32"), 176 + permissions: get_u32_permission(key)?, 158 177 destination: destination.into_os_string().into_string().unwrap(), 159 178 }, 160 179 buf, 161 180 )) 162 181 } 163 182 164 - pub struct UploadKeyStep { 165 - pub moment: UploadKeyAt, 183 + pub struct KeysStep { 184 + pub filter: UploadKeyAt, 166 185 } 167 186 pub struct PushKeyAgentStep; 168 187 169 - impl Display for UploadKeyStep { 188 + impl Display for KeysStep { 170 189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 171 - write!(f, "Upload key @ {:?}", self.moment) 190 + write!(f, "Upload key @ {:?}", self.filter) 172 191 } 173 192 } 174 193 ··· 178 197 } 179 198 } 180 199 181 - fn should_execute(moment: &UploadKeyAt, ctx: &Context) -> bool { 182 - if ctx.no_keys { 183 - return false; 184 - } 185 - if should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) { 186 - warn!( 187 - "SKIP STEP FOR {}: Pushing keys locally is unimplemented", 188 - ctx.name.to_string() 189 - ); 190 - return false; 191 - } 192 - 193 - if *moment == UploadKeyAt::AnyOpportunity && matches!(ctx.goal, Goal::Keys) { 194 - return true; 195 - } 196 - 197 - matches!( 198 - ctx.goal, 199 - Goal::SwitchToConfiguration(super::node::SwitchToConfigurationGoal::Switch) 200 - ) 201 - } 202 - 203 200 #[async_trait] 204 - impl ExecuteStep for UploadKeyStep { 201 + impl ExecuteStep for KeysStep { 205 202 fn should_execute(&self, ctx: &Context) -> bool { 206 - should_execute(&self.moment, ctx) 203 + should_execute(&self.filter, ctx) 207 204 } 208 205 209 206 async fn execute(&self, ctx: &mut Context<'_>) -> Result<(), HiveLibError> { ··· 214 211 .keys 215 212 .iter() 216 213 .filter(|key| { 217 - self.moment == UploadKeyAt::AnyOpportunity 218 - || (self.moment != UploadKeyAt::AnyOpportunity && key.upload_at != self.moment) 214 + self.filter == UploadKeyAt::NoFilter 215 + || (self.filter != UploadKeyAt::NoFilter && key.upload_at != self.filter) 219 216 }) 220 217 .map(|key| async move { process_key(key).await }); 221 218 222 219 let (keys, bufs): (Vec<key_agent::keys::Key>, Vec<Vec<u8>>) = join_all(futures) 223 220 .await 224 221 .into_iter() 225 - .collect::<Result<Vec<_>, Error>>() 222 + .collect::<Result<Vec<_>, KeyError>>() 226 223 .map_err(HiveLibError::KeyError)? 227 224 .into_iter() 228 225 .unzip(); ··· 233 230 234 231 let buf = msg.encode_to_vec(); 235 232 236 - let mut command = create_ssh_command(&ctx.node.target, true); 233 + let mut command = 234 + if should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) { 235 + warn!("Placing keys locally for node {0}", ctx.name); 236 + get_elevation("wire key agent")?; 237 + Command::new("sudo") 238 + } else { 239 + create_ssh_command(&ctx.node.target, true) 240 + }; 237 241 238 242 let mut child = command 239 243 .args([ ··· 280 284 #[async_trait] 281 285 impl ExecuteStep for PushKeyAgentStep { 282 286 fn should_execute(&self, ctx: &Context) -> bool { 283 - should_execute(&UploadKeyAt::AnyOpportunity, ctx) 287 + should_execute(&UploadKeyAt::NoFilter, ctx) 284 288 } 285 289 286 290 async fn execute(&self, ctx: &mut Context<'_>) -> Result<(), HiveLibError> { ··· 298 302 ), 299 303 }; 300 304 301 - push(ctx.node, ctx.name, Push::Path(&agent_directory)).await?; 305 + if !should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) { 306 + push(ctx.node, ctx.name, Push::Path(&agent_directory)).await?; 307 + } 302 308 303 309 ctx.state.key_agent_directory = Some(agent_directory); 304 310
+9 -6
wire/lib/src/hive/mod.rs
··· 9 9 10 10 use crate::nix::{EvalGoal, get_eval_command}; 11 11 use crate::{HiveLibError, SubCommandModifiers}; 12 - pub mod key; 13 12 pub mod node; 14 - mod steps; 13 + pub mod steps; 15 14 16 15 #[derive(Serialize, Deserialize, Debug, PartialEq)] 17 16 pub struct Hive { ··· 101 100 mod tests { 102 101 use im::vector; 103 102 104 - use crate::{get_test_path, test_support::make_flake_sandbox}; 103 + use crate::{ 104 + get_test_path, 105 + hive::steps::keys::{Key, Source, UploadKeyAt}, 106 + test_support::make_flake_sandbox, 107 + }; 105 108 106 109 use super::*; 107 110 use std::env; ··· 156 159 user: "root".into(), 157 160 port: 22, 158 161 }, 159 - keys: vector![key::Key { 162 + keys: vector![Key { 160 163 name: "different-than-a".into(), 161 164 dest_dir: "/run/keys/".into(), 162 165 path: "/run/keys/different-than-a".into(), 163 166 group: "root".into(), 164 167 user: "root".into(), 165 168 permissions: "0600".into(), 166 - source: key::Source::String("hi".into()), 167 - upload_at: key::UploadKeyAt::PreActivation, 169 + source: Source::String("hi".into()), 170 + upload_at: UploadKeyAt::PreActivation, 168 171 }], 169 172 ..Default::default() 170 173 };
+7 -7
wire/lib/src/hive/node.rs
··· 10 10 use tracing_indicatif::span_ext::IndicatifSpanExt; 11 11 12 12 use crate::SubCommandModifiers; 13 + use crate::hive::steps::keys::{Key, KeysStep, PushKeyAgentStep, UploadKeyAt}; 13 14 use crate::nix::StreamTracing; 14 15 15 16 use super::HiveLibError; 16 - use super::key::{Key, PushKeyAgentStep, UploadKeyAt, UploadKeyStep}; 17 17 use super::steps::activate::SwitchToConfigurationStep; 18 18 19 19 #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, derive_more::Display)] ··· 166 166 Self { 167 167 steps: vec![ 168 168 Box::new(PushKeyAgentStep), 169 - Box::new(UploadKeyStep { 170 - moment: UploadKeyAt::AnyOpportunity, 169 + Box::new(KeysStep { 170 + filter: UploadKeyAt::NoFilter, 171 171 }), 172 - Box::new(UploadKeyStep { 173 - moment: UploadKeyAt::PreActivation, 172 + Box::new(KeysStep { 173 + filter: UploadKeyAt::PreActivation, 174 174 }), 175 175 Box::new(super::steps::evaluate::Step), 176 176 Box::new(super::steps::push::EvaluatedOutputStep), 177 177 Box::new(super::steps::build::Step), 178 178 Box::new(super::steps::push::BuildOutputStep), 179 179 Box::new(SwitchToConfigurationStep), 180 - Box::new(UploadKeyStep { 181 - moment: UploadKeyAt::PostActivation, 180 + Box::new(KeysStep { 181 + filter: UploadKeyAt::PostActivation, 182 182 }), 183 183 ], 184 184 context,
+4 -4
wire/lib/src/hive/steps/activate.rs
··· 19 19 } 20 20 } 21 21 22 - fn get_elevation() -> Result<Output, HiveLibError> { 23 - info!("Attempting to elevate for local deployment."); 22 + pub(crate) fn get_elevation(reason: &str) -> Result<Output, HiveLibError> { 23 + info!("Attempting to elevate for {reason}."); 24 24 suspend_tracing_indicatif(|| { 25 25 let mut command = std::process::Command::new("sudo"); 26 26 command.arg("-v").output() ··· 49 49 if should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) { 50 50 // Refresh sudo timeout 51 51 warn!("Running nix-env ON THIS MACHINE for node {0}", ctx.name); 52 - get_elevation()?; 52 + get_elevation("nix-env")?; 53 53 let mut command = Command::new("sudo"); 54 54 command.arg("nix-env"); 55 55 command ··· 87 87 "Running switch-to-configuration {goal:?} ON THIS MACHINE for node {0}", 88 88 ctx.name 89 89 ); 90 - get_elevation()?; 90 + get_elevation("switch-to-configuration")?; 91 91 let mut command = Command::new("sudo"); 92 92 command.arg(cmd); 93 93 command
+1
wire/lib/src/hive/steps/mod.rs
··· 1 1 pub mod activate; 2 2 pub mod build; 3 3 pub mod evaluate; 4 + pub mod keys; 4 5 pub mod push;
+2 -2
wire/lib/src/lib.rs
··· 6 6 clippy::missing_panics_doc 7 7 )] 8 8 use hive::{ 9 - key::Error, 10 9 node::{Name, SwitchToConfigurationGoal, Target}, 10 + steps::keys::KeyError, 11 11 }; 12 12 use nix_log::{NixLog, Trace}; 13 13 use std::path::PathBuf; ··· 86 86 KeyCommandError(Name, Vec<String>), 87 87 88 88 #[error("failed to push a key")] 89 - KeyError(#[source] Error), 89 + KeyError(#[source] KeyError), 90 90 91 91 #[error("node {0} not exist in hive")] 92 92 NodeDoesNotExist(String),