[libvirt] [PATCH v9 08/13] backup: Implement virsh support for checkpoints

Eric Blake eblake at redhat.com
Sun Jul 7 03:56:08 UTC 2019


Introduce a bunch of new virsh commands for managing checkpoints in
isolation. More commands are needed for performing incremental
backups, but these commands were easy to implement by modeling heavily
after virsh-snapshot.c. There is no need for checkpoint-revert or
checkpoint-current since those snapshot APIs have no checkpoint
counterpart.  Similarly, it is not necessary to change which
checkpoint is current when redefining from XML, since checkpoints
expose whether they are current in the public XML (rather than the way
snapshots did it behind the scenese).  checkpoint-list is a bit
simpler, in part because we don't have to cater to back-compat to
older API.  checkpoint-info is a bit trickier, because it requires
parsing XML (maybe we'll want virDomainCheckpointIsCurrent() as an API
after all).

Signed-off-by: Eric Blake <eblake at redhat.com>
---
 tools/virsh-checkpoint.h     |   26 +
 tools/virsh-completer.h      |    4 +
 tools/virsh-util.h           |    3 +
 tools/virsh.h                |    1 +
 po/POTFILES                  |    1 +
 tools/Makefile.am            |    1 +
 tools/virsh-checkpoint.c     | 1226 ++++++++++++++++++++++++++++++++++
 tools/virsh-completer.c      |   51 ++
 tools/virsh-domain-monitor.c |   23 +
 tools/virsh-domain.c         |    7 +
 tools/virsh-util.c           |   11 +
 tools/virsh.c                |    2 +
 tools/virsh.pod              |  221 +++++-
 13 files changed, 1570 insertions(+), 7 deletions(-)
 create mode 100644 tools/virsh-checkpoint.h
 create mode 100644 tools/virsh-checkpoint.c

diff --git a/tools/virsh-checkpoint.h b/tools/virsh-checkpoint.h
new file mode 100644
index 0000000000..7cc998638f
--- /dev/null
+++ b/tools/virsh-checkpoint.h
@@ -0,0 +1,26 @@
+/*
+ * virsh-checkpoint.h: Commands to manage domain checkpoints
+ *
+ * Copyright (C) 2005-2019 Red Hat, Inc.
+ *
+ * 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.1 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/>.
+ *
+ */
+
+#pragma once
+
+#include "virsh.h"
+
+extern const vshCmdDef checkpointCmds[];
diff --git a/tools/virsh-completer.h b/tools/virsh-completer.h
index 9b3951f4fd..fc377f798b 100644
--- a/tools/virsh-completer.h
+++ b/tools/virsh-completer.h
@@ -78,6 +78,10 @@ char ** virshSecretUUIDCompleter(vshControl *ctl,
                                  const vshCmd *cmd,
                                  unsigned int flags);

+char ** virshCheckpointNameCompleter(vshControl *ctl,
+                                     const vshCmd *cmd,
+                                     unsigned int flags);
+
 char ** virshSnapshotNameCompleter(vshControl *ctl,
                                    const vshCmd *cmd,
                                    unsigned int flags);
