setup flow mvp complete

This commit is contained in:
Aiden McClelland
2021-09-11 18:27:23 -06:00
parent 16dc0ffcf5
commit 7f194fbe0e
12 changed files with 359 additions and 119 deletions

View File

@@ -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(())

View File

@@ -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
}

View File

@@ -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,