Expand codebase

Signed-off-by: Ana Hobden <operator@hoverbear.org>
This commit is contained in:
Ana Hobden 2022-09-06 12:48:37 -07:00
parent bca0549c30
commit 609ed3563f
10 changed files with 318 additions and 42 deletions

10
Cargo.lock generated
View file

@ -968,15 +968,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.22.0+1.1.1q"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.75"
@ -986,7 +977,6 @@ dependencies = [
"autocfg",
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]

View file

@ -16,10 +16,10 @@ crossterm = { version = "0.25.0", features = ["event-stream"] }
eyre = "0.6.8"
futures = "0.3.24"
owo-colors = { version = "3.5.0", features = [ "supports-colors" ] }
reqwest = { version = "0.11.11", features = ["native-tls-vendored", "stream"] }
reqwest = { version = "0.11.11", features = ["native-tls", "stream"] }
target-lexicon = "0.12.4"
thiserror = "1.0.33"
tokio = { version = "1.21.0", features = ["time", "process", "fs", "tracing", "rt-multi-thread", "macros", "io-util"] }
tokio = { version = "1.21.0", features = ["time", "io-std", "process", "fs", "tracing", "rt-multi-thread", "macros", "io-util"] }
tracing = { version = "0.1.36", features = [ "valuable" ] }
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.15", features = [ "env-filter", "valuable" ] }

View file

@ -1,5 +1,7 @@
pub(crate) mod arg;
pub(crate) mod subcommand;
use crate::interaction;
use clap::Parser;
use harmonic::Harmonic;
use reqwest::Url;
@ -12,7 +14,6 @@ pub(crate) trait CommandExecute {
#[derive(Debug, Parser)]
#[clap(version)]
pub(crate) struct HarmonicCli {
#[clap(flatten)]
pub(crate) instrumentation: arg::Instrumentation,
@ -22,6 +23,8 @@ pub(crate) struct HarmonicCli {
pub(crate) no_modify_profile: bool,
#[clap(long, default_value = "32")]
pub(crate) daemon_user_count: usize,
#[clap(subcommand)]
subcommand: Option<subcommand::Subcommand>,
}
#[async_trait::async_trait]
@ -37,14 +40,30 @@ impl CommandExecute for HarmonicCli {
daemon_user_count,
channels,
no_modify_profile,
subcommand,
} = self;
match subcommand {
Some(subcommand::Subcommand::NixOs(nixos)) => return nixos.execute().await,
None => (),
}
let mut harmonic = Harmonic::default();
harmonic.daemon_user_count(daemon_user_count);
harmonic.channels(channels);
harmonic.modify_profile(!no_modify_profile);
harmonic.install().await?;
if !interaction::confirm("Are you ready to continue?").await? {
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
}
harmonic.create_group().await?;
harmonic.create_users().await?;
harmonic.create_directories().await?;
harmonic.place_channel_configuration().await?;
harmonic.fetch_nix().await?;
harmonic.configure_shell_profile().await?;
Ok(ExitCode::SUCCESS)
}

View file

@ -0,0 +1,8 @@
mod nixos;
#[derive(Debug, clap::Subcommand)]
pub(crate) enum Subcommand {
#[cfg(target_os = "linux")]
#[clap(name = "nixos")]
NixOs(nixos::NixOsCommand),
}

View file

@ -0,0 +1,43 @@
use std::{path::PathBuf, process::ExitCode};
use crate::{cli::CommandExecute, interaction};
use harmonic::NixOs;
use owo_colors::OwoColorize;
/// Install an opinionated NixOS on a device
#[derive(Debug, clap::Parser)]
pub(crate) struct NixOsCommand {
/// The disk to install on (eg. `/dev/nvme1n1`, `/dev/sda`)
#[clap(long)]
target_device: PathBuf,
}
#[async_trait::async_trait]
impl CommandExecute for NixOsCommand {
#[tracing::instrument(skip_all, fields(
target_device = %self.target_device.display(),
))]
async fn execute(self) -> eyre::Result<ExitCode> {
let Self { target_device } = self;
if interaction::confirm(format!(
"\
This will:
1. Irrecoverably wipe `{target_device}`
2. Write a GPT partition table to `{target_device}`
3. Write a partition 1 to `{target_device}` as a 1G FAT32 EFI ESP
4. Write partition 2 to `{target_device}` as a BTRFS disk consuming
the remaining disk\n\
5. Create several BTRFS subvolumes supporting an ephemeral
(aka \'Erase your darlings\') installation\n\
",
target_device = target_device.display().cyan()
))
.await?
{
NixOs::new(target_device).install().await?;
}
Ok(ExitCode::SUCCESS)
}
}

View file

