Introduce a new QEMU hook operation "stop" that is called before a
domain is terminated (via virsh shutdown or virsh destroy). This allows
external scripts to perform cleanup or veto the stop process by
returning a non-zero exit code. The hook is called as:

    /etc/libvirt/hooks/qemu <guest_name> stop begin -

The full domain XML is provided on stdin.

Example qemu hook that protects domains from being stopped (to be
dropped in /etc/libfirt/hooks/qemu.d/10-vm-protection or similar):

```bash
#!/bin/bash

# Protection hook - vetos domain stops for domains listed in
# /etc/libvirt/protected-domains

set -euo pipefail

readonly PROTECT_DIR="/etc/libvirt"
readonly DOMAIN_NAME="${1:-}"
readonly OPERATION="${2:-}"

# Only act on stop operation
if [[ "$OPERATION" != "stop" ]]; then
    exit 0
fi

readonly PROTECT_LIST="${PROTECT_DIR}/protected-domains"

if [[ -f "$PROTECT_LIST" ]]; then
    # Read file, strip comments and empty lines, check for exact domain match
    if grep -Fxq -- "$DOMAIN_NAME" <(sed -e 's/#.*$//' -e '/^[[:space:]]*$/d' 
"$PROTECT_LIST"); then
        logger -t libvirt-hook-test "20-protection: BLOCKING stop for protected 
domain '$DOMAIN_NAME'"
        echo "$(date '+%Y-%m-%d %H:%M:%S'): BLOCKED stop for '$DOMAIN_NAME'" \
          >> /tmp/libvirt-test-logs/protection.log
        printf 'vm-protection: stop blocked for %s\n' "$DOMAIN_NAME" >&2
        exit 1
    fi
fi

exit 0
```

With a list of domains to protect in /etc/libvirt/protected-domains.

With the above hook in place, attempting to destroy a domain listed in
/etc/libvirt/protected-domains will fail:

```
> virsh list --all
 Id   Name           State
-------------------------------
 2    protected-vm   running
 -    test-vm        shut off

> virsh destroy protected-vm
error: Failed to destroy domain 'protected-vm'
error: Hook script execution failed: internal error: Child process (LC_ALL=C 
LD_LIBRARY_PATH=/opt/libvirt-test/lib/x86_64-linux-gnu:/opt/libvirt-test/lib 
PATH=/usr/local/sbin:/usr/local/bin:/usr/bin HOME=/root USER=root LOGNAME=root 
/opt/libvirt-test/etc/libvirt/hooks/qemu.d/20-protection protected-vm stop 
begin -) unexpected exit status 1: vm-protection: stop blocked for protected-vm

> virsh list --all
 Id   Name           State
-------------------------------
 2    protected-vm   running
 -    test-vm        shut off
```

Signed-off-by: Mitchel Humpherys <[email protected]>
---
 docs/hooks.rst          | 14 +++++++++++---
 src/qemu/qemu_process.c | 17 +++++++++++++++++
 src/util/virhook.c      |  1 +
 src/util/virhook.h      |  1 +
 4 files changed, 30 insertions(+), 3 deletions(-)

diff --git a/docs/hooks.rst b/docs/hooks.rst
index e1745b8cc7..5ae03a36cd 100644
--- a/docs/hooks.rst
+++ b/docs/hooks.rst
@@ -202,9 +202,17 @@ operation. There is no specific operation to indicate a 
"restart" is occurring.
 
       /etc/libvirt/hooks/qemu guest_name started begin -
 
--  When a QEMU guest is stopped, the qemu hook script is called in two
-   locations, to match the startup. First, :since:`since 0.8.0`, the hook is
-   called before libvirt restores any labels:
+-  When a QEMU guest is stopped, the qemu hook script is called in three
+   locations, to match the startup. The first location, :since:`since 11.10.0`,
+   is called before the domain is stopped. This allows the hook to perform
+   cleanup tasks or veto the stop operation by returning a non-zero exit code:
+
+   ::
+
+      /etc/libvirt/hooks/qemu guest_name stop begin -
+
+   The second location, :since:`since 0.8.0`, is called after the QEMU process
+   has terminated but before libvirt restores any labels:
 
    ::
 
diff --git a/src/qemu/qemu_process.c b/src/qemu/qemu_process.c
index 0e50cd1ccc..fa2b691d99 100644
--- a/src/qemu/qemu_process.c
+++ b/src/qemu/qemu_process.c
@@ -9015,6 +9015,23 @@ qemuProcessBeginStopJob(virDomainObj *vm,
     qemuDomainObjPrivate *priv = vm->privateData;
     unsigned int killFlags = forceKill ? VIR_QEMU_PROCESS_KILL_FORCE : 0;
 
+    /* call stop hook if present */
+    if (virHookPresent(VIR_HOOK_DRIVER_QEMU)) {
+        virQEMUDriver *driver = priv->driver;
+        g_autofree char *xml = qemuDomainDefFormatXML(driver, NULL, vm->def, 
0);
+        int hookret;
+
+        if (!xml)
+            return -1;
+
+        hookret = virHookCall(VIR_HOOK_DRIVER_QEMU, vm->def->name,
+                              VIR_HOOK_QEMU_OP_STOP, VIR_HOOK_SUBOP_BEGIN,
+                              NULL, xml, NULL);
+
+        if (hookret < 0)
+            return -1;
+    }
+
     /* We need to prevent monitor EOF callback from doing our work (and
      * sending misleading events) while the vm is unlocked inside
      * BeginJob/ProcessKill API or any other code path before 'vm->def->id' is
diff --git a/src/util/virhook.c b/src/util/virhook.c
index d012bb1825..01ba17e406 100644
--- a/src/util/virhook.c
+++ b/src/util/virhook.c
@@ -76,6 +76,7 @@ VIR_ENUM_IMPL(virHookSubop,
 VIR_ENUM_IMPL(virHookQemuOp,
               VIR_HOOK_QEMU_OP_LAST,
               "start",
+              "stop",
               "stopped",
               "prepare",
               "release",
diff --git a/src/util/virhook.h b/src/util/virhook.h
index d8237c837e..ea8c540c3f 100644
--- a/src/util/virhook.h
+++ b/src/util/virhook.h
@@ -52,6 +52,7 @@ typedef enum {
 
 typedef enum {
     VIR_HOOK_QEMU_OP_START,            /* domain is about to start */
+    VIR_HOOK_QEMU_OP_STOP,             /* domain is about to stop */
     VIR_HOOK_QEMU_OP_STOPPED,          /* domain has stopped */
     VIR_HOOK_QEMU_OP_PREPARE,          /* domain startup initiated */
     VIR_HOOK_QEMU_OP_RELEASE,          /* domain destruction is over */
-- 
2.52.0

Reply via email to