install wizard project (#1893)

* install wizard project

* reboot endpoint

* Update frontend/projects/install-wizard/src/app/pages/home/home.page.ts

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

* Update frontend/projects/install-wizard/src/app/pages/home/home.page.ts

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

* Update frontend/projects/install-wizard/src/app/pages/home/home.page.ts

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

* update build

* fix build

* backend portion

* increase image size

* loaded

* dont auto resize

* fix install wizard

* use localhost if still in setup mode

* fix compat

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2022-11-07 07:30:57 -07:00
committed by Aiden McClelland
parent bc23129759
commit a2f65de1ce
46 changed files with 2309 additions and 869 deletions

1283
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -68,7 +68,9 @@ digest-old = { package = "digest", version = "0.9.0" }
divrem = "1.0.0"
ed25519 = { version = "1.5.2", features = ["pkcs8", "pem", "alloc"] }
ed25519-dalek = { version = "1.0.1", features = ["serde"] }
emver = { version = "0.1.6", features = ["serde"] }
emver = { version = "0.1.7", git = "https://github.com/Start9Labs/emver-rs.git", features = [
"serde",
] }
fd-lock-rs = "0.1.4"
futures = "0.3.21"
git-version = "0.3.5"
@@ -89,6 +91,7 @@ jsonpath_lib = "0.3.0"
lazy_static = "1.4.0"
libc = "0.2.126"
log = "0.4.17"
mbrman = "0.5.0"
models = { version = "*", path = "../libs/models" }
nix = "0.25.0"
nom = "7.1.1"

View File

@@ -64,7 +64,7 @@ sudo chown -R $USER target
sudo chown -R $USER ~/.cargo
sudo chown -R $USER ../libs/target
if [-n fail]; then
if [ -n "$fail" ]; then
exit 1
fi

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use std::time::Duration;
use embassy::context::rpc::RpcContextConfig;
use embassy::context::{DiagnosticContext, SetupContext};
use embassy::context::{DiagnosticContext, InstallContext, SetupContext};
use embassy::disk::fsck::RepairStrategy;
use embassy::disk::main::DEFAULT_PASSWORD;
use embassy::disk::REPAIR_DISK_PATH;
@@ -28,7 +28,45 @@ fn status_fn(_: i32) -> StatusCode {
#[instrument]
async fn setup_or_init(cfg_path: Option<PathBuf>) -> Result<(), Error> {
if tokio::fs::metadata("/media/embassy/config/disk.guid")
if tokio::fs::metadata("/cdrom").await.is_ok() {
#[cfg(feature = "avahi")]
let _mdns = MdnsController::init();
tokio::fs::write(
"/etc/nginx/sites-available/default",
include_str!("../nginx/install-wizard.conf"),
)
.await
.with_ctx(|_| {
(
embassy::ErrorKind::Filesystem,
"/etc/nginx/sites-available/default",
)
})?;
Command::new("systemctl")
.arg("reload")
.arg("nginx")
.invoke(embassy::ErrorKind::Nginx)
.await?;
let ctx = InstallContext::init(cfg_path).await?;
tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this
CHIME.play().await?;
rpc_server!({
command: embassy::install_api,
context: ctx.clone(),
status: status_fn,
middleware: [
cors,
]
})
.with_graceful_shutdown({
let mut shutdown = ctx.shutdown.subscribe();
async move {
shutdown.recv().await.expect("context dropped");
}
})
.await
.with_kind(embassy::ErrorKind::Network)?;
} else if tokio::fs::metadata("/media/embassy/config/disk.guid")
.await
.is_err()
{

View File

@@ -0,0 +1,74 @@
use std::net::{IpAddr, SocketAddr};
use std::ops::Deref;
use std::path::Path;
use std::sync::Arc;
use rpc_toolkit::Context;
use serde::Deserialize;
use tokio::sync::broadcast::Sender;
use tracing::instrument;
use url::Host;
use crate::util::config::load_config_from_paths;
use crate::Error;
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct InstallContextConfig {
pub bind_rpc: Option<SocketAddr>,
}
impl InstallContextConfig {
#[instrument(skip(path))]
pub async fn load<P: AsRef<Path> + Send + 'static>(path: Option<P>) -> Result<Self, Error> {
tokio::task::spawn_blocking(move || {
load_config_from_paths(
path.as_ref()
.into_iter()
.map(|p| p.as_ref())
.chain(std::iter::once(Path::new(
"/media/embassy/config/config.yaml",
)))
.chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))),
)
})
.await
.unwrap()
}
}
pub struct InstallContextSeed {
pub bind_rpc: SocketAddr,
pub shutdown: Sender<()>,
}
#[derive(Clone)]
pub struct InstallContext(Arc<InstallContextSeed>);
impl InstallContext {
#[instrument(skip(path))]
pub async fn init<P: AsRef<Path> + Send + 'static>(path: Option<P>) -> Result<Self, Error> {
let cfg = InstallContextConfig::load(path.as_ref().map(|p| p.as_ref().to_owned())).await?;
let (shutdown, _) = tokio::sync::broadcast::channel(1);
Ok(Self(Arc::new(InstallContextSeed {
bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
shutdown,
})))
}
}
impl Context for InstallContext {
fn host(&self) -> Host<&str> {
match self.0.bind_rpc.ip() {
IpAddr::V4(a) => Host::Ipv4(a),
IpAddr::V6(a) => Host::Ipv6(a),
}
}
fn port(&self) -> u16 {
self.0.bind_rpc.port()
}
}
impl Deref for InstallContext {
type Target = InstallContextSeed;
fn deref(&self) -> &Self::Target {
&*self.0
}
}

View File

@@ -1,11 +1,13 @@
pub mod cli;
pub mod diagnostic;
pub mod install;
pub mod rpc;
pub mod sdk;
pub mod setup;
pub use cli::CliContext;
pub use diagnostic::DiagnosticContext;
pub use install::InstallContext;
pub use rpc::RpcContext;
pub use sdk::SdkContext;
pub use setup::SetupContext;
@@ -35,3 +37,8 @@ impl From<SetupContext> for () {
()
}
}
impl From<InstallContext> for () {
fn from(_: InstallContext) -> Self {
()
}
}

View File

@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use clap::ArgMatches;
use rpc_toolkit::command;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use crate::context::RpcContext;
use crate::disk::util::DiskInfo;
@@ -18,7 +18,7 @@ pub mod util;
pub const BOOT_RW_PATH: &str = "/media/boot-rw";
pub const REPAIR_DISK_PATH: &str = "/media/embassy/config/repair-disk";
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct OsPartitionInfo {
pub boot: PathBuf,

View File

@@ -12,7 +12,6 @@ use nom::sequence::{pair, preceded, terminated};
use nom::IResult;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::fs::File;
use tokio::process::Command;
use tracing::instrument;
@@ -20,7 +19,6 @@ use super::mount::filesystem::block_dev::BlockDev;
use super::mount::filesystem::ReadOnly;
use super::mount::guard::TmpMountGuard;
use crate::disk::OsPartitionInfo;
use crate::util::io::from_yaml_async_reader;
use crate::util::serde::IoFormat;
use crate::util::{Invoke, Version};
use crate::{Error, ResultExt as _};
@@ -44,6 +42,7 @@ pub struct PartitionInfo {
pub capacity: u64,
pub used: Option<u64>,
pub embassy_os: Option<EmbassyOsRecoveryInfo>,
pub guid: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
@@ -219,15 +218,6 @@ pub async fn recovery_info(
)?,
));
}
let version_path = mountpoint.as_ref().join("root/appmgr/version");
if tokio::fs::metadata(&version_path).await.is_ok() {
return Ok(Some(EmbassyOsRecoveryInfo {
version: from_yaml_async_reader(File::open(&version_path).await?).await?,
full: true,
password_hash: None,
wrapped_key: None,
}));
}
Ok(None)
}
@@ -323,7 +313,11 @@ pub async fn list(os: &OsPartitionInfo) -> Result<Vec<DiskInfo>, Error> {
disk_info.guid = g.clone();
} else {
for part in index.parts {
disk_info.partitions.push(part_info(part).await);
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);
@@ -398,6 +392,7 @@ async fn part_info(part: PathBuf) -> PartitionInfo {
capacity,
used,
embassy_os,
guid: None,
}
}

View File

