From 67f11f6e5d31de9187d27fbdb9449d3918a18bd5 Mon Sep 17 00:00:00 2001 From: David Galloway Date: Thu, 15 Jan 2026 20:11:12 -0500 Subject: [PATCH] fog: Set ipmitool chassis bootdev during deployments Signed-off-by: David Galloway --- teuthology/orchestra/console.py | 50 +++++++++++++++++++++++++++++++++ teuthology/provision/fog.py | 2 ++ 2 files changed, 52 insertions(+) diff --git a/teuthology/orchestra/console.py b/teuthology/orchestra/console.py index b3ec963d7..5cce96bc1 100644 --- a/teuthology/orchestra/console.py +++ b/teuthology/orchestra/console.py @@ -3,6 +3,7 @@ import logging import os import pexpect import psutil +import re import subprocess import sys import time @@ -269,6 +270,55 @@ class PhysicalConsole(RemoteConsole): self.log.exception('Failed to get ipmi console status') return False + def set_bootdev(self, bootdev, uefi=True, persistent=False, timeout=None): + """ + Set next boot device via IPMI. + + Examples: + - PXE (UEFI): ipmitool chassis bootdev pxe options=efiboot + - Disk (UEFI): ipmitool chassis bootdev disk options=efiboot + + :param bootdev: one of: pxe, disk, cdrom, bios, safe, diag, none + :param uefi: add 'options=efiboot' for UEFI systems + :param persistent: request persistent bootdev if supported + (adds 'options=persistent' or combined with efiboot) + :param timeout: override console timeout for ipmitool invocation + """ + bootdev = str(bootdev).strip().lower() + valid = {"pxe", "disk", "cdrom", "bios", "safe", "diag", "floppy", "none"} + if bootdev not in valid: + raise ValueError("Invalid bootdev '%s' (valid: %s)" % + (bootdev, ", ".join(sorted(valid)))) + + options = [] + if uefi: + options.append("efiboot") + if persistent: + # ipmitool supports 'options=persistent' on many BMCs. + # If your BMC doesn't, this is harmless to omit by caller. + options.append("persistent") + + cmd = "chassis bootdev %s" % bootdev + if options: + cmd += " options=" + ",".join(options) + + self.log.info("Setting bootdev: %s", cmd) + child = self._pexpect_spawn_ipmi(cmd) + # Some ipmitool builds return EOF quickly; accept EOF as success-ish. + child.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=timeout or self.timeout) + out = child.logfile_read.getvalue().strip() + if out: + self.log.debug("bootdev output: %s", out) + return True + + def boot_pxe_once(self, uefi=True, persistent=False): + """Convenience: set next boot to PXE.""" + return self.set_bootdev("pxe", uefi=uefi, persistent=persistent) + + def boot_disk_once(self, uefi=True, persistent=False): + """Convenience: set next boot to local disk.""" + return self.set_bootdev("disk", uefi=uefi, persistent=persistent) + def power_cycle(self, timeout=300): """ Power cycle and wait for login. diff --git a/teuthology/provision/fog.py b/teuthology/provision/fog.py index 101da2464..5d230a70d 100644 --- a/teuthology/provision/fog.py +++ b/teuthology/provision/fog.py @@ -83,11 +83,13 @@ class FOG(object): # _wait_for_login, which will not work here since the newly-imaged # host will have an incorrect hostname self.remote.console.power_off() + self.remote.console.boot_pxe_once() self.remote.console.power_on() self.wait_for_deploy_task(task_id) except Exception: self.cancel_deploy_task(task_id) raise + self.remote.console.boot_disk_once() self._wait_for_ready() self._fix_hostname() self._verify_installed_os() -- 2.47.3