Skip to content

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

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

feat(azure): Add Azure Ignition fragment generation#1264
peytonr18 wants to merge 15 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.

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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The afterburn-ignition-fragment.service is being linked into ignition-complete.target.requires. This will cause it to run after all main Ignition stages are complete, which is too late for the generated fragment to be used by Ignition.

According to its own unit file, this service must run before ignition-kargs.service. To ensure correct ordering and activation, it should be made a requirement of ignition-kargs.service instead of ignition-complete.target.

Suggested change
ln -s "../afterburn-ignition-fragment.service" "$initdir/$systemdsystemunitdir/ignition-complete.target.requires/afterburn-ignition-fragment.service"
mkdir -p "$initdir/$systemdsystemunitdir/ignition-kargs.service.requires"
ln -s "../afterburn-ignition-fragment.service" "$initdir/$systemdsystemunitdir/ignition-kargs.service.requires/afterburn-ignition-fragment.service"

@prestist prestist Mar 23, 2026

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.

Hmm, the ordering concern is valid but ignition-kargs.service.requires might also be too late. Looking at the Ignition boot flow (coreos/ignition#2195), both ignition-fetch-offline.service and ignition-fetch.service read from base.platform.d/{platform}/ before ignition-kargs.service runs.

Maybe we should use Before=ignition-fetch-offline.service or similar to ensure the fragments are written before Ignition discovers them?

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.

@peytonr18 how are we going to handle the ordering of this?

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.

You're right, the ordering we had was likely too late.

I don't think we can go all the way to Before=ignition-fetch-offline.service because the service needs network-online.target for the IMDS call, and the network isn't up yet at that point.

For Azure that's not really a big deal: the provider always requires the network, so fetch-offline exits early with neednet and ignition-fetch.service is the stage that matters.

With that being said, I think switching the ordering to Before=ignition-fetch.service is the best approach? Let me know your thoughts!

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.

That's a great point, yeah I think that should work for this use case.

Comment thread src/providers/microsoft/azure/mod.rs Outdated
Comment on lines +119 to +124
let key_data = item
.key_data
.replace("\r\n", "")
.replace('\n', "")
.trim()
.to_string();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The current string manipulation for cleaning key_data involves a chain of operations that results in an unnecessary String allocation because .to_string() is called on a temporary &str. This can be made more efficient and readable by separating the steps.

Suggested change
let key_data = item
.key_data
.replace("\r\n", "")
.replace('\n', "")
.trim()
.to_string();
let key_data_cleaned = item.key_data.replace("\r\n", "").replace('\n', "");
let key_data = key_data_cleaned.trim();

@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
peytonr18 added 7 commits May 4, 2026 15:41
…tandalone IMDS client

Route admin username and SSH key fetches through the Azure provider's
existing client, remove duplicate IMDS_ENDPOINT and helpers from
config.rs, and restrict the subcommand to the azure provider only.
Overlay a tmpfs on /usr/lib/ignition/base.platform.d in the dracut
service so afterburn can write .ign fragments on the read-only OSTree
filesystem. Move the test module after all impl blocks in azure/mod.rs
to fix the clippy items_after_test_module lint, and add a release notes
entry for the render-ignition feature.
Use Ignition’s writable local config directory for generated Azure fragments, and translate OVF adminPassword into an Ignition passwordHash instead of rejecting it.
@peytonr18 peytonr18 force-pushed the probertson-generate-ignition-config branch from b2da779 to af9bd12 Compare May 4, 2026 22:42
Comment thread src/providers/microsoft/azure/config.rs Outdated
Comment thread src/providers/microsoft/azure/config.rs Outdated
Replace the sha-crypt crate with an inline SHA-512 crypt implementation
backed by the sha2 crate (packaged in Fedora as rust-sha2). This removes
a dependency not available in upstream Fedora packaging.

Replace naive string-based XML namespace stripping with structural
parsing via xml-rs EventReader and EventWriter.
@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
serde-xml-rs = ">= 0.4, < 0.9"
serde_json = "1.0"
serde_yaml = ">= 0.8, < 0.10"
sha-crypt = "0.5"

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.

We likely can not do that as this will not be FIPS compliant. Is there something in the openssl crate that we can use instead?

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.

I swapped the sha-crypt crate for a small FFI call to the system's crypt(3) (libxcrypt) — same function passwd and PAM use to write shadow, so there isn't a new dependency to worry about.

By my searching, openssl doesn't have a crypt equivalent, so going that route would mean re-implementing the password hashing scheme by hand, which I'd like to avoid.

Let me know your thoughts on this, I was trying to find the middle ground in best practices here but I'm open to whatever implementation feels best for you all.

@peytonr18 peytonr18 force-pushed the probertson-generate-ignition-config branch from 3786e89 to c555f66 Compare May 18, 2026 21:41
Building on Ubuntu baked NEEDED libcrypt.so.1 into the binary, which the
Fedora dynamic loader could not satisfy (it ships libcrypt.so.2). Drop the
#[link] attribute and look up crypt_r at runtime, trying libcrypt.so.2 then
libcrypt.so.1, so the same binary works on both distros.

@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

# before ignition-fetch.service, which produces /run/ignition.json from
# /etc/ignition/base.platform.d/{platform}/.
Wants=network-online.target
After=network-online.target

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

to clarify, the current ignition-fetch.service is After=network.target. So this does somewhat indirectly change behavior if we order before fetch.service (it has been seen that fetch will fail while DHCP is ongoing). There may be some implications to waiting for online target in multi-nic scenarios where maybe a subset don't dhcp [fast enough].

@prestist what ordering issue specifically are you referring to?

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.

Hmm, so the concern I had was about module-setup.sh — the fragment service gets symlinked into ignition-complete.target.requires. That would mean the unit only gets pulled into the transaction when ignition-complete.target is being assembled, which is after Ignition has already read from base.platform.d/ and produced /run/ignition.json. The fragments wouldn't be consumed at that point.

Its entirely possible I have a misunderstanding here, but I think it might make sense to ensure this service runs before ignition-fetch-offline.service or ignition-fetch.service, since those are what read base.platform.d/{platform}/.

On the network-online.target vs network.target point — good point. If we add Before=ignition-fetch.service here while keeping After=network-online.target, that would transitively force fetch to also wait for network-online.target, which could be a problem in multi-NIC setups where not all interfaces come up quickly. IMDS is a link-local endpoint right? So it should work once the primary interface has an address — would network.target be sufficient here instead?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think they read base.platform.d entries (it's certainly logged), but I'm unsure that it does anything with it. Can you look into if these parameters are used at all @peytonr18?

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.

Looking at ignition the fragments are actively merged, not just logged.

FetchBaseConfig() reads .ign files from base.platform.d/{platform}/ across all system config dirs (/run/ignition, /etc/ignition, /usr/lib/ignition), parses them as Ignition configs, and merges them via latest.Merge().

engine.go:Run() calls FetchBaseConfig at the start of every stage, then merges the result into the final config at line 127: fullConfig := latest.Merge(baseConfig, latest.Merge(systemBaseConfig, cfg))

Since FetchBaseConfig runs at the start of every stage including fetch-offline, the afterburn fragment service would need to complete before ignition-fetch-offline.service to ensure the fragments are present when Ignition first reads them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In practice though, that fullConfig has little or no impact to behavior.

fetch-offline:
https://github.com/coreos/ignition/blob/e3c445c673d2b5c9af4aa61836bd5911a9bc4c30/internal/exec/stages/fetch_offline/fetch-offline.go#L71

used to determine if it needs networking at this stage (which results in offline bailing).

fetch:
https://github.com/coreos/ignition/blob/e3c445c673d2b5c9af4aa61836bd5911a9bc4c30/internal/exec/stages/fetch/fetch.go#L66

no-op.

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.

Very good point, you're right that the fetch stages don't act on the merged config in practice.

I think the stage that actually matters is files, where createPasswd() and createFilesystemsEntries() run. So the fragments just need to exist before ignition-files.service.

On the network-online.target vs network.target question — since IMDS is link-local (169.254.169.254) and afterburn already has retry logic, would network.target be sufficient here? That'd be consistent with how ignition-fetch.service handles it and would avoid the multi-NIC stalling issue.

I think I confused this thread a bit, I want to make sure so going back to the ordering concern I raised earlier (#1264 (comment)) — I think the module-setup.sh symlink is still relevant here. The fragment service is currently linked into ignition-complete.target.requires, but ignition-files.service has Before=ignition-complete.target. Wouldn't that mean the files stage finishes before systemd even pulls in the fragment service? If so, would switching the symlink to ignition-files.service.requires make more sense?

@cjp256

@cjp256 cjp256 Jun 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stage order: fetch-offline [-> fetch] [-> kargs] -> disks -> mount -> files.

https://github.com/coreos/ignition/blob/e3c445c673d2b5c9af4aa61836bd5911a9bc4c30/dracut/30ignition/ignition-files.service#L11

Given this, we will want to order before disks. ignition-complete.target requires would be fine, but it would be redundant once we order before ignition-disks.

I would take the above bot suggestion and just switch from kargs -> disks, because kargs is optional and we don't actually care to order before that either (unless someone knows a good reason to). We do need to order before disks (and implicitly, mount and files).

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.

That makes sense; Before=ignition-disks.service covers disks, mount, and files implicitly through the chain. And agreed, no reason to order before kargs since it's optional and the fragments don't contain kargs config anyway.

Oook, So to summarize, the service unit would need Before=ignition-disks.service instead of Before=ignition-kargs.service, and it might make sense to switch After=network-online.target to After=network.target to stay consistent with how ignition-fetch.service handles it and avoid the multi-NIC stalling concern.

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.

That's what my takeaways were as well - will get the files updated asap!

@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
ln -s "../afterburn-network-kargs.service" "$initdir/$systemdsystemunitdir/ignition-complete.target.requires/afterburn-network-kargs.service"

mkdir -p "$initdir/$systemdsystemunitdir/ignition-files.service.requires"
ln -s "../afterburn-ignition-fragment.service" "$initdir/$systemdsystemunitdir/ignition-files.service.requires/afterburn-ignition-fragment.service"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

keep this tied to ignition-complete.target. the before/after config will order it correctly. this is just to enable the unit as being enabled if ignition-complete.target is enabled.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

(just verify that this unit runs parallel to ignition-fetch now)

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.

Addressed in latest commit!

# Order before ignition-disks.service so fragments exist before the
# disks/mount/files stages run (files is where they're consumed).
# ignition-kargs is optional and does not consume these fragments.
After=network.target

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

upon further consideration, if we are in parallel with other ignition services, is that are in the middle of writing the config fragment when an ignition service is loading the configs and breaks the parser. unlikely race condition but I suspect possible nonetheless. we would want to be sure the config write is atomic (file written before moved into target location), or maybe order in between fetch.service and disks.service.

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.

Addressed in latest commit!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sorry, let's include after=ignition-kargs.service too.

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.

6 participants