Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ regex = "1.11.1"
rpassword = "7.3.1"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"
tempfile = "3"
thiserror = "2.0.0"

# Internal crates
Expand Down
1 change: 1 addition & 0 deletions crates/system-manager-engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ nix.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
thiserror.workspace = true
55 changes: 14 additions & 41 deletions crates/system-manager-engine/src/activate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod etc_files;
mod services;
pub(crate) mod etc_files;
pub(crate) mod services;
mod tmp_files;
pub(crate) mod users;

use anyhow::Result;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -35,8 +36,8 @@ pub type ActivationResult<R> = Result<R, ActivationError<R>>;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct State {
file_tree: FileTree,
services: services::Services,
pub(crate) file_tree: FileTree,
pub(crate) services: services::Services,
}

impl State {
Expand Down Expand Up @@ -81,6 +82,14 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {

match etc_files::activate(store_path, old_state.file_tree, ephemeral) {
Ok(etc_tree) => {
log::info!("Restarting sysinit-reactivation.target...");
services::restart_sysinit_reactivation_target()?;

// Restart userborn before tmpfiles so users exist when tmpfiles runs
if let Err(e) = services::restart_userborn_if_exists() {
log::error!("Error restarting userborn.service: {e}");
}

log::info!("Activating tmp files...");
let tmp_result = tmp_files::activate(&etc_tree);
if let Err(e) = &tmp_result {
Expand Down Expand Up @@ -169,42 +178,6 @@ pub fn prepopulate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
Ok(())
}

pub fn deactivate() -> Result<()> {
log::info!("Deactivating system-manager");
let state_file = &get_state_file()?;
let old_state = State::from_file(state_file)?;
log::debug!("{old_state:?}");

match etc_files::deactivate(old_state.file_tree) {
Ok(etc_tree) => {
log::info!("Deactivating systemd services...");
match services::deactivate(old_state.services) {
Ok(services) => State {
file_tree: etc_tree,
services,
},
Err(ActivationError::WithPartialResult { result, source }) => {
log::error!("Error during deactivation: {source:?}");
State {
file_tree: etc_tree,
services: result,
}
}
}
}
Err(ActivationError::WithPartialResult { result, source }) => {
log::error!("Error during deactivation: {source:?}");
State {
file_tree: result,
..old_state
}
}
}
.write_to_file(state_file)?;

Ok(())
}

fn run_preactivation_assertions(store_path: &StorePath) -> Result<process::ExitStatus> {
let status = process::Command::new(
store_path
Expand All @@ -218,7 +191,7 @@ fn run_preactivation_assertions(store_path: &StorePath) -> Result<process::ExitS
Ok(status)
}

fn get_state_file() -> Result<PathBuf> {
pub(crate) fn get_state_file() -> Result<PathBuf> {
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(STATE_FILE_NAME);
DirBuilder::new()
.recursive(true)
Expand Down
7 changes: 0 additions & 7 deletions crates/system-manager-engine/src/activate/etc_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,6 @@ impl std::fmt::Display for EtcFilesConfig {
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CreatedEtcFile {
path: PathBuf,
}

fn read_config(store_path: &StorePath) -> anyhow::Result<EtcFilesConfig> {
log::info!("Reading etc file definitions...");
let file = fs::File::open(
Expand Down
56 changes: 49 additions & 7 deletions crates/system-manager-engine/src/activate/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,6 @@ pub fn activate(
)
.map_err(|e| ActivationError::with_partial_result(services.clone(), e))?;

// 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()
.map_err(|e| ActivationError::with_partial_result(services.clone(), e))?;

wait_for_jobs(
&service_manager,
&job_monitor,
Expand Down Expand Up @@ -308,3 +301,52 @@ impl From<JobId> for String {
value.id
}
}

pub fn restart_sysinit_reactivation_target() -> anyhow::Result<()> {
let service_manager = systemd::ServiceManager::new_session()?;
let job_monitor = service_manager.monitor_jobs_init()?;
let timeout = Some(Duration::from_secs(30));

log::info!("Reloading the systemd daemon...");
service_manager.daemon_reload()?;

let jobs = for_each_unit(
|unit| service_manager.restart_unit(unit),
["sysinit-reactivation.target"],
"restarting",
);

wait_for_jobs(&service_manager, &job_monitor, jobs, &timeout)?;
Ok(())
}

/// This must be called after daemon-reload so systemd knows about the unit,
/// but before tmpfiles activation since tmpfiles may reference users that
/// userborn needs to create.
pub fn restart_userborn_if_exists() -> anyhow::Result<()> {
let service_manager = systemd::ServiceManager::new_session()?;

// Check if userborn.service exists by listing units matching the pattern
let units = service_manager.list_units_by_patterns(&[], &["userborn.service"])?;

if units.is_empty() {
log::debug!("userborn.service not found, skipping");
return Ok(());
}

log::info!("Restarting userborn.service to create users before tmpfiles...");
let job_monitor = service_manager.monitor_jobs_init()?;
let timeout = Some(Duration::from_secs(30));

// We use restart rather than start because userborn is a oneshot service
// with RemainAfterExit=true.
let jobs = for_each_unit(
|unit| service_manager.restart_unit(unit),
["userborn.service"],
"restarting",
);

wait_for_jobs(&service_manager, &job_monitor, jobs, &timeout)?;
log::info!("userborn.service completed");
Ok(())
}
57 changes: 57 additions & 0 deletions crates/system-manager-engine/src/activate/users.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use anyhow::{Context, Result};
use std::io::Write;
use std::process::{Command, Stdio};
use tempfile::NamedTempFile;

const USERBORN_PREVIOUS_CONFIG: &str = "/var/lib/userborn/previous-userborn.json";

/// Locks user accounts that were previously managed by userborn.
pub fn lock_managed_users() -> Result<()> {
if Command::new("which")
.arg("userborn")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| !s.success())
.unwrap_or(true)
{
log::debug!("userborn not found in PATH, skipping user account locking");
return Ok(());
}

log::info!("Locking previously managed user accounts...");

// Create a temporary file with an empty userborn config
let empty_config = serde_json::json!({
"users": [],
"groups": []
});

let mut temp_file = NamedTempFile::new().context("Failed to create temporary config file")?;
serde_json::to_writer(&mut temp_file, &empty_config)
.context("Failed to write empty userborn config")?;
temp_file.flush()?;

let temp_path = temp_file.path();

let output = Command::new("userborn")
.arg(temp_path)
.arg("/etc")
.env("USERBORN_MUTABLE_USERS", "true")
.env("USERBORN_PREVIOUS_CONFIG", USERBORN_PREVIOUS_CONFIG)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.context("Failed to execute userborn")?;

if !output.status.success() {
anyhow::bail!(
"userborn exited with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}

log::info!("Successfully locked managed user accounts");
Ok(())
}
Loading