#!/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