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

@ -1,73 +1,135 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::fs::DirBuilder;
use std::path::Path;
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")]
struct ServiceConfig {
name: String,
service: String,
#[serde(flatten)]
store_path: StorePath,
}
impl ServiceConfig {
fn store_path(&self) -> StorePath {
StorePath::from(self.service.to_owned())
}
type Services = HashMap<String, ServiceConfig>;
#[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<()> {
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 services: Services = serde_json::from_reader(reader)?;
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 linked_services = link_services(services);
serialise_linked_services(&linked_services)?;
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(())
}
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(
service_manager: &systemd::ServiceManager,
services: &[ServiceConfig],
services: LinkedServices,
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) {
let successful_services = services.keys().fold(
HashSet::with_capacity(services.len()),
|mut set, service| match service_manager.restart_unit(service) {
Ok(_) => {
log::info!("Restarting service {}...", service.name);
set.into_iter()
.chain(iter::once(Box::new(service.name.to_owned())))
.collect()
log::info!("Restarting service {}...", service);
set.insert(Box::new(service.to_owned()));
set
}
Err(e) => {
log::error!(
"Error restarting unit, please consult the logs: {}",
service.name
service
);
log::error!("{}", e);
set
}
}
});
},
);
if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? {
anyhow::bail!("Timeout waiting for systemd jobs");

View file

@ -4,7 +4,7 @@ use std::collections::HashMap;
use std::path::Path;
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)]
#[serde(rename_all = "camelCase")]
@ -14,9 +14,6 @@ struct NixBuildOutput {
}
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
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)?;
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...");
create_gcroot(&gcroot_path, &profile_path)?;
create_gcroot(GCROOT_PATH, PROFILE_PATH)?;
Ok(())
}
@ -36,7 +33,7 @@ fn install_nix_profile(store_path: &StorePath, profile_path: &str) -> Result<pro
.arg("--profile")
.arg(profile_path)
.arg("--set")
.arg(&store_path.path)
.arg(&store_path.store_path)
.output()
.map_err(anyhow::Error::from)
}

View file

@ -3,29 +3,34 @@ pub mod generate;
mod systemd;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::os::unix;
use std::path::Path;
use std::{fs, str};
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 path: String,
pub store_path: String,
}
impl From<String> for StorePath {
fn from(path: String) -> Self {
StorePath {
path: path.trim().into(),
store_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)
write!(f, "{}", self.store_path)
}
}
@ -34,7 +39,7 @@ fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> {
if from.is_symlink() {
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

View file

@ -28,7 +28,7 @@ fn main() -> ExitCode {
let args = Args::parse();
// 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))
}