From b01a2da2a50984d6da8f94dcc25fa33b733224fe Mon Sep 17 00:00:00 2001 From: Peyton Date: Wed, 19 Nov 2025 08:10:15 -0800 Subject: [PATCH 1/2] Ignition-fetch.service demo --- .../30ignition/README-azure-provisioning.md | 156 +++++++++++++++++ .../ignition-fetch-userdata-helper.sh | 165 ++++++++++++++++++ dracut/30ignition/ignition-fetch.service | 2 + dracut/30ignition/module-setup.sh | 4 + 4 files changed, 327 insertions(+) create mode 100644 dracut/30ignition/README-azure-provisioning.md create mode 100644 dracut/30ignition/ignition-fetch-userdata-helper.sh diff --git a/dracut/30ignition/README-azure-provisioning.md b/dracut/30ignition/README-azure-provisioning.md new file mode 100644 index 000000000..e1ca7921e --- /dev/null +++ b/dracut/30ignition/README-azure-provisioning.md @@ -0,0 +1,156 @@ + `# Azure Provisioning Integration - Proof of Concept + +## Overview + +This proof-of-concept demonstrates how to integrate Azure's provisioning metadata directly into Ignition's fetch stage. Instead of relying on external agents like WAAgent, this implementation reads user provisioning data from Azure's virtual CD-ROM during early boot and generates an Ignition configuration on-the-fly. + +## How It Works + +### Boot Flow + +1. **Early Boot** - System boots into initrd/initramfs environment +2. **Media Mount** - Azure's virtual CD-ROM (`/dev/sr0`) is auto-mounted by the system +3. **Fetch Stage** - `ignition-fetch.service` runs +4. **Helper Script** - `ignition-fetch-userdata-helper` (ExecStartPre) executes: + - Locates the mounted CD-ROM using `findmnt` + - Reads and parses `ovf-env.xml` + - Extracts user provisioning data (username, password, SSH keys) + - Generates an Ignition 3.3.0 configuration + - Writes it to `/run/ignition.json` +5. **Skip Normal Fetch** - The condition `ConditionPathExists=!/run/ignition.json` in the service file prevents the normal Ignition fetch from running since our helper already created the config +6. **Subsequent Stages** - Later stages (disks, mount, files) process the generated config to create users and configure the system + +### Stage Order + +``` +fetch-offline -> fetch (with our helper) -> kargs -> disks -> mount -> files +``` + +## Files Modified/Created + +### New Files + +- **`ignition-fetch-userdata-helper.sh`** - Shell script that: + - Finds where `/dev/sr0` is mounted + - Parses Azure's `ovf-env.xml` + - Generates Ignition JSON configuration + - Writes to `/run/ignition.json` + +### Modified Files + +- **`ignition-fetch.service`** - Added `ExecStartPre` to run helper script before normal fetch +- **`module-setup.sh`** - Added helper script installation and `findmnt` utility + +## Azure Provisioning Media Format + +On Azure, the provisioning media is: +- **Device**: Virtual CD-ROM attached as `/dev/sr0` (also visible as `ata-Virtual_CD`) +- **Filesystem**: UDF format +- **Content**: Single XML file called `ovf-env.xml` + +### Expected ovf-env.xml Structure + +```xml + + + + 1.0 + + LinuxProvisioningConfiguration + myhost + azureuser + $6$rounds=4096$abcdefgh$hashedpassword... + false + + + + ... + /home/azureuser/.ssh/authorized_keys + ssh-rsa AAAAB3NzaC1yc2EAAAA... user@host + + + + + + +``` + +### Extracted Fields + +The helper script extracts only what it uses: +- **UserName** - Linux username to create +- **UserPassword** - Password hash (Azure typically provides pre-hashed) +- **SSH Keys** - Public SSH keys from `` elements + +## Generated Ignition Configuration + +The helper generates an Ignition 3.3.0 config that: + +1. **Creates a user** with: + - Username from `` + - Password hash from `` + - SSH authorized keys from `` + - Home directory at `/home/` + - Shell set to `/bin/bash` + - Membership in `wheel` group + +2. **Configures sudo** via `/etc/sudoers.d/99_wheel_nopasswd`: + - Allows wheel group passwordless sudo + +3. **Configures SSH** via `/etc/ssh/sshd_config.d/10-custom.conf`: + - Enables password authentication + - Disables root login + - Sets custom SSHD settings + +### Sample Output + +```json +{ + "ignition": { + "version": "3.3.0" + }, + "passwd": { + "users": [ + { + "name": "azureuser", + "groups": ["wheel"], + "homeDir": "/home/azureuser", + "shell": "/bin/bash", + "passwordHash": "$6$rounds=4096$...", + "sshAuthorizedKeys": ["ssh-rsa AAAAB3..."] + } + ] + }, + "storage": { + "files": [ + { + "path": "/etc/sudoers.d/99_wheel_nopasswd", + "contents": { + "source": "data:,%25wheel%20ALL%3D(ALL)%20NOPASSWD%3AALL%0A" + }, + "mode": 288 + }, + { + "path": "/etc/ssh/sshd_config.d/10-custom.conf", + "contents": { + "source": "data:,..." + }, + "mode": 420 + } + ] + } +} +``` + + +## References + +- [Ignition Documentation](https://coreos.github.io/ignition/) +- [Ignition Configuration v3.3.0 Spec](https://coreos.github.io/ignition/configuration-v3_3/) +- [Azure Linux Provisioning](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/) +- [OVF Environment Specification](http://schemas.dmtf.org/ovf/) + + diff --git a/dracut/30ignition/ignition-fetch-userdata-helper.sh b/dracut/30ignition/ignition-fetch-userdata-helper.sh new file mode 100644 index 000000000..7f0877bc0 --- /dev/null +++ b/dracut/30ignition/ignition-fetch-userdata-helper.sh @@ -0,0 +1,165 @@ +#!/bin/bash +# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +# ex: ts=8 sw=4 sts=4 et filetype=sh +# +# ignition-fetch-userdata-helper.sh +# Reads Azure provisioning media (ovf-env.xml) and generates Ignition config + +set -euo pipefail + +CDROM_DEV="/dev/sr0" +OVF_FILE="ovf-env.xml" +OUTPUT_CONFIG="/run/ignition.json" + +log() { + echo "ignition-fetch-userdata-helper: $*" >&2 +} + +# Function to extract value from XML using basic text processing +# Usage: extract_xml_value "ElementName" +extract_xml_value() { + local element="$1" + local xml_content="$2" + echo "$xml_content" | grep -oP "(?<=<${element}>)[^<]+" | head -1 || echo "" +} + +# Function to extract value with namespace prefix +extract_ns_xml_value() { + local element="$1" + local xml_content="$2" + echo "$xml_content" | grep -oP "(?<=<[^:]*:${element}>)[^<]+" | head -1 || echo "" +} + +main() { + log "Starting Azure provisioning media fetch..." + + if [[ ! -b "$CDROM_DEV" ]]; then + log "CD-ROM device $CDROM_DEV not found, skipping..." + exit 0 + fi + + # Find where /dev/sr0 is mounted (assume it's already mounted) + mount_dir=$(findmnt -n -o TARGET --source "$CDROM_DEV" 2>/dev/null | head -1) + + if [[ -z "$mount_dir" ]]; then + log "CD-ROM device $CDROM_DEV is not mounted, skipping..." + exit 0 + fi + + log "CD-ROM mounted at $mount_dir" + + # Check if ovf-env.xml exists + if [[ ! -f "$mount_dir/$OVF_FILE" ]]; then + log "ovf-env.xml not found at $mount_dir/$OVF_FILE, skipping..." + exit 0 + fi + + log "Found $OVF_FILE, parsing..." + + xml_content=$(cat "$mount_dir/$OVF_FILE") + + # Extract user data from LinuxProvisioningConfigurationSet: + # - UserName + # - UserPassword (may be hashed or plain) + # - DisableSSHPasswordAuthentication + # - SSH/PublicKeys/PublicKey/Fingerprint + # - SSH/PublicKeys/PublicKey/Path + # - SSH/PublicKeys/PublicKey/Value + # - CustomData (base64 encoded) + + username=$(extract_ns_xml_value "UserName" "$xml_content") + userpassword=$(extract_ns_xml_value "UserPassword" "$xml_content") + + ssh_keys=$(echo "$xml_content" | grep -oP '(?<=)[^<]+' | grep '^ssh-' || echo "") + + custom_data=$(extract_ns_xml_value "CustomData" "$xml_content") + + log "Extracted username: ${username:-}" + log "Extracted password: ${userpassword:+}" + log "Extracted SSH keys: ${ssh_keys:+}" + log "Extracted custom data: ${custom_data:+}" + + if [[ -z "$username" ]]; then + log "No username found in provisioning data, skipping config generation..." + exit 0 + fi + + # Generate password hash if we have a plain password + password_hash="$userpassword" + + # Build SSH authorized keys JSON array + ssh_keys_json="[]" + if [[ -n "$ssh_keys" ]]; then + ssh_keys_json="[" + first=true + while IFS= read -r key; do + [[ -z "$key" ]] && continue + if [[ "$first" == "true" ]]; then + first=false + else + ssh_keys_json+="," + fi + escaped_key=$(echo "$key" | sed 's/"/\\"/g') + ssh_keys_json+="\"$escaped_key\"" + done <<< "$ssh_keys" + ssh_keys_json+="]" + fi + + log "Generating Ignition config..." + + cat > "$OUTPUT_CONFIG" < Date: Wed, 19 Nov 2025 08:10:58 -0800 Subject: [PATCH 2/2] Updating changes --- .../30ignition/ignition-fetch-userdata-helper.sh | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/dracut/30ignition/ignition-fetch-userdata-helper.sh b/dracut/30ignition/ignition-fetch-userdata-helper.sh index 7f0877bc0..8460337fd 100644 --- a/dracut/30ignition/ignition-fetch-userdata-helper.sh +++ b/dracut/30ignition/ignition-fetch-userdata-helper.sh @@ -58,26 +58,14 @@ main() { xml_content=$(cat "$mount_dir/$OVF_FILE") - # Extract user data from LinuxProvisioningConfigurationSet: - # - UserName - # - UserPassword (may be hashed or plain) - # - DisableSSHPasswordAuthentication - # - SSH/PublicKeys/PublicKey/Fingerprint - # - SSH/PublicKeys/PublicKey/Path - # - SSH/PublicKeys/PublicKey/Value - # - CustomData (base64 encoded) - + # Extract user data from LinuxProvisioningConfigurationSet username=$(extract_ns_xml_value "UserName" "$xml_content") userpassword=$(extract_ns_xml_value "UserPassword" "$xml_content") - ssh_keys=$(echo "$xml_content" | grep -oP '(?<=)[^<]+' | grep '^ssh-' || echo "") - - custom_data=$(extract_ns_xml_value "CustomData" "$xml_content") log "Extracted username: ${username:-}" log "Extracted password: ${userpassword:+}" log "Extracted SSH keys: ${ssh_keys:+}" - log "Extracted custom data: ${custom_data:+}" if [[ -z "$username" ]]; then log "No username found in provisioning data, skipping config generation..." @@ -163,3 +151,4 @@ EOF # Run main function main "$@" +