diff --git a/tools/virsh-util.h b/tools/virsh-util.h
index 55520302ff..9005aa9d36 100644
--- a/tools/virsh-util.h
+++ b/tools/virsh-util.h
@@ -42,6 +42,9 @@ virshCommandOptDomain(vshControl *ctl,
 void
 virshDomainFree(virDomainPtr dom);

+void
+virshDomainCheckpointFree(virDomainCheckpointPtr chk);
+
 void
 virshDomainSnapshotFree(virDomainSnapshotPtr snap);

diff --git a/tools/virsh.h b/tools/virsh.h
index 847ed25151..b4e610b2a4 100644
--- a/tools/virsh.h
+++ b/tools/virsh.h
@@ -40,6 +40,7 @@
 /*
  * Command group types
  */
+#define VIRSH_CMD_GRP_CHECKPOINT       "Checkpoint"
 #define VIRSH_CMD_GRP_DOM_MANAGEMENT   "Domain Management"
 #define VIRSH_CMD_GRP_DOM_MONITORING   "Domain Monitoring"
 #define VIRSH_CMD_GRP_STORAGE_POOL     "Storage Pool"
diff --git a/po/POTFILES b/po/POTFILES
index adf907ed93..460e54d4fb 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -298,6 +298,7 @@ src/xenconfig/xen_xl.c
 src/xenconfig/xen_xm.c
 tests/virpolkittest.c
 tools/libvirt-guests.sh.in
+tools/virsh-checkpoint.c
 tools/virsh-console.c
 tools/virsh-domain-monitor.c
 tools/virsh-domain.c
diff --git a/tools/Makefile.am b/tools/Makefile.am
index c6064dee08..2807b9f6fd 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -216,6 +216,7 @@ virt_login_shell_CFLAGS = \

 virsh_SOURCES = \
 		virsh.c virsh.h \
+		virsh-checkpoint.c virsh-checkpoint.h \
 		virsh-completer.c virsh-completer.h \
 		virsh-console.c virsh-console.h \
 		virsh-domain.c virsh-domain.h \
diff --git a/tools/virsh-checkpoint.c b/tools/virsh-checkpoint.c
new file mode 100644
index 0000000000..8200687f8a
--- /dev/null
+++ b/tools/virsh-checkpoint.c
@@ -0,0 +1,1226 @@
+/*
+ * virsh-checkpoint.c: Commands to manage domain checkpoints
+ *
+ * Copyright (C) 2005-2019 Red Hat, Inc.
+ *
+ * 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.1 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/>.
+ *
+ *  Daniel Veillard <veillard at redhat.com>
+ *  Karel Zak <kzak at redhat.com>
+ *  Daniel P. Berrange <berrange at redhat.com>
+ *
+ */
+
+#include <config.h>
+#include "virsh-checkpoint.h"
+
+#include <assert.h>
+
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/xpath.h>
+#include <libxml/xmlsave.h>
+
+#include "internal.h"
+#include "virbuffer.h"
+#include "viralloc.h"
+#include "virfile.h"
+#include "virsh-util.h"
+#include "virstring.h"
+#include "virxml.h"
+#include "conf/checkpoint_conf.h"
+#include "vsh-table.h"
+
+/* Helper for checkpoint-create and checkpoint-create-as */
+static bool
+virshCheckpointCreate(vshControl *ctl,
+                      virDomainPtr dom,
+                      const char *buffer,
+                      unsigned int flags,
+                      const char *from)
+{
+    bool ret = false;
+    virDomainCheckpointPtr checkpoint;
+    const char *name = NULL;
+
+    checkpoint = virDomainCheckpointCreateXML(dom, buffer, flags);
+
+    if (checkpoint == NULL)
+        goto cleanup;
+
+    name = virDomainCheckpointGetName(checkpoint);
+    if (!name) {
+        vshError(ctl, "%s", _("Could not get checkpoint name"));
+        goto cleanup;
+    }
+
+    if (from)
+        vshPrintExtra(ctl, _("Domain checkpoint %s created from '%s'"),
+                      name, from);
+    else
+        vshPrintExtra(ctl, _("Domain checkpoint %s created"), name);
+
+    ret = true;
+
+ cleanup:
+    virshDomainCheckpointFree(checkpoint);
+    return ret;
+}
+
+
+/*
+ * "checkpoint-create" command
+ */
+static const vshCmdInfo info_checkpoint_create[] = {
+    {.name = "help",
+     .data = N_("Create a checkpoint from XML")
+    },
+    {.name = "desc",
+     .data = N_("Create a checkpoint from XML for use in "
+                "future incremental backups")
+    },
+    {.name = NULL}
+};
+
+static const vshCmdOptDef opts_checkpoint_create[] = {
+    VIRSH_COMMON_OPT_DOMAIN_FULL(0),
+    {.name = "xmlfile",
+     .type = VSH_OT_STRING,
+     .help = N_("domain checkpoint XML")
+    },
+    {.name = "redefine",
+     .type = VSH_OT_BOOL,
+     .help = N_("redefine metadata for existing checkpoint")
+    },
+    {.name = "no-metadata",
+     .type = VSH_OT_BOOL,
+     .help = N_("create checkpoint but create no metadata")
+    },
+    {.name = "quiesce",
+     .type = VSH_OT_BOOL,
+     .help = N_("quiesce guest's file systems")
+    },
+    {.name = NULL}
+};
+
+static bool
+cmdCheckpointCreate(vshControl *ctl,
+                    const vshCmd *cmd)
+{
+    virDomainPtr dom = NULL;
+    bool ret = false;
+    const char *from = NULL;
+    char *buffer = NULL;
+    unsigned int flags = 0;
+
+    if (vshCommandOptBool(cmd, "redefine"))
+        flags |= VIR_DOMAIN_CHECKPOINT_CREATE_REDEFINE;
+    if (vshCommandOptBool(cmd, "no-metadata"))
+        flags |= VIR_DOMAIN_CHECKPOINT_CREATE_NO_METADATA;
+    if (vshCommandOptBool(cmd, "quiesce"))
+        flags |= VIR_DOMAIN_CHECKPOINT_CREATE_QUIESCE;
+
+    if (!(dom = virshCommandOptDomain(ctl, cmd, NULL)))
+        goto cleanup;
+
+    if (vshCommandOptStringReq(ctl, cmd, "xmlfile", &from) < 0)
+        goto cleanup;
+    if (!from) {
+        buffer = vshStrdup(ctl, "<domaincheckpoint/>");
+    } else {
+        if (virFileReadAll(from, VSH_MAX_XML_FILE, &buffer) < 0) {
+            vshSaveLibvirtError();
+            goto cleanup;
+        }
+    }
+
+    ret = virshCheckpointCreate(ctl, dom, buffer, flags, from);
+
+ cleanup:
+    VIR_FREE(buffer);
+    virshDomainFree(dom);
+
+    return ret;
+}
+
+
+/*
+ * "checkpoint-create-as" command
+ */
+static int
+virshParseCheckpointDiskspec(vshControl *ctl,
+                             virBufferPtr buf,
+                             const char *str)
+{
+    int ret = -1;
+    const char *name = NULL;
+    const char *checkpoint = NULL;
+    const char *bitmap = NULL;
+    char **array = NULL;
+    int narray;
+    size_t i;
+
+    narray = vshStringToArray(str, &array);
+    if (narray <= 0)
+        goto cleanup;
+
+    name = array[0];
+    for (i = 1; i < narray; i++) {
+        if (!checkpoint && STRPREFIX(array[i], "checkpoint="))
+            checkpoint = array[i] + strlen("checkpoint=");
+        else if (!bitmap && STRPREFIX(array[i], "bitmap="))
+            bitmap = array[i] + strlen("bitmap=");
+        else
+            goto cleanup;
+    }
+
+    virBufferEscapeString(buf, "<disk name='%s'", name);
+    if (checkpoint)
+        virBufferAsprintf(buf, " checkpoint='%s'", checkpoint);
+    if (bitmap)
+        virBufferAsprintf(buf, " bitmap='%s'", bitmap);
+    virBufferAddLit(buf, "/>\n");
+    ret = 0;
+ cleanup:
+    if (ret < 0)
+        vshError(ctl, _("unable to parse diskspec: %s"), str);
+    virStringListFree(array);
+    return ret;
+}
+
+static const vshCmdInfo info_checkpoint_create_as[] = {
+    {.name = "help",
+     .data = N_("Create a checkpoint from a set of args")
+    },
+    {.name = "desc",
+     .data = N_("Create a checkpoint from arguments for use in "
+                "future incremental backups")
+    },
+    {.name = NULL}
+};
+
+static const vshCmdOptDef opts_checkpoint_create_as[] = {
+    VIRSH_COMMON_OPT_DOMAIN_FULL(0),
+    {.name = "name",
+     .type = VSH_OT_STRING,
+     .help = N_("name of checkpoint")
+    },
+    {.name = "description",
+     .type = VSH_OT_STRING,
+     .help = N_("description of checkpoint")
+    },
+    {.name = "print-xml",
+     .type = VSH_OT_BOOL,
+     .help = N_("print XML document rather than create")
+    },
+    {.name = "no-metadata",
+     .type = VSH_OT_BOOL,
+     .help = N_("take checkpoint but create no metadata")
+    },
+    {.name = "quiesce",
+     .type = VSH_OT_BOOL,
+     .help = N_("quiesce guest's file systems")
+    },
+    {.name = "diskspec",
+     .type = VSH_OT_ARGV,
+     .help = N_("disk attributes: disk[,checkpoint=type][,bitmap=name]")
+    },
+    {.name = NULL}
+};
+
+
+static bool
+cmdCheckpointCreateAs(vshControl *ctl,
+                      const vshCmd *cmd)
+{
+    virDomainPtr dom = NULL;
+    bool ret = false;
+    char *buffer = NULL;
+    const char *name = NULL;
+    const char *desc = NULL;
+    virBuffer buf = VIR_BUFFER_INITIALIZER;
+    unsigned int flags = 0;
+    const vshCmdOpt *opt = NULL;
+
+    if (vshCommandOptBool(cmd, "no-metadata"))
+        flags |= VIR_DOMAIN_CHECKPOINT_CREATE_NO_METADATA;
+    if (vshCommandOptBool(cmd, "quiesce"))
+        flags |= VIR_DOMAIN_CHECKPOINT_CREATE_QUIESCE;
+
+    if (!(dom = virshCommandOptDomain(ctl, cmd, NULL)))
+        return false;
+
+    if (vshCommandOptStringReq(ctl, cmd, "name", &name) < 0 ||
+        vshCommandOptStringReq(ctl, cmd, "description", &desc) < 0)
+        goto cleanup;
+
+    virBufferAddLit(&buf, "<domaincheckpoint>\n");
+    virBufferAdjustIndent(&buf, 2);
+    virBufferEscapeString(&buf, "<name>%s</name>\n", name);
+    virBufferEscapeString(&buf, "<description>%s</description>\n", desc);
+
+    if (vshCommandOptBool(cmd, "diskspec")) {
+        virBufferAddLit(&buf, "<disks>\n");
+        virBufferAdjustIndent(&buf, 2);
+        while ((opt = vshCommandOptArgv(ctl, cmd, opt))) {
+            if (virshParseCheckpointDiskspec(ctl, &buf, opt->data) < 0)
+                goto cleanup;
+        }
+        virBufferAdjustIndent(&buf, -2);
+        virBufferAddLit(&buf, "</disks>\n");
+    }
+    virBufferAdjustIndent(&buf, -2);
+    virBufferAddLit(&buf, "</domaincheckpoint>\n");
+
+    if (virBufferError(&buf)) {
+        vshError(ctl, "%s", _("Out of memory"));
+        goto cleanup;
+    }
+
+    buffer = virBufferContentAndReset(&buf);
+
+    if (vshCommandOptBool(cmd, "print-xml")) {
+        vshPrint(ctl, "%s\n",  buffer);
+        ret = true;
+        goto cleanup;
+    }
+
+    ret = virshCheckpointCreate(ctl, dom, buffer, flags, NULL);
+
+ cleanup:
+    virBufferFreeAndReset(&buf);
+    VIR_FREE(buffer);
+    virshDomainFree(dom);
+
+    return ret;
+}
+
+
+/* Helper for resolving {--current | --ARG name} into a checkpoint
+ * belonging to DOM.  On success, populate *CHK and *NAME, before
+ * returning 0.  On failure, return -1 after issuing an error
+ * message.  */
+static int
+virshLookupCheckpoint(vshControl *ctl,
+                      const vshCmd *cmd,
+                      const char *arg,
+                      virDomainPtr dom,
+                      virDomainCheckpointPtr *chk,
+                      const char **name)
+{
+    bool current = vshCommandOptBool(cmd, "current");
+    const char *chkname = NULL;
+
+    if (vshCommandOptStringReq(ctl, cmd, arg, &chkname) < 0)
+        return -1;
+
+    if (current && chkname) {
+        vshError(ctl, _("--%s and --current are mutually exclusive"), arg);
+        return -1;
+    }
+
+    if (chkname) {
+        *chk = virDomainCheckpointLookupByName(dom, chkname, 0);
+    } else if (current) {
+        int count;
+        virDomainCheckpointPtr *checkpoints = NULL;
+
+        count = virDomainListAllCheckpoints(dom, &checkpoints,
+                                            VIR_DOMAIN_CHECKPOINT_LIST_CURRENT);
+        if (count < 0)
+            return -1;
+        if (count == 0) {
+            vshError(ctl, _("domain has no current checkpoint"));
+            return -1;
+        }
+        if (count > 1) {
+            while (count-- > 0)
+                virDomainCheckpointFree(checkpoints[count]);
+            VIR_FREE(checkpoints);
+            vshError(ctl, _("domain has more than one current checkpoint"));
+            return -1;
+        }
+        *chk = checkpoints[0];
+        VIR_FREE(checkpoints);
+    } else {
+        vshError(ctl, _("--%s or --current is required"), arg);
+        return -1;
+    }
+    if (!*chk) {
+        vshReportError(ctl);
+        return -1;
+    }
+
+    *name = virDomainCheckpointGetName(*chk);
+    return 0;
+}
+
+
+/*
+ * "checkpoint-edit" command
+ */
+static const vshCmdInfo info_checkpoint_edit[] = {
+    {.name = "help",
+     .data = N_("edit XML for a checkpoint")
+    },
+    {.name = "desc",
+     .data = N_("Edit the domain checkpoint XML for a named checkpoint")
+    },
+    {.name = NULL}
+};
+
+static const vshCmdOptDef opts_checkpoint_edit[] = {
+    VIRSH_COMMON_OPT_DOMAIN_FULL(0),
+    {.name = "checkpointname",
+     .type = VSH_OT_STRING,
+     .help = N_("checkpoint name"),
+     .completer = virshCheckpointNameCompleter,
+    },
+    VIRSH_COMMON_OPT_CURRENT(N_("edit current checkpoint")),
+    {.name = NULL}
+};
+
+static bool
+cmdCheckpointEdit(vshControl *ctl,
+                  const vshCmd *cmd)
+{
+    virDomainPtr dom = NULL;
+    virDomainCheckpointPtr checkpoint = NULL;
+    virDomainCheckpointPtr edited = NULL;
+    const char *name = NULL;
+    const char *edited_name;
+    bool ret = false;
+    unsigned int getxml_flags = VIR_DOMAIN_CHECKPOINT_XML_SECURE;
+    unsigned int define_flags = VIR_DOMAIN_CHECKPOINT_CREATE_REDEFINE;
+
+    if (!(dom = virshCommandOptDomain(ctl, cmd, NULL)))
+        return false;
+
+    if (virshLookupCheckpoint(ctl, cmd, "checkpointname", dom,
+                              &checkpoint, &name) < 0)
+        goto cleanup;
+
+#define EDIT_GET_XML \
+    virDomainCheckpointGetXMLDesc(checkpoint, getxml_flags)
+#define EDIT_NOT_CHANGED \
+    do { \
+        vshPrintExtra(ctl, \
+                      _("Checkpoint %s XML configuration not changed.\n"), \
+                      name); \
+        ret = true; \
+        goto edit_cleanup; \
+    } while (0)
+#define EDIT_DEFINE \
+    edited = virDomainCheckpointCreateXML(dom, doc_edited, define_flags)
+#include "virsh-edit.c"
+
+    edited_name = virDomainCheckpointGetName(edited);
+    if (STREQ(name, edited_name)) {
+        vshPrintExtra(ctl, _("Checkpoint %s edited.\n"), name);
+    } else {
+        unsigned int delete_flags = VIR_DOMAIN_CHECKPOINT_DELETE_METADATA_ONLY;
+
+        if (virDomainCheckpointDelete(edited, delete_flags) < 0) {
+            vshReportError(ctl);
+            vshError(ctl, _("Failed to clean up %s"), edited_name);
+            goto cleanup;
+        }
+        vshError(ctl, _("Cannot rename checkpoint %s to %s"),
+                 name, edited_name);
+        goto cleanup;
+    }
+
+    ret = true;
+
+ cleanup:
+    if (!ret && name)
+        vshError(ctl, _("Failed to update %s"), name);
+    virshDomainCheckpointFree(edited);
+    virshDomainCheckpointFree(checkpoint);
+    virshDomainFree(dom);
+    return ret;
+}
+
+
+/* Helper function to get the name of a checkpoint's parent.  Caller
+ * must free the result.  Returns 0 on success (including when it was
+ * proven no parent exists), and -1 on failure with error reported
+ * (such as no checkpoint support or domain deleted in meantime).  */
+static int
+virshGetCheckpointParent(vshControl *ctl,
+                         virDomainCheckpointPtr checkpoint,
+                         char **parent_name)
+{
+    virDomainCheckpointPtr parent = NULL;
+    int ret = -1;
+
+    *parent_name = NULL;
+
+    parent = virDomainCheckpointGetParent(checkpoint, 0);
+    if (parent) {
+        /* API works, and virDomainCheckpointGetName will succeed */
+        *parent_name = vshStrdup(ctl, virDomainCheckpointGetName(parent));
+        ret = 0;
+    } else if (last_error->code == VIR_ERR_NO_DOMAIN_CHECKPOINT) {
+        /* API works, and we found a root with no parent */
+        ret = 0;
+    }
+
+    if (ret < 0) {
+        vshReportError(ctl);
+        vshError(ctl, "%s", _("unable to determine if checkpoint has parent"));
+    } else {
+        vshResetLibvirtError();
+    }
+    virshDomainCheckpointFree(parent);
+    return ret;
+}
+
+
+/*
+ * "checkpoint-info" command
+ */
+static const vshCmdInfo info_checkpoint_info[] = {
+    {.name = "help",
+     .data = N_("checkpoint information")
+    },
+    {.name = "desc",
+     .data = N_("Returns basic information about a checkpoint.")
+    },
+    {.name = NULL}
+};
+
+static const vshCmdOptDef opts_checkpoint_info[] = {
+    VIRSH_COMMON_OPT_DOMAIN_FULL(0),
+    {.name = "checkpointname",
+     .type = VSH_OT_STRING,
+     .help = N_("checkpoint name"),
+     .completer = virshCheckpointNameCompleter,
+    },
+    VIRSH_COMMON_OPT_CURRENT(N_("info on current checkpoint")),
+    {.name = NULL}
+};
+
+
+static bool
+cmdCheckpointInfo(vshControl *ctl,
+                  const vshCmd *cmd)
+{
+    virDomainPtr dom;
+    virDomainCheckpointPtr checkpoint = NULL;
+    const char *name;
+    char *parent = NULL;
+    char *xml = NULL;
+    xmlDocPtr xmldoc = NULL;
+    xmlXPathContextPtr ctxt = NULL;
+    bool ret = false;
+    int count;
+    unsigned int flags;
+    int current;
+
+    dom = virshCommandOptDomain(ctl, cmd, NULL);
+    if (dom == NULL)
+        return false;
+
+    if (virshLookupCheckpoint(ctl, cmd, "checkpointname", dom,
+                              &checkpoint, &name) < 0)
+        goto cleanup;
+
+    vshPrint(ctl, "%-15s %s\n", _("Name:"), name);
+    vshPrint(ctl, "%-15s %s\n", _("Domain:"), virDomainGetName(dom));
+
+    /* Determine if checkpoint is current.  */
+    xml = virDomainCheckpointGetXMLDesc(checkpoint,
+                                        VIR_DOMAIN_CHECKPOINT_XML_NO_DOMAIN);
+    if (!xml)
+        goto cleanup;
+
+    xmldoc = virXMLParseStringCtxt(xml, _("(domain_checkpoint)"), &ctxt);
+    if (!xmldoc)
+        goto cleanup;
+
+    if (virXPathInt("string(/domaincheckpoint/current)", ctxt, &current) < 0)
+        goto cleanup;
+    vshPrint(ctl, "%-15s %s\n", _("Current:"),
+             current > 0 ? _("yes") : _("no"));
+
+    if (virshGetCheckpointParent(ctl, checkpoint, &parent) < 0) {
+        vshError(ctl, "%s",
+                 _("unexpected problem querying checkpoint state"));
+        goto cleanup;
+    }
+    vshPrint(ctl, "%-15s %s\n", _("Parent:"), parent ? parent : "-");
+
+    /* Children, Descendants.  */
+    flags = 0;
+    count = virDomainCheckpointListAllChildren(checkpoint, NULL, flags);
+    if (count < 0) {
+        if (last_error->code == VIR_ERR_NO_SUPPORT) {
+            vshResetLibvirtError();
+            ret = true;
+        }
+        goto cleanup;
+    }
+    vshPrint(ctl, "%-15s %d\n", _("Children:"), count);
+    flags = VIR_DOMAIN_CHECKPOINT_LIST_DESCENDANTS;
+    count = virDomainCheckpointListAllChildren(checkpoint, NULL, flags);
+    if (count < 0)
+        goto cleanup;
+    vshPrint(ctl, "%-15s %d\n", _("Descendants:"), count);
+
+    ret = true;
+
+ cleanup:
+    xmlXPathFreeContext(ctxt);
+    xmlFreeDoc(xmldoc);
+    VIR_FREE(xml);
+    VIR_FREE(parent);
+    virshDomainCheckpointFree(checkpoint);
+    virshDomainFree(dom);
+    return ret;
+}
+
+
+/* Helpers for collecting a list of checkpoints.  */
+struct virshChk {
+    virDomainCheckpointPtr chk;
+    char *parent;
+};
+struct virshCheckpointList {
+    struct virshChk *chks;
+    int nchks;
+};
+typedef struct virshCheckpointList *virshCheckpointListPtr;
+
+static void
+virshCheckpointListFree(virshCheckpointListPtr checkpointlist)
+{
+    size_t i;
+
+    if (!checkpointlist)
+        return;
+    if (checkpointlist->chks) {
+        for (i = 0; i < checkpointlist->nchks; i++) {
+            virshDomainCheckpointFree(checkpointlist->chks[i].chk);
+            VIR_FREE(checkpointlist->chks[i].parent);
+        }
+        VIR_FREE(checkpointlist->chks);
+    }
+    VIR_FREE(checkpointlist);
+}
+
+
+static int
+virshChkSorter(const void *a,
+               const void *b)
+{
+    const struct virshChk *sa = a;
+    const struct virshChk *sb = b;
+
+    if (sa->chk && !sb->chk)
+        return -1;
+    if (!sa->chk)
+        return sb->chk != NULL;
+
+    return vshStrcasecmp(virDomainCheckpointGetName(sa->chk),
+                         virDomainCheckpointGetName(sb->chk));
+}
+
+
+/* Compute a list of checkpoints from DOM.  If FROM is provided, the
+ * list is limited to descendants of the given checkpoint.  If FLAGS is
+ * given, the list is filtered.  If TREE is specified, then all but
+ * FROM or the roots will also have parent information.  */
+static virshCheckpointListPtr
+virshCheckpointListCollect(vshControl *ctl,
+                           virDomainPtr dom,
+                           virDomainCheckpointPtr from,
+                           unsigned int orig_flags,
+                           bool tree)
+{
+    size_t i;
+    char **names = NULL;
+    int count = -1;
+    virDomainCheckpointPtr *chks;
+    virshCheckpointListPtr checkpointlist = vshMalloc(ctl,
+                                                      sizeof(*checkpointlist));
+    virshCheckpointListPtr ret = NULL;
+    unsigned int flags = orig_flags;
+
+    if (from)
+        count = virDomainCheckpointListAllChildren(from, &chks, flags);
+    else
+        count = virDomainListAllCheckpoints(dom, &chks, flags);
+    if (count < 0) {
+        vshError(ctl, "%s",
+                 _("unexpected problem querying checkpoints"));
+        goto cleanup;
+    }
+
+    /* When mixing --from and --tree, we also want a copy of from
+     * in the list, but with no parent for that one entry.  */
+    checkpointlist->chks = vshCalloc(ctl, count + (tree && from),
+                                     sizeof(*checkpointlist->chks));
+    checkpointlist->nchks = count;
+    for (i = 0; i < count; i++)
+        checkpointlist->chks[i].chk = chks[i];
+    VIR_FREE(chks);
+    if (tree) {
+        for (i = 0; i < count; i++) {
+            if (virshGetCheckpointParent(ctl, checkpointlist->chks[i].chk,
+                                         &checkpointlist->chks[i].parent) < 0)
+                goto cleanup;
+        }
+        if (from) {
+            checkpointlist->chks[checkpointlist->nchks++].chk = from;
+            virDomainCheckpointRef(from);
+        }
+    }
+
+    if (!(orig_flags & VIR_DOMAIN_CHECKPOINT_LIST_TOPOLOGICAL))
+        qsort(checkpointlist->chks, checkpointlist->nchks,
+              sizeof(*checkpointlist->chks), virshChkSorter);
+
+    ret = checkpointlist;
+    checkpointlist = NULL;
+
+ cleanup:
+    virshCheckpointListFree(checkpointlist);
+    if (names && count > 0)
+        for (i = 0; i < count; i++)
+            VIR_FREE(names[i]);
+    VIR_FREE(names);
+    return ret;
+}
+
+
+static const char *
+virshCheckpointListLookup(int id,
+                          bool parent,
+                          void *opaque)
+{
+    virshCheckpointListPtr checkpointlist = opaque;
+    if (parent)
+        return checkpointlist->chks[id].parent;
+    return virDomainCheckpointGetName(checkpointlist->chks[id].chk);
+}
+
+
+/*
+ * "checkpoint-list" command
+ */
+static const vshCmdInfo info_checkpoint_list[] = {
+    {.name = "help",
+     .data = N_("List checkpoints for a domain")
+    },
+    {.name = "desc",
+     .data = N_("Checkpoint List")
+    },
+    {.name = NULL}
+};
+
+static const vshCmdOptDef opts_checkpoint_list[] = {
+    VIRSH_COMMON_OPT_DOMAIN_FULL(0),
+    {.name = "parent",
+     .type = VSH_OT_BOOL,
+     .help = N_("add a column showing parent checkpoint")
+    },
+    {.name = "roots",
+     .type = VSH_OT_BOOL,
+     .help = N_("list only checkpoints without parents")
+    },
+    {.name = "leaves",
+     .type = VSH_OT_BOOL,
+     .help = N_("list only checkpoints without children")
+    },
+    {.name = "no-leaves",
+     .type = VSH_OT_BOOL,
+     .help = N_("list only checkpoints that are not leaves (with children)")
+    },
+    {.name = "tree",
+     .type = VSH_OT_BOOL,
+     .help = N_("list checkpoints in a tree")
+    },
+    {.name = "from",
+     .type = VSH_OT_STRING,
+     .help = N_("limit list to children of given checkpoint"),
+     .completer = virshCheckpointNameCompleter,
+    },
+    VIRSH_COMMON_OPT_CURRENT(N_("limit list to children of current checkpoint")),
+    {.name = "descendants",
+     .type = VSH_OT_BOOL,
+     .help = N_("with --from, list all descendants")
+    },
+    {.name = "name",
+     .type = VSH_OT_BOOL,
+     .help = N_("list checkpoint names only")
+    },
+    {.name = "topological",
+     .type = VSH_OT_BOOL,
+     .help = N_("sort list topologically rather than by name"),
+    },
+    {.name = "current-only",
+     .type = VSH_OT_BOOL,
+     .help = N_("filter to just current checkpoints"),
+    },
+    {.name = "no-current",
+     .type = VSH_OT_BOOL,
+     .help = N_("filter out current checkpoints"),
+    },
+    {.name = NULL}
+};
+
+static bool
+cmdCheckpointList(vshControl *ctl,
+                  const vshCmd *cmd)
+{
+    virDomainPtr dom = NULL;
+    bool ret = false;
+    unsigned int flags = 0;
+    size_t i;
+    xmlDocPtr xml = NULL;
+    xmlXPathContextPtr ctxt = NULL;
+    char *doc = NULL;
+    virDomainCheckpointPtr checkpoint = NULL;
+    long long creation_longlong;
+    time_t creation_time_t;
+    char timestr[100];
+    struct tm time_info;
+    bool tree = vshCommandOptBool(cmd, "tree");
+    bool name = vshCommandOptBool(cmd, "name");
+    bool from = vshCommandOptBool(cmd, "from");
+    bool parent = vshCommandOptBool(cmd, "parent");
+    bool roots = vshCommandOptBool(cmd, "roots");
+    bool current = vshCommandOptBool(cmd, "current");
+    const char *from_chk = NULL;
+    char *parent_chk = NULL;
+    virDomainCheckpointPtr start = NULL;
+    virshCheckpointListPtr checkpointlist = NULL;
+    vshTablePtr table = NULL;
+
+    VSH_EXCLUSIVE_OPTIONS_VAR(tree, name);
+    VSH_EXCLUSIVE_OPTIONS_VAR(parent, roots);
+    VSH_EXCLUSIVE_OPTIONS_VAR(parent, tree);
+    VSH_EXCLUSIVE_OPTIONS_VAR(roots, tree);
+    VSH_EXCLUSIVE_OPTIONS_VAR(roots, from);
+    VSH_EXCLUSIVE_OPTIONS_VAR(roots, current);
+
+#define FILTER(option, flag) \
+    do { \
+        if (vshCommandOptBool(cmd, option)) { \
+            if (tree) { \
+                vshError(ctl, \
+                         _("--%s and --tree are mutually exclusive"), \
+                         option); \
+                return false; \
+            } \
+            flags |= VIR_DOMAIN_CHECKPOINT_LIST_ ## flag; \
+        } \
+    } while (0)
+
+    FILTER("leaves", LEAVES);
+    FILTER("no-leaves", NO_LEAVES);
+    FILTER("current-only", CURRENT);
+    FILTER("no-current", NO_CURRENT);
+#undef FILTER
+
+    if (vshCommandOptBool(cmd, "topological"))
+        flags |= VIR_DOMAIN_CHECKPOINT_LIST_TOPOLOGICAL;
+
+    if (roots)
+        flags |= VIR_DOMAIN_CHECKPOINT_LIST_ROOTS;
+
+    if (vshCommandOptBool(cmd, "descendants")) {
+        if (!from && !current) {
+            vshError(ctl, "%s",
+                     _("--descendants requires either --from or --current"));
+            return false;
+        }
+        flags |= VIR_DOMAIN_CHECKPOINT_LIST_DESCENDANTS;
+    }
+
+    if (!(dom = virshCommandOptDomain(ctl, cmd, NULL)))
+        return false;
+
+    if ((from || current) &&
+        virshLookupCheckpoint(ctl, cmd, "from", dom, &start, &from_chk) < 0)
+        goto cleanup;
+
+    if (!(checkpointlist = virshCheckpointListCollect(ctl, dom, start, flags,
+                                                      tree)))
+        goto cleanup;
+
+    if (!tree && !name) {
+        if (parent)
+            table = vshTableNew(_("Name"), _("Creation Time"), _("Parent"),
+                                NULL);
+        else
+            table = vshTableNew(_("Name"), _("Creation Time"), NULL);
+        if (!table)
+            goto cleanup;
+    }
+
+    if (tree) {
+        for (i = 0; i < checkpointlist->nchks; i++) {
+            if (!checkpointlist->chks[i].parent &&
+                vshTreePrint(ctl, virshCheckpointListLookup, checkpointlist,
+                             checkpointlist->nchks, i) < 0)
+                goto cleanup;
+        }
+        ret = true;
+        goto cleanup;
+    }
+
+    for (i = 0; i < checkpointlist->nchks; i++) {
+        const char *chk_name;
+
+        /* free up memory from previous iterations of the loop */
+        VIR_FREE(parent_chk);
+        xmlXPathFreeContext(ctxt);
+        xmlFreeDoc(xml);
+        VIR_FREE(doc);
+
+        checkpoint = checkpointlist->chks[i].chk;
+        chk_name = virDomainCheckpointGetName(checkpoint);
+        assert(chk_name);
+
+        if (name) {
+            /* just print the checkpoint name */
+            vshPrint(ctl, "%s\n", chk_name);
+            continue;
+        }
+
+        if (!(doc = virDomainCheckpointGetXMLDesc(checkpoint, 0)))
+            continue;
+
+        if (!(xml = virXMLParseStringCtxt(doc, _("(domain_checkpoint)"), &ctxt)))
+            continue;
+
+        if (parent)
+            parent_chk = virXPathString("string(/domaincheckpoint/parent/name)",
+                                        ctxt);
+
+        if (virXPathLongLong("string(/domaincheckpoint/creationTime)", ctxt,
+                             &creation_longlong) < 0)
+            continue;
+        creation_time_t = creation_longlong;
+        if (creation_time_t != creation_longlong) {
+            vshError(ctl, "%s", _("time_t overflow"));
+            continue;
+        }
+        localtime_r(&creation_time_t, &time_info);
+        strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M:%S %z",
+                 &time_info);
+
+        if (parent) {
+            if (vshTableRowAppend(table, chk_name, timestr,
+                                  NULLSTR_EMPTY(parent_chk), NULL) < 0)
+                goto cleanup;
+        } else {
+            if (vshTableRowAppend(table, chk_name, timestr, NULL) < 0)
+                goto cleanup;
+        }
+    }
+
+    if (table)
+        vshTablePrintToStdout(table, ctl);
+
+    ret = true;
+
+ cleanup:
+    /* this frees up memory from the last iteration of the loop */
+    virshCheckpointListFree(checkpointlist);
+    VIR_FREE(parent_chk);
+    virshDomainCheckpointFree(start);
+    xmlXPathFreeContext(ctxt);
+    xmlFreeDoc(xml);
+    VIR_FREE(doc);
+    virshDomainFree(dom);
+    vshTableFree(table);
+
+    return ret;
+}
+
+
+/*
+ * "checkpoint-dumpxml" command
+ */
+static const vshCmdInfo info_checkpoint_dumpxml[] = {
+    {.name = "help",
+     .data = N_("Dump XML for a domain checkpoint")
+    },
+    {.name = "desc",
+     .data = N_("Checkpoint Dump XML")
+    },
+    {.name = NULL}
+};
+
+static const vshCmdOptDef opts_checkpoint_dumpxml[] = {
+    VIRSH_COMMON_OPT_DOMAIN_FULL(0),
+    {.name = "checkpointname",
+     .type = VSH_OT_STRING,
+     .help = N_("checkpoint name"),
+     .completer = virshCheckpointNameCompleter,
+    },
+    VIRSH_COMMON_OPT_CURRENT(N_("dump xml for current checkpoint")),
+    {.name = "security-info",
+     .type = VSH_OT_BOOL,
+     .help = N_("include security sensitive information in XML dump")
+    },
+    {.name = "no-domain",
+     .type = VSH_OT_BOOL,
+     .help = N_("exclude <domain> from XML")
+    },
+    {.name = "size",
+     .type = VSH_OT_BOOL,
+     .help = N_("include backup size estimate in XML dump")
+    },
+    {.name = NULL}
+};
+
+static bool
+cmdCheckpointDumpXML(vshControl *ctl,
+                     const vshCmd *cmd)
+{
+    virDomainPtr dom = NULL;
+    bool ret = false;
+    const char *name = NULL;
+    virDomainCheckpointPtr checkpoint = NULL;
+    char *xml = NULL;
+    unsigned int flags = 0;
+
+    if (vshCommandOptBool(cmd, "security-info"))
+        flags |= VIR_DOMAIN_CHECKPOINT_XML_SECURE;
+    if (vshCommandOptBool(cmd, "no-domain"))
+        flags |= VIR_DOMAIN_CHECKPOINT_XML_NO_DOMAIN;
+    if (vshCommandOptBool(cmd, "size"))
+        flags |= VIR_DOMAIN_CHECKPOINT_XML_SIZE;
+
+    if (!(dom = virshCommandOptDomain(ctl, cmd, NULL)))
+        return false;
+
+    if (virshLookupCheckpoint(ctl, cmd, "checkpointname", dom,
+                              &checkpoint, &name) < 0)
+        goto cleanup;
+
+    if (!(xml = virDomainCheckpointGetXMLDesc(checkpoint, flags)))
+        goto cleanup;
+
+    vshPrint(ctl, "%s", xml);
+    ret = true;
+
+ cleanup:
+    VIR_FREE(xml);
+    virshDomainCheckpointFree(checkpoint);
+    virshDomainFree(dom);
+
+    return ret;
+}
+
+
+/*
+ * "checkpoint-parent" command
+ */
+static const vshCmdInfo info_checkpoint_parent[] = {
+    {.name = "help",
+     .data = N_("Get the name of the parent of a checkpoint")
+    },
+    {.name = "desc",
+     .data = N_("Extract the checkpoint's parent, if any")
+    },
+    {.name = NULL}
+};
+
+static const vshCmdOptDef opts_checkpoint_parent[] = {
+    VIRSH_COMMON_OPT_DOMAIN_FULL(0),
+    {.name = "checkpointname",
+     .type = VSH_OT_STRING,
+     .help = N_("find parent of checkpoint name"),
+     .completer = virshCheckpointNameCompleter,
+    },
+    VIRSH_COMMON_OPT_CURRENT(N_("find parent of current checkpoint")),
+    {.name = NULL}
+};
+
+static bool
+cmdCheckpointParent(vshControl *ctl,
+                    const vshCmd *cmd)
+{
+    virDomainPtr dom = NULL;
+    bool ret = false;
+    const char *name = NULL;
+    virDomainCheckpointPtr checkpoint = NULL;
+    char *parent = NULL;
+
+    dom = virshCommandOptDomain(ctl, cmd, NULL);
+    if (dom == NULL)
+        goto cleanup;
+
+    if (virshLookupCheckpoint(ctl, cmd, "checkpointname", dom,
+                              &checkpoint, &name) < 0)
+        goto cleanup;
+
+    if (virshGetCheckpointParent(ctl, checkpoint, &parent) < 0)
+        goto cleanup;
+    if (!parent) {
+        vshError(ctl, _("checkpoint '%s' has no parent"), name);
+        goto cleanup;
+    }
+
+    vshPrint(ctl, "%s", parent);
+
+    ret = true;
+
+ cleanup:
+    VIR_FREE(parent);
+    virshDomainCheckpointFree(checkpoint);
+    virshDomainFree(dom);
+
+    return ret;
+}
+
+
+/*
+ * "checkpoint-delete" command
+ */
+static const vshCmdInfo info_checkpoint_delete[] = {
+    {.name = "help",
+     .data = N_("Delete a domain checkpoint")
+    },
+    {.name = "desc",
+     .data = N_("Checkpoint Delete")
+    },
+    {.name = NULL}
+};
+
+static const vshCmdOptDef opts_checkpoint_delete[] = {
+    VIRSH_COMMON_OPT_DOMAIN_FULL(0),
+    {.name = "checkpointname",
+     .type = VSH_OT_STRING,
+     .help = N_("checkpoint name"),
+     .completer = virshCheckpointNameCompleter,
+    },
+    VIRSH_COMMON_OPT_CURRENT(N_("delete current checkpoint")),
+    {.name = "children",
+     .type = VSH_OT_BOOL,
+     .help = N_("delete checkpoint and all children")
+    },
+    {.name = "children-only",
+     .type = VSH_OT_BOOL,
+     .help = N_("delete children but not checkpoint")
+    },
+    {.name = "metadata",
+     .type = VSH_OT_BOOL,
+     .help = N_("delete only libvirt metadata, leaving checkpoint contents behind")
+    },
+    {.name = NULL}
+};
+
+static bool
+cmdCheckpointDelete(vshControl *ctl,
+                    const vshCmd *cmd)
+{
+    virDomainPtr dom = NULL;
+    bool ret = false;
+    const char *name = NULL;
+    virDomainCheckpointPtr checkpoint = NULL;
+    unsigned int flags = 0;
+
+    dom = virshCommandOptDomain(ctl, cmd, NULL);
+    if (dom == NULL)
+        goto cleanup;
+
+    if (virshLookupCheckpoint(ctl, cmd, "checkpointname", dom,
+                              &checkpoint, &name) < 0)
+        goto cleanup;
+
+    if (vshCommandOptBool(cmd, "children"))
+        flags |= VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN;
+    if (vshCommandOptBool(cmd, "children-only"))
+        flags |= VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN_ONLY;
+    if (vshCommandOptBool(cmd, "metadata"))
+        flags |= VIR_DOMAIN_CHECKPOINT_DELETE_METADATA_ONLY;
+
+    if (virDomainCheckpointDelete(checkpoint, flags) == 0) {
+        if (flags & VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN_ONLY)
+            vshPrintExtra(ctl, _("Domain checkpoint %s children deleted\n"), name);
+        else
+            vshPrintExtra(ctl, _("Domain checkpoint %s deleted\n"), name);
+    } else {
+        vshError(ctl, _("Failed to delete checkpoint %s"), name);
+        goto cleanup;
+    }
+
+    ret = true;
+
+ cleanup:
+    virshDomainCheckpointFree(checkpoint);
+    virshDomainFree(dom);
+
+    return ret;
+}
+
+
+const vshCmdDef checkpointCmds[] = {
+    {.name = "checkpoint-create",
+     .handler = cmdCheckpointCreate,
+     .opts = opts_checkpoint_create,
+     .info = info_checkpoint_create,
+     .flags = 0
+    },
+    {.name = "checkpoint-create-as",
+     .handler = cmdCheckpointCreateAs,
+     .opts = opts_checkpoint_create_as,
+     .info = info_checkpoint_create_as,
+     .flags = 0
+    },
+    {.name = "checkpoint-delete",
+     .handler = cmdCheckpointDelete,
+     .opts = opts_checkpoint_delete,
+     .info = info_checkpoint_delete,
+     .flags = 0
+    },
+    {.name = "checkpoint-dumpxml",
+     .handler = cmdCheckpointDumpXML,
+     .opts = opts_checkpoint_dumpxml,
+     .info = info_checkpoint_dumpxml,
+     .flags = 0
+    },
+    {.name = "checkpoint-edit",
+     .handler = cmdCheckpointEdit,
+     .opts = opts_checkpoint_edit,
+     .info = info_checkpoint_edit,
+     .flags = 0
+    },
+    {.name = "checkpoint-info",
+     .handler = cmdCheckpointInfo,
+     .opts = opts_checkpoint_info,
+     .info = info_checkpoint_info,
+     .flags = 0
+    },
+    {.name = "checkpoint-list",
+     .handler = cmdCheckpointList,
+     .opts = opts_checkpoint_list,
+     .info = info_checkpoint_list,
+     .flags = 0
+    },
+    {.name = "checkpoint-parent",
+     .handler = cmdCheckpointParent,
+     .opts = opts_checkpoint_parent,
+     .info = info_checkpoint_parent,
+     .flags = 0
+    },
+    {.name = NULL}
+};
diff --git a/tools/virsh-completer.c b/tools/virsh-completer.c
index 7d5cf8cb90..b54699fafa 100644
--- a/tools/virsh-completer.c
+++ b/tools/virsh-completer.c
@@ -619,6 +619,57 @@ virshSecretUUIDCompleter(vshControl *ctl,
 }


+char **
+virshCheckpointNameCompleter(vshControl *ctl,
+                             const vshCmd *cmd,
+                             unsigned int flags)
+{
+    virshControlPtr priv = ctl->privData;
+    virDomainPtr dom = NULL;
+    virDomainCheckpointPtr *checkpoints = NULL;
+    int ncheckpoints = 0;
+    size_t i = 0;
+    char **ret = NULL;
+
+    virCheckFlags(0, NULL);
+
+    if (!priv->conn || virConnectIsAlive(priv->conn) <= 0)
+        return NULL;
+
+    if (!(dom = virshCommandOptDomain(ctl, cmd, NULL)))
+        return NULL;
+
+    if ((ncheckpoints = virDomainListAllCheckpoints(dom, &checkpoints,
+                                                    flags)) < 0)
+        goto error;
+
+    if (VIR_ALLOC_N(ret, ncheckpoints + 1) < 0)
+        goto error;
+
+    for (i = 0; i < ncheckpoints; i++) {
+        const char *name = virDomainCheckpointGetName(checkpoints[i]);
+
+        if (VIR_STRDUP(ret[i], name) < 0)
+            goto error;
+
+        virshDomainCheckpointFree(checkpoints[i]);
+    }
+    VIR_FREE(checkpoints);
+    virshDomainFree(dom);
+
+    return ret;
+
+ error:
+    for (; i < ncheckpoints; i++)
+        virshDomainCheckpointFree(checkpoints[i]);
+    VIR_FREE(checkpoints);
+    for (i = 0; i < ncheckpoints; i++)
+        VIR_FREE(ret[i]);
+    VIR_FREE(ret);
+    virshDomainFree(dom);
+    return NULL;
+}
+
 char **
 virshSnapshotNameCompleter(vshControl *ctl,
                            const vshCmd *cmd,
diff --git a/tools/virsh-domain-monitor.c b/tools/virsh-domain-monitor.c
index e399195deb..0e2c4191d7 100644
--- a/tools/virsh-domain-monitor.c
+++ b/tools/virsh-domain-monitor.c
@@ -1621,6 +1621,7 @@ virshDomainListCollect(vshControl *ctl, unsigned int flags)
     int autostart;
     int state;
     int nsnap;
+    int nchk;
     int mansave;
     virshControlPtr priv = ctl->privData;

@@ -1788,6 +1789,17 @@ virshDomainListCollect(vshControl *ctl, unsigned int flags)
                 goto remove_entry;
         }

+        /* checkpoint filter */
+        if (VSH_MATCH(VIR_CONNECT_LIST_DOMAINS_FILTERS_CHECKPOINT)) {
+            if ((nchk = virDomainListAllCheckpoints(dom, NULL, 0)) < 0) {
+                vshError(ctl, "%s", _("Failed to get checkpoint count"));
+                goto cleanup;
+            }
+            if (!((VSH_MATCH(VIR_CONNECT_LIST_DOMAINS_HAS_CHECKPOINT) && nchk > 0) ||
+                  (VSH_MATCH(VIR_CONNECT_LIST_DOMAINS_NO_CHECKPOINT) && nchk == 0)))
+                goto remove_entry;
+        }
+
         /* the domain matched all filters, it may stay */
         continue;

@@ -1849,6 +1861,14 @@ static const vshCmdOptDef opts_list[] = {
      .type = VSH_OT_BOOL,
      .help = N_("list domains without a snapshot")
     },
+    {.name = "with-checkpoint",
+     .type = VSH_OT_BOOL,
+     .help = N_("list domains with existing checkpoint")
+    },
+    {.name = "without-checkpoint",
+     .type = VSH_OT_BOOL,
+     .help = N_("list domains without a checkpoint")
+    },
     {.name = "state-running",
      .type = VSH_OT_BOOL,
      .help = N_("list domains in running state")
@@ -1948,6 +1968,9 @@ cmdList(vshControl *ctl, const vshCmd *cmd)
     FILTER("with-snapshot",    VIR_CONNECT_LIST_DOMAINS_HAS_SNAPSHOT);
     FILTER("without-snapshot", VIR_CONNECT_LIST_DOMAINS_NO_SNAPSHOT);

+    FILTER("with-checkpoint",    VIR_CONNECT_LIST_DOMAINS_HAS_CHECKPOINT);
+    FILTER("without-checkpoint", VIR_CONNECT_LIST_DOMAINS_NO_CHECKPOINT);
+
     FILTER("state-running", VIR_CONNECT_LIST_DOMAINS_RUNNING);
     FILTER("state-paused",  VIR_CONNECT_LIST_DOMAINS_PAUSED);
     FILTER("state-shutoff", VIR_CONNECT_LIST_DOMAINS_SHUTOFF);
diff --git a/tools/virsh-domain.c b/tools/virsh-domain.c
index 828ae30789..0b5aca4758 100644
--- a/tools/virsh-domain.c
+++ b/tools/virsh-domain.c
@@ -3633,6 +3633,10 @@ static const vshCmdOptDef opts_undefine[] = {
      .type = VSH_OT_BOOL,
      .help = N_("remove all domain snapshot metadata (vm must be inactive)")
     },
+    {.name = "checkpoints-metadata",
+     .type = VSH_OT_BOOL,
+     .help = N_("remove all domain checkpoint metadata (vm must be inactive)")
+    },
     {.name = "nvram",
      .type = VSH_OT_BOOL,
      .help = N_("remove nvram file, if inactive")
@@ -3662,6 +3666,7 @@ cmdUndefine(vshControl *ctl, const vshCmd *cmd)
     /* User-requested actions.  */
     bool managed_save = vshCommandOptBool(cmd, "managed-save");
     bool snapshots_metadata = vshCommandOptBool(cmd, "snapshots-metadata");
+    bool checkpoints_metadata = vshCommandOptBool(cmd, "checkpoints-metadata");
     bool wipe_storage = vshCommandOptBool(cmd, "wipe-storage");
     bool remove_all_storage = vshCommandOptBool(cmd, "remove-all-storage");
     bool delete_snapshots = vshCommandOptBool(cmd, "delete-snapshots");
@@ -3716,6 +3721,8 @@ cmdUndefine(vshControl *ctl, const vshCmd *cmd)
         flags |= VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA;
         snapshots_safe = true;
     }
+    if (checkpoints_metadata)
+        flags |= VIR_DOMAIN_UNDEFINE_CHECKPOINTS_METADATA;
     if (nvram)
         flags |= VIR_DOMAIN_UNDEFINE_NVRAM;
     if (keep_nvram)
diff --git a/tools/virsh-util.c b/tools/virsh-util.c
index aa88397d61..933d1c825d 100644
--- a/tools/virsh-util.c
+++ b/tools/virsh-util.c
@@ -228,6 +228,17 @@ virshDomainFree(virDomainPtr dom)
 }


+void
+virshDomainCheckpointFree(virDomainCheckpointPtr chk)
+{
+    if (!chk)
+        return;
+
+    vshSaveLibvirtHelperError();
+    virDomainCheckpointFree(chk); /* sc_prohibit_obj_free_apis_in_virsh */
+}
+
+
 void
 virshDomainSnapshotFree(virDomainSnapshotPtr snap)
 {
diff --git a/tools/virsh.c b/tools/virsh.c
index b41304a888..0de41e33b8 100644
--- a/tools/virsh.c
+++ b/tools/virsh.c
@@ -50,6 +50,7 @@
 #include "virstring.h"
 #include "virgettext.h"

+#include "virsh-checkpoint.h"
 #include "virsh-console.h"
 #include "virsh-domain.h"
 #include "virsh-domain-monitor.h"
@@ -832,6 +833,7 @@ static const vshCmdGrp cmdGroups[] = {
     {VIRSH_CMD_GRP_DOM_MANAGEMENT, "domain", domManagementCmds},
     {VIRSH_CMD_GRP_DOM_MONITORING, "monitor", domMonitoringCmds},
     {VIRSH_CMD_GRP_HOST_AND_HV, "host", hostAndHypervisorCmds},
+    {VIRSH_CMD_GRP_CHECKPOINT, "checkpoint", checkpointCmds},
     {VIRSH_CMD_GRP_IFACE, "interface", ifaceCmds},
     {VIRSH_CMD_GRP_NWFILTER, "filter", nwfilterCmds},
     {VIRSH_CMD_GRP_NETWORK, "network", networkCmds},
diff --git a/tools/virsh.pod b/tools/virsh.pod
index 4dffcafea0..8d69d349e9 100644
--- a/tools/virsh.pod
+++ b/tools/virsh.pod
@@ -409,6 +409,7 @@ Inject NMI to the guest.
               [I<--with-managed-save>] [I<--without-managed-save>]
               [I<--autostart>] [I<--no-autostart>]
               [I<--with-snapshot>] [I<--without-snapshot>]
+              [I<--with-checkpoint>] [I<--without-checkpoint>]
               [I<--state-running>] [I<--state-paused>]
               [I<--state-shutoff>] [I<--state-other>]

@@ -514,6 +515,11 @@ this feature disabled use I<--no-autostart>.
 Domains that have snapshot images can be listed using flag I<--with-snapshot>,
 domains without a snapshot I<--without-snapshot>.

+=item B<Checkpoint existence>
+
+Domains that have checkpoints can be listed using flag I<--with-checkpoint>,
+domains without a checkpoint I<--without-checkpoint>.
+
 =back

 When talking to older servers, this command is forced to use a series of API
@@ -809,7 +815,8 @@ can be restarted later.
 If I<domain> is transient, then the metadata of any snapshots will
 be lost once the guest stops running, but the snapshot contents still
 exist, and a new domain with the same name and UUID can restore the
-snapshot metadata with B<snapshot-create>.
+snapshot metadata with B<snapshot-create>.  Similarly, the metadata of
+any checkpoints will be lost, but can be restored with B<checkpoint-create>.

 If I<--graceful> is specified, don't resort to extreme measures
 (e.g. SIGKILL) when the guest doesn't stop after a reasonable timeout;
@@ -1574,7 +1581,7 @@ Convert a domain Id (or UUID) to domain name
 Rename a domain. This command changes current domain name to the new name
 specified in the second argument.

-B<Note>: Domain must be inactive and without snapshots.
+B<Note>: Domain must be inactive and without snapshots or checkpoints.

 =item B<domstate> I<domain> [I<--reason>]

@@ -2815,10 +2822,11 @@ services must be shutdown in the domain.
 The exact behavior of a domain when it shuts down is set by the
 I<on_poweroff> parameter in the domain's XML definition.

-If I<domain> is transient, then the metadata of any snapshots will
-be lost once the guest stops running, but the snapshot contents still
-exist, and a new domain with the same name and UUID can restore the
-snapshot metadata with B<snapshot-create>.
+If I<domain> is transient, then the metadata of any snapshots and
+checkpoints will be lost once the guest stops running, but the underlying
+contents still exist, and a new domain with the same name and UUID can
+restore the snapshot metadata with B<snapshot-create>, and the checkpoint
+metadata with B<checkpoint-create>.

 By default the hypervisor will try to pick a suitable shutdown
 method. To specify an alternative method, the I<--mode> parameter
@@ -2895,7 +2903,7 @@ Output the device used for the TTY console of the domain. If the information
 is not available the processes will provide an exit code of 1.

 =item B<undefine> I<domain> [I<--managed-save>] [I<--snapshots-metadata>]
-[I<--nvram>] [I<--keep-nvram>]
+[I<--checkpoints-metadata>] [I<--nvram>] [I<--keep-nvram>]
 [ {I<--storage> B<volumes> | I<--remove-all-storage>
 [I<--delete-storage-volume-snapshots>]} I<--wipe-storage>]

@@ -2913,6 +2921,12 @@ domain.  Without the flag, attempts to undefine an inactive domain with
 snapshot metadata will fail.  If the domain is active, this flag is
 ignored.

+The I<--checkpoints-metadata> flag guarantees that any checkpoints (see the
+B<checkpoint-list> command) are also cleaned up when undefining an inactive
+domain.  Without the flag, attempts to undefine an inactive domain with
+checkpoint metadata will fail.  If the domain is active, this flag is
+ignored.
+
 I<--nvram> and I<--keep-nvram> specify accordingly to delete or keep nvram
 (/domain/os/nvram/) file. If the domain has an nvram file and the flags are
 omitted, the undefine will fail.
@@ -4655,6 +4669,10 @@ a persistent domain.  However, for transient domains, snapshot
 metadata is silently lost when the domain quits running (whether
 by command such as B<destroy> or by internal guest action).

+For now, it is not possible to create snapshots in a domain that has
+checkpoints, although this restriction will be lifted in a future
+release.
+
 =item B<snapshot-create-as> I<domain> {[I<--print-xml>]
 [I<--no-metadata>] [I<--halt>] [I<--reuse-external>]} [I<name>]
 [I<description>] [I<--disk-only> [I<--quiesce>]] [I<--atomic>]
@@ -4718,6 +4736,10 @@ If I<--live> is specified, libvirt takes the snapshot while the guest is
 running. This increases the size of the memory image of the external
 snapshot. This is currently supported only for external full system snapshots.

+For now, it is not possible to create snapshots in a domain that has
+checkpoints, although this restriction will be lifted in a future
+release.
+
 =item B<snapshot-current> I<domain> {[I<--name>] | [I<--security-info>]
 | [I<snapshotname>]}

@@ -4892,6 +4914,191 @@ the data contents from that point in time.

 =back

+=head1 CHECKPOINT COMMANDS
+
+The following commands manipulate domain checkpoints.  Checkpoints serve as
+a point in time to identify which portions of a guest's disks have changed
+after that time, making it possible to perform incremental and differential
+backups.  Checkpoints are identified with a unique name.  See
+L<https://libvirt.org/formatcheckpoint.html> for documentation of the XML
+format used to represent properties of checkpoints.
+
+=over 4
+
+=item B<checkpoint-create> I<domain> [I<xmlfile>] { I<--redefine>
+| [I<--no-metadata>] [I<--quiesce>]}
+
+Create a checkpoint for domain I<domain> with the properties specified
+in I<xmlfile> describing a <domaincheckpoint> top-level element. The
+format of the input XML file will be validated against an internal RNG
+schema (idential to using the L<virt-xml-validate(1)> tool). If
+I<xmlfile> is completely omitted, then libvirt will create a
+checkpoint with a name based on the current time.
+
+If I<--redefine> is specified, then all XML elements produced by
+B<checkpoint-dumpxml> are valid; this can be used to migrate
+checkpoint hierarchy from one machine to another, to recreate
+hierarchy for the case of a transient domain that goes away and is
+later recreated with the same name and UUID, or to make slight
+alterations in the checkpoint metadata (such as host-specific aspects
+of the domain XML embedded in the checkpoint).  When this flag is
+supplied, the I<xmlfile> argument is mandatory.
+
+If I<--no-metadata> is specified, then the checkpoint is only created
+if the server does not require libvirt to track metadata for the
+checkpoint (some hypervisors may always fail if this flag is
+requested).
+
+If I<--quiesce> is specified, libvirt will try to use guest agent
+to freeze and unfreeze domain's mounted file systems. However,
+if domain has no guest agent, checkpoint creation will fail.
+
+Existence of checkpoint metadata will prevent attempts to B<undefine>
+a persistent domain.  However, for transient domains, checkpoint
+metadata is silently lost when the domain quits running (whether
+by command such as B<destroy> or by internal guest action).
+
+For now, it is not possible to create checkpoints in a domain that has
+snapshots, although this restriction will be lifted in a future
+release.
+
+=item B<checkpoint-create-as> I<domain> {[I<--print-xml>]
+| [I<--no-metadata>]} [I<name>] [I<description>] [I<--quiesce>]
+[I<--diskspec>] B<diskspec>]...
+
+Create a checkpoint for domain I<domain> with the given <name> and
+<description>; if either value is omitted, libvirt will choose a
+value.  If I<--print-xml> is specified, then XML appropriate for
+I<checkpoint-create> is output, rather than actually creating a
+checkpoint.
+
+The I<--diskspec> option can be used to control which guest disks
+participate in the checkpoint. This option can occur multiple times,
+according to the number of <disk> elements in the domain xml.  Each
+<diskspec> is in the form B<disk[,checkpoint=type][,bitmap=name]>. A
+literal I<--diskspec> must precede each B<diskspec> unless
+all three of I<domain>, I<name>, and I<description> are also present.
+For example, a diskspec of "vda,checkpoint=bitmap,bitmap=map1"
+results in the following XML:
+  <disk name='vda' checkpoint='bitmap' bitmap='map1'/>
+
+If I<--quiesce> is specified, libvirt will try to use guest agent
+to freeze and unfreeze domain's mounted file systems. However,
+if domain has no guest agent, checkpoint creation will fail.
+
+If I<--no-metadata> is specified, then the checkpoint is only created
+if the server does not require libvirt to track metadata for the
+checkpoint (some hypervisors may always fail if this flag is
+requested).
+
+For now, it is not possible to create checkpoints in a domain that has
+snapshots, although this restriction will be lifted in a future
+release.
+
+=item B<checkpoint-edit> I<domain> {I<checkpointname> | I<--current>}
+
+Edit the XML configuration file for I<checkpointname> of a domain. If
+the domain has exactly one current checkpoint, I<--current> can be used
+in place of the checkpoint's name.
+
+This is equivalent to:
+
+ virsh checkpoint-dumpxml dom name > checkpoint.xml
+ vi checkpoint.xml (or make changes with your other text editor)
+ virsh checkpoint-create dom checkpoint.xml --redefine
+
+except that it does some error checking, including that the edits
+should not attempt to change the checkpoint name.
+
+The editor used can be supplied by the C<$VISUAL> or C<$EDITOR> environment
+variables, and defaults to C<vi>.
+
+=item B<checkpoint-info> I<domain> {I<checkpoint> | I<--current>}
+
+Output basic information about a named <checkpoint>, or the current
+checkpoint with I<--current>.
+
+=item B<checkpoint-list> I<domain> [I<--metadata>] [I<--no-metadata>]
+[{I<--parent> | I<--roots> | [{I<--tree> | I<--name>}]}] [I<--topological>]
+[{[I<--from>] B<checkpoint> | I<--current>} [I<--descendants>]]
+[I<--leaves>] [I<--no-leaves>] [I<--current-only>] [I<--no-current>]
+
+List all of the available checkpoints for the given domain, defaulting
+to show columns for the checkpoint name and creation time.
+
+Normally, table form output is sorted by checkpoint name; using
+I<--topological> instead sorts so that no child is listed before its
+ancestors (although there may be more than one possible ordering with
+this property).
+
+If I<--parent> is specified, add a column to the output table giving
+the name of the parent of each checkpoint.  If I<--roots> is
+specified, the list will be filtered to just checkpoints that have no
+parents.  If I<--tree> is specified, the output will be in a tree
+format, listing just checkpoint names.  These three options are
+mutually exclusive. If I<--name> is specified only the checkpoint name
+is printed. This option is mutually exclusive with I<--tree>.
+
+If I<--from> is provided, filter the list to checkpoints which are
+children of the given B<checkpoint>; or if I<--current> is provided,
+start at the current checkpoint.  When used in isolation or with
+I<--parent>, the list is limited to direct children unless
+I<--descendants> is also present.  When used with I<--tree>, the use
+of I<--descendants> is implied.  This option is not compatible with
+I<--roots>.  Note that the starting point of I<--from> or I<--current>
+is not included in the list unless the I<--tree> option is also
+present.
+
+If I<--leaves> is specified, the list will be filtered to just
+checkpoints that have no children.  Likewise, if I<--no-leaves> is
+specified, the list will be filtered to just checkpoints with
+children.  (Note that omitting both options does no filtering, while
+providing both options will either produce the same list or error out
+depending on whether the server recognizes the flags).  Filtering
+options are not compatible with I<--tree>.
+
+If I<--current-only> is specified, the list will be filtered to just
+current checkpoints.  If I<--no-current> is specified, the list will
+be filtered to exclude current checkpoints.
+
+=item B<checkpoint-dumpxml> I<domain> {I<checkpoint> | I<--current>}
+[I<--security-info>] [I<--no-domain>] [I<--size>]
+
+Output the checkpoint XML for the domain's checkpoint named
+I<checkpoint>.  If the domain has exactly one current checkpoint,
+I<--current> can be used in place of the checkpoint's name. Using
+I<--security-info> will also include security sensitive information.
+Using I<--size> will add XML indicating the current size in bytes of
+guest data that has changed since the checkpoint was created (although
+remember that guest activity between a size check and actually
+creating a backup can result in the backup needing slightly more
+space).  Using I<--no-domain> will omit the <domain> element from the
+output for a more compact view.
+
+=item B<checkpoint-parent> I<domain> {I<checkpoint> | I<--current>}
+
+Output the name of the parent checkpoint, if any, for the given
+I<checkpoint>, or for the current checkpoint with I<--current>.
+
+=item B<checkpoint-delete> I<domain> {I<checkpoint> | I<--current>}
+[I<--metadata>] [{I<--children> | I<--children-only>}]
+
+Delete the checkpoint for the domain named I<checkpoint>, or the
+current checkpoint with I<--current>.  The record of which portions of
+the disk changed since the checkpoint are merged into the parent
+checkpoint (if any). If I<--children> is passed, then delete this
+checkpoint and any children of this checkpoint.  If I<--children-only>
+is passed, then delete any children of this checkpoint, but leave this
+checkpoint intact. These two flags are mutually exclusive.
+
+If I<--metadata> is specified, then only delete the checkpoint
+metadata maintained by libvirt, while leaving the checkpoint contents
+intact for access by external tools; otherwise deleting a checkpoint
+also removes the ability to perform an incremental backup from that
+point in time.
+
+=back
+
 =head1 NWFILTER COMMANDS

 The following commands manipulate network filters. Network filters allow
-- 
2.20.1




More information about the libvir-list mailing list