Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ base64 = "0.22"
cfg-if = "1.0"
clap = { version = "4", "default-features" = false, "features" = ["std", "cargo", "derive", "error-context", "help", "suggestions", "usage", "wrap_help"] }
ipnetwork = ">= 0.17, < 0.22"
libc = "0.2"
libflate = "2.1"
libsystemd = ">= 0.2.1, < 0.8.0"
mailparse = ">= 0.13, < 0.17"
maplit = "1.0"
nix = { version = ">= 0.19, < 0.31", "default-features" = false, "features" = [ "mount", "user"] }
openssh-keys = ">= 0.5, < 0.7"
openssl = ">= 0.10.46, < 0.11"
percent-encoding = "2.3.2"
pnet_base = ">= 0.26, < 0.36"
pnet_datalink = ">= 0.26, < 0.36"
reqwest = { version = ">= 0.10, < 0.13", features = [ "blocking" ] }
Expand All @@ -54,6 +56,7 @@ serde-xml-rs = ">= 0.4, < 0.9"
serde_json = "1.0"
serde_yaml = ">= 0.8, < 0.10"
slog = { version = "2.7", features = ["max_level_trace", "release_max_level_info"] }
xml-rs = "0.8"
slog-async = ">= 2.5, < 3"
slog-scope = "4.3"
slog-term = ">= 2.6, < 3"
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ nav_order: 8
Major changes:

- KubeVirt: Add support for static and dynamic IP configuration from cloud-init
- Azure: Add `render-ignition` subcommand to generate Ignition config fragments from IMDS metadata
- Hetzner: Add support for network configuration

Minor changes:
Expand Down
26 changes: 26 additions & 0 deletions dracut/30afterburn/afterburn-ignition-fragment.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[Unit]
Description=Afterburn Ignition Config Fragment Generator
Documentation=https://coreos.github.io/afterburn/

# Only run on platforms that support ignition fragment generation.
ConditionKernelCommandLine=|ignition.platform.id=azure

# Run after networking is available (needed for the Azure IMDS call). Use
# network.target (not network-online.target) to match ignition-fetch.service
# and avoid stalling on multi-NIC DHCP; IMDS is link-local and afterburn
# has its own retry logic.
#
# Run between fetch and disks so writes don't race the fetch stage's parse
# of base.platform.d (Ignition re-reads it every stage; applied at files).
After=network.target
Comment thread
peytonr18 marked this conversation as resolved.
After=ignition-fetch.service
Before=ignition-disks.service

OnFailure=emergency.target
OnFailureJobMode=isolate

[Service]
ExecStartPre=/usr/bin/mkdir -p /etc/ignition/base.platform.d/azure
ExecStart=/usr/bin/afterburn render-ignition --cmdline --render-ignition-dir /etc/ignition/base.platform.d/azure/ --platform-extensions
Type=oneshot
RemainAfterExit=yes
4 changes: 4 additions & 0 deletions dracut/30afterburn/module-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ install() {
inst_simple "$moddir/afterburn-network-kargs.service" \
"$systemdutildir/system/afterburn-network-kargs.service"

inst_simple "$moddir/afterburn-ignition-fragment.service" \
"$systemdutildir/system/afterburn-ignition-fragment.service"

# These services are only run once on first-boot, so they piggyback
# on Ignition completion target.
mkdir -p "$initdir/$systemdsystemunitdir/ignition-complete.target.requires"
ln -s "../afterburn-hostname.service" "$initdir/$systemdsystemunitdir/ignition-complete.target.requires/afterburn-hostname.service"
ln -s "../afterburn-network-kargs.service" "$initdir/$systemdsystemunitdir/ignition-complete.target.requires/afterburn-network-kargs.service"
ln -s "../afterburn-ignition-fragment.service" "$initdir/$systemdsystemunitdir/ignition-complete.target.requires/afterburn-ignition-fragment.service"
Comment thread
peytonr18 marked this conversation as resolved.
}
63 changes: 63 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use slog_scope::trace;

mod exp;
mod multi;
mod render_ignition;

/// Path to kernel command-line (requires procfs mount).
const CMDLINE_PATH: &str = "/proc/cmdline";
Expand All @@ -19,6 +20,7 @@ pub(crate) enum CliConfig {
Multi(multi::CliMulti),
#[clap(subcommand)]
Exp(exp::CliExp),
RenderIgnition(render_ignition::CliRenderIgnition),
}

impl CliConfig {
Expand All @@ -27,6 +29,7 @@ impl CliConfig {
match self {
CliConfig::Multi(cmd) => cmd.run(),
CliConfig::Exp(cmd) => cmd.run(),
CliConfig::RenderIgnition(cmd) => cmd.run(),
}
}
}
Expand Down Expand Up @@ -231,4 +234,64 @@ mod tests {

parse_args(t3).unwrap();
}

#[test]
fn test_render_ignition_cmd() {
let args: Vec<_> = [
"afterburn",
"render-ignition",
"--provider",
"azure",
"--render-ignition-dir",
"/tmp/fragments",
"--hostname",
"--platform-user",
]
.iter()
.map(ToString::to_string)
.collect();

let cmd = parse_args(args).unwrap();
match cmd {
CliConfig::RenderIgnition(_) => {}
x => panic!("unexpected cmd: {x:?}"),
};
}

#[test]
fn test_render_ignition_platform_extensions() {
let args: Vec<_> = [
"afterburn",
"render-ignition",
"--cmdline",
"--render-ignition-dir",
"/tmp/fragments",
"--platform-extensions",
]
.iter()
.map(ToString::to_string)
.collect();

let cmd = parse_args(args).unwrap();
match cmd {
CliConfig::RenderIgnition(_) => {}
x => panic!("unexpected cmd: {x:?}"),
};
}

#[test]
fn test_render_ignition_requires_render_ignition_dir() {
let args: Vec<_> = [
"afterburn",
"render-ignition",
"--provider",
"azure",
"--hostname",
]
.iter()
.map(ToString::to_string)
.collect();

parse_args(args).unwrap_err();
}
}
78 changes: 78 additions & 0 deletions src/cli/render_ignition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! `render-ignition` CLI sub-command.
//!
//! Generates per-feature Ignition config fragment files in an output directory.
//! Each enabled feature writes its own `.ign` file; Ignition merges them
//! natively from `base.platform.d/<platform>/` under a system config directory
//! such as `/etc/ignition`.

use anyhow::{bail, Context, Result};
use clap::{ArgGroup, Parser};

/// Render Ignition config fragments from cloud provider metadata
#[derive(Debug, Parser)]
#[command(group(ArgGroup::new("provider-group").args(["cmdline", "provider"]).required(true)))]
pub struct CliRenderIgnition {
/// The name of the cloud provider
#[arg(long, value_name = "name")]
provider: Option<String>,
Comment thread
peytonr18 marked this conversation as resolved.
/// Read the cloud provider from the kernel cmdline
#[arg(long)]
cmdline: bool,
/// Directory to write Ignition config fragment files into
#[arg(long = "render-ignition-dir", value_name = "path")]
render_ignition_dir: String,
/// Include hostname in a fragment file
#[arg(long)]
hostname: bool,
/// Include platform user and SSH keys in a fragment file
#[arg(long)]
platform_user: bool,
/// Enable all platform extensions (implies --hostname --platform-user)
Comment thread
Rolv-Apneseth marked this conversation as resolved.
Outdated
#[arg(long)]
platform_extensions: bool,
}

impl CliRenderIgnition {
const SUPPORTED_PROVIDERS: &[&str] = &["azure"];

pub(crate) fn run(self) -> Result<()> {
let provider_id = super::get_provider(self.provider.as_deref())?;

if !Self::SUPPORTED_PROVIDERS.contains(&provider_id.as_str()) {
bail!(
"render-ignition is only supported for providers {:?}, got '{}'",
Self::SUPPORTED_PROVIDERS,
provider_id,
);
}

let hostname = self.hostname || self.platform_extensions;
let platform_user = self.platform_user || self.platform_extensions;

if !hostname && !platform_user {
slog_scope::warn!("render-ignition: no features specified");
return Ok(());
}

let metadata = crate::metadata::fetch_metadata(&provider_id)
.context("fetching metadata from provider")?;

if hostname {
crate::providers::microsoft::azure::config::generate_hostname_fragment(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is azure-specific call in cli code. this should be generic to provider

@peytonr18 peytonr18 Jul 7, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick design question: after our offline discussion, ignition.rs holds the Ignition config structs + serialization (with the write_to() method on IgnitionConfig), and the render-ignition CLI keeps small generate_hostname_fragment / generate_user_fragment functions that pull data off the provider and write the fragments.

An alternative would be to move that provider→fragment orchestration onto the MetadataProvider trait as default methods, mirroring the existing write_hostname / write_network_units helpers — e.g.:

// src/providers/mod.rs
fn write_hostname_ignition_fragment(&self, output_dir: &str) -> Result<()> {
    let Some(hostname) = self.hostname()? else { return Ok(()); };
    let path = Path::new(output_dir).join("hostname.ign");
    IgnitionConfig::hostname_fragment(&hostname).write_to(&path)
}

fn write_user_ignition_fragment(&self, output_dir: &str) -> Result<()> {
    // admin_username / ssh_keys / admin_password_hash -> IgnitionConfig::user_fragment(..).write_to(..)
}

Do you think the trait-default-method approach would be cleaner/preferred here, or is the current split fine? I'm happy to switch the approach if the trait method seems better - it's a small change either way.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think defaults defined in the trait would be preferable but I would like to hear @prestist's thoughts on the subject

metadata.as_ref(),
&self.render_ignition_dir,
)
.context("generating hostname ignition fragment")?;
}

if platform_user {
crate::providers::microsoft::azure::config::generate_user_fragment(
metadata.as_ref(),
&self.render_ignition_dir,
)
.context("generating platform-user ignition fragment")?;
}

Ok(())
}
}
Loading