Skip to content

Commit

Permalink
WIP: Script to upgrade from focal to noble
Browse files Browse the repository at this point in the history
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
legoktm committed Nov 15, 2024
1 parent ffbfd35 commit 94b84b7
Show file tree
Hide file tree
Showing 4 changed files with 317 additions and 1 deletion.
1 change: 1 addition & 0 deletions noble-migration/files/apt_freedom_press.list
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
deb [arch=amd64] https://apt.freedom.press noble main
13 changes: 13 additions & 0 deletions noble-migration/files/sources.list
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
301 changes: 301 additions & 0 deletions noble-migration/src/bin/upgrade.rs
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)
}
3 changes: 2 additions & 1 deletion securedrop/debian/rules
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ override_dh_auto_install:
cd /srv/rust/noble-migration && cargo build --release --locked && \
cd /srv/securedrop && \
mkdir -p ./debian/securedrop-config/usr/bin && \
mv /srv/rust/target/release/check ./debian/securedrop-config/usr/bin/securedrop-noble-migration-check
mv /srv/rust/target/release/check ./debian/securedrop-config/usr/bin/securedrop-noble-migration-check && \
mv /srv/rust/target/release/upgrade ./debian/securedrop-config/usr/bin/securedrop-noble-upgrade
# Build redwood wheel
python3 /srv/rust/redwood/build-wheel.py --release --redwood /srv/rust/redwood --target /srv/rust/target
# Set up virtualenv and install dependencies
Expand Down

0 comments on commit 94b84b7

Please sign in to comment.