diff mbox series

[v2] fence: introduce a file-based self-fence mechanism

Message ID 20200205095737.20153-1-felipe@nutanix.com (mailing list archive)
State New, archived
Headers show
Series [v2] fence: introduce a file-based self-fence mechanism | expand

Commit Message

Felipe Franciosi Feb. 5, 2020, 9:57 a.m. UTC
This introduces a self-fence mechanism to Qemu, causing it to die if a
heartbeat condition is not met. Currently, a file-based heartbeat is
available and can be configured as follows:

-object file-fence,id=ff0,file=/foo,qtimeout=20,ktimeout=25,signal=kill

Qemu will watch 'file' for attribute changes. Touching the file works as
a heartbeat. This parameter is mandatory.

Fencing happens after 'qtimeout' or 'ktimeout' seconds elapse without a
heartbeat. At least one of these must be specified. Both may be used, in
which case 'ktimeout' must be greater than 'qtimeout'. Setting either to
zero has no effect (as if they weren't specified).

When using 'qtimeout', an internal Qemu timer is used. Fencing with this
method gives Qemu a chance to write a log message indicating which file
caused the event. If Qemu's main loop is hung for whatever reason, this
method won't successfully kill Qemu.

When using 'ktimeout', a kernel timer is used. In this case, 'signal'
can be 'kill' (for SIGKILL, default) or 'quit' (for SIGQUIT). Using
SIGQUIT may be preferred for obtaining core dumps. If Qemu is hung
(eg. uninterruptable sleep), this method won't successfully kill Qemu.

It is worth noting that even successfully killing Qemu may not be
sufficient to completely fence a VM as certain operations like network
packets or block commands may be pending in the kernel. If that is a
concern, systems should consider using further fencing mechanisms like
hardware watchdogs either instead or in conjunction with this for
additional protection.

Signed-off-by: Felipe Franciosi <felipe@nutanix.com>
---
 backends/Makefile.objs |   2 +
 backends/file-fence.c  | 374 +++++++++++++++++++++++++++++++++++++++++
 qemu-options.hx        |  27 ++-
 3 files changed, 402 insertions(+), 1 deletion(-)
 create mode 100644 backends/file-fence.c

Changelog:
v1->v2:
- Publish patch in https://github.com/franciozzy/qemu/tree/filefence
- Rename file_fence to file-fence and move to backends/
- Use error_printf() instead of printf() when fencing
- Replace a check already done by filemonitor-inotify with assert
- Add return value to _setup() functions to simplify error logic
- Use g_ascii_strcasecmp() to simplify logic in _set_signal()
- Use glib memory allocation helpers in _set_file()
- Fix bug to allow using qtimeout without ktimeout
- Clarify usage of q/k timeouts in commit message
- Clarify usage of hardware watchdogs in commits message

Comments

no-reply@patchew.org Feb. 5, 2020, 10:12 a.m. UTC | #1
Patchew URL: https://patchew.org/QEMU/20200205095737.20153-1-felipe@nutanix.com/



Hi,

This series seems to have some coding style problems. See output below for
more information:

Subject: [PATCH v2] fence: introduce a file-based self-fence mechanism
Message-id: 20200205095737.20153-1-felipe@nutanix.com
Type: series

=== TEST SCRIPT BEGIN ===
#!/bin/bash
git rev-parse base > /dev/null || exit 0
git config --local diff.renamelimit 0
git config --local diff.renames True
git config --local diff.algorithm histogram
./scripts/checkpatch.pl --mailback base..
=== TEST SCRIPT END ===

Updating 3c8cf5a9c21ff8782164d1def7f44bd888713384
From https://github.com/patchew-project/qemu
 * [new tag]         patchew/20200205095737.20153-1-felipe@nutanix.com -> patchew/20200205095737.20153-1-felipe@nutanix.com
Switched to a new branch 'test'
f62851d fence: introduce a file-based self-fence mechanism

=== OUTPUT BEGIN ===
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#55: 
new file mode 100644

ERROR: use QEMU instead of Qemu or QEmu
#212: FILE: backends/file-fence.c:153:
+        error_setg(errp, "Error creating Qemu timer");

total: 1 errors, 1 warnings, 416 lines checked

Commit f62851d9675e (fence: introduce a file-based self-fence mechanism) has style problems, please review.  If any of these errors
are false positives report them to the maintainer, see
CHECKPATCH in MAINTAINERS.
=== OUTPUT END ===

Test command exited with code: 1


The full log is available at
http://patchew.org/logs/20200205095737.20153-1-felipe@nutanix.com/testing.checkpatch/?type=message.
---
Email generated automatically by Patchew [https://patchew.org/].
Please send your feedback to patchew-devel@redhat.com
no-reply@patchew.org Feb. 5, 2020, 10:14 a.m. UTC | #2
Patchew URL: https://patchew.org/QEMU/20200205095737.20153-1-felipe@nutanix.com/



Hi,

This series failed the docker-mingw@fedora build test. Please find the testing commands and
their output below. If you have Docker installed, you can probably reproduce it
locally.

=== TEST SCRIPT BEGIN ===
#! /bin/bash
export ARCH=x86_64
make docker-image-fedora V=1 NETWORK=1
time make docker-test-mingw@fedora J=14 NETWORK=1
=== TEST SCRIPT END ===

  CC      hw/block/block.o
  CC      hw/block/cdrom.o
  CC      hw/block/hd-geometry.o
/tmp/qemu-test/src/backends/file-fence.c:44:5: error: unknown type name 'timer_t'
     timer_t ktimer;
     ^~~~~~~
/tmp/qemu-test/src/backends/file-fence.c: In function 'timer_update':
/tmp/qemu-test/src/backends/file-fence.c:68:15: error: implicit declaration of function 'timer_settime'; did you mean 'timer_get'? [-Werror=implicit-function-declaration]
         err = timer_settime(ff->ktimer, 0, &its, NULL);
               ^~~~~~~~~~~~~
               timer_get
/tmp/qemu-test/src/backends/file-fence.c:68:15: error: nested extern declaration of 'timer_settime' [-Werror=nested-externs]
/tmp/qemu-test/src/backends/file-fence.c: In function 'ktimer_tear':
/tmp/qemu-test/src/backends/file-fence.c:103:15: error: implicit declaration of function 'timer_delete'; did you mean 'timer_del'? [-Werror=implicit-function-declaration]
         err = timer_delete(ff->ktimer);
               ^~~~~~~~~~~~
               timer_del
/tmp/qemu-test/src/backends/file-fence.c:103:15: error: nested extern declaration of 'timer_delete' [-Werror=nested-externs]
/tmp/qemu-test/src/backends/file-fence.c:105:20: error: assignment to 'int' from 'void *' makes integer from pointer without a cast [-Werror=int-conversion]
         ff->ktimer = NULL;
                    ^
/tmp/qemu-test/src/backends/file-fence.c: In function 'ktimer_setup':
/tmp/qemu-test/src/backends/file-fence.c:114:12: error: variable 'sev' has initializer but incomplete type
     struct sigevent sev = {
            ^~~~~~~~
/tmp/qemu-test/src/backends/file-fence.c:115:10: error: 'struct sigevent' has no member named 'sigev_notify'
         .sigev_notify = SIGEV_SIGNAL,
          ^~~~~~~~~~~~
/tmp/qemu-test/src/backends/file-fence.c:115:25: error: 'SIGEV_SIGNAL' undeclared (first use in this function); did you mean 'SIG_IGN'?
         .sigev_notify = SIGEV_SIGNAL,
                         ^~~~~~~~~~~~
                         SIG_IGN
/tmp/qemu-test/src/backends/file-fence.c:115:25: note: each undeclared identifier is reported only once for each function it appears in
/tmp/qemu-test/src/backends/file-fence.c:115:25: error: excess elements in struct initializer [-Werror]
/tmp/qemu-test/src/backends/file-fence.c:115:25: note: (near initialization for 'sev')
/tmp/qemu-test/src/backends/file-fence.c:116:10: error: 'struct sigevent' has no member named 'sigev_signo'
         .sigev_signo = ff->signal ? ff->signal : SIGKILL,
          ^~~~~~~~~~~
/tmp/qemu-test/src/backends/file-fence.c:116:50: error: 'SIGKILL' undeclared (first use in this function); did you mean 'SIGILL'?
         .sigev_signo = ff->signal ? ff->signal : SIGKILL,
                                                  ^~~~~~~
                                                  SIGILL
/tmp/qemu-test/src/backends/file-fence.c:116:24: error: excess elements in struct initializer [-Werror]
         .sigev_signo = ff->signal ? ff->signal : SIGKILL,
                        ^~
/tmp/qemu-test/src/backends/file-fence.c:116:24: note: (near initialization for 'sev')
/tmp/qemu-test/src/backends/file-fence.c:114:21: error: storage size of 'sev' isn't known
     struct sigevent sev = {
                     ^~~
/tmp/qemu-test/src/backends/file-fence.c:123:11: error: implicit declaration of function 'timer_create'; did you mean 'timer_update'? [-Werror=implicit-function-declaration]
     err = timer_create(CLOCK_MONOTONIC, &sev, &ff->ktimer);
           ^~~~~~~~~~~~
           timer_update
/tmp/qemu-test/src/backends/file-fence.c:123:11: error: nested extern declaration of 'timer_create' [-Werror=nested-externs]
/tmp/qemu-test/src/backends/file-fence.c:114:21: error: unused variable 'sev' [-Werror=unused-variable]
     struct sigevent sev = {
                     ^~~
/tmp/qemu-test/src/backends/file-fence.c: In function 'file_fence_set_signal':
/tmp/qemu-test/src/backends/file-fence.c:248:22: error: 'SIGQUIT' undeclared (first use in this function); did you mean 'SIGABRT'?
         ff->signal = SIGQUIT;
                      ^~~~~~~
                      SIGABRT
/tmp/qemu-test/src/backends/file-fence.c:253:22: error: 'SIGKILL' undeclared (first use in this function); did you mean 'SIGILL'?
         ff->signal = SIGKILL;
                      ^~~~~~~
                      SIGILL
/tmp/qemu-test/src/backends/file-fence.c: In function 'file_fence_get_signal':
/tmp/qemu-test/src/backends/file-fence.c:267:10: error: 'SIGKILL' undeclared (first use in this function); did you mean 'SIGILL'?
     case SIGKILL:
          ^~~~~~~
          SIGILL
/tmp/qemu-test/src/backends/file-fence.c:269:10: error: 'SIGQUIT' undeclared (first use in this function); did you mean 'SIGABRT'?
     case SIGQUIT:
          ^~~~~~~
          SIGABRT
/tmp/qemu-test/src/backends/file-fence.c: In function 'file_fence_instance_init':
/tmp/qemu-test/src/backends/file-fence.c:343:36: error: 'OBJ_PROP_FLAG_READWRITE' undeclared (first use in this function); did you mean 'OBJ_PROP_LINK_DIRECT'?
                                    OBJ_PROP_FLAG_READWRITE, &error_abort);
                                    ^~~~~~~~~~~~~~~~~~~~~~~
                                    OBJ_PROP_LINK_DIRECT
/tmp/qemu-test/src/backends/file-fence.c:342:5: error: too many arguments to function 'object_property_add_uint32_ptr'
     object_property_add_uint32_ptr(obj, "qtimeout", &ff->qtimeout,
     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /tmp/qemu-test/src/include/qom/object_interfaces.h:4,
---
/tmp/qemu-test/src/include/qom/object.h:1709:6: note: declared here
 void object_property_add_uint32_ptr(Object *obj, const char *name,
      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/tmp/qemu-test/src/backends/file-fence.c:344:5: error: too many arguments to function 'object_property_add_uint32_ptr'
     object_property_add_uint32_ptr(obj, "ktimeout", &ff->ktimeout,
     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /tmp/qemu-test/src/include/qom/object_interfaces.h:4,
---
  CC      hw/block/fdc.o
  CC      hw/block/m25p80.o
  CC      hw/block/nand.o
make: *** [/tmp/qemu-test/src/rules.mak:69: backends/file-fence.o] Error 1
make: *** Waiting for unfinished jobs....
  CC      hw/block/pflash_cfi01.o
Traceback (most recent call last):
---
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['sudo', '-n', 'docker', 'run', '--label', 'com.qemu.instance.uuid=3831efbbac3f42a78230b2ca17f24b5c', '-u', '1001', '--security-opt', 'seccomp=unconfined', '--rm', '-e', 'TARGET_LIST=', '-e', 'EXTRA_CONFIGURE_OPTS=', '-e', 'V=', '-e', 'J=14', '-e', 'DEBUG=', '-e', 'SHOW_ENV=', '-e', 'CCACHE_DIR=/var/tmp/ccache', '-v', '/home/patchew/.cache/qemu-docker-ccache:/var/tmp/ccache:z', '-v', '/var/tmp/patchew-tester-tmp-5cvmkgft/src/docker-src.2020-02-05-05.12.43.31686:/var/tmp/qemu:z,ro', 'qemu:fedora', '/var/tmp/qemu/run', 'test-mingw']' returned non-zero exit status 2.
filter=--filter=label=com.qemu.instance.uuid=3831efbbac3f42a78230b2ca17f24b5c
make[1]: *** [docker-run] Error 1
make[1]: Leaving directory `/var/tmp/patchew-tester-tmp-5cvmkgft/src'
make: *** [docker-run-test-mingw@fedora] Error 2

real    2m16.282s
user    0m8.876s


The full log is available at
http://patchew.org/logs/20200205095737.20153-1-felipe@nutanix.com/testing.docker-mingw@fedora/?type=message.
---
Email generated automatically by Patchew [https://patchew.org/].
Please send your feedback to patchew-devel@redhat.com
Marc-André Lureau Feb. 5, 2020, 10:24 a.m. UTC | #3
Hi

On Wed, Feb 5, 2020 at 10:57 AM Felipe Franciosi <felipe@nutanix.com> wrote:
>
> This introduces a self-fence mechanism to Qemu, causing it to die if a
> heartbeat condition is not met. Currently, a file-based heartbeat is
> available and can be configured as follows:
>
> -object file-fence,id=ff0,file=/foo,qtimeout=20,ktimeout=25,signal=kill
>
> Qemu will watch 'file' for attribute changes. Touching the file works as
> a heartbeat. This parameter is mandatory.
>
> Fencing happens after 'qtimeout' or 'ktimeout' seconds elapse without a
> heartbeat. At least one of these must be specified. Both may be used, in
> which case 'ktimeout' must be greater than 'qtimeout'. Setting either to
> zero has no effect (as if they weren't specified).
>
> When using 'qtimeout', an internal Qemu timer is used. Fencing with this
> method gives Qemu a chance to write a log message indicating which file
> caused the event. If Qemu's main loop is hung for whatever reason, this
> method won't successfully kill Qemu.
>
> When using 'ktimeout', a kernel timer is used. In this case, 'signal'
> can be 'kill' (for SIGKILL, default) or 'quit' (for SIGQUIT). Using
> SIGQUIT may be preferred for obtaining core dumps. If Qemu is hung
> (eg. uninterruptable sleep), this method won't successfully kill Qemu.
>
> It is worth noting that even successfully killing Qemu may not be
> sufficient to completely fence a VM as certain operations like network
> packets or block commands may be pending in the kernel. If that is a
> concern, systems should consider using further fencing mechanisms like
> hardware watchdogs either instead or in conjunction with this for
> additional protection.
>
> Signed-off-by: Felipe Franciosi <felipe@nutanix.com>
> ---
>  backends/Makefile.objs |   2 +
>  backends/file-fence.c  | 374 +++++++++++++++++++++++++++++++++++++++++
>  qemu-options.hx        |  27 ++-
>  3 files changed, 402 insertions(+), 1 deletion(-)
>  create mode 100644 backends/file-fence.c
>
> Changelog:
> v1->v2:
> - Publish patch in https://github.com/franciozzy/qemu/tree/filefence
> - Rename file_fence to file-fence and move to backends/
> - Use error_printf() instead of printf() when fencing
> - Replace a check already done by filemonitor-inotify with assert
> - Add return value to _setup() functions to simplify error logic
> - Use g_ascii_strcasecmp() to simplify logic in _set_signal()
> - Use glib memory allocation helpers in _set_file()
> - Fix bug to allow using qtimeout without ktimeout
> - Clarify usage of q/k timeouts in commit message
> - Clarify usage of hardware watchdogs in commits message
>
> diff --git a/backends/Makefile.objs b/backends/Makefile.objs
> index 28a847cd57..da2a589bdf 100644
> --- a/backends/Makefile.objs
> +++ b/backends/Makefile.objs
> @@ -9,6 +9,8 @@ common-obj-$(CONFIG_POSIX) += hostmem-file.o
>  common-obj-y += cryptodev.o
>  common-obj-y += cryptodev-builtin.o
>
> +common-obj-y += file-fence.o
> +
>  ifeq ($(CONFIG_VIRTIO_CRYPTO),y)
>  common-obj-y += cryptodev-vhost.o
>  common-obj-$(CONFIG_VHOST_CRYPTO) += cryptodev-vhost-user.o
> diff --git a/backends/file-fence.c b/backends/file-fence.c
> new file mode 100644
> index 0000000000..3dbbed7325
> --- /dev/null
> +++ b/backends/file-fence.c
> @@ -0,0 +1,374 @@
> +/*
> + * QEMU file-based self-fence mechanism
> + *
> + * Copyright (c) 2019 Nutanix Inc. All rights reserved.
> + *
> + * Authors:
> + *    Felipe Franciosi <felipe@nutanix.com>
> + *
> + * This library is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU Lesser General Public
> + * License as published by the Free Software Foundation; either
> + * version 2 of the License, or (at your option) any later version.
> + *
> + * This library is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> + * Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public
> + * License along with this library; if not, see <http://www.gnu.org/licenses/>.
> + *
> + */
> +
> +#include "qemu/osdep.h"
> +#include "qapi/error.h"
> +#include "qom/object_interfaces.h"
> +#include "qemu/error-report.h"
> +#include "qemu/filemonitor.h"
> +#include "qemu/timer.h"
> +
> +#include <time.h>
> +
> +#define TYPE_FILE_FENCE "file-fence"
> +
> +typedef struct FileFence {
> +    Object parent_obj;
> +
> +    gchar *dir;
> +    gchar *file;
> +    uint32_t qtimeout;
> +    uint32_t ktimeout;
> +    int signal;
> +
> +    timer_t ktimer;
> +    QEMUTimer *qtimer;
> +
> +    QFileMonitor *fm;
> +    uint64_t id;
> +} FileFence;
> +
> +#define FILE_FENCE(obj) \
> +    OBJECT_CHECK(FileFence, (obj), TYPE_FILE_FENCE)
> +
> +static void
> +timer_update(FileFence *ff)
> +{
> +    struct itimerspec its = {
> +        .it_value.tv_sec = ff->ktimeout,
> +    };
> +    int err;
> +
> +    if (ff->qtimeout) {
> +        timer_mod(ff->qtimer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) +
> +                              ff->qtimeout * 1000);
> +    }
> +
> +    if (ff->ktimeout) {
> +        err = timer_settime(ff->ktimer, 0, &its, NULL);
> +        g_assert(err == 0);
> +    }
> +}
> +
> +static void
> +file_fence_abort_cb(void *opaque)
> +{
> +    FileFence *ff = opaque;
> +    error_printf("Fencing after %u seconds on '%s'\n",
> +                 ff->qtimeout, g_strconcat(ff->dir, "/", ff->file, NULL));
> +    abort();
> +}
> +
> +static void
> +file_fence_watch_cb(int64_t id, QFileMonitorEvent ev, const char *file,
> +                    void *opaque)
> +{
> +    FileFence *ff = opaque;
> +
> +    if (ev != QFILE_MONITOR_EVENT_ATTRIBUTES) {
> +        return;
> +    }
> +
> +    g_assert(g_str_equal(file, ff->file));
> +
> +    timer_update(ff);
> +}
> +
> +static void
> +ktimer_tear(FileFence *ff)
> +{
> +    int err;
> +
> +    if (ff->ktimer) {
> +        err = timer_delete(ff->ktimer);
> +        g_assert(err == 0);
> +        ff->ktimer = NULL;
> +    }
> +}
> +
> +static gboolean
> +ktimer_setup(FileFence *ff, Error **errp)
> +{
> +    int err;
> +
> +    struct sigevent sev = {
> +        .sigev_notify = SIGEV_SIGNAL,
> +        .sigev_signo = ff->signal ? ff->signal : SIGKILL,
> +    };
> +
> +    if (ff->ktimeout == 0) {
> +        return TRUE;
> +    }
> +
> +    err = timer_create(CLOCK_MONOTONIC, &sev, &ff->ktimer);
> +    if (err == -1) {
> +        error_setg(errp, "Error creating kernel timer: %m");
> +        return FALSE;
> +    }
> +
> +    return TRUE;
> +}
> +
> +static void
> +qtimer_tear(FileFence *ff)
> +{
> +    if (ff->qtimer) {
> +        timer_del(ff->qtimer);
> +        timer_free(ff->qtimer);
> +    }
> +    ff->qtimer = NULL;
> +}
> +
> +static gboolean
> +qtimer_setup(FileFence *ff, Error **errp)
> +{
> +    QEMUTimer *qtimer;
> +
> +    if (ff->qtimeout == 0) {
> +        return TRUE;
> +    }
> +
> +    qtimer = timer_new_ms(QEMU_CLOCK_REALTIME, file_fence_abort_cb, ff);
> +    if (qtimer == NULL) {
> +        error_setg(errp, "Error creating Qemu timer");
> +        return FALSE;
> +    }
> +
> +    ff->qtimer = qtimer;
> +
> +    return TRUE;
> +}
> +
> +static void
> +watch_tear(FileFence *ff)
> +{
> +    if (ff->fm) {
> +        qemu_file_monitor_remove_watch(ff->fm, ff->dir, ff->id);
> +        qemu_file_monitor_free(ff->fm);
> +        ff->fm = NULL;
> +        ff->id = 0;
> +    }
> +}
> +
> +static gboolean
> +watch_setup(FileFence *ff, Error **errp)
> +{
> +    QFileMonitor *fm;
> +    int64_t id;
> +
> +    fm = qemu_file_monitor_new(errp);
> +    if (!fm) {
> +        return FALSE;
> +    }
> +
> +    id = qemu_file_monitor_add_watch(fm, ff->dir, ff->file,
> +                                     file_fence_watch_cb, ff, errp);
> +    if (id < 0) {
> +        qemu_file_monitor_free(fm);
> +        return FALSE;
> +    }
> +
> +    ff->fm = fm;
> +    ff->id = id;
> +
> +    return TRUE;
> +}
> +
> +static void
> +file_fence_complete(UserCreatable *obj, Error **errp)
> +{
> +    FileFence *ff = FILE_FENCE(obj);
> +
> +    if (ff->dir == NULL) {
> +        error_setg(errp, "A 'file' must be set");
> +        return;
> +    }
> +
> +    if (ff->signal != 0 && ff->ktimeout == 0) {
> +        error_setg(errp, "Using 'signal' requires 'ktimeout' to be set");
> +        return;
> +    }
> +
> +    if (ff->ktimeout == 0 && ff->qtimeout == 0) {
> +        error_setg(errp, "One or both of 'ktimeout' or 'qtimeout' must be set");
> +        return;
> +    }
> +
> +    if (ff->qtimeout >= ff->ktimeout && ff->ktimeout != 0) {
> +        error_setg(errp, "Using 'qtimeout' >= 'ktimeout' doesn't make sense");
> +        return;
> +    }
> +
> +    if (!watch_setup(ff, errp) ||
> +        !qtimer_setup(ff, errp) ||
> +        !ktimer_setup(ff, errp)) {
> +        return;
> +    }
> +
> +    timer_update(ff);
> +
> +    return;
> +}
> +
> +static void
> +file_fence_set_signal(Object *obj, const char *value, Error **errp)
> +{
> +    FileFence *ff = FILE_FENCE(obj);
> +
> +    if (ff->signal) {
> +        error_setg(errp, "Signal property already set");
> +        return;
> +    }
> +
> +    if (value == NULL) {
> +        goto err;
> +    }
> +
> +    if (g_ascii_strcasecmp(value, "QUIT") == 0) {
> +        ff->signal = SIGQUIT;
> +        return;
> +    }
> +
> +    if (g_ascii_strcasecmp(value, "KILL") == 0) {
> +        ff->signal = SIGKILL;
> +        return;
> +    }
> +
> +err:
> +    error_setg(errp, "Invalid signal. Must be 'quit' or 'kill'");
> +}
> +
> +static char *
> +file_fence_get_signal(Object *obj, Error **errp)
> +{
> +    FileFence *ff = FILE_FENCE(obj);
> +
> +    switch (ff->signal) {
> +    case SIGKILL:
> +        return g_strdup("kill");
> +    case SIGQUIT:
> +        return g_strdup("quit");
> +    }
> +
> +    /* Unreachable */
> +    abort();
> +}
> +
> +static void
> +file_fence_set_file(Object *obj, const char *value, Error **errp)
> +{
> +    FileFence *ff = FILE_FENCE(obj);
> +    g_autofree gchar *dir = NULL, *file = NULL;
> +
> +    if (ff->dir) {
> +        error_setg(errp, "File property already set");
> +        return;
> +    }
> +
> +    dir = g_path_get_dirname(value);
> +    if (g_str_equal(dir, ".")) {
> +        error_setg(errp, "Path for file-fence must be absolute");

g_path_is_absolute() ? why such limitation ?

> +        return;
> +    }
> +
> +    file = g_path_get_basename(value);
> +    if (g_str_equal(file, ".")) {
> +        error_setg(errp, "Path for file-fence must be a file");

I think you would get "." if value is "".

I am not sure you need extra error handling here, since watch_setup()
will fail if it can't open the file.

> +        return;
> +    }
> +
> +    ff->dir = g_steal_pointer(&dir);
> +    ff->file = g_steal_pointer(&file);
> +}
> +
> +static char *
> +file_fence_get_file(Object *obj, Error **errp)
> +{
> +    FileFence *ff = FILE_FENCE(obj);
> +
> +    if (ff->file) {
> +        return g_build_filename(ff->dir, ff->file, NULL);
> +    }
> +
> +    return NULL;
> +}
> +
> +static void
> +file_fence_instance_finalize(Object *obj)
> +{
> +    FileFence *ff = FILE_FENCE(obj);
> +
> +    ktimer_tear(ff);
> +    qtimer_tear(ff);
> +    watch_tear(ff);
> +
> +    g_free(ff->file);
> +    g_free(ff->dir);
> +}
> +
> +static void
> +file_fence_instance_init(Object *obj)
> +{
> +    FileFence *ff = FILE_FENCE(obj);
> +
> +    object_property_add_str(obj, "file",
> +                            file_fence_get_file,
> +                            file_fence_set_file,
> +                            &error_abort);
> +    object_property_add_str(obj, "signal",
> +                            file_fence_get_signal,
> +                            file_fence_set_signal,
> +                            &error_abort);
> +    object_property_add_uint32_ptr(obj, "qtimeout", &ff->qtimeout,
> +                                   OBJ_PROP_FLAG_READWRITE, &error_abort);
> +    object_property_add_uint32_ptr(obj, "ktimeout", &ff->ktimeout,
> +                                   OBJ_PROP_FLAG_READWRITE, &error_abort);

You could make them all class properties, right?

> +}
> +
> +static void
> +file_fence_class_init(ObjectClass *klass, void *class_data)
> +{
> +    UserCreatableClass *ucc = USER_CREATABLE_CLASS(klass);
> +    ucc->complete = file_fence_complete;
> +}
> +
> +static const TypeInfo file_fence_info = {
> +    .name = TYPE_FILE_FENCE,
> +    .parent = TYPE_OBJECT,
> +    .class_init = file_fence_class_init,
> +    .instance_size = sizeof(FileFence),
> +    .instance_init = file_fence_instance_init,
> +    .instance_finalize = file_fence_instance_finalize,
> +    .interfaces = (InterfaceInfo[]) {
> +        { TYPE_USER_CREATABLE },
> +        { }
> +    }
> +};
> +
> +static void
> +register_types(void)
> +{
> +    type_register_static(&file_fence_info);
> +}
> +
> +type_init(register_types);
> diff --git a/qemu-options.hx b/qemu-options.hx
> index 224a8e8712..5ea94b37af 100644
> --- a/qemu-options.hx
> +++ b/qemu-options.hx
> @@ -4974,8 +4974,33 @@ The polling parameters can be modified at run-time using the @code{qom-set} comm
>
>  @end table
>
> -ETEXI
> +@item -object file-fence,id=@var{id},file=@var{file},qtimeout=@var{qtimeout},ktimeout=@var{ktimeout},signal=@{signal}
> +
> +Self-fence Qemu if @var{file} is not modified within a given timeout.
> +
> +Qemu will watch @var{file} for attribute changes. Touching the file works as a
> +heartbeat. This parameter is mandatory.
> +
> +Fencing happens after @var{qtimeout} or @var{ktimeout} seconds elapse
> +without a heartbeat. At least one of these must be specified. Both may be used.
>
> +When using @var{qtimeout}, an internal Qemu timer is used. Fencing with
> +this method gives Qemu a chance to write a log message indicating which file
> +caused the event. If Qemu's main loop is hung for whatever reason, this method
> +won't successfully kill Qemu.
> +
> +When using @var{ktimeout}, a kernel timer is used. In this case, @var{signal}
> +can be 'kill' (for SIGKILL, default) or 'quit' (for SIGQUIT). Using SIGQUIT may
> +be preferred for obtaining core dumps. If Qemu is hung (eg. uninterruptable
> +sleep), this method won't successfully kill Qemu.
> +
> +It is worth noting that even successfully killing Qemu may not be sufficient to
> +completely fence a VM as certain operations like network packets or block
> +commands may be pending in the kernel. If that is a concern, systems should
> +consider using further fencing mechanisms like hardware watchdogs either in
> +addition or in conjunction with this feature for additional protection.
> +
> +ETEXI
>
>  HXCOMM This is the last statement. Insert new options before this line!
>  STEXI
> --
> 2.20.1
>
no-reply@patchew.org Feb. 5, 2020, 10:31 a.m. UTC | #4
Patchew URL: https://patchew.org/QEMU/20200205095737.20153-1-felipe@nutanix.com/



Hi,

This series failed the docker-quick@centos7 build test. Please find the testing commands and
their output below. If you have Docker installed, you can probably reproduce it
locally.

=== TEST SCRIPT BEGIN ===
#!/bin/bash
make docker-image-centos7 V=1 NETWORK=1
time make docker-test-quick@centos7 SHOW_ENV=1 J=14 NETWORK=1
=== TEST SCRIPT END ===

  CC      hw/acpi/nvdimm.o
  CC      hw/acpi/vmgenid.o
/tmp/qemu-test/src/backends/file-fence.c: In function 'file_fence_instance_init':
/tmp/qemu-test/src/backends/file-fence.c:343:36: error: 'OBJ_PROP_FLAG_READWRITE' undeclared (first use in this function)
                                    OBJ_PROP_FLAG_READWRITE, &error_abort);
                                    ^
/tmp/qemu-test/src/backends/file-fence.c:343:36: note: each undeclared identifier is reported only once for each function it appears in
/tmp/qemu-test/src/backends/file-fence.c:343:36: error: too many arguments to function 'object_property_add_uint32_ptr'
In file included from /tmp/qemu-test/src/include/qom/object_interfaces.h:4:0,
                 from /tmp/qemu-test/src/backends/file-fence.c:26:
/tmp/qemu-test/src/include/qom/object.h:1709:6: note: declared here
 void object_property_add_uint32_ptr(Object *obj, const char *name,
      ^
/tmp/qemu-test/src/backends/file-fence.c:345:36: error: too many arguments to function 'object_property_add_uint32_ptr'
                                    OBJ_PROP_FLAG_READWRITE, &error_abort);
                                    ^
In file included from /tmp/qemu-test/src/include/qom/object_interfaces.h:4:0,
---
/tmp/qemu-test/src/include/qom/object.h:1709:6: note: declared here
 void object_property_add_uint32_ptr(Object *obj, const char *name,
      ^
make: *** [backends/file-fence.o] Error 1
make: *** Waiting for unfinished jobs....
rm tests/qemu-iotests/socket_scm_helper.o
Traceback (most recent call last):
---
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['sudo', '-n', 'docker', 'run', '--label', 'com.qemu.instance.uuid=fd41c7e165b64e988307aea8da6473d8', '-u', '1001', '--security-opt', 'seccomp=unconfined', '--rm', '-e', 'TARGET_LIST=', '-e', 'EXTRA_CONFIGURE_OPTS=', '-e', 'V=', '-e', 'J=14', '-e', 'DEBUG=', '-e', 'SHOW_ENV=1', '-e', 'CCACHE_DIR=/var/tmp/ccache', '-v', '/home/patchew/.cache/qemu-docker-ccache:/var/tmp/ccache:z', '-v', '/var/tmp/patchew-tester-tmp-0g859qeh/src/docker-src.2020-02-05-05.29.57.7079:/var/tmp/qemu:z,ro', 'qemu:centos7', '/var/tmp/qemu/run', 'test-quick']' returned non-zero exit status 2.
filter=--filter=label=com.qemu.instance.uuid=fd41c7e165b64e988307aea8da6473d8
make[1]: *** [docker-run] Error 1
make[1]: Leaving directory `/var/tmp/patchew-tester-tmp-0g859qeh/src'
make: *** [docker-run-test-quick@centos7] Error 2

real    2m0.985s
user    0m8.492s


The full log is available at
http://patchew.org/logs/20200205095737.20153-1-felipe@nutanix.com/testing.docker-quick@centos7/?type=message.
---
Email generated automatically by Patchew [https://patchew.org/].
Please send your feedback to patchew-devel@redhat.com
Felipe Franciosi Feb. 5, 2020, 10:47 a.m. UTC | #5
Heya,

> On Feb 5, 2020, at 10:24 AM, Marc-André Lureau <marcandre.lureau@gmail.com> wrote:
> 
> Hi
> 
> On Wed, Feb 5, 2020 at 10:57 AM Felipe Franciosi <felipe@nutanix.com> wrote:
>> 
>> This introduces a self-fence mechanism to Qemu, causing it to die if a
>> heartbeat condition is not met. Currently, a file-based heartbeat is
>> available and can be configured as follows:
>> 
>> -object file-fence,id=ff0,file=/foo,qtimeout=20,ktimeout=25,signal=kill
>> 
>> Qemu will watch 'file' for attribute changes. Touching the file works as
>> a heartbeat. This parameter is mandatory.
>> 
>> Fencing happens after 'qtimeout' or 'ktimeout' seconds elapse without a
>> heartbeat. At least one of these must be specified. Both may be used, in
>> which case 'ktimeout' must be greater than 'qtimeout'. Setting either to
>> zero has no effect (as if they weren't specified).
>> 
>> When using 'qtimeout', an internal Qemu timer is used. Fencing with this
>> method gives Qemu a chance to write a log message indicating which file
>> caused the event. If Qemu's main loop is hung for whatever reason, this
>> method won't successfully kill Qemu.
>> 
>> When using 'ktimeout', a kernel timer is used. In this case, 'signal'
>> can be 'kill' (for SIGKILL, default) or 'quit' (for SIGQUIT). Using
>> SIGQUIT may be preferred for obtaining core dumps. If Qemu is hung
>> (eg. uninterruptable sleep), this method won't successfully kill Qemu.
>> 
>> It is worth noting that even successfully killing Qemu may not be
>> sufficient to completely fence a VM as certain operations like network
>> packets or block commands may be pending in the kernel. If that is a
>> concern, systems should consider using further fencing mechanisms like
>> hardware watchdogs either instead or in conjunction with this for
>> additional protection.
>> 
>> Signed-off-by: Felipe Franciosi <felipe@nutanix.com>
>> ---
>> backends/Makefile.objs |   2 +
>> backends/file-fence.c  | 374 +++++++++++++++++++++++++++++++++++++++++
>> qemu-options.hx        |  27 ++-
>> 3 files changed, 402 insertions(+), 1 deletion(-)
>> create mode 100644 backends/file-fence.c
>> 
>> Changelog:
>> v1->v2:
>> - Publish patch in https://github.com/franciozzy/qemu/tree/filefence
>> - Rename file_fence to file-fence and move to backends/
>> - Use error_printf() instead of printf() when fencing
>> - Replace a check already done by filemonitor-inotify with assert
>> - Add return value to _setup() functions to simplify error logic
>> - Use g_ascii_strcasecmp() to simplify logic in _set_signal()
>> - Use glib memory allocation helpers in _set_file()
>> - Fix bug to allow using qtimeout without ktimeout
>> - Clarify usage of q/k timeouts in commit message
>> - Clarify usage of hardware watchdogs in commits message
>> 
>> diff --git a/backends/Makefile.objs b/backends/Makefile.objs
>> index 28a847cd57..da2a589bdf 100644
>> --- a/backends/Makefile.objs
>> +++ b/backends/Makefile.objs
>> @@ -9,6 +9,8 @@ common-obj-$(CONFIG_POSIX) += hostmem-file.o
>> common-obj-y += cryptodev.o
>> common-obj-y += cryptodev-builtin.o
>> 
>> +common-obj-y += file-fence.o
>> +
>> ifeq ($(CONFIG_VIRTIO_CRYPTO),y)
>> common-obj-y += cryptodev-vhost.o
>> common-obj-$(CONFIG_VHOST_CRYPTO) += cryptodev-vhost-user.o
>> diff --git a/backends/file-fence.c b/backends/file-fence.c
>> new file mode 100644
>> index 0000000000..3dbbed7325
>> --- /dev/null
>> +++ b/backends/file-fence.c
>> @@ -0,0 +1,374 @@
>> +/*
>> + * QEMU file-based self-fence mechanism
>> + *
>> + * Copyright (c) 2019 Nutanix Inc. All rights reserved.
>> + *
>> + * Authors:
>> + *    Felipe Franciosi <felipe@nutanix.com>
>> + *
>> + * This library is free software; you can redistribute it and/or
>> + * modify it under the terms of the GNU Lesser General Public
>> + * License as published by the Free Software Foundation; either
>> + * version 2 of the License, or (at your option) any later version.
>> + *
>> + * This library is distributed in the hope that it will be useful,
>> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
>> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
>> + * Lesser General Public License for more details.
>> + *
>> + * You should have received a copy of the GNU Lesser General Public
>> + * License along with this library; if not, see <http://www.gnu.org/licenses/>.
>> + *
>> + */
>> +
>> +#include "qemu/osdep.h"
>> +#include "qapi/error.h"
>> +#include "qom/object_interfaces.h"
>> +#include "qemu/error-report.h"
>> +#include "qemu/filemonitor.h"
>> +#include "qemu/timer.h"
>> +
>> +#include <time.h>
>> +
>> +#define TYPE_FILE_FENCE "file-fence"
>> +
>> +typedef struct FileFence {
>> +    Object parent_obj;
>> +
>> +    gchar *dir;
>> +    gchar *file;
>> +    uint32_t qtimeout;
>> +    uint32_t ktimeout;
>> +    int signal;
>> +
>> +    timer_t ktimer;
>> +    QEMUTimer *qtimer;
>> +
>> +    QFileMonitor *fm;
>> +    uint64_t id;
>> +} FileFence;
>> +
>> +#define FILE_FENCE(obj) \
>> +    OBJECT_CHECK(FileFence, (obj), TYPE_FILE_FENCE)
>> +
>> +static void
>> +timer_update(FileFence *ff)
>> +{
>> +    struct itimerspec its = {
>> +        .it_value.tv_sec = ff->ktimeout,
>> +    };
>> +    int err;
>> +
>> +    if (ff->qtimeout) {
>> +        timer_mod(ff->qtimer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) +
>> +                              ff->qtimeout * 1000);
>> +    }
>> +
>> +    if (ff->ktimeout) {
>> +        err = timer_settime(ff->ktimer, 0, &its, NULL);
>> +        g_assert(err == 0);
>> +    }
>> +}
>> +
>> +static void
>> +file_fence_abort_cb(void *opaque)
>> +{
>> +    FileFence *ff = opaque;
>> +    error_printf("Fencing after %u seconds on '%s'\n",
>> +                 ff->qtimeout, g_strconcat(ff->dir, "/", ff->file, NULL));
>> +    abort();
>> +}
>> +
>> +static void
>> +file_fence_watch_cb(int64_t id, QFileMonitorEvent ev, const char *file,
>> +                    void *opaque)
>> +{
>> +    FileFence *ff = opaque;
>> +
>> +    if (ev != QFILE_MONITOR_EVENT_ATTRIBUTES) {
>> +        return;
>> +    }
>> +
>> +    g_assert(g_str_equal(file, ff->file));
>> +
>> +    timer_update(ff);
>> +}
>> +
>> +static void
>> +ktimer_tear(FileFence *ff)
>> +{
>> +    int err;
>> +
>> +    if (ff->ktimer) {
>> +        err = timer_delete(ff->ktimer);
>> +        g_assert(err == 0);
>> +        ff->ktimer = NULL;
>> +    }
>> +}
>> +
>> +static gboolean
>> +ktimer_setup(FileFence *ff, Error **errp)
>> +{
>> +    int err;
>> +
>> +    struct sigevent sev = {
>> +        .sigev_notify = SIGEV_SIGNAL,
>> +        .sigev_signo = ff->signal ? ff->signal : SIGKILL,
>> +    };
>> +
>> +    if (ff->ktimeout == 0) {
>> +        return TRUE;
>> +    }
>> +
>> +    err = timer_create(CLOCK_MONOTONIC, &sev, &ff->ktimer);
>> +    if (err == -1) {
>> +        error_setg(errp, "Error creating kernel timer: %m");
>> +        return FALSE;
>> +    }
>> +
>> +    return TRUE;
>> +}
>> +
>> +static void
>> +qtimer_tear(FileFence *ff)
>> +{
>> +    if (ff->qtimer) {
>> +        timer_del(ff->qtimer);
>> +        timer_free(ff->qtimer);
>> +    }
>> +    ff->qtimer = NULL;
>> +}
>> +
>> +static gboolean
>> +qtimer_setup(FileFence *ff, Error **errp)
>> +{
>> +    QEMUTimer *qtimer;
>> +
>> +    if (ff->qtimeout == 0) {
>> +        return TRUE;
>> +    }
>> +
>> +    qtimer = timer_new_ms(QEMU_CLOCK_REALTIME, file_fence_abort_cb, ff);
>> +    if (qtimer == NULL) {
>> +        error_setg(errp, "Error creating Qemu timer");
>> +        return FALSE;
>> +    }
>> +
>> +    ff->qtimer = qtimer;
>> +
>> +    return TRUE;
>> +}
>> +
>> +static void
>> +watch_tear(FileFence *ff)
>> +{
>> +    if (ff->fm) {
>> +        qemu_file_monitor_remove_watch(ff->fm, ff->dir, ff->id);
>> +        qemu_file_monitor_free(ff->fm);
>> +        ff->fm = NULL;
>> +        ff->id = 0;
>> +    }
>> +}
>> +
>> +static gboolean
>> +watch_setup(FileFence *ff, Error **errp)
>> +{
>> +    QFileMonitor *fm;
>> +    int64_t id;
>> +
>> +    fm = qemu_file_monitor_new(errp);
>> +    if (!fm) {
>> +        return FALSE;
>> +    }
>> +
>> +    id = qemu_file_monitor_add_watch(fm, ff->dir, ff->file,
>> +                                     file_fence_watch_cb, ff, errp);
>> +    if (id < 0) {
>> +        qemu_file_monitor_free(fm);
>> +        return FALSE;
>> +    }
>> +
>> +    ff->fm = fm;
>> +    ff->id = id;
>> +
>> +    return TRUE;
>> +}
>> +
>> +static void
>> +file_fence_complete(UserCreatable *obj, Error **errp)
>> +{
>> +    FileFence *ff = FILE_FENCE(obj);
>> +
>> +    if (ff->dir == NULL) {
>> +        error_setg(errp, "A 'file' must be set");
>> +        return;
>> +    }
>> +
>> +    if (ff->signal != 0 && ff->ktimeout == 0) {
>> +        error_setg(errp, "Using 'signal' requires 'ktimeout' to be set");
>> +        return;
>> +    }
>> +
>> +    if (ff->ktimeout == 0 && ff->qtimeout == 0) {
>> +        error_setg(errp, "One or both of 'ktimeout' or 'qtimeout' must be set");
>> +        return;
>> +    }
>> +
>> +    if (ff->qtimeout >= ff->ktimeout && ff->ktimeout != 0) {
>> +        error_setg(errp, "Using 'qtimeout' >= 'ktimeout' doesn't make sense");
>> +        return;
>> +    }
>> +
>> +    if (!watch_setup(ff, errp) ||
>> +        !qtimer_setup(ff, errp) ||
>> +        !ktimer_setup(ff, errp)) {
>> +        return;
>> +    }
>> +
>> +    timer_update(ff);
>> +
>> +    return;
>> +}
>> +
>> +static void
>> +file_fence_set_signal(Object *obj, const char *value, Error **errp)
>> +{
>> +    FileFence *ff = FILE_FENCE(obj);
>> +
>> +    if (ff->signal) {
>> +        error_setg(errp, "Signal property already set");
>> +        return;
>> +    }
>> +
>> +    if (value == NULL) {
>> +        goto err;
>> +    }
>> +
>> +    if (g_ascii_strcasecmp(value, "QUIT") == 0) {
>> +        ff->signal = SIGQUIT;
>> +        return;
>> +    }
>> +
>> +    if (g_ascii_strcasecmp(value, "KILL") == 0) {
>> +        ff->signal = SIGKILL;
>> +        return;
>> +    }
>> +
>> +err:
>> +    error_setg(errp, "Invalid signal. Must be 'quit' or 'kill'");
>> +}
>> +
>> +static char *
>> +file_fence_get_signal(Object *obj, Error **errp)
>> +{
>> +    FileFence *ff = FILE_FENCE(obj);
>> +
>> +    switch (ff->signal) {
>> +    case SIGKILL:
>> +        return g_strdup("kill");
>> +    case SIGQUIT:
>> +        return g_strdup("quit");
>> +    }
>> +
>> +    /* Unreachable */
>> +    abort();
>> +}
>> +
>> +static void
>> +file_fence_set_file(Object *obj, const char *value, Error **errp)
>> +{
>> +    FileFence *ff = FILE_FENCE(obj);
>> +    g_autofree gchar *dir = NULL, *file = NULL;
>> +
>> +    if (ff->dir) {
>> +        error_setg(errp, "File property already set");
>> +        return;
>> +    }
>> +
>> +    dir = g_path_get_dirname(value);
>> +    if (g_str_equal(dir, ".")) {
>> +        error_setg(errp, "Path for file-fence must be absolute");
> 
> g_path_is_absolute() ? why such limitation ?

You have a good point. My original thinking was that this would only
be used in production and you want to avoid someone accidentally using
relative paths, but tests and such could benefit from relative paths.
I'll fix this.

> 
>> +        return;
>> +    }
>> +
>> +    file = g_path_get_basename(value);
>> +    if (g_str_equal(file, ".")) {
>> +        error_setg(errp, "Path for file-fence must be a file");
> 
> I think you would get "." if value is "".
> 
> I am not sure you need extra error handling here, since watch_setup()
> will fail if it can't open the file.

I prefer to fail gracefully as early as possible. The error from a
lower-level method would have less meaningful information.

Let me work on the checks above for a v3 and we can review.

> 
>> +        return;
>> +    }
>> +
>> +    ff->dir = g_steal_pointer(&dir);
>> +    ff->file = g_steal_pointer(&file);
>> +}
>> +
>> +static char *
>> +file_fence_get_file(Object *obj, Error **errp)
>> +{
>> +    FileFence *ff = FILE_FENCE(obj);
>> +
>> +    if (ff->file) {
>> +        return g_build_filename(ff->dir, ff->file, NULL);
>> +    }
>> +
>> +    return NULL;
>> +}
>> +
>> +static void
>> +file_fence_instance_finalize(Object *obj)
>> +{
>> +    FileFence *ff = FILE_FENCE(obj);
>> +
>> +    ktimer_tear(ff);
>> +    qtimer_tear(ff);
>> +    watch_tear(ff);
>> +
>> +    g_free(ff->file);
>> +    g_free(ff->dir);
>> +}
>> +
>> +static void
>> +file_fence_instance_init(Object *obj)
>> +{
>> +    FileFence *ff = FILE_FENCE(obj);
>> +
>> +    object_property_add_str(obj, "file",
>> +                            file_fence_get_file,
>> +                            file_fence_set_file,
>> +                            &error_abort);
>> +    object_property_add_str(obj, "signal",
>> +                            file_fence_get_signal,
>> +                            file_fence_set_signal,
>> +                            &error_abort);
>> +    object_property_add_uint32_ptr(obj, "qtimeout", &ff->qtimeout,
>> +                                   OBJ_PROP_FLAG_READWRITE, &error_abort);
>> +    object_property_add_uint32_ptr(obj, "ktimeout", &ff->ktimeout,
>> +                                   OBJ_PROP_FLAG_READWRITE, &error_abort);
> 
> You could make them all class properties, right?

That's what I said on the other thread. Let me consolidate here:

> I tried to fit some of these in the class, as well as justify a split
> of the file-based fencing with a more generic self-fencer right off
> the bat. But it didn't make sense in the end. I envisioned scenarios
> where you may have different heartbeats for one Qemu with different
> timeouts. In that case, it wouldn't work as a class property, right?

Maybe I misunderstand how class properties work. Let me know.

> 
>> +}
>> +
>> +static void
>> +file_fence_class_init(ObjectClass *klass, void *class_data)
>> +{
>> +    UserCreatableClass *ucc = USER_CREATABLE_CLASS(klass);
>> +    ucc->complete = file_fence_complete;
>> +}
>> +
>> +static const TypeInfo file_fence_info = {
>> +    .name = TYPE_FILE_FENCE,
>> +    .parent = TYPE_OBJECT,
>> +    .class_init = file_fence_class_init,
>> +    .instance_size = sizeof(FileFence),
>> +    .instance_init = file_fence_instance_init,
>> +    .instance_finalize = file_fence_instance_finalize,
>> +    .interfaces = (InterfaceInfo[]) {
>> +        { TYPE_USER_CREATABLE },
>> +        { }
>> +    }
>> +};
>> +
>> +static void
>> +register_types(void)
>> +{
>> +    type_register_static(&file_fence_info);
>> +}
>> +
>> +type_init(register_types);
>> diff --git a/qemu-options.hx b/qemu-options.hx
>> index 224a8e8712..5ea94b37af 100644
>> --- a/qemu-options.hx
>> +++ b/qemu-options.hx
>> @@ -4974,8 +4974,33 @@ The polling parameters can be modified at run-time using the @code{qom-set} comm
>> 
>> @end table
>> 
>> -ETEXI
>> +@item -object file-fence,id=@var{id},file=@var{file},qtimeout=@var{qtimeout},ktimeout=@var{ktimeout},signal=@{signal}
>> +
>> +Self-fence Qemu if @var{file} is not modified within a given timeout.
>> +
>> +Qemu will watch @var{file} for attribute changes. Touching the file works as a
>> +heartbeat. This parameter is mandatory.
>> +
>> +Fencing happens after @var{qtimeout} or @var{ktimeout} seconds elapse
>> +without a heartbeat. At least one of these must be specified. Both may be used.
>> 
>> +When using @var{qtimeout}, an internal Qemu timer is used. Fencing with
>> +this method gives Qemu a chance to write a log message indicating which file
>> +caused the event. If Qemu's main loop is hung for whatever reason, this method
>> +won't successfully kill Qemu.
>> +
>> +When using @var{ktimeout}, a kernel timer is used. In this case, @var{signal}
>> +can be 'kill' (for SIGKILL, default) or 'quit' (for SIGQUIT). Using SIGQUIT may
>> +be preferred for obtaining core dumps. If Qemu is hung (eg. uninterruptable
>> +sleep), this method won't successfully kill Qemu.
>> +
>> +It is worth noting that even successfully killing Qemu may not be sufficient to
>> +completely fence a VM as certain operations like network packets or block
>> +commands may be pending in the kernel. If that is a concern, systems should
>> +consider using further fencing mechanisms like hardware watchdogs either in
>> +addition or in conjunction with this feature for additional protection.
>> +
>> +ETEXI
>> 
>> HXCOMM This is the last statement. Insert new options before this line!
>> STEXI
>> --
>> 2.20.1
>> 
> 

I forgot to add a "Based-on" tag (this needs the other series) and
Patchew got a bit annoyed. I'll hopefully get that right on v3 as well
as adding a missing include for fedora builds.

F.

> 
> -- 
> Marc-André Lureau
diff mbox series

Patch

diff --git a/backends/Makefile.objs b/backends/Makefile.objs
index 28a847cd57..da2a589bdf 100644
--- a/backends/Makefile.objs
+++ b/backends/Makefile.objs
@@ -9,6 +9,8 @@  common-obj-$(CONFIG_POSIX) += hostmem-file.o
 common-obj-y += cryptodev.o
 common-obj-y += cryptodev-builtin.o
 
+common-obj-y += file-fence.o
+
 ifeq ($(CONFIG_VIRTIO_CRYPTO),y)
 common-obj-y += cryptodev-vhost.o
 common-obj-$(CONFIG_VHOST_CRYPTO) += cryptodev-vhost-user.o
diff --git a/backends/file-fence.c b/backends/file-fence.c
new file mode 100644
index 0000000000..3dbbed7325
--- /dev/null
+++ b/backends/file-fence.c
@@ -0,0 +1,374 @@ 
+/*
+ * QEMU file-based self-fence mechanism
+ *
+ * Copyright (c) 2019 Nutanix Inc. All rights reserved.
+ *
+ * Authors:
+ *    Felipe Franciosi <felipe@nutanix.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qom/object_interfaces.h"
+#include "qemu/error-report.h"
+#include "qemu/filemonitor.h"
+#include "qemu/timer.h"
+
+#include <time.h>
+
+#define TYPE_FILE_FENCE "file-fence"
+
+typedef struct FileFence {
+    Object parent_obj;
+
+    gchar *dir;
+    gchar *file;
+    uint32_t qtimeout;
+    uint32_t ktimeout;
+    int signal;
+
+    timer_t ktimer;
+    QEMUTimer *qtimer;
+
+    QFileMonitor *fm;
+    uint64_t id;
+} FileFence;
+
+#define FILE_FENCE(obj) \
+    OBJECT_CHECK(FileFence, (obj), TYPE_FILE_FENCE)
+
+static void
+timer_update(FileFence *ff)
+{
+    struct itimerspec its = {
+        .it_value.tv_sec = ff->ktimeout,
+    };
+    int err;
+
+    if (ff->qtimeout) {
+        timer_mod(ff->qtimer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) +
+                              ff->qtimeout * 1000);
+    }
+
+    if (ff->ktimeout) {
+        err = timer_settime(ff->ktimer, 0, &its, NULL);
+        g_assert(err == 0);
+    }
+}
+
+static void
+file_fence_abort_cb(void *opaque)
+{
+    FileFence *ff = opaque;
+    error_printf("Fencing after %u seconds on '%s'\n",
+                 ff->qtimeout, g_strconcat(ff->dir, "/", ff->file, NULL));
+    abort();
+}
+
+static void
+file_fence_watch_cb(int64_t id, QFileMonitorEvent ev, const char *file,
+                    void *opaque)
+{
+    FileFence *ff = opaque;
+
+    if (ev != QFILE_MONITOR_EVENT_ATTRIBUTES) {
+        return;
+    }
+
+    g_assert(g_str_equal(file, ff->file));
+
+    timer_update(ff);
+}
+
+static void
+ktimer_tear(FileFence *ff)
+{
+    int err;
+
+    if (ff->ktimer) {
+        err = timer_delete(ff->ktimer);
+        g_assert(err == 0);
+        ff->ktimer = NULL;
+    }
+}
+
+static gboolean
+ktimer_setup(FileFence *ff, Error **errp)
+{
+    int err;
+
+    struct sigevent sev = {
+        .sigev_notify = SIGEV_SIGNAL,
+        .sigev_signo = ff->signal ? ff->signal : SIGKILL,
+    };
+
+    if (ff->ktimeout == 0) {
+        return TRUE;
+    }
+
+    err = timer_create(CLOCK_MONOTONIC, &sev, &ff->ktimer);
+    if (err == -1) {
+        error_setg(errp, "Error creating kernel timer: %m");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+qtimer_tear(FileFence *ff)
+{
+    if (ff->qtimer) {
+        timer_del(ff->qtimer);
+        timer_free(ff->qtimer);
+    }
+    ff->qtimer = NULL;
+}
+
+static gboolean
+qtimer_setup(FileFence *ff, Error **errp)
+{
+    QEMUTimer *qtimer;
+
+    if (ff->qtimeout == 0) {
+        return TRUE;
+    }
+
+    qtimer = timer_new_ms(QEMU_CLOCK_REALTIME, file_fence_abort_cb, ff);
+    if (qtimer == NULL) {
+        error_setg(errp, "Error creating Qemu timer");
+        return FALSE;
+    }
+
+    ff->qtimer = qtimer;
+
+    return TRUE;
+}
+
+static void
+watch_tear(FileFence *ff)
+{
+    if (ff->fm) {
+        qemu_file_monitor_remove_watch(ff->fm, ff->dir, ff->id);
+        qemu_file_monitor_free(ff->fm);
+        ff->fm = NULL;
+        ff->id = 0;
+    }
+}
+
+static gboolean
+watch_setup(FileFence *ff, Error **errp)
+{
+    QFileMonitor *fm;
+    int64_t id;
+
+    fm = qemu_file_monitor_new(errp);
+    if (!fm) {
+        return FALSE;
+    }
+
+    id = qemu_file_monitor_add_watch(fm, ff->dir, ff->file,
+                                     file_fence_watch_cb, ff, errp);
+    if (id < 0) {
+        qemu_file_monitor_free(fm);
+        return FALSE;
+    }
+
+    ff->fm = fm;
+    ff->id = id;
+
+    return TRUE;
+}
+
+static void
+file_fence_complete(UserCreatable *obj, Error **errp)
+{
+    FileFence *ff = FILE_FENCE(obj);
+
+    if (ff->dir == NULL) {
+        error_setg(errp, "A 'file' must be set");
+        return;
+    }
+
+    if (ff->signal != 0 && ff->ktimeout == 0) {
+        error_setg(errp, "Using 'signal' requires 'ktimeout' to be set");
+        return;
+    }
+
+    if (ff->ktimeout == 0 && ff->qtimeout == 0) {
+        error_setg(errp, "One or both of 'ktimeout' or 'qtimeout' must be set");
+        return;
+    }
+
+    if (ff->qtimeout >= ff->ktimeout && ff->ktimeout != 0) {
+        error_setg(errp, "Using 'qtimeout' >= 'ktimeout' doesn't make sense");
+        return;
+    }
+
+    if (!watch_setup(ff, errp) ||
+        !qtimer_setup(ff, errp) ||
+        !ktimer_setup(ff, errp)) {
+        return;
+    }
+
+    timer_update(ff);
+
+    return;
+}
+
+static void
+file_fence_set_signal(Object *obj, const char *value, Error **errp)
+{
+    FileFence *ff = FILE_FENCE(obj);
+
+    if (ff->signal) {
+        error_setg(errp, "Signal property already set");
+        return;
+    }
+
+    if (value == NULL) {
+        goto err;
+    }
+
+    if (g_ascii_strcasecmp(value, "QUIT") == 0) {
+        ff->signal = SIGQUIT;
+        return;
+    }
+
+    if (g_ascii_strcasecmp(value, "KILL") == 0) {
+        ff->signal = SIGKILL;
+        return;
+    }
+
+err:
+    error_setg(errp, "Invalid signal. Must be 'quit' or 'kill'");
+}
+
+static char *
+file_fence_get_signal(Object *obj, Error **errp)
+{
+    FileFence *ff = FILE_FENCE(obj);
+
+    switch (ff->signal) {
+    case SIGKILL:
+        return g_strdup("kill");
+    case SIGQUIT:
+        return g_strdup("quit");
+    }
+
+    /* Unreachable */
+    abort();
+}
+
+static void
+file_fence_set_file(Object *obj, const char *value, Error **errp)
+{
+    FileFence *ff = FILE_FENCE(obj);
+    g_autofree gchar *dir = NULL, *file = NULL;
+
+    if (ff->dir) {
+        error_setg(errp, "File property already set");
+        return;
+    }
+
+    dir = g_path_get_dirname(value);
+    if (g_str_equal(dir, ".")) {
+        error_setg(errp, "Path for file-fence must be absolute");
+        return;
+    }
+
+    file = g_path_get_basename(value);
+    if (g_str_equal(file, ".")) {
+        error_setg(errp, "Path for file-fence must be a file");
+        return;
+    }
+
+    ff->dir = g_steal_pointer(&dir);
+    ff->file = g_steal_pointer(&file);
+}
+
+static char *
+file_fence_get_file(Object *obj, Error **errp)
+{
+    FileFence *ff = FILE_FENCE(obj);
+
+    if (ff->file) {
+        return g_build_filename(ff->dir, ff->file, NULL);
+    }
+
+    return NULL;
+}
+
+static void
+file_fence_instance_finalize(Object *obj)
+{
+    FileFence *ff = FILE_FENCE(obj);
+
+    ktimer_tear(ff);
+    qtimer_tear(ff);
+    watch_tear(ff);
+
+    g_free(ff->file);
+    g_free(ff->dir);
+}
+
+static void
+file_fence_instance_init(Object *obj)
+{
+    FileFence *ff = FILE_FENCE(obj);
+
+    object_property_add_str(obj, "file",
+                            file_fence_get_file,
+                            file_fence_set_file,
+                            &error_abort);
+    object_property_add_str(obj, "signal",
+                            file_fence_get_signal,
+                            file_fence_set_signal,
+                            &error_abort);
+    object_property_add_uint32_ptr(obj, "qtimeout", &ff->qtimeout,
+                                   OBJ_PROP_FLAG_READWRITE, &error_abort);
+    object_property_add_uint32_ptr(obj, "ktimeout", &ff->ktimeout,
+                                   OBJ_PROP_FLAG_READWRITE, &error_abort);
+}
+
+static void
+file_fence_class_init(ObjectClass *klass, void *class_data)
+{
+    UserCreatableClass *ucc = USER_CREATABLE_CLASS(klass);
+    ucc->complete = file_fence_complete;
+}
+
+static const TypeInfo file_fence_info = {
+    .name = TYPE_FILE_FENCE,
+    .parent = TYPE_OBJECT,
+    .class_init = file_fence_class_init,
+    .instance_size = sizeof(FileFence),
+    .instance_init = file_fence_instance_init,
+    .instance_finalize = file_fence_instance_finalize,
+    .interfaces = (InterfaceInfo[]) {
+        { TYPE_USER_CREATABLE },
+        { }
+    }
+};
+
+static void
+register_types(void)
+{
+    type_register_static(&file_fence_info);
+}
+
+type_init(register_types);
diff --git a/qemu-options.hx b/qemu-options.hx
index 224a8e8712..5ea94b37af 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -4974,8 +4974,33 @@  The polling parameters can be modified at run-time using the @code{qom-set} comm
 
 @end table
 
-ETEXI
+@item -object file-fence,id=@var{id},file=@var{file},qtimeout=@var{qtimeout},ktimeout=@var{ktimeout},signal=@{signal}
+
+Self-fence Qemu if @var{file} is not modified within a given timeout.
+
+Qemu will watch @var{file} for attribute changes. Touching the file works as a
+heartbeat. This parameter is mandatory.
+
+Fencing happens after @var{qtimeout} or @var{ktimeout} seconds elapse
+without a heartbeat. At least one of these must be specified. Both may be used.
 
+When using @var{qtimeout}, an internal Qemu timer is used. Fencing with
+this method gives Qemu a chance to write a log message indicating which file
+caused the event. If Qemu's main loop is hung for whatever reason, this method
+won't successfully kill Qemu.
+
+When using @var{ktimeout}, a kernel timer is used. In this case, @var{signal}
+can be 'kill' (for SIGKILL, default) or 'quit' (for SIGQUIT). Using SIGQUIT may
+be preferred for obtaining core dumps. If Qemu is hung (eg. uninterruptable
+sleep), this method won't successfully kill Qemu.
+
+It is worth noting that even successfully killing Qemu may not be sufficient to
+completely fence a VM as certain operations like network packets or block
+commands may be pending in the kernel. If that is a concern, systems should
+consider using further fencing mechanisms like hardware watchdogs either in
+addition or in conjunction with this feature for additional protection.
+
+ETEXI
 
 HXCOMM This is the last statement. Insert new options before this line!
 STEXI