mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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.
This commit is contained in:
@@ -68,21 +68,6 @@ fi
|
||||
|
||||
EOF
|
||||
|
||||
# Promote the USB installer boot entry back to first in EFI boot order.
|
||||
# The entry number was saved during initial OS install.
|
||||
if [ -d /sys/firmware/efi ] && [ -f /media/startos/config/efi-installer-entry ]; then
|
||||
USB_ENTRY=$(cat /media/startos/config/efi-installer-entry)
|
||||
if [ -n "$USB_ENTRY" ]; then
|
||||
CURRENT_ORDER=$(efibootmgr | grep BootOrder | sed 's/BootOrder: //')
|
||||
OTHER_ENTRIES=$(echo "$CURRENT_ORDER" | tr ',' '\n' | grep -v "$USB_ENTRY" | tr '\n' ',' | sed 's/,$//')
|
||||
if [ -n "$OTHER_ENTRIES" ]; then
|
||||
efibootmgr -o "$USB_ENTRY,$OTHER_ENTRIES"
|
||||
else
|
||||
efibootmgr -o "$USB_ENTRY"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sign unsigned kernel modules for Secure Boot
|
||||
SIGN_FILE="$(ls -1 /media/startos/next/usr/lib/linux-kbuild-*/scripts/sign-file 2>/dev/null | head -1)"
|
||||
/media/startos/next/usr/lib/startos/scripts/sign-unsigned-modules \
|
||||
|
||||
@@ -9,7 +9,6 @@ use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MAIN_DATA;
|
||||
use crate::disk::OsPartitionInfo;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::IoFormat;
|
||||
use crate::version::VersionT;
|
||||
@@ -120,8 +119,6 @@ impl ClientConfig {
|
||||
pub struct ServerConfig {
|
||||
#[arg(short, long, help = "help.arg.config-file-path")]
|
||||
pub config: Option<PathBuf>,
|
||||
#[arg(skip)]
|
||||
pub os_partitions: Option<OsPartitionInfo>,
|
||||
#[arg(long, help = "help.arg.socks-listen-address")]
|
||||
pub socks_listen: Option<SocketAddr>,
|
||||
#[arg(long, help = "help.arg.revision-cache-size")]
|
||||
@@ -138,7 +135,6 @@ impl ContextConfig for ServerConfig {
|
||||
self.config.take()
|
||||
}
|
||||
fn merge_with(&mut self, other: Self) {
|
||||
self.os_partitions = self.os_partitions.take().or(other.os_partitions);
|
||||
self.socks_listen = self.socks_listen.take().or(other.socks_listen);
|
||||
self.revision_cache_size = self
|
||||
.revision_cache_size
|
||||
|
||||
@@ -327,12 +327,7 @@ impl RpcContext {
|
||||
|
||||
let seed = Arc::new(RpcContextSeed {
|
||||
is_closed: AtomicBool::new(false),
|
||||
os_partitions: config.os_partitions.clone().ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("context.rpc.os-partition-info-missing")),
|
||||
ErrorKind::Filesystem,
|
||||
)
|
||||
})?,
|
||||
os_partitions: OsPartitionInfo::from_fstab().await?,
|
||||
wifi_interface: wifi_interface.clone(),
|
||||
ethernet_interface: find_eth_iface().await?,
|
||||
disk_guid,
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
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;
|
||||
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;
|
||||
@@ -21,27 +25,143 @@ 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 efi: Option<PathBuf>,
|
||||
pub bios: Option<PathBuf>,
|
||||
pub boot: PathBuf,
|
||||
pub root: PathBuf,
|
||||
#[serde(skip)] // internal use only
|
||||
#[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 {
|
||||
self.efi
|
||||
.as_ref()
|
||||
.map(|p| p == logicalname.as_ref())
|
||||
.unwrap_or(false)
|
||||
|| self
|
||||
.bios
|
||||
.as_ref()
|
||||
.map(|p| p == logicalname.as_ref())
|
||||
.unwrap_or(false)
|
||||
|| &*self.boot == logicalname.as_ref()
|
||||
|| &*self.root == logicalname.as_ref()
|
||||
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> {
|
||||
|
||||
@@ -197,11 +197,19 @@ pub async fn partition(
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
|
||||
let mut extra_boot = std::collections::BTreeMap::new();
|
||||
let bios;
|
||||
if efi {
|
||||
extra_boot.insert("efi".to_string(), partition_for(&disk_path, 1));
|
||||
bios = None;
|
||||
} else {
|
||||
bios = Some(partition_for(&disk_path, 1));
|
||||
}
|
||||
Ok(OsPartitionInfo {
|
||||
efi: efi.then(|| partition_for(&disk_path, 1)),
|
||||
bios: (!efi).then(|| partition_for(&disk_path, 1)),
|
||||
bios,
|
||||
boot: partition_for(&disk_path, 2),
|
||||
root: partition_for(&disk_path, 3),
|
||||
extra_boot,
|
||||
data: data_part,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -164,10 +164,10 @@ pub async fn partition(
|
||||
.await?;
|
||||
|
||||
Ok(OsPartitionInfo {
|
||||
efi: None,
|
||||
bios: None,
|
||||
boot: partition_for(&disk_path, 1),
|
||||
root: partition_for(&disk_path, 2),
|
||||
extra_boot: Default::default(),
|
||||
data: data_part,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,53 +27,6 @@ use crate::util::serde::IoFormat;
|
||||
mod gpt;
|
||||
mod mbr;
|
||||
|
||||
/// Get the EFI BootCurrent entry number (the entry firmware used to boot).
|
||||
/// Returns None on non-EFI systems or if BootCurrent is not set.
|
||||
async fn get_efi_boot_current() -> Result<Option<String>, Error> {
|
||||
let efi_output = String::from_utf8(Command::new("efibootmgr").invoke(ErrorKind::Grub).await?)?;
|
||||
|
||||
Ok(efi_output
|
||||
.lines()
|
||||
.find(|line| line.starts_with("BootCurrent:"))
|
||||
.and_then(|line| line.strip_prefix("BootCurrent:"))
|
||||
.map(|s| s.trim().to_string()))
|
||||
}
|
||||
|
||||
/// Promote a specific boot entry to first in the EFI boot order.
|
||||
async fn promote_efi_entry(entry: &str) -> Result<(), Error> {
|
||||
let efi_output = String::from_utf8(Command::new("efibootmgr").invoke(ErrorKind::Grub).await?)?;
|
||||
|
||||
let current_order = efi_output
|
||||
.lines()
|
||||
.find(|line| line.starts_with("BootOrder:"))
|
||||
.and_then(|line| line.strip_prefix("BootOrder:"))
|
||||
.map(|s| s.trim())
|
||||
.unwrap_or("");
|
||||
|
||||
if current_order.is_empty() || current_order.starts_with(entry) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let other_entries: Vec<&str> = current_order
|
||||
.split(',')
|
||||
.filter(|e| e.trim() != entry)
|
||||
.collect();
|
||||
|
||||
let new_order = if other_entries.is_empty() {
|
||||
entry.to_string()
|
||||
} else {
|
||||
format!("{},{}", entry, other_entries.join(","))
|
||||
};
|
||||
|
||||
Command::new("efibootmgr")
|
||||
.arg("-o")
|
||||
.arg(&new_order)
|
||||
.invoke(ErrorKind::Grub)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Probe a squashfs image to determine its target architecture
|
||||
async fn probe_squashfs_arch(squashfs_path: &Path) -> Result<InternedString, Error> {
|
||||
let output = String::from_utf8(
|
||||
@@ -190,7 +143,7 @@ pub async fn install_os_to(
|
||||
|
||||
let part_info = partition(disk_path, capacity, partition_table, protect, use_efi).await?;
|
||||
|
||||
if let Some(efi) = &part_info.efi {
|
||||
if let Some(efi) = part_info.extra_boot.get("efi") {
|
||||
Command::new("mkfs.vfat")
|
||||
.arg(efi)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
@@ -307,10 +260,7 @@ pub async fn install_os_to(
|
||||
|
||||
tokio::fs::write(
|
||||
rootfs.path().join("config/config.yaml"),
|
||||
IoFormat::Yaml.to_vec(&ServerConfig {
|
||||
os_partitions: Some(part_info.clone()),
|
||||
..Default::default()
|
||||
})?,
|
||||
IoFormat::Yaml.to_vec(&ServerConfig::default())?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -329,7 +279,7 @@ pub async fn install_os_to(
|
||||
ReadWrite,
|
||||
)
|
||||
.await?;
|
||||
let efi = if let Some(efi) = &part_info.efi {
|
||||
let efi = if let Some(efi) = part_info.extra_boot.get("efi") {
|
||||
Some(
|
||||
MountGuard::mount(
|
||||
&BlockDev::new(efi),
|
||||
@@ -370,8 +320,8 @@ pub async fn install_os_to(
|
||||
include_str!("fstab.template"),
|
||||
boot = part_info.boot.display(),
|
||||
efi = part_info
|
||||
.efi
|
||||
.as_ref()
|
||||
.extra_boot
|
||||
.get("efi")
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|| "# N/A".to_owned()),
|
||||
root = part_info.root.display(),
|
||||
@@ -502,20 +452,6 @@ pub async fn install_os(
|
||||
|
||||
let use_efi = tokio::fs::metadata("/sys/firmware/efi").await.is_ok();
|
||||
|
||||
// Save the boot entry we booted from (the USB installer) before grub-install
|
||||
// overwrites the boot order.
|
||||
let boot_current = if use_efi {
|
||||
match get_efi_boot_current().await {
|
||||
Ok(entry) => entry,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to get EFI BootCurrent: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let InstallOsResult {
|
||||
part_info,
|
||||
rootfs,
|
||||
@@ -531,23 +467,6 @@ pub async fn install_os(
|
||||
)
|
||||
.await?;
|
||||
|
||||
// grub-install prepends its new entry to the EFI boot order, overriding the
|
||||
// USB-first priority. Promote the USB entry (identified by BootCurrent from
|
||||
// when we booted the installer) back to first, and persist the entry number
|
||||
// so the upgrade script can do the same.
|
||||
if let Some(ref entry) = boot_current {
|
||||
if let Err(e) = promote_efi_entry(entry).await {
|
||||
tracing::warn!("Failed to restore EFI boot order: {e}");
|
||||
}
|
||||
let efi_entry_path = rootfs.path().join("config/efi-installer-entry");
|
||||
if let Err(e) = tokio::fs::write(&efi_entry_path, entry).await {
|
||||
tracing::warn!("Failed to save EFI installer entry number: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
ctx.config
|
||||
.mutate(|c| c.os_partitions = Some(part_info.clone()));
|
||||
|
||||
let mut setup_info = SetupInfo::default();
|
||||
setup_info.mok_enrolled = mok_enrolled;
|
||||
|
||||
|
||||
@@ -95,8 +95,8 @@ const LIVE_MEDIUM_PATH: &str = "/run/live/medium";
|
||||
|
||||
pub async fn list_disks(ctx: SetupContext) -> Result<Vec<DiskInfo>, Error> {
|
||||
let mut disks = crate::disk::util::list(
|
||||
&ctx.config
|
||||
.peek(|c| c.os_partitions.clone())
|
||||
&crate::disk::OsPartitionInfo::from_fstab()
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Reference in New Issue
Block a user