Continue implementing basic features.

This commit is contained in:
R-VdP 2023-02-02 17:31:10 +00:00
parent fbe9f2eabb
commit f784f06107
No known key found for this signature in database
6 changed files with 244 additions and 27 deletions

View file

@ -1,10 +1,15 @@
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error;
use std::os::unix;
use std::path::Path;
use std::{env, fs, process, str};
use std::{env, fs, io, process, str};
#[derive(Debug)]
const FLAKE_ATTR: &str = "serviceConfig";
const PROFILE_NAME: &str = "service-manager";
#[derive(Debug, Clone)]
struct StorePath {
path: String,
}
@ -17,33 +22,114 @@ impl From<String> for StorePath {
}
}
#[derive(Parser, Debug)]
impl std::fmt::Display for StorePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path)
}
}
#[derive(clap::Parser, Debug)]
#[command(author, version, about, long_about=None)]
struct Args {
#[arg(short, long)]
flake_uri: String,
#[command(subcommand)]
action: Action,
}
#[derive(clap::Subcommand, Debug)]
enum Action {
Activate {
#[arg(long)]
store_path: StorePath,
},
Generate {
#[arg(long)]
flake_uri: String,
},
}
fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse();
let profile_name = "service-manager";
match args.action {
Action::Activate { store_path } => activate(store_path),
Action::Generate { flake_uri } => generate(&flake_uri),
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceConfig {
name: String,
service: String,
}
impl ServiceConfig {
fn store_path(&self) -> StorePath {
StorePath::from(self.service.to_owned())
}
}
// TODO: error message if not euid 0
fn activate(store_path: StorePath) -> Result<(), Box<dyn Error>> {
if nix::unistd::Uid::is_root(nix::unistd::getuid()) {
return Err("We need root permissions.".into());
}
println!("Activating service-manager profile: {}", store_path);
let file = fs::File::open(store_path.path + "/services/services.json")?;
let reader = io::BufReader::new(file);
let services: Vec<ServiceConfig> = serde_json::from_reader(reader)?;
println!("{:?}", services);
services.iter().try_for_each(|service| {
create_store_link(
&service.store_path(),
Path::new(&format!("/run/systemd/system/{}.service", service.name)),
)
})?;
if process::Command::new("systemctl")
.arg("daemon-reload")
.output()
.expect("Unable to run systemctl.")
.status
.success()
{
services.iter().for_each(|service| {
print_out_and_err(
process::Command::new("systemctl")
.arg("start")
.arg(&service.name)
.output()
.expect("Unable to run systemctl"),
);
});
}
Ok(())
}
fn generate(flake_uri: &str) -> Result<(), Box<dyn Error>> {
// TODO: we temporarily put this under per-user to avoid needing root access
// we will move this to /nix/var/nix/profiles/system later on.
let user = env::var("USER").expect("USER env var undefined");
let profile_path = format!("/nix/var/nix/profiles/per-user/{}/{}", user, profile_name);
let profile_path = format!("/nix/var/nix/profiles/per-user/{}/{}", user, PROFILE_NAME);
let gcroot_path = format!(
"/nix/var/nix/gcroots/per-user/{}/{}-current",
user, profile_name
user, PROFILE_NAME
);
let flake_attr = "serviceConfig.x86_64-linux";
// FIXME: we should not hard-code the system here
let flake_attr = format!("{}.x86_64-linux", FLAKE_ATTR);
let nix_build_output = run_nix_build(&args.flake_uri, flake_attr);
println!("Running nix build...");
let nix_build_output = run_nix_build(flake_uri, &flake_attr);
let store_path = get_store_path(nix_build_output)?;
println!("Found store path: {:?}", store_path);
println!("Generating new generation from {}", store_path);
print_out_and_err(install_nix_profile(&store_path, &profile_path));
println!("Registering GC root...");
create_gcroot(&gcroot_path, &store_path).expect("Failed to create GC root.");
Ok(())
}
@ -60,28 +146,57 @@ fn install_nix_profile(store_path: &StorePath, profile_path: &str) -> process::O
}
fn create_gcroot(gcroot_path: &str, store_path: &StorePath) -> Result<(), Box<dyn Error>> {
let path = Path::new(gcroot_path);
if path.is_symlink() {
fs::remove_file(path).expect("Error removing old GC root.");
create_store_link(store_path, Path::new(gcroot_path))
}
fn create_store_link(store_path: &StorePath, from: &Path) -> Result<(), Box<dyn Error>> {
println!("Creating symlink: {} -> {}", from.display(), store_path);
if from.is_symlink() {
fs::remove_file(from)
.unwrap_or_else(|_| panic!("Error removing old symlink: {}.", from.display()));
}
unix::fs::symlink(&store_path.path, gcroot_path).map_err(Box::from)
unix::fs::symlink(&store_path.path, from).map_err(Box::from)
}
fn get_store_path(nix_build_result: process::Output) -> Result<StorePath, Box<dyn Error>> {
if nix_build_result.status.success() {
String::from_utf8(nix_build_result.stdout)
.map_err(Box::from)
.map(StorePath::from)
.and_then(parse_nix_build_output)
} else {
String::from_utf8(nix_build_result.stderr).map_or_else(boxed_error(), boxed_error())
}
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NixBuildOutput {
drv_path: String,
outputs: HashMap<String, String>,
}
fn parse_nix_build_output(output: String) -> Result<StorePath, Box<dyn Error>> {
let expected_output_name = "out";
let results: Vec<NixBuildOutput> = serde_json::from_str(&output)?;
if let [result] = results.as_slice() {
if let Some(store_path) = result.outputs.get(expected_output_name) {
return Ok(StorePath::from(store_path.to_owned()));
}
return Err(format!(
"No output '{}' found in nix build result.",
expected_output_name
)
.into());
}
Err("Multiple build results were returned, we cannot handle that yet.No output '{}' found in nix build result.".into())
}
fn run_nix_build(flake_uri: &str, flake_attr: &str) -> process::Output {
process::Command::new("nix")
.arg("build")
.arg(format!("{}#{}", flake_uri, flake_attr))
.arg("--print-out-paths")
.arg("--json")
.output()
.expect("Failed to execute nix, is it on your path?")
}
@ -94,8 +209,8 @@ fn print_out_and_err(output: process::Output) -> process::Output {
fn print_u8(bytes: &[u8]) {
str::from_utf8(bytes).map_or((), |s| {
if !s.is_empty() {
println!("{}", s)
if !s.trim().is_empty() {
println!("{}", s.trim())
}
})
}