git.delta.rocks / jrsonnet / refs/commits / 1470de8a447c

difftreelog

feat minimal rollback support

Lach2025-06-18parent: #11412de.patch.diff
in: trunk

20 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -83,10 +83,10 @@
  "i18n-embed",
  "i18n-embed-fl",
  "lazy_static",
- "nom",
+ "nom 7.1.3",
  "num-traits",
  "pin-project",
- "rand",
+ "rand 0.8.5",
  "rsa",
  "rust-embed",
  "scrypt",
@@ -107,8 +107,8 @@
  "cookie-factory",
  "hkdf",
  "io_tee",
- "nom",
- "rand",
+ "nom 7.1.3",
+ "rand 0.8.5",
  "secrecy",
  "sha2",
 ]
@@ -233,18 +233,18 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
 name = "async-trait"
-version = "0.1.83"
+version = "0.1.88"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -395,15 +395,15 @@
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.87",
+ "syn",
  "which",
 ]
 
 [[package]]
 name = "bitflags"
-version = "2.6.0"
+version = "2.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
 dependencies = [
  "serde",
 ]
@@ -493,7 +493,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
 dependencies = [
- "nom",
+ "nom 7.1.3",
 ]
 
 [[package]]
@@ -534,9 +534,9 @@
 
 [[package]]
 name = "chrono"
-version = "0.4.38"
+version = "0.4.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
 dependencies = [
  "android-tzdata",
  "iana-time-zone",
@@ -544,7 +544,7 @@
  "num-traits",
  "serde",
  "wasm-bindgen",
- "windows-targets",
+ "windows-link",
 ]
 
 [[package]]
