Setup default UEFI PXE vs HTTP mode in firmware within VM.

Himcules

New Member
Jan 8, 2024
20
0
1
Hello,

On my VMs, by default I setup using the UEFI BIOS as OVMF.

When first booting the default order by net boot is as follows:

PXE IPv4
PXE IPv6
UEFI HTTP

1707515214694.png

Now, I can subsequently enter the VM FW menu to change this to specify boot using UEFI HTTP as the first option.
Q is... how can I by default change the order from either GUI or CLI for all my VMs without needing to do this manually per each?

As I have to wait for this timeout to go through each boot option, before finally reaching the UEFI HTTP boot as I intend. ( I have no need for TFTP in this case )
 
As I ran into the same problem today, I wanted to share the resulting bash script for changing the boot order. On PVE 9 all required tools are already available, on PVE 8 eventually the package virt-firmware must be installed first as it contains the required virt-fw-vars program.

How to run
./setboottohttp.sh [-n] [-p] [-m <id>] /path/to/efidisk


What is does
simple script which reads vars from EFI disk and updates them to
  • only next boot is done via HTTPv4 (default or -n) OR
  • Boot order change to always boot via HTTPv4 first (-p)
  • if boot entry id is provided this will be used instead of HTTPv4 detection (-m <id>), can be combined with -n (default) and -p.

requirements
  • virt-fw-vars (part of package virt-firmware and installed on PVE 9 by default)
  • python3
  • access to efi disk

I tested it with PVE 9 and a vm disk stored on ZFS pool successfully.

Bash:
#!/bin/bash

# 1. Root check
if [ "$EUID" -ne 0 ]; then
    echo "Error: This script must be run as root." >&2
    exit 255
fi

# 2. Tool check
if ! command -v virt-fw-vars &> /dev/null; then
    echo "Error: The program 'virt-fw-vars' is not installed." >&2
    exit 255
fi

# Default values
MODE="next"
MANUAL_ID=""
DISK=""

# Flags for collision detection
FLAG_N=0
FLAG_P=0

# Process parameters
while [[ "$#" -gt 0 ]]; do
    case $1 in
        -n) FLAG_N=1; MODE="next"; shift ;;
        -p) FLAG_P=1; MODE="permanent"; shift ;;
        -m) MANUAL_ID="$2"; shift 2 ;;
        -*) echo "Error: Invalid option $1" >&2; exit 255 ;;
        *)  DISK="$1"; shift ;;
    esac
done

# Collision check
if [ "$FLAG_N" -eq 1 ] && [ "$FLAG_P" -eq 1 ]; then
    echo "Error: The options -n (once) and -p (permanent) are mutually exclusive." >&2
    echo "Usage: $0 [-n|-p] [-m <boot_id>] <path_to_efidisk>" >&2
    exit 255
fi

if [ -z "$DISK" ]; then
    echo "Error: Path to EFI disk is missing." >&2
    echo "Usage: $0 [-n|-p] [-m <boot_id>] <path_to_efidisk>" >&2
    exit 255
fi

if [ ! -f "$DISK" ] && [ ! -b "$DISK" ]; then
    echo "Error: Disk '$DISK' not found." >&2
    exit 255
fi

# 3. Check format of manual ID (4-digit hex value)
if [ -n "$MANUAL_ID" ]; then
    # Convert to uppercase for uniform matching
    MANUAL_ID="${MANUAL_ID^^}"
    if [[ ! "$MANUAL_ID" =~ ^[0-9A-F]{4}$ ]]; then
        echo "Error: Manual ID '$MANUAL_ID' is invalid. It must be a 4-digit hex value (e.g., 0005 or 000A)." >&2
        exit 255
    fi
fi

# 4. Read variables
TMP_DUMP=$(mktemp /tmp/efidump_XXXXXX.json)

virt-fw-vars -i "$DISK" --output-json "$TMP_DUMP" >/dev/null 2>&1
if [ $? -ne 0 ]; then
    echo "Error: virt-fw-vars could not read the EFI disk (Lock active?)." >&2
    rm -f "$TMP_DUMP"
    exit 255
fi

# Python extracts HTTP-IDs, BootOrder and ALL existing Boot-IDs
MATCHES=$(python3 -c "
import sys, json

http_ids = []
all_boot_ids = set()
boot_order = ''

def parse_node(node):
    global boot_order
    if isinstance(node, list):
        for item in node:
            parse_node(item)
    elif isinstance(node, dict):
        name = node.get('name', '')
        if isinstance(name, str):
            if name == 'BootOrder':
                boot_order = node.get('data', '')
            elif name.startswith('Boot') and len(name) == 8:
                try:
                    # Check if the last 4 characters are hex
                    hex_id = name[4:].upper()
                    int(hex_id, 16)
                    all_boot_ids.add(hex_id)
                    
                    val = node.get('data', '')
                    if isinstance(val, str):
                        data_str = val.replace('\n', '').replace('\r', '').replace(' ', '').lower()
                        if '480054005400500076003400' in data_str:
                            http_ids.append(hex_id)
                except ValueError:
                    pass
        for val in node.values():
            parse_node(val)

try:
    with open('$TMP_DUMP', 'r') as f:
        parse_node(json.load(f))
    print(f'BOOTORDER={boot_order}')
    for hid in http_ids:
        print(f'ID={hid}')
    for bid in all_boot_ids:
        print(f'ALL_ID={bid}')
except Exception as e:
    print(f'Python parsing error: {e}', file=sys.stderr)
")

rm -f "$TMP_DUMP"

# Bash parses the Python output
BOOTORDER_RAW=$(echo "$MATCHES" | grep "^BOOTORDER=" | cut -d= -f2)
mapfile -t ID_ARRAY <<< "$(echo "$MATCHES" | grep "^ID=" | cut -d= -f2)"
mapfile -t ALL_IDS_ARRAY <<< "$(echo "$MATCHES" | grep "^ALL_ID=" | cut -d= -f2)"

# Filter empty entries
ID_ARRAY=($(for id in "${ID_ARRAY[@]}"; do echo "$id"; done | grep -v "^$"))
ALL_IDS_ARRAY=($(for id in "${ALL_IDS_ARRAY[@]}"; do echo "$id"; done | grep -v "^$"))

# 5. Determine Target ID
if [ -n "$MANUAL_ID" ]; then
    TARGET_ID="$MANUAL_ID"
    echo "Using manually provided Boot-ID: $TARGET_ID"
else
    MATCH_COUNT=${#ID_ARRAY[@]}
    if [ "$MATCH_COUNT" -eq 0 ]; then
        echo "Error: No UEFI HTTPv4 boot entry found." >&2
        exit 2
    elif [ "$MATCH_COUNT" -gt 1 ]; then
        echo "Abort: Multiple UEFI HTTPv4 entries found." >&2
        printf " - %s\n" "${ID_ARRAY[@]}" >&2
        exit 3
    else
        TARGET_ID="${ID_ARRAY[0]}"
        echo "Exactly one HTTPv4 entry found: Boot${TARGET_ID}"
    fi
fi

# 6. Check existence of target ID
ID_EXISTS=0
for valid_id in "${ALL_IDS_ARRAY[@]}"; do
    if [ "$valid_id" == "$TARGET_ID" ]; then
        ID_EXISTS=1
        break
    fi
done

if [ "$ID_EXISTS" -eq 0 ]; then
    echo "Error: The boot option 'Boot${TARGET_ID}' does not exist in the EFI variables of this VM." >&2
    exit 255
fi

# Little Endian byte-swap for the payload (e.g., 0005 -> 0500)
DATA_LE="${TARGET_ID:2:2}${TARGET_ID:0:2}"

TMP_JSON=$(mktemp /tmp/bootupdate_XXXXXX.json)

# 7. Generate JSON (depending on mode)
if [ "$MODE" = "permanent" ]; then
    # Manipulate BootOrder: Completely remove target ID and prepend it
    # Case-insensitive replace in Bash to catch both lower and upper case hex
    CLEAN_ORDER="${BOOTORDER_RAW//${DATA_LE,,}/}"
    CLEAN_ORDER="${CLEAN_ORDER//${DATA_LE^^}/}"
    NEW_ORDER="${DATA_LE,,}${CLEAN_ORDER}"
    
    cat <<EOF > "$TMP_JSON"
{
    "BootOrder": {
        "name": "BootOrder",
        "guid": "8be4df61-93ca-11d2-aa0d-00e098032b8c",
        "attr": 7,
        "data": "${NEW_ORDER}"
    }
}
EOF
    echo "Changing permanent BootOrder (Putting $DATA_LE in first position)..."
else
    # BootNext (Default)
    cat <<EOF > "$TMP_JSON"
{
    "BootNext": {
        "name": "BootNext",
        "guid": "8be4df61-93ca-11d2-aa0d-00e098032b8c",
        "attr": 7,
        "data": "${DATA_LE,,}"
    }
}
EOF
    echo "Writing BootNext ($DATA_LE)..."
fi

# 8. Write changes to disk
virt-fw-vars --inplace "$DISK" --set-json "$TMP_JSON" >/dev/null 2>&1
STATUS=$?

rm -f "$TMP_JSON"

if [ $STATUS -eq 0 ]; then
    echo "Successfully executed!"
    exit 0
else
    echo "Error writing to the EFI disk with virt-fw-vars." >&2
    exit 255
fi
 
  • Like
Reactions: UdoB