Split code in modules.

This commit is contained in:
R-VdP 2023-02-08 14:10:16 +00:00
parent c7a481976d
commit 1620a81d15
No known key found for this signature in database
7 changed files with 277 additions and 244 deletions

78
src/activate.rs Normal file
View file

@ -0,0 +1,78 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
use std::time::Duration;
use std::{fs, io, iter, str};
use super::{create_store_link, systemd, StorePath};
#[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())
}
}
pub fn activate(store_path: StorePath) -> Result<()> {
log::info!("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)?;
services.iter().try_for_each(|service| {
create_store_link(
&service.store_path(),
Path::new(&format!("/run/systemd/system/{}", service.name)),
)
})?;
let service_manager = systemd::ServiceManager::new_session()?;
start_services(&service_manager, &services, &Some(Duration::from_secs(30)))?;
Ok(())
}
fn start_services(
service_manager: &systemd::ServiceManager,
services: &[ServiceConfig],
timeout: &Option<Duration>,
) -> Result<()> {
service_manager.daemon_reload()?;
let job_monitor = service_manager.monitor_jobs_init()?;
let successful_services = services.iter().fold(HashSet::new(), |set, service| {
match service_manager.restart_unit(&service.name) {
Ok(_) => {
log::info!("Restarting service {}...", service.name);
set.into_iter()
.chain(iter::once(Box::new(service.name.to_owned())))
.collect()
}
Err(e) => {
log::error!(
"Error restarting unit, please consult the logs: {}",
service.name
);
log::error!("{}", e);
set
}
}
});
if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? {
anyhow::bail!("Timeout waiting for systemd jobs");
}
// TODO: do we want to propagate unit failures here in some way?
Ok(())
}

109
src/generate.rs Normal file
View file

@ -0,0 +1,109 @@
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::{env, fs, process, str};
use super::{create_store_link, StorePath, FLAKE_ATTR, PROFILE_NAME};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NixBuildOutput {
drv_path: String,
outputs: HashMap<String, String>,
}
pub fn generate(flake_uri: &str) -> Result<()> {
let user = env::var("USER")?;
// TODO: we temporarily put this under per-user to avoid needing root access
// we will move this to /nix/var/nix/profiles/ later on.
let profiles_dir = format!("profiles/per-user/{}", user);
let gcroots_dir = format!("gcroots/per-user/{}", user);
let profile_path = format!("/nix/var/nix/{}/{}", profiles_dir, PROFILE_NAME);
let gcroot_path = format!("/nix/var/nix/{}/{}-current", gcroots_dir, PROFILE_NAME);
// FIXME: we should not hard-code the system here
let flake_attr = format!("{}.x86_64-linux", FLAKE_ATTR);
log::info!("Running nix build...");
let store_path = run_nix_build(flake_uri, &flake_attr).and_then(get_store_path)?;
log::info!("Generating new generation from {}", store_path);
install_nix_profile(&store_path, &profile_path).map(print_out_and_err)?;
log::info!("Registering GC root...");
create_gcroot(&gcroot_path, &profile_path)?;
Ok(())
}
fn install_nix_profile(store_path: &StorePath, profile_path: &str) -> Result<process::Output> {
process::Command::new("nix-env")
.arg("--profile")
.arg(profile_path)
.arg("--set")
.arg(&store_path.path)
.output()
.map_err(anyhow::Error::from)
}
fn create_gcroot(gcroot_path: &str, profile_path: &str) -> Result<()> {
let profile_store_path = fs::canonicalize(profile_path)?;
let store_path = StorePath::from(String::from(profile_store_path.to_string_lossy()));
create_store_link(&store_path, Path::new(gcroot_path))
}
fn get_store_path(nix_build_result: process::Output) -> Result<StorePath> {
if nix_build_result.status.success() {
String::from_utf8(nix_build_result.stdout)
.map_err(anyhow::Error::from)
.and_then(parse_nix_build_output)
} else {
String::from_utf8(nix_build_result.stderr)
.map_err(anyhow::Error::from)
.and_then(|e| {
log::error!("{}", e);
Err(anyhow!("Nix build failed."))
})
}
}
fn parse_nix_build_output(output: String) -> Result<StorePath> {
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(anyhow!(
"No output '{}' found in nix build result.",
expected_output_name
));
}
Err(anyhow!(
"Multiple build results were returned, we cannot handle that yet."
))
}
fn run_nix_build(flake_uri: &str, flake_attr: &str) -> Result<process::Output> {
process::Command::new("nix")
.arg("build")
.arg(format!("{}#{}", flake_uri, flake_attr))
.arg("--json")
.output()
.map_err(anyhow::Error::from)
}
fn print_out_and_err(output: process::Output) -> process::Output {
print_u8(&output.stdout);
print_u8(&output.stderr);
output
}
fn print_u8(bytes: &[u8]) {
str::from_utf8(bytes).map_or((), |s| {
if !s.trim().is_empty() {
log::info!("{}", s.trim())
}
})
}

