WIP
This commit is contained in:
parent
0df7ab21e5
commit
cf91b29724
8 changed files with 357 additions and 59 deletions
|
|
@ -11,6 +11,8 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
|
|||
log::info!("Running in ephemeral mode");
|
||||
}
|
||||
|
||||
// TODO we probably need to first deactivate left-over files and services
|
||||
// before we start putting in place the new ones.
|
||||
etc_files::activate(store_path, ephemeral)?;
|
||||
services::activate(store_path, ephemeral)?;
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::DirBuilder;
|
||||
use std::fs::{DirBuilder, Permissions};
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
use std::path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{fs, io};
|
||||
|
||||
use crate::{create_link, create_store_link, StorePath};
|
||||
use crate::{
|
||||
create_link, create_store_link, remove_dir, remove_file, remove_link, StorePath,
|
||||
ETC_STATE_FILE_NAME, SYSTEM_MANAGER_STATE_DIR,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EtcFile {
|
||||
source: StorePath,
|
||||
|
|
@ -22,13 +27,77 @@ struct EtcFile {
|
|||
|
||||
type EtcFiles = HashMap<String, EtcFile>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EtcFilesConfig {
|
||||
entries: EtcFiles,
|
||||
static_env: StorePath,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EtcStateInfo {
|
||||
status: EtcFileStatus,
|
||||
// TODO directories and files are now both represented as a string associated with a nested
|
||||
// map. For files the nested map is simple empty.
|
||||
// We could potentially optimise this.
|
||||
nested: HashMap<String, EtcStateInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum EtcFileStatus {
|
||||
Managed,
|
||||
Unmanaged,
|
||||
}
|
||||
|
||||
impl EtcFileStatus {
|
||||
fn merge(&self, other: &Self) -> Self {
|
||||
use EtcFileStatus::*;
|
||||
|
||||
match (self, other) {
|
||||
(Unmanaged, Unmanaged) => Unmanaged,
|
||||
_ => Managed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EtcStateInfo {
|
||||
fn new() -> Self {
|
||||
Self::with_status(EtcFileStatus::Unmanaged)
|
||||
}
|
||||
|
||||
fn with_status(status: EtcFileStatus) -> Self {
|
||||
Self {
|
||||
status,
|
||||
nested: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO unit tests
|
||||
fn register_managed_path(mut self, components: &[String], path: String) -> Self {
|
||||
let state = components.iter().fold(&mut self, |state, component| {
|
||||
if !state.nested.contains_key(component) {
|
||||
let new_state = Self::new();
|
||||
state.nested.insert(component.to_owned(), new_state);
|
||||
}
|
||||
state.nested.get_mut(component).unwrap()
|
||||
});
|
||||
|
||||
state
|
||||
.nested
|
||||
.insert(path, Self::with_status(EtcFileStatus::Managed));
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CreatedEtcFile {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
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"))?;
|
||||
|
|
@ -40,48 +109,205 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
|
|||
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))?;
|
||||
// TODO: constant?
|
||||
let static_dir_name = ".system-manager-static";
|
||||
let static_path = etc_dir.join(static_dir_name);
|
||||
create_store_link(&config.static_env, static_path.as_path())?;
|
||||
|
||||
// TODO register the crated files (including .system-manager-static) in a
|
||||
// state file, and clean up old files from the previous generation
|
||||
// TODO remove all paths that are in the state file from the previous generation
|
||||
// and not in the current one.
|
||||
|
||||
let state = read_created_files()?;
|
||||
let new_state = create_etc_links(config.entries.values(), &etc_dir, state);
|
||||
serialise_created_files(&new_state.register_managed_path(&[], static_dir_name.to_owned()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO we need to record in a state file which files we created and then remove them
|
||||
pub fn deactivate() -> Result<()> {
|
||||
let old_created_files = read_created_files()?;
|
||||
log::debug!("{:?}", old_created_files);
|
||||
|
||||
// TODO
|
||||
//old_created_files
|
||||
// .iter()
|
||||
// .try_for_each(|created_file| remove_created_file(&created_file.path, "etc"))?;
|
||||
|
||||
serialise_created_files(&EtcStateInfo::new())?;
|
||||
|
||||
log::info!("Done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_etc_link(name: &str, entry: &EtcFile, etc_dir: &Path) -> Result<()> {
|
||||
fn remove_created_file<P>(created_file: &P, root_dir: &str) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path = created_file.as_ref();
|
||||
let recurse = if path.is_file() {
|
||||
remove_file(path)?;
|
||||
true
|
||||
} else if path.is_symlink() {
|
||||
remove_link(path)?;
|
||||
true
|
||||
} else if path.is_dir() && fs::read_dir(created_file)?.next().is_none() {
|
||||
log::info!("We will remove the following directory: {}", path.display());
|
||||
remove_dir(path)?;
|
||||
true
|
||||
} else {
|
||||
log::debug!("Stopped at directory {}.", path.display());
|
||||
false
|
||||
};
|
||||
|
||||
if recurse {
|
||||
if let Some(parent) = path.parent() {
|
||||
if parent
|
||||
.file_name()
|
||||
.filter(|name| &root_dir != name)
|
||||
.is_some()
|
||||
{
|
||||
log::debug!("Recursing up into directory {}...", parent.display());
|
||||
return remove_created_file(&parent, root_dir);
|
||||
}
|
||||
log::debug!("Reached root dir: {}", parent.display());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_etc_links<'a, E>(entries: E, etc_dir: &Path, state: EtcStateInfo) -> EtcStateInfo
|
||||
where
|
||||
E: Iterator<Item = &'a EtcFile>,
|
||||
{
|
||||
entries.fold(state, |state, entry| {
|
||||
let (new_state, status) = create_etc_link(entry, etc_dir, state);
|
||||
match status {
|
||||
Ok(_) => new_state,
|
||||
Err(e) => {
|
||||
log::error!("Error while creating file in {}: {e}", etc_dir.display());
|
||||
new_state
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_etc_link(
|
||||
entry: &EtcFile,
|
||||
etc_dir: &Path,
|
||||
state: EtcStateInfo,
|
||||
) -> (EtcStateInfo, Result<()>) {
|
||||
if entry.mode == "symlink" {
|
||||
if let Some(path::Component::Normal(link_target)) =
|
||||
entry.target.components().into_iter().next()
|
||||
{
|
||||
create_link(
|
||||
let link_name = etc_dir.join(link_target);
|
||||
match create_link(
|
||||
Path::new(".")
|
||||
.join(".system-manager-static")
|
||||
.join("etc")
|
||||
.join(link_target)
|
||||
.as_path(),
|
||||
etc_dir.join(link_target).as_path(),
|
||||
)
|
||||
link_name.as_path(),
|
||||
) {
|
||||
Ok(_) => (
|
||||
state.register_managed_path(&[], link_target.to_string_lossy().into_owned()),
|
||||
Ok(()),
|
||||
),
|
||||
e => (state, e),
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("Cannot create link for this entry ({name}).")
|
||||
(
|
||||
state,
|
||||
Err(anyhow!("Cannot create link: {}", entry.target.display(),)),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
let dirbuilder = DirBuilder::new();
|
||||
let target_path = etc_dir.join(entry.target.as_path());
|
||||
|
||||
// Create all parent dirs that do not exist yet
|
||||
let (new_state, created_paths, _, status) = target_path
|
||||
.parent()
|
||||
.unwrap() // TODO
|
||||
.components()
|
||||
.fold_while(
|
||||
(state, Vec::new(), PathBuf::from("/"), Ok(())),
|
||||
|(state, mut created, path, _), component| {
|
||||
use itertools::FoldWhile::{Continue, Done};
|
||||
use path::Component;
|
||||
|
||||
match component {
|
||||
Component::RootDir => Continue((state, created, path, Ok(()))),
|
||||
Component::Normal(dir) => {
|
||||
let new_path = path.join(dir);
|
||||
if !new_path.exists() {
|
||||
log::debug!("Creating path: {}", new_path.display());
|
||||
match dirbuilder.create(new_path.as_path()) {
|
||||
Ok(_) => {
|
||||
let new_state = state.register_managed_path(
|
||||
&created,
|
||||
dir.to_string_lossy().into_owned(),
|
||||
);
|
||||
created.push(dir.to_string_lossy().into_owned());
|
||||
Continue((new_state, created, new_path, Ok(())))
|
||||
}
|
||||
Err(e) => Done((state, created, path, Err(anyhow!(e)))),
|
||||
}
|
||||
} else {
|
||||
created.push(dir.to_string_lossy().into_owned());
|
||||
Continue((state, created, new_path, Ok(())))
|
||||
}
|
||||
}
|
||||
otherwise => Done((
|
||||
state,
|
||||
created,
|
||||
path,
|
||||
Err(anyhow!(
|
||||
"Unexpected path component encountered: {:?}",
|
||||
otherwise
|
||||
)),
|
||||
)),
|
||||
}
|
||||
},
|
||||
)
|
||||
.into_inner();
|
||||
|
||||
match status.and_then(|_| {
|
||||
copy_file(
|
||||
entry
|
||||
.source
|
||||
.store_path
|
||||
.join("etc")
|
||||
.join(&entry.target)
|
||||
.as_path(),
|
||||
&target_path,
|
||||
&entry.mode,
|
||||
)
|
||||
}) {
|
||||
Ok(_) => (
|
||||
new_state.register_managed_path(
|
||||
&created_paths,
|
||||
target_path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
),
|
||||
Ok(()),
|
||||
),
|
||||
e => (new_state, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_file(source: &Path, target: &Path, mode: &str) -> Result<()> {
|
||||
fs::copy(source, target)?;
|
||||
let mode_int = u32::from_str_radix(mode, 8).map_err(anyhow::Error::from)?;
|
||||
fs::set_permissions(target, Permissions::from_mode(mode_int))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn etc_dir(ephemeral: bool) -> PathBuf {
|
||||
if ephemeral {
|
||||
Path::new("/run").join("etc")
|
||||
|
|
@ -89,3 +315,35 @@ fn etc_dir(ephemeral: bool) -> PathBuf {
|
|||
Path::new("/etc").to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
fn serialise_created_files(created_files: &EtcStateInfo) -> Result<()> {
|
||||
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(ETC_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, created_files)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_created_files() -> Result<EtcStateInfo> {
|
||||
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(ETC_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(created_files) => return Ok(created_files),
|
||||
Err(e) => {
|
||||
log::error!("Error reading the state file, ignoring.");
|
||||
log::error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(EtcStateInfo::new())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,8 +87,6 @@ pub fn deactivate() -> Result<()> {
|
|||
let old_linked_services = read_linked_services()?;
|
||||
log::debug!("{:?}", old_linked_services);
|
||||
|
||||
serialise_linked_services(&HashMap::new())?;
|
||||
|
||||
let service_manager = systemd::ServiceManager::new_session()?;
|
||||
let timeout = Some(Duration::from_secs(30));
|
||||
|
||||
|
|
@ -102,6 +100,8 @@ pub fn deactivate() -> Result<()> {
|
|||
log::info!("Reloading the systemd daemon...");
|
||||
service_manager.daemon_reload()?;
|
||||
|
||||
serialise_linked_services(&HashMap::new())?;
|
||||
|
||||
log::info!("Done");
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
12
src/lib.rs
12
src/lib.rs
|
|
@ -14,7 +14,7 @@ const PROFILE_NAME: &str = "system-manager";
|
|||
const GCROOT_PATH: &str = "/nix/var/nix/gcroots/system-manager-current";
|
||||
const SYSTEM_MANAGER_STATE_DIR: &str = "/var/lib/system-manager/state";
|
||||
const SERVICES_STATE_FILE_NAME: &str = "services.json";
|
||||
//const ETC_STATE_FILE_NAME: &str = "etc-files.json";
|
||||
const ETC_STATE_FILE_NAME: &str = "etc-files.json";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(from = "String", into = "String", rename_all = "camelCase")]
|
||||
|
|
@ -77,6 +77,16 @@ fn remove_link(from: &Path) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
fn remove_file(from: &Path) -> Result<()> {
|
||||
log::info!("Removing file: {}", from.display());
|
||||
fs::remove_file(from).map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
fn remove_dir(from: &Path) -> Result<()> {
|
||||
log::info!("Removing directory: {}", from.display());
|
||||
fs::remove_dir(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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue