Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
156 changes: 156 additions & 0 deletions dracut/30ignition/README-azure-provisioning.md
Original file line number Diff line number Diff line change
@@ -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
<?xml version="1.0" encoding="utf-8"?>
<Environment xmlns="http://schemas.dmtf.org/ovf/environment/1"
xmlns:oe="http://schemas.dmtf.org/ovf/environment/1"
xmlns:wa="http://schemas.microsoft.com/windowsazure"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<wa:ProvisioningSection>
<wa:Version>1.0</wa:Version>
<LinuxProvisioningConfigurationSet>
<ConfigurationSetType>LinuxProvisioningConfiguration</ConfigurationSetType>
<HostName>myhost</HostName>
<UserName>azureuser</UserName>
<UserPassword>$6$rounds=4096$abcdefgh$hashedpassword...</UserPassword>
<DisableSshPasswordAuthentication>false</DisableSshPasswordAuthentication>
<SSH>
<PublicKeys>
<PublicKey>
<Fingerprint>...</Fingerprint>
<Path>/home/azureuser/.ssh/authorized_keys</Path>
<Value>ssh-rsa AAAAB3NzaC1yc2EAAAA... user@host</Value>
</PublicKey>
</PublicKeys>
</SSH>
</LinuxProvisioningConfigurationSet>
</wa:ProvisioningSection>
</Environment>
```

### 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 `<Value>` elements

## Generated Ignition Configuration

The helper generates an Ignition 3.3.0 config that:

1. **Creates a user** with:
- Username from `<UserName>`
- Password hash from `<UserPassword>`
- SSH authorized keys from `<SSH><PublicKeys>`
- Home directory at `/home/<username>`
- 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/)


154 changes: 154 additions & 0 deletions dracut/30ignition/ignition-fetch-userdata-helper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/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=$(extract_ns_xml_value "UserName" "$xml_content")
userpassword=$(extract_ns_xml_value "UserPassword" "$xml_content")
ssh_keys=$(echo "$xml_content" | grep -oP '(?<=<Value>)[^<]+' | grep '^ssh-' || echo "")

log "Extracted username: ${username:-<none>}"
log "Extracted password: ${userpassword:+<present>}"
log "Extracted SSH keys: ${ssh_keys:+<present>}"

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" <<EOF
{
"ignition": {
"version": "3.3.0"
},
"passwd": {
"users": [
{
"name": "$username",
"groups": ["wheel"],
"homeDir": "/home/$username",
"shell": "/bin/bash"$(
if [[ -n "$password_hash" ]]; then
echo ","
echo " \"passwordHash\": \"$password_hash\""
fi
)$(
if [[ "$ssh_keys_json" != "[]" ]]; then
echo ","
echo " \"sshAuthorizedKeys\": $ssh_keys_json"
fi
)
}
]
},
"storage": {
"files": [
{
"path": "/etc/sudoers.d/99_wheel_nopasswd",
"contents": {
"compression": "",
"source": "data:,%25wheel%20ALL%3D(ALL)%20NOPASSWD%3AALL%0A"
},
"mode": 288
},
{
"path": "/etc/ssh/sshd_config.d/10-custom.conf",
"contents": {
"compression": "",
"source": "data:,%23%20Custom%20SSHD%20settings%0APasswordAuthentication%20yes%0APermitRootLogin%20no%0A"
},
"mode": 420
}
]
}
}
EOF

log "Ignition config written to $OUTPUT_CONFIG"
log "Config will be processed by subsequent Ignition stages"

exit 0
}

# Run main function
main "$@"

2 changes: 2 additions & 0 deletions dracut/30ignition/ignition-fetch.service
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ After=network.target
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=/run/ignition.env
# Run custom helper to fetch user data from Azure provisioning media
ExecStartPre=/usr/sbin/ignition-fetch-userdata-helper
ExecStart=/usr/bin/ignition --root=/sysroot --platform=${PLATFORM_ID} --stage=fetch ${IGNITION_ARGS}
4 changes: 4 additions & 0 deletions dracut/30ignition/module-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ install_ignition_unit() {
install() {
inst_multiple \
basename \
findmnt \
lsblk

# Not all features of the configuration may be available on all systems
Expand Down Expand Up @@ -73,6 +74,9 @@ install() {
inst_script "$moddir/ignition-kargs-helper.sh" \
"/usr/sbin/ignition-kargs-helper"

inst_script "$moddir/ignition-fetch-userdata-helper.sh" \
"/usr/sbin/ignition-fetch-userdata-helper"

# Distro packaging is expected to install the ignition binary into the
# module directory.
inst_simple "$moddir/ignition" \
Expand Down