mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
Merge branch 'next/patch' of github.com:Start9Labs/start-os into next/minor
This commit is contained in:
32
core/startos/src/disk/fsck/btrfs.rs
Normal file
32
core/startos/src/disk/fsck/btrfs.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::path::Path;
|
||||
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::disk::fsck::RequiresReboot;
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn btrfs_check_readonly(logicalname: impl AsRef<Path>) -> Result<RequiresReboot, Error> {
|
||||
Command::new("btrfs")
|
||||
.arg("check")
|
||||
.arg("--readonly")
|
||||
.arg(logicalname.as_ref())
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
|
||||
Ok(RequiresReboot(false))
|
||||
}
|
||||
|
||||
pub async fn btrfs_check_repair(logicalname: impl AsRef<Path>) -> Result<RequiresReboot, Error> {
|
||||
Command::new("btrfs")
|
||||
.arg("check")
|
||||
.arg("--repair")
|
||||
.arg("--force")
|
||||
.arg(logicalname.as_ref())
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
|
||||
Ok(RequiresReboot(false))
|
||||
}
|
||||
95
core/startos/src/disk/fsck/ext4.rs
Normal file
95
core/startos/src/disk/fsck/ext4.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::disk::fsck::RequiresReboot;
|
||||
use crate::Error;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn e2fsck_preen(
|
||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
e2fsck_runner(Command::new("e2fsck").arg("-p"), logicalname).await
|
||||
}
|
||||
|
||||
fn backup_existing_undo_file<'a>(path: &'a Path) -> BoxFuture<'a, Result<(), Error>> {
|
||||
async move {
|
||||
if tokio::fs::metadata(path).await.is_ok() {
|
||||
let bak = path.with_extension(format!(
|
||||
"{}.bak",
|
||||
path.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default()
|
||||
));
|
||||
backup_existing_undo_file(&bak).await?;
|
||||
tokio::fs::rename(path, &bak).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn e2fsck_aggressive(
|
||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
let undo_path = Path::new("/media/embassy/config")
|
||||
.join(
|
||||
logicalname
|
||||
.as_ref()
|
||||
.file_name()
|
||||
.unwrap_or(OsStr::new("unknown")),
|
||||
)
|
||||
.with_extension("e2undo");
|
||||
backup_existing_undo_file(&undo_path).await?;
|
||||
e2fsck_runner(
|
||||
Command::new("e2fsck").arg("-y").arg("-z").arg(undo_path),
|
||||
logicalname,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn e2fsck_runner(
|
||||
e2fsck_cmd: &mut Command,
|
||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
let e2fsck_out = e2fsck_cmd.arg(logicalname.as_ref()).output().await?;
|
||||
let e2fsck_stderr = String::from_utf8(e2fsck_out.stderr)?;
|
||||
let code = e2fsck_out.status.code().ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("e2fsck: process terminated by signal"),
|
||||
crate::ErrorKind::DiskManagement,
|
||||
)
|
||||
})?;
|
||||
if code & 4 != 0 {
|
||||
tracing::error!(
|
||||
"some filesystem errors NOT corrected on {}:\n{}",
|
||||
logicalname.as_ref().display(),
|
||||
e2fsck_stderr,
|
||||
);
|
||||
} else if code & 1 != 0 {
|
||||
tracing::warn!(
|
||||
"filesystem errors corrected on {}:\n{}",
|
||||
logicalname.as_ref().display(),
|
||||
e2fsck_stderr,
|
||||
);
|
||||
}
|
||||
if code < 8 {
|
||||
if code & 2 != 0 {
|
||||
tracing::warn!("reboot required");
|
||||
Ok(RequiresReboot(true))
|
||||
} else {
|
||||
Ok(RequiresReboot(false))
|
||||
}
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("e2fsck: {}", e2fsck_stderr),
|
||||
crate::ErrorKind::DiskManagement,
|
||||
))
|
||||
}
|
||||
}
|
||||
70
core/startos/src/disk/fsck/mod.rs
Normal file
70
core/startos/src/disk/fsck/mod.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::disk::fsck::btrfs::{btrfs_check_readonly, btrfs_check_repair};
|
||||
use crate::disk::fsck::ext4::{e2fsck_aggressive, e2fsck_preen};
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
pub mod btrfs;
|
||||
pub mod ext4;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[must_use]
|
||||
pub struct RequiresReboot(pub bool);
|
||||
impl std::ops::BitOrAssign for RequiresReboot {
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
self.0 |= rhs.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RepairStrategy {
|
||||
Preen,
|
||||
Aggressive,
|
||||
}
|
||||
impl RepairStrategy {
|
||||
pub async fn fsck(
|
||||
&self,
|
||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
match &*String::from_utf8(
|
||||
Command::new("grub-probe")
|
||||
.arg("-d")
|
||||
.arg(logicalname.as_ref())
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?,
|
||||
)?
|
||||
.trim()
|
||||
{
|
||||
"ext2" => self.e2fsck(logicalname).await,
|
||||
"btrfs" => self.btrfs_check(logicalname).await,
|
||||
fs => {
|
||||
return Err(Error::new(
|
||||
eyre!("Unknown filesystem {fs}"),
|
||||
crate::ErrorKind::DiskManagement,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn e2fsck(
|
||||
&self,
|
||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
match self {
|
||||
RepairStrategy::Preen => e2fsck_preen(logicalname).await,
|
||||
RepairStrategy::Aggressive => e2fsck_aggressive(logicalname).await,
|
||||
}
|
||||
}
|
||||
pub async fn btrfs_check(
|
||||
&self,
|
||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
match self {
|
||||
RepairStrategy::Preen => btrfs_check_readonly(logicalname).await,
|
||||
RepairStrategy::Aggressive => btrfs_check_repair(logicalname).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
337
core/startos/src/disk/main.rs
Normal file
337
core/startos/src/disk/main.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::fsck::{RepairStrategy, RequiresReboot};
|
||||
use super::util::pvscan;
|
||||
use crate::disk::mount::filesystem::block_dev::mount;
|
||||
use crate::disk::mount::filesystem::ReadWrite;
|
||||
use crate::disk::mount::util::unmount;
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
pub const PASSWORD_PATH: &'static str = "/run/embassy/password";
|
||||
pub const DEFAULT_PASSWORD: &'static str = "password";
|
||||
pub const MAIN_FS_SIZE: FsSize = FsSize::Gigabytes(8);
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create<I, P>(
|
||||
disks: &I,
|
||||
pvscan: &BTreeMap<PathBuf, Option<String>>,
|
||||
datadir: impl AsRef<Path>,
|
||||
password: Option<&str>,
|
||||
) -> Result<String, Error>
|
||||
where
|
||||
for<'a> &'a I: IntoIterator<Item = &'a P>,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let guid = create_pool(disks, pvscan, password.is_some()).await?;
|
||||
create_all_fs(&guid, &datadir, password).await?;
|
||||
export(&guid, datadir).await?;
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create_pool<I, P>(
|
||||
disks: &I,
|
||||
pvscan: &BTreeMap<PathBuf, Option<String>>,
|
||||
encrypted: bool,
|
||||
) -> Result<String, Error>
|
||||
where
|
||||
for<'a> &'a I: IntoIterator<Item = &'a P>,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
Command::new("dmsetup")
|
||||
.arg("remove_all") // TODO: find a higher finesse way to do this for portability reasons
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
for disk in disks {
|
||||
if pvscan.contains_key(disk.as_ref()) {
|
||||
Command::new("pvremove")
|
||||
.arg("-yff")
|
||||
.arg(disk.as_ref())
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
}
|
||||
tokio::fs::write(disk.as_ref(), &[0; 2048]).await?; // wipe partition table
|
||||
Command::new("pvcreate")
|
||||
.arg("-yff")
|
||||
.arg(disk.as_ref())
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
}
|
||||
let mut guid = format!(
|
||||
"EMBASSY_{}",
|
||||
base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&rand::random::<[u8; 32]>(),
|
||||
)
|
||||
);
|
||||
if !encrypted {
|
||||
guid += "_UNENC";
|
||||
}
|
||||
let mut cmd = Command::new("vgcreate");
|
||||
cmd.arg("-y").arg(&guid);
|
||||
for disk in disks {
|
||||
cmd.arg(disk.as_ref());
|
||||
}
|
||||
cmd.invoke(crate::ErrorKind::DiskManagement).await?;
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FsSize {
|
||||
Gigabytes(usize),
|
||||
FreePercentage(usize),
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create_fs<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
name: &str,
|
||||
size: FsSize,
|
||||
password: Option<&str>,
|
||||
) -> Result<(), Error> {
|
||||
let mut cmd = Command::new("lvcreate");
|
||||
match size {
|
||||
FsSize::Gigabytes(a) => cmd.arg("-L").arg(format!("{}G", a)),
|
||||
FsSize::FreePercentage(a) => cmd.arg("-l").arg(format!("{}%FREE", a)),
|
||||
};
|
||||
cmd.arg("-y")
|
||||
.arg("-n")
|
||||
.arg(name)
|
||||
.arg(guid)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
let mut blockdev_path = Path::new("/dev").join(guid).join(name);
|
||||
if let Some(password) = password {
|
||||
if let Some(parent) = Path::new(PASSWORD_PATH).parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
tokio::fs::write(PASSWORD_PATH, password)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
|
||||
Command::new("cryptsetup")
|
||||
.arg("-q")
|
||||
.arg("luksFormat")
|
||||
.arg(format!("--key-file={}", PASSWORD_PATH))
|
||||
.arg(format!("--keyfile-size={}", password.len()))
|
||||
.arg(&blockdev_path)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
Command::new("cryptsetup")
|
||||
.arg("-q")
|
||||
.arg("luksOpen")
|
||||
.arg("--allow-discards")
|
||||
.arg(format!("--key-file={}", PASSWORD_PATH))
|
||||
.arg(format!("--keyfile-size={}", password.len()))
|
||||
.arg(&blockdev_path)
|
||||
.arg(format!("{}_{}", guid, name))
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
tokio::fs::remove_file(PASSWORD_PATH)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
|
||||
blockdev_path = Path::new("/dev/mapper").join(format!("{}_{}", guid, name));
|
||||
}
|
||||
Command::new("mkfs.btrfs")
|
||||
.arg(&blockdev_path)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
mount(&blockdev_path, datadir.as_ref().join(name), ReadWrite).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create_all_fs<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
password: Option<&str>,
|
||||
) -> Result<(), Error> {
|
||||
create_fs(guid, &datadir, "main", MAIN_FS_SIZE, password).await?;
|
||||
create_fs(
|
||||
guid,
|
||||
&datadir,
|
||||
"package-data",
|
||||
FsSize::FreePercentage(100),
|
||||
password,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn unmount_fs<P: AsRef<Path>>(guid: &str, datadir: P, name: &str) -> Result<(), Error> {
|
||||
unmount(datadir.as_ref().join(name)).await?;
|
||||
if !guid.ends_with("_UNENC") {
|
||||
Command::new("cryptsetup")
|
||||
.arg("-q")
|
||||
.arg("luksClose")
|
||||
.arg(format!("{}_{}", guid, name))
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn unmount_all_fs<P: AsRef<Path>>(guid: &str, datadir: P) -> Result<(), Error> {
|
||||
unmount_fs(guid, &datadir, "main").await?;
|
||||
unmount_fs(guid, &datadir, "package-data").await?;
|
||||
Command::new("dmsetup")
|
||||
.arg("remove_all") // TODO: find a higher finesse way to do this for portability reasons
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn export<P: AsRef<Path>>(guid: &str, datadir: P) -> Result<(), Error> {
|
||||
Command::new("sync").invoke(ErrorKind::Filesystem).await?;
|
||||
unmount_all_fs(guid, datadir).await?;
|
||||
Command::new("vgchange")
|
||||
.arg("-an")
|
||||
.arg(guid)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
Command::new("vgexport")
|
||||
.arg(guid)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn import<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
repair: RepairStrategy,
|
||||
password: Option<&str>,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
let scan = pvscan().await?;
|
||||
if scan
|
||||
.values()
|
||||
.filter_map(|a| a.as_ref())
|
||||
.filter(|a| a.starts_with("EMBASSY_"))
|
||||
.next()
|
||||
.is_none()
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("StartOS disk not found."),
|
||||
crate::ErrorKind::DiskNotAvailable,
|
||||
));
|
||||
}
|
||||
if !scan
|
||||
.values()
|
||||
.filter_map(|a| a.as_ref())
|
||||
.any(|id| id == guid)
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("A StartOS disk was found, but it is not the correct disk for this device."),
|
||||
crate::ErrorKind::IncorrectDisk,
|
||||
));
|
||||
}
|
||||
Command::new("dmsetup")
|
||||
.arg("remove_all") // TODO: find a higher finesse way to do this for portability reasons
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
match Command::new("vgimport")
|
||||
.arg(guid)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(e)
|
||||
if format!("{}", e.source)
|
||||
.lines()
|
||||
.any(|l| l.trim() == format!("Volume group \"{}\" is not exported", guid)) =>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}?;
|
||||
Command::new("vgchange")
|
||||
.arg("-ay")
|
||||
.arg(guid)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
mount_all_fs(guid, datadir, repair, password).await
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount_fs<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
name: &str,
|
||||
repair: RepairStrategy,
|
||||
password: Option<&str>,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
let orig_path = Path::new("/dev").join(guid).join(name);
|
||||
let mut blockdev_path = orig_path.clone();
|
||||
let full_name = format!("{}_{}", guid, name);
|
||||
if !guid.ends_with("_UNENC") {
|
||||
let password = password.unwrap_or(DEFAULT_PASSWORD);
|
||||
if let Some(parent) = Path::new(PASSWORD_PATH).parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
tokio::fs::write(PASSWORD_PATH, password)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
|
||||
Command::new("cryptsetup")
|
||||
.arg("-q")
|
||||
.arg("luksOpen")
|
||||
.arg(format!("--key-file={}", PASSWORD_PATH))
|
||||
.arg(format!("--keyfile-size={}", password.len()))
|
||||
.arg(&blockdev_path)
|
||||
.arg(&full_name)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
tokio::fs::remove_file(PASSWORD_PATH)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
|
||||
blockdev_path = Path::new("/dev/mapper").join(&full_name);
|
||||
}
|
||||
let reboot = repair.fsck(&blockdev_path).await?;
|
||||
|
||||
if !guid.ends_with("_UNENC") {
|
||||
// Backup LUKS header if e2fsck succeeded
|
||||
let luks_folder = Path::new("/media/embassy/config/luks");
|
||||
tokio::fs::create_dir_all(luks_folder).await?;
|
||||
let tmp_luks_bak = luks_folder.join(format!(".{full_name}.luks.bak.tmp"));
|
||||
if tokio::fs::metadata(&tmp_luks_bak).await.is_ok() {
|
||||
tokio::fs::remove_file(&tmp_luks_bak).await?;
|
||||
}
|
||||
let luks_bak = luks_folder.join(format!("{full_name}.luks.bak"));
|
||||
Command::new("cryptsetup")
|
||||
.arg("-q")
|
||||
.arg("luksHeaderBackup")
|
||||
.arg("--header-backup-file")
|
||||
.arg(&tmp_luks_bak)
|
||||
.arg(&orig_path)
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
tokio::fs::rename(&tmp_luks_bak, &luks_bak).await?;
|
||||
}
|
||||
|
||||
mount(&blockdev_path, datadir.as_ref().join(name), ReadWrite).await?;
|
||||
|
||||
Ok(reboot)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount_all_fs<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
repair: RepairStrategy,
|
||||
password: Option<&str>,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
let mut reboot = RequiresReboot(false);
|
||||
reboot |= mount_fs(guid, &datadir, "main", repair, password).await?;
|
||||
reboot |= mount_fs(guid, &datadir, "package-data", repair, password).await?;
|
||||
Ok(reboot)
|
||||
}
|
||||
118
core/startos/src/disk/mod.rs
Normal file
118
core/startos/src/disk/mod.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::ArgMatches;
|
||||
use rpc_toolkit::command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::disk::util::DiskInfo;
|
||||
use crate::util::display_none;
|
||||
use crate::util::serde::{display_serializable, IoFormat};
|
||||
use crate::Error;
|
||||
|
||||
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/embassy/config/repair-disk";
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct OsPartitionInfo {
|
||||
pub efi: Option<PathBuf>,
|
||||
pub bios: Option<PathBuf>,
|
||||
pub boot: PathBuf,
|
||||
pub root: 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()
|
||||
}
|
||||
}
|
||||
|
||||
#[command(subcommands(list, repair))]
|
||||
pub fn disk() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_disk_info(info: Vec<DiskInfo>, matches: &ArgMatches) {
|
||||
use prettytable::*;
|
||||
|
||||
if matches.is_present("format") {
|
||||
return display_serializable(info, matches);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc =>
|
||||
"LOGICALNAME",
|
||||
"LABEL",
|
||||
"CAPACITY",
|
||||
"USED",
|
||||
"EMBASSY OS VERSION"
|
||||
]);
|
||||
for disk in info {
|
||||
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))
|
||||
.as_ref()
|
||||
{
|
||||
used
|
||||
} else {
|
||||
"N/A"
|
||||
},
|
||||
if let Some(eos) = part.embassy_os.as_ref() {
|
||||
eos.version.as_str()
|
||||
} else {
|
||||
"N/A"
|
||||
},
|
||||
];
|
||||
table.add_row(row);
|
||||
}
|
||||
}
|
||||
table.print_tty(false).unwrap();
|
||||
}
|
||||
|
||||
#[command(display(display_disk_info))]
|
||||
pub async fn list(
|
||||
#[context] ctx: RpcContext,
|
||||
#[allow(unused_variables)]
|
||||
#[arg]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<Vec<DiskInfo>, Error> {
|
||||
crate::disk::util::list(&ctx.os_partitions).await
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
pub async fn repair() -> Result<(), Error> {
|
||||
tokio::fs::write(REPAIR_DISK_PATH, b"").await?;
|
||||
Ok(())
|
||||
}
|
||||
262
core/startos/src/disk/mount/backup.rs
Normal file
262
core/startos/src/disk/mount/backup.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use helpers::AtomicFile;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::filesystem::ecryptfs::EcryptFS;
|
||||
use super::guard::{GenericMountGuard, TmpMountGuard};
|
||||
use super::util::{bind, unmount};
|
||||
use crate::auth::check_password;
|
||||
use crate::backup::target::BackupInfo;
|
||||
use crate::disk::mount::filesystem::ReadWrite;
|
||||
use crate::disk::util::EmbassyOsRecoveryInfo;
|
||||
use crate::middleware::encrypt::{decrypt_slice, encrypt_slice};
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::IoFormat;
|
||||
use crate::util::FileLock;
|
||||
use crate::volume::BACKUP_DIR;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
pub struct BackupMountGuard<G: GenericMountGuard> {
|
||||
backup_disk_mount_guard: Option<G>,
|
||||
encrypted_guard: Option<TmpMountGuard>,
|
||||
enc_key: String,
|
||||
pub unencrypted_metadata: EmbassyOsRecoveryInfo,
|
||||
pub metadata: BackupInfo,
|
||||
}
|
||||
impl<G: GenericMountGuard> BackupMountGuard<G> {
|
||||
fn backup_disk_path(&self) -> &Path {
|
||||
if let Some(guard) = &self.backup_disk_mount_guard {
|
||||
guard.as_ref()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount(backup_disk_mount_guard: G, password: &str) -> Result<Self, Error> {
|
||||
let backup_disk_path = backup_disk_mount_guard.as_ref();
|
||||
let unencrypted_metadata_path =
|
||||
backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor");
|
||||
let mut unencrypted_metadata: EmbassyOsRecoveryInfo =
|
||||
if tokio::fs::metadata(&unencrypted_metadata_path)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
IoFormat::Cbor.from_slice(
|
||||
&tokio::fs::read(&unencrypted_metadata_path)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
unencrypted_metadata_path.display().to_string(),
|
||||
)
|
||||
})?,
|
||||
)?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let enc_key = if let (Some(hash), Some(wrapped_key)) = (
|
||||
unencrypted_metadata.password_hash.as_ref(),
|
||||
unencrypted_metadata.wrapped_key.as_ref(),
|
||||
) {
|
||||
let wrapped_key =
|
||||
base32::decode(base32::Alphabet::RFC4648 { padding: true }, wrapped_key)
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("failed to decode wrapped key"),
|
||||
crate::ErrorKind::Backup,
|
||||
)
|
||||
})?;
|
||||
check_password(hash, password)?;
|
||||
String::from_utf8(decrypt_slice(wrapped_key, password))?
|
||||
} else {
|
||||
base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&rand::random::<[u8; 32]>()[..],
|
||||
)
|
||||
};
|
||||
|
||||
if unencrypted_metadata.password_hash.is_none() {
|
||||
unencrypted_metadata.password_hash = Some(
|
||||
argon2::hash_encoded(
|
||||
password.as_bytes(),
|
||||
&rand::random::<[u8; 16]>()[..],
|
||||
&argon2::Config::rfc9106_low_mem(),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::PasswordHashGeneration)?,
|
||||
);
|
||||
}
|
||||
if unencrypted_metadata.wrapped_key.is_none() {
|
||||
unencrypted_metadata.wrapped_key = Some(base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: true },
|
||||
&encrypt_slice(&enc_key, password),
|
||||
));
|
||||
}
|
||||
|
||||
let crypt_path = backup_disk_path.join("EmbassyBackups/crypt");
|
||||
if tokio::fs::metadata(&crypt_path).await.is_err() {
|
||||
tokio::fs::create_dir_all(&crypt_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
crypt_path.display().to_string(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let encrypted_guard =
|
||||
TmpMountGuard::mount(&EcryptFS::new(&crypt_path, &enc_key), ReadWrite).await?;
|
||||
|
||||
let metadata_path = encrypted_guard.as_ref().join("metadata.cbor");
|
||||
let metadata: BackupInfo = if tokio::fs::metadata(&metadata_path).await.is_ok() {
|
||||
IoFormat::Cbor.from_slice(&tokio::fs::read(&metadata_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
metadata_path.display().to_string(),
|
||||
)
|
||||
})?)?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
backup_disk_mount_guard: Some(backup_disk_mount_guard),
|
||||
encrypted_guard: Some(encrypted_guard),
|
||||
enc_key,
|
||||
unencrypted_metadata,
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn change_password(&mut self, new_password: &str) -> Result<(), Error> {
|
||||
self.unencrypted_metadata.password_hash = Some(
|
||||
argon2::hash_encoded(
|
||||
new_password.as_bytes(),
|
||||
&rand::random::<[u8; 16]>()[..],
|
||||
&argon2::Config::rfc9106_low_mem(),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::PasswordHashGeneration)?,
|
||||
);
|
||||
self.unencrypted_metadata.wrapped_key = Some(base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&encrypt_slice(&self.enc_key, new_password),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount_package_backup(
|
||||
&self,
|
||||
id: &PackageId,
|
||||
) -> Result<PackageBackupMountGuard, Error> {
|
||||
let lock = FileLock::new(Path::new(BACKUP_DIR).join(format!("{}.lock", id)), false).await?;
|
||||
let mountpoint = Path::new(BACKUP_DIR).join(id);
|
||||
bind(self.as_ref().join(id), &mountpoint, false).await?;
|
||||
Ok(PackageBackupMountGuard {
|
||||
mountpoint: Some(mountpoint),
|
||||
lock: Some(lock),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn save(&self) -> Result<(), Error> {
|
||||
let metadata_path = self.as_ref().join("metadata.cbor");
|
||||
let backup_disk_path = self.backup_disk_path();
|
||||
let mut file = AtomicFile::new(&metadata_path, None::<PathBuf>)
|
||||
.await
|
||||
.with_kind(ErrorKind::Filesystem)?;
|
||||
file.write_all(&IoFormat::Cbor.to_vec(&self.metadata)?)
|
||||
.await?;
|
||||
file.save().await.with_kind(ErrorKind::Filesystem)?;
|
||||
let unencrypted_metadata_path =
|
||||
backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor");
|
||||
let mut file = AtomicFile::new(&unencrypted_metadata_path, None::<PathBuf>)
|
||||
.await
|
||||
.with_kind(ErrorKind::Filesystem)?;
|
||||
file.write_all(&IoFormat::Cbor.to_vec(&self.unencrypted_metadata)?)
|
||||
.await?;
|
||||
file.save().await.with_kind(ErrorKind::Filesystem)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn unmount(mut self) -> Result<(), Error> {
|
||||
if let Some(guard) = self.encrypted_guard.take() {
|
||||
guard.unmount().await?;
|
||||
}
|
||||
if let Some(guard) = self.backup_disk_mount_guard.take() {
|
||||
guard.unmount().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn save_and_unmount(self) -> Result<(), Error> {
|
||||
self.save().await?;
|
||||
self.unmount().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl<G: GenericMountGuard> AsRef<Path> for BackupMountGuard<G> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
if let Some(guard) = &self.encrypted_guard {
|
||||
guard.as_ref()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<G: GenericMountGuard> Drop for BackupMountGuard<G> {
|
||||
fn drop(&mut self) {
|
||||
let first = self.encrypted_guard.take();
|
||||
let second = self.backup_disk_mount_guard.take();
|
||||
tokio::spawn(async move {
|
||||
if let Some(guard) = first {
|
||||
guard.unmount().await.unwrap();
|
||||
}
|
||||
if let Some(guard) = second {
|
||||
guard.unmount().await.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PackageBackupMountGuard {
|
||||
mountpoint: Option<PathBuf>,
|
||||
lock: Option<FileLock>,
|
||||
}
|
||||
impl PackageBackupMountGuard {
|
||||
pub async fn unmount(mut self) -> Result<(), Error> {
|
||||
if let Some(mountpoint) = self.mountpoint.take() {
|
||||
unmount(&mountpoint).await?;
|
||||
}
|
||||
if let Some(lock) = self.lock.take() {
|
||||
lock.unlock().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for PackageBackupMountGuard {
|
||||
fn as_ref(&self) -> &Path {
|
||||
if let Some(mountpoint) = &self.mountpoint {
|
||||
mountpoint
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Drop for PackageBackupMountGuard {
|
||||
fn drop(&mut self) {
|
||||
let mountpoint = self.mountpoint.take();
|
||||
let lock = self.lock.take();
|
||||
tokio::spawn(async move {
|
||||
if let Some(mountpoint) = mountpoint {
|
||||
unmount(&mountpoint).await.unwrap();
|
||||
}
|
||||
if let Some(lock) = lock {
|
||||
lock.unlock().await.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
54
core/startos/src/disk/mount/filesystem/bind.rs
Normal file
54
core/startos/src/disk/mount/filesystem/bind.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType, ReadOnly};
|
||||
use crate::disk::mount::util::bind;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub struct Bind<SrcDir: AsRef<Path>> {
|
||||
src_dir: SrcDir,
|
||||
}
|
||||
impl<SrcDir: AsRef<Path>> Bind<SrcDir> {
|
||||
pub fn new(src_dir: SrcDir) -> Self {
|
||||
Self { src_dir }
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl<SrcDir: AsRef<Path> + Send + Sync> FileSystem for Bind<SrcDir> {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
bind(
|
||||
self.src_dir.as_ref(),
|
||||
mountpoint,
|
||||
matches!(mount_type, ReadOnly),
|
||||
)
|
||||
.await
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("Bind");
|
||||
sha.update(
|
||||
tokio::fs::canonicalize(self.src_dir.as_ref())
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
self.src_dir.as_ref().display().to_string(),
|
||||
)
|
||||
})?
|
||||
.as_os_str()
|
||||
.as_bytes(),
|
||||
);
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
67
core/startos/src/disk/mount/filesystem/block_dev.rs
Normal file
67
core/startos/src/disk/mount/filesystem/block_dev.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType, ReadOnly};
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub async fn mount(
|
||||
logicalname: impl AsRef<Path>,
|
||||
mountpoint: impl AsRef<Path>,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||
let mut cmd = tokio::process::Command::new("mount");
|
||||
cmd.arg(logicalname.as_ref()).arg(mountpoint.as_ref());
|
||||
if mount_type == ReadOnly {
|
||||
cmd.arg("-o").arg("ro");
|
||||
}
|
||||
cmd.invoke(crate::ErrorKind::Filesystem).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct BlockDev<LogicalName: AsRef<Path>> {
|
||||
logicalname: LogicalName,
|
||||
}
|
||||
impl<LogicalName: AsRef<Path>> BlockDev<LogicalName> {
|
||||
pub fn new(logicalname: LogicalName) -> Self {
|
||||
BlockDev { logicalname }
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl<LogicalName: AsRef<Path> + Send + Sync> FileSystem for BlockDev<LogicalName> {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
mount(self.logicalname.as_ref(), mountpoint, mount_type).await
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("BlockDev");
|
||||
sha.update(
|
||||
tokio::fs::canonicalize(self.logicalname.as_ref())
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
self.logicalname.as_ref().display().to_string(),
|
||||
)
|
||||
})?
|
||||
.as_os_str()
|
||||
.as_bytes(),
|
||||
);
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
107
core/startos/src/disk/mount/filesystem/cifs.rs
Normal file
107
core/startos/src/disk/mount/filesystem/cifs.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::net::IpAddr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{FileSystem, MountType, ReadOnly};
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
async fn resolve_hostname(hostname: &str) -> Result<IpAddr, Error> {
|
||||
if let Ok(addr) = hostname.parse() {
|
||||
return Ok(addr);
|
||||
}
|
||||
if hostname.ends_with(".local") {
|
||||
return Ok(IpAddr::V4(crate::net::mdns::resolve_mdns(hostname).await?));
|
||||
}
|
||||
Ok(String::from_utf8(
|
||||
Command::new("nmblookup")
|
||||
.arg(hostname)
|
||||
.invoke(crate::ErrorKind::Network)
|
||||
.await?,
|
||||
)?
|
||||
.split(" ")
|
||||
.next()
|
||||
.unwrap()
|
||||
.trim()
|
||||
.parse()?)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount_cifs(
|
||||
hostname: &str,
|
||||
path: impl AsRef<Path>,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
mountpoint: impl AsRef<Path>,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||
let ip: IpAddr = resolve_hostname(hostname).await?;
|
||||
let absolute_path = Path::new("/").join(path.as_ref());
|
||||
let mut cmd = Command::new("mount");
|
||||
cmd.arg("-t")
|
||||
.arg("cifs")
|
||||
.env("USER", username)
|
||||
.env("PASSWD", password.unwrap_or_default())
|
||||
.arg(format!("//{}{}", ip, absolute_path.display()))
|
||||
.arg(mountpoint.as_ref());
|
||||
if mount_type == ReadOnly {
|
||||
cmd.arg("-o").arg("ro,noserverino");
|
||||
} else {
|
||||
cmd.arg("-o").arg("noserverino");
|
||||
}
|
||||
cmd.invoke(crate::ErrorKind::Filesystem).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Cifs {
|
||||
pub hostname: String,
|
||||
pub path: PathBuf,
|
||||
pub username: String,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
impl Cifs {
|
||||
pub async fn mountable(&self) -> Result<(), Error> {
|
||||
let guard = TmpMountGuard::mount(self, ReadOnly).await?;
|
||||
guard.unmount().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl FileSystem for Cifs {
|
||||
async fn mount<P: AsRef<std::path::Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
mount_cifs(
|
||||
&self.hostname,
|
||||
&self.path,
|
||||
&self.username,
|
||||
self.password.as_ref().map(|p| p.as_str()),
|
||||
mountpoint,
|
||||
mount_type,
|
||||
)
|
||||
.await
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("Cifs");
|
||||
sha.update(self.hostname.as_bytes());
|
||||
sha.update(self.path.as_os_str().as_bytes());
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
71
core/startos/src/disk/mount/filesystem/ecryptfs.rs
Normal file
71
core/startos/src/disk/mount/filesystem/ecryptfs.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType};
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub async fn mount_ecryptfs<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
src: P0,
|
||||
dst: P1,
|
||||
key: &str,
|
||||
) -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(dst.as_ref()).await?;
|
||||
tokio::process::Command::new("mount")
|
||||
.arg("-t")
|
||||
.arg("ecryptfs")
|
||||
.arg(src.as_ref())
|
||||
.arg(dst.as_ref())
|
||||
.arg("-o")
|
||||
// for more information `man ecryptfs`
|
||||
.arg(format!("key=passphrase:passphrase_passwd={},ecryptfs_cipher=aes,ecryptfs_key_bytes=32,ecryptfs_passthrough=n,ecryptfs_enable_filename_crypto=y,no_sig_cache", key))
|
||||
.input(Some(&mut std::io::Cursor::new(b"\n")))
|
||||
.invoke(crate::ErrorKind::Filesystem).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct EcryptFS<EncryptedDir: AsRef<Path>, Key: AsRef<str>> {
|
||||
encrypted_dir: EncryptedDir,
|
||||
key: Key,
|
||||
}
|
||||
impl<EncryptedDir: AsRef<Path>, Key: AsRef<str>> EcryptFS<EncryptedDir, Key> {
|
||||
pub fn new(encrypted_dir: EncryptedDir, key: Key) -> Self {
|
||||
EcryptFS { encrypted_dir, key }
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl<EncryptedDir: AsRef<Path> + Send + Sync, Key: AsRef<str> + Send + Sync> FileSystem
|
||||
for EcryptFS<EncryptedDir, Key>
|
||||
{
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
_mount_type: MountType, // ignored - inherited from parent fs
|
||||
) -> Result<(), Error> {
|
||||
mount_ecryptfs(self.encrypted_dir.as_ref(), mountpoint, self.key.as_ref()).await
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("EcryptFS");
|
||||
sha.update(
|
||||
tokio::fs::canonicalize(self.encrypted_dir.as_ref())
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
self.encrypted_dir.as_ref().display().to_string(),
|
||||
)
|
||||
})?
|
||||
.as_os_str()
|
||||
.as_bytes(),
|
||||
);
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
39
core/startos/src/disk/mount/filesystem/efivarfs.rs
Normal file
39
core/startos/src/disk/mount/filesystem/efivarfs.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType, ReadOnly};
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
pub struct EfiVarFs;
|
||||
#[async_trait]
|
||||
impl FileSystem for EfiVarFs {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||
let mut cmd = tokio::process::Command::new("mount");
|
||||
cmd.arg("-t")
|
||||
.arg("efivarfs")
|
||||
.arg("efivarfs")
|
||||
.arg(mountpoint.as_ref());
|
||||
if mount_type == ReadOnly {
|
||||
cmd.arg("-o").arg("ro");
|
||||
}
|
||||
cmd.invoke(crate::ErrorKind::Filesystem).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("EfiVarFs");
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
52
core/startos/src/disk/mount/filesystem/httpdirfs.rs
Normal file
52
core/startos/src/disk/mount/filesystem/httpdirfs.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType};
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
pub async fn mount_httpdirfs(url: &Url, mountpoint: impl AsRef<Path>) -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||
let mut cmd = tokio::process::Command::new("httpdirfs");
|
||||
cmd.arg("--cache")
|
||||
.arg("--single-file-mode")
|
||||
.arg(url.as_str())
|
||||
.arg(mountpoint.as_ref());
|
||||
cmd.invoke(crate::ErrorKind::Filesystem).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct HttpDirFS {
|
||||
url: Url,
|
||||
}
|
||||
impl HttpDirFS {
|
||||
pub fn new(url: Url) -> Self {
|
||||
HttpDirFS { url }
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl FileSystem for HttpDirFS {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
_mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
mount_httpdirfs(&self.url, mountpoint).await
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("HttpDirFS");
|
||||
sha.update(self.url.as_str());
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
52
core/startos/src/disk/mount/filesystem/label.rs
Normal file
52
core/startos/src/disk/mount/filesystem/label.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType, ReadOnly};
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
pub async fn mount_label(
|
||||
label: &str,
|
||||
mountpoint: impl AsRef<Path>,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||
let mut cmd = tokio::process::Command::new("mount");
|
||||
cmd.arg("-L").arg(label).arg(mountpoint.as_ref());
|
||||
if mount_type == ReadOnly {
|
||||
cmd.arg("-o").arg("ro");
|
||||
}
|
||||
cmd.invoke(crate::ErrorKind::Filesystem).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Label<S: AsRef<str>> {
|
||||
label: S,
|
||||
}
|
||||
impl<S: AsRef<str>> Label<S> {
|
||||
pub fn new(label: S) -> Self {
|
||||
Label { label }
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl<S: AsRef<str> + Send + Sync> FileSystem for Label<S> {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
mount_label(self.label.as_ref(), mountpoint, mount_type).await
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("Label");
|
||||
sha.update(self.label.as_ref().as_bytes());
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
89
core/startos/src/disk/mount/filesystem/loop_dev.rs
Normal file
89
core/startos/src/disk/mount/filesystem/loop_dev.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType, ReadOnly};
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub async fn mount(
|
||||
logicalname: impl AsRef<Path>,
|
||||
offset: u64,
|
||||
size: u64,
|
||||
mountpoint: impl AsRef<Path>,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||
let mut opts = format!("loop,offset={offset},sizelimit={size}");
|
||||
if mount_type == ReadOnly {
|
||||
opts += ",ro";
|
||||
}
|
||||
|
||||
tokio::process::Command::new("mount")
|
||||
.arg(logicalname.as_ref())
|
||||
.arg(mountpoint.as_ref())
|
||||
.arg("-o")
|
||||
.arg(opts)
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LoopDev<LogicalName: AsRef<Path>> {
|
||||
logicalname: LogicalName,
|
||||
offset: u64,
|
||||
size: u64,
|
||||
}
|
||||
impl<LogicalName: AsRef<Path>> LoopDev<LogicalName> {
|
||||
pub fn new(logicalname: LogicalName, offset: u64, size: u64) -> Self {
|
||||
Self {
|
||||
logicalname,
|
||||
offset,
|
||||
size,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl<LogicalName: AsRef<Path> + Send + Sync> FileSystem for LoopDev<LogicalName> {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
mount(
|
||||
self.logicalname.as_ref(),
|
||||
self.offset,
|
||||
self.size,
|
||||
mountpoint,
|
||||
mount_type,
|
||||
)
|
||||
.await
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("LoopDev");
|
||||
sha.update(
|
||||
tokio::fs::canonicalize(self.logicalname.as_ref())
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
self.logicalname.as_ref().display().to_string(),
|
||||
)
|
||||
})?
|
||||
.as_os_str()
|
||||
.as_bytes(),
|
||||
);
|
||||
sha.update(&u64::to_be_bytes(self.offset)[..]);
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
37
core/startos/src/disk/mount/filesystem/mod.rs
Normal file
37
core/startos/src/disk/mount/filesystem/mod.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::OutputSizeUser;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
pub mod bind;
|
||||
pub mod block_dev;
|
||||
pub mod cifs;
|
||||
pub mod ecryptfs;
|
||||
pub mod efivarfs;
|
||||
pub mod httpdirfs;
|
||||
pub mod label;
|
||||
pub mod loop_dev;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum MountType {
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
pub use MountType::*;
|
||||
|
||||
#[async_trait]
|
||||
pub trait FileSystem {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error>;
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error>;
|
||||
}
|
||||
142
core/startos/src/disk/mount/guard.rs
Normal file
142
core/startos/src/disk/mount/guard.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use models::ResultExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::filesystem::{FileSystem, MountType, ReadOnly, ReadWrite};
|
||||
use super::util::unmount;
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
pub const TMP_MOUNTPOINT: &'static str = "/media/embassy/tmp";
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait GenericMountGuard: AsRef<Path> + std::fmt::Debug + Send + Sync + 'static {
|
||||
async fn unmount(mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MountGuard {
|
||||
mountpoint: PathBuf,
|
||||
mounted: bool,
|
||||
}
|
||||
impl MountGuard {
|
||||
pub async fn mount(
|
||||
filesystem: &impl FileSystem,
|
||||
mountpoint: impl AsRef<Path>,
|
||||
mount_type: MountType,
|
||||
) -> Result<Self, Error> {
|
||||
let mountpoint = mountpoint.as_ref().to_owned();
|
||||
filesystem.mount(&mountpoint, mount_type).await?;
|
||||
Ok(MountGuard {
|
||||
mountpoint,
|
||||
mounted: true,
|
||||
})
|
||||
}
|
||||
pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> {
|
||||
if self.mounted {
|
||||
unmount(&self.mountpoint).await?;
|
||||
if delete_mountpoint {
|
||||
match tokio::fs::remove_dir(&self.mountpoint).await {
|
||||
Err(e) if e.raw_os_error() == Some(39) => Ok(()), // directory not empty
|
||||
a => a,
|
||||
}
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
format!("rm {}", self.mountpoint.display()),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
self.mounted = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for MountGuard {
|
||||
fn as_ref(&self) -> &Path {
|
||||
&self.mountpoint
|
||||
}
|
||||
}
|
||||
impl Drop for MountGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.mounted {
|
||||
let mountpoint = std::mem::take(&mut self.mountpoint);
|
||||
tokio::spawn(async move { unmount(mountpoint).await.unwrap() });
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl GenericMountGuard for MountGuard {
|
||||
async fn unmount(mut self) -> Result<(), Error> {
|
||||
MountGuard::unmount(self, false).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn tmp_mountpoint(source: &impl FileSystem) -> Result<PathBuf, Error> {
|
||||
Ok(Path::new(TMP_MOUNTPOINT).join(base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&source.source_hash().await?,
|
||||
)))
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref TMP_MOUNTS: Mutex<BTreeMap<PathBuf, (MountType, Weak<MountGuard>)>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TmpMountGuard {
|
||||
guard: Arc<MountGuard>,
|
||||
}
|
||||
impl TmpMountGuard {
|
||||
/// DRAGONS: if you try to mount something as ro and rw at the same time, the ro mount will be upgraded to rw.
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount(filesystem: &impl FileSystem, mount_type: MountType) -> Result<Self, Error> {
|
||||
let mountpoint = tmp_mountpoint(filesystem).await?;
|
||||
let mut tmp_mounts = TMP_MOUNTS.lock().await;
|
||||
if !tmp_mounts.contains_key(&mountpoint) {
|
||||
tmp_mounts.insert(mountpoint.clone(), (mount_type, Weak::new()));
|
||||
}
|
||||
let (prev_mt, weak_slot) = tmp_mounts.get_mut(&mountpoint).unwrap();
|
||||
if let Some(guard) = weak_slot.upgrade() {
|
||||
// upgrade to rw
|
||||
if *prev_mt == ReadOnly && mount_type == ReadWrite {
|
||||
tokio::process::Command::new("mount")
|
||||
.arg("-o")
|
||||
.arg("remount,rw")
|
||||
.arg(&mountpoint)
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?;
|
||||
*prev_mt = ReadWrite;
|
||||
}
|
||||
Ok(TmpMountGuard { guard })
|
||||
} else {
|
||||
let guard = Arc::new(MountGuard::mount(filesystem, &mountpoint, mount_type).await?);
|
||||
*weak_slot = Arc::downgrade(&guard);
|
||||
*prev_mt = mount_type;
|
||||
Ok(TmpMountGuard { guard })
|
||||
}
|
||||
}
|
||||
pub async fn unmount(self) -> Result<(), Error> {
|
||||
if let Ok(guard) = Arc::try_unwrap(self.guard) {
|
||||
guard.unmount(true).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for TmpMountGuard {
|
||||
fn as_ref(&self) -> &Path {
|
||||
(&*self.guard).as_ref()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl GenericMountGuard for TmpMountGuard {
|
||||
async fn unmount(mut self) -> Result<(), Error> {
|
||||
TmpMountGuard::unmount(self).await
|
||||
}
|
||||
}
|
||||
4
core/startos/src/disk/mount/mod.rs
Normal file
4
core/startos/src/disk/mount/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod backup;
|
||||
pub mod filesystem;
|
||||
pub mod guard;
|
||||
pub mod util;
|
||||
52
core/startos/src/disk/mount/util.rs
Normal file
52
core/startos/src/disk/mount/util.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::path::Path;
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
src: P0,
|
||||
dst: P1,
|
||||
read_only: bool,
|
||||
) -> Result<(), Error> {
|
||||
tracing::info!(
|
||||
"Binding {} to {}",
|
||||
src.as_ref().display(),
|
||||
dst.as_ref().display()
|
||||
);
|
||||
let is_mountpoint = tokio::process::Command::new("mountpoint")
|
||||
.arg(dst.as_ref())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.await?;
|
||||
if is_mountpoint.success() {
|
||||
unmount(dst.as_ref()).await?;
|
||||
}
|
||||
tokio::fs::create_dir_all(&src).await?;
|
||||
tokio::fs::create_dir_all(&dst).await?;
|
||||
let mut mount_cmd = tokio::process::Command::new("mount");
|
||||
mount_cmd.arg("--bind");
|
||||
if read_only {
|
||||
mount_cmd.arg("-o").arg("ro");
|
||||
}
|
||||
mount_cmd
|
||||
.arg(src.as_ref())
|
||||
.arg(dst.as_ref())
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn unmount<P: AsRef<Path>>(mountpoint: P) -> Result<(), Error> {
|
||||
tracing::debug!("Unmounting {}.", mountpoint.as_ref().display());
|
||||
tokio::process::Command::new("umount")
|
||||
.arg("-l")
|
||||
.arg(mountpoint.as_ref())
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
489
core/startos/src/disk/util.rs
Normal file
489
core/startos/src/disk/util.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use color_eyre::eyre::{self, eyre};
|
||||
use futures::TryStreamExt;
|
||||
use nom::bytes::complete::{tag, take_till1};
|
||||
use nom::character::complete::multispace1;
|
||||
use nom::character::is_space;
|
||||
use nom::combinator::{opt, rest};
|
||||
use nom::sequence::{pair, preceded, terminated};
|
||||
use nom::IResult;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::mount::filesystem::block_dev::BlockDev;
|
||||
use super::mount::filesystem::ReadOnly;
|
||||
use super::mount::guard::TmpMountGuard;
|
||||
use crate::disk::OsPartitionInfo;
|
||||
use crate::util::serde::IoFormat;
|
||||
use crate::util::{Invoke, Version};
|
||||
use crate::{Error, ResultExt as _};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PartitionTable {
|
||||
Mbr,
|
||||
Gpt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DiskInfo {
|
||||
pub logicalname: PathBuf,
|
||||
pub partition_table: Option<PartitionTable>,
|
||||
pub vendor: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub partitions: Vec<PartitionInfo>,
|
||||
pub capacity: u64,
|
||||
pub guid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PartitionInfo {
|
||||
pub logicalname: PathBuf,
|
||||
pub label: Option<String>,
|
||||
pub capacity: u64,
|
||||
pub used: Option<u64>,
|
||||
pub embassy_os: Option<EmbassyOsRecoveryInfo>,
|
||||
pub guid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct EmbassyOsRecoveryInfo {
|
||||
pub version: Version,
|
||||
pub full: bool,
|
||||
pub password_hash: Option<String>,
|
||||
pub wrapped_key: Option<String>,
|
||||
}
|
||||
|
||||
const DISK_PATH: &str = "/dev/disk/by-path";
|
||||
const SYS_BLOCK_PATH: &str = "/sys/block";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref PARTITION_REGEX: Regex = Regex::new("-part[0-9]+$").unwrap();
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_partition_table<P: AsRef<Path>>(path: P) -> Result<Option<PartitionTable>, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("fdisk")
|
||||
.arg("-l")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::BlockDevice)
|
||||
.await?,
|
||||
)?
|
||||
.lines()
|
||||
.find_map(|l| l.strip_prefix("Disklabel type:"))
|
||||
.and_then(|t| match t.trim() {
|
||||
"dos" => Some(PartitionTable::Mbr),
|
||||
"gpt" => Some(PartitionTable::Gpt),
|
||||
_ => None,
|
||||
}))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_vendor<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error> {
|
||||
let vendor = tokio::fs::read_to_string(
|
||||
Path::new(SYS_BLOCK_PATH)
|
||||
.join(path.as_ref().strip_prefix("/dev").map_err(|_| {
|
||||
Error::new(
|
||||
eyre!("not a canonical block device"),
|
||||
crate::ErrorKind::BlockDevice,
|
||||
)
|
||||
})?)
|
||||
.join("device")
|
||||
.join("vendor"),
|
||||
)
|
||||
.await?
|
||||
.trim()
|
||||
.to_owned();
|
||||
Ok(if vendor.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(vendor)
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_model<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error> {
|
||||
let model = tokio::fs::read_to_string(
|
||||
Path::new(SYS_BLOCK_PATH)
|
||||
.join(path.as_ref().strip_prefix("/dev").map_err(|_| {
|
||||
Error::new(
|
||||
eyre!("not a canonical block device"),
|
||||
crate::ErrorKind::BlockDevice,
|
||||
)
|
||||
})?)
|
||||
.join("device")
|
||||
.join("model"),
|
||||
)
|
||||
.await?
|
||||
.trim()
|
||||
.to_owned();
|
||||
Ok(if model.is_empty() { None } else { Some(model) })
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_capacity<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("blockdev")
|
||||
.arg("--getsize64")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::BlockDevice)
|
||||
.await?,
|
||||
)?
|
||||
.trim()
|
||||
.parse::<u64>()?)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_label<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error> {
|
||||
let label = String::from_utf8(
|
||||
Command::new("lsblk")
|
||||
.arg("-no")
|
||||
.arg("label")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::BlockDevice)
|
||||
.await?,
|
||||
)?
|
||||
.trim()
|
||||
.to_owned();
|
||||
Ok(if label.is_empty() { None } else { Some(label) })
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_used<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("df")
|
||||
.arg("--output=used")
|
||||
.arg("--block-size=1")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?,
|
||||
)?
|
||||
.lines()
|
||||
.skip(1)
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.parse::<u64>()?)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_available<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("df")
|
||||
.arg("--output=avail")
|
||||
.arg("--block-size=1")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?,
|
||||
)?
|
||||
.lines()
|
||||
.skip(1)
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.parse::<u64>()?)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_percentage<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("df")
|
||||
.arg("--output=pcent")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?,
|
||||
)?
|
||||
.lines()
|
||||
.skip(1)
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.strip_suffix("%")
|
||||
.unwrap()
|
||||
.parse::<u64>()?)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn pvscan() -> Result<BTreeMap<PathBuf, Option<String>>, Error> {
|
||||
let pvscan_out = Command::new("pvscan")
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
.await?;
|
||||
let pvscan_out_str = std::str::from_utf8(&pvscan_out)?;
|
||||
Ok(parse_pvscan_output(pvscan_out_str))
|
||||
}
|
||||
|
||||
pub async fn recovery_info(
|
||||
mountpoint: impl AsRef<Path>,
|
||||
) -> Result<Option<EmbassyOsRecoveryInfo>, Error> {
|
||||
let backup_unencrypted_metadata_path = mountpoint
|
||||
.as_ref()
|
||||
.join("EmbassyBackups/unencrypted-metadata.cbor");
|
||||
if tokio::fs::metadata(&backup_unencrypted_metadata_path)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(Some(
|
||||
IoFormat::Cbor.from_slice(
|
||||
&tokio::fs::read(&backup_unencrypted_metadata_path)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
backup_unencrypted_metadata_path.display().to_string(),
|
||||
)
|
||||
})?,
|
||||
)?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn list(os: &OsPartitionInfo) -> Result<Vec<DiskInfo>, Error> {
|
||||
struct DiskIndex {
|
||||
parts: BTreeSet<PathBuf>,
|
||||
internal: bool,
|
||||
}
|
||||
let disk_guids = pvscan().await?;
|
||||
let disks = tokio_stream::wrappers::ReadDirStream::new(
|
||||
tokio::fs::read_dir(DISK_PATH)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, DISK_PATH))?,
|
||||
)
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
eyre::Error::from(e).wrap_err(DISK_PATH),
|
||||
crate::ErrorKind::Filesystem,
|
||||
)
|
||||
})
|
||||
.try_fold(
|
||||
BTreeMap::<PathBuf, DiskIndex>::new(),
|
||||
|mut disks, dir_entry| async move {
|
||||
if let Some(disk_path) = dir_entry.path().file_name().and_then(|s| s.to_str()) {
|
||||
let (disk_path, part_path) = if let Some(end) = PARTITION_REGEX.find(disk_path) {
|
||||
(
|
||||
disk_path.strip_suffix(end.as_str()).unwrap_or_default(),
|
||||
Some(disk_path),
|
||||
)
|
||||
} else {
|
||||
(disk_path, None)
|
||||
};
|
||||
let disk_path = Path::new(DISK_PATH).join(disk_path);
|
||||
let disk = tokio::fs::canonicalize(&disk_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
disk_path.display().to_string(),
|
||||
)
|
||||
})?;
|
||||
let part = if let Some(part_path) = part_path {
|
||||
let part_path = Path::new(DISK_PATH).join(part_path);
|
||||
let part = tokio::fs::canonicalize(&part_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
part_path.display().to_string(),
|
||||
)
|
||||
})?;
|
||||
Some(part)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if !disks.contains_key(&disk) {
|
||||
disks.insert(
|
||||
disk.clone(),
|
||||
DiskIndex {
|
||||
parts: BTreeSet::new(),
|
||||
internal: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some(part) = part {
|
||||
if os.contains(&part) {
|
||||
disks.get_mut(&disk).unwrap().internal = true;
|
||||
} else {
|
||||
disks.get_mut(&disk).unwrap().parts.insert(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(disks)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut res = Vec::with_capacity(disks.len());
|
||||
for (disk, index) in disks {
|
||||
if index.internal {
|
||||
for part in index.parts {
|
||||
let mut disk_info = disk_info(disk.clone()).await;
|
||||
let part_info = part_info(part).await;
|
||||
disk_info.logicalname = part_info.logicalname.clone();
|
||||
disk_info.capacity = part_info.capacity;
|
||||
if let Some(g) = disk_guids.get(&disk_info.logicalname) {
|
||||
disk_info.guid = g.clone();
|
||||
} else {
|
||||
disk_info.partitions = vec![part_info];
|
||||
}
|
||||
res.push(disk_info);
|
||||
}
|
||||
} else {
|
||||
let mut disk_info = disk_info(disk).await;
|
||||
disk_info.partitions = Vec::with_capacity(index.parts.len());
|
||||
if let Some(g) = disk_guids.get(&disk_info.logicalname) {
|
||||
disk_info.guid = g.clone();
|
||||
} else {
|
||||
for part in index.parts {
|
||||
let mut part_info = part_info(part).await;
|
||||
if let Some(g) = disk_guids.get(&part_info.logicalname) {
|
||||
part_info.guid = g.clone();
|
||||
}
|
||||
disk_info.partitions.push(part_info);
|
||||
}
|
||||
}
|
||||
res.push(disk_info);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn disk_info(disk: PathBuf) -> DiskInfo {
|
||||
let partition_table = get_partition_table(&disk)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(
|
||||
"Could not get partition table of {}: {}",
|
||||
disk.display(),
|
||||
e.source
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let vendor = get_vendor(&disk)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get vendor of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let model = get_model(&disk)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get model of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let capacity = get_capacity(&disk)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get capacity of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
DiskInfo {
|
||||
logicalname: disk,
|
||||
partition_table,
|
||||
vendor,
|
||||
model,
|
||||
partitions: Vec::new(),
|
||||
capacity,
|
||||
guid: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn part_info(part: PathBuf) -> PartitionInfo {
|
||||
let mut embassy_os = None;
|
||||
let label = get_label(&part)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get label of {}: {}", part.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let capacity = get_capacity(&part)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get capacity of {}: {}", part.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let mut used = None;
|
||||
|
||||
match TmpMountGuard::mount(&BlockDev::new(&part), ReadOnly).await {
|
||||
Err(e) => tracing::warn!("Could not collect usage information: {}", e.source),
|
||||
Ok(mount_guard) => {
|
||||
used = get_used(&mount_guard)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Could not get usage of {}: {}", part.display(), e.source)
|
||||
})
|
||||
.ok();
|
||||
if let Some(recovery_info) = match recovery_info(&mount_guard).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::error!("Error fetching unencrypted backup metadata: {}", e);
|
||||
None
|
||||
}
|
||||
} {
|
||||
embassy_os = Some(recovery_info)
|
||||
}
|
||||
if let Err(e) = mount_guard.unmount().await {
|
||||
tracing::error!("Error unmounting partition {}: {}", part.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PartitionInfo {
|
||||
logicalname: part,
|
||||
label,
|
||||
capacity,
|
||||
used,
|
||||
embassy_os,
|
||||
guid: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_pvscan_output(pvscan_output: &str) -> BTreeMap<PathBuf, Option<String>> {
|
||||
fn parse_line(line: &str) -> IResult<&str, (&str, Option<&str>)> {
|
||||
let pv_parse = preceded(
|
||||
tag(" PV "),
|
||||
terminated(take_till1(|c| is_space(c as u8)), multispace1),
|
||||
);
|
||||
let vg_parse = preceded(
|
||||
opt(tag("is in exported ")),
|
||||
preceded(
|
||||
tag("VG "),
|
||||
terminated(take_till1(|c| is_space(c as u8)), multispace1),
|
||||
),
|
||||
);
|
||||
let mut parser = terminated(pair(pv_parse, opt(vg_parse)), rest);
|
||||
parser(line)
|
||||
}
|
||||
let lines = pvscan_output.lines();
|
||||
let n = lines.clone().count();
|
||||
let entries = lines.take(n.saturating_sub(1));
|
||||
let mut ret = BTreeMap::new();
|
||||
for entry in entries {
|
||||
match parse_line(entry) {
|
||||
Ok((_, (pv, vg))) => {
|
||||
ret.insert(PathBuf::from(pv), vg.map(|s| s.to_owned()));
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("Failed to parse pvscan output line: {}", entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pvscan_parser() {
|
||||
let s1 = r#" PV /dev/mapper/cryptdata VG data lvm2 [1.81 TiB / 0 free]
|
||||
PV /dev/sdb lvm2 [931.51 GiB]
|
||||
Total: 2 [2.72 TiB] / in use: 1 [1.81 TiB] / in no VG: 1 [931.51 GiB]
|
||||
"#;
|
||||
let s2 = r#" PV /dev/sdb VG EMBASSY_LZHJAENWGPCJJL6C6AXOD7OOOIJG7HFBV4GYRJH6HADXUCN4BRWQ lvm2 [931.51 GiB / 0 free]
|
||||
Total: 1 [931.51 GiB] / in use: 1 [931.51 GiB] / in no VG: 0 [0 ]
|
||||
"#;
|
||||
let s3 = r#" PV /dev/mapper/cryptdata VG data lvm2 [1.81 TiB / 0 free]
|
||||
Total: 1 [1.81 TiB] / in use: 1 [1.81 TiB] / in no VG: 0 [0 ]
|
||||
"#;
|
||||
let s4 = r#" PV /dev/sda is in exported VG EMBASSY_ZFHOCTYV3ZJMJW3OTFMG55LSQZLP667EDNZKDNUJKPJX5HE6S5HQ [931.51 GiB / 0 free]
|
||||
Total: 1 [931.51 GiB] / in use: 1 [931.51 GiB] / in no VG: 0 [0 ]
|
||||
"#;
|
||||
println!("{:?}", parse_pvscan_output(s1));
|
||||
println!("{:?}", parse_pvscan_output(s2));
|
||||
println!("{:?}", parse_pvscan_output(s3));
|
||||
println!("{:?}", parse_pvscan_output(s4));
|
||||
}
|
||||
Reference in New Issue
Block a user