Skip to content

feat(azure): Add Azure Ignition fragment generation#1264

Open
peytonr18 wants to merge 4 commits into
coreos:mainfrom
peytonr18:probertson-generate-ignition-config
Open

feat(azure): Add Azure Ignition fragment generation#1264
peytonr18 wants to merge 4 commits into
coreos:mainfrom
peytonr18:probertson-generate-ignition-config

Conversation

@peytonr18

@peytonr18 peytonr18 commented Mar 19, 2026

Copy link
Copy Markdown

Summary

This PR adds a render-ignition CLI subcommand to Afterburn that generates Ignition config fragment files from Azure IMDS and OVF metadata. These fragments are written to /etc/ignition/base.platform.d/azure/ during initrd, allowing Ignition to natively merge platform-provided user data (hostname, SSH keys, password) without requiring a custom Ignition config.

Changes

New render-ignition subcommand

  • Added src/cli/render_ignition.rs with clap-derived argument parsing.
  • Accepts --provider/--cmdline, --render-ignition-dir, and per-feature flags (--hostname, --platform-user, --platform-extensions).
  • Currently gated to Azure only; other providers bail with an informative error.

Azure fragment generation (src/providers/microsoft/azure/config.rs)

  • generate_hostname_fragment(): fetches hostname via MetadataProvider::hostname(), writes hostname.ign with a storage.files entry for /etc/hostname using a data:,<hostname> URI.
  • generate_user_fragment(): fetches admin username and SSH keys from IMDS via MetadataProvider, optionally reads adminPassword from OVF (ovf-env.xml on CD-ROM), hashes it with SHA-512, and writes user.ign with a passwd.users entry.

OVF / IMDS source rules

  • Username and SSH keys are sourced from IMDS.
  • adminPassword is the only field read from OVF. If present and non-empty, it is SHA-512 hashed and included as passwordHash in the Ignition user fragment. If absent or empty, it is omitted.

Systemd / Dracut integration

  • New afterburn-ignition-fragment.service unit (dracut module) runs during initrd.
  • Conditioned on ignition.platform.id=azure.
  • Runs after network-online.target, before ignition-kargs.service.
  • Creates the output directory and invokes afterburn render-ignition --cmdline --render-ignition-dir /etc/ignition/base.platform.d/azure/ --platform-extensions.

CLI usage

afterburn render-ignition --cmdline --render-ignition-dir <path> --hostname --platform-user
afterburn render-ignition --cmdline --render-ignition-dir <path> --platform-extensions
Flag Effect
--hostname Generate hostname.ign (storage.files entry for /etc/hostname)
--platform-user Generate user.ign (passwd.users entry with username + SSH keys + optional password hash)
--platform-extensions Implies --hostname --platform-user

Each feature writes its own standalone fragment file. Missing data (e.g., no hostname available) logs a warning and skips, matching existing Afterburn behavior.

Existing functionality is untouched

The multi subcommand's --hostname=<path> direct-file-write path is unchanged. The afterburn-hostname.service (including its Azure/AzureStack conditions) is not modified.

The CLI design is up for discussion — would love feedback on the flag names, subcommand structure, and overall approach.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for generating Azure-specific Ignition fragments. The implementation is well-structured, adding a new CLI command, a systemd service for execution, and the core logic for fetching data from IMDS and OVF. The refactoring to share SSH key parsing logic is a good improvement.

I've found one critical issue regarding the systemd service activation which would cause the feature to not work as intended, and one medium-severity suggestion to improve code efficiency and readability. Please see the detailed comments.

Comment thread dracut/30afterburn/module-setup.sh
Comment thread src/providers/microsoft/azure/mod.rs Outdated
@peytonr18 peytonr18 force-pushed the probertson-generate-ignition-config branch from 6af9c2e to 50a81df Compare March 19, 2026 19:04
@prestist prestist self-requested a review March 20, 2026 19:16
@prestist

Copy link
Copy Markdown
Contributor

@peytonr18, I have started looking into the changes, I will add my feedback in the next day or so.

@prestist prestist left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall this looks good! just a few concerns around the CLI design and how the IMDS client is built.

Comment thread src/cli/render_ignition.rs
Comment thread src/providers/microsoft/azure/config.rs Outdated
Comment thread src/providers/microsoft/azure/config.rs Outdated
Comment thread src/providers/microsoft/azure/config.rs Outdated
@peytonr18

Copy link
Copy Markdown
Author

@prestist I should have addressed all of your comments - thank you so much for these! I agreed with all of your points and adjusted the logic accordingly.

@prestist

Copy link
Copy Markdown
Contributor

@peytonr18 I will take another pass today! thank you for the quick changes!

@prestist

Copy link
Copy Markdown
Contributor

@peytonr18 we do have some failing lints in the CI and you would need to add an entry to the release notes file in the "upcoming" release notes section. https://github.com/coreos/afterburn/blob/main/docs/release-notes.md

Comment thread src/providers/microsoft/azure/config.rs Outdated
Comment thread dracut/30afterburn/afterburn-ignition-fragment.service Outdated
Comment thread src/providers/microsoft/azure/config.rs Outdated
Comment thread src/providers/microsoft/azure/config.rs Outdated
@peytonr18 peytonr18 force-pushed the probertson-generate-ignition-config branch from 9d71b9b to aef7dca Compare May 11, 2026 18:24

@prestist prestist left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Quick look over, a few small questions/nits

Comment thread src/providers/microsoft/azure/config.rs Outdated
Comment thread src/providers/microsoft/azure/config.rs Outdated
Comment thread Cargo.lock Outdated
Comment thread Cargo.toml Outdated
@peytonr18 peytonr18 force-pushed the probertson-generate-ignition-config branch from 3786e89 to c555f66 Compare May 18, 2026 21:41

