Implement files under /etc
This commit is contained in:
parent
8b3bba30af
commit
b3c7f71456
10 changed files with 494 additions and 242 deletions
|
|
@ -57,7 +57,7 @@
|
||||||
inherit system;
|
inherit system;
|
||||||
overlays = [ (import rust-overlay) devshell.overlay ];
|
overlays = [ (import rust-overlay) devshell.overlay ];
|
||||||
};
|
};
|
||||||
rust = pkgs.rust-bin.stable."1.66.0";
|
rust = pkgs.rust-bin.stable."1.67.1";
|
||||||
llvm = pkgs.llvmPackages_latest;
|
llvm = pkgs.llvmPackages_latest;
|
||||||
|
|
||||||
craneLib = (crane.mkLib pkgs).overrideToolchain rust.default;
|
craneLib = (crane.mkLib pkgs).overrideToolchain rust.default;
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
serviceConfig = self.lib.makeServiceConfig {
|
systemConfig = self.lib.makeServiceConfig {
|
||||||
inherit system;
|
inherit system;
|
||||||
modules = [
|
modules = [
|
||||||
./nix/modules
|
./nix/modules
|
||||||
|
|
|
||||||
71
nix/lib.nix
71
nix/lib.nix
|
|
@ -12,10 +12,19 @@ in
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
nixosConfig = lib.nixosSystem {
|
nixosConfig = (lib.nixosSystem {
|
||||||
inherit system modules;
|
inherit system;
|
||||||
|
modules = [ ./modules/system-manager.nix ] ++ modules;
|
||||||
specialArgs = { };
|
specialArgs = { };
|
||||||
};
|
}).config;
|
||||||
|
|
||||||
|
returnIfNoAssertions = drv:
|
||||||
|
let
|
||||||
|
failedAssertions = map (x: x.message) (lib.filter (x: !x.assertion) nixosConfig.assertions);
|
||||||
|
in
|
||||||
|
if failedAssertions != [ ]
|
||||||
|
then throw "\nFailed assertions:\n${lib.concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
|
||||||
|
else lib.showWarnings nixosConfig.warnings drv;
|
||||||
|
|
||||||
services =
|
services =
|
||||||
lib.listToAttrs
|
lib.listToAttrs
|
||||||
|
|
@ -24,21 +33,69 @@ in
|
||||||
let
|
let
|
||||||
serviceName = "${name}.service";
|
serviceName = "${name}.service";
|
||||||
in
|
in
|
||||||
lib.nameValuePair serviceName { storePath = ''${nixosConfig.config.systemd.units."${serviceName}".unit}/${serviceName}''; })
|
lib.nameValuePair serviceName {
|
||||||
nixosConfig.config.system-manager.services);
|
storePath =
|
||||||
|
''${nixosConfig.systemd.units."${serviceName}".unit}/${serviceName}'';
|
||||||
|
})
|
||||||
|
nixosConfig.system-manager.services);
|
||||||
|
|
||||||
servicesPath = pkgs.writeTextFile {
|
servicesPath = pkgs.writeTextFile {
|
||||||
name = "services";
|
name = "services";
|
||||||
destination = "/services.json";
|
destination = "/services.json";
|
||||||
text = lib.generators.toJSON { } services;
|
text = lib.generators.toJSON { } services;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# TODO: handle globbing
|
||||||
|
etcFiles =
|
||||||
|
let
|
||||||
|
isManaged = name: lib.elem name nixosConfig.system-manager.etcFiles;
|
||||||
|
|
||||||
|
addToStore = name: file: pkgs.runCommandLocal "${name}-etc-link" { } ''
|
||||||
|
mkdir -p "$out/etc/$(dirname "${file.target}")"
|
||||||
|
ln -s "${file.source}" "$out/etc/${file.target}"
|
||||||
|
|
||||||
|
if [ "${file.mode}" != symlink ]; then
|
||||||
|
echo "${file.mode}" > "$out/etc/${file.target}.mode"
|
||||||
|
echo "${file.user}" > "$out/etc/${file.target}.uid"
|
||||||
|
echo "${file.group}" > "$out/etc/${file.target}.gid"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
filteredEntries = lib.filterAttrs
|
||||||
|
(name: etcFile: etcFile.enable && isManaged name)
|
||||||
|
nixosConfig.environment.etc;
|
||||||
|
|
||||||
|
srcDrvs = lib.mapAttrs addToStore filteredEntries;
|
||||||
|
|
||||||
|
entries = lib.mapAttrs
|
||||||
|
(name: file: file // { source = "${srcDrvs.${name}}"; })
|
||||||
|
filteredEntries;
|
||||||
|
|
||||||
|
staticEnv = pkgs.buildEnv {
|
||||||
|
name = "etc-static-env";
|
||||||
|
paths = lib.attrValues srcDrvs;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{ inherit entries staticEnv; };
|
||||||
|
|
||||||
|
etcPath = pkgs.writeTextFile {
|
||||||
|
name = "etcFiles";
|
||||||
|
destination = "/etcFiles.json";
|
||||||
|
text = lib.generators.toJSON { } etcFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
# TODO: remove --ephemeral
|
||||||
activationScript = pkgs.writeShellScript "activate" ''
|
activationScript = pkgs.writeShellScript "activate" ''
|
||||||
${system-manager}/bin/system-manager activate \
|
${system-manager}/bin/system-manager activate \
|
||||||
--store-path "$(realpath $(dirname ''${0}))"
|
--store-path "$(realpath $(dirname ''${0}))" \
|
||||||
|
"$@"
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
|
returnIfNoAssertions (
|
||||||
pkgs.linkFarmFromDrvs "system-manager" [
|
pkgs.linkFarmFromDrvs "system-manager" [
|
||||||
servicesPath
|
servicesPath
|
||||||
|
etcPath
|
||||||
activationScript
|
activationScript
|
||||||
];
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{ lib
|
{ lib
|
||||||
, pkgs
|
, pkgs
|
||||||
|
, config
|
||||||
, ...
|
, ...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
|
@ -30,16 +31,42 @@ let
|
||||||
'';
|
'';
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
in
|
|
||||||
{
|
etcFiles = {
|
||||||
options = {
|
foo = {
|
||||||
system-manager.services = lib.mkOption {
|
text = ''
|
||||||
type = with lib.types; listOf str;
|
This is just a test!
|
||||||
};
|
'';
|
||||||
|
target = "foo_test";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"baz/bar/foo2" = {
|
||||||
|
text = ''
|
||||||
|
Another test!
|
||||||
|
'';
|
||||||
|
mode = "symlink";
|
||||||
|
};
|
||||||
|
|
||||||
|
foo3 = {
|
||||||
|
text = "boo!";
|
||||||
|
mode = "0700";
|
||||||
|
user = "root";
|
||||||
|
group = "root";
|
||||||
|
};
|
||||||
|
|
||||||
|
out-of-store = {
|
||||||
|
source = "/run/systemd/system/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
config = {
|
config = {
|
||||||
system-manager.services = lib.attrNames services;
|
system.stateVersion = lib.trivial.release;
|
||||||
|
system-manager = {
|
||||||
|
etcFiles = lib.attrNames etcFiles;
|
||||||
|
services = lib.attrNames services;
|
||||||
|
};
|
||||||
|
environment.etc = etcFiles;
|
||||||
systemd = { inherit services; };
|
systemd = { inherit services; };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
nix/modules/system-manager.nix
Normal file
34
nix/modules/system-manager.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{ lib
|
||||||
|
, pkgs
|
||||||
|
, config
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
options.system-manager = {
|
||||||
|
services = lib.mkOption {
|
||||||
|
type = with lib.types; listOf str;
|
||||||
|
};
|
||||||
|
|
||||||
|
etcFiles = lib.mkOption {
|
||||||
|
type = with lib.types; listOf str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
# Avoid some standard NixOS assertions
|
||||||
|
boot = {
|
||||||
|
loader.grub.enable = false;
|
||||||
|
initrd.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
assertions = lib.flip map config.system-manager.etcFiles (entry:
|
||||||
|
{
|
||||||
|
assertion = lib.hasAttr entry config.environment.etc;
|
||||||
|
message = lib.concatStringsSep " " [
|
||||||
|
"The entry ${entry} that was passed to system-manager.etcFiles"
|
||||||
|
"is not present in environment.etc"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
218
src/activate.rs
218
src/activate.rs
|
|
@ -1,215 +1,17 @@
|
||||||
|
mod etc_files;
|
||||||
|
mod services;
|
||||||
|
|
||||||
use anyhow::Result;
|
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 super::{
|
use crate::StorePath;
|
||||||
create_store_link, remove_store_link, systemd, StorePath, STATE_FILE_NAME, SYSTEMD_UNIT_DIR,
|
|
||||||
SYSTEM_MANAGER_STATE_DIR,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
pub fn activate(store_path: StorePath, ephemeral: bool) -> Result<()> {
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct ServiceConfig {
|
|
||||||
#[serde(flatten)]
|
|
||||||
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: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinkedServiceConfig {
|
|
||||||
fn linked_path(&self) -> PathBuf {
|
|
||||||
PathBuf::from(self.path.to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new(service_config: ServiceConfig, path: PathBuf) -> Result<Self> {
|
|
||||||
if let Some(path) = path.to_str() {
|
|
||||||
return Ok(LinkedServiceConfig {
|
|
||||||
service_config,
|
|
||||||
path: String::from(path),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
anyhow::bail!("Could not decode path")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type LinkedServices = HashMap<String, LinkedServiceConfig>;
|
|
||||||
|
|
||||||
pub fn activate(store_path: StorePath) -> Result<()> {
|
|
||||||
log::info!("Activating system-manager profile: {}", store_path);
|
log::info!("Activating system-manager profile: {}", store_path);
|
||||||
|
if ephemeral {
|
||||||
|
log::info!("Running in ephemeral mode");
|
||||||
|
}
|
||||||
|
|
||||||
let old_linked_services = read_linked_services()?;
|
etc_files::activate(store_path.clone(), ephemeral)?;
|
||||||
log::debug!("{:?}", old_linked_services);
|
services::activate(store_path, ephemeral)?;
|
||||||
|
|
||||||
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 linked_services = link_services(services)?;
|
|
||||||
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_store_link(linked_service.linked_path().as_path()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn link_services(services: Services) -> Result<LinkedServices> {
|
|
||||||
services.iter().try_fold(
|
|
||||||
HashMap::with_capacity(services.len()),
|
|
||||||
|mut linked_services, (name, service_config)| {
|
|
||||||
let linked_path = PathBuf::from(format!("{}/{}", SYSTEMD_UNIT_DIR, 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)?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
e @ Err(_) => {
|
|
||||||
log::error!("Error linking service {}, skipping.", name);
|
|
||||||
log::error!("{:?}", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(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!("{}/{}", SYSTEM_MANAGER_STATE_DIR, STATE_FILE_NAME);
|
|
||||||
DirBuilder::new()
|
|
||||||
.recursive(true)
|
|
||||||
.create(SYSTEM_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!("{}/{}", SYSTEM_MANAGER_STATE_DIR, 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);
|
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
83
src/activate/etc_files.rs
Normal file
83
src/activate/etc_files.rs
Normal 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
224
src/activate/services.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ struct NixBuildOutput {
|
||||||
|
|
||||||
pub fn generate(flake_uri: &str) -> Result<()> {
|
pub fn generate(flake_uri: &str) -> Result<()> {
|
||||||
// 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!("{FLAKE_ATTR}.x86_64-linux");
|
||||||
|
|
||||||
log::info!("Building new system-manager generation...");
|
log::info!("Building new system-manager generation...");
|
||||||
log::info!("Running nix build...");
|
log::info!("Running nix build...");
|
||||||
|
|
@ -83,7 +83,7 @@ fn parse_nix_build_output(output: String) -> Result<StorePath> {
|
||||||
fn run_nix_build(flake_uri: &str, flake_attr: &str) -> Result<process::Output> {
|
fn run_nix_build(flake_uri: &str, flake_attr: &str) -> Result<process::Output> {
|
||||||
process::Command::new("nix")
|
process::Command::new("nix")
|
||||||
.arg("build")
|
.arg("build")
|
||||||
.arg(format!("{}#{}", flake_uri, flake_attr))
|
.arg(format!("{flake_uri}#{flake_attr}"))
|
||||||
.arg("--json")
|
.arg("--json")
|
||||||
.output()
|
.output()
|
||||||
.map_err(anyhow::Error::from)
|
.map_err(anyhow::Error::from)
|
||||||
|
|
|
||||||
44
src/lib.rs
44
src/lib.rs
|
|
@ -5,45 +5,65 @@ mod systemd;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::os::unix;
|
use std::os::unix;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::{fs, str};
|
use std::{fs, str};
|
||||||
|
|
||||||
const FLAKE_ATTR: &str = "serviceConfig";
|
const FLAKE_ATTR: &str = "systemConfig";
|
||||||
const PROFILE_PATH: &str = "/nix/var/nix/profiles/system-manager";
|
const PROFILE_PATH: &str = "/nix/var/nix/profiles/system-manager";
|
||||||
const GCROOT_PATH: &str = "/nix/var/nix/gcroots/system-manager-current";
|
const GCROOT_PATH: &str = "/nix/var/nix/gcroots/system-manager-current";
|
||||||
const SYSTEMD_UNIT_DIR: &str = "/run/systemd/system";
|
|
||||||
const SYSTEM_MANAGER_STATE_DIR: &str = "/var/lib/system-manager/state";
|
const SYSTEM_MANAGER_STATE_DIR: &str = "/var/lib/system-manager/state";
|
||||||
const STATE_FILE_NAME: &str = "services.json";
|
const SERVICES_STATE_FILE_NAME: &str = "services.json";
|
||||||
|
//const ETC_STATE_FILE_NAME: &str = "etc-files.json";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(from = "String", into = "String", rename_all = "camelCase")]
|
||||||
pub struct StorePath {
|
pub struct StorePath {
|
||||||
pub store_path: String,
|
pub store_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for StorePath {
|
impl From<String> for StorePath {
|
||||||
fn from(path: String) -> Self {
|
fn from(path: String) -> Self {
|
||||||
StorePath {
|
// FIXME: handle this better
|
||||||
store_path: path.trim().into(),
|
if !path.starts_with("/nix/store/") {
|
||||||
|
panic!("Error constructing store path, not in store: {path}");
|
||||||
}
|
}
|
||||||
|
StorePath {
|
||||||
|
store_path: PathBuf::from(path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorePath> for PathBuf {
|
||||||
|
fn from(value: StorePath) -> Self {
|
||||||
|
value.store_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorePath> for String {
|
||||||
|
fn from(value: StorePath) -> Self {
|
||||||
|
format!("{}", value.store_path.display())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.store_path)
|
write!(f, "{}", self.store_path.display())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> {
|
fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> {
|
||||||
log::info!("Creating symlink: {} -> {}", from.display(), store_path);
|
create_link(Path::new(&store_path.store_path), from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_link(to: &Path, from: &Path) -> Result<()> {
|
||||||
|
log::info!("Creating symlink: {} -> {}", from.display(), to.display());
|
||||||
if from.is_symlink() {
|
if from.is_symlink() {
|
||||||
fs::remove_file(from)?;
|
fs::remove_file(from)?;
|
||||||
}
|
}
|
||||||
unix::fs::symlink(&store_path.store_path, from).map_err(anyhow::Error::from)
|
unix::fs::symlink(to, from).map_err(anyhow::Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_store_link(from: &Path) -> Result<()> {
|
fn remove_link(from: &Path) -> Result<()> {
|
||||||
log::info!("Removing symlink: {}", from.display());
|
log::info!("Removing symlink: {}", from.display());
|
||||||
if from.is_symlink() {
|
if from.is_symlink() {
|
||||||
fs::remove_file(from)?;
|
fs::remove_file(from)?;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ enum Action {
|
||||||
Activate {
|
Activate {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
store_path: StorePath,
|
store_path: StorePath,
|
||||||
|
#[arg(long, action)]
|
||||||
|
ephemeral: bool,
|
||||||
},
|
},
|
||||||
Generate {
|
Generate {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|
@ -35,7 +37,10 @@ fn main() -> ExitCode {
|
||||||
fn go(action: Action) -> Result<()> {
|
fn go(action: Action) -> Result<()> {
|
||||||
check_root()?;
|
check_root()?;
|
||||||
match action {
|
match action {
|
||||||
Action::Activate { store_path } => system_manager::activate::activate(store_path),
|
Action::Activate {
|
||||||
|
store_path,
|
||||||
|
ephemeral,
|
||||||
|
} => system_manager::activate::activate(store_path, ephemeral),
|
||||||
Action::Generate { flake_uri } => system_manager::generate::generate(&flake_uri),
|
Action::Generate { flake_uri } => system_manager::generate::generate(&flake_uri),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +53,7 @@ fn check_root() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_toplevel_error<T>(r: Result<T>) -> ExitCode {
|
fn handle_toplevel_error<T>(r: Result<T>) -> ExitCode {
|
||||||
if let Err(e) = &r {
|
if let Err(e) = r {
|
||||||
log::error!("{}", e);
|
log::error!("{}", e);
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue