Files
start-os/core/src/disk/mod.rs
Aiden McClelland 0070a8e692 refactor: derive OsPartitionInfo from fstab instead of config.yaml
Replace the serialized os_partitions field in ServerConfig with runtime
fstab parsing. OsPartitionInfo::from_fstab() resolves PARTUUID/UUID/LABEL
device specs via blkid and discovers the BIOS boot partition by scanning
for its GPT type GUID via lsblk.

Also removes the efibootmgr-based boot order management (replaced by
GRUB-based USB detection in a subsequent commit) and adds a dedicated
bios: Option<PathBuf> field for the unformatted BIOS boot partition.
2026-03-12 11:10:24 -06:00

259 lines
8.0 KiB
Rust

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use itertools::Itertools;
use lazy_format::lazy_format;
use rpc_toolkit::{CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use crate::{Error, ErrorKind};
use crate::context::{CliContext, RpcContext};
use crate::disk::util::DiskInfo;
use crate::prelude::*;
use crate::util::Invoke;
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
pub mod fsck;
pub mod main;
pub mod mount;
pub mod util;
pub const BOOT_RW_PATH: &str = "/media/boot-rw";
pub const REPAIR_DISK_PATH: &str = "/media/startos/config/repair-disk";
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OsPartitionInfo {
pub bios: Option<PathBuf>,
pub boot: PathBuf,
pub root: PathBuf,
#[serde(default)]
pub extra_boot: BTreeMap<String, PathBuf>,
#[serde(skip)]
pub data: Option<PathBuf>,
}
impl OsPartitionInfo {
pub fn contains(&self, logicalname: impl AsRef<Path>) -> bool {
let p = logicalname.as_ref();
self.bios.as_deref() == Some(p)
|| p == &*self.boot
|| p == &*self.root
|| self.extra_boot.values().any(|v| v == p)
}
/// Build partition info by parsing /etc/fstab and resolving device specs,
/// then discovering the BIOS boot partition (which is never mounted).
pub async fn from_fstab() -> Result<Self, Error> {
let fstab = tokio::fs::read_to_string("/etc/fstab")
.await
.with_ctx(|_| (ErrorKind::Filesystem, "/etc/fstab"))?;
let mut boot = None;
let mut root = None;
let mut extra_boot = BTreeMap::new();
for line in fstab.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut fields = line.split_whitespace();
let Some(source) = fields.next() else {
continue;
};
let Some(target) = fields.next() else {
continue;
};
let dev = match resolve_fstab_source(source).await {
Ok(d) => d,
Err(e) => {
tracing::warn!("Failed to resolve fstab source {source}: {e}");
continue;
}
};
match target {
"/" => root = Some(dev),
"/boot" => boot = Some(dev),
t if t.starts_with("/boot/") => {
if let Some(name) = t.strip_prefix("/boot/") {
extra_boot.insert(name.to_string(), dev);
}
}
_ => {}
}
}
let boot = boot.unwrap_or_default();
let bios = if !boot.as_os_str().is_empty() {
find_bios_boot_partition(&boot).await.ok().flatten()
} else {
None
};
Ok(Self {
bios,
boot,
root: root.unwrap_or_default(),
extra_boot,
data: None,
})
}
}
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6e6f-744e-656564726548";
/// Find the BIOS boot partition on the same disk as `known_part`.
async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> {
let output = Command::new("lsblk")
.args(["-n", "-l", "-o", "NAME,PKNAME,PARTTYPE"])
.arg(known_part)
.invoke(ErrorKind::DiskManagement)
.await?;
let text = String::from_utf8(output)?;
let parent_disk = text.lines().find_map(|line| {
let mut fields = line.split_whitespace();
let _name = fields.next()?;
let pkname = fields.next()?;
(!pkname.is_empty()).then(|| pkname.to_string())
});
let Some(parent_disk) = parent_disk else {
return Ok(None);
};
let output = Command::new("lsblk")
.args(["-n", "-l", "-o", "NAME,PARTTYPE"])
.arg(format!("/dev/{parent_disk}"))
.invoke(ErrorKind::DiskManagement)
.await?;
let text = String::from_utf8(output)?;
for line in text.lines() {
let mut fields = line.split_whitespace();
let Some(name) = fields.next() else { continue };
let Some(parttype) = fields.next() else {
continue;
};
if parttype.eq_ignore_ascii_case(BIOS_BOOT_TYPE_GUID) {
return Ok(Some(PathBuf::from(format!("/dev/{name}"))));
}
}
Ok(None)
}
/// Resolve an fstab device spec (e.g. /dev/sda1, PARTUUID=..., UUID=...) to a
/// canonical device path.
async fn resolve_fstab_source(source: &str) -> Result<PathBuf, Error> {
if source.starts_with('/') {
return Ok(
tokio::fs::canonicalize(source)
.await
.unwrap_or_else(|_| PathBuf::from(source)),
);
}
// PARTUUID=, UUID=, LABEL= — resolve via blkid
let output = Command::new("blkid")
.args(["-o", "device", "-t", source])
.invoke(ErrorKind::DiskManagement)
.await?;
Ok(PathBuf::from(String::from_utf8(output)?.trim()))
}
pub fn disk<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"list",
from_fn_async(list)
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_disk_info(handle.params, result))
.with_about("about.list-disk-info")
.with_call_remote::<CliContext>(),
)
.subcommand("repair", from_fn_async(|_: C| repair()).no_cli())
.subcommand(
"repair",
CallRemoteHandler::<CliContext, _, _>::new(
from_fn_async(|_: RpcContext| repair())
.no_display()
.with_about("about.repair-disk-corruption"),
),
)
}
fn display_disk_info(params: WithIoFormat<Empty>, args: Vec<DiskInfo>) -> Result<(), Error> {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, args);
}
let mut table = Table::new();
table.add_row(row![bc =>
"LOGICALNAME",
"LABEL",
"CAPACITY",
"USED",
"STARTOS VERSION"
]);
for disk in args {
let row = row![
disk.logicalname.display(),
"N/A",
&format!("{:.2} GiB", disk.capacity as f64 / 1024.0 / 1024.0 / 1024.0),
"N/A",
"N/A",
];
table.add_row(row);
for part in disk.partitions {
let row = row![
part.logicalname.display(),
if let Some(label) = part.label.as_ref() {
label
} else {
"N/A"
},
part.capacity,
&if let Some(used) = part
.used
.map(|u| format!("{:.2} GiB", u as f64 / 1024.0 / 1024.0 / 1024.0))
{
used
} else {
"N/A".to_owned()
},
&if part.start_os.is_empty() {
"N/A".to_owned()
} else if part.start_os.len() == 1 {
part.start_os
.first_key_value()
.map(|(_, info)| info.version.to_string())
.unwrap()
} else {
part.start_os
.iter()
.map(|(id, info)| lazy_format!("{} ({})", info.version, id))
.join(", ")
},
];
table.add_row(row);
}
}
table.print_tty(false)?;
Ok(())
}
// #[command(display(display_disk_info))]
pub async fn list(ctx: RpcContext, _: Empty) -> Result<Vec<DiskInfo>, Error> {
crate::disk::util::list(&ctx.os_partitions).await
}
pub async fn repair() -> Result<(), Error> {
tokio::fs::write(REPAIR_DISK_PATH, b"").await?;
Ok(())
}