Skip to content

Commit af46809

Browse files
committed
ephemeral: Use SMBIOS credentials for systemd units
Fix ephemeral mode to use SMBIOS credentials (systemd.extra-unit.*) instead of writing systemd units directly to `/run/source-image/etc/systemd/system/`. This addresses ConditionFirstBoot issues on Fedora CoreOS where directly written units trigger systemd preset cleanup. The libvirt mode already uses SMBIOS credentials successfully. This change aligns ephemeral mode with that proven approach. Fixes: #106 Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 3fd4098 commit af46809

1 file changed

Lines changed: 97 additions & 91 deletions

File tree

crates/kit/src/run_ephemeral.rs

Lines changed: 97 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,8 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> {
788788

789789
// Process host mounts and prepare virtiofsd instances for each using async manager
790790
let mut additional_mounts = Vec::new();
791+
// Collect mount unit credentials to inject via SMBIOS instead of writing to filesystem
792+
let mut mount_unit_smbios_creds = Vec::new();
791793

792794
debug!(
793795
"Checking for host mounts directory: /run/host-mounts exists = {}",
@@ -798,8 +800,7 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> {
798800
Utf8Path::new("/run/systemd-units").exists()
799801
);
800802

801-
let target_unitdir = "/run/source-image/etc/systemd/system";
802-
803+
let mut mount_unit_names = Vec::new();
803804
if Utf8Path::new("/run/host-mounts").exists() {
804805
for entry in fs::read_dir("/run/host-mounts")? {
805806
let entry = entry?;
@@ -832,73 +833,48 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> {
832833
};
833834
additional_mounts.push((virtiofsd_config, tag.clone()));
834835

835-
// Create individual .mount unit for this virtiofs mount
836+
// Generate mount unit via SMBIOS credentials instead of writing to filesystem
836837
let mount_point = format!("/run/virtiofs-mnt-{}", mount_name_str);
837-
838-
// Use systemd-escape to properly escape the mount path
839-
let escaped_path = Command::new("systemd-escape")
840-
.args(["-p", &mount_point])
841-
.output()
842-
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
843-
.unwrap_or_else(|_| {
844-
// Fallback if systemd-escape is not available
845-
mount_point
846-
.replace("/", "-")
847-
.trim_start_matches('-')
848-
.to_string()
849-
});
850-
851-
let mount_unit_name = format!("{}.mount", escaped_path);
852-
let mount_options = if is_readonly { "ro" } else { "defaults" };
853-
854-
let mount_unit_content = format!(
855-
r#"[Unit]
856-
Description=Mount virtiofs {}
857-
DefaultDependencies=no
858-
After=systemd-remount-fs.service
859-
Before=local-fs.target shutdown.target
860-
861-
[Mount]
862-
What={}
863-
Where={}
864-
Type=virtiofs
865-
Options={}
866-
867-
[Install]
868-
WantedBy=local-fs.target
869-
"#,
870-
mount_name_str, tag, mount_point, mount_options
838+
let unit_name = crate::credentials::guest_path_to_unit_name(&mount_point);
839+
let mount_unit_content =
840+
crate::credentials::generate_mount_unit(&tag, &mount_point, is_readonly);
841+
let encoded_mount = data_encoding::BASE64.encode(mount_unit_content.as_bytes());
842+
843+
// Create SMBIOS credential for the mount unit
844+
let mount_cred = format!(
845+
"io.systemd.credential.binary:systemd.extra-unit.{unit_name}={encoded_mount}"
871846
);
847+
mount_unit_smbios_creds.push(mount_cred);
872848

873-
let mount_unit_path = format!("{target_unitdir}/{mount_unit_name}");
874-
fs::write(&mount_unit_path, mount_unit_content)
875-
.with_context(|| format!("Failed to write mount unit to {}", mount_unit_path))?;
876-
877-
// Enable the mount unit by creating symlink in local-fs.target.wants/
878-
let wants_dir = format!("{target_unitdir}/local-fs.target.wants");
879-
fs::create_dir_all(&wants_dir)?;
880-
let wants_link = format!("{}/{}", wants_dir, mount_unit_name);
881-
let relative_target = format!("../{}", mount_unit_name);
882-
std::os::unix::fs::symlink(&relative_target, &wants_link)?;
883-
884-
// Create mount point directory in the image
885-
let image_mount_point = format!("/run/source-image{}", mount_point);
886-
fs::create_dir_all(&image_mount_point).ok();
849+
// Collect unit name for the local-fs.target dropin
850+
mount_unit_names.push(unit_name.clone());
887851

888852
debug!(
889-
"Generated mount unit: {} (enabled in local-fs.target)",
890-
mount_unit_name
853+
"Generated SMBIOS credential for mount unit: {} ({})",
854+
unit_name, mode
891855
);
892856
}
893857
}
894858

859+
// If we have mount units, create a single dropin for local-fs.target
860+
if !mount_unit_names.is_empty() {
861+
let wants_list = mount_unit_names.join(" ");
862+
let dropin_content = format!("[Unit]\nWants={}\n", wants_list);
863+
let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes());
864+
let dropin_cred = format!(
865+
"io.systemd.credential.binary:systemd.unit-dropin.local-fs.target~bcvk-mounts={encoded_dropin}"
866+
);
867+
mount_unit_smbios_creds.push(dropin_cred);
868+
debug!(
869+
"Created local-fs.target dropin for {} mount units",
870+
mount_unit_names.len()
871+
);
872+
}
873+
895874
// Handle --execute: pipes will be created when adding to qemu_config later
896875
// No need to create files anymore as we're using pipes
897876

898-
let default_wantsdir = format!("{target_unitdir}/default.target.wants");
899-
fs::create_dir_all(&default_wantsdir)?;
900-
901-
// Create systemd unit to stream journal to virtio-serial device
877+
// Create systemd unit to stream journal to virtio-serial device via SMBIOS credential
902878
let journal_stream_unit = r#"[Unit]
903879
Description=Stream systemd journal to host via virtio-serial
904880
DefaultDependencies=no
@@ -916,17 +892,23 @@ RestartSec=1s
916892
[Install]
917893
WantedBy=sysinit.target
918894
"#;
919-
let journal_unit_path = format!("{target_unitdir}/bcvk-journal-stream.service");
920-
tokio::fs::write(&journal_unit_path, journal_stream_unit).await?;
921-
debug!("Created journal streaming unit at {journal_unit_path}");
922-
923-
// Enable the journal streaming unit
924-
let sysinit_wantsdir = format!("{target_unitdir}/sysinit.target.wants");
925-
tokio::fs::create_dir_all(&sysinit_wantsdir).await?;
926-
let journal_wants_link = format!("{sysinit_wantsdir}/bcvk-journal-stream.service");
927-
tokio::fs::symlink("../bcvk-journal-stream.service", &journal_wants_link).await?;
928-
debug!("Enabled journal streaming unit in sysinit.target.wants");
895+
let encoded_journal = data_encoding::BASE64.encode(journal_stream_unit.as_bytes());
896+
let journal_cred = format!(
897+
"io.systemd.credential.binary:systemd.extra-unit.bcvk-journal-stream.service={encoded_journal}"
898+
);
899+
mount_unit_smbios_creds.push(journal_cred);
900+
debug!("Generated SMBIOS credential for journal streaming unit");
901+
902+
// Create dropin for sysinit.target to enable journal streaming
903+
let journal_dropin = "[Unit]\nWants=bcvk-journal-stream.service\n";
904+
let encoded_dropin = data_encoding::BASE64.encode(journal_dropin.as_bytes());
905+
let dropin_cred = format!(
906+
"io.systemd.credential.binary:systemd.unit-dropin.sysinit.target~bcvk-journal={encoded_dropin}"
907+
);
908+
mount_unit_smbios_creds.push(dropin_cred);
909+
debug!("Created sysinit.target dropin to enable journal streaming unit");
929910

911+
// Create execute units via SMBIOS credentials if needed
930912
match opts.common.execute.as_slice() {
931913
[] => {}
932914
elts => {
@@ -946,8 +928,7 @@ StandardError=inherit
946928
service_content.push_str(&format!("ExecStart={elt}\n"));
947929
}
948930

949-
let service_finish = format!(
950-
r#"[Unit]
931+
let service_finish = r#"[Unit]
951932
Description=Execute Script Service Completion
952933
After=bootc-execute.service
953934
Requires=dev-virtio\\x2dports-executestatus.device
@@ -957,24 +938,34 @@ Type=oneshot
957938
ExecStart=systemctl show bootc-execute
958939
ExecStart=systemctl poweroff
959940
StandardOutput=file:/dev/virtio-ports/executestatus
960-
"#
961-
);
941+
"#;
962942

963-
let service_path = format!("{target_unitdir}/bootc-execute.service");
964-
fs::write(service_path, service_content)?;
965-
let service_path = format!("{target_unitdir}/bootc-execute-finish.service");
966-
fs::write(service_path, service_finish)?;
943+
// Inject execute units via SMBIOS credentials
944+
let encoded_execute = data_encoding::BASE64.encode(service_content.as_bytes());
945+
let execute_cred = format!(
946+
"io.systemd.credential.binary:systemd.extra-unit.bootc-execute.service={encoded_execute}"
947+
);
948+
mount_unit_smbios_creds.push(execute_cred);
967949

968-
for svc in ["bootc-execute.service", "bootc-execute-finish.service"] {
969-
let wants_link = format!("{default_wantsdir}/{svc}");
970-
debug!("Creating execute service symlink: {}", &wants_link);
971-
std::os::unix::fs::symlink(format!("../{svc}"), wants_link)?;
972-
}
950+
let encoded_finish = data_encoding::BASE64.encode(service_finish.as_bytes());
951+
let finish_cred = format!(
952+
"io.systemd.credential.binary:systemd.extra-unit.bootc-execute-finish.service={encoded_finish}"
953+
);
954+
mount_unit_smbios_creds.push(finish_cred);
955+
956+
// Create dropin for default.target to enable execute services
957+
let execute_dropin =
958+
"[Unit]\nWants=bootc-execute.service bootc-execute-finish.service\n";
959+
let encoded_dropin = data_encoding::BASE64.encode(execute_dropin.as_bytes());
960+
let dropin_cred = format!(
961+
"io.systemd.credential.binary:systemd.unit-dropin.default.target~bcvk-execute={encoded_dropin}"
962+
);
963+
mount_unit_smbios_creds.push(dropin_cred);
964+
debug!("Generated SMBIOS credentials for execute units");
973965
}
974966
}
975967

976-
// Copy systemd units if provided (after mount units have been generated)
977-
// Also inject if we created mount units that need to be copied
968+
// Copy systemd units if provided (for --systemd-units-dir option)
978969
inject_systemd_units()?;
979970

980971
// Prepare main virtiofsd config for the source image (will be spawned by QEMU)
@@ -1059,22 +1050,30 @@ StandardOutput=file:/dev/virtio-ports/executestatus
10591050
"swap".into(),
10601051
crate::to_disk::Format::Raw,
10611052
);
1062-
let svc = format!(
1063-
r#"[Unit]
1053+
1054+
// Create swap unit via SMBIOS credential
1055+
let svc = r#"[Unit]
10641056
Description=bcvk ephemeral swap
10651057
10661058
[Swap]
10671059
What=/dev/disk/by-id/virtio-swap
10681060
Options=
1069-
"#
1070-
);
1071-
1061+
"#;
10721062
let service_name = r#"dev-disk-by\x2did-virtio\x2dswap.swap"#;
1073-
let service_path = format!("{target_unitdir}/{service_name}");
1074-
fs::write(&service_path, svc)?;
1063+
let encoded_swap = data_encoding::BASE64.encode(svc.as_bytes());
1064+
let swap_cred = format!(
1065+
"io.systemd.credential.binary:systemd.extra-unit.{service_name}={encoded_swap}"
1066+
);
1067+
mount_unit_smbios_creds.push(swap_cred);
10751068

1076-
let wants_link = format!("{default_wantsdir}/{service_name}");
1077-
std::os::unix::fs::symlink(format!("../{service_name}"), wants_link)?;
1069+
// Create dropin for default.target to enable swap
1070+
let swap_dropin = format!("[Unit]\nWants={service_name}\n");
1071+
let encoded_dropin = data_encoding::BASE64.encode(swap_dropin.as_bytes());
1072+
let dropin_cred = format!(
1073+
"io.systemd.credential.binary:systemd.unit-dropin.default.target~bcvk-swap={encoded_dropin}"
1074+
);
1075+
mount_unit_smbios_creds.push(dropin_cred);
1076+
debug!("Generated SMBIOS credential for swap unit");
10781077

10791078
tmp_swapfile = Some(tmpf);
10801079
}
@@ -1204,6 +1203,13 @@ Options=
12041203
})?;
12051204
};
12061205

1206+
// Add all SMBIOS credentials for mount units, journal, and execute services
1207+
let cred_count = mount_unit_smbios_creds.len();
1208+
for cred in mount_unit_smbios_creds {
1209+
qemu_config.add_smbios_credential(cred);
1210+
}
1211+
debug!("Added {} SMBIOS credentials to QEMU config", cred_count);
1212+
12071213
debug!("Starting QEMU with systemd debugging enabled");
12081214

12091215
// Spawn QEMU with all virtiofsd processes handled internally

0 commit comments

Comments
 (0)