@@ -242,7 +242,12 @@ impl From<std::net::AddrParseError> for Error {
}
impl From<openssl::error::ErrorStack> for Error {
fn from(e: openssl::error::ErrorStack) -> Self {
Error::new(eyre!("OpenSSL ERROR:\n{}", e), ErrorKind::OpenSsl)
Error::new(eyre!("{}", e), ErrorKind::OpenSsl)
}
}
impl From<mbrman::Error> for Error {
fn from(e: mbrman::Error) -> Self {
Error::new(e, ErrorKind::DiskManagement)
}
}
impl From<Error> for RpcError {

View File

@@ -0,0 +1,2 @@
{boot} /boot vfat defaults 0 2
{root} / ext4 defaults 0 1

View File

@@ -35,6 +35,7 @@ pub mod middleware;
pub mod migration;
pub mod net;
pub mod notifications;
pub mod os_install;
pub mod procedure;
pub mod properties;
pub mod s9pk;
@@ -133,3 +134,8 @@ pub fn diagnostic_api() -> Result<(), RpcError> {
pub fn setup_api() -> Result<(), RpcError> {
Ok(())
}
#[command(subcommands(version::git_info, echo, os_install::install))]
pub fn install_api() -> Result<(), RpcError> {
Ok(())
}

View File

@@ -0,0 +1,29 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html/install;
index index.html index.htm index.nginx-debian.html;
server_name _;
proxy_buffering off;
proxy_request_buffering off;
proxy_socket_keepalive on;
proxy_http_version 1.1;
proxy_read_timeout 1800;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
location /rpc/ {
proxy_pass http://127.0.0.1:5959/;
}
location / {
try_files $uri $uri/ =404;
}
}

317
backend/src/os_install.rs Normal file
View File

@@ -0,0 +1,317 @@
use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre;
use mbrman::{MBRPartitionEntry, CHS, MBR};
use rpc_toolkit::command;
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::block_dev::BlockDev;
use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
use crate::disk::OsPartitionInfo;
use crate::util::serde::IoFormat;
use crate::util::{display_none, Invoke};
use crate::{Error, ResultExt};
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct PostInstallConfig {
os_partitions: OsPartitionInfo,
ethernet_interface: String,
wifi_interface: Option<String>,
}
#[command(subcommands(status, execute, reboot))]
pub fn install() -> Result<(), Error> {
Ok(())
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct InstallTarget {
logicalname: PathBuf,
embassy_data: bool,
}
#[command(display(display_none))]
pub async fn status() -> Result<Vec<InstallTarget>, Error> {
let disks = crate::disk::util::list(&Default::default()).await?;
Ok(disks
.into_iter()
.map(|d| InstallTarget {
logicalname: d.logicalname,
embassy_data: d.guid.is_some() || d.partitions.into_iter().any(|p| p.guid.is_some()),
})
.collect())
}
pub async fn find_wifi_iface() -> Result<Option<String>, Error> {
let mut ifaces = tokio::fs::read_dir("/sys/class/net").await?;
while let Some(iface) = ifaces.next_entry().await? {
if tokio::fs::metadata(iface.path().join("wireless"))
.await
.is_ok()
{
if let Some(iface) = iface.file_name().into_string().ok() {
return Ok(Some(iface));
}
}
}
Ok(None)
}
pub async fn find_eth_iface() -> Result<String, Error> {
let mut ifaces = tokio::fs::read_dir("/sys/class/net").await?;
while let Some(iface) = ifaces.next_entry().await? {
if tokio::fs::metadata(iface.path().join("wireless"))
.await
.is_err()
&& tokio::fs::metadata(iface.path().join("device"))
.await
.is_ok()
{
if let Some(iface) = iface.file_name().into_string().ok() {
return Ok(iface);
}
}
}
Err(Error::new(
eyre!("Could not detect ethernet interface"),
crate::ErrorKind::Network,
))
}
// pub struct FDisk {
// child: Child,
// stdin: ChildStdin,
// }
// impl FDisk {
// pub async fn command(&mut self, cmd: &[u8]) -> Result<(), Error> {
// }
// }
pub fn partition_for(disk: impl AsRef<Path>, idx: usize) -> PathBuf {
let disk_path = disk.as_ref();
let (root, leaf) = if let (Some(root), Some(leaf)) = (
disk_path.parent(),
disk_path.file_name().and_then(|s| s.to_str()),
) {
(root, leaf)
} else {
return Default::default();
};
if leaf.ends_with(|c: char| c.is_ascii_digit()) {
root.join(format!("{}p{}", leaf, idx))
} else {
root.join(format!("{}{}", leaf, idx))
}
}
#[command(display(display_none))]
pub async fn execute(
#[arg] logicalname: PathBuf,
#[arg(short = 'o')] mut overwrite: bool,
) -> Result<(), Error> {
let disk = crate::disk::util::list(&Default::default())
.await?
.into_iter()
.find(|d| &d.logicalname == &logicalname)
.ok_or_else(|| {
Error::new(
eyre!("Unknown disk {}", logicalname.display()),
crate::ErrorKind::DiskManagement,
)
})?;
let eth_iface = find_eth_iface().await?;
let wifi_iface = find_wifi_iface().await?;
overwrite |= disk.guid.is_none() && disk.partitions.iter().all(|p| p.guid.is_none());
let sectors = (disk.capacity / 512) as u32;
tokio::task::spawn_blocking(move || {
let mut file = std::fs::File::options()
.read(true)
.write(true)
.open(&logicalname)?;
let (mut mbr, guid_part) = if overwrite {
(MBR::new_from(&mut file, 512, rand::random())?, None)
} else {
let mut mbr = MBR::read_from(&mut file, 512)?;
let mut guid_part = None;
for (idx, part_info) in disk
.partitions
.iter()
.enumerate()
.map(|(idx, x)| (idx + 1, x))
{
if let Some(entry) = mbr.get_mut(idx) {
if entry.starting_lba >= 33556480 {
if idx < 3 {
guid_part = Some(std::mem::replace(entry, MBRPartitionEntry::empty()))
}
break;
}
if part_info.guid.is_some() {
return Err(Error::new(
eyre!("Not enough space before embassy data"),
crate::ErrorKind::InvalidRequest,
));
}
*entry = MBRPartitionEntry::empty();
}
}
(mbr, guid_part)
};
mbr[1] = MBRPartitionEntry {
boot: 0x80,
first_chs: CHS::empty(),
sys: 0x0b,
last_chs: CHS::empty(),
starting_lba: 2048,
sectors: 2099200 - 2048,
};
mbr[2] = MBRPartitionEntry {
boot: 0,
first_chs: CHS::empty(),
sys: 0x83,
last_chs: CHS::empty(),
starting_lba: 2099200,
sectors: 33556480 - 2099200,
};
if overwrite {
mbr[3] = MBRPartitionEntry {
boot: 0,
first_chs: CHS::empty(),
sys: 0x8e,
last_chs: CHS::empty(),
starting_lba: 33556480,
sectors: sectors - 33556480,
}
} else if let Some(guid_part) = guid_part {
mbr[3] = guid_part;
}
mbr.write_into(&mut file)?;
Ok(())
})
.await
.unwrap()?;
let boot_part = partition_for(&disk.logicalname, 1);
let root_part = partition_for(&disk.logicalname, 2);
Command::new("mkfs.vfat")
.arg(&boot_part)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Command::new("fatlabel")
.arg(&boot_part)
.arg("boot")
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Command::new("mkfs.ext4")
.arg(&root_part)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Command::new("e2label")
.arg(&root_part)
.arg("rootfs")
.invoke(crate::ErrorKind::DiskManagement)
.await?;
let rootfs = TmpMountGuard::mount(&BlockDev::new(&root_part), ReadWrite).await?;
tokio::fs::create_dir(rootfs.as_ref().join("config")).await?;
tokio::fs::create_dir(rootfs.as_ref().join("next")).await?;
let current = rootfs.as_ref().join("current");
tokio::fs::create_dir(&current).await?;
tokio::fs::create_dir(current.join("boot")).await?;
let boot =
MountGuard::mount(&BlockDev::new(&boot_part), current.join("boot"), ReadWrite).await?;
Command::new("unsquashfs")
.arg("-n")
.arg("-f")
.arg("-d")
.arg(&current)
.arg("/cdrom/casper/filesystem.squashfs")
.invoke(crate::ErrorKind::Filesystem)
.await?;
tokio::fs::write(
rootfs.as_ref().join("config/config.yaml"),
IoFormat::Yaml.to_vec(&PostInstallConfig {
os_partitions: OsPartitionInfo {
boot: boot_part.clone(),
root: root_part.clone(),
},
ethernet_interface: eth_iface,
wifi_interface: wifi_iface,
})?,
)
.await?;
tokio::fs::write(
current.join("etc/fstab"),
format!(
include_str!("fstab.template"),
boot = boot_part.display(),
root = root_part.display()
),
)
.await?;
Command::new("chroot")
.arg(&current)
.arg("systemd-machine-id-setup")
.invoke(crate::ErrorKind::Unknown) // TODO systemd
.await?;
Command::new("chroot")
.arg(&current)
.arg("ssh-keygen")
.arg("-A")
.invoke(crate::ErrorKind::Unknown) // TODO ssh
.await?;
let dev = MountGuard::mount(&Bind::new("/dev"), current.join("dev"), ReadWrite).await?;
let sys = MountGuard::mount(&Bind::new("/sys"), current.join("sys"), ReadWrite).await?;
let proc = MountGuard::mount(&Bind::new("/proc"), current.join("proc"), ReadWrite).await?;
Command::new("chroot")
.arg(&current)
.arg("update-grub")
.invoke(crate::ErrorKind::Unknown) // TODO grub
.await?;
Command::new("chroot")
.arg(&current)
.arg("grub-install")
.arg(&disk.logicalname)
.invoke(crate::ErrorKind::Unknown) // TODO grub
.await?;
dev.unmount().await?;
sys.unmount().await?;
proc.unmount().await?;
boot.unmount().await?;
rootfs.unmount().await?;
Ok(())
}
#[command(display(display_none))]
pub async fn reboot() -> Result<(), Error> {
Command::new("sync")
.invoke(crate::ErrorKind::Filesystem)
.await?;
Command::new("reboot")
.invoke(crate::ErrorKind::Filesystem)
.await?;
Ok(())
}