diff --git a/Cargo.lock b/Cargo.lock index bafefe56..4017992e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "agent" +version = "1.0.0-beta.0" +dependencies = [ + "anyhow", + "base64", + "futures-util", + "nix 0.30.1", + "prost", + "prost-build", + "sha2", + "tokio", + "tokio-util", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -296,7 +311,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -326,15 +341,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -346,9 +352,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -408,7 +414,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -432,7 +438,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -443,7 +449,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -465,24 +471,22 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "convert_case", "proc-macro2", "quote", - "rustc_version", - "syn 2.0.111", + "syn 2.0.110", "unicode-xid", ] @@ -522,7 +526,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -590,7 +594,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -771,7 +775,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -1119,21 +1123,6 @@ dependencies = [ "libc", ] -[[package]] -name = "key_agent" -version = "1.0.0-beta.0" -dependencies = [ - "anyhow", - "base64", - "futures-util", - "nix 0.30.1", - "prost", - "prost-build", - "sha2", - "tokio", - "tokio-util", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1147,6 +1136,7 @@ dependencies = [ name = "lib" version = "1.0.0-beta.0" dependencies = [ + "agent", "aho-corasick", "anyhow", "base64", @@ -1157,7 +1147,6 @@ dependencies = [ "gjson", "im", "itertools", - "key_agent", "miette", "nix 0.30.1", "nix-compat", @@ -1172,7 +1161,7 @@ dependencies = [ "sha2", "sqlx", "strip-ansi-escapes", - "syn 2.0.111", + "syn 2.0.110", "tempdir", "termion", "thiserror 2.0.17", @@ -1297,7 +1286,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -1399,7 +1388,7 @@ source = "git+https://git.snix.dev/snix/snix.git#4aaef4cdf6f7766eedcfe1b5bad8f1c dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -1485,7 +1474,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -1664,7 +1653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -1711,7 +1700,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.111", + "syn 2.0.110", "tempfile", ] @@ -1725,7 +1714,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -2009,7 +1998,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -2056,7 +2045,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -2253,7 +2242,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -2276,7 +2265,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.111", + "syn 2.0.110", "tokio", "url", ] @@ -2466,9 +2455,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -2483,7 +2472,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -2565,7 +2554,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -2576,7 +2565,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -2644,7 +2633,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -2703,9 +2692,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -2715,20 +2704,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.31" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -2747,9 +2736,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -2798,12 +2787,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-width" version = "0.1.14" @@ -3249,7 +3232,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", "synstructure", ] @@ -3270,7 +3253,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] @@ -3290,7 +3273,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", "synstructure", ] @@ -3330,7 +3313,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.110", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6745e8a9..c3731195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["wire/key_agent", "wire/lib", "wire/cli"] +members = ["wire/agent", "wire/lib", "wire/cli"] resolver = "2" package.edition = "2024" package.version = "1.0.0-beta.0" diff --git a/README.md b/README.md index aa0cc140..9da40ec3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ wire │ │ └── Rust library containing business logic, consumed by `wire` │ ├── cli │ │ └── Rust binary, using `lib` -│ └── key_agent +│ └── agent │ └── Rust binary ran on a target node. receives key file bytes and metadata w/ protobuf over SSH stdin ├── doc │ └── a [vitepress](https://vitepress.dev/) site diff --git a/doc/guides/keys.md b/doc/guides/keys.md index 8d8c067b..60d02b82 100644 --- a/doc/guides/keys.md +++ b/doc/guides/keys.md @@ -38,7 +38,7 @@ user must be trusted or you must add garnix as a trusted public key: Otherwise, you may see errors such as: ``` -error: cannot add path '/nix/store/...-wire-tool-key_agent-x86_64-linux-...' because it lacks a signature by a trusted key +error: cannot add path '/nix/store/...-wire-tool-agent-x86_64-linux-...' because it lacks a signature by a trusted key ``` This is a requirement because `nix copy` is used to copy the binary. diff --git a/doc/guides/non-root-user.md b/doc/guides/non-root-user.md index 84fa1d9c..0c9d29cc 100644 --- a/doc/guides/non-root-user.md +++ b/doc/guides/non-root-user.md @@ -56,7 +56,7 @@ $ wire apply keys --on media INFO eval_hive: evaluating hive Flake("/path/to/hive") ... INFO media | step="Upload key @ NoFilter" progress="3/4" -deploy-user@node:22 | Authenticate for "sudo /nix/store/.../bin/key_agent": +deploy-user@node:22 | Authenticate for "sudo /nix/store/.../bin/agent": [sudo] password for deploy-user: ``` diff --git a/flake.nix b/flake.nix index 86963146..74ed9f22 100644 --- a/flake.nix +++ b/flake.nix @@ -38,7 +38,7 @@ ./nix/shells.nix ./nix/tests.nix ./wire/cli - ./wire/key_agent + ./wire/agent ./doc ./tests/nix ./runtime diff --git a/justfile b/justfile index 5b6f96d4..03740781 100644 --- a/justfile +++ b/justfile @@ -4,4 +4,4 @@ build-dhat: cargo build --profile profiling --features dhat-heap @echo 'dhat binaries in target/profiling' @echo 'Example:' - @echo 'WIRE_KEY_AGENT=/nix/store/...-key_agent-0.1.0 PROJECT/target/profiling/wire apply ...' + @echo 'WIRE_AGENT=/nix/store/...-agent-0.1.0 PROJECT/target/profiling/wire apply ...' diff --git a/nix/tests.nix b/nix/tests.nix index 86c6d30e..21f51029 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -20,7 +20,7 @@ installPhaseCommand = '' mkdir -p $out - cp $(ls target/debug/deps/{wire,lib,key_agent}-* | grep -v "\.d") $out + cp $(ls target/debug/deps/{wire,lib,agent}-* | grep -v "\.d") $out ''; } // commonArgs diff --git a/runtime/module/config.nix b/runtime/module/config.nix index fa9d11c0..75df994d 100644 --- a/runtime/module/config.nix +++ b/runtime/module/config.nix @@ -22,7 +22,74 @@ } ) config.deployment.keys; - services = lib.mapAttrs' ( + system.activationScripts.setup-wire-rollback.text = '' + mkdir -p /var/lib/wire-rollback + chmod 700 /var/lib/wire-rollback + ''; + + services = { + wire-rollback = { + enable = config.deployment.rollback; + description = "Rolls back the NixOS profile if `/var/lib/wire-rollback/heartbeat` is not created in 30 + seconds after this service starts."; + documentation = [ + "https://wire.althaea.zone/guides/rollback" + ]; + path = [ + pkgs.coreutils + ]; + wantedBy = [ "multi-user.target" ]; + script = '' + set -euo pipefail + + goal=$(<"/var/lib/wire-rollback/goal") + + case $goal in + "check" | "switch" | "boot" | "test" | "dry-activate") + echo "<5>using goal $goal" + ;; + *) + echo "<3>'$goal' is not a valid goal." + exit 1 + ;; + esac + + sleep 30 + + if [ -f "/var/lib/wire-rollback/heartbeat" ]; then + exit 0 + fi + + echo "<1>/var/lib/wire-rollback/heartbeat does not exist, rolling back system" + + # set current system + nix-env --rollback --profile /nix/var/nix/profiles/system + # get the path to the system we are now rolling back to + system=$(readlink -f /nix/var/nix/profiles/system) + + echo "<5>rolling back to $system" + + # switch to the system using goal + "$system/bin/switch-to-configuration $goal" + ''; + unitConfig = { + ConditionPathExists = [ + "/var/lib/wire-rollback/goal" + "!/var/lib/wire-rollback/heartbeat" + ]; + }; + serviceConfig = { + Type = "oneshot"; + Restart = "no"; + StateDirectory = "wire-rollback"; + NotifyAccess = "all"; + RemainAfterExit = "yes"; + + ExecStopPost = "${pkgs.coreutils}/bin/rm -f /var/lib/wire-rollback/goal"; + }; + }; + } + // (lib.mapAttrs' ( _name: value: lib.nameValuePair "${value.name}-key" { description = "Service that requires ${value.path}"; @@ -55,7 +122,7 @@ RemainAfterExit = "yes"; }; } - ) config.deployment.keys; + ) config.deployment.keys); }; deployment = { diff --git a/runtime/module/options.nix b/runtime/module/options.nix index ff4fca6b..6eee73ce 100644 --- a/runtime/module/options.nix +++ b/runtime/module/options.nix @@ -50,6 +50,12 @@ in default = { }; }; + rollback = lib.mkOption { + type = types.bool; + default = true; + description = "Attempt to rollback this node if it cannot be contacted after activation."; + }; + buildOnTarget = lib.mkOption { type = types.bool; default = false; diff --git a/tests/nix/default.nix b/tests/nix/default.nix index b10cc6b5..4eb17c95 100644 --- a/tests/nix/default.nix +++ b/tests/nix/default.nix @@ -30,6 +30,7 @@ in ./suite/test_local_deploy ./suite/test_keys ./suite/test_stdin + ./suite/test_rollback ]; options.wire.testing = mkOption { type = attrsOf ( diff --git a/tests/nix/suite/test_rollback/default.nix b/tests/nix/suite/test_rollback/default.nix new file mode 100644 index 00000000..85d5918f --- /dev/null +++ b/tests/nix/suite/test_rollback/default.nix @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright 2024-2025 wire Contributors + +{ + wire.testing.test_remote_deploy = { + nodes.deployer = { + _wire.deployer = true; + }; + nodes.receiver = { + _wire.receiver = true; + }; + testScript = '' + with subtest("Deploy broken config"): + deployer.fail(f"wire apply --on receiver --no-progress --path {TEST_DIR}/hive.nix --no-keys -vvv >&2") + + with subtest("Configuration must revert"): + receiver.wait_for_unit("sshd.service") + ''; + }; +} diff --git a/tests/nix/suite/test_rollback/hive.nix b/tests/nix/suite/test_rollback/hive.nix new file mode 100644 index 00000000..fef4bb09 --- /dev/null +++ b/tests/nix/suite/test_rollback/hive.nix @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright 2024-2025 wire Contributors + +let + inherit (import ../utils.nix { testName = "test_rollback-@IDENT@"; }) makeHive mkHiveNode; +in +makeHive { + meta.nixpkgs = import { localSystem = "x86_64-linux"; }; + + receiver = mkHiveNode { hostname = "receiver"; } { + environment.etc."identity".text = "first"; + + services.openssh.enable = false; + }; +} diff --git a/wire/key_agent/Cargo.toml b/wire/agent/Cargo.toml similarity index 95% rename from wire/key_agent/Cargo.toml rename to wire/agent/Cargo.toml index a19c1022..7efb8766 100644 --- a/wire/key_agent/Cargo.toml +++ b/wire/agent/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "key_agent" +name = "agent" edition.workspace = true version.workspace = true diff --git a/wire/key_agent/build.rs b/wire/agent/build.rs similarity index 100% rename from wire/key_agent/build.rs rename to wire/agent/build.rs diff --git a/wire/key_agent/default.nix b/wire/agent/default.nix similarity index 57% rename from wire/key_agent/default.nix rename to wire/agent/default.nix index 527d7143..4bc90811 100644 --- a/wire/key_agent/default.nix +++ b/wire/agent/default.nix @@ -8,9 +8,9 @@ { packages = { agent = buildRustProgram { - name = "key_agent"; - pname = "wire-tool-key_agent-${system}"; - cargoExtraArgs = "-p key_agent"; + name = "agent"; + pname = "wire-tool-agent-${system}"; + cargoExtraArgs = "-p agent"; }; }; }; diff --git a/wire/key_agent/src/keys.proto b/wire/agent/src/keys.proto similarity index 92% rename from wire/key_agent/src/keys.proto rename to wire/agent/src/keys.proto index 2b424c4f..d995a1c9 100644 --- a/wire/key_agent/src/keys.proto +++ b/wire/agent/src/keys.proto @@ -3,7 +3,7 @@ syntax = "proto3"; -package key_agent.keys; +package agent.keys; message KeySpec { string destination = 1; diff --git a/wire/key_agent/src/lib.rs b/wire/agent/src/lib.rs similarity index 62% rename from wire/key_agent/src/lib.rs rename to wire/agent/src/lib.rs index 49424a60..64a9d1e9 100644 --- a/wire/key_agent/src/lib.rs +++ b/wire/agent/src/lib.rs @@ -2,5 +2,5 @@ // Copyright 2024-2025 wire Contributors pub mod keys { - include!(concat!(env!("OUT_DIR"), "/key_agent.keys.rs")); + include!(concat!(env!("OUT_DIR"), "/agent.keys.rs")); } diff --git a/wire/key_agent/src/main.rs b/wire/agent/src/main.rs similarity index 98% rename from wire/key_agent/src/main.rs rename to wire/agent/src/main.rs index dba65d6d..bb2946b7 100644 --- a/wire/key_agent/src/main.rs +++ b/wire/agent/src/main.rs @@ -2,10 +2,10 @@ // Copyright 2024-2025 wire Contributors #![deny(clippy::pedantic)] +use agent::keys::KeySpec; use base64::Engine; use base64::prelude::BASE64_STANDARD; use futures_util::stream::StreamExt; -use key_agent::keys::KeySpec; use nix::unistd::{Group, User}; use prost::Message; use prost::bytes::Bytes; diff --git a/wire/cli/default.nix b/wire/cli/default.nix index 5b123f2e..f8e78dde 100644 --- a/wire/cli/default.nix +++ b/wire/cli/default.nix @@ -12,7 +12,7 @@ let cleanSystem = system: lib.replaceStrings [ "-" ] [ "_" ] system; agents = lib.strings.concatMapStrings ( - system: "--set WIRE_KEY_AGENT_${cleanSystem system} ${(getSystem system).packages.agent} " + system: "--set WIRE_AGENT_${cleanSystem system} ${(getSystem system).packages.agent} " ) (import inputs.linux-systems); in { @@ -70,7 +70,7 @@ pkgs.makeWrapper ]; postBuild = '' - wrapProgram $out/bin/wire --set WIRE_KEY_AGENT_${cleanSystem system} ${self'.packages.agent} + wrapProgram $out/bin/wire --set WIRE_AGENT_${cleanSystem system} ${self'.packages.agent} ''; meta.mainProgram = "wire"; }; diff --git a/wire/lib/Cargo.toml b/wire/lib/Cargo.toml index 1c4c15e8..56093163 100644 --- a/wire/lib/Cargo.toml +++ b/wire/lib/Cargo.toml @@ -17,7 +17,7 @@ tracing = { workspace = true } im = { workspace = true } thiserror = "2.0.17" derive_more = { version = "2.0.1", features = ["display"] } -key_agent = { path = "../key_agent" } +agent = { path = "../agent" } futures = "0.3.31" prost = { workspace = true } gethostname = "1.1.0" diff --git a/wire/lib/src/errors.rs b/wire/lib/src/errors.rs index 4c116d40..52e6d812 100644 --- a/wire/lib/src/errors.rs +++ b/wire/lib/src/errors.rs @@ -87,6 +87,20 @@ pub enum ActivationError { )] #[error("failed to run switch-to-configuration {0} on node {1}")] SwitchToConfigurationError(SwitchToConfigurationGoal, Name, #[source] CommandError), + + #[diagnostic( + code(wire::activation::Heartbeat), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("failed to touch /var/lib/wire-rollback/heartbeat on node {name}")] + FailedHeartbeatError { + name: Name, + #[source] + activation_failure: CommandError, + + #[related] + related_errors: Vec, + }, } #[derive(Debug, Diagnostic, Error)] diff --git a/wire/lib/src/hive/node.rs b/wire/lib/src/hive/node.rs index 216151fe..45188477 100644 --- a/wire/lib/src/hive/node.rs +++ b/wire/lib/src/hive/node.rs @@ -154,11 +154,19 @@ impl Display for Target { } } +const fn rollback_default() -> bool { + true +} + #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] pub struct Node { #[serde(rename = "target")] pub target: Target, + /// default value as this is a new attribute + #[serde(rename = "rollback", default = "rollback_default")] + pub rollback: bool, + #[serde(rename = "buildOnTarget")] pub build_remotely: bool, @@ -192,6 +200,7 @@ impl Default for Node { allow_local_deployment: true, build_remotely: false, host_platform: "x86_64-linux".into(), + rollback: rollback_default(), } } } @@ -291,9 +300,10 @@ pub struct StepState { pub evaluation: Option, pub evaluation_rx: Option>>, pub build: Option, - pub key_agent_directory: Option, + pub agent_directory: Option, } +#[allow(clippy::struct_excessive_bools)] pub struct Context<'a> { pub name: &'a Name, pub node: &'a mut Node, diff --git a/wire/lib/src/hive/steps/activate.rs b/wire/lib/src/hive/steps/activate.rs index 45b4190d..3ae1208c 100644 --- a/wire/lib/src/hive/steps/activate.rs +++ b/wire/lib/src/hive/steps/activate.rs @@ -8,7 +8,7 @@ use tracing::{error, info, instrument, warn}; use crate::{ HiveLibError, commands::{CommandArguments, WireCommandChip, run_command}, - errors::{ActivationError, NetworkError}, + errors::{ActivationError, CommandError, NetworkError}, hive::node::{Context, ExecuteStep, Goal, SwitchToConfigurationGoal}, }; @@ -71,6 +71,121 @@ async fn set_profile( Ok(()) } +async fn reboot(ctx: &Context<'_>) -> Result<(), HiveLibError> { + if !ctx.reboot { + return Ok(()); + } + + if ctx.should_apply_locally { + error!("Refusing to reboot local machine!"); + + return Ok(()); + } + + warn!("Rebooting {name}!", name = ctx.name); + + let reboot = run_command( + &CommandArguments::new("reboot now", ctx.modifiers) + .log_stdout() + .on_target(Some(&ctx.node.target)) + .elevated(ctx.node), + ) + .await?; + + // consume result, impossible to know if the machine failed to reboot or we + // simply disconnected + let _ = reboot + .wait_till_success() + .await + .map_err(HiveLibError::CommandError)?; + + info!("Rebooted {name}, waiting to reconnect...", name = ctx.name); + + if wait_for_ping(ctx).await.is_ok() { + return Ok(()); + } + + error!( + "Failed to get regain connection to {name} via {host} after reboot.", + name = ctx.name, + host = ctx.node.target.get_preferred_host()? + ); + + return Err(HiveLibError::NetworkError( + NetworkError::HostUnreachableAfterReboot(ctx.node.target.get_preferred_host()?.to_string()), + )); +} + +async fn rollback( + ctx: &Context<'_>, + goal: &SwitchToConfigurationGoal, + original_error: CommandError, +) -> Result<(), HiveLibError> { + let command_string = "touch /var/lib/wire-rollback/heartbeat".to_string(); + + let child = run_command( + &CommandArguments::new(command_string, ctx.modifiers) + .on_target(Some(&ctx.node.target)) + .elevated(ctx.node) + .log_stdout(), + ) + .await?; + + let result = child.wait_till_success().await; + + match result { + Ok(_) => Err(HiveLibError::ActivationError( + ActivationError::SwitchToConfigurationError(*goal, ctx.name.clone(), original_error), + )), + Err(err) => Err(HiveLibError::ActivationError( + ActivationError::FailedHeartbeatError { + name: ctx.name.clone(), + activation_failure: original_error, + related_errors: vec![err], + }, + )), + } +} + +async fn reconnect_or_rollback( + ctx: &Context<'_>, + goal: &SwitchToConfigurationGoal, + error: CommandError, +) -> Result<(), HiveLibError> { + warn!( + "Activation command for {name} exited unsuccessfully.", + name = ctx.name + ); + + // Bail if the command couldn't of broken the system + // and don't try to regain connection to localhost + if matches!(goal, SwitchToConfigurationGoal::DryActivate) || ctx.should_apply_locally { + return Err(HiveLibError::ActivationError( + ActivationError::SwitchToConfigurationError(*goal, ctx.name.clone(), error), + )); + } + + if wait_for_ping(ctx).await.is_ok() { + if ctx.node.rollback { + return rollback(ctx, goal, error).await; + } + + return Err(HiveLibError::ActivationError( + ActivationError::SwitchToConfigurationError(*goal, ctx.name.clone(), error), + )); + } + + error!( + "Failed to get regain connection to {name} via {host} after {goal} activation.", + name = ctx.name, + host = ctx.node.target.get_preferred_host()? + ); + + return Err(HiveLibError::NetworkError( + NetworkError::HostUnreachableAfterReboot(ctx.node.target.get_preferred_host()?.to_string()), + )); +} + impl ExecuteStep for SwitchToConfiguration { fn should_execute(&self, ctx: &Context) -> bool { matches!(ctx.goal, Goal::SwitchToConfiguration(..)) @@ -95,13 +210,19 @@ impl ExecuteStep for SwitchToConfiguration { info!("Running switch-to-configuration {goal}"); + let goal_str = match goal { + SwitchToConfigurationGoal::Switch => "switch", + SwitchToConfigurationGoal::Boot => "boot", + SwitchToConfigurationGoal::Test => "test", + SwitchToConfigurationGoal::DryActivate => "dry-activate", + }; + let command_string = format!( - "{built_path}/bin/switch-to-configuration {}", - match goal { - SwitchToConfigurationGoal::Switch => "switch", - SwitchToConfigurationGoal::Boot => "boot", - SwitchToConfigurationGoal::Test => "test", - SwitchToConfigurationGoal::DryActivate => "dry-activate", + "{rollback}{built_path}/bin/switch-to-configuration {goal_str}", + rollback = if ctx.node.rollback { + format!("echo \"{goal_str}\" > /var/lib/wire-rollback/goal && ") + } else { + String::new() } ); @@ -120,86 +241,8 @@ impl ExecuteStep for SwitchToConfiguration { let result = child.wait_till_success().await; match result { - Ok(_) => { - if !ctx.reboot { - return Ok(()); - } - - if ctx.should_apply_locally { - error!("Refusing to reboot local machine!"); - - return Ok(()); - } - - warn!("Rebooting {name}!", name = ctx.name); - - let reboot = run_command( - &CommandArguments::new("reboot now", ctx.modifiers) - .log_stdout() - .on_target(Some(&ctx.node.target)) - .elevated(ctx.node), - ) - .await?; - - // consume result, impossible to know if the machine failed to reboot or we - // simply disconnected - let _ = reboot - .wait_till_success() - .await - .map_err(HiveLibError::CommandError)?; - - info!("Rebooted {name}, waiting to reconnect...", name = ctx.name); - - if wait_for_ping(ctx).await.is_ok() { - return Ok(()); - } - - error!( - "Failed to get regain connection to {name} via {host} after reboot.", - name = ctx.name, - host = ctx.node.target.get_preferred_host()? - ); - - return Err(HiveLibError::NetworkError( - NetworkError::HostUnreachableAfterReboot( - ctx.node.target.get_preferred_host()?.to_string(), - ), - )); - } - Err(error) => { - warn!( - "Activation command for {name} exited unsuccessfully.", - name = ctx.name - ); - - // Bail if the command couldn't of broken the system - // and don't try to regain connection to localhost - if matches!(goal, SwitchToConfigurationGoal::DryActivate) - || ctx.should_apply_locally - { - return Err(HiveLibError::ActivationError( - ActivationError::SwitchToConfigurationError(*goal, ctx.name.clone(), error), - )); - } - - if wait_for_ping(ctx).await.is_ok() { - return Err(HiveLibError::ActivationError( - ActivationError::SwitchToConfigurationError(*goal, ctx.name.clone(), error), - )); - } - - error!( - "Failed to get regain connection to {name} via {host} after {goal} activation.", - name = ctx.name, - host = ctx.node.target.get_preferred_host()? - ); - - return Err(HiveLibError::NetworkError( - NetworkError::HostUnreachableAfterReboot( - ctx.node.target.get_preferred_host()?.to_string(), - ), - )); - } + Ok(_) => reboot(ctx).await, + Err(error) => reconnect_or_rollback(ctx, goal, error).await, } } } diff --git a/wire/lib/src/hive/steps/keys.rs b/wire/lib/src/hive/steps/keys.rs index ef0654f3..0f217457 100644 --- a/wire/lib/src/hive/steps/keys.rs +++ b/wire/lib/src/hive/steps/keys.rs @@ -125,7 +125,7 @@ async fn create_reader(key: &'_ Key) -> Result Result<(key_agent::keys::KeySpec, Vec), KeyError> { +async fn process_key(key: &Key) -> Result<(agent::keys::KeySpec, Vec), KeyError> { let mut reader = create_reader(key).await?; let mut buf = Vec::new(); @@ -140,7 +140,7 @@ async fn process_key(key: &Key) -> Result<(key_agent::keys::KeySpec, Vec), K debug!("Staging push to {}", destination.clone().display()); Ok(( - key_agent::keys::KeySpec { + agent::keys::KeySpec { length: buf .len() .try_into() @@ -221,7 +221,7 @@ impl ExecuteStep for Keys { #[instrument(skip_all, name = "keys")] async fn execute(&self, ctx: &mut Context<'_>) -> Result<(), HiveLibError> { - let agent_directory = ctx.state.key_agent_directory.as_ref().unwrap(); + let agent_directory = ctx.state.agent_directory.as_ref().unwrap(); let futures = ctx .node @@ -249,7 +249,7 @@ impl ExecuteStep for Keys { return Ok(()); } - let command_string = format!("{agent_directory}/bin/key_agent"); + let command_string = format!("{agent_directory}/bin/agent"); let mut child = run_command( &CommandArguments::new(command_string, ctx.modifiers) @@ -305,7 +305,7 @@ impl ExecuteStep for PushKeyAgent { #[instrument(skip_all, name = "push_agent")] async fn execute(&self, ctx: &mut Context<'_>) -> Result<(), HiveLibError> { let arg_name = format!( - "WIRE_KEY_AGENT_{platform}", + "WIRE_AGENT_{platform}", platform = ctx.node.host_platform.replace('-', "_") ); @@ -322,7 +322,7 @@ impl ExecuteStep for PushKeyAgent { push(ctx, Push::Path(&agent_directory)).await?; } - ctx.state.key_agent_directory = Some(agent_directory); + ctx.state.agent_directory = Some(agent_directory); Ok(()) }