Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions crates/alertd/src/doctor/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,38 @@ pub struct CheckContext {
pub has_install: bool,
}

/// Whether the doctor is running as root (euid 0), read from `/proc/self/status`.
async fn is_root() -> bool {
// euid is the second field of the `Uid:` line.
tokio::fs::read_to_string("/proc/self/status")
.await
.ok()
.and_then(|status| {
status
.lines()
.find_map(|line| line.strip_prefix("Uid:"))
.and_then(|rest| rest.split_whitespace().nth(1).map(str::to_owned))
})
.and_then(|euid| euid.parse::<u32>().ok())
.is_some_and(|euid| euid == 0)
}

/// Build a command that runs `program` as root: directly when we already are,
/// else via `sudo`. Some checks read root-only interfaces — btrfs ioctls
/// (`device stats`), and the like — that see nothing as a normal user. The
/// alertd daemon runs the sweep as root (direct); an interactive `bestool tamanu
/// doctor` elevates rather than reporting blind. (Assumes passwordless sudo
/// where used; otherwise sudo's own failure surfaces in the check.)
pub(crate) async fn privileged(program: &str) -> tokio::process::Command {
if is_root().await {
tokio::process::Command::new(program)
} else {
let mut cmd = tokio::process::Command::new("sudo");
cmd.arg(program);
cmd
}
}

/// `tokio_postgres::Error`'s top-level Display is the unhelpful `"db error"`.
/// The actual SQL message lives in the optional `DbError` underneath; this
/// helper surfaces it where present and falls back to the source chain.
Expand Down
5 changes: 3 additions & 2 deletions crates/alertd/src/doctor/checks/btrfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
use std::{collections::BTreeSet, path::PathBuf};

use serde_json::{Value, json};
use tokio::process::Command;

use super::SweepContext;
use crate::doctor::check::Check;
Expand Down Expand Up @@ -145,7 +144,9 @@ enum CmdErr {
}

async fn btrfs_cmd(args: &[&str]) -> Result<String, CmdErr> {
match Command::new("btrfs").args(args).output().await {
// device stats / subvolume list need CAP_SYS_ADMIN; elevate when not root so
// an interactive run still collects, matching the root daemon sweep.
match super::privileged("btrfs").await.args(args).output().await {
Ok(o) if o.status.success() => Ok(String::from_utf8_lossy(&o.stdout).into_owned()),
Ok(o) => Err(CmdErr::Failed(format!(
"`btrfs {}` exited {}: {}",
Expand Down
32 changes: 31 additions & 1 deletion crates/tamanu/src/versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,18 @@ pub fn parse_image_tag(image: &str) -> Option<&str> {
#[cfg(target_os = "linux")]
#[instrument(level = "debug")]
pub async fn running_versions_linux() -> Result<HashMap<String, String>, String> {
let result = tokio::process::Command::new("podman")
// The deployment runs rootful podman, so the containers are only visible to
// root. When we're not root (an interactive `tamanu status` / `doctor`),
// elevate via sudo rather than letting podman run rootless and see nothing or
// error; the alertd daemon already runs as root and invokes it directly.
let mut command = if is_root().await {
tokio::process::Command::new("podman")
} else {
let mut c = tokio::process::Command::new("sudo");
c.arg("podman");
c
};
let result = command
.args([
"ps",
"--format",
Expand Down Expand Up @@ -185,6 +196,25 @@ pub async fn running_versions_linux() -> Result<HashMap<String, String>, String>
Ok(HashMap::new())
}

/// Whether this process is running as root (euid 0), read from
/// `/proc/self/status`. Used to decide whether reading the rootful podman needs
/// elevating via sudo.
#[cfg(target_os = "linux")]
async fn is_root() -> bool {
// euid is the second field of the `Uid:` line.
tokio::fs::read_to_string("/proc/self/status")
.await
.ok()
.and_then(|status| {
status
.lines()
.find_map(|line| line.strip_prefix("Uid:"))
.and_then(|rest| rest.split_whitespace().nth(1).map(str::to_owned))
})
.and_then(|euid| euid.parse::<u32>().ok())
.is_some_and(|euid| euid == 0)
}

/// Parse a version string leniently, tolerating a leading `v` (image tags and
/// env values both occur in `v2.48.5` form) and surrounding whitespace.
pub fn parse_version_loose(s: &str) -> Option<Version> {
Expand Down
Loading