mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
setup flow mvp complete
This commit is contained in:
@@ -3,49 +3,57 @@ use std::path::Path;
|
||||
use anyhow::anyhow;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::context::rpc::RpcContextConfig;
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub const PASSWORD_PATH: &'static str = "/etc/embassy/password";
|
||||
pub const DEFAULT_PASSWORD: &'static str = "password";
|
||||
|
||||
pub async fn create(
|
||||
cfg: &RpcContextConfig,
|
||||
disks: &[&str],
|
||||
pub async fn create<I: IntoIterator<Item = P>, P: AsRef<Path>>(
|
||||
pool_name: &str,
|
||||
disks: I,
|
||||
password: &str,
|
||||
) -> Result<String, Error> {
|
||||
let guid = create_pool(cfg, disks).await?;
|
||||
create_fs(cfg, password).await?;
|
||||
export(cfg).await?;
|
||||
let guid = create_pool(pool_name, disks).await?;
|
||||
create_fs(pool_name, password).await?;
|
||||
export(pool_name).await?;
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
pub async fn load(cfg: &RpcContextConfig, guid: &str, password: &str) -> Result<(), Error> {
|
||||
pub async fn load<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
pool_name: &str,
|
||||
datadir: P,
|
||||
password: &str,
|
||||
) -> Result<(), Error> {
|
||||
import(guid).await?;
|
||||
mount(cfg, password).await?;
|
||||
mount(pool_name, datadir, password).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_pool(cfg: &RpcContextConfig, disks: &[&str]) -> Result<String, Error> {
|
||||
Command::new("zpool")
|
||||
.arg("create")
|
||||
.arg(cfg.zfs_pool_name())
|
||||
.args(disks)
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
pub async fn create_pool<I: IntoIterator<Item = P>, P: AsRef<Path>>(
|
||||
pool_name: &str,
|
||||
disks: I,
|
||||
) -> Result<String, Error> {
|
||||
let mut cmd = Command::new("zpool");
|
||||
cmd.arg("create").arg(pool_name);
|
||||
for disk in disks {
|
||||
cmd.arg(disk.as_ref());
|
||||
}
|
||||
cmd.invoke(crate::ErrorKind::Zfs).await?;
|
||||
Ok(String::from_utf8(
|
||||
Command::new("zpool")
|
||||
.arg("get")
|
||||
.arg("-H")
|
||||
.arg("-ovalue")
|
||||
.arg("guid")
|
||||
.arg(cfg.zfs_pool_name())
|
||||
.arg(pool_name)
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub async fn create_fs(cfg: &RpcContextConfig, password: &str) -> Result<(), Error> {
|
||||
pub async fn create_fs(pool_name: &str, password: &str) -> Result<(), Error> {
|
||||
tokio::fs::write(PASSWORD_PATH, password)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
|
||||
@@ -59,14 +67,14 @@ pub async fn create_fs(cfg: &RpcContextConfig, password: &str) -> Result<(), Err
|
||||
.arg("keylocation=file:///etc/embassy/password")
|
||||
.arg("-o")
|
||||
.arg("keyformat=passphrase")
|
||||
.arg(format!("{}/main", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/main", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Command::new("zfs")
|
||||
.arg("create")
|
||||
.arg("-o")
|
||||
.arg("reservation=5G")
|
||||
.arg(format!("{}/updates", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/updates", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Command::new("zfs")
|
||||
@@ -77,7 +85,7 @@ pub async fn create_fs(cfg: &RpcContextConfig, password: &str) -> Result<(), Err
|
||||
.arg("keylocation=file:///etc/embassy/password")
|
||||
.arg("-o")
|
||||
.arg("keyformat=passphrase")
|
||||
.arg(format!("{}/package-data", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/package-data", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Command::new("zfs")
|
||||
@@ -88,7 +96,7 @@ pub async fn create_fs(cfg: &RpcContextConfig, password: &str) -> Result<(), Err
|
||||
.arg("keylocation=file:///etc/embassy/password")
|
||||
.arg("-o")
|
||||
.arg("keyformat=passphrase")
|
||||
.arg(format!("{}/tmp", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/tmp", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
tokio::fs::remove_file(PASSWORD_PATH)
|
||||
@@ -97,7 +105,7 @@ pub async fn create_fs(cfg: &RpcContextConfig, password: &str) -> Result<(), Err
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_swap(cfg: &RpcContextConfig) -> Result<(), Error> {
|
||||
pub async fn create_swap(pool_name: &str) -> Result<(), Error> {
|
||||
let pagesize = String::from_utf8(
|
||||
Command::new("getconf")
|
||||
.arg("PAGESIZE")
|
||||
@@ -121,32 +129,24 @@ pub async fn create_swap(cfg: &RpcContextConfig) -> Result<(), Error> {
|
||||
.await?;
|
||||
Command::new("mkswap")
|
||||
.arg("-f")
|
||||
.arg(
|
||||
Path::new("/dev/zvol")
|
||||
.join(cfg.zfs_pool_name())
|
||||
.join("swap"),
|
||||
)
|
||||
.arg(Path::new("/dev/zvol").join(pool_name).join("swap"))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn use_swap(cfg: &RpcContextConfig) -> Result<(), Error> {
|
||||
pub async fn use_swap(pool_name: &str) -> Result<(), Error> {
|
||||
Command::new("swapon")
|
||||
.arg(
|
||||
Path::new("/dev/zvol")
|
||||
.join(cfg.zfs_pool_name())
|
||||
.join("swap"),
|
||||
)
|
||||
.arg(Path::new("/dev/zvol").join(pool_name).join("swap"))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn export(cfg: &RpcContextConfig) -> Result<(), Error> {
|
||||
pub async fn export(pool_name: &str) -> Result<(), Error> {
|
||||
Command::new("zpool")
|
||||
.arg("export")
|
||||
.arg(cfg.zfs_pool_name())
|
||||
.arg(pool_name)
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -178,22 +178,26 @@ pub async fn import(guid: &str) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mount(cfg: &RpcContextConfig, password: &str) -> Result<(), Error> {
|
||||
pub async fn mount<P: AsRef<Path>>(
|
||||
pool_name: &str,
|
||||
datadir: P,
|
||||
password: &str,
|
||||
) -> Result<(), Error> {
|
||||
let mountpoint = String::from_utf8(
|
||||
Command::new("zfs")
|
||||
.arg("get")
|
||||
.arg("-H")
|
||||
.arg("-ovalue")
|
||||
.arg("mountpoint")
|
||||
.arg(cfg.zfs_pool_name())
|
||||
.arg(pool_name)
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?,
|
||||
)?;
|
||||
if Path::new(mountpoint.trim()) != &cfg.datadir() {
|
||||
if Path::new(mountpoint.trim()) != datadir.as_ref() {
|
||||
Command::new("zfs")
|
||||
.arg("set")
|
||||
.arg(format!("mountpoint={}", cfg.datadir().display()))
|
||||
.arg(cfg.zfs_pool_name())
|
||||
.arg(format!("mountpoint={}", datadir.as_ref().display()))
|
||||
.arg(pool_name)
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
}
|
||||
@@ -203,17 +207,17 @@ pub async fn mount(cfg: &RpcContextConfig, password: &str) -> Result<(), Error>
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
|
||||
Command::new("zfs")
|
||||
.arg("load-key")
|
||||
.arg(format!("{}/main", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/main", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Command::new("zfs")
|
||||
.arg("load-key")
|
||||
.arg(format!("{}/package-data", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/package-data", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Command::new("zfs")
|
||||
.arg("load-key")
|
||||
.arg(format!("{}/tmp", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/tmp", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
tokio::fs::remove_file(PASSWORD_PATH)
|
||||
@@ -222,17 +226,17 @@ pub async fn mount(cfg: &RpcContextConfig, password: &str) -> Result<(), Error>
|
||||
|
||||
Command::new("zfs")
|
||||
.arg("mount")
|
||||
.arg(format!("{}/main", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/main", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Command::new("zfs")
|
||||
.arg("mount")
|
||||
.arg(format!("{}/package-data", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/package-data", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Command::new("zfs")
|
||||
.arg("mount")
|
||||
.arg(format!("{}/tmp", cfg.zfs_pool_name()))
|
||||
.arg(format!("{}/tmp", pool_name))
|
||||
.invoke(crate::ErrorKind::Zfs)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,2 +1,77 @@
|
||||
use clap::ArgMatches;
|
||||
use rpc_toolkit::command;
|
||||
|
||||
use self::util::DiskInfo;
|
||||
use crate::util::{display_serializable, IoFormat};
|
||||
use crate::Error;
|
||||
|
||||
pub mod main;
|
||||
pub mod util;
|
||||
|
||||
#[command(subcommands(list))]
|
||||
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",
|
||||
if let Some(eos_info) = disk.embassy_os.as_ref() {
|
||||
eos_info.version.as_str()
|
||||
} else {
|
||||
"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"
|
||||
},
|
||||
"N/A",
|
||||
];
|
||||
table.add_row(row);
|
||||
}
|
||||
}
|
||||
table.print_tty(false);
|
||||
}
|
||||
|
||||
#[command(display(display_disk_info))]
|
||||
pub async fn list(
|
||||
#[allow(unused_variables)]
|
||||
#[arg]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<Vec<DiskInfo>, Error> {
|
||||
crate::disk::util::list().await
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use futures::future::try_join_all;
|
||||
use futures::TryStreamExt;
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use regex::Regex;
|
||||
use rpc_toolkit::command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::util::{Invoke, Version};
|
||||
use crate::util::{from_yaml_async_reader, GeneralGuard, Invoke, Version};
|
||||
use crate::{Error, ResultExt as _};
|
||||
|
||||
pub const TMP_MOUNTPOINT: &'static str = "/media/embassy-os";
|
||||
@@ -18,40 +17,78 @@ pub const TMP_MOUNTPOINT: &'static str = "/media/embassy-os";
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DiskInfo {
|
||||
logicalname: PathBuf,
|
||||
partitions: Vec<PartitionInfo>,
|
||||
capacity: usize,
|
||||
embassy_os: Option<PartitionInfo>,
|
||||
pub logicalname: PathBuf,
|
||||
pub vendor: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub partitions: Vec<PartitionInfo>,
|
||||
pub capacity: usize,
|
||||
pub embassy_os: Option<EmbassyOsDiskInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PartitionInfo {
|
||||
logicalname: PathBuf,
|
||||
label: Option<String>,
|
||||
capacity: usize,
|
||||
used: Option<usize>,
|
||||
pub logicalname: PathBuf,
|
||||
pub label: Option<String>,
|
||||
pub capacity: usize,
|
||||
pub used: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct EmbassyOsDiskInfo {
|
||||
version: Version,
|
||||
name: String,
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
const DISK_PATH: &'static str = "/dev/disk/by-path";
|
||||
const SYS_BLOCK_PATH: &'static str = "/sys/block";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref PARTITION_REGEX: Regex = Regex::new("-part[0-9]+$").unwrap();
|
||||
}
|
||||
|
||||
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(
|
||||
anyhow!("not a canonical block device"),
|
||||
crate::ErrorKind::BlockDevice,
|
||||
)
|
||||
})?)
|
||||
.join("device")
|
||||
.join("vendor"),
|
||||
)
|
||||
.await?;
|
||||
Ok(if vendor.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(vendor)
|
||||
})
|
||||
}
|
||||
|
||||
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(
|
||||
anyhow!("not a canonical block device"),
|
||||
crate::ErrorKind::BlockDevice,
|
||||
)
|
||||
})?)
|
||||
.join("device")
|
||||
.join("model"),
|
||||
)
|
||||
.await?;
|
||||
Ok(if model.is_empty() { None } else { Some(model) })
|
||||
}
|
||||
|
||||
pub async fn get_capacity<P: AsRef<Path>>(path: P) -> Result<usize, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("blockdev")
|
||||
Command::new("BlockDevice")
|
||||
.arg("--getsize64")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::BlockDev)
|
||||
.invoke(crate::ErrorKind::BlockDevice)
|
||||
.await?,
|
||||
)?
|
||||
.parse()?)
|
||||
@@ -63,7 +100,7 @@ pub async fn get_label<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error>
|
||||
.arg("-no")
|
||||
.arg("label")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::BlockDev) // TODO: error kind
|
||||
.invoke(crate::ErrorKind::BlockDevice)
|
||||
.await?,
|
||||
)?;
|
||||
Ok(if label.is_empty() { None } else { Some(label) })
|
||||
@@ -74,7 +111,7 @@ pub async fn get_used<P: AsRef<Path>>(path: P) -> Result<usize, Error> {
|
||||
Command::new("df")
|
||||
.arg("--output=used")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::Unknown)
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?,
|
||||
)?
|
||||
.lines()
|
||||
@@ -129,6 +166,14 @@ pub async fn list() -> Result<Vec<DiskInfo>, Error> {
|
||||
let mut res = Vec::with_capacity(disks.len());
|
||||
for (disk, parts) in disks {
|
||||
let mut partitions = Vec::with_capacity(parts.len());
|
||||
let vendor = get_vendor(&disk)
|
||||
.await
|
||||
.map_err(|e| log::warn!("Could not get vendor of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let model = get_model(&disk)
|
||||
.await
|
||||
.map_err(|e| log::warn!("Could not get model of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let capacity = get_capacity(&disk)
|
||||
.await
|
||||
.map_err(|e| log::warn!("Could not get capacity of {}: {}", disk.display(), e.source))
|
||||
@@ -148,14 +193,29 @@ pub async fn list() -> Result<Vec<DiskInfo>, Error> {
|
||||
if let Err(e) = mount(&part, &tmp_mountpoint).await {
|
||||
log::warn!("Could not collect usage information: {}", e.source)
|
||||
} else {
|
||||
let mount_guard = GeneralGuard::new(|| {
|
||||
let path = tmp_mountpoint.clone();
|
||||
tokio::spawn(unmount(path))
|
||||
});
|
||||
used = get_used(&tmp_mountpoint)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::warn!("Could not get usage of {}: {}", part.display(), e.source)
|
||||
})
|
||||
.ok();
|
||||
// todo!("check embassy-os");
|
||||
unmount(&tmp_mountpoint).await?; // TODO: mount guard
|
||||
if label.as_deref() == Some("rootfs") {
|
||||
let version_path = tmp_mountpoint.join("root").join("appmgr").join("version");
|
||||
if tokio::fs::metadata(&version_path).await.is_ok() {
|
||||
embassy_os = Some(EmbassyOsDiskInfo {
|
||||
version: from_yaml_async_reader(File::open(&version_path).await?)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
mount_guard
|
||||
.drop()
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Unknown)??;
|
||||
}
|
||||
|
||||
partitions.push(PartitionInfo {
|
||||
@@ -167,6 +227,8 @@ pub async fn list() -> Result<Vec<DiskInfo>, Error> {
|
||||
}
|
||||
res.push(DiskInfo {
|
||||
logicalname: disk,
|
||||
vendor,
|
||||
model,
|
||||
partitions,
|
||||
capacity,
|
||||
embassy_os,
|
||||
|
||||
Reference in New Issue
Block a user