46
src/lib.rs Normal file
View file

@ -0,0 +1,46 @@
pub mod activate;
pub mod generate;
mod systemd;
use anyhow::Result;
use std::os::unix;
use std::path::Path;
use std::{fs, str};
const FLAKE_ATTR: &str = "serviceConfig";
const PROFILE_NAME: &str = "service-manager";
#[derive(Debug, Clone)]
pub struct StorePath {
pub path: String,
}
impl From<String> for StorePath {
fn from(path: String) -> Self {
StorePath {
path: path.trim().into(),
}
}
}
impl std::fmt::Display for StorePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path)
}
}
fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> {
log::info!("Creating symlink: {} -> {}", from.display(), store_path);
if from.is_symlink() {
fs::remove_file(from)?;
}
unix::fs::symlink(&store_path.path, from).map_err(anyhow::Error::from)
}
pub fn compose<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
F: Fn(B) -> C,
G: Fn(A) -> B,
{
move |x| f(g(x))
}

View file

@ -1,35 +1,7 @@
mod systemd;
use anyhow::{anyhow, Result};
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::os::unix;
use std::path::Path;
use std::time::Duration;
use std::{env, fs, io, iter, process, str};
const FLAKE_ATTR: &str = "serviceConfig";
const PROFILE_NAME: &str = "service-manager";
#[derive(Debug, Clone)]
struct StorePath {
path: String,
}
impl From<String> for StorePath {
fn from(path: String) -> Self {
StorePath {
path: path.trim().into(),
}
}
}
impl std::fmt::Display for StorePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path)
}
}
use service_manager::StorePath;
#[derive(clap::Parser, Debug)]
#[command(author, version, about, long_about=None)]
@ -58,26 +30,9 @@ fn main() {
match args.action {
Action::Activate { store_path } => handle_toplevel_error(activate(store_path)),
Action::Generate { flake_uri } => handle_toplevel_error(generate(&flake_uri)),
}
}
fn handle_toplevel_error<T>(r: Result<T>) {
if let Err(e) = r {
log::error!("{}", e)
}
}
#[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())
Action::Generate { flake_uri } => {
handle_toplevel_error(service_manager::generate::generate(&flake_uri))
}
}
}
@ -85,176 +40,11 @@ fn activate(store_path: StorePath) -> Result<()> {
if !nix::unistd::Uid::is_root(nix::unistd::getuid()) {
return Err(anyhow!("We need root permissions."));
}
log::info!("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)?;
services.iter().try_for_each(|service| {
create_store_link(
&service.store_path(),
Path::new(&format!("/run/systemd/system/{}", service.name)),
)
})?;
let service_manager = systemd::ServiceManager::new_session()?;
start_services(&service_manager, &services, &Some(Duration::from_secs(30)))?;
Ok(())
service_manager::activate::activate(store_path)
}
fn start_services(
service_manager: &systemd::ServiceManager,
services: &[ServiceConfig],
timeout: &Option<Duration>,
) -> Result<()> {
service_manager.daemon_reload()?;
let job_monitor = service_manager.monitor_jobs_init()?;
let successful_services = services.iter().fold(HashSet::new(), |set, service| {
match service_manager.restart_unit(&service.name) {
Ok(_) => {
log::info!("Restarting service {}...", service.name);
set.into_iter()
.chain(iter::once(Box::new(service.name.to_owned())))
.collect()
}
Err(e) => {
log::error!(
"Error restarting unit, please consult the logs: {}",
service.name
);
log::error!("{}", e);
set
}
}
});
if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? {
anyhow::bail!("Timeout waiting for systemd jobs");
}
// TODO: do we want to propagate unit failures here in some way?
Ok(())
}
fn generate(flake_uri: &str) -> Result<()> {
let user = env::var("USER")?;
// TODO: we temporarily put this under per-user to avoid needing root access
// we will move this to /nix/var/nix/profiles/ later on.
let profiles_dir = format!("profiles/per-user/{}", user);
let gcroots_dir = format!("gcroots/per-user/{}", user);
let profile_path = format!("/nix/var/nix/{}/{}", profiles_dir, PROFILE_NAME);
let gcroot_path = format!("/nix/var/nix/{}/{}-current", gcroots_dir, PROFILE_NAME);
// FIXME: we should not hard-code the system here
let flake_attr = format!("{}.x86_64-linux", FLAKE_ATTR);
log::info!("Running nix build...");
let store_path = run_nix_build(flake_uri, &flake_attr).and_then(get_store_path)?;
log::info!("Generating new generation from {}", store_path);
install_nix_profile(&store_path, &profile_path).map(print_out_and_err)?;
log::info!("Registering GC root...");
create_gcroot(&gcroot_path, &profile_path)?;
Ok(())
}
fn install_nix_profile(store_path: &StorePath, profile_path: &str) -> Result<process::Output> {
process::Command::new("nix-env")
.arg("--profile")
.arg(profile_path)
.arg("--set")
.arg(&store_path.path)
.output()
.map_err(anyhow::Error::from)
}
fn create_gcroot(gcroot_path: &str, profile_path: &str) -> Result<()> {
let profile_store_path = fs::canonicalize(profile_path)?;
let store_path = StorePath::from(String::from(profile_store_path.to_string_lossy()));
create_store_link(&store_path, Path::new(gcroot_path))
}
fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> {
log::info!("Creating symlink: {} -> {}", from.display(), store_path);
if from.is_symlink() {
fs::remove_file(from)?;
}
unix::fs::symlink(&store_path.path, from).map_err(anyhow::Error::from)
}
fn get_store_path(nix_build_result: process::Output) -> Result<StorePath> {
if nix_build_result.status.success() {
String::from_utf8(nix_build_result.stdout)
.map_err(anyhow::Error::from)
.and_then(parse_nix_build_output)
} else {
String::from_utf8(nix_build_result.stderr)
.map_err(anyhow::Error::from)
.and_then(|e| {
log::error!("{}", e);
Err(anyhow!("Nix build failed."))
})
fn handle_toplevel_error<T>(r: Result<T>) {
if let Err(e) = r {
log::error!("{}", e)
}
}
#[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> {
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(anyhow!(
"No output '{}' found in nix build result.",
expected_output_name
));
}
Err(anyhow!(
"Multiple build results were returned, we cannot handle that yet."
))
}
fn run_nix_build(flake_uri: &str, flake_attr: &str) -> Result<process::Output> {
process::Command::new("nix")
.arg("build")
.arg(format!("{}#{}", flake_uri, flake_attr))
.arg("--json")
.output()
.map_err(anyhow::Error::from)
}
fn print_out_and_err(output: process::Output) -> process::Output {
print_u8(&output.stdout);
print_u8(&output.stderr);
output
}
fn print_u8(bytes: &[u8]) {
str::from_utf8(bytes).map_or((), |s| {
if !s.trim().is_empty() {
log::info!("{}", s.trim())
}
})
}
pub fn compose<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
F: Fn(B) -> C,
G: Fn(A) -> B,
{
move |x| f(g(x))
}

View file

@ -155,8 +155,8 @@ impl ServiceManager {
let job_names_clone = Arc::clone(&job_names);
let token = self.proxy.match_signal(
move |h: OrgFreedesktopSystemd1ManagerJobRemoved, _: &Connection, _: &Message| {
log::debug!("{} added", h.unit);
log_thread("Signal handling");
log::info!("Job for {} done", h.unit);
{
// Insert a new name, and let the lock go out of scope immediately
job_names_clone.lock().unwrap().insert(h.unit);