Write generation state into a state file and restructure the services.json file produced by nix.

This commit is contained in:
R-VdP 2023-02-09 10:25:04 +00:00
parent 8f31818a27
commit c3be9ceb19
No known key found for this signature in database
6 changed files with 143 additions and 80 deletions

View file

@ -18,16 +18,14 @@ in
}; };
services = services =
map lib.listToAttrs
(name: (map
let (name:
serviceName = "${name}.service"; let
in serviceName = "${name}.service";
{ in
name = serviceName; lib.nameValuePair serviceName { storePath = ''${nixosConfig.config.systemd.units."${serviceName}".unit}/${serviceName}''; })
service = ''${nixosConfig.config.systemd.units."${serviceName}".unit}/${serviceName}''; nixosConfig.config.service-manager.services);
})
nixosConfig.config.service-manager.services;
servicesPath = pkgs.writeTextFile { servicesPath = pkgs.writeTextFile {
name = "services"; name = "services";

View file

@ -4,31 +4,32 @@
}: }:
let let
services = services =
lib.listToAttrs (lib.flip lib.genList 30 (ix: { lib.listToAttrs
name = "service-${toString ix}"; (lib.flip lib.genList 10 (ix:
value = { lib.nameValuePair "service-${toString ix}"
enable = true; {
description = "service-${toString ix}"; enable = true;
wants = [ "network-online.target" ]; description = "service-${toString ix}";
after = [ wants = [ "network-online.target" ];
"network-online.target" after = [
"avahi-daemon.service" "network-online.target"
"chrony.service" "avahi-daemon.service"
"nss-lookup.target" "chrony.service"
"tinc.service" "nss-lookup.target"
"pulseaudio.service" "tinc.service"
]; "pulseaudio.service"
serviceConfig = { ];
Type = "oneshot"; serviceConfig = {
RemainAfterExit = true; Type = "oneshot";
ExecReload = "true"; RemainAfterExit = true;
}; ExecReload = "true";
wantedBy = [ "multi-user.target" ]; };
script = '' wantedBy = [ "multi-user.target" ];
sleep ${if ix > 20 then "3" else "1"} script = ''
''; sleep ${if ix > 5 then "3" else "1"}
}; '';
})); })
);
in in
{ {
options = { options = {

View file

@ -1,73 +1,135 @@
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use std::fs::DirBuilder;
use std::path::Path; use std::path::Path;
use std::time::Duration; use std::time::Duration;
use std::{fs, io, iter, str}; use std::{fs, io, str};
use super::{create_store_link, systemd, StorePath}; use super::{create_store_link, systemd, StorePath, SERVICE_MANAGER_STATE_DIR, SYSTEMD_UNIT_DIR};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ServiceConfig { struct ServiceConfig {
name: String, #[serde(flatten)]
service: String, store_path: StorePath,
} }
impl ServiceConfig { type Services = HashMap<String, ServiceConfig>;
fn store_path(&self) -> StorePath {
StorePath::from(self.service.to_owned()) #[derive(Debug, Clone, Serialize, Deserialize)]
} #[serde(rename_all = "camelCase")]
struct LinkedServiceConfig {
#[serde(flatten)]
service_config: ServiceConfig,
linked_path: String,
} }
type LinkedServices = HashMap<String, LinkedServiceConfig>;
pub fn activate(store_path: StorePath) -> Result<()> { pub fn activate(store_path: StorePath) -> Result<()> {
log::info!("Activating service-manager profile: {}", store_path); log::info!("Activating service-manager profile: {}", store_path);
let file = fs::File::open(store_path.path + "/services/services.json")?; log::debug!("{:?}", read_linked_services()?);
log::info!("Reading service definitions...");
let file = fs::File::open(store_path.store_path + "/services/services.json")?;
let reader = io::BufReader::new(file); let reader = io::BufReader::new(file);
let services: Services = serde_json::from_reader(reader)?;
let services: Vec<ServiceConfig> = serde_json::from_reader(reader)?; let linked_services = link_services(services);
serialise_linked_services(&linked_services)?;
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()?; let service_manager = systemd::ServiceManager::new_session()?;
start_services(&service_manager, &services, &Some(Duration::from_secs(30)))?; start_services(
&service_manager,
linked_services,
&Some(Duration::from_secs(30)),
)?;
Ok(()) Ok(())
} }
fn link_services(services: Services) -> LinkedServices {
services.iter().fold(
HashMap::with_capacity(services.len()),
|mut linked_services, (name, service_config)| {
let linked_path = format!("{}/{}", SYSTEMD_UNIT_DIR, name);
match create_store_link(&service_config.store_path, Path::new(&linked_path)) {
Ok(_) => {
linked_services.insert(
name.to_owned(),
LinkedServiceConfig {
service_config: service_config.to_owned(),
linked_path,
},
);
linked_services
}
e @ Err(_) => {
log::error!("Error linking service {}, skipping.", name);
log::error!("{:?}", e);
linked_services
}
}
},
)
}
// FIXME: we should probably lock this file to avoid concurrent writes
fn serialise_linked_services(linked_services: &LinkedServices) -> Result<()> {
let state_file = format!("{}/services.json", SERVICE_MANAGER_STATE_DIR);
DirBuilder::new()
.recursive(true)
.create(SERVICE_MANAGER_STATE_DIR)?;
log::info!("Writing state info into file: {}", state_file);
let writer = io::BufWriter::new(fs::File::create(state_file)?);
serde_json::to_writer(writer, linked_services)?;
Ok(())
}
fn read_linked_services() -> Result<LinkedServices> {
let state_file = format!("{}/services.json", SERVICE_MANAGER_STATE_DIR);
DirBuilder::new()
.recursive(true)
.create(SERVICE_MANAGER_STATE_DIR)?;
if Path::new(&state_file).is_file() {
log::info!("Reading state info from {}", state_file);
let reader = io::BufReader::new(fs::File::open(state_file)?);
let linked_services = serde_json::from_reader(reader)?;
return Ok(linked_services);
}
Ok(HashMap::default())
}
fn start_services( fn start_services(
service_manager: &systemd::ServiceManager, service_manager: &systemd::ServiceManager,
services: &[ServiceConfig], services: LinkedServices,
timeout: &Option<Duration>, timeout: &Option<Duration>,
) -> Result<()> { ) -> Result<()> {
service_manager.daemon_reload()?; service_manager.daemon_reload()?;
let job_monitor = service_manager.monitor_jobs_init()?; let job_monitor = service_manager.monitor_jobs_init()?;
let successful_services = services.iter().fold(HashSet::new(), |set, service| { let successful_services = services.keys().fold(
match service_manager.restart_unit(&service.name) { HashSet::with_capacity(services.len()),
|mut set, service| match service_manager.restart_unit(service) {
Ok(_) => { Ok(_) => {
log::info!("Restarting service {}...", service.name); log::info!("Restarting service {}...", service);
set.into_iter() set.insert(Box::new(service.to_owned()));
.chain(iter::once(Box::new(service.name.to_owned()))) set
.collect()
} }
Err(e) => { Err(e) => {
log::error!( log::error!(
"Error restarting unit, please consult the logs: {}", "Error restarting unit, please consult the logs: {}",
service.name service
); );
log::error!("{}", e); log::error!("{}", e);
set set
} }
} },
}); );
if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? { if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? {
anyhow::bail!("Timeout waiting for systemd jobs"); anyhow::bail!("Timeout waiting for systemd jobs");

View file

@ -4,7 +4,7 @@ use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use std::{fs, process, str}; use std::{fs, process, str};
use super::{create_store_link, StorePath, FLAKE_ATTR, PROFILE_NAME}; use super::{create_store_link, StorePath, FLAKE_ATTR, GCROOT_PATH, PROFILE_PATH};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -14,9 +14,6 @@ struct NixBuildOutput {
} }
pub fn generate(flake_uri: &str) -> Result<()> { pub fn generate(flake_uri: &str) -> Result<()> {
let profile_path = format!("/nix/var/nix/profiles/{}", PROFILE_NAME);
let gcroot_path = format!("/nix/var/nix/gcroots/{}-current", PROFILE_NAME);
// FIXME: we should not hard-code the system here // FIXME: we should not hard-code the system here
let flake_attr = format!("{}.x86_64-linux", FLAKE_ATTR); let flake_attr = format!("{}.x86_64-linux", FLAKE_ATTR);
@ -24,10 +21,10 @@ pub fn generate(flake_uri: &str) -> Result<()> {
let store_path = run_nix_build(flake_uri, &flake_attr).and_then(get_store_path)?; let store_path = run_nix_build(flake_uri, &flake_attr).and_then(get_store_path)?;
log::info!("Generating new generation from {}", store_path); log::info!("Generating new generation from {}", store_path);
install_nix_profile(&store_path, &profile_path).map(print_out_and_err)?; install_nix_profile(&store_path, PROFILE_PATH).map(print_out_and_err)?;
log::info!("Registering GC root..."); log::info!("Registering GC root...");
create_gcroot(&gcroot_path, &profile_path)?; create_gcroot(GCROOT_PATH, PROFILE_PATH)?;
Ok(()) Ok(())
} }
@ -36,7 +33,7 @@ fn install_nix_profile(store_path: &StorePath, profile_path: &str) -> Result<pro
.arg("--profile") .arg("--profile")
.arg(profile_path) .arg(profile_path)
.arg("--set") .arg("--set")
.arg(&store_path.path) .arg(&store_path.store_path)
.output() .output()
.map_err(anyhow::Error::from) .map_err(anyhow::Error::from)
} }

View file

@ -3,29 +3,34 @@ pub mod generate;
mod systemd; mod systemd;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::os::unix; use std::os::unix;
use std::path::Path; use std::path::Path;
use std::{fs, str}; use std::{fs, str};
const FLAKE_ATTR: &str = "serviceConfig"; const FLAKE_ATTR: &str = "serviceConfig";
const PROFILE_NAME: &str = "service-manager"; const PROFILE_PATH: &str = "/nix/var/nix/profiles/service-manager";
const GCROOT_PATH: &str = "/nix/var/nix/gcroots/service-manager-current";
const SYSTEMD_UNIT_DIR: &str = "/run/systemd/system";
const SERVICE_MANAGER_STATE_DIR: &str = "/var/lib/service-manager/state";
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorePath { pub struct StorePath {
pub path: String, pub store_path: String,
} }
impl From<String> for StorePath { impl From<String> for StorePath {
fn from(path: String) -> Self { fn from(path: String) -> Self {
StorePath { StorePath {
path: path.trim().into(), store_path: path.trim().into(),
} }
} }
} }
impl std::fmt::Display for StorePath { impl std::fmt::Display for StorePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path) write!(f, "{}", self.store_path)
} }
} }
@ -34,7 +39,7 @@ fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> {
if from.is_symlink() { if from.is_symlink() {
fs::remove_file(from)?; fs::remove_file(from)?;
} }
unix::fs::symlink(&store_path.path, from).map_err(anyhow::Error::from) unix::fs::symlink(&store_path.store_path, from).map_err(anyhow::Error::from)
} }
pub fn compose<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C pub fn compose<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C

View file

@ -28,7 +28,7 @@ fn main() -> ExitCode {
let args = Args::parse(); let args = Args::parse();
// FIXME: set default level to info // FIXME: set default level to info
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
handle_toplevel_error(go(args.action)) handle_toplevel_error(go(args.action))
} }