mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-27 10:42:28 +02:00
fix(qemu): turn off kvmclock and TSC deadline timer on Hyper-V hosts without a usable TSC
When the compute is itself a Hyper-V guest without an invariant TSC (e.g. the GNS3 VM on hosts whose CPU lacks TSC scaling), Linux falls back to the hyperv_clocksource_tsc_page clocksource and the guest timekeeping KVM derives from the TSC is broken with recent kernels: - kvmclock runs thousands of times too fast: Linux guests (MikroTik RouterOS, ...) panic with "watchdog: BUG: soft lockup - CPU#0 stuck for 29604s" and reboot in a loop right after boot; - TSC deadline timers fire far too late: guests with -cpu host hang. The only workaround so far was -machine accel=tcg, i.e. no KVM at all. Add a kvmclock setting to the [Qemu] section (auto, enabled, disabled). In auto mode, KVM guests started on a host whose clocksource is a Hyper-V one get kvmclock and tsc-deadline turned off: on the -cpu given in the additional options (unless it sets these features itself), otherwise on QEMU's default CPU model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
09ad486966
commit
53164eccee
@ -63,6 +63,31 @@ log = logging.getLogger(__name__)
|
|||||||
FORBIDDEN_OPTIONS = {"-blockdev", "-drive", "-hda", "-hdb", "-hdc", "-hdd", "-fsdev", "-virtfs", "-nic", "-netdev"}
|
FORBIDDEN_OPTIONS = {"-blockdev", "-drive", "-hda", "-hdb", "-hdc", "-hdd", "-fsdev", "-virtfs", "-nic", "-netdev"}
|
||||||
FORBIDDEN_OPTIONS |= {"-" + opt for opt in FORBIDDEN_OPTIONS if opt.startswith("-") and not opt.startswith("--")}
|
FORBIDDEN_OPTIONS |= {"-" + opt for opt in FORBIDDEN_OPTIONS if opt.startswith("-") and not opt.startswith("--")}
|
||||||
|
|
||||||
|
CLOCKSOURCE_PATH = "/sys/devices/system/clocksource/clocksource0/current_clocksource"
|
||||||
|
|
||||||
|
# Clocksources of a Linux host running on Hyper-V without a usable TSC. The guest
|
||||||
|
# timekeeping KVM derives from the TSC there is broken with recent kernels: kvmclock
|
||||||
|
# runs thousands of times too fast (Linux guests panic with soft lockups and reboot
|
||||||
|
# in a loop) and TSC deadline timers fire far too late (guests hang while booting).
|
||||||
|
HYPERV_CLOCKSOURCES = ("hyperv_clocksource_tsc_page", "hyperv_clocksource_msr")
|
||||||
|
|
||||||
|
# guest CPU features turned off on such hosts
|
||||||
|
KVMCLOCK_OFF_FEATURES = ("kvmclock", "tsc-deadline")
|
||||||
|
|
||||||
|
|
||||||
|
def host_clocksource():
|
||||||
|
"""
|
||||||
|
Returns the clocksource used by the host kernel (Linux only).
|
||||||
|
|
||||||
|
:returns: clocksource name or None
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(CLOCKSOURCE_PATH, encoding="utf-8") as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class QemuVM(KernelDatapathMixin, BaseNode):
|
class QemuVM(KernelDatapathMixin, BaseNode):
|
||||||
module_name = "qemu"
|
module_name = "qemu"
|
||||||
@ -2763,6 +2788,44 @@ class QemuVM(KernelDatapathMixin, BaseNode):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _kvmclock_off_reason(self):
|
||||||
|
"""
|
||||||
|
Returns why kvmclock and the TSC deadline timer must be turned off for this KVM guest.
|
||||||
|
|
||||||
|
:returns: reason (string) or None to keep them
|
||||||
|
"""
|
||||||
|
|
||||||
|
kvmclock = self.manager.config.settings.Qemu.kvmclock
|
||||||
|
if kvmclock == "disabled":
|
||||||
|
return "kvmclock = disabled in the [Qemu] server settings"
|
||||||
|
if kvmclock == "auto":
|
||||||
|
clocksource = host_clocksource()
|
||||||
|
if clocksource in HYPERV_CLOCKSOURCES:
|
||||||
|
return (
|
||||||
|
f"the host runs on Hyper-V without a usable TSC (clocksource {clocksource}), "
|
||||||
|
"set kvmclock = enabled in the [Qemu] server settings to keep them"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _kvmclock_off_options(self, additional_options):
|
||||||
|
"""
|
||||||
|
Turns kvmclock and the TSC deadline timer off on the -cpu of the additional
|
||||||
|
options (except the features it sets itself) or, without one, on QEMU's
|
||||||
|
default CPU model.
|
||||||
|
|
||||||
|
:param additional_options: additional options (list), updated in place
|
||||||
|
:returns: options to add to the command line
|
||||||
|
"""
|
||||||
|
|
||||||
|
for index in range(len(additional_options) - 2, -1, -1):
|
||||||
|
if additional_options[index] in ("-cpu", "--cpu"):
|
||||||
|
for feature in KVMCLOCK_OFF_FEATURES:
|
||||||
|
if feature not in additional_options[index + 1]:
|
||||||
|
additional_options[index + 1] += f",{feature}=off"
|
||||||
|
return []
|
||||||
|
cpu_model = "qemu32" if self._platform == "i386" else "qemu64"
|
||||||
|
return ["-cpu", ",".join([cpu_model] + [f"{feature}=off" for feature in KVMCLOCK_OFF_FEATURES])]
|
||||||
|
|
||||||
async def _clear_save_vm_stated(self, snapshot_name="GNS3_SAVED_STATE"):
|
async def _clear_save_vm_stated(self, snapshot_name="GNS3_SAVED_STATE"):
|
||||||
|
|
||||||
drives = ["a", "b", "c", "d"]
|
drives = ["a", "b", "c", "d"]
|
||||||
@ -2862,9 +2925,11 @@ class QemuVM(KernelDatapathMixin, BaseNode):
|
|||||||
if self._cpus > maxcpus:
|
if self._cpus > maxcpus:
|
||||||
maxcpus = self._cpus
|
maxcpus = self._cpus
|
||||||
command.extend(["-smp", f"cpus={self._cpus},maxcpus={maxcpus},sockets=1"])
|
command.extend(["-smp", f"cpus={self._cpus},maxcpus={maxcpus},sockets=1"])
|
||||||
|
kvm = False
|
||||||
if await self._run_with_hardware_acceleration(self.qemu_path, self._options):
|
if await self._run_with_hardware_acceleration(self.qemu_path, self._options):
|
||||||
if sys.platform.startswith("linux"):
|
if sys.platform.startswith("linux"):
|
||||||
command.extend(["-enable-kvm"])
|
command.extend(["-enable-kvm"])
|
||||||
|
kvm = True
|
||||||
elif sys.platform.startswith("darwin"):
|
elif sys.platform.startswith("darwin"):
|
||||||
command.extend(["-enable-hax"])
|
command.extend(["-enable-hax"])
|
||||||
command.extend(["-boot", f"order={self._boot_priority}"])
|
command.extend(["-boot", f"order={self._boot_priority}"])
|
||||||
@ -2887,6 +2952,7 @@ class QemuVM(KernelDatapathMixin, BaseNode):
|
|||||||
command.extend(await self._disable_graphics())
|
command.extend(await self._disable_graphics())
|
||||||
if self._tpm:
|
if self._tpm:
|
||||||
command.extend(self._tpm_options())
|
command.extend(self._tpm_options())
|
||||||
|
additional_opt_list = []
|
||||||
if additional_options:
|
if additional_options:
|
||||||
try:
|
try:
|
||||||
additional_opt_list = shlex.split(additional_options)
|
additional_opt_list = shlex.split(additional_options)
|
||||||
@ -2897,7 +2963,15 @@ class QemuVM(KernelDatapathMixin, BaseNode):
|
|||||||
for opt in additional_opt_list:
|
for opt in additional_opt_list:
|
||||||
if opt in FORBIDDEN_OPTIONS:
|
if opt in FORBIDDEN_OPTIONS:
|
||||||
raise QemuError(f"Forbidden additional option: {opt}")
|
raise QemuError(f"Forbidden additional option: {opt}")
|
||||||
command.extend(additional_opt_list)
|
if kvm:
|
||||||
|
kvmclock_off_reason = self._kvmclock_off_reason()
|
||||||
|
if kvmclock_off_reason:
|
||||||
|
log.info(
|
||||||
|
f'QEMU VM "{self._name}" [{self._id}]: kvmclock and TSC deadline timer turned off, '
|
||||||
|
f"{kvmclock_off_reason}"
|
||||||
|
)
|
||||||
|
command.extend(self._kvmclock_off_options(additional_opt_list))
|
||||||
|
command.extend(additional_opt_list)
|
||||||
|
|
||||||
# avoiding mouse offset (see https://github.com/GNS3/gns3-server/issues/2335)
|
# avoiding mouse offset (see https://github.com/GNS3/gns3-server/issues/2335)
|
||||||
if self._console_type == "vnc":
|
if self._console_type == "vnc":
|
||||||
|
|||||||
@ -79,6 +79,13 @@ class IOUSettings(BaseModel):
|
|||||||
model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True)
|
model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True)
|
||||||
|
|
||||||
|
|
||||||
|
class QemuKvmclock(StrEnum):
|
||||||
|
# turned off only when the host runs on Hyper-V without a usable TSC
|
||||||
|
auto = "auto"
|
||||||
|
enabled = "enabled"
|
||||||
|
disabled = "disabled"
|
||||||
|
|
||||||
|
|
||||||
class QemuSettings(BaseModel):
|
class QemuSettings(BaseModel):
|
||||||
enable_monitor: bool = Field(True, description="Use the Qemu monitor feature to communicate with Qemu VMs")
|
enable_monitor: bool = Field(True, description="Use the Qemu monitor feature to communicate with Qemu VMs")
|
||||||
monitor_host: str = Field("127.0.0.1", description="IP used to listen for the monitor")
|
monitor_host: str = Field("127.0.0.1", description="IP used to listen for the monitor")
|
||||||
@ -86,6 +93,12 @@ class QemuSettings(BaseModel):
|
|||||||
require_hardware_acceleration: bool = Field(
|
require_hardware_acceleration: bool = Field(
|
||||||
False, description="Require hardware acceleration in order to start VMs"
|
False, description="Require hardware acceleration in order to start VMs"
|
||||||
)
|
)
|
||||||
|
kvmclock: QemuKvmclock = Field(
|
||||||
|
QemuKvmclock.auto,
|
||||||
|
description="kvmclock and TSC deadline timer of KVM guests: 'auto' turns them off when this server runs on "
|
||||||
|
"Hyper-V without a usable TSC (guest clocks run away there, Linux guests panic with soft lockups or hang), "
|
||||||
|
"'enabled' or 'disabled' forces them",
|
||||||
|
)
|
||||||
allow_unsafe_options: bool = Field(False, description="Allow unsafe additional command line options")
|
allow_unsafe_options: bool = Field(False, description="Allow unsafe additional command line options")
|
||||||
ovmf_firmware_dir: str = Field("/usr/share/OVMF", description="Path to the OVMF firmware directory")
|
ovmf_firmware_dir: str = Field("/usr/share/OVMF", description="Path to the OVMF firmware directory")
|
||||||
model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True)
|
model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True)
|
||||||
|
|||||||
@ -35,6 +35,7 @@ from ..config import (
|
|||||||
ControllerSettings,
|
ControllerSettings,
|
||||||
DynamipsSettings,
|
DynamipsSettings,
|
||||||
IOUSettings,
|
IOUSettings,
|
||||||
|
QemuKvmclock,
|
||||||
QemuSettings,
|
QemuSettings,
|
||||||
ServerProtocol,
|
ServerProtocol,
|
||||||
ServerSettings,
|
ServerSettings,
|
||||||
@ -194,6 +195,7 @@ class QemuSettingsUpdate(BaseModel):
|
|||||||
monitor_host: Optional[str] = None
|
monitor_host: Optional[str] = None
|
||||||
enable_hardware_acceleration: Optional[bool] = None
|
enable_hardware_acceleration: Optional[bool] = None
|
||||||
require_hardware_acceleration: Optional[bool] = None
|
require_hardware_acceleration: Optional[bool] = None
|
||||||
|
kvmclock: Optional[QemuKvmclock] = None
|
||||||
allow_unsafe_options: Optional[bool] = None
|
allow_unsafe_options: Optional[bool] = None
|
||||||
ovmf_firmware_dir: Optional[str] = None
|
ovmf_firmware_dir: Optional[str] = None
|
||||||
|
|
||||||
|
|||||||
@ -204,6 +204,20 @@ class TestSettingsRoutes:
|
|||||||
response = await client.put(app.url_path_for("update_server_settings"), json={"Server": {"prot": "http"}})
|
response = await client.put(app.url_path_for("update_server_settings"), json={"Server": {"prot": "http"}})
|
||||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
||||||
|
|
||||||
|
async def test_put_settings_qemu_kvmclock(
|
||||||
|
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
response = await client.put(app.url_path_for("update_server_settings"), json={"Qemu": {"kvmclock": "disabled"}})
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.json()["Qemu"]["kvmclock"] == "disabled"
|
||||||
|
parsed = configparser.ConfigParser()
|
||||||
|
parsed.read(config._main_config_file)
|
||||||
|
assert parsed["Qemu"]["kvmclock"] == "disabled"
|
||||||
|
|
||||||
|
response = await client.put(app.url_path_for("update_server_settings"), json={"Qemu": {"kvmclock": "off"}})
|
||||||
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
||||||
|
|
||||||
async def test_put_settings_deprecated_sections_rejected(
|
async def test_put_settings_deprecated_sections_rejected(
|
||||||
self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str
|
self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import pytest_asyncio
|
|||||||
from gns3server.compute.notification_manager import NotificationManager
|
from gns3server.compute.notification_manager import NotificationManager
|
||||||
from gns3server.compute.qemu import Qemu
|
from gns3server.compute.qemu import Qemu
|
||||||
from gns3server.compute.qemu.qemu_error import QemuError
|
from gns3server.compute.qemu.qemu_error import QemuError
|
||||||
from gns3server.compute.qemu.qemu_vm import QemuVM
|
from gns3server.compute.qemu.qemu_vm import QemuVM, host_clocksource
|
||||||
from gns3server.utils import force_unix_path, int_to_macaddress, macaddress_to_int
|
from gns3server.utils import force_unix_path, int_to_macaddress, macaddress_to_int
|
||||||
from tests.utils import AsyncioMagicMock, asyncio_patch
|
from tests.utils import AsyncioMagicMock, asyncio_patch
|
||||||
|
|
||||||
@ -815,6 +815,110 @@ async def test_build_command_with_forbidden_options(vm):
|
|||||||
await vm._build_command()
|
await vm._build_command()
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_command_with_kvm(vm, clocksource):
|
||||||
|
"""
|
||||||
|
Builds the command line as on a KVM host whose kernel uses the given clocksource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM._run_with_hardware_acceleration", return_value=True):
|
||||||
|
with patch("gns3server.compute.qemu.qemu_vm.host_clocksource", return_value=clocksource):
|
||||||
|
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()):
|
||||||
|
return await vm._build_command()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("clocksource", ["hyperv_clocksource_tsc_page", "hyperv_clocksource_msr"])
|
||||||
|
async def test_build_command_kvmclock_off_on_hyperv_host(linux_platform, vm, clocksource):
|
||||||
|
"""
|
||||||
|
A host running on Hyper-V without a usable TSC falls back to a Hyper-V clocksource:
|
||||||
|
kvmclock runs away in KVM guests there (Linux guests panic with soft lockups) and
|
||||||
|
TSC deadline timers fire far too late (guests hang), so both are turned off on
|
||||||
|
QEMU's default CPU model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cmd = await _build_command_with_kvm(vm, clocksource)
|
||||||
|
assert "-enable-kvm" in cmd
|
||||||
|
assert cmd[cmd.index("-cpu") + 1] == "qemu64,kvmclock=off,tsc-deadline=off"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("clocksource", ["tsc", None])
|
||||||
|
async def test_build_command_kvmclock_kept_on_other_hosts(linux_platform, vm, clocksource):
|
||||||
|
|
||||||
|
cmd = await _build_command_with_kvm(vm, clocksource)
|
||||||
|
assert "-enable-kvm" in cmd
|
||||||
|
assert "-cpu" not in cmd
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_build_command_kvmclock_kept_without_kvm(vm):
|
||||||
|
|
||||||
|
with patch("gns3server.compute.qemu.qemu_vm.host_clocksource", return_value="hyperv_clocksource_tsc_page"):
|
||||||
|
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()):
|
||||||
|
cmd = await vm._build_command()
|
||||||
|
assert "-cpu" not in cmd
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_build_command_kvmclock_off_on_user_cpu(linux_platform, vm):
|
||||||
|
"""
|
||||||
|
A -cpu given in the additional options keeps its model and gets the features turned off.
|
||||||
|
"""
|
||||||
|
|
||||||
|
vm.options = "-nographic -cpu host"
|
||||||
|
cmd = await _build_command_with_kvm(vm, "hyperv_clocksource_tsc_page")
|
||||||
|
assert cmd.count("-cpu") == 1
|
||||||
|
assert cmd[cmd.index("-cpu") + 1] == "host,kvmclock=off,tsc-deadline=off"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cpu,expected",
|
||||||
|
[
|
||||||
|
("host,kvmclock=on", "host,kvmclock=on,tsc-deadline=off"),
|
||||||
|
("max,-kvmclock,+tsc-deadline", "max,-kvmclock,+tsc-deadline"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_build_command_kvmclock_user_choice_kept(linux_platform, vm, cpu, expected):
|
||||||
|
|
||||||
|
vm.options = f"-cpu {cpu}"
|
||||||
|
cmd = await _build_command_with_kvm(vm, "hyperv_clocksource_tsc_page")
|
||||||
|
assert cmd.count("-cpu") == 1
|
||||||
|
assert cmd[cmd.index("-cpu") + 1] == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_build_command_kvmclock_off_i386(linux_platform, vm):
|
||||||
|
|
||||||
|
vm._platform = "i386"
|
||||||
|
cmd = await _build_command_with_kvm(vm, "hyperv_clocksource_tsc_page")
|
||||||
|
assert cmd[cmd.index("-cpu") + 1] == "qemu32,kvmclock=off,tsc-deadline=off"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"setting,clocksource,kvmclock_off",
|
||||||
|
[
|
||||||
|
("disabled", "tsc", True),
|
||||||
|
("enabled", "hyperv_clocksource_tsc_page", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_build_command_kvmclock_setting(linux_platform, vm, setting, clocksource, kvmclock_off):
|
||||||
|
|
||||||
|
vm.manager.config.settings.Qemu.kvmclock = setting
|
||||||
|
cmd = await _build_command_with_kvm(vm, clocksource)
|
||||||
|
assert ("qemu64,kvmclock=off,tsc-deadline=off" in cmd) is kvmclock_off
|
||||||
|
|
||||||
|
|
||||||
|
def test_host_clocksource(monkeypatch, tmpdir):
|
||||||
|
|
||||||
|
path = tmpdir / "current_clocksource"
|
||||||
|
monkeypatch.setattr("gns3server.compute.qemu.qemu_vm.CLOCKSOURCE_PATH", str(path))
|
||||||
|
assert host_clocksource() is None
|
||||||
|
path.write_text("hyperv_clocksource_tsc_page\n", encoding="utf-8")
|
||||||
|
assert host_clocksource() == "hyperv_clocksource_tsc_page"
|
||||||
|
|
||||||
|
|
||||||
def test_hda_disk_image(vm, images_dir):
|
def test_hda_disk_image(vm, images_dir):
|
||||||
|
|
||||||
open(os.path.join(images_dir, "test1"), "w+").close()
|
open(os.path.join(images_dir, "test1"), "w+").close()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user