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

1
.gitignore vendored
View file

@ -7,3 +7,4 @@
result result
.pre-commit-config.yaml .pre-commit-config.yaml
.direnv

93
Cargo.lock generated
View file

@ -2,6 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -14,6 +20,12 @@ version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.1.4" version = "4.1.4"
@ -109,6 +121,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "itoa"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.139" version = "0.2.139"
@ -121,6 +139,29 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "nix"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"memoffset",
"pin-utils",
"static_assertions",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.17.0" version = "1.17.0"
@ -133,6 +174,12 @@ version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
version = "1.0.4" version = "1.0.4"
@ -189,13 +236,59 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "ryu"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
[[package]]
name = "serde"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "service-manager" name = "service-manager"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"nix",
"serde",
"serde_json",
] ]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"

View file

@ -7,3 +7,6 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.1.4", features = ["derive"] } clap = { version = "4.1.4", features = ["derive"] }
nix = "0.26.2"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"

View file

@ -58,9 +58,10 @@
modules = [ modules = [
./nix/modules ./nix/modules
]; ];
inherit (self.packages.${system}) service-manager;
}; };
packages = rec { packages = {
service-manager = service-manager =
pkgs.rustPlatform.buildRustPackage pkgs.rustPlatform.buildRustPackage
{ {
@ -74,7 +75,7 @@
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = ./Cargo.lock;
}; };
default = service-manager; default = self.packages.${system}.service-manager;
}; };
devShells.default = pkgs.devshell.mkShell { devShells.default = pkgs.devshell.mkShell {
packages = with pkgs; [ packages = with pkgs; [

View file

@ -6,6 +6,7 @@ in
makeServiceConfig = makeServiceConfig =
{ system { system
, modules , modules
, service-manager
, ,
}: }:
let let
@ -15,10 +16,12 @@ in
inherit system modules; inherit system modules;
specialArgs = { }; specialArgs = { };
}; };
services =
lib.flip lib.genAttrs services = map
(serviceName: (name: {
nixosConfig.config.systemd.units."${serviceName}.service".unit) inherit name;
service = ''${nixosConfig.config.systemd.units."${name}.service".unit}/${name}.service'';
})
nixosConfig.config.service-manager.services; nixosConfig.config.service-manager.services;
servicesPath = pkgs.writeTextFile { servicesPath = pkgs.writeTextFile {
@ -27,7 +30,8 @@ in
text = lib.generators.toJSON { } services; text = lib.generators.toJSON { } services;
}; };
activationScript = pkgs.writeShellScript "activate" '' activationScript = pkgs.writeShellScript "activate" ''
echo "${servicesPath}" ${service-manager}/bin/service-manager activate \
--store-path "$(realpath $(dirname ''${0}))"
''; '';
in in
pkgs.linkFarmFromDrvs "service-manager" [ pkgs.linkFarmFromDrvs "service-manager" [

View file

@ -1,10 +1,15 @@
use clap::Parser; use clap::Parser;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::os::unix; use std::os::unix;
use std::path::Path; 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 { struct StorePath {
path: String, 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)] #[command(author, version, about, long_about=None)]
struct Args { struct Args {
#[arg(short, long)] #[command(subcommand)]
action: Action,
}
#[derive(clap::Subcommand, Debug)]
enum Action {
Activate {
#[arg(long)]
store_path: StorePath,
},
Generate {
#[arg(long)]
flake_uri: String, flake_uri: String,
},
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse(); 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 // 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. // we will move this to /nix/var/nix/profiles/system later on.
let user = env::var("USER").expect("USER env var undefined"); 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!( let gcroot_path = format!(
"/nix/var/nix/gcroots/per-user/{}/{}-current", "/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)?; 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)); 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."); create_gcroot(&gcroot_path, &store_path).expect("Failed to create GC root.");
Ok(()) 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>> { fn create_gcroot(gcroot_path: &str, store_path: &StorePath) -> Result<(), Box<dyn Error>> {
let path = Path::new(gcroot_path); create_store_link(store_path, Path::new(gcroot_path))
if path.is_symlink() {
fs::remove_file(path).expect("Error removing old GC root.");
} }
unix::fs::symlink(&store_path.path, gcroot_path).map_err(Box::from)
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, from).map_err(Box::from)
} }
fn get_store_path(nix_build_result: process::Output) -> Result<StorePath, Box<dyn Error>> { fn get_store_path(nix_build_result: process::Output) -> Result<StorePath, Box<dyn Error>> {
if nix_build_result.status.success() { if nix_build_result.status.success() {
String::from_utf8(nix_build_result.stdout) String::from_utf8(nix_build_result.stdout)
.map_err(Box::from) .map_err(Box::from)
.map(StorePath::from) .and_then(parse_nix_build_output)
} else { } else {
String::from_utf8(nix_build_result.stderr).map_or_else(boxed_error(), boxed_error()) 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 { fn run_nix_build(flake_uri: &str, flake_attr: &str) -> process::Output {
process::Command::new("nix") process::Command::new("nix")
.arg("build") .arg("build")
.arg(format!("{}#{}", flake_uri, flake_attr)) .arg(format!("{}#{}", flake_uri, flake_attr))
.arg("--print-out-paths") .arg("--json")
.output() .output()
.expect("Failed to execute nix, is it on your path?") .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]) { fn print_u8(bytes: &[u8]) {
str::from_utf8(bytes).map_or((), |s| { str::from_utf8(bytes).map_or((), |s| {
if !s.is_empty() { if !s.trim().is_empty() {
println!("{}", s) println!("{}", s.trim())
} }
}) })
} }