1use std::time::Duration;23use anyhow::{Context as _, Result, anyhow, bail};4use camino::Utf8PathBuf;5use clap::ValueEnum;6use itertools::Itertools;7use remowt_endpoints::fs::FsClient;8use remowt_endpoints::systemd::SystemdClient;9use remowt_fleet::NixClient;10use remowt_link_shared::BifConfig;11use tokio::time::sleep;12use tracing::{Instrument as _, error, info, info_span, warn};1314use crate::host::{ConfigHost, DeployKind};15use crate::pins::{Generation, GenerationStorage};1617#[derive(ValueEnum, Clone, Copy)]18pub enum DeployAction {19 20 Upload,21 22 Test,23 24 Boot,25 26 Switch,27}2829impl DeployAction {30 pub(crate) fn name(&self) -> Option<&'static str> {31 match self {32 Self::Upload => None,33 Self::Test => Some("test"),34 Self::Boot => Some("boot"),35 Self::Switch => Some("switch"),36 }37 }38 pub(crate) fn should_switch_profile(&self) -> bool {39 matches!(self, Self::Switch | Self::Boot)40 }41 pub(crate) fn should_activate(&self) -> bool {42 matches!(self, Self::Switch | Self::Test | Self::Boot)43 }44 pub(crate) fn should_create_rollback_marker(&self) -> bool {45 46 47 !matches!(self, Self::Upload)48 }49 pub(crate) fn should_schedule_rollback_run(&self) -> bool {50 matches!(self, Self::Switch | Self::Test)51 }52}5354async fn get_current_generation(host: &ConfigHost) -> Result<Generation> {55 let generations = host.list_generations("system").await?;56 let current = generations57 .into_iter()58 .filter(|g| g.current)59 .at_most_one()60 .map_err(|_e| anyhow!("bad list-generations output"))?61 .ok_or_else(|| anyhow!("failed to find generation"))?;62 Ok(current)63}6465pub async fn deploy_task(66 action: DeployAction,67 host: &ConfigHost,68 built: Utf8PathBuf,69 specialisation: Option<String>,70 disable_rollback: bool,71) -> Result<()> {72 let remowt = host.remowt().await?;73 let deploy_kind = host.deploy_kind().await?;74 if (deploy_kind == DeployKind::NixosInstall || deploy_kind == DeployKind::NixosLustrate)75 && !matches!(action, DeployAction::Boot | DeployAction::Upload)76 {77 bail!("{deploy_kind:?} deploy kind only supports boot and upload actions");78 }7980 let mut failed = false;8182 83 84 85 86 87 if !disable_rollback && action.should_create_rollback_marker() {88 89 info!("preparing for rollback");90 let generation = get_current_generation(host).await?;91 info!(92 "rollback target would be {} {}",93 generation.id, generation.datetime94 );95 {96 let mut cmd = remowt.cmd("sh");97 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));98 if let Err(e) = cmd.sudo().run().await {99 error!("failed to set rollback marker: {e}");100 failed = true;101 }102 }103 104 105 106 107 108109 110 111 112 113 if action.should_schedule_rollback_run() {114 let mut cmd = remowt.cmd("systemd-run");115 cmd.comparg("--on-active", "3min")116 .comparg("--unit", "rollback-watchdog-run")117 .arg("systemctl")118 .arg("start")119 .arg("rollback-watchdog.service");120 if let Err(e) = cmd.sudo().run().await {121 error!("failed to schedule rollback run: {e}");122 failed = true;123 }124 }125 }126127 let remowt = host.remowt().await?;128 let fs = remowt.endpoints::<FsClient<_>>();129 if deploy_kind == DeployKind::NixosLustrate {130 131 132 if !fs133 .file_exists(Utf8PathBuf::from("/etc/NIXOS_LUSTRATE"))134 .await?135 {136 bail!("/etc/NIXOS_LUSTRATE should be created on remote host");137 }138 139 let mut cmd = remowt.cmd("touch");140 cmd.arg("/etc/NIXOS");141 cmd.sudo().run().await.context("creating /etc/NIXOS")?;142 }143 if deploy_kind == DeployKind::NixosInstall {144 info!(145 "running nixos-install to switch profile, install bootloader, and perform activation"146 );147 let mut cmd = remowt.cmd("nixos-install");148 cmd.arg("--system").arg(&built).args([149 150 151 "--no-channel-copy",152 "--root",153 "/mnt",154 ]);155 if let Err(e) = cmd.sudo().run().await {156 error!("failed to execute nixos-install: {e}");157 failed = true;158 }159 } else {160 if action.should_switch_profile() && !failed {161 info!("switching system profile generation");162163 match host.ensure_nix_plugin().await {164 Ok(plugin_id) => {165 let nix_elevated = remowt.plugin_endpoints::<NixClient<_>>(plugin_id);166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 if let Err(e) = nix_elevated184 .switch_profile("/nix/var/nix/profiles/system".to_owned(), built.clone())185 .await186 {187 error!("failed to switch system profile generation: {e}");188 failed = true;189 }190 }191 Err(e) => {192 failed = true;193 error!("failed to enable nix plugin: {e:?}");194 }195 }196 }197198 199200 if action.should_activate() && !failed {201 202 info!("executing activation script");203 let specialised = if let Some(specialisation) = specialisation {204 let mut specialised = built.join("specialisation");205 specialised.push(specialisation);206 specialised207 } else {208 built.clone()209 };210 let switch_script = specialised.join("bin/switch-to-configuration");211 let mut cmd = remowt.cmd("systemd-run");212 if deploy_kind == DeployKind::NixosLustrate {213 cmd.arg("--setenv=NIXOS_INSTALL_BOOTLOADER=1");214 }215 cmd.arg("--setenv=FLEET_ONLINE_ACTIVATION=1")216 .arg("--collect")217 .arg("--no-ask-password")218 .arg("--pipe")219 .arg("--quiet")220 .arg("--service-type=exec")221 .arg("--unit=fleet-switch-to-configuration")222 .arg(switch_script)223 .arg(action.name().expect("upload.should_activate == false"));224 if let Err(e) = cmd.sudo().run().in_current_span().await {225 error!("failed to activate: {e}");226 failed = true;227 }228 }229 }230 if action.should_create_rollback_marker() {231 let elevated_systemd = remowt.run0_endpoints::<SystemdClient<BifConfig>>().await?;232 let elevated_fs = remowt.run0_endpoints::<FsClient<BifConfig>>().await?;233 if !disable_rollback {234 if failed {235 if action.should_schedule_rollback_run() {236 info!("executing rollback");237 if let Err(e) = elevated_systemd238 .start("rollback-watchdog.service".to_owned())239 .instrument(info_span!("rollback"))240 .await241 {242 error!("failed to trigger rollback: {e}")243 }244 }245 } else {246 info!("trying to mark upgrade as successful");247 if let Err(e) = elevated_fs248 .rm_file(Utf8PathBuf::from("/etc/fleet_rollback_marker"))249 .in_current_span()250 .await251 {252 error!(253 "failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}"254 )255 }256 }257 info!("disarming watchdog, just in case");258 if let Err(_e) = elevated_systemd259 .stop("rollback-watchdog.timer".to_owned())260 .await261 {262 263 }264 if action.should_schedule_rollback_run()265 && let Err(e) = elevated_systemd266 .stop("rollback-watchdog-run.timer".to_owned())267 .await268 {269 error!("failed to disarm rollback run: {e}");270 }271 } else if let Err(_e) = elevated_fs272 .rm_file(Utf8PathBuf::from("/etc/fleet_rollback_marker"))273 .in_current_span()274 .await275 {276 277 }278 }279 Ok(())280}281282pub async fn upload_task(283 host: &ConfigHost,284 location: GenerationStorage,285 generation: Utf8PathBuf,286) -> Result<Utf8PathBuf> {287 if matches!(location, GenerationStorage::Pusher) {288 bail!("pusher is not enabled in this version of fleet");289 }290 if !host.local {291 info!("uploading system closure");292 let mut tries = 0;293 loop {294 match host.remote_derivation(&generation).await {295 Ok(remote) => {296 assert!(remote == generation, "CA derivations aren't implemented");297 return Ok(remote);298 }299 Err(e) if tries < 3 => {300 tries += 1;301 warn!("copy failure ({}/3): {:#}", tries, e);302 sleep(Duration::from_millis(5000)).await;303 }304 Err(e) => {305 bail!("upload failed: {e:#}");306 }307 }308 }309 }310 Ok(generation)311}