Unattended Proxmox VE install over iDRAC
How a Dell server in the rack is (re)installed with Proxmox VE
entirely remotely, with no keyboard, no USB stick and nobody in the server room. Used for
rumba on 2026-07-31; the same recipe applies to epyc0 / epyc1
and to any future node of the target architecture.
Requirements: an iDRAC Enterprise license (virtual media), a machine on the LAN able to serve a file over HTTP, and the iDRAC credentials from the secretzone.
iDRAC8 nodes take a different path
install-via-redfish.shThe R630s of the carnaval cluster carry iDRAC8 (firmware 2.86, perpetual Enterprise). Three of this page's mechanisms behave differently there, all measured 2026-08-04:
| Mechanism | iDRAC9 (rumba) | iDRAC8 (R630) |
|---|---|---|
racadm over SSH | reliable | unreliable — the one-shot ssh idrac "racadm …" form authenticates then returns nothing (1 success in 6); the interactive form intermittently dies with message authentication code incorrect |
| Redfish virtual media + reset | works | works, and is the reliable channel |
Redfish Boot.BootSourceOverrideTarget=Cd | works | lies — accepted, reads back as Cd, then silently discarded; the BIOS boots its own order and the attribute reverts to None |
| Console screenshot (§5) | works | no — both Redfish ExportServerScreenShot paths return HTTP 405 |
Two consequences. First, the one-time CD boot must be set at the iDRAC level
(iDRAC.serverboot.FirstBootDevice=VCD-DVD + BootOnce=Enabled), which is only reachable over
racadm — /Managers/iDRAC.Embedded.1/Attributes exists but exposes zero attributes. So
install-via-redfish.sh does that one step over racadm, retries it three times and reads the
values back before it will reset the machine.
Second, with no screenshots, the ISO server's stream of 206s is the only progress signal.
Budget more time than on rumba: a node writing to 2013-era SAS disks took ~25 minutes.
A trap that cost a wasted install: a node whose old bootloader still works will happily ignore the virtual CD. Two of the three carnaval nodes installed only because rebuilding their RAID volumes had destroyed the bootloader on their other bootable disk — the CD won by elimination, not by the override. The third kept its array, kept its bootloader, and booted its old OS instead.
This page explains the why. The runnable versions of every step — plus the answer file that
actually produced rumba — live in provisioning/pve/ in this repository:
provisioning/pve/build-auto-iso.sh 9.2-1 answer.rumba.toml pve-rumba-auto.iso
provisioning/pve/serve-iso.sh 192.168.88.248 8081 # verifies Range support itself
provisioning/pve/install-via-idrac.sh 192.168.92.10 http://192.168.88.248:8081/pve-rumba-auto.iso
provisioning/pve/console-screenshot.sh 192.168.92.10 # see the console at any moment
The scripts refuse to continue on the two failure modes documented below (a server that ignores
Range, and a share that does not report Enabled), and install-via-idrac.sh asks for an
explicit yes before the destructive power-cycle.
1. Build a self-installing ISO
Since PVE 8.2 the installer can read all its answers from a TOML file baked into the ISO, so the
install needs no interaction at all. proxmox-auto-install-assistant lives in the Proxmox
repository; run it in a throwaway Debian container so nothing is added to the host:
# on a machine with docker + disk space (calypsomaster)
mkdir -p ~/pve-install && cd ~/pve-install
curl -sSLO https://enterprise.proxmox.com/iso/proxmox-ve_9.2-1.iso
curl -s https://enterprise.proxmox.com/iso/SHA256SUMS | grep 9.2-1 # verify!
sha256sum proxmox-ve_9.2-1.iso
sudo docker run --rm -v ~/pve-install:/work -w /work debian:trixie bash -c '
apt-get update -qq && apt-get install -y -qq curl ca-certificates
curl -fsSL https://enterprise.proxmox.com/debian/proxmox-release-trixie.gpg \
-o /etc/apt/trusted.gpg.d/proxmox-release-trixie.gpg
echo "deb http://download.proxmox.com/debian/pve trixie pve-no-subscription" \
> /etc/apt/sources.list.d/pve.list
apt-get update -qq && apt-get install -y -qq proxmox-auto-install-assistant
proxmox-auto-install-assistant validate-answer answer.toml
proxmox-auto-install-assistant prepare-iso proxmox-ve_9.2-1.iso \
--fetch-from iso --answer-file answer.toml --output pve-9.2-1-auto.iso'
The answer.toml used for rumba (adapt FQDN, disks and keys per machine):
[global]
keyboard = "fr-ch"
country = "ch"
fqdn = "rumba.calypso"
mailto = "pierre-andre.mudry@hevs.ch"
timezone = "Europe/Zurich"
root-password-hashed = "$6$..." # openssl passwd -6
root-ssh-keys = ["ssh-ed25519 AAAA..."] # admin key, for immediate access
[network]
source = "from-answer" # do NOT use from-dhcp, see below
cidr = "192.168.88.51/24"
gateway = "192.168.88.1"
dns = "192.168.88.1"
filter.ID_NET_NAME = "eno1" # pin the NIC that is actually cabled
[disk-setup]
filesystem = "ext4"
disk-list = ["nvme0n1"] # ONLY this disk is touched
lvm.maxroot = 100 # GB for /, remainder = LVM-thin for VMs
Two things worth care here. disk-list is what protects every other disk in the machine (rumba's
eight SATA HDDs were deliberately left out). And the [network] section should be static, with
the interface pinned — the reason is worth a warning of its own:
source = "from-dhcp" picked the wrong NIC on rumbaOn a multi-NIC server the installer DHCPDISCOVERs on every interface (rumba has five: eno1-eno4
plus a 10 G ens8f1np1). Rumba's cabled link is an SFP+ port on the CCR2004, which negotiates
slower than the installer's DHCP window, so no lease arrived in time. The installer then silently
fell back to its default 192.168.100.2/24 and put vmbr0 on an interface with no link — the
install itself succeeded but the host was unreachable, with none of its MACs appearing in the
router's bridge table.
Pin the interface with filter.ID_NET_NAME (check the candidate names with
udevadm info /sys/class/net/<if> | grep ID_NET_NAME) and give the address explicitly. Diagnosing
this cost a full reinstall cycle. If a freshly installed node is unreachable, suspect this first —
grab a console screenshot (below) and read the address the login banner advertises.
2. Serve the ISO — must support HTTP range requests
python -m http.serverThe iDRAC emulates a CD drive by issuing ranged GETs over the share. python -m http.server
speaks HTTP/1.0 and ignores the Range header: it answers 200 with the file from byte zero.
The symptom is subtle and cost us a boot cycle on rumba — the server does show "booting from
iDRAC virtual disc", then falls through to the local disk as soon as the boot loader seeks, and
racadm remoteimage -s reports Disabled with a single GET in the log.
Use nginx (or any HTTP/1.1 server honouring Range):
sudo docker run -d --rm --name pve-iso -p <serving-ip>:8081:80 \
-v ~/pve-install:/usr/share/nginx/html:ro nginx:alpine
Verify before power-cycling anything — a ranged request must return 206, not 200:
curl -sI http://<serving-ip>:8081/pve-9.2-1-auto.iso | grep -i accept-ranges
curl -s -r 1000000-1000511 -D - -o /dev/null http://<serving-ip>:8081/pve-9.2-1-auto.iso | head -1
# expect: HTTP/1.1 206 Partial Content
Note that calypsomaster already runs squid on port 8000, so pick another port (8081 works).
3. Attach, boot once, install
racadm remoteimage -d # clear any stale share
racadm remoteimage -c -l http://<serving-ip>:8081/pve-9.2-1-auto.iso
racadm remoteimage -s # MUST say "Enabled"
racadm set iDRAC.serverboot.FirstBootDevice VCD-DVD
racadm set iDRAC.serverboot.BootOnce Enabled # so later reboots use the disk
racadm serveraction powercycle
Progress is visible in the HTTP server log: a healthy install streams a continuous series of
206 responses of 128 KB each. Optionally watch the HTML5 console at https://<idrac-ip>.
The installer partitions, installs and reboots on its own; the node answers on
https://<name>:8006 after roughly 10–20 minutes.
Afterwards, stop the ISO server and detach the share (racadm remoteimage -d) so the virtual CD
does not stay mapped.
3bis. First thing after the install: repositories
A fresh node points only at the enterprise repositories, which return 401 without a subscription,
so apt-get update fails until they are replaced. PVE 9 uses deb822 files in
/etc/apt/sources.list.d/ (not legacy one-liners):
cat > /etc/apt/sources.list.d/pve-no-subscription.sources <<'EOF'
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
# disable the enterprise repo rather than deleting it
printf 'Enabled: false\n' >> /etc/apt/sources.list.d/pve-enterprise.sources
# and point the ceph repo at its no-subscription component (or disable it too)
sed -i 's/^Components: enterprise$/Components: no-subscription/' /etc/apt/sources.list.d/ceph.sources
apt-get update
Adjust Suites: for the Debian release the PVE version is based on (trixie for PVE 9).
4. Gotchas seen in practice
- Unclean power-cycle leaves Ubuntu's GRUB waiting. If a boot attempt fails and the machine
falls back to an existing Ubuntu install,
recordfailmakes GRUB wait indefinitely for a keypress instead of timing out — the machine looks hung with no network. Harmless; the next power-cycle clears it. remoteimage -creports success even when the share fails. Always confirm withremoteimage -s; "Remote Image is now Configured" is not proof of anything.- Interactive password prompts. iDRAC and the MikroTiks only accept password auth, which
breaks in non-TTY contexts. Drive them with an
expectwrapper that reads the password from the secretzone rather than putting it on a command line.
5. Seeing the console without a browser
The iDRAC can hand over a PNG of whatever is on the server's screen right now, over Redfish. This is the fastest way to tell "still installing" from "stuck at a boot menu" from "installed but on the wrong IP" — all three happened during the rumba install:
printf 'machine <idrac-ip> login <user> password <pw>\n' > ~/.netrc-idrac && chmod 600 ~/.netrc-idrac
curl -sk --netrc-file ~/.netrc-idrac -X POST -H "Content-Type: application/json" \
-d '{"FileType":"ServerScreenShot"}' \
"https://<idrac-ip>/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellLCService/Actions/DellLCService.ExportServerScreenShot" \
-o shot.json
python3 -c "import json,base64;open('shot.png','wb').write(base64.b64decode(json.load(open('shot.json'))['ServerScreenShotFile']))"
Note the endpoint path: the older /redfish/v1/Dell/Systems/... form returns 404 on iDRAC9
firmware 7.x. A healthy install also shows up in the HTTP server log as a steady stream of 206
responses; if that stream stops while the machine stays powered on, take a screenshot.