@@ -609,10 +609,10 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
 dependencies = [
- "heck 0.5.0",
+ "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -647,6 +647,15 @@
 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
 
 [[package]]
+name = "convert_case"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
 name = "cookie-factory"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -684,16 +693,18 @@
 
 [[package]]
 name = "crossterm"
-version = "0.28.1"
+version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
 dependencies = [
  "bitflags",
  "crossterm_winapi",
+ "derive_more",
+ "document-features",
  "filedescriptor",
  "mio",
  "parking_lot",
- "rustix",
+ "rustix 1.0.7",
  "signal-hook",
  "signal-hook-mio",
  "winapi",
@@ -715,7 +726,7 @@
 checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
- "rand_core",
+ "rand_core 0.6.4",
  "typenum",
 ]
 
@@ -752,7 +763,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -781,15 +792,36 @@
 
 [[package]]
 name = "deranged"
-version = "0.3.11"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
 dependencies = [
  "powerfmt",
  "serde",
 ]
 
 [[package]]
+name = "derive_more"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "digest"
 version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -809,10 +841,19 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
+name = "document-features"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
 name = "ed25519"
 version = "2.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -830,7 +871,6 @@
 dependencies = [
  "curve25519-dalek",
  "ed25519",
- "rand_core",
  "serde",
  "sha2",
  "subtle",
@@ -857,12 +897,12 @@
 
 [[package]]
 name = "errno"
-version = "0.3.9"
+version = "0.3.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
 dependencies = [
  "libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -924,10 +964,10 @@
  "hostname",
  "human-repr",
  "indicatif",
- "itertools 0.13.0",
+ "itertools 0.14.0",
  "nix-eval",
  "nixlike",
- "nom",
+ "nom 8.0.0",
  "openssh",
  "owo-colors",
  "peg",
@@ -958,15 +998,17 @@
  "futures",
  "hostname",
  "indoc",
- "itertools 0.13.0",
+ "itertools 0.14.0",
  "nix-eval",
  "nixlike",
- "nom",
+ "nom 8.0.0",
  "openssh",
- "rand",
+ "rand 0.9.1",
  "serde",
  "serde_json",
+ "tabled",
  "tempfile",
+ "time",
  "tokio",
  "tokio-util",
  "tracing",
@@ -983,7 +1025,7 @@
  "ed25519-dalek",
  "fleet-shared",
  "hex",
- "rand",
+ "rand 0.9.1",
  "x25519-dalek",
 ]
 
@@ -1119,7 +1161,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -1170,7 +1212,19 @@
 dependencies = [
  "cfg-if",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
 ]
 
 [[package]]
@@ -1237,12 +1291,6 @@
 version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
-
-[[package]]
-name = "heck"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
 [[package]]
 name = "heck"
@@ -1297,13 +1345,13 @@
 
 [[package]]
 name = "hostname"
-version = "0.4.0"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
+checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
 dependencies = [
  "cfg-if",
  "libc",
- "windows",
+ "windows-link",
 ]
 
 [[package]]
@@ -1463,7 +1511,7 @@
  "proc-macro2",
  "quote",
  "strsim",
- "syn 2.0.87",
+ "syn",
  "unic-langid",
 ]
 
@@ -1477,7 +1525,7 @@
  "i18n-config",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -1620,6 +1668,15 @@
 ]
 
 [[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
 name = "itoa"
 version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1651,9 +1708,9 @@
 
 [[package]]
 name = "libc"
-version = "0.2.164"
+version = "0.2.174"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
 
 [[package]]
 name = "libloading"
@@ -1694,6 +1751,18 @@
 checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litrs"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
+
+[[package]]
 name = "lock_api"
 version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1778,7 +1847,7 @@
  "hermit-abi 0.3.9",
  "libc",
  "log",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "windows-sys 0.52.0",
 ]
 
@@ -1790,9 +1859,9 @@
 
 [[package]]
 name = "nix"
-version = "0.29.0"
+version = "0.30.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
 dependencies = [
  "bitflags",
  "cfg-if",
@@ -1807,13 +1876,13 @@
  "anyhow",
  "better-command",
  "futures",
- "itertools 0.13.0",
+ "itertools 0.14.0",
  "nixlike",
  "r2d2",
  "regex",
  "serde",
  "serde_json",
- "thiserror 2.0.3",
+ "thiserror 2.0.12",
  "tokio",
  "tokio-util",
  "tracing",
@@ -1839,7 +1908,7 @@
  "serde",
  "serde-transcode",
  "serde_json",
- "thiserror 2.0.3",
+ "thiserror 2.0.12",
 ]
 
 [[package]]
@@ -1872,6 +1941,15 @@
 ]
 
 [[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
 name = "nu-ansi-term"
 version = "0.46.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1893,7 +1971,7 @@
  "num-integer",
  "num-iter",
  "num-traits",
- "rand",
+ "rand 0.8.5",
  "smallvec",
  "zeroize",
 ]
@@ -1963,15 +2041,15 @@
 
 [[package]]
 name = "openssh"
-version = "0.11.3"
+version = "0.11.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b52987a10526b8daef7f1946b0aadfc214479f897ba624776327fd3beec2722c"
+checksum = "ea0bb128ba90e86bc55dae66031935f361cda4cbc1f011547c55a7d80079bc3e"
 dependencies = [
  "libc",
  "once_cell",
  "shell-escape",
  "tempfile",
- "thiserror 2.0.3",
+ "thiserror 2.0.12",
  "tokio",
 ]
 
@@ -1983,9 +2061,9 @@
 
 [[package]]
 name = "owo-colors"
-version = "4.1.0"
+version = "4.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56"
+checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec"
 dependencies = [
  "supports-color 2.1.0",
  "supports-color 3.0.1",
@@ -1993,13 +2071,13 @@
 
 [[package]]
 name = "papergrid"
-version = "0.12.0"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725"
+checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1"
 dependencies = [
  "bytecount",
  "fnv",
- "unicode-width 0.1.11",
+ "unicode-width 0.2.0",
 ]
 
 [[package]]
@@ -2043,9 +2121,9 @@
 
 [[package]]
 name = "peg"
-version = "0.8.4"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f"
+checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477"
 dependencies = [
  "peg-macros",
  "peg-runtime",
@@ -2053,9 +2131,9 @@
 
 [[package]]
 name = "peg-macros"
-version = "0.8.4"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426"
+checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71"
 dependencies = [
  "peg-runtime",
  "proc-macro2",
@@ -2064,9 +2142,9 @@
 
 [[package]]
 name = "peg-runtime"
-version = "0.8.3"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
+checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca"
 
 [[package]]
 name = "pem"
@@ -2111,7 +2189,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -2204,31 +2282,7 @@
 checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
 dependencies = [
  "proc-macro2",
- "syn 2.0.87",
-]
-
-[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
-dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
+ "syn",
 ]
 
 [[package]]
@@ -2250,7 +2304,7 @@
  "proc-macro-error-attr2",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -2279,7 +2333,7 @@
 checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15"
 dependencies = [
  "bytes",
- "heck 0.5.0",
+ "heck",
  "itertools 0.13.0",
  "log",
  "multimap",
@@ -2289,7 +2343,7 @@
  "prost",
  "prost-types",
  "regex",
- "syn 2.0.87",
+ "syn",
  "tempfile",
 ]
 
@@ -2303,7 +2357,7 @@
  "itertools 0.13.0",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -2325,6 +2379,12 @@
 ]
 
 [[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
 name = "r2d2"
 version = "0.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2342,18 +2402,38 @@
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
 ]
 
 [[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
 name = "rand_chacha"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
 ]
 
 [[package]]
@@ -2362,10 +2442,19 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
 ]
 
 [[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
 name = "rcgen"
 version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2439,7 +2528,7 @@
 dependencies = [
  "cc",
  "cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
  "libc",
  "spin",
  "untrusted",
@@ -2481,14 +2570,15 @@
 
 [[package]]
 name = "ron"
-version = "0.8.1"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f"
 dependencies = [
- "base64 0.21.7",
+ "base64 0.22.1",
  "bitflags",
  "serde",
  "serde_derive",
+ "unicode-ident",
 ]
 
 [[package]]
@@ -2517,7 +2607,7 @@
  "num-traits",
  "pkcs1",
  "pkcs8",
- "rand_core",
+ "rand_core 0.6.4",
  "signature",
  "spki",
  "subtle",
@@ -2544,7 +2634,7 @@
  "proc-macro2",
  "quote",
  "rust-embed-utils",
- "syn 2.0.87",
+ "syn",
  "walkdir",
 ]
 
@@ -2588,11 +2678,24 @@
  "bitflags",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.14",
  "windows-sys 0.52.0",
 ]
 
 [[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.4",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "rustls"
 version = "0.23.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2721,9 +2824,9 @@
 
 [[package]]
 name = "serde"
-version = "1.0.215"
+version = "1.0.219"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
 dependencies = [
  "serde_derive",
 ]
@@ -2748,20 +2851,20 @@
 
 [[package]]
 name = "serde_derive"
-version = "1.0.215"
+version = "1.0.219"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.133"
+version = "1.0.140"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
 dependencies = [
  "itoa",
  "memchr",
@@ -2838,7 +2941,7 @@
 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
 dependencies = [
  "digest",
- "rand_core",
+ "rand_core 0.6.4",
 ]
 
 [[package]]
@@ -2920,17 +3023,6 @@
 checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77"
 dependencies = [
  "is_ci",
-]
-
-[[package]]
-name = "syn"
-version = "1.0.109"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
 ]
 
 [[package]]
@@ -2958,37 +3050,38 @@
 
 [[package]]
 name = "tabled"
-version = "0.16.0"
+version = "0.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b"
+checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d"
 dependencies = [
  "papergrid",
  "tabled_derive",
+ "testing_table",
 ]
 
 [[package]]
 name = "tabled_derive"
-version = "0.8.0"
+version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069"
+checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846"
 dependencies = [
- "heck 0.4.1",
- "proc-macro-error",
+ "heck",
+ "proc-macro-error2",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn",
 ]
 
 [[package]]
 name = "tempfile"
-version = "3.14.0"
+version = "3.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
 dependencies = [
- "cfg-if",
  "fastrand",
+ "getrandom 0.3.3",
  "once_cell",
- "rustix",
+ "rustix 1.0.7",
  "windows-sys 0.59.0",
 ]
 
@@ -2998,7 +3091,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef"
 dependencies = [
- "rustix",
+ "rustix 0.38.40",
  "windows-sys 0.59.0",
 ]
 
@@ -3014,6 +3107,15 @@
 ]
 
 [[package]]
+name = "testing_table"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc"
+dependencies = [
+ "unicode-width 0.2.0",
+]
+
+[[package]]
 name = "text-size"
 version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3058,11 +3160,11 @@
 
 [[package]]
 name = "thiserror"
-version = "2.0.3"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
 dependencies = [
- "thiserror-impl 2.0.3",
+ "thiserror-impl 2.0.12",
 ]
 
 [[package]]
@@ -3073,18 +3175,18 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.3"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -3099,9 +3201,9 @@
 
 [[package]]
 name = "time"
-version = "0.3.36"
+version = "0.3.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
 dependencies = [
  "deranged",
  "num-conv",
@@ -3113,15 +3215,15 @@
 
 [[package]]
 name = "time-core"
-version = "0.1.2"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
 
 [[package]]
 name = "time-macros"
-version = "0.2.18"
+version = "0.2.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
 dependencies = [
  "num-conv",
  "time-core",
@@ -3138,9 +3240,9 @@
 
 [[package]]
 name = "tokio"
-version = "1.41.1"
+version = "1.45.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
 dependencies = [
  "backtrace",
  "bytes",
@@ -3155,13 +3257,13 @@
 
 [[package]]
 name = "tokio-macros"
-version = "2.4.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -3189,9 +3291,9 @@
 
 [[package]]
 name = "tokio-util"
-version = "0.7.12"
+version = "0.7.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
 dependencies = [
  "bytes",
  "futures-core",
@@ -3252,7 +3354,7 @@
  "prost-build",
  "prost-types",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -3266,7 +3368,7 @@
  "indexmap 1.9.3",
  "pin-project",
  "pin-project-lite",
- "rand",
+ "rand 0.8.5",
  "slab",
  "tokio",
  "tokio-util",
@@ -3337,7 +3439,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -3457,6 +3559,12 @@
 checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
 
 [[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.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3476,9 +3584,9 @@
 
 [[package]]
 name = "unindent"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
+checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
 
 [[package]]
 name = "universal-hash"
@@ -3573,6 +3681,15 @@
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
 name = "wasm-bindgen"
 version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3594,7 +3711,7 @@
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
  "wasm-bindgen-shared",
 ]
 
@@ -3616,7 +3733,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -3646,7 +3763,7 @@
  "either",
  "home",
  "once_cell",
- "rustix",
+ "rustix 0.38.40",
 ]
 
 [[package]]
@@ -3681,23 +3798,19 @@
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
-name = "windows"
+name = "windows-core"
 version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-core",
  "windows-targets",
 ]
 
 [[package]]
-name = "windows-core"
-version = "0.52.0"
+name = "windows-link"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
-dependencies = [
- "windows-targets",
-]
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
 
 [[package]]
 name = "windows-sys"
@@ -3782,13 +3895,22 @@
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
 name = "x25519-dalek"
 version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
 dependencies = [
  "curve25519-dalek",
- "rand_core",
+ "rand_core 0.6.4",
  "serde",
  "zeroize",
 ]
@@ -3804,9 +3926,9 @@
 
 [[package]]
 name = "z85"
-version = "3.0.5"
+version = "3.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a599daf1b507819c1121f0bf87fa37eb19daac6aff3aefefd4e6e2e0f2020fc"
+checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64"
 
 [[package]]
 name = "zerocopy"
@@ -3826,7 +3948,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
 
 [[package]]
@@ -3846,5 +3968,5 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.87",
+ "syn",
 ]
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,12 +15,12 @@
 anyhow = "1.0"
 clap = { version = "4.5", features = ["derive", "env", "unicode", "wrap_help"] }
 clap_complete = "4.5"
-nix = { version = "0.29.0", features = ["fs", "user"] }
+nix = { version = "0.30.1", features = ["fs", "user"] }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
-tempfile = "3.10"
-thiserror = "2.0.3"
-tokio = { version = "1.36.0", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
-tokio-util = { version = "0.7.11", features = ["codec"] }
+tempfile = "3.20"
+thiserror = "2.0.12"
+tokio = { version = "1.45.1", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
+tokio-util = { version = "0.7.15", features = ["codec"] }
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -5,6 +5,7 @@
 authors = ["Yaroslav Bolyukin <iam@lach.pw>"]
 edition.workspace = true
 rust-version.workspace = true
+default-run = "fleet"
 
 [dependencies]
 age = { workspace = true, features = ["armor"] }
@@ -27,23 +28,23 @@
 async-trait = "0.1"
 base64 = "0.22.1"
 chrono = { version = "0.4", features = ["serde"] }
-crossterm = { version = "0.28.0", features = ["use-dev-tty"] }
+crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
 futures = "0.3"
-hostname = "0.4.0"
-itertools = "0.13"
+hostname = "0.4.1"
+itertools = "0.14"
 openssh = "0.11"
-owo-colors = { version = "4.0", features = ["supports-color", "supports-colors"] }
+owo-colors = { version = "4.2", features = ["supports-color", "supports-colors"] }
 peg = "0.8"
-regex = "1.10"
+regex = "1.11"
 shlex = "1.3"
-tabled = { version = "0.16" }
+tabled = { version = "0.20" }
 time = { version = "0.3", features = ["serde"] }
 tokio-util = { version = "0.7", features = ["codec"] }
 
 fleet-base = { version = "0.1.0", path = "../../crates/fleet-base" }
 human-repr = { version = "1.1", optional = true }
 indicatif = { version = "0.17", optional = true }
-nom = "7.1.3"
+nom = "8.0.0"
 tracing-indicatif = { version = "0.3", optional = true }
 
 [features]
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -1,14 +1,14 @@
-use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf, time::Duration};
+use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};
 
-use anyhow::{anyhow, bail, Context, Result};
-use clap::{Parser, ValueEnum};
+use anyhow::{anyhow, Result};
+use clap::Parser;
 use fleet_base::{
-	host::{Config, ConfigHost, DeployKind},
+	deploy::{deploy_task, upload_task, DeployAction},
+	host::{Config, DeployKind, GenerationStorage},
 	opts::FleetOpts,
 };
-use itertools::Itertools as _;
 use nix_eval::{nix_go, NixBuildBatch};
-use tokio::{task::LocalSet, time::sleep};
+use tokio::task::LocalSet;
 use tracing::{error, field, info, info_span, warn, Instrument};
 
 #[derive(Parser)]
@@ -18,300 +18,16 @@
 	disable_rollback: bool,
 	/// Action to execute after system is built
 	action: DeployAction,
-}
-
-#[derive(ValueEnum, Clone, Copy)]
-enum DeployAction {
-	/// Upload derivation, but do not execute the update.
-	Upload,
-	/// Upload and execute the activation script, old version will be used after reboot.
-	Test,
-	/// Upload and set as current system profile, but do not execute activation script.
-	Boot,
-	/// Upload, set current profile, and execute activation script.
-	Switch,
 }
 
-impl DeployAction {
-	pub(crate) fn name(&self) -> Option<&'static str> {
-		match self {
-			Self::Upload => None,
-			Self::Test => Some("test"),
-			Self::Boot => Some("boot"),
-			Self::Switch => Some("switch"),
-		}
-	}
-	pub(crate) fn should_switch_profile(&self) -> bool {
-		matches!(self, Self::Switch | Self::Boot)
-	}
-	pub(crate) fn should_activate(&self) -> bool {
-		matches!(self, Self::Switch | Self::Test | Self::Boot)
-	}
-	pub(crate) fn should_create_rollback_marker(&self) -> bool {
-		// Upload does nothing on the target machine, other than uploading the closure.
-		// In boot case we want to have rollback marker prepared, so that the system may rollback itself on the next boot.
-		!matches!(self, Self::Upload)
-	}
-	pub(crate) fn should_schedule_rollback_run(&self) -> bool {
-		matches!(self, Self::Switch | Self::Test)
-	}
-}
-
 #[derive(Parser, Clone)]
 pub struct BuildSystems {
 	/// Attribute to build. Systems are deployed from "toplevel" attr, well-known used attributes
 	/// are "sdImage"/"isoImage", and your configuration may include any other build attributes.
 	#[clap(long, default_value = "toplevel")]
 	build_attr: String,
-}
-
-struct Generation {
-	id: u32,
-	current: bool,
-	datetime: String,
-}
-
-fn parse_generation_line(g: &str) -> Option<Generation> {
-	let mut parts = g.split_whitespace();
-	let id = parts.next()?;
-	let id: u32 = id.parse().ok()?;
-	let date = parts.next()?;
-	let time = parts.next()?;
-	let current = if let Some(current) = parts.next() {
-		if current == "(current)" {
-			Some(true)
-		} else {
-			None
-		}
-	} else {
-		Some(false)
-	};
-	let current = current?;
-	if parts.next().is_some() {
-		warn!("unexpected text after generation: {g}");
-	}
-	Some(Generation {
-		id,
-		current,
-		datetime: format!("{date} {time}"),
-	})
-}
-
-async fn get_current_generation(host: &ConfigHost) -> Result<Generation> {
-	let mut cmd = host.cmd("nix-env").await?;
-	cmd.comparg("--profile", "/nix/var/nix/profiles/system")
-		.arg("--list-generations");
-	// Sudo is required due to --list-generations acquiring lock on the profile.
-	let data = cmd.sudo().run_string().await?;
-	let generations = data
-		.split('\n')
-		.map(|e| e.trim())
-		.filter(|&l| !l.is_empty())
-		.filter_map(|g| {
-			let gen = parse_generation_line(g);
-			if gen.is_none() {
-				warn!("bad generation: {g}");
-			}
-			gen
-		})
-		.collect::<Vec<_>>();
-	let current = generations
-		.into_iter()
-		.filter(|g| g.current)
-		.at_most_one()
-		.map_err(|_e| anyhow!("bad list-generations output"))?
-		.ok_or_else(|| anyhow!("failed to find generation"))?;
-	Ok(current)
 }
-
-async fn deploy_task(
-	action: DeployAction,
-	host: &ConfigHost,
-	built: PathBuf,
-	specialisation: Option<String>,
-	disable_rollback: bool,
-) -> Result<()> {
-	let deploy_kind = host.deploy_kind().await?;
-	if (deploy_kind == DeployKind::NixosInstall || deploy_kind == DeployKind::NixosLustrate)
-		&& !matches!(action, DeployAction::Boot | DeployAction::Upload)
-	{
-		bail!("{deploy_kind:?} deploy kind only supports boot and upload actions");
-	}
-
-	let mut failed = false;
 
-	// TODO: Lockfile, to prevent concurrent system switch?
-	// TODO: If rollback target exists - bail, it should be removed. Lockfile will not work in case if rollback
-	// is scheduler on next boot (default behavior). On current boot - rollback activator will fail due to
-	// unit name conflict in systemd-run
-	// This code is tied to rollback.nix
-	if !disable_rollback && action.should_create_rollback_marker() {
-		let _span = info_span!("preparing").entered();
-		info!("preparing for rollback");
-		let generation = get_current_generation(host).await?;
-		info!(
-			"rollback target would be {} {}",
-			generation.id, generation.datetime
-		);
-		{
-			let mut cmd = host.cmd("sh").await?;
-			cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));
-			if let Err(e) = cmd.sudo().run().await {
-				error!("failed to set rollback marker: {e}");
-				failed = true;
-			}
-		}
-		// Activation script also starts rollback-watchdog.timer, however, it is possible that it won't be started.
-		// Kicking it on manually will work best.
-		//
-		// There wouldn't be conflict, because here we trigger start of the primary service, and systemd will
-		// only allow one instance of it.
-
-		// TODO: We should also watch how this process is going.
-		// After running this command, we have less than 3 minutes to deploy everything,
-		// if we fail to perform generation switch in time, then we will still call the activation script, and this may break something.
-		// Anyway, reboot will still help in this case.
-		if action.should_schedule_rollback_run() {
-			let mut cmd = host.cmd("systemd-run").await?;
-			cmd.comparg("--on-active", "3min")
-				.comparg("--unit", "rollback-watchdog-run")
-				.arg("systemctl")
-				.arg("start")
-				.arg("rollback-watchdog.service");
-			if let Err(e) = cmd.sudo().run().await {
-				error!("failed to schedule rollback run: {e}");
-				failed = true;
-			}
-		}
-	}
-	if deploy_kind == DeployKind::NixosLustrate {
-		// Fleet could also create this file, but as this operation is potentially disruptive,
-		// make user do it themself.
-		if !host.file_exists("/etc/NIXOS_LUSTRATE").await? {
-			bail!("/etc/NIXOS_LUSTRATE should be created on remote host");
-		}
-		// Wanted by NixOS to recognize the system as NixOS.
-		let mut cmd = host.cmd("touch").await?;
-		cmd.arg("/etc/NIXOS");
-		cmd.sudo().run().await.context("creating /etc/NIXOS")?;
-	}
-	if deploy_kind == DeployKind::NixosInstall {
-		info!(
-			"running nixos-install to switch profile, install bootloader, and perform activation"
-		);
-		let mut cmd = host.cmd("nixos-install").await?;
-		cmd.arg("--system").arg(&built).args([
-			// Channels here aren't fleet host system channels, but channels embedded in installation cd, which might be old.
-			// It is possible to copy host channels, but I would prefer non-flake nix just to be unsupported.
-			"--no-channel-copy",
-			"--root",
-			"/mnt",
-		]);
-		if let Err(e) = cmd.sudo().run().await {
-			error!("failed to execute nixos-install: {e}");
-			failed = true;
-		}
-	} else {
-		if action.should_switch_profile() && !failed {
-			info!("switching system profile generation");
-
-			// To avoid even more problems, using nixos-install for now.
-			// // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
-			// // falling back to using nix-env command
-			// // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
-			// let mut cmd = host.cmd("nix-env").await?;
-			// cmd.args([
-			// 	"--store",
-			// 	"/mnt",
-			// 	"--profile",
-			// 	"/mnt/nix/var/nix/profiles/system",
-			// 	"--set",
-			// ])
-			// .arg(&built);
-			// if let Err(e) = cmd.sudo().run_nix().await {
-			// 	error!("failed to switch system profile generation: {e}");
-			// 	failed = true;
-			// }
-			// It would also be possible to update profile atomically during copy:
-			// https://github.com/NixOS/nix/pull/11657
-			let mut cmd = host.nix_cmd().await?;
-			cmd.arg("build");
-			cmd.comparg("--profile", "/nix/var/nix/profiles/system");
-			cmd.arg(&built);
-			if let Err(e) = cmd.sudo().run_nix().await {
-				error!("failed to switch system profile generation: {e}");
-				failed = true;
-			}
-		}
-
-		// FIXME: Connection might be disconnected after activation run
-
-		if action.should_activate() && !failed {
-			let _span = info_span!("activating").entered();
-			info!("executing activation script");
-			let specialised = if let Some(specialisation) = specialisation {
-				let mut specialised = built.join("specialisation");
-				specialised.push(specialisation);
-				specialised
-			} else {
-				built.clone()
-			};
-			let switch_script = specialised.join("bin/switch-to-configuration");
-			let mut cmd = host.cmd(switch_script).in_current_span().await?;
-			if deploy_kind == DeployKind::NixosLustrate {
-				cmd.env("NIXOS_INSTALL_BOOTLOADER", "1");
-			}
-			cmd.env("FLEET_ONLINE_ACTIVATION", "1")
-				.arg(action.name().expect("upload.should_activate == false"));
-			if let Err(e) = cmd.sudo().run().in_current_span().await {
-				error!("failed to activate: {e}");
-				failed = true;
-			}
-		}
-	}
-	if action.should_create_rollback_marker() {
-		if !disable_rollback {
-			if failed {
-				if action.should_schedule_rollback_run() {
-					info!("executing rollback");
-					if let Err(e) = host
-						.systemctl_start("rollback-watchdog.service")
-						.instrument(info_span!("rollback"))
-						.await
-					{
-						error!("failed to trigger rollback: {e}")
-					}
-				}
-			} else {
-				info!("trying to mark upgrade as successful");
-				if let Err(e) = host
-					.rm_file("/etc/fleet_rollback_marker", true)
-					.in_current_span()
-					.await
-				{
-					error!("failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}")
-				}
-			}
-			info!("disarming watchdog, just in case");
-			if let Err(_e) = host.systemctl_stop("rollback-watchdog.timer").await {
-				// It is ok, if there was no reboot - then timer might not be running.
-			}
-			if action.should_schedule_rollback_run() {
-				if let Err(e) = host.systemctl_stop("rollback-watchdog-run.timer").await {
-					error!("failed to disarm rollback run: {e}");
-				}
-			}
-		} else if let Err(_e) = host
-			.rm_file("/etc/fleet_rollback_marker", true)
-			.in_current_span()
-			.await
-		{
-			// Marker might not exist, yet better try to remove it.
-		}
-	}
-	Ok(())
-}
-
 async fn build_task(
 	config: Config,
 	hostname: String,
@@ -328,7 +44,8 @@
 		.get("out")
 		.ok_or_else(|| anyhow!("system build should produce \"out\" output"))?;
 
-	{
+	// We already have system profiles for backups.
+	if !host.local {
 		info!("adding gc root");
 		let mut cmd = config.local_host().cmd("nix").await?;
 		cmd.arg("build")
@@ -403,7 +120,6 @@
 			let config = config.clone();
 			let span = info_span!("deploy", host = field::display(&host.name));
 			let hostname = host.name.clone();
-			let local_host = config.local_host();
 			let opts = opts.clone();
 			let batch = batch.clone();
 			if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {
@@ -437,51 +153,20 @@
 						disable_rollback = true;
 					}
 
-					if !opts.is_local(&hostname) {
-						info!("uploading system closure");
+					let remote_path =
+						match upload_task(&config, &host, GenerationStorage::Deployer, built).await
 						{
-							// TODO: Move to remote_derivation method.
-							// Alternatively, nix store make-content-addressed can be used,
-							// at least for the first deployment, to provide trusted store key.
-							//
-							// It is much slower, yet doesn't require root on the deployer machine.
-							let Ok(mut sign) = local_host.cmd("nix").await else {
-								error!("failed to setup local");
+							Ok(v) => v,
+							Err(e) => {
+								error!("upload failed: {e}");
 								return;
-							};
-							// Private key for host machine is registered in nix-sign.nix
-							sign.arg("store")
-								.arg("sign")
-								.comparg("--key-file", "/etc/nix/private-key")
-								.arg("-r")
-								.arg(&built);
-							if let Err(e) = sign.sudo().run_nix().await {
-								warn!("failed to sign store paths: {e}");
-							};
-						}
-						let mut tries = 0;
-						loop {
-							match host.remote_derivation(&built).await {
-								Ok(remote) => {
-									assert!(remote == built, "CA derivations aren't implemented");
-									break;
-								}
-								Err(e) if tries < 3 => {
-									tries += 1;
-									warn!("copy failure ({}/3): {}", tries, e);
-									sleep(Duration::from_millis(5000)).await;
-								}
-								Err(e) => {
-									error!("upload failed: {e}");
-									return;
-								}
 							}
-						}
-					}
+						};
+
 					if let Err(e) = deploy_task(
 						self.action,
 						&host,
-						built,
+						remote_path,
 						if let Ok(v) = opts.action_attr(&host, "specialisation").await {
 							v
 						} else {
modifiedcmds/fleet/src/cmds/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/mod.rs
+++ b/cmds/fleet/src/cmds/mod.rs
@@ -3,3 +3,4 @@
 pub mod info;
 pub mod secrets;
 pub mod tf;
+pub mod rollback;
\ No newline at end of file
addedcmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -0,0 +1,127 @@
+use std::collections::HashSet;
+
+use anyhow::{bail, Result};
+use clap::Parser;
+use fleet_base::{
+	deploy::{deploy_task, upload_task, DeployAction},
+	host::{Config, ConfigHost, Generation, GenerationStorage},
+	opts::FleetOpts,
+};
+use tabled::Table;
+use tracing::{info, warn};
+
+#[derive(Parser)]
+pub struct RollbackSingle {
+	machine: String,
+	#[clap(subcommand)]
+	action: RollbackAction,
+}
+
+#[derive(Parser, Clone)]
+struct DeployOptions {
+	/// Rollback target to use
+	id: String,
+	/// Rollback to the current generation if rollback fails
+	// Automatic rollback seems to be unnecessary for manual rollback...
+	#[clap(long)]
+	enable_rollback: bool,
+	/// Specialization to use
+	#[clap(long)]
+	specialization: Option<String>,
+}
+
+#[derive(Parser, Clone)]
+enum RollbackAction {
+	/// List available rollback targets
+	ListTargets,
+	/// Upload and execute the activation script, old version will be used after reboot.
+	Test(#[clap(flatten)] DeployOptions),
+	/// Upload, set current profile, and execute activation script.
+	Switch(#[clap(flatten)] DeployOptions),
+	/// Upload and set as current system profile, but do not execute activation script.
+	Boot(#[clap(flatten)] DeployOptions),
+}
+
+pub async fn list_all_generations(host: &ConfigHost, config: &Config) -> Vec<Generation> {
+	let stored_on_machine = host
+		.list_generations("system")
+		.await
+		.inspect_err(|e| {
+			warn!("failed to list generations available on the remote machine: {e}");
+		})
+		.unwrap_or_default();
+	let on_machine_store_paths = stored_on_machine
+		.iter()
+		.map(|g| &g.store_path)
+		.collect::<HashSet<_>>();
+	let mut stored_locally = config
+		.local_host()
+		.list_generations(&format!("{}-{}", config.data().gc_root_prefix, host.name))
+		.await
+		.inspect_err(|e| {
+			warn!("failed to list generations available locally: {e}");
+		})
+		.unwrap_or_default();
+	dbg!(&stored_locally);
+	stored_locally.retain(|g| !on_machine_store_paths.contains(&g.store_path));
+	for ele in stored_locally.iter_mut() {
+		ele.current = false;
+		ele.location = GenerationStorage::Deployer;
+	}
+	stored_locally.extend(stored_on_machine);
+	stored_locally.sort_by_key(|v| v.datetime);
+	stored_locally
+}
+
+impl RollbackSingle {
+	pub(crate) async fn run(&self, config: &Config, _opts: &FleetOpts) -> Result<()> {
+		let host = config.host(&self.machine).await?;
+		match &self.action {
+			RollbackAction::ListTargets => {
+				let generations = list_all_generations(&host, config).await;
+				if generations.is_empty() {
+					bail!("no available rollback targets found");
+				}
+				info!("Generation list:\n{}", Table::new(&generations));
+				Ok(())
+			}
+			RollbackAction::Boot(o) | RollbackAction::Test(o) | RollbackAction::Switch(o) => {
+				let DeployOptions {
+					id,
+					enable_rollback,
+					specialization,
+				} = o;
+				let action: DeployAction = match self.action {
+					RollbackAction::Test { .. } => DeployAction::Test,
+					RollbackAction::Switch { .. } => DeployAction::Switch,
+					RollbackAction::Boot { .. } => DeployAction::Boot,
+					_ => unreachable!(),
+				};
+				let generations = list_all_generations(&host, config).await;
+				let Some(generation) = generations.iter().find(|g| &g.rollback_id() == id) else {
+					bail!(
+						"generation by this name is not found, existing generations:\n{}",
+						Table::new(&generations)
+					);
+				};
+				let remote_path = upload_task(
+					config,
+					&host,
+					generation.location,
+					generation.store_path.clone(),
+				)
+				.await?;
+
+				deploy_task(
+					action,
+					&host,
+					remote_path,
+					specialization.clone(),
+					!*enable_rollback,
+				)
+				.await?;
+				Ok(())
+			}
+		}
+	}
+}
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -10,6 +10,7 @@
 use clap::{CommandFactory, Parser};
 use cmds::{
 	build_systems::{BuildSystems, Deploy},
+	rollback::RollbackSingle,
 	complete::Complete,
 	info::Info,
 	secrets::Secret,
@@ -70,6 +71,8 @@
 	BuildSystems(BuildSystems),
 	/// Upload and switch system closures
 	Deploy(Deploy),
+	/// Rollback remote machine by redeploying old generation as the new one
+	RollbackSingle(RollbackSingle),
 	/// Secret management
 	#[clap(subcommand)]
 	Secret(Secret),
@@ -97,6 +100,7 @@
 	match command {
 		Opts::BuildSystems(c) => c.run(config, &opts).await?,
 		Opts::Deploy(d) => d.run(config, &opts).await?,
+		Opts::RollbackSingle(r) => r.run(config, &opts).await?,
 		Opts::Secret(s) => s.run(config, &opts).await?,
 		Opts::Info(i) => i.run(config).await?,
 		Opts::Prefetch(p) => p.run(config).await?,
modifiedcmds/generator-helper/Cargo.tomldiffbeforeafterboth
--- a/cmds/generator-helper/Cargo.toml
+++ b/cmds/generator-helper/Cargo.toml
@@ -11,7 +11,7 @@
 fleet-shared.workspace = true
 
 base64 = "0.22.1"
-ed25519-dalek = { version = "2.1", features = ["rand_core"] }
+ed25519-dalek = { version = "2.1" }
 hex = "0.4.3"
-rand = "0.8.5"
-x25519-dalek = "2.0.1"
+rand = "0.9.1"
+x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
modifiedcmds/generator-helper/src/main.rsdiffbeforeafterboth
--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -11,10 +11,11 @@
 };
 use anyhow::{anyhow, bail, ensure, Context, Result};
 use clap::{Parser, ValueEnum};
+use ed25519_dalek::SecretKey;
 use fleet_shared::SecretData;
 use rand::{
-	distributions::{Alphanumeric, DistString, Distribution, Uniform},
-	thread_rng, RngCore,
+	distr::{Alphanumeric, Distribution, SampleString, Uniform},
+	rng, RngCore,
 };
 
 fn write_output_file(out: &str) -> Result<File> {
@@ -224,7 +225,7 @@
 fn main() -> Result<()> {
 	let opts = Opts::parse();
 	// Assumed to be secure, seeded from secure OsRng+reseeded.
-	let mut rng = thread_rng();
+	let mut rng = rng();
 
 	match opts {
 		Opts::Public { output, encoding } => {
@@ -245,7 +246,10 @@
 					use ed25519_dalek::SigningKey;
 
 					let recipients = load_identities()?;
-					let key = SigningKey::generate(&mut rng).to_keypair_bytes();
+					let mut secret = SecretKey::default();
+					rng.fill_bytes(&mut secret);
+					// TODO: Use SigningKey::generate after https://github.com/dalek-cryptography/curve25519-dalek/pull/762
+					let key = SigningKey::from_bytes(&secret).to_keypair_bytes();
 					write_public(&public, &key[32..], encoding)?;
 					write_private(
 						&recipients,
@@ -268,7 +272,8 @@
 					use x25519_dalek::{PublicKey, StaticSecret};
 
 					let recipients = load_identities()?;
-					let key = StaticSecret::random_from_rng(rng);
+					// TODO: Use random_from_rng after https://github.com/dalek-cryptography/curve25519-dalek/pull/762
+					let key = StaticSecret::random();
 					let public_key: PublicKey = (&key).into();
 					write_public(&public, public_key.as_bytes().as_slice(), encoding)?;
 					write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;
@@ -289,7 +294,8 @@
 					} else {
 						// Alphabet of Alphanumberic + symbols
 						const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
-						let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());
+						let uniform =
+							Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");
 						(0..size)
 							.map(|_| uniform.sample(&mut rng))
 							.map(|i| GEN_ASCII_SYMBOLS[i] as char)
@@ -310,7 +316,9 @@
 					let recipients = load_identities()?;
 					let mut bytes = vec![0u8; count];
 					if no_nuls {
-						let rand = Uniform::new_inclusive(0x1u8, 0xffu8).sample_iter(&mut rng);
+						let rand = Uniform::new_inclusive(0x1u8, 0xffu8)
+							.expect("range is valid")
+							.sample_iter(&mut rng);
 						for (byte, rand) in bytes.iter_mut().zip(rand) {
 							*byte = rand;
 						}
modifiedcmds/terraform-provider-fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/terraform-provider-fleet/Cargo.toml
+++ b/cmds/terraform-provider-fleet/Cargo.toml
@@ -9,5 +9,5 @@
 serde = { workspace = true, features = ["derive"] }
 tokio.workspace = true
 
-async-trait = "0.1.81"
+async-trait = "0.1.88"
 tf-provider = "0.2.2"
modifiedcrates/better-command/Cargo.tomldiffbeforeafterboth
--- a/crates/better-command/Cargo.toml
+++ b/crates/better-command/Cargo.toml
@@ -5,7 +5,7 @@
 rust-version.workspace = true
 
 [dependencies]
-regex = "1.10"
+regex = "1.11"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 tracing = "0.1"
modifiedcrates/fleet-base/Cargo.tomldiffbeforeafterboth
--- a/crates/fleet-base/Cargo.toml
+++ b/crates/fleet-base/Cargo.toml
@@ -8,21 +8,23 @@
 age.workspace = true
 anyhow.workspace = true
 better-command.workspace = true
-chrono = "0.4.38"
+chrono = "0.4.41"
 clap = { workspace = true, features = ["derive"] }
 fleet-shared.workspace = true
-futures = "0.3.30"
-hostname = "0.4.0"
+futures = "0.3.31"
+hostname = "0.4.1"
 indoc = "2.0.6"
-itertools = "0.13.0"
+itertools = "0.14.0"
 nix-eval.workspace = true
 nixlike.workspace = true
-nom = "7.1.3"
-openssh = "0.11.0"
-rand = "0.8.5"
+nom = "8.0.0"
+openssh = "0.11.5"
+rand = "0.9.1"
 serde.workspace = true
-serde_json = "1.0.127"
+serde_json = "1.0.140"
+tabled = "0.20.0"
 tempfile.workspace = true
+time = { version = "0.3.41", features = ["parsing"] }
 tokio.workspace = true
-tokio-util = "0.7.11"
+tokio-util = "0.7.15"
 tracing.workspace = true
addedcrates/fleet-base/src/deploy.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/fleet-base/src/deploy.rs
@@ -0,0 +1,297 @@
+use std::{path::PathBuf, time::Duration};
+
+use anyhow::{anyhow, bail, Context as _, Result};
+use clap::ValueEnum;
+use itertools::Itertools;
+use tokio::time::sleep;
+use tracing::{error, info, info_span, warn, Instrument as _};
+
+use crate::host::{Config, ConfigHost, DeployKind, Generation, GenerationStorage};
+
+#[derive(ValueEnum, Clone, Copy)]
+pub enum DeployAction {
+	/// Upload derivation, but do not execute the update.
+	Upload,
+	/// Upload and execute the activation script, old version will be used after reboot.
+	Test,
+	/// Upload and set as current system profile, but do not execute activation script.
+	Boot,
+	/// Upload, set current profile, and execute activation script.
+	Switch,
+}
+
+impl DeployAction {
+	pub(crate) fn name(&self) -> Option<&'static str> {
+		match self {
+			Self::Upload => None,
+			Self::Test => Some("test"),
+			Self::Boot => Some("boot"),
+			Self::Switch => Some("switch"),
+		}
+	}
+	pub(crate) fn should_switch_profile(&self) -> bool {
+		matches!(self, Self::Switch | Self::Boot)
+	}
+	pub(crate) fn should_activate(&self) -> bool {
+		matches!(self, Self::Switch | Self::Test | Self::Boot)
+	}
+	pub(crate) fn should_create_rollback_marker(&self) -> bool {
+		// Upload does nothing on the target machine, other than uploading the closure.
+		// In boot case we want to have rollback marker prepared, so that the system may rollback itself on the next boot.
+		!matches!(self, Self::Upload)
+	}
+	pub(crate) fn should_schedule_rollback_run(&self) -> bool {
+		matches!(self, Self::Switch | Self::Test)
+	}
+}
+
+async fn get_current_generation(host: &ConfigHost) -> Result<Generation> {
+	let generations = host.list_generations("system").await?;
+	let current = generations
+		.into_iter()
+		.filter(|g| g.current)
+		.at_most_one()
+		.map_err(|_e| anyhow!("bad list-generations output"))?
+		.ok_or_else(|| anyhow!("failed to find generation"))?;
+	Ok(current)
+}
+
+pub async fn deploy_task(
+	action: DeployAction,
+	host: &ConfigHost,
+	built: PathBuf,
+	specialisation: Option<String>,
+	disable_rollback: bool,
+) -> Result<()> {
+	let deploy_kind = host.deploy_kind().await?;
+	if (deploy_kind == DeployKind::NixosInstall || deploy_kind == DeployKind::NixosLustrate)
+		&& !matches!(action, DeployAction::Boot | DeployAction::Upload)
+	{
+		bail!("{deploy_kind:?} deploy kind only supports boot and upload actions");
+	}
+
+	let mut failed = false;
+
+	// TODO: Lockfile, to prevent concurrent system switch?
+	// TODO: If rollback target exists - bail, it should be removed. Lockfile will not work in case if rollback
+	// is scheduler on next boot (default behavior). On current boot - rollback activator will fail due to
+	// unit name conflict in systemd-run
+	// This code is tied to rollback.nix
+	if !disable_rollback && action.should_create_rollback_marker() {
+		let _span = info_span!("preparing").entered();
+		info!("preparing for rollback");
+		let generation = get_current_generation(host).await?;
+		info!(
+			"rollback target would be {} {}",
+			generation.id, generation.datetime
+		);
+		{
+			let mut cmd = host.cmd("sh").await?;
+			cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));
+			if let Err(e) = cmd.sudo().run().await {
+				error!("failed to set rollback marker: {e}");
+				failed = true;
+			}
+		}
+		// Activation script also starts rollback-watchdog.timer, however, it is possible that it won't be started.
+		// Kicking it on manually will work best.
+		//
+		// There wouldn't be conflict, because here we trigger start of the primary service, and systemd will
+		// only allow one instance of it.
+
+		// TODO: We should also watch how this process is going.
+		// After running this command, we have less than 3 minutes to deploy everything,
+		// if we fail to perform generation switch in time, then we will still call the activation script, and this may break something.
+		// Anyway, reboot will still help in this case.
+		if action.should_schedule_rollback_run() {
+			let mut cmd = host.cmd("systemd-run").await?;
+			cmd.comparg("--on-active", "3min")
+				.comparg("--unit", "rollback-watchdog-run")
+				.arg("systemctl")
+				.arg("start")
+				.arg("rollback-watchdog.service");
+			if let Err(e) = cmd.sudo().run().await {
+				error!("failed to schedule rollback run: {e}");
+				failed = true;
+			}
+		}
+	}
+	if deploy_kind == DeployKind::NixosLustrate {
+		// Fleet could also create this file, but as this operation is potentially disruptive,
+		// make user do it themself.
+		if !host.file_exists("/etc/NIXOS_LUSTRATE").await? {
+			bail!("/etc/NIXOS_LUSTRATE should be created on remote host");
+		}
+		// Wanted by NixOS to recognize the system as NixOS.
+		let mut cmd = host.cmd("touch").await?;
+		cmd.arg("/etc/NIXOS");
+		cmd.sudo().run().await.context("creating /etc/NIXOS")?;
+	}
+	if deploy_kind == DeployKind::NixosInstall {
+		info!(
+			"running nixos-install to switch profile, install bootloader, and perform activation"
+		);
+		let mut cmd = host.cmd("nixos-install").await?;
+		cmd.arg("--system").arg(&built).args([
+			// Channels here aren't fleet host system channels, but channels embedded in installation cd, which might be old.
+			// It is possible to copy host channels, but I would prefer non-flake nix just to be unsupported.
+			"--no-channel-copy",
+			"--root",
+			"/mnt",
+		]);
+		if let Err(e) = cmd.sudo().run().await {
+			error!("failed to execute nixos-install: {e}");
+			failed = true;
+		}
+	} else {
+		if action.should_switch_profile() && !failed {
+			info!("switching system profile generation");
+
+			// To avoid even more problems, using nixos-install for now.
+			// // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
+			// // falling back to using nix-env command
+			// // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
+			// let mut cmd = host.cmd("nix-env").await?;
+			// cmd.args([
+			// 	"--store",
+			// 	"/mnt",
+			// 	"--profile",
+			// 	"/mnt/nix/var/nix/profiles/system",
+			// 	"--set",
+			// ])
+			// .arg(&built);
+			// if let Err(e) = cmd.sudo().run_nix().await {
+			// 	error!("failed to switch system profile generation: {e}");
+			// 	failed = true;
+			// }
+			// It would also be possible to update profile atomically during copy:
+			// https://github.com/NixOS/nix/pull/11657
+			let mut cmd = host.nix_cmd().await?;
+			cmd.arg("build");
+			cmd.comparg("--profile", "/nix/var/nix/profiles/system");
+			cmd.arg(&built);
+			if let Err(e) = cmd.sudo().run_nix().await {
+				error!("failed to switch system profile generation: {e}");
+				failed = true;
+			}
+		}
+
+		// FIXME: Connection might be disconnected after activation run
+
+		if action.should_activate() && !failed {
+			let _span = info_span!("activating").entered();
+			info!("executing activation script");
+			let specialised = if let Some(specialisation) = specialisation {
+				let mut specialised = built.join("specialisation");
+				specialised.push(specialisation);
+				specialised
+			} else {
+				built.clone()
+			};
+			let switch_script = specialised.join("bin/switch-to-configuration");
+			let mut cmd = host.cmd(switch_script).in_current_span().await?;
+			if deploy_kind == DeployKind::NixosLustrate {
+				cmd.env("NIXOS_INSTALL_BOOTLOADER", "1");
+			}
+			cmd.env("FLEET_ONLINE_ACTIVATION", "1")
+				.arg(action.name().expect("upload.should_activate == false"));
+			if let Err(e) = cmd.sudo().run().in_current_span().await {
+				error!("failed to activate: {e}");
+				failed = true;
+			}
+		}
+	}
+	if action.should_create_rollback_marker() {
+		if !disable_rollback {
+			if failed {
+				if action.should_schedule_rollback_run() {
+					info!("executing rollback");
+					if let Err(e) = host
+						.systemctl_start("rollback-watchdog.service")
+						.instrument(info_span!("rollback"))
+						.await
+					{
+						error!("failed to trigger rollback: {e}")
+					}
+				}
+			} else {
+				info!("trying to mark upgrade as successful");
+				if let Err(e) = host
+					.rm_file("/etc/fleet_rollback_marker", true)
+					.in_current_span()
+					.await
+				{
+					error!("failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}")
+				}
+			}
+			info!("disarming watchdog, just in case");
+			if let Err(_e) = host.systemctl_stop("rollback-watchdog.timer").await {
+				// It is ok, if there was no reboot - then timer might not be running.
+			}
+			if action.should_schedule_rollback_run() {
+				if let Err(e) = host.systemctl_stop("rollback-watchdog-run.timer").await {
+					error!("failed to disarm rollback run: {e}");
+				}
+			}
+		} else if let Err(_e) = host
+			.rm_file("/etc/fleet_rollback_marker", true)
+			.in_current_span()
+			.await
+		{
+			// Marker might not exist, yet better try to remove it.
+		}
+	}
+	Ok(())
+}
+
+pub async fn upload_task(
+	config: &Config,
+	host: &ConfigHost,
+	location: GenerationStorage,
+	generation: PathBuf,
+) -> Result<PathBuf> {
+	let local_host = config.local_host();
+	if matches!(location, GenerationStorage::Pusher) {
+		bail!("pusher is not enabled in this version of fleet");
+	}
+	if !host.local {
+		info!("uploading system closure");
+		{
+			// TODO: Move to remote_derivation method.
+			// Alternatively, nix store make-content-addressed can be used,
+			// at least for the first deployment, to provide trusted store key.
+			//
+			// It is much slower, yet doesn't require root on the deployer machine.
+			let Ok(mut sign) = local_host.cmd("nix").await else {
+				bail!("failed to setup local");
+			};
+			// Private key for host machine is registered in nix-sign.nix
+			sign.arg("store")
+				.arg("sign")
+				.comparg("--key-file", "/etc/nix/private-key")
+				.arg("-r")
+				.arg(&generation);
+			if let Err(e) = sign.sudo().run_nix().await {
+				warn!("failed to sign store paths: {e}");
+			};
+		}
+		let mut tries = 0;
+		loop {
+			match host.remote_derivation(&generation).await {
+				Ok(remote) => {
+					assert!(remote == generation, "CA derivations aren't implemented");
+					return Ok(remote);
+				}
+				Err(e) if tries < 3 => {
+					tries += 1;
+					warn!("copy failure ({}/3): {}", tries, e);
+					sleep(Duration::from_millis(5000)).await;
+				}
+				Err(e) => {
+					bail!("upload failed: {e}");
+				}
+			}
+		}
+	}
+	Ok(generation)
+}
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -7,8 +7,8 @@
 use chrono::{DateTime, Utc};
 use fleet_shared::SecretData;
 use rand::{
-	distributions::{Alphanumeric, DistString},
-	thread_rng,
+	distr::{Alphanumeric, SampleString as _},
+	rng,
 };
 use serde::{de::Error, Deserialize, Serialize};
 use serde_json::Value;
@@ -47,7 +47,7 @@
 }
 
 fn generate_gc_prefix() -> String {
-	let id = Alphanumeric.sample_string(&mut thread_rng(), 8);
+	let id = Alphanumeric.sample_string(&mut rng(), 8);
 	format!("fleet-gc-{id}")
 }
 
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
before · crates/fleet-base/src/host.rs
1use std::{2	cell::OnceCell,3	collections::BTreeSet,4	ffi::{OsStr, OsString},5	fmt::Display,6	io::Write,7	ops::Deref,8	path::PathBuf,9	str::FromStr,10	sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{anyhow, bail, ensure, Context, Result};14use fleet_shared::SecretData;15use nix_eval::{nix_go, nix_go_json, util::assert_warn, NixSession, Value};16use openssh::SessionBuilder;17use serde::de::DeserializeOwned;18use tempfile::NamedTempFile;1920use crate::{21	command::MyCommand,22	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},23};2425pub struct FleetConfigInternals {26	/// Fleet project directory, containing fleet.nix file.27	pub directory: PathBuf,28	/// builtins.currentSystem29	pub local_system: String,30	pub data: Mutex<FleetData>,31	pub nix_args: Vec<OsString>,32	/// fleet_config.config33	pub config_field: Value,34	// TODO: Remove with connectivity refactor35	pub localhost: String,3637	/// import nixpkgs {system = local};38	pub default_pkgs: Value,39	/// inputs.nixpkgs40	pub nixpkgs: Value,4142	pub nix_session: NixSession,43}4445// TODO: Make field not pub46#[derive(Clone)]47pub struct Config(pub Arc<FleetConfigInternals>);4849impl Deref for Config {50	type Target = FleetConfigInternals;5152	fn deref(&self) -> &Self::Target {53		&self.054	}55}5657#[derive(Clone, Copy, Debug)]58pub enum EscalationStrategy {59	Sudo,60	Run0,61	Su,62}6364#[derive(Clone, PartialEq, Copy, Debug)]65pub enum DeployKind {66	/// NixOS => NixOS managed by fleet67	UpgradeToFleet,68	/// NixOS managed by fleet => NixOS managed by fleet69	Fleet,70	/// Remote host has /mnt, /mnt/boot mounted,71	/// generated config is added to fleet configuration.72	NixosInstall,73	/// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),74	/// generated config is added to fleet configuration,75	/// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.76	NixosLustrate,77}7879impl FromStr for DeployKind {80	type Err = anyhow::Error;81	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {82		match s {83			"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),84			"fleet" => Ok(Self::Fleet),85			"nixos-install" => Ok(Self::NixosInstall),86			"nixos-lustrate" => Ok(Self::NixosLustrate),87			v => bail!("unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""),88		}89	}90}91pub struct ConfigHost {92	config: Config,93	pub name: String,94	groups: OnceCell<Vec<String>>,9596	deploy_kind: OnceCell<DeployKind>,9798	pub host_config: Option<Value>,99	pub nixos_config: OnceCell<Value>,100	pub nixos_unchecked_config: OnceCell<Value>,101	pub pkgs_override: Option<Value>,102103	// TODO: Move command helpers away with connectivity refactor104	pub local: bool,105	pub session: OnceLock<Arc<openssh::Session>>,106}107// TODO: Move command helpers away with connectivity refactor108impl ConfigHost {109	pub fn set_deploy_kind(&self, kind: DeployKind) {110		self.deploy_kind111			.set(kind)112			.ok()113			.expect("deploy kind is already set");114	}115	pub async fn deploy_kind(&self) -> Result<DeployKind> {116		if let Some(kind) = self.deploy_kind.get() {117			return Ok(kind.clone());118		}119		let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {120			Ok(v) => v,121			Err(e) => {122				bail!("failed to query remote system kind: {}", e);123			}124		};125		if !is_fleet_managed {126			bail!(indoc::indoc! {"127				host is not marked as managed by fleet128				if you're not trying to lustrate/install system from scratch,129				you should either130					1. manually create /etc/FLEET_HOST file on the target host,131					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet132					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos133			"});134		}135		// TOCTOU is possible136		let _ = self.deploy_kind.set(DeployKind::Fleet);137		Ok(self138			.deploy_kind139			.get()140			.expect("deploy kind is just set")141			.clone())142	}143	pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {144		// Prefer sudo, as run0 has some gotchas with polkit145		// and too many repeating prompts.146		if (self.find_in_path("sudo").await).is_ok() {147			return Ok(EscalationStrategy::Sudo);148		}149		if (self.find_in_path("run0").await).is_ok() {150			return Ok(EscalationStrategy::Run0);151		}152		Ok(EscalationStrategy::Su)153	}154	async fn open_session(&self) -> Result<Arc<openssh::Session>> {155		assert!(!self.local, "do not open ssh connection to local session");156		// FIXME: TOCTOU157		if let Some(session) = &self.session.get() {158			return Ok((*session).clone());159		};160		let session = SessionBuilder::default();161		let session = session162			.connect(&self.name)163			.await164			.map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;165		let session = Arc::new(session);166		self.session.set(session.clone()).expect("TOCTOU happened");167		Ok(session)168	}169	pub async fn mktemp_dir(&self) -> Result<String> {170		let mut cmd = self.cmd("mktemp").await?;171		cmd.arg("-d");172		let path = cmd.run_string().await?;173		Ok(path.trim_end().to_owned())174	}175	pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {176		let mut cmd = self.cmd("sh").await?;177		cmd.arg("-c")178			.arg("test -e \"$1\" && echo true || echo false")179			.arg("_")180			.arg(path);181		Ok(cmd.run_value().await?)182	}183	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {184		let mut cmd = self.cmd("cat").await?;185		cmd.arg(path);186		cmd.run_bytes().await187	}188	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {189		let mut cmd = self.cmd("cat").await?;190		cmd.arg(path);191		cmd.run_string().await192	}193	pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {194		let mut cmd = self.cmd("ls").await?;195		cmd.arg(path);196		let out = cmd.run_string().await?;197		let mut lines = out.split('\n');198		if let Some(last) = lines.next_back() {199			ensure!(last.is_empty(), "output of ls should end with newline");200		}201		Ok(lines.map(ToOwned::to_owned).collect())202	}203	#[allow(dead_code)]204	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {205		let text = self.read_file_text(path).await?;206		Ok(serde_json::from_str(&text)?)207	}208	pub async fn read_env(&self, env: &str) -> Result<String> {209		let mut cmd = self.cmd("printenv").await?;210		cmd.arg(env);211		cmd.run_string().await212	}213	pub async fn find_in_path(&self, command: &str) -> Result<String> {214		// // `which` is not a part of coreutils, and it might not exist on machine.215		// let path = self.read_env("PATH").await?;216		// // Assuming delimiter is :, we don't work with windows host, this check will be much217		// // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)218		// for ele in path.split(':') {219		// 	let test_path = format!("{ele}/{cmd}");220		// 	test -x etc221		// }222		// let mut cmd = self.cmd("printenv").await?;223		// cmd.arg(env);224		// Ok(cmd.run_string().await?)225		// Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.226		let mut cmd = self227			.cmd_escalation(228				// Not used229				EscalationStrategy::Su,230				"which",231			)232			.await?;233		cmd.arg(command);234		cmd.run_string().await235	}236	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>237	where238		<D as FromStr>::Err: Display,239	{240		let text = self.read_file_text(path).await?;241		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))242	}243	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {244		self.cmd_escalation(self.escalation_strategy().await?, cmd)245			.await246	}247	pub async fn cmd_escalation(248		&self,249		escalation: EscalationStrategy,250		cmd: impl AsRef<OsStr>,251	) -> Result<MyCommand> {252		if self.local {253			Ok(MyCommand::new(escalation, cmd))254		} else {255			let session = self.open_session().await?;256			Ok(MyCommand::new_on(escalation, cmd, session))257		}258	}259	pub async fn nix_cmd(&self) -> Result<MyCommand> {260		let mut nix = self.cmd("nix").await?;261		nix.args([262			"--extra-experimental-features",263			"nix-command",264			"--extra-experimental-features",265			"flakes",266		]);267		Ok(nix)268	}269270	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {271		ensure!(data.encrypted, "secret is not encrypted");272		let mut cmd = self.cmd("fleet-install-secrets").await?;273		cmd.arg("decrypt").eqarg("--secret", data.to_string());274		let encoded = cmd275			.sudo()276			.run_string()277			.await278			.context("failed to call remote host for decrypt")?;279		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;280		ensure!(!data.encrypted, "secret came out encrypted");281		Ok(data.data)282	}283	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {284		ensure!(data.encrypted, "secret is not encrypted");285		let mut cmd = self.cmd("fleet-install-secrets").await?;286		cmd.arg("reencrypt").eqarg("--secret", data.to_string());287		for target in targets {288			let key = self.config.key(&target).await?;289			cmd.eqarg("--targets", key);290		}291		let encoded = cmd292			.sudo()293			.run_string()294			.await295			.context("failed to call remote host for decrypt")?;296		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;297		ensure!(data.encrypted, "secret came out not encrypted");298		Ok(data)299	}300	/// Returns path for futureproofing, as path might change i.e on conversion to CA301	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {302		if self.local {303			// Path is located locally, thus already trusted.304			return Ok(path.to_owned());305		}306		let mut nix = MyCommand::new(307			// Not used308			EscalationStrategy::Su,309			"nix",310		);311		nix.arg("copy").arg("--substitute-on-destination");312313		match self.deploy_kind().await? {314			DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {315				nix.comparg("--to", format!("ssh-ng://{}", self.name));316			}317			DeployKind::NixosInstall => {318				nix319					// Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon320					.arg("--no-check-sigs")321					.comparg(322						"--to",323						format!("ssh-ng://root@{}?remote-store=/mnt", self.name),324					);325			}326		}327		nix.arg(path);328		nix.run_nix().await.context("nix copy")?;329		Ok(path.to_owned())330	}331	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {332		let mut cmd = self.cmd("systemctl").await?;333		cmd.arg("stop").arg(name);334		cmd.sudo().run().await335	}336	pub async fn systemctl_start(&self, name: &str) -> Result<()> {337		let mut cmd = self.cmd("systemctl").await?;338		cmd.arg("start").arg(name);339		cmd.sudo().run().await340	}341342	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {343		let mut cmd = self.cmd("rm").await?;344		cmd.arg("-f").arg(path);345		if sudo {346			cmd = cmd.sudo()347		}348		cmd.run().await349	}350}351impl ConfigHost {352	// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,353	// assuming getting tags always returns the same value.354	pub async fn tags(&self) -> Result<Vec<String>> {355		if let Some(v) = self.groups.get() {356			return Ok(v.clone());357		}358		let Some(host_config) = &self.host_config else {359			return Ok(vec![]);360		};361		let tags: Vec<String> = nix_go_json!(host_config.tags);362363		let _ = self.groups.set(tags.clone());364365		Ok(tags)366	}367	pub async fn nixos_config(&self) -> Result<Value> {368		if let Some(v) = self.nixos_config.get() {369			return Ok(v.clone());370		}371		let Some(host_config) = &self.host_config else {372			bail!("local host has no nixos_config");373		};374		let nixos_config = nix_go!(host_config.nixos.config);375		assert_warn("nixos config evaluation", &nixos_config).await?;376377		let _ = self.nixos_config.set(nixos_config.clone());378379		Ok(nixos_config)380	}381	pub async fn nixos_unchecked_config(&self) -> Result<Value> {382		if let Some(v) = self.nixos_unchecked_config.get() {383			return Ok(v.clone());384		}385		let Some(host_config) = &self.host_config else {386			bail!("local host has no nixos_config");387		};388		let nixos_config = nix_go!(host_config.nixos_unchecked.config);389390		let _ = self.nixos_unchecked_config.set(nixos_config.clone());391392		Ok(nixos_config)393	}394395	pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {396		let nixos = self.nixos_unchecked_config().await?;397		let secrets = nix_go!(nixos.secrets);398		let mut out = Vec::new();399		for name in secrets.list_fields().await? {400			let secret = nix_go!(secrets[{ name }]);401			let is_shared: bool = nix_go_json!(secret.shared);402			if is_shared {403				continue;404			}405			out.push(name);406		}407		Ok(out)408	}409	pub async fn secret_field(&self, name: &str) -> Result<Value> {410		let nixos = self.nixos_unchecked_config().await?;411		Ok(nix_go!(nixos.secrets[{ name }]))412	}413414	/// Packages for this host, resolved with nixpkgs overlays415	pub async fn pkgs(&self) -> Result<Value> {416		if let Some(value) = &self.pkgs_override {417			return Ok(value.clone());418		}419		let Some(host_config) = &self.host_config else {420			bail!("local host has no host_config");421		};422		// TODO: Should nixos.options be cached?423		Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))424	}425}426427impl Config {428	pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {429		let config = &self.config_field;430		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);431		Ok(tagged)432	}433	pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {434		let mut out = BTreeSet::new();435		for owner in owners {436			if let Some(tag) = owner.strip_prefix('@') {437				let hosts = self.tagged_hostnames(tag).await?;438				out.extend(hosts);439			} else {440				out.insert(owner);441			}442		}443		Ok(out)444	}445	pub fn local_host(&self) -> ConfigHost {446		ConfigHost {447			config: self.clone(),448			name: "<virtual localhost>".to_owned(),449			host_config: None,450			nixos_config: OnceCell::new(),451			nixos_unchecked_config: OnceCell::new(),452			groups: {453				let cell = OnceCell::new();454				let _ = cell.set(vec![]);455				cell456			},457			pkgs_override: Some(self.default_pkgs.clone()),458459			local: true,460			session: OnceLock::new(),461			deploy_kind: OnceCell::new(),462		}463	}464465	pub async fn host(&self, name: &str) -> Result<ConfigHost> {466		let config = &self.config_field;467		let host_config = nix_go!(config.hosts[{ name }]);468469		Ok(ConfigHost {470			config: self.clone(),471			name: name.to_owned(),472			host_config: Some(host_config),473			nixos_config: OnceCell::new(),474			nixos_unchecked_config: OnceCell::new(),475			groups: OnceCell::new(),476			pkgs_override: None,477478			// TODO: Remove with connectivit refactor479			local: self.localhost == name,480			session: OnceLock::new(),481			deploy_kind: OnceCell::new(),482		})483	}484	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {485		let config = &self.config_field;486		let names = nix_go!(config.hosts).list_fields().await?;487		let mut out = vec![];488		for name in names {489			out.push(self.host(&name).await?);490		}491		Ok(out)492	}493	// TODO: Replace usages with .host().nixos_config494	pub async fn system_config(&self, host: &str) -> Result<Value> {495		let fleet_field = &self.config_field;496		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))497	}498499	/// Shared secrets configured in fleet.nix or in flake500	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {501		let config_field = &self.config_field;502		Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)503	}504	/// Shared secrets configured in fleet.nix505	pub fn list_shared(&self) -> Vec<String> {506		let data = self.data();507		data.shared_secrets.keys().cloned().collect()508	}509	pub fn has_shared(&self, name: &str) -> bool {510		let data = self.data();511		data.shared_secrets.contains_key(name)512	}513	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {514		let mut data = self.data_mut();515		data.shared_secrets.insert(name.to_owned(), shared);516	}517	pub fn remove_shared(&self, secret: &str) {518		let mut data = self.data_mut();519		data.shared_secrets.remove(secret);520	}521522	pub fn list_secrets(&self, host: &str) -> Vec<String> {523		let data = self.data();524		let Some(secrets) = data.host_secrets.get(host) else {525			return Vec::new();526		};527		secrets.keys().cloned().collect()528	}529530	pub fn has_secret(&self, host: &str, secret: &str) -> bool {531		let data = self.data();532		let Some(host_secrets) = data.host_secrets.get(host) else {533			return false;534		};535		host_secrets.contains_key(secret)536	}537	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {538		let mut data = self.data_mut();539		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();540		host_secrets.insert(secret, value);541	}542543	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {544		let data = self.data();545		let Some(host_secrets) = data.host_secrets.get(host) else {546			bail!("no secrets for machine {host}");547		};548		let Some(secret) = host_secrets.get(secret) else {549			bail!("machine {host} has no secret {secret}");550		};551		Ok(secret.clone())552	}553	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {554		let data = self.data();555		let Some(secret) = data.shared_secrets.get(secret) else {556			bail!("no shared secret {secret}");557		};558		Ok(secret.clone())559	}560	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {561		let config_field = &self.config_field;562		Ok(nix_go_json!(563			config_field.sharedSecrets[{ secret }].expectedOwners564		))565	}566567	// TODO: Should this be something modifiable from other processes?568	// E.g terraform provider might want to update FleetData (e.g secrets),569	// and current implementation assumes only one process holds current fleet.nix570	// Given that it is no longer needs to be a file for nix evaluation,571	// maybe it can be a .nix file for persistence, but accessible only572	// thru some shared state controller? Might it be stored in terraform573	// state provider?574	pub fn data(&self) -> MutexGuard<FleetData> {575		self.data.lock().unwrap()576	}577	pub fn data_mut(&self) -> MutexGuard<FleetData> {578		self.data.lock().unwrap()579	}580	pub fn save(&self) -> Result<()> {581		let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;582		let data = nixlike::serialize(&self.data() as &FleetData)?;583		tempfile.write_all(584			format!(585				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",586				data587			)588			.as_bytes(),589		)?;590		let mut fleet_data_path = self.directory.clone();591		fleet_data_path.push("fleet.nix");592		tempfile.persist(fleet_data_path)?;593		Ok(())594	}595}
modifiedcrates/fleet-base/src/lib.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/lib.rs
+++ b/crates/fleet-base/src/lib.rs
@@ -3,3 +3,4 @@
 pub mod host;
 mod keys;
 pub mod opts;
+pub mod deploy;
\ No newline at end of file
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -7,7 +7,6 @@
 };
 
 use anyhow::{bail, Context, Result};
-use clap::Parser;
 use nix_eval::{nix_go, util::assert_warn, NixSessionPool, Value};
 use nom::{
 	bytes::complete::take_while1,
@@ -15,6 +14,7 @@
 	combinator::{map, opt},
 	multi::separated_list1,
 	sequence::{preceded, separated_pair},
+	Parser,
 };
 
 use crate::{
@@ -38,11 +38,13 @@
 		err.to_string()
 	}
 
-	let (input, is_tag) = map(opt(char('@')), |c| c.is_some())(input).map_err(err_to_string)?;
+	let (input, is_tag) = map(opt(char('@')), |c| c.is_some())
+		.parse_complete(input)
+		.map_err(err_to_string)?;
 	let (input, name) = map(
 		take_while1(|v| v != ',' && v != '?' && v != '@'),
 		str::to_owned,
-	)(input)
+	).parse_complete(input)
 	.map_err(err_to_string)?;
 
 	let kw_item = separated_pair(
@@ -55,7 +57,7 @@
 	});
 	let mut opt_kw = map(opt(preceded(char('?'), kw)), Option::unwrap_or_default);
 
-	let (input, attrs) = opt_kw(input).map_err(err_to_string)?;
+	let (input, attrs) = opt_kw.parse_complete(input).map_err(err_to_string)?;
 
 	if !input.is_empty() {
 		return Err(format!("unexpected trailing input: {input:?}"));
@@ -68,7 +70,7 @@
 }
 
 // TODO: Rename to HostSelector
-#[derive(Parser, Clone)]
+#[derive(clap::Parser, Clone)]
 pub struct FleetOpts {
 	/// All hosts except those would be skipped
 	#[clap(long, number_of_values = 1, value_parser = host_item_parser)]
modifiedcrates/fleet-shared/Cargo.tomldiffbeforeafterboth
--- a/crates/fleet-shared/Cargo.toml
+++ b/crates/fleet-shared/Cargo.toml
@@ -6,6 +6,6 @@
 
 [dependencies]
 base64 = "0.22.1"
-serde = "1.0.202"
+serde = "1.0.219"
 unicode_categories = "0.1.1"
-z85 = "3.0.5"
+z85 = "3.0.6"
modifiedcrates/nix-eval/Cargo.tomldiffbeforeafterboth
--- a/crates/nix-eval/Cargo.toml
+++ b/crates/nix-eval/Cargo.toml
@@ -16,11 +16,11 @@
 tokio-util.workspace = true
 tracing.workspace = true
 
-futures = "0.3.30"
-itertools = "0.13.0"
+futures = "0.3.31"
+itertools = "0.14.0"
 r2d2 = "0.8.10"
-regex = "1.10.6"
-unindent = "0.2.3"
+regex = "1.11.1"
+unindent = "0.2.4"
 
 # [build-dependencies]
 # bindgen = "0.69.4"
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -9,8 +9,8 @@
 
 alejandra = { git = "https://github.com/kamadorueda/alejandra" }
 linked-hash-map = "0.5.6"
-peg = "0.8.2"
-ron = "0.8.1"
-serde = "1.0.196"
+peg = "0.8.5"
+ron = "0.10.1"
+serde = "1.0.219"
 serde-transcode = "1.1.1"
-serde_json = "1.0.113"
+serde_json = "1.0.140"