diff --git a/qubes/app.py b/qubes/app.py index a00ef4799..17778276c 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -1972,7 +1972,7 @@ def register_event_handlers(self, old_connection=None): if not vm.is_running(): vm.on_libvirt_domain_stopped() - def _domain_event_callback(self, _conn, domain, event, _detail, _opaque): + def _domain_event_callback(self, _conn, domain, event, detail, _opaque): """Generic libvirt event handler (virConnectDomainEventCallback), translate libvirt event into qubes.events. """ @@ -1984,32 +1984,7 @@ def _domain_event_callback(self, _conn, domain, event, _detail, _opaque): except KeyError: # ignore events for unknown domains return - - if event == libvirt.VIR_DOMAIN_EVENT_STOPPED: - vm.on_libvirt_domain_stopped() - elif event == libvirt.VIR_DOMAIN_EVENT_SUSPENDED: - try: - vm.fire_event("domain-paused") - except Exception: # pylint: disable=broad-except - self.log.exception( - "Uncaught exception from domain-paused handler " - "for domain %s", - vm.name, - ) - elif event == libvirt.VIR_DOMAIN_EVENT_RESUMED: - try: - if getattr(vm, "skip_unpause_event", False): - vm.skip_unpause_event = False - else: - asyncio.ensure_future( - vm.fire_event_async("domain-unpaused") - ) - except Exception: # pylint: disable=broad-except - self.log.exception( - "Uncaught exception from domain-unpaused handler " - "for domain %s", - vm.name, - ) + vm.on_libvirt_domain_lifecycle(event=event, detail=detail) @qubes.events.handler("domain-pre-delete") def on_domain_pre_deleted(self, event, vm): diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 19db93291..f735afdcb 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -5133,9 +5133,11 @@ async def coroutine_mock(*args, **kwargs): self.assertEventFired(self.emitter, "domain-volume-import-end") def test_900_current_state_default(self): - value = self.call_mgmt_func(b"admin.vm.CurrentState", b"test-vm1") + vm = self.app.domains["test-vm1"] + value = self.call_mgmt_func(b"admin.vm.CurrentState", vm.name.encode()) + maxmem = vm.maxmem * 1024 self.assertEqual( - value, "mem=0 mem_static_max=0 cputime=0 power_state=Halted" + value, f"mem=0 mem_static_max={maxmem} cputime=0 power_state=Halted" ) def test_901_current_state_changed(self): diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index f7668db7b..821c726f7 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -32,7 +32,8 @@ import string import subprocess -from typing import Awaitable +from contextlib import asynccontextmanager +from typing import Awaitable, Any, Literal import libvirt # pylint: disable=import-error import lxml.etree @@ -334,6 +335,49 @@ def _default_kernelopts(self): ) + extra_opts +LibvirtEvents = Literal[ + "DEFINED", + "UNDEFINED", + "STARTED", + "SUSPENDED", + "RESUMED", + "STOPPED", + "SHUTDOWN", + "PMSUSPENDED", + "CRASHED", +] + + +def _get_libvirt_event_dict() -> dict[int, dict[str, Any]]: + libvirt_event_dict = { + 0: {"event": "DEFINED", "pretty": "Defined", "details": {}}, + 1: {"event": "UNDEFINED", "pretty": "Undefined", "details": {}}, + 2: {"event": "STARTED", "pretty": "Started", "details": {}}, + 3: {"event": "SUSPENDED", "pretty": "Paused", "details": {}}, + 4: {"event": "RESUMED", "pretty": "Resumed", "details": {}}, + 5: {"event": "STOPPED", "pretty": "Halted", "details": {}}, + 6: {"event": "SHUTDOWN", "pretty": "Halting", "details": {}}, + 7: {"event": "PMSUSPENDED", "pretty": "Suspended", "details": {}}, + 8: {"event": "CRASHED", "pretty": "Crashed", "details": {}}, + } + libvirt_names = dir(libvirt) + event_prefix = "VIR_DOMAIN_EVENT_" + for event_number, event_dict in libvirt_event_dict.items(): + curr_event = event_prefix + str(event_dict["event"]) + assert event_number == getattr(libvirt, curr_event) + curr_event_prefix = curr_event + "_" + for name in libvirt_names: + if not name.startswith(curr_event_prefix): + continue + detail_id = int(getattr(libvirt, name)) + detail_pretty = str( + name[len(curr_event_prefix) :].capitalize().replace("_", " ") + ) + assert isinstance(event_dict["details"], dict) + event_dict["details"][detail_id] = detail_pretty + return libvirt_event_dict + + class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.LocalVM): """Base functionality of Qubes VM shared between all VMs. @@ -447,6 +491,15 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.LocalVM): :param subject: Event emitter (the qube object) :param event: Event name (``'domain-unpaused'``) + .. event:: domain-suspended (subject, event) + + Fired when the domain has been suspended. + + Handler for this event may be asynchronous. + + :param subject: Event emitter (the qube object) + :param event: Event name (``'domain-suspended'``) + .. event:: domain-resumed (subject, event) Fired when the domain has been resumed from suspend. @@ -722,12 +775,14 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.LocalVM): performing various post-installation setup. Handler for this event may be asynchronous. - """ + """ # pylint: disable=too-many-instance-attributes # # per-class properties # + libvirt_event_dict = _get_libvirt_event_dict() + #: directory in which domains of this class will reside dir_path_prefix = qubes.config.system_path["qubes_appvms_dir"] @@ -985,9 +1040,12 @@ def xid(self) -> int: Or not Xen, but ID. """ + if self._id != -1: + return self._id try: if self.is_running(): - return self.libvirt_domain.ID() + self._id: int = int(self.libvirt_domain.ID()) + return self._id return -1 except libvirt.libvirtError as e: @@ -1000,17 +1058,28 @@ def xid(self) -> int: @qubes.stateless_property def stubdom_uuid(self) -> str: + _stubdom_uuid = self._stubdom_uuid # type: ignore[has-type] + assert isinstance(_stubdom_uuid, str) + if _stubdom_uuid != "": + return _stubdom_uuid stubdom_xid = self.stubdom_xid if stubdom_xid == -1: return "" - stubdom_uuid = self.app.vmm.xs.read( + stubdom_uuid_bytes = self.app.vmm.xs.read( "", "/local/domain/{}/vm".format(stubdom_xid) ) - assert _vm_uuid_re.match(stubdom_uuid), "Invalid UUID in XenStore" - return stubdom_uuid[4:].decode("ascii", "strict") + assert _vm_uuid_re.match(stubdom_uuid_bytes), "Invalid UUID in XenStore" + stubdom_uuid = stubdom_uuid_bytes[4:].decode("ascii", "strict") + self._stubdom_uuid = stubdom_uuid + return self._stubdom_uuid @qubes.stateless_property def stubdom_xid(self) -> int: + _stubdom_xid = self._stubdom_xid # type: ignore[has-type] + assert isinstance(_stubdom_xid, int) + if _stubdom_xid != -1: + return _stubdom_xid + if self.virt_mode != "hvm": return -1 @@ -1026,7 +1095,9 @@ def stubdom_xid(self) -> int: if stubdom_xid_str is None or not stubdom_xid_str.isdigit(): return -1 - return int(stubdom_xid_str) + stubdom_xid = int(stubdom_xid_str) + self._stubdom_xid = stubdom_xid + return self._stubdom_xid @property def attached_volumes(self): @@ -1135,7 +1206,12 @@ def __init__(self, app, xml, volume_config=None, **kwargs): node_hvm.getparent().remove(node_hvm) super().__init__(app, xml, **kwargs) - self.__waiter = None + self._lifecycle_waiter = {} + libvirt_events = [ + event["event"] for event in self.libvirt_event_dict.values() + ] + for power_event in libvirt_events: + self._lifecycle_waiter[power_event] = None if volume_config is None: volume_config = {} @@ -1175,6 +1251,11 @@ def __init__(self, app, xml, volume_config=None, **kwargs): self._libvirt_domain = None self._qdb_connection = None + self._id = -1 + self._stubdom_xid = -1 + self._stubdom_uuid = "" + self._is_running = None + self._power_state = None # We assume a fully halted VM here. The 'domain-init' handler will # check if the VM is already running. @@ -1384,16 +1465,16 @@ async def _ensure_shutdown_handled(self): try: await self.fire_event_async("domain-stopped") await self.fire_event_async("domain-shutdown") - - if self.__waiter is not None: - self.__waiter.set_result(None) - self.__waiter = None + self.end_lifecycle_waiter(event="STOPPED") except Exception as e: - if self.__waiter is not None: - self.__waiter.set_exception(e) - self.__waiter = None + self.end_lifecycle_waiter(event="STOPPED", exc=e) raise + async def notify_failed_startup(self, exc: Exception): + self.log.error("Start failed: %s", str(exc)) + # let anyone receiving domain-pre-start know that startup failed + await self.fire_event_async("domain-start-failed", reason=str(exc)) + async def start( self, start_guid=True, notify_function=None, mem_required=None ): @@ -1427,6 +1508,7 @@ async def start( ) self.log.info("Starting qube {}".format(self.name)) + self._power_state = "Transient" try: await self.fire_event_async( @@ -1436,10 +1518,8 @@ async def start( mem_required=mem_required, ) except Exception as exc: - self.log.error("Start failed: %s", str(exc)) - await self.fire_event_async( - "domain-start-failed", reason=str(exc) - ) + await self.notify_failed_startup(exc=exc) + self._power_state = "Halted" raise qmemman_client = None @@ -1477,11 +1557,8 @@ async def start( await self.storage.start() except Exception as exc: - self.log.error("Start failed: %s", str(exc)) - # let anyone receiving domain-pre-start know that startup failed - await self.fire_event_async( - "domain-start-failed", reason=str(exc) - ) + await self.notify_failed_startup(exc=exc) + self._power_state = "Halted" if qmemman_client: qmemman_client.close() raise @@ -1493,9 +1570,10 @@ async def start( self._update_libvirt_domain() - self.libvirt_domain.createWithFlags( - libvirt.VIR_DOMAIN_START_PAUSED - ) + async with self.change_libvirt_state(event="STARTED"): + self.libvirt_domain.createWithFlags( + libvirt.VIR_DOMAIN_START_PAUSED, + ) self.create_xs_entries() # the above allocates xid, lets announce that @@ -1522,18 +1600,13 @@ async def start( "Failed to start an HVM qube with PCI devices assigned " "- hardware does not support IOMMU/VT-d/AMD-Vi" ) - self.log.error("Start failed: %s", str(exc)) - await self.fire_event_async( - "domain-start-failed", reason=str(exc) - ) + await self.notify_failed_startup(exc=exc) + self._power_state = "Halted" await self.storage.stop() raise exc except Exception as exc: - self.log.error("Start failed: %s", str(exc)) - # let anyone receiving domain-pre-start know that startup failed - await self.fire_event_async( - "domain-start-failed", reason=str(exc) - ) + await self.notify_failed_startup(exc=exc) + self._power_state = "Halted" await self.storage.stop() raise @@ -1560,12 +1633,9 @@ async def start( self.start_qdb_watch() self.log.info("Activating qube") - await self.fire_event_async( - "domain-pre-unpaused", pre_event=True - ) self.skip_unpause_event = True - self.libvirt_domain.resume() - await self.fire_event_async("domain-unpaused") + async with self.change_libvirt_state(event="RESUMED"): + self.libvirt_domain.resume() if ( self.virt_mode == "hvm" @@ -1576,16 +1646,13 @@ async def start( await self.start_qrexec_daemon(stubdom=True) await self.start_qrexec_daemon() + self._power_state = "Running" await self.fire_event_async( "domain-start", start_guid=start_guid ) except Exception as exc: # pylint: disable=bare-except - self.log.error("Start failed: %s", str(exc)) - # let anyone receiving domain-pre-start know that startup failed - await self.fire_event_async( - "domain-start-failed", reason=str(exc) - ) + await self.notify_failed_startup(exc=exc) # This avoids losing the exception if an exception is # raised in self.kill(), because the vm is not # running or paused @@ -1597,6 +1664,137 @@ async def start( return self + @asynccontextmanager + async def change_libvirt_state( + self, + event: LibvirtEvents, + wait: bool = True, + timeout: int | float | None = None, + ): + """ + Asynchronously wait for libvirt method to complete. + """ + if self._lifecycle_waiter[event] is None: + self._lifecycle_waiter[event] = ( + asyncio.get_running_loop().create_future() + ) + waiter = self._lifecycle_waiter[event] + try: + yield + finally: + if wait: + await asyncio.wait_for(waiter, timeout=timeout) + + def end_lifecycle_waiter( + self, event: LibvirtEvents, exc: BaseException | None = None + ): + """ + Set power state waiter's result. + """ + if self._lifecycle_waiter[event] is not None: + if exc: + self._lifecycle_waiter[event].set_exception(exc) + else: + self._lifecycle_waiter[event].set_result(None) + self._lifecycle_waiter[event] = None + + def on_libvirt_domain_lifecycle(self, event: int, detail: int) -> None: + """Handle VIR_DOMAIN_EVENT_ID_LIFECYCLE events from libvirt. + + This is not a Qubes event handler. + """ + + pretty_event = self.libvirt_event_dict[event]["pretty"] + pretty_detail = self.libvirt_event_dict[event]["details"][detail] + + self.log.info( + "Libvirt event received for domain: %s: %s", + pretty_event, + pretty_detail, + ) + if event == libvirt.VIR_DOMAIN_EVENT_DEFINED: + self.on_libvirt_domain_defined() + elif event == libvirt.VIR_DOMAIN_EVENT_STARTED: + self.on_libvirt_domain_started(detail=detail) + elif event == libvirt.VIR_DOMAIN_EVENT_PMSUSPENDED: + self.on_libvirt_domain_pmsuspended() + elif event == libvirt.VIR_DOMAIN_EVENT_SUSPENDED: + self.on_libvirt_domain_suspended() + elif event == libvirt.VIR_DOMAIN_EVENT_RESUMED: + self.on_libvirt_domain_resumed() + elif event == libvirt.VIR_DOMAIN_EVENT_STOPPED: + self.on_libvirt_domain_stopped() + + def on_libvirt_domain_defined(self): + """Handle VIR_DOMAIN_EVENT_DEFINED event from libvirt. + + This is not a Qubes event handler. + """ + if self._is_running is None: + self._is_running = False + if self._power_state is None: + self._power_state = "Halted" + self.end_lifecycle_waiter(event="DEFINED") + + def on_libvirt_domain_started(self, detail): + """Handle VIR_DOMAIN_EVENT_STARTED event from libvirt when booted. + + This is not a Qubes event handler. + """ + self._is_running = True + if detail == libvirt.VIR_DOMAIN_EVENT_STARTED_BOOTED: + # Let power state be "Transient/Starting" if set. + if self._power_state is None: + self._power_state = "Running" + elif detail == libvirt.VIR_DOMAIN_EVENT_STARTED_WAKEUP: + self._power_state = "Running" + self.end_lifecycle_waiter(event="STARTED") + + def on_libvirt_domain_suspended(self): + """Handle VIR_DOMAIN_EVENT_SUSPENDED events from libvirt. + + This is not a Qubes event handler. + """ + self._power_state = "Paused" + event = "domain-paused" + try: + self.fire_event(event) + except Exception: # pylint: disable=broad-except + self.log.exception("Uncaught exception from %s handler ", event) + self.end_lifecycle_waiter(event="SUSPENDED") + + def on_libvirt_domain_pmsuspended(self): + """Handle VIR_DOMAIN_EVENT_PMSUSPENDED events from libvirt. + + This is not a Qubes event handler. + """ + self._power_state = "Suspended" + event = "domain-suspended" + try: + self.fire_event(event) + except Exception: # pylint: disable=broad-except + self.log.exception("Uncaught exception from %s handler ", event) + self.end_lifecycle_waiter(event="PMSUSPENDED") + + def on_libvirt_domain_resumed(self): + """Handle VIR_DOMAIN_EVENT_RESUMED events from libvirt. + + This is not a Qubes event handler. + """ + if self.is_fully_usable(): + self._power_state = "Running" + else: + self._power_state = "Transient" + event = "domain-unpaused" + try: + if getattr(self, "skip_unpause_event", False): + self.skip_unpause_event = False + else: + asyncio.ensure_future(self.fire_event_async(event)) + except Exception: # pylint: disable=broad-except + self.log.exception("Uncaught exception from %s handler ", event) + self.end_lifecycle_waiter(event="RESUMED") + def on_libvirt_domain_stopped(self): """Handle VIR_DOMAIN_EVENT_STOPPED events from libvirt. @@ -1604,14 +1802,11 @@ def on_libvirt_domain_stopped(self): and synchronization with start() and then emits Qubes events. """ - state = self.get_power_state() - if state not in ["Halted", "Crashed", "Dying"]: - self.log.warning( - "Stopped event from libvirt received," - " but domain is in state {}!".format(state) - ) - # ignore this unexpected event - return + self._id = -1 + self._stubdom_xid = -1 + self._stubdom_uuid = "" + self._is_running = False + self._power_state = "Halted" if self._domain_stopped_event_received: # ignore this event - already triggered by subsequent start() @@ -1631,19 +1826,14 @@ async def _domain_stopped_coro(self): # an exception gets thrown. self._domain_stopped_event_handled = True - while self.get_power_state() == "Dying": + while self.get_power_state() == "Halting": await asyncio.sleep(0.25) try: await self.fire_event_async("domain-stopped") await self.fire_event_async("domain-shutdown") - - if self.__waiter is not None: - self.__waiter.set_result(None) - self.__waiter = None + self.end_lifecycle_waiter(event="STOPPED") except Exception as e: - if self.__waiter is not None: - self.__waiter.set_exception(e) - self.__waiter = None + self.end_lifecycle_waiter(event="STOPPED", exc=e) raise @qubes.events.handler("domain-stopped") @@ -1672,6 +1862,7 @@ async def shutdown(self, force=False, wait=False, timeout=None): if self.is_halted(): raise qubes.exc.QubesVMNotStartedError(self) + old_power_state = self._power_state try: await self.fire_event_async( "domain-pre-shutdown", pre_event=True, force=force @@ -1681,22 +1872,24 @@ async def shutdown(self, force=False, wait=False, timeout=None): if self.is_paused() and not force and not is_preload: raise qubes.exc.QubesVMNotRunningError(self) - if self.__waiter is None: - self.__waiter = asyncio.get_running_loop().create_future() - waiter = self.__waiter - if self.is_paused(): - self.libvirt_domain.destroy() + return await self.kill() + + self._power_state = "Halting" + # Some libvirt actions have a global lock on a domain, blocking + # a lot of libvirt operations and even qubesd. When possible to + # act without it, do so to avoid the whole qubesd hanging. + if self.app.vmm.is_xen: + command = ["xl", "shutdown", "-F", self.name] else: - # Some libvirt actions have a global lock on a domain, blocking - # a lot of libvirt operations and even qubesd. When possible to - # act without it, do so to avoid the whole qubesd hanging. - if self.app.vmm.is_xen: - command = ["xl", "shutdown", "-F", self.name] - else: - uri = self.app.vmm.libvirt_conn_uri - command = ["virsh", "-c", uri, "shutdown", self.name] - try: + uri = self.app.vmm.libvirt_conn_uri + command = ["virsh", "-c", uri, "shutdown", self.name] + if wait and timeout is None: + timeout = self.shutdown_timeout + try: + async with self.change_libvirt_state( + event="STOPPED", wait=wait, timeout=timeout + ): proc = await asyncio.create_subprocess_exec( *command, stdin=asyncio.subprocess.DEVNULL, @@ -1711,25 +1904,21 @@ async def shutdown(self, force=False, wait=False, timeout=None): command, output=stdout, ) - except subprocess.CalledProcessError as e: - self.log.error( - "Attempted {!s} with subprocess but exited with " - "error code: {!s}: {!r}".format( - "shutdown", - e.returncode, - qubes.utils.sanitize_stderr_for_log(e.output), - ) + except asyncio.TimeoutError: + raise qubes.exc.QubesVMShutdownTimeoutError(self) + except subprocess.CalledProcessError as e: + self.log.error( + "Attempted {!s} with subprocess but exited with " + "error code: {!s}: {!r}".format( + "shutdown", + e.returncode, + qubes.utils.sanitize_stderr_for_log(e.output), ) - raise qubes.exc.QubesVMShutdownTimeoutError(self) + ) + raise qubes.exc.QubesVMShutdownTimeoutError(self) - if wait: - if timeout is None: - timeout = self.shutdown_timeout - try: - await asyncio.wait_for(waiter, timeout=timeout) - except asyncio.TimeoutError: - raise qubes.exc.QubesVMShutdownTimeoutError(self) except Exception as ex: + self._power_state = old_power_state await self.fire_event_async( "domain-shutdown-failed", reason=str(ex) ) @@ -1747,19 +1936,19 @@ async def kill(self): if not self.is_running() and not self.is_paused(): raise qubes.exc.QubesVMNotStartedError(self) - if self.__waiter is None: - self.__waiter = asyncio.get_running_loop().create_future() - waiter = self.__waiter - + self._power_state = "Halting" try: - self.libvirt_domain.destroy() + async with self.change_libvirt_state(event="STOPPED"): + self.libvirt_domain.destroy() except libvirt.libvirtError as e: if e.get_error_code() == libvirt.VIR_ERR_OPERATION_INVALID: + self._is_running = False + self._power_state = "Halted" raise qubes.exc.QubesVMNotStartedError(self) + self._is_running = None + self._power_state = None raise - await waiter - async def suspend(self): """Suspend (pause) domain. @@ -1790,14 +1979,16 @@ async def suspend(self): qubes.config.suspend_timeout, ) try: - self.libvirt_domain.pMSuspendForDuration( - libvirt.VIR_NODE_SUSPEND_TARGET_MEM, 0, 0 - ) + async with self.change_libvirt_state(event="PMSUSPENDED"): + self.libvirt_domain.pMSuspendForDuration( + libvirt.VIR_NODE_SUSPEND_TARGET_MEM, + 0, + 0, + ) except libvirt.libvirtError as e: if e.get_error_code() == libvirt.VIR_ERR_OPERATION_UNSUPPORTED: # OS inside doesn't support full suspend, just pause it - await self.fire_event_async("domain-pre-paused", pre_event=True) - self.libvirt_domain.suspend() + await self.pause() else: self.log.warning("Failed to suspend qube") raise @@ -1811,7 +2002,8 @@ async def pause(self): raise qubes.exc.QubesVMNotRunningError(self) await self.fire_event_async("domain-pre-paused", pre_event=True) - self.libvirt_domain.suspend() + async with self.change_libvirt_state(event="SUSPENDED"): + self.libvirt_domain.suspend() return self @@ -1823,7 +2015,8 @@ async def resume(self): """ if self.get_power_state() == "Suspended": - self.libvirt_domain.pMWakeup() + async with self.change_libvirt_state(event="STARTED"): + self.libvirt_domain.pMWakeup() if self.features.check_with_template("qrexec", False): try: await asyncio.wait_for( @@ -1858,7 +2051,8 @@ async def unpause(self): await self.fire_event_async("domain-pre-unpaused", pre_event=True) self.skip_unpause_event = True - self.libvirt_domain.resume() + async with self.change_libvirt_state(event="RESUMED"): + self.libvirt_domain.resume() await self.fire_event_async("domain-unpaused") return self @@ -2394,6 +2588,13 @@ def get_default_user(self): return bootmode_default_user def get_power_state(self): + """ + Get cached power state as a string. + """ + self.set_power_state() + return self._power_state + + def set_power_state(self): """Return power state description string. Return value may be one of those: @@ -2401,88 +2602,167 @@ def get_power_state(self): =============== ======================================================== return value meaning =============== ======================================================== - ``'Halted'`` Machine is not active. - ``'Transient'`` Machine is running, but does not have :program:`guid` - or :program:`qrexec` available. - ``'Running'`` Machine is ready and running. - ``'Paused'`` Machine is paused. - ``'Suspended'`` Machine is S3-suspended. - ``'Halting'`` Machine is in process of shutting down. - ``'Dying'`` Machine is still in process of shutting down. - ``'Crashed'`` Machine crashed and is unusable, probably because of + ``"Halted"`` Machine is not active. + ``"Transient"`` Machine is in process of starting. This state will be + deprecated in the future in favor of ``"Starting"``. + ``"Running"`` Machine is ready and running. + ``"Paused"`` Machine is paused. + ``"Suspended"`` Machine is S3-suspended. + ``"Halting"`` Machine is in process of shutting down. + ``"Crashed"`` Machine crashed and is unusable, probably because of bug in dom0. - ``'NA'`` Machine is in unknown state (most likely libvirt domain + ``"NA"`` Machine is in unknown state (most likely libvirt domain is undefined). =============== ======================================================== - FIXME: graph below may be incomplete and wrong. Click on method name to - see its documentation. + Click on method name to see its documentation. .. graphviz:: digraph { - node [fontname="sans-serif"]; - edge [fontname="mono"]; - - - Halted; - NA; - Dying; - Crashed; - Transient; - Halting; - Running; - Paused [color=gray75 fontcolor=gray75]; - Suspended; - + rankdir=LR; + + node [ + fontname="sans-serif", + shape=box, + style="rounded,filled", + color="darkslategray", + fillcolor="aliceblue", + ]; + + edge [ + fontname="mono", + color="lightslategray", + ]; + + NA [fillcolor="lightgray", color="dimgray"]; + Halted [fillcolor="lightgray", color="black"]; + Transient [fillcolor="lightblue", color="blue"]; + Running [fillcolor="palegreen", color="forestgreen"]; + Paused [fillcolor="lightyellow", color="yellow"]; + Suspended [fillcolor="lavenderblush", color="purple"]; + Crashed [fillcolor="mistyrose", color="red"]; + Halting [fillcolor="lightcoral", color="red"]; + + // From NA/Halted/Transient NA -> Halted; Halted -> NA [constraint=false]; + Halted -> Transient -> Running [ + label="start()", + URL="#qubes.vm.qubesvm.QubesVM.start", + color="blue", + constraint=true, + ]; + Transient -> Halted [ + label="domain-start-failed", + URL="#event-domain-start-failed", + color="black", + constraint=true, + ]; + + // From Running + Running -> Halting [ + label="shutdown()", + URL="#qubes.vm.qubesvm.QubesVM.shutdown", + constraint=false, + color="red", + ]; + Running -> Halting [ + label="kill()", + URL="#qubes.vm.qubesvm.QubesVM.kill", + color="red", + constraint=false, + ]; + Running -> Paused [ + label="pause()", + URL="#qubes.vm.qubesvm.QubesVM.pause", + color="yellow", + constraint=true, + ]; + Running -> Suspended [ + label="suspend()", + URL="#qubes.vm.qubesvm.QubesVM.suspend", + color="purple", + constraint=true, + ]; + Running -> Crashed [ + color="black", + constraint=false, + ]; + Running -> Halted [ + label="shutdown without API", + color="black", + constraint=false, + ]; + + // From Paused/Suspended + Paused -> Running [ + label="unpause()", + URL="#qubes.vm.qubesvm.QubesVM.unpause", + color="yellow", + constraint=false, + ]; + Paused -> Halting [ + label="kill()", + URL="#qubes.vm.qubesvm.QubesVM.kill", + color="red4", + constraint=true, + ]; + Suspended -> Running [ + label="resume()", + URL="#qubes.vm.qubesvm.QubesVM.resume", + color="purple", + constraint=false, + ]; + Suspended -> Halting [ + label="kill()", + URL="#qubes.vm.qubesvm.QubesVM.kill", + color="red4", + constraint=true, + ]; + + // From Halting + Halting -> Halted [ + color="black", + constraint=true, + ]; + Halting -> Running [ + label="domain-shutdown-failed", + URL="#event-domain-shutdown-failed", + color="blue", + constraint=true, + ]; + + // From Crashed + Crashed -> Halted [ + color="black", + constraint=false, + ]; + + + subgraph cluster_inactive { + label="Inactive (no ID)" + color="gray" + { rank=""; Halted; NA; } + } - Halted -> Transient - [xlabel="start()" URL="#qubes.vm.qubesvm.QubesVM.start"]; - Transient -> Running; - - Running -> Halting - [xlabel="shutdown()" - URL="#qubes.vm.qubesvm.QubesVM.shutdown" - constraint=false]; - Halting -> Dying -> Halted [constraint=false]; - - /* cosmetic, invisible edges to put rank constraint */ - Dying -> Halting [style="invis"]; - Halting -> Transient [style="invis"]; - - Running -> Halted - [label="kill()" - URL="#qubes.vm.qubesvm.QubesVM.kill" - constraint=false]; - - Running -> Crashed [constraint=false]; - Crashed -> Halted [constraint=false]; - - Running -> Paused - [label="pause()" URL="#qubes.vm.qubesvm.QubesVM.pause" - color=gray75 fontcolor=gray75]; - Running -> Suspended - [label="suspend()" URL="#qubes.vm.qubesvm.QubesVM.suspend" - color=gray50 fontcolor=gray50]; - Paused -> Running - [label="unpause()" URL="#qubes.vm.qubesvm.QubesVM.unpause" - color=gray75 fontcolor=gray75]; - Suspended -> Running - [label="resume()" URL="#qubes.vm.qubesvm.QubesVM.resume" - color=gray50 fontcolor=gray50]; - - Running -> Suspended - [label="suspend()" URL="#qubes.vm.qubesvm.QubesVM.suspend"]; - Suspended -> Running - [label="resume()" URL="#qubes.vm.qubesvm.QubesVM.resume"]; - - - { rank=source; Halted NA }; - { rank=same; Transient Halting }; - { rank=same; Crashed Dying }; - { rank=sink; Paused Suspended }; + subgraph cluster_active { + label="Active (has ID)" + color="dodgerblue" + { rank="same"; Running; } + subgraph cluster_transition { + label="Transition" + style="dashed" + color="lightskyblue" + { rank="sink"; Transient; Halting; Crashed; } + } + subgraph cluster_suspended { + label="Suspended" + style="rounded" + color="plum" + { rank="sink"; Paused; Suspended; } + } + } } .. seealso:: @@ -2494,49 +2774,36 @@ def get_power_state(self): Libvirt's enum describing precise state of a domain. """ # pylint: disable=too-many-return-statements - # don't try to define libvirt domain, if it isn't there, VM surely - # isn't running - # reason for this "if": allow vm.is_running() in PCI (or other - # device) extension while constructing libvirt XML if self.app.vmm.offline_mode: - return "Halted" - if self._libvirt_domain is None: - try: - self._libvirt_domain = self.app.vmm.libvirt_conn.lookupByUUID( - self.uuid.bytes - ) - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: - return "Halted" - raise + self._power_state = "Halted" + return + + if self.libvirt_domain is None: + self._power_state = "Halted" + return - libvirt_domain = self.libvirt_domain - if libvirt_domain is None: - return "Halted" + if self._power_state not in [None, "NA"]: + return try: - if libvirt_domain.isActive(): - state_dict = { - libvirt.VIR_DOMAIN_PAUSED: "Paused", # 0x3 - libvirt.VIR_DOMAIN_SHUTDOWN: "Halting", # 0x4 - libvirt.VIR_DOMAIN_SHUTOFF: "Dying", # 0x5 - libvirt.VIR_DOMAIN_CRASHED: "Crashed", # 0x6 - libvirt.VIR_DOMAIN_PMSUSPENDED: "Suspended", # 0x7 - } - state = libvirt_domain.state()[0] - if state in state_dict: - return state_dict[state] - if not self.is_fully_usable(): - return "Transient" - return "Running" # 0x1 - - return "Halted" + if not self.is_running(): + self._power_state = "Halted" + return + self._power_state = "Running" + state = self.libvirt_domain.state()[0] + state_dict = { + libvirt.VIR_DOMAIN_PAUSED: "Paused", # 0x3 + libvirt.VIR_DOMAIN_SHUTDOWN: "Halting", # 0x4 + libvirt.VIR_DOMAIN_SHUTOFF: "Halted", # 0x5 + libvirt.VIR_DOMAIN_CRASHED: "Crashed", # 0x6 + libvirt.VIR_DOMAIN_PMSUSPENDED: "Suspended", # 0x7 + } + if state in state_dict: + self._power_state = state_dict[state] except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: - return "Halted" - raise - - assert False + if e.get_error_code() != libvirt.VIR_ERR_NO_DOMAIN: + raise + self._power_state = "NA" def is_halted(self): """ Check whether this domain's state is 'Halted' @@ -2547,7 +2814,8 @@ def is_halted(self): return self.get_power_state() == "Halted" def is_running(self): - """Check whether this domain is running. + """Check whether this domain is active. This is not the same as + ``Running`` power state, it only means the qube has a libvirt ID. :returns: :py:obj:`True` if this domain is started, \ :py:obj:`False` otherwise. @@ -2557,21 +2825,13 @@ def is_running(self): if self.app.vmm.offline_mode: return False - # don't try to define libvirt domain, if it isn't there, VM surely - # isn't running - # reason for this "if": allow vm.is_running() in PCI (or other - # device) extension while constructing libvirt XML - if self._libvirt_domain is None: - try: - self._libvirt_domain = self.app.vmm.libvirt_conn.lookupByUUID( - self.uuid.bytes - ) - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: - return False - raise + if self.libvirt_domain is None: + self._is_running = False + return self._is_running - return bool(self.libvirt_domain.isActive()) + if self._is_running is None: + self._is_running = bool(self.libvirt_domain.isActive()) + return self._is_running def is_paused(self): """Check whether this domain is paused. @@ -2581,10 +2841,7 @@ def is_paused(self): :rtype: bool """ - return ( - self.libvirt_domain - and self.libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_PAUSED - ) + return self._power_state == "Paused" def is_qrexec_running(self, stubdom=False): """Check whether qrexec for this domain is available. @@ -2622,11 +2879,9 @@ def get_mem(self): :returns: Memory assigned in KiB. :rtype: int """ - if self.libvirt_domain is None: + if not self.is_running(): return 0 try: - if not self.libvirt_domain.isActive(): - return 0 return self.libvirt_domain.memoryStats()["actual"] except libvirt.libvirtError as e: if e.get_error_code() in ( @@ -2647,24 +2902,7 @@ def get_mem_static_max(self): :returns: Memory limit in KiB. :rtype: int """ - if self.libvirt_domain is None: - return 0 - try: - if not self.libvirt_domain.isActive(): - return 0 - return self.maxmem * 1024 - except libvirt.libvirtError as e: - if e.get_error_code() in ( - # qube no longer exists - libvirt.VIR_ERR_NO_DOMAIN, - # libxl_domain_info failed (race condition from isActive) - libvirt.VIR_ERR_INTERNAL_ERROR, - ): - return 0 - self.log.exception( - "libvirt error code: {!r}".format(e.get_error_code()) - ) - raise + return self.maxmem * 1024 def get_cputime(self): """Get total CPU time burned by this domain since start. @@ -2678,11 +2916,11 @@ def get_cputime(self): if self.libvirt_domain is None: return 0 - if not self.libvirt_domain.isActive(): + if not self.is_running(): return 0 try: - if not self.libvirt_domain.isActive(): + if not self.is_running(): return 0 # this does not work, because libvirt diff --git a/test-packages/libvirt.py b/test-packages/libvirt.py index 9bc533dbd..763c415e5 100644 --- a/test-packages/libvirt.py +++ b/test-packages/libvirt.py @@ -40,3 +40,65 @@ def registerErrorHandler(f, ctx): VIR_DOMAIN_PMSUSPENDED = 7 VIR_ERR_NO_DOMAIN = 0 + +# Begin virDomainEventType +VIR_DOMAIN_EVENT_CRASHED = 8 +VIR_DOMAIN_EVENT_CRASHED_CRASHLOADED = 1 +VIR_DOMAIN_EVENT_CRASHED_PANICKED = 0 + +VIR_DOMAIN_EVENT_DEFINED = 0 +VIR_DOMAIN_EVENT_DEFINED_ADDED = 0 +VIR_DOMAIN_EVENT_DEFINED_FROM_SNAPSHOT = 3 +VIR_DOMAIN_EVENT_DEFINED_RENAMED = 2 +VIR_DOMAIN_EVENT_DEFINED_UPDATED = 1 + +VIR_DOMAIN_EVENT_PMSUSPENDED = 7 +VIR_DOMAIN_EVENT_PMSUSPENDED_DISK = 1 +VIR_DOMAIN_EVENT_PMSUSPENDED_MEMORY = 0 + +VIR_DOMAIN_EVENT_RESUMED = 4 +VIR_DOMAIN_EVENT_RESUMED_FROM_SNAPSHOT = 2 +VIR_DOMAIN_EVENT_RESUMED_MIGRATED = 1 +VIR_DOMAIN_EVENT_RESUMED_POSTCOPY = 3 +VIR_DOMAIN_EVENT_RESUMED_POSTCOPY_FAILED = 4 +VIR_DOMAIN_EVENT_RESUMED_UNPAUSED = 0 + +VIR_DOMAIN_EVENT_SHUTDOWN = 6 +VIR_DOMAIN_EVENT_SHUTDOWN_FINISHED = 0 +VIR_DOMAIN_EVENT_SHUTDOWN_GUEST = 1 +VIR_DOMAIN_EVENT_SHUTDOWN_HOST = 2 + +VIR_DOMAIN_EVENT_STARTED = 2 +VIR_DOMAIN_EVENT_STARTED_BOOTED = 0 +VIR_DOMAIN_EVENT_STARTED_FROM_SNAPSHOT = 3 +VIR_DOMAIN_EVENT_STARTED_MIGRATED = 1 +VIR_DOMAIN_EVENT_STARTED_RESTORED = 2 +VIR_DOMAIN_EVENT_STARTED_WAKEUP = 4 + +VIR_DOMAIN_EVENT_STOPPED = 5 +VIR_DOMAIN_EVENT_STOPPED_CRASHED = 2 +VIR_DOMAIN_EVENT_STOPPED_DESTROYED = 1 +VIR_DOMAIN_EVENT_STOPPED_FAILED = 5 +VIR_DOMAIN_EVENT_STOPPED_FROM_SNAPSHOT = 6 +VIR_DOMAIN_EVENT_STOPPED_MIGRATED = 3 +VIR_DOMAIN_EVENT_STOPPED_SAVED = 4 +VIR_DOMAIN_EVENT_STOPPED_SHUTDOWN = 0 + +VIR_DOMAIN_EVENT_SUSPENDED = 3 +VIR_DOMAIN_EVENT_SUSPENDED_API_ERROR = 6 +VIR_DOMAIN_EVENT_SUSPENDED_FROM_SNAPSHOT = 5 +VIR_DOMAIN_EVENT_SUSPENDED_IOERROR = 2 +VIR_DOMAIN_EVENT_SUSPENDED_MIGRATED = 1 +VIR_DOMAIN_EVENT_SUSPENDED_PAUSED = 0 +VIR_DOMAIN_EVENT_SUSPENDED_POSTCOPY = 7 +VIR_DOMAIN_EVENT_SUSPENDED_POSTCOPY_FAILED = 8 +VIR_DOMAIN_EVENT_SUSPENDED_RESTORED = 4 +VIR_DOMAIN_EVENT_SUSPENDED_WATCHDOG = 3 + +VIR_DOMAIN_EVENT_TRAY_CHANGE_CLOSE = 1 +VIR_DOMAIN_EVENT_TRAY_CHANGE_OPEN = 0 + +VIR_DOMAIN_EVENT_UNDEFINED = 1 +VIR_DOMAIN_EVENT_UNDEFINED_REMOVED = 0 +VIR_DOMAIN_EVENT_UNDEFINED_RENAMED = 1 +# End virDomainEventType