@ -1,7 +1,19 @@
#[derive(thiserror::Error, Debug)]
pub enum HarmonicError {
#[error("Downloading Nix: {0}")]
#[error("Downloading Nix")]
DownloadingNix(#[from] reqwest::Error),
#[error("Unpacking Nix: {0}")]
UnpackingNix(#[from] std::io::Error),
#[error("Unpacking Nix")]
UnpackingNix(std::io::Error),
#[error("Running `groupadd`")]
GroupAddSpawn(std::io::Error),
#[error("`groupadd` returned failure")]
GroupAddFailure(std::process::ExitStatus),
#[error("Running `useradd`")]
UserAddSpawn(std::io::Error),
#[error("`useradd` returned failure")]
UserAddFailure(std::process::ExitStatus),
#[error("Creating directory")]
CreateDirectory(std::io::Error),
#[error("Placing channel configuration")]
PlaceChannelConfiguration(std::io::Error),
}

73
src/interaction.rs Normal file
View file

@ -0,0 +1,73 @@
use crossterm::event::{EventStream, KeyCode};
use eyre::{eyre, WrapErr};
use futures::{FutureExt, StreamExt};
use owo_colors::OwoColorize;
use tokio::{io::AsyncWriteExt, process::Command};
pub(crate) async fn confirm_command(
question: impl AsRef<str>,
command: Command,
) -> eyre::Result<bool> {
confirm(format!(
"\
{question}\n\
\n\
{ticks}\n\
{command_styled}\n\
{ticks}\n\
",
question = question.as_ref(),
ticks = "```".dimmed(),
command_styled = format!("{:?}", command.as_std()).green(),
))
.await
}
pub(crate) async fn confirm(question: impl AsRef<str>) -> eyre::Result<bool> {
let mut stdout = tokio::io::stdout();
let with_confirm = format!(
"\
{question}\n\
\n\
{are_you_sure} ({yes}/{no})\
",
question = question.as_ref(),
are_you_sure = "Are you sure?".bright_white().bold(),
no = "N".red().bold(),
yes = "y".green(),
);
stdout.write_all(with_confirm.as_bytes()).await?;
stdout.flush().await?;
let mut reader = EventStream::new();
loop {
let event = reader.next().fuse().await;
match event {
Some(Ok(event)) => match event {
crossterm::event::Event::Key(key) => match key.code {
KeyCode::Char('y') => break Ok(true),
_ => {
stdout
.write_all("Cancelled!".red().to_string().as_bytes())
.await?;
stdout.flush().await?;
break Ok(false);
}
},
_ => (),
},
Some(Err(err)) => return Err(err).wrap_err("Getting response"),
None => return Err(eyre!("Bailed, no confirmation event")),
}
}
}
pub(crate) async fn clean_exit_with_message(message: impl AsRef<str>) -> ! {
eprintln!("{}", message.as_ref());
std::process::exit(0)
}
pub(crate) async fn angry_bail_with_message(message: impl AsRef<str>, code: i32) -> ! {
eprintln!("{}", message.as_ref());
std::process::exit(code)
}

View file

@ -1,8 +1,20 @@
mod error;
use std::{fs::Permissions, os::unix::fs::PermissionsExt, path::Path};
pub use error::HarmonicError;
#[cfg(target_os = "linux")]
mod nixos;
#[cfg(target_os = "linux")]
pub use nixos::NixOs;
use error::HarmonicError;
use futures::stream::TryStreamExt;
use reqwest::Url;
use tokio::{
fs::{create_dir, set_permissions, OpenOptions},
io::AsyncWriteExt,
process::Command,
};
// This uses a Rust builder pattern
#[derive(Debug)]
@ -10,6 +22,10 @@ pub struct Harmonic {
daemon_user_count: usize,
channels: Vec<Url>,
modify_profile: bool,
nix_build_group_name: String,
nix_build_group_id: usize,
nix_build_user_prefix: String,
nix_build_user_id_base: usize,
}
impl Harmonic {
@ -29,19 +45,8 @@ impl Harmonic {
}
}
#[cfg(target_os = "linux")]
impl Harmonic {
#[tracing::instrument(skip_all, fields(
channels = %self.channels.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
daemon_user_count = %self.daemon_user_count,
modify_profile = %self.modify_profile
))]
pub async fn install(&self) -> Result<(), HarmonicError> {
self.download_nix().await?;
Ok(())
}
pub async fn download_nix(&self) -> Result<(), HarmonicError> {
pub async fn fetch_nix(&self) -> Result<(), HarmonicError> {
// TODO(@hoverbear): architecture specific download
// TODO(@hoverbear): hash check
let res = reqwest::get(
@ -56,27 +61,116 @@ impl Harmonic {
let buffered = futures::io::BufReader::new(async_read);
let decoder = async_compression::futures::bufread::XzDecoder::new(buffered);
let archive = async_tar::Archive::new(decoder);
// TODO(@Hoverbear): Pick directory
let destination = "/nix/store";
archive
.unpack("boop")
.unpack(destination)
.await
.map_err(HarmonicError::UnpackingNix)?;
tracing::info!("Jobs done!!!");
tracing::debug!(%destination, "Downloaded & extracted Nix");
Ok(())
}
pub async fn create_group(&self) -> Result<(), HarmonicError> {
let status = Command::new("groupadd")
.arg("-g")
.arg(self.nix_build_group_id.to_string())
.arg("--system")
.arg(&self.nix_build_group_name)
.status()
.await
.map_err(HarmonicError::GroupAddSpawn)?;
if !status.success() {
Err(HarmonicError::GroupAddFailure(status))
} else {
Ok(())
}
}
#[cfg(target_os = "macos")]
impl Harmonic {
#[tracing::instrument]
pub async fn install(&self) -> Result<(), HarmonicError> {
// TODO(@hoverbear): Check MacOS version
pub async fn create_users(&self) -> Result<(), HarmonicError> {
for index in 1..=self.daemon_user_count {
let user_name = format!("{}{index}", self.nix_build_user_prefix);
let user_id = self.nix_build_user_id_base + index;
let status = Command::new("useradd")
.args([
"--home-dir",
"/var/empty",
"--comment",
&format!("\"Nix build user {user_id}\""),
"--gid",
&self.nix_build_group_id.to_string(),
"--groups",
&self.nix_build_group_name.to_string(),
"--no-user-group",
"--system",
"--shell",
"/sbin/nologin",
"--uid",
&user_id.to_string(),
"--password",
"\"!\"",
&user_name.to_string(),
])
.status()
.await
.map_err(HarmonicError::UserAddSpawn)?;
if !status.success() {
return Err(HarmonicError::UserAddFailure(status));
}
}
Ok(())
}
pub async fn create_directories(&self) -> Result<(), HarmonicError> {
let permissions = Permissions::from_mode(755);
let paths = [
"/nix",
"/nix/var",
"/nix/var/log",
"/nix/var/log/nix",
"/nix/var/log/nix/drvs",
"/nix/var/nix/db",
"/nix/var/nix/gcroots",
"/nix/var/nix/gcroots/per-user",
"/nix/var/nix/profiles",
"/nix/var/nix/profiles/per-user",
"/nix/var/nix/temproots",
"/nix/var/nix/userpool",
"/nix/var/nix/daemon-socket",
];
for path in paths {
// We use `create_dir` over `create_dir_all` to ensure we always set permissions right
create_dir_with_permissions(path, permissions.clone())
.await
.map_err(HarmonicError::CreateDirectory)?;
}
Ok(())
}
pub async fn place_channel_configuration(&self) -> Result<(), HarmonicError> {
let mut file = OpenOptions::new()
.create(true)
.write(true)
.read(true)
.open("/root/.nix-channels") // TODO(@hoverbear): We should figure out the actual root dir
.await
.map_err(HarmonicError::PlaceChannelConfiguration)?;
let buf = self
.channels
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
file.write_all(buf.as_bytes())
.await
.map_err(HarmonicError::PlaceChannelConfiguration)?;
Ok(())
}
pub async fn configure_shell_profile(&self) -> Result<(), HarmonicError> {
todo!();
Ok(())
}
pub async fn download_nix(&self) -> Result<(), HarmonicError> {
Ok(())
}
}
impl Default for Harmonic {
@ -87,6 +181,20 @@ impl Default for Harmonic {
.unwrap()],
daemon_user_count: 32,
modify_profile: true,
nix_build_group_name: String::from("nixbld"),
nix_build_group_id: 30000,
nix_build_user_prefix: String::from("nixbld"),
nix_build_user_id_base: 30000,
}
}
}
async fn create_dir_with_permissions(
path: impl AsRef<Path>,
permissions: Permissions,
) -> Result<(), std::io::Error> {
let path = path.as_ref();
create_dir(path).await?;
set_permissions(path, permissions).await?;
Ok(())
}

View file

@ -2,6 +2,8 @@ pub(crate) mod cli;
use std::process::ExitCode;
pub mod interaction;
use clap::Parser;
use cli::CommandExecute;

21
src/nixos.rs Normal file
View file

@ -0,0 +1,21 @@
use std::path::PathBuf;
use crate::HarmonicError;
pub struct NixOs {
/// The disk to install on (eg. `/dev/nvme1n1`, `/dev/sda`)
target_device: PathBuf,
}
impl NixOs {
pub fn new(target_device: PathBuf) -> Self {
Self { target_device }
}
#[tracing::instrument(skip_all, fields(
target_device = %self.target_device.display(),
))]
pub async fn install(&self) -> Result<(), HarmonicError> {
tracing::warn!("Kicking your socks in");
Ok(())
}
}