Support native SD storage and consolidate fds-flash tooling
Accept native SD cards for internal settings alongside legacy NVMe, reject USB ancestry and ambiguous disks, and preserve read-only boot loading with explicit recovery writes. Require built-in MMC drivers, validate cached kernel configuration, and provide SD-only, SD-first and USB-first EEPROM profiles. Expose typed create and inspect commands through fds-flash, reuse the existing cartridge creation code, and document the optional e2fsprogs package dependency. Extend storage, EEPROM, flashing and hardware-test coverage and advance the wiki reference to the published SD guide. Validation: rootfs checks for CLI/development, boot matrix, internal storage, EEPROM, flash, workstation, emulator, make check and wiki checks passed. The full internal suite passed on an unchanged rerun after one unexplained VM shutdown stall. Standalone init-test was blocked by its unavailable pinned upstream kernel. Physical Pi checks remain pending.
This commit is contained in:
+128
-24
@@ -1,4 +1,4 @@
|
||||
//! Internal NVMe access is explicit and isolated in this process's mount namespace.
|
||||
//! Internal SD/NVMe access is explicit and isolated in this process's mount namespace.
|
||||
use fds_common::{
|
||||
Error, Result,
|
||||
machine::{self, Config},
|
||||
@@ -58,29 +58,55 @@ fn parent(part: &sysfs::BlockPartition) -> Result<PathBuf> {
|
||||
.map(Path::to_path_buf)
|
||||
.ok_or_else(|| Error("Partition has no parent disk".into()))
|
||||
}
|
||||
fn select() -> Result<(sysfs::BlockPartition, PathBuf, String)> {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Transport {
|
||||
Sd,
|
||||
Nvme,
|
||||
}
|
||||
impl Transport {
|
||||
fn source(self) -> &'static str {
|
||||
match self {
|
||||
Self::Sd => "internal_sd",
|
||||
Self::Nvme => "internal_nvme",
|
||||
}
|
||||
}
|
||||
}
|
||||
fn transport(disk: &Path) -> Result<Option<Transport>> {
|
||||
let buses: Vec<_> = disk
|
||||
.ancestors()
|
||||
.filter_map(|path| fs::read_link(path.join("subsystem")).ok())
|
||||
.filter_map(|path| path.file_name().map(|s| s.to_owned()))
|
||||
.collect();
|
||||
// A USB card reader is a workstation flashing destination, never internal
|
||||
// machine storage at runtime. Names such as mmcblk0 are not identities.
|
||||
if buses.iter().any(|b| b == "usb") {
|
||||
return Ok(None);
|
||||
}
|
||||
if buses.iter().any(|b| b == "mmc") {
|
||||
let kind = read_text(&disk.join("device/type"), 32)?;
|
||||
return Ok((kind.trim() == "SD").then_some(Transport::Sd));
|
||||
}
|
||||
if buses.iter().any(|b| b == "nvme") && read_text(&disk.join("removable"), 32)?.trim() == "0" {
|
||||
return Ok(Some(Transport::Nvme));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn select() -> Result<(sysfs::BlockPartition, PathBuf, String, Transport)> {
|
||||
let parts = sysfs::partitions(Path::new("/sys"))?;
|
||||
let mut candidates = Vec::new();
|
||||
for part in parts.iter().filter(|p| p.partition_name == "FDS_INTERNAL") {
|
||||
let disk = parent(part)?;
|
||||
let buses: Vec<_> = disk
|
||||
.ancestors()
|
||||
.filter_map(|path| fs::read_link(path.join("subsystem")).ok())
|
||||
.filter_map(|path| path.file_name().map(|s| s.to_owned()))
|
||||
.collect();
|
||||
if !buses.iter().any(|b| b == "nvme") || buses.iter().any(|b| b == "usb") {
|
||||
let Some(transport) = transport(&disk)? else {
|
||||
continue;
|
||||
}
|
||||
if read_text(&disk.join("removable"), 32)?.trim() != "0" {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let siblings: Vec<_> = parts
|
||||
.iter()
|
||||
.filter(|p| parent(p).ok().as_ref() == Some(&disk))
|
||||
.collect();
|
||||
if siblings.len() != 3 {
|
||||
return Err(Error(
|
||||
"Internal NVMe must contain exactly FDS_BOOT, FDS_RECOVERY and FDS_INTERNAL".into(),
|
||||
"Internal storage must contain exactly FDS_BOOT, FDS_RECOVERY and FDS_INTERNAL"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
for (number, label) in [(1, "FDS_BOOT"), (2, "FDS_RECOVERY"), (3, "FDS_INTERNAL")] {
|
||||
@@ -99,22 +125,22 @@ fn select() -> Result<(sysfs::BlockPartition, PathBuf, String)> {
|
||||
.trim()
|
||||
!= number.to_string()
|
||||
{
|
||||
return Err(Error("Invalid internal NVMe partition layout".into()));
|
||||
return Err(Error("Invalid internal storage partition layout".into()));
|
||||
}
|
||||
}
|
||||
let sequence = read_text(&disk.join("diskseq"), 32)?.trim().to_owned();
|
||||
sequence
|
||||
.parse::<u64>()
|
||||
.map_err(|_| Error("Invalid internal disk sequence".into()))?;
|
||||
candidates.push((part.clone(), disk, sequence));
|
||||
candidates.push((part.clone(), disk, sequence, transport));
|
||||
}
|
||||
match candidates.len() {
|
||||
0 => Err(Error(
|
||||
"No complete internal NVMe layout found; image defaults remain available".into(),
|
||||
"No complete internal SD/NVMe layout found; image defaults remain available".into(),
|
||||
)),
|
||||
1 => Ok(candidates.pop().unwrap()),
|
||||
_ => Err(Error(
|
||||
"Multiple internal NVMe layouts found; refusing to choose one".into(),
|
||||
"Multiple internal SD/NVMe layouts found; refusing to choose one".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -122,11 +148,12 @@ struct Internal {
|
||||
file: File,
|
||||
disk: PathBuf,
|
||||
sequence: String,
|
||||
transport: Transport,
|
||||
mounted: bool,
|
||||
}
|
||||
impl Internal {
|
||||
fn open(writable: bool) -> Result<Self> {
|
||||
let (part, disk, sequence) = select()?;
|
||||
let (part, disk, sequence, transport) = select()?;
|
||||
let mounts = read_text(Path::new("/proc/self/mountinfo"), 4 * 1024 * 1024)?;
|
||||
if mounts.lines().any(|line| {
|
||||
line.split_whitespace().nth(2) == Some(&format!("{}:{}", part.major, part.minor))
|
||||
@@ -180,6 +207,7 @@ impl Internal {
|
||||
file,
|
||||
disk,
|
||||
sequence,
|
||||
transport,
|
||||
mounted: false,
|
||||
};
|
||||
internal.identity()?;
|
||||
@@ -209,7 +237,9 @@ impl Internal {
|
||||
}
|
||||
fn identity(&self) -> Result<()> {
|
||||
if read_text(&self.disk.join("diskseq"), 32)?.trim() != self.sequence {
|
||||
return Err(Error("Internal NVMe changed during the operation".into()));
|
||||
return Err(Error(
|
||||
"Internal storage changed during the operation".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -309,15 +339,16 @@ pub fn load() -> Result<()> {
|
||||
if Path::new(machine::SNAPSHOT).exists() && Path::new(machine::STATUS).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let attempt = || -> Result<(Config, String)> {
|
||||
let attempt = || -> Result<(Config, String, Transport)> {
|
||||
let internal = Internal::open(false)?;
|
||||
let config = Config::parse(&machine::trusted_text(
|
||||
Path::new(&format!("{MOUNT}/config/machine.json")),
|
||||
machine::MAX_BUNDLE,
|
||||
)?)?;
|
||||
let sequence = internal.sequence.clone();
|
||||
let transport = internal.transport;
|
||||
internal.close(false)?;
|
||||
Ok((config, sequence))
|
||||
Ok((config, sequence, transport))
|
||||
};
|
||||
let emulated = emulator_config()?;
|
||||
let (config, status) = if let Some(config) = emulated {
|
||||
@@ -325,8 +356,8 @@ pub fn load() -> Result<()> {
|
||||
(config, status)
|
||||
} else {
|
||||
match attempt() {
|
||||
Ok((config, sequence)) => {
|
||||
let status = serde_json::json!({"source":"internal_nvme", "name":config.name, "disk_sequence":sequence, "error":null});
|
||||
Ok((config, sequence, transport)) => {
|
||||
let status = serde_json::json!({"source":transport.source(), "name":config.name, "disk_sequence":sequence, "error":null});
|
||||
(config, status)
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -410,7 +441,7 @@ fn store(name: &str, input: &Path) -> Result<()> {
|
||||
}
|
||||
atomic(&output, &bytes, 0o600)?;
|
||||
internal.close(true)?;
|
||||
println!("Saved diagnostics/{name} on internal NVMe; storage is flushed and unmounted.");
|
||||
println!("Saved diagnostics/{name} on internal storage; storage is flushed and unmounted.");
|
||||
Ok(())
|
||||
}
|
||||
pub fn run(command: crate::cli::MachineCommand, json: bool) -> Result<()> {
|
||||
@@ -508,3 +539,76 @@ pub fn run(command: crate::cli::MachineCommand, json: bool) -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod transport_tests {
|
||||
use super::{Transport, transport};
|
||||
use std::{fs, os::unix::fs::symlink, path::PathBuf};
|
||||
|
||||
struct Fixture(PathBuf);
|
||||
impl Fixture {
|
||||
fn new() -> Self {
|
||||
let nonce = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"fds-internal-transport-{}-{nonce}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir(&root).unwrap();
|
||||
Self(root)
|
||||
}
|
||||
fn disk(&self, bus: &str, kind: &str, removable: &str, usb: bool) -> PathBuf {
|
||||
let ancestor = self.0.join(format!("{bus}-{kind}-{removable}-{usb}"));
|
||||
let card = ancestor.join("card");
|
||||
let disk = card.join("block/arbitrary-disk-name");
|
||||
fs::create_dir_all(&disk).unwrap();
|
||||
symlink(format!("/sys/bus/{bus}"), card.join("subsystem")).unwrap();
|
||||
if usb {
|
||||
symlink("/sys/bus/usb", ancestor.join("subsystem")).unwrap();
|
||||
}
|
||||
symlink(&card, disk.join("device")).unwrap();
|
||||
fs::write(card.join("type"), kind).unwrap();
|
||||
fs::write(disk.join("removable"), removable).unwrap();
|
||||
disk
|
||||
}
|
||||
}
|
||||
impl Drop for Fixture {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_sd_uses_mmc_card_identity_not_removable_flag_or_device_name() {
|
||||
let fixture = Fixture::new();
|
||||
for removable in ["0", "1"] {
|
||||
let disk = fixture.disk("mmc", "SD", removable, false);
|
||||
assert_eq!(transport(&disk).unwrap(), Some(Transport::Sd));
|
||||
}
|
||||
assert_eq!(Transport::Sd.source(), "internal_sd");
|
||||
let disk = fixture.disk("nvme", "", "0", false);
|
||||
assert_eq!(transport(&disk).unwrap(), Some(Transport::Nvme));
|
||||
assert_eq!(Transport::Nvme.source(), "internal_nvme");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usb_lookalikes_other_mmc_devices_and_unknown_transports_are_rejected() {
|
||||
let fixture = Fixture::new();
|
||||
for (bus, kind, removable, usb) in [
|
||||
("mmc", "SD", "0", true),
|
||||
("nvme", "", "0", true),
|
||||
("mmc", "MMC", "0", false),
|
||||
("mmc", "SDIO", "1", false),
|
||||
("scsi", "SD", "0", false),
|
||||
("nvme", "", "1", false),
|
||||
] {
|
||||
let disk = fixture.disk(bus, kind, removable, usb);
|
||||
assert_eq!(transport(&disk).unwrap(), None, "{}", disk.display());
|
||||
}
|
||||
let disk = fixture.disk("mmc", "SD", "1", false);
|
||||
fs::remove_file(disk.join("device/type")).unwrap();
|
||||
assert!(transport(&disk).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user