Implement files under /etc

This commit is contained in:
R-VdP 2023-02-13 20:24:23 +01:00
parent 8b3bba30af
commit b3c7f71456
No known key found for this signature in database
10 changed files with 494 additions and 242 deletions

83
src/activate/etc_files.rs Normal file
View file

@ -0,0 +1,83 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::DirBuilder;
use std::path;
use std::path::{Path, PathBuf};
use std::{fs, io};
use crate::{create_link, create_store_link, StorePath};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EtcFile {
source: StorePath,
target: PathBuf,
uid: u32,
gid: u32,
group: String,
user: String,
mode: String,
}
type EtcFiles = HashMap<String, EtcFile>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EtcFilesConfig {
entries: EtcFiles,
static_env: StorePath,
}
pub fn activate(store_path: StorePath, ephemeral: bool) -> Result<()> {
log::info!("Reading etc file definitions...");
let file = fs::File::open(Path::new(&store_path.store_path).join("etcFiles/etcFiles.json"))?;
let reader = io::BufReader::new(file);
let config: EtcFilesConfig = serde_json::from_reader(reader)?;
log::debug!("{:?}", config);
let etc_dir = etc_dir(ephemeral);
log::debug!("Storing /etc entries in {}", etc_dir.display());
DirBuilder::new().recursive(true).create(&etc_dir)?;
create_store_link(
&config.static_env,
etc_dir.join(".system-manager-static").as_path(),
)?;
config
.entries
.into_iter()
.try_for_each(|(name, entry)| create_etc_link(&name, &entry, &etc_dir))?;
Ok(())
}
fn create_etc_link(name: &str, entry: &EtcFile, etc_dir: &Path) -> Result<()> {
if entry.mode == "symlink" {
if let Some(path::Component::Normal(link_target)) =
entry.target.components().into_iter().next()
{
create_link(
Path::new(".")
.join(".system-manager-static")
.join("etc")
.join(link_target)
.as_path(),
etc_dir.join(link_target).as_path(),
)
} else {
anyhow::bail!("Cannot create link for this entry ({}).", name)
}
} else {
Ok(())
}
}
fn etc_dir(ephemeral: bool) -> PathBuf {
if ephemeral {
Path::new("/run").join("etc")
} else {
Path::new("/etc").to_path_buf()
}
}

224
src/activate/services.rs Normal file
View file

@ -0,0 +1,224 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs::DirBuilder;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{fs, io, str};
use crate::{
create_store_link, remove_link, systemd, StorePath, SERVICES_STATE_FILE_NAME,
SYSTEM_MANAGER_STATE_DIR,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceConfig {
store_path: StorePath,
}
type Services = HashMap<String, ServiceConfig>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LinkedServiceConfig {
#[serde(flatten)]
service_config: ServiceConfig,
#[serde(rename = "linkedPath")]
path: PathBuf,
}
impl LinkedServiceConfig {
fn linked_path(&self) -> PathBuf {
PathBuf::from(&self.path)
}
fn new(service_config: ServiceConfig, path: PathBuf) -> Self {
LinkedServiceConfig {
service_config,
path,
}
}
}
type LinkedServices = HashMap<String, LinkedServiceConfig>;
pub fn activate(store_path: StorePath, ephemeral: bool) -> Result<()> {
let old_linked_services = read_linked_services()?;
log::debug!("{:?}", old_linked_services);
log::info!("Reading service definitions...");
let file = fs::File::open(
Path::new(&store_path.store_path)
.join("services")
.join("services.json"),
)?;
let reader = io::BufReader::new(file);
let services: Services = serde_json::from_reader(reader)?;
let linked_services = link_services(services, ephemeral)?;
serialise_linked_services(&linked_services)?;
let services_to_stop = old_linked_services
.into_iter()
.filter(|(name, _)| !linked_services.contains_key(name))
.collect();
let service_manager = systemd::ServiceManager::new_session()?;
let timeout = Some(Duration::from_secs(30));
// We need to do this before we reload the systemd daemon, so that the daemon
// still knows about these units.
stop_services(&service_manager, &services_to_stop, &timeout)?;
unlink_services(&services_to_stop)?;
// We added all new services and removed old ones, so let's reload the units
// to tell systemd about them.
log::info!("Reloading the systemd daemon...");
service_manager.daemon_reload()?;
start_services(&service_manager, &linked_services, &timeout)?;
log::info!("Done");
Ok(())
}
fn unlink_services(services: &LinkedServices) -> Result<()> {
services
.values()
.try_for_each(|linked_service| remove_link(linked_service.linked_path().as_path()))
}
fn link_services(services: Services, ephemeral: bool) -> Result<LinkedServices> {
let systemd_system_dir = systemd_system_dir(ephemeral);
services.iter().try_fold(
HashMap::with_capacity(services.len()),
|mut linked_services, (name, service_config)| {
let linked_path = systemd_system_dir.join(name);
match create_store_link(&service_config.store_path, linked_path.as_path()) {
Ok(_) => {
linked_services.insert(
name.to_owned(),
LinkedServiceConfig::new(service_config.to_owned(), linked_path),
);
}
Err(e) => {
log::error!("Error linking service {}, skipping.", name);
log::error!("{:?}", e);
}
};
Ok(linked_services)
},
)
}
fn systemd_system_dir(ephemeral: bool) -> PathBuf {
(if ephemeral {
Path::new("/run")
} else {
Path::new("/etc")
})
.join("systemd")
.join("system")
}
// FIXME: we should probably lock this file to avoid concurrent writes
fn serialise_linked_services(linked_services: &LinkedServices) -> Result<()> {
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(SERVICES_STATE_FILE_NAME);
DirBuilder::new()
.recursive(true)
.create(SYSTEM_MANAGER_STATE_DIR)?;
log::info!("Writing state info into file: {}", state_file.display());
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 = Path::new(SYSTEM_MANAGER_STATE_DIR).join(SERVICES_STATE_FILE_NAME);
DirBuilder::new()
.recursive(true)
.create(SYSTEM_MANAGER_STATE_DIR)?;
if Path::new(&state_file).is_file() {
log::info!("Reading state info from {}", state_file.display());
let reader = io::BufReader::new(fs::File::open(state_file)?);
match serde_json::from_reader(reader) {
Ok(linked_services) => return Ok(linked_services),
Err(e) => {
log::error!("Error reading the state file, ignoring.");
log::error!("{:?}", e);
}
}
}
Ok(HashMap::default())
}
fn start_services(
service_manager: &systemd::ServiceManager,
services: &LinkedServices,
timeout: &Option<Duration>,
) -> Result<()> {
for_each_service(
|s| service_manager.start_unit(s),
service_manager,
services,
timeout,
"restarting",
)
}
fn stop_services(
service_manager: &systemd::ServiceManager,
services: &LinkedServices,
timeout: &Option<Duration>,
) -> Result<()> {
for_each_service(
|s| service_manager.stop_unit(s),
service_manager,
services,
timeout,
"stopping",
)
}
fn for_each_service<F, R>(
action: F,
service_manager: &systemd::ServiceManager,
services: &LinkedServices,
timeout: &Option<Duration>,
log_action: &str,
) -> Result<()>
where
F: Fn(&str) -> Result<R>,
{
let job_monitor = service_manager.monitor_jobs_init()?;
let successful_services = services.keys().fold(
HashSet::with_capacity(services.len()),
|mut set, service| match action(service) {
Ok(_) => {
log::info!("Service {}: {}...", service, log_action);
set.insert(Box::new(service.to_owned()));
set
}
Err(e) => {
log::error!(
"Service {}: error {}, please consult the logs",
service,
log_action
);
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(())
}