@prestist prestist left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From my perspective this looks sane, though I think we still have an ordering issue here, wdyt?

Comment thread Cargo.lock Outdated

@prestist prestist left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for all your work on this! from my perspective it lgtm, I would like @travier 's feedback as well.

@prestist prestist dismissed their stale review June 8, 2026 21:36

No longer relevant

Comment thread dracut/30afterburn/afterburn-ignition-fragment.service Outdated
@peytonr18 peytonr18 force-pushed the probertson-generate-ignition-config branch from d2bb19f to 9d6be5c Compare June 17, 2026 16:12
Comment thread dracut/30afterburn/module-setup.sh Outdated
Comment thread dracut/30afterburn/afterburn-ignition-fragment.service

@prestist prestist left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks wonderful thank you @peytonr18. I dont see anything that needs to change outside of a small nit about the commit structure. Since we merge directly into the main branch lets squash and rename some of the commits to follow the repo convention of : (lowercase subsystem, imperative tense). Something
like:

  • azure: Add render-ignition subcommand for Ignition fragment generation
  • dracut: Add afterburn-ignition-fragment.service ordered between fetch and disks

Or if you'd prefer to keep more granularity, each commit should at minimum get a subsystem prefix (e.g. azure:, dracut:, cli:).

@peytonr18 peytonr18 force-pushed the probertson-generate-ignition-config branch from e40335b to 68762e9 Compare June 30, 2026 00:10
@peytonr18

Copy link
Copy Markdown
Author

@prestist Should have changed the git commit history based on that suggestion, thanks!

Comment thread src/providers/microsoft/azure/config.rs Outdated
Comment thread src/cli/render_ignition.rs Outdated
Comment thread src/cli/render_ignition.rs Outdated
.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

peytonr18 added 2 commits July 7, 2026 10:46
Move the Ignition fragment types and rendering from the Azure provider into render_ignition and source platform data through the MetadataProvider trait (admin_username, admin_password_hash), and switch the feature flags to opt-out (--disable-hostname-fragment / --disable-user-fragment).
Move the Ignition config structs and serialization into src/ignition.rs
with a write_to() method on IgnitionConfig, leaving the
provider-to-fragment orchestration in the render-ignition CLI.

Signed-off-by: peytonr18 <peytonr18@sbcglobal.net>
@prestist

Copy link
Copy Markdown
Contributor

@peytonr18, this still has my stamp :) from my perspective we should be able to merge.

@Rolv-Apneseth Rolv-Apneseth left a comment

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.

Thanks for working on this @peytonr18 . Mostly LGTM but I have a couple questions and suggestions. Realistically though most are nits and not essential.

let platform_user = !self.disable_user_fragment;

if !hostname && !platform_user {
slog_scope::warn!("render-ignition: all fragments disabled, nothing to do");

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.

Suggested change
slog_scope::warn!("render-ignition: all fragments disabled, nothing to do");
warn!("render-ignition: all fragments disabled, nothing to do");

}

fn generate_hostname_fragment(provider: &dyn MetadataProvider, output_dir: &str) -> Result<()> {
let hostname = match provider.hostname()? {

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.

Suggested change
let hostname = match provider.hostname()? {
let Some(hostname) = provider.hostname()? else {
warn!("hostname requested, but not available from this provider");
return Ok(());
};

.admin_username()
.context("failed to query admin username from provider")?;
let username = match username {
Some(u) => u,

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.

Suggested change
Some(u) => u,
let Some(username) = username else {
warn!("platform-user requested, but admin username not available from this provider");
return Ok(());
};

let password_hash = provider
.admin_password_hash()
.context("failed to query admin password hash from provider")?;

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.

Might be worth checking if any credentials are actually available before writing, WDYT?

Suggested change
if ssh_keys.is_empty() && password_hash.is_none() {
warn!("admin username present but no SSH keys or password; skipping user fragment");
return Ok(());
}


/// Minimal generic provider used to exercise the fragment generators
/// without any platform-specific plumbing.
struct FakeProvider {

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.

Some of the boilerplate below could potentially be cut down with a #[derive(Default)], e.g.

let provider = FakeProvider {
    hostname: Some("myhost".into()),
    ..Default::default()
};
// Or, if no differing fields are required
let provider = FakeProvider::default();

.send::<String>()
.context("failed to query IMDS for adminUsername")?;

match username {

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.

Suggested change
match username {
match username.map(|u| u.trim().to_owned()) {
Some(u) if !u.is_empty() => Ok(Some(u)),
_ => Ok(None),
}

Comment thread src/ignition.rs
@@ -0,0 +1,334 @@
// Copyright 2017 CoreOS, Inc.

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.

Suggested change
// Copyright 2017 CoreOS, Inc.
// Copyright 2026 CoreOS, Inc.

Comment thread src/ignition.rs
storage: Some(Storage {
files: vec![StorageFile {
path: "/etc/hostname".into(),
mode: 420,

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 this is clearer in octal:

Suggested change
mode: 420,
mode: 0o644,

Comment thread src/ignition.rs
}
let json =
serde_json::to_string_pretty(self).context("failed to serialize ignition config")?;
fs::write(path, json.as_bytes())

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.

Would it be worth writing to a temp file and renaming after the permissions are set to keep this atomic in case of a failure / crash?

Comment thread src/ignition.rs
storage: Some(Storage {
files: vec![StorageFile {
path: "/etc/hostname".into(),
mode: 420,

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.

Suggested change
mode: 420,
mode: 0o644,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants