-
Notifications
You must be signed in to change notification settings - Fork 687
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: Script to upgrade from focal to noble
The script is split into various stages where progress is tracked on-disk. The script is able to resume where it was at any point, and needs to, given multiple reboots in the middle. Fixes #7332.
- Loading branch information
Showing
4 changed files
with
317 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
deb [arch=amd64] https://apt.freedom.press noble main |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
## newer versions of the distribution. | ||
deb http://archive.ubuntu.com/ubuntu/ noble main | ||
|
||
## newer versions of the distribution. | ||
deb http://archive.ubuntu.com/ubuntu/ noble universe | ||
|
||
## Major bug fix updates produced after the final release of the | ||
## distribution. | ||
deb http://archive.ubuntu.com/ubuntu/ noble-updates main | ||
|
||
### Security fixes for distribution packages | ||
deb http://security.ubuntu.com/ubuntu noble-security main | ||
deb http://security.ubuntu.com/ubuntu noble-security universe |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,301 @@ | ||
//! Migrate a SecureDrop server from focal to noble | ||
//! | ||
//! This script should never be run directly, only via the | ||
//! systemd service. | ||
use anyhow::{anyhow, Result}; | ||
use rustix::process::geteuid; | ||
use serde::{Deserialize, Serialize}; | ||
use std::{ | ||
collections::HashMap, | ||
fs::{self, Permissions}, | ||
os::unix::fs::PermissionsExt, | ||
process::{Command, ExitCode}, | ||
thread::sleep, | ||
time::Duration, | ||
}; | ||
|
||
const STATE_PATH: &str = "/etc/securedrop-noble-migration.json"; | ||
|
||
#[derive(Serialize, Deserialize, PartialEq, Eq)] | ||
enum Stage { | ||
None, | ||
PendingUpdates, | ||
MigrationCheck, | ||
Backup, | ||
SuspendOSSEC, | ||
DisableUnattendedUpdates, | ||
ChangeAptSources, | ||
AptGetUpdate, | ||
AptGetUpgradeNoNew, | ||
AptGetFullUpgrade, | ||
ReenableUnattendedUpdates, | ||
ReenableOSSEC, | ||
Reboot, | ||
SwitchUbuntuSources, | ||
IntegrityCheck, | ||
RemoveBackup, | ||
Done, | ||
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
struct State { | ||
finished: Stage, | ||
} | ||
|
||
impl State { | ||
fn load() -> Result<Self> { | ||
if !fs::exists(STATE_PATH)? { | ||
return Ok(State { | ||
finished: Stage::None, | ||
}); | ||
} | ||
// FIXME: what if this fails? | ||
Ok(serde_json::from_str(&fs::read_to_string(STATE_PATH)?)?) | ||
} | ||
|
||
fn set(&mut self, stage: Stage) -> Result<()> { | ||
self.finished = stage; | ||
self.save() | ||
} | ||
|
||
fn save(&self) -> Result<()> { | ||
fs::write(STATE_PATH, serde_json::to_string(self)?)?; | ||
Ok(()) | ||
} | ||
} | ||
|
||
fn run_next_stage(state: &mut State) -> Result<()> { | ||
match state.finished { | ||
Stage::None => { | ||
pending_updates(state)?; | ||
state.set(Stage::PendingUpdates)?; | ||
Ok(()) | ||
} | ||
Stage::PendingUpdates => { | ||
migration_check()?; | ||
state.set(Stage::MigrationCheck)?; | ||
Ok(()) | ||
} | ||
Stage::MigrationCheck => { | ||
backup()?; | ||
state.set(Stage::Backup)?; | ||
Ok(()) | ||
} | ||
Stage::Backup => { | ||
suspend_ossec()?; | ||
state.set(Stage::SuspendOSSEC)?; | ||
Ok(()) | ||
} | ||
Stage::SuspendOSSEC => { | ||
disable_unattended_updates()?; | ||
state.set(Stage::DisableUnattendedUpdates)?; | ||
Ok(()) | ||
} | ||
Stage::DisableUnattendedUpdates => { | ||
change_apt_sources()?; | ||
state.set(Stage::ChangeAptSources)?; | ||
Ok(()) | ||
} | ||
Stage::ChangeAptSources => { | ||
apt_get_update()?; | ||
state.set(Stage::AptGetUpdate)?; | ||
Ok(()) | ||
} | ||
Stage::AptGetUpdate => { | ||
apt_get_upgrade_no_new()?; | ||
state.set(Stage::AptGetUpgradeNoNew)?; | ||
Ok(()) | ||
} | ||
Stage::AptGetUpgradeNoNew => { | ||
apt_get_full_upgrade()?; | ||
state.set(Stage::AptGetFullUpgrade)?; | ||
Ok(()) | ||
} | ||
Stage::AptGetFullUpgrade => { | ||
reenable_unattended_updates()?; | ||
state.set(Stage::ReenableUnattendedUpdates)?; | ||
Ok(()) | ||
} | ||
Stage::ReenableUnattendedUpdates => { | ||
reenable_ossec()?; | ||
state.set(Stage::ReenableOSSEC)?; | ||
Ok(()) | ||
} | ||
Stage::ReenableOSSEC => { | ||
reboot(state)?; | ||
state.set(Stage::Reboot)?; | ||
Ok(()) | ||
} | ||
Stage::Reboot => { | ||
switch_ubuntu_sources()?; | ||
state.set(Stage::SwitchUbuntuSources)?; | ||
Ok(()) | ||
} | ||
Stage::SwitchUbuntuSources => { | ||
integrity_check()?; | ||
state.set(Stage::IntegrityCheck)?; | ||
Ok(()) | ||
} | ||
Stage::IntegrityCheck => { | ||
remove_backup()?; | ||
state.set(Stage::RemoveBackup)?; | ||
Ok(()) | ||
} | ||
Stage::RemoveBackup => { | ||
state.set(Stage::Done)?; | ||
Ok(()) | ||
} | ||
Stage::Done => Ok(()), | ||
} | ||
} | ||
|
||
/// A wrapper to roughly implement Python's subprocess.check_call/check_output | ||
fn check_call(binary: &str, args: &[&str]) -> Result<String> { | ||
let output = Command::new(binary).args(args).output()?; | ||
|
||
if !output.status.success() { | ||
eprintln!("{}", String::from_utf8_lossy(&output.stderr)); | ||
return Err(anyhow!("{} failed", binary)); | ||
} | ||
Ok(String::from_utf8(output.stdout)?) | ||
} | ||
|
||
fn pending_updates(state: &mut State) -> Result<()> { | ||
check_call("apt-get", &["update"])?; | ||
check_call("unattended-upgrade", &[])?; | ||
state.set(Stage::PendingUpdates)?; | ||
check_call("systemctl", &["reboot"])?; | ||
Ok(()) | ||
} | ||
|
||
fn migration_check() -> Result<()> { | ||
// TODO: consider just invoking the Rust code directly | ||
check_call("systemctl", &["start", "securedrop-noble-migration-check"])?; | ||
loop { | ||
// Wait for the service to finish | ||
let output = check_call( | ||
"systemctl", | ||
&["is-active", "securedrop-noble-migration-check"], | ||
)?; | ||
if output == "active" { | ||
sleep(Duration::from_secs(1)); | ||
} else { | ||
break; | ||
} | ||
} | ||
let data: HashMap<String, bool> = serde_json::from_str( | ||
&fs::read_to_string("/etc/securedrop-noble-migration-check.json")?, | ||
)?; | ||
if data.contains_key("error") { | ||
return Err(anyhow!("Migration check errored")); | ||
} else if data.values().any(|val| val == &false) { | ||
return Err(anyhow!("Migration check failed")); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
fn backup() -> Result<()> { | ||
// Create a root-only directory to store the backup | ||
fs::create_dir("/var/lib/securedrop-backups")?; | ||
let permissions = Permissions::from_mode(0o700); | ||
fs::set_permissions("/var/lib/securedrop-backups", permissions)?; | ||
check_call( | ||
"/usr/bin/securedrop-app-backup.py", | ||
&["--dest", "/var/lib/securedrop-backups"], | ||
)?; | ||
Ok(()) | ||
} | ||
|
||
fn suspend_ossec() -> Result<()> { | ||
// FIXME: edit ossec.conf | ||
// FIXME: check we're on the mon server | ||
check_call("systemctl", &["restart", "ossec"])?; | ||
Ok(()) | ||
} | ||
|
||
fn disable_unattended_updates() -> Result<()> { | ||
// FIXME: apt-daily timers | ||
check_call("systemctl", &["mask", "unattended-upgrades"])?; | ||
Ok(()) | ||
} | ||
|
||
fn change_apt_sources() -> Result<()> { | ||
fs::write( | ||
"/etc/apt/sources.list", | ||
include_str!("../../files/sources.list"), | ||
)?; | ||
fs::write( | ||
"/etc/apt/sources.list.d/apt_freedom_press.list", | ||
include_str!("../../files/apt_freedom_press.list"), | ||
)?; | ||
Ok(()) | ||
} | ||
|
||
fn apt_get_update() -> Result<()> { | ||
check_call("apt-get", &["update"])?; | ||
Ok(()) | ||
} | ||
|
||
fn apt_get_upgrade_no_new() -> Result<()> { | ||
check_call("apt-get", &["upgrade", "--without-new-pkgs"])?; | ||
Ok(()) | ||
} | ||
|
||
fn apt_get_full_upgrade() -> Result<()> { | ||
check_call("apt-get", &["full-upgrade"])?; | ||
Ok(()) | ||
} | ||
|
||
fn reenable_unattended_updates() -> Result<()> { | ||
// FIXME: apt-daily timers | ||
check_call("systemctl", &["unmask", "unattended-upgrades"])?; | ||
Ok(()) | ||
} | ||
|
||
fn reenable_ossec() -> Result<()> { | ||
// FIXME: edit ossec.conf | ||
// FIXME: check we're on the mon server | ||
check_call("systemctl", &["restart", "ossec"])?; | ||
Ok(()) | ||
} | ||
|
||
fn reboot(state: &mut State) -> Result<()> { | ||
state.set(Stage::Reboot)?; | ||
check_call("systemctl", &["reboot"])?; | ||
Ok(()) | ||
} | ||
|
||
fn switch_ubuntu_sources() -> Result<()> { | ||
// FIXME: do something here | ||
Ok(()) | ||
} | ||
|
||
fn integrity_check() -> Result<()> { | ||
// FIXME: do something here | ||
// FIXME: check no failing systemd units | ||
// FIXME: check firewall is up | ||
Ok(()) | ||
} | ||
|
||
fn remove_backup() -> Result<()> { | ||
fs::remove_dir_all("/var/lib/securedrop-backups")?; | ||
Ok(()) | ||
} | ||
|
||
fn main() -> Result<ExitCode> { | ||
if !geteuid().is_root() { | ||
println!("This script must be run as root"); | ||
return Ok(ExitCode::FAILURE); | ||
} | ||
|
||
let mut state = State::load()?; | ||
loop { | ||
run_next_stage(&mut state)?; | ||
if state.finished == Stage::Done { | ||
break; | ||
} | ||
} | ||
|
||
Ok(ExitCode::SUCCESS) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters