Expand codebase
Signed-off-by: Ana Hobden <operator@hoverbear.org>
This commit is contained in:
parent
bca0549c30
commit
609ed3563f
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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" ] }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
8
src/cli/subcommand/mod.rs
Normal file
8
src/cli/subcommand/mod.rs
Normal 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),
|
||||
}
|
43
src/cli/subcommand/nixos.rs
Normal file
43
src/cli/subcommand/nixos.rs
Normal 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)
|
||||
}
|
||||
}
|
18
src/error.rs
18
src/error.rs
|
@ -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
73
src/interaction.rs
Normal 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)
|
||||
}
|
156
src/lib.rs
156
src/lib.rs
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
21
src/nixos.rs
Normal 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(())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue