On Wed, Mar 27, 2019 at 05:10:46AM -0500, Eric Blake wrote:
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 (no need for checkpoint-revert,
and checkpoint-list was a lot easier since we don't have to cater
to older libvirt API).
Signed-off-by: Eric Blake <eblake(a)redhat.com>
---
tools/virsh-checkpoint.h | 29 +
tools/virsh-completer.h | 4 +
tools/virsh-util.h | 3 +
tools/virsh.h | 1 +
po/POTFILES | 1 +
tools/Makefile.am | 3 +-
tools/virsh-checkpoint.c | 1370 ++++++++++++++++++++++++++++++++++
tools/virsh-completer.c | 53 +-
tools/virsh-domain-monitor.c | 25 +-
tools/virsh-domain.c | 15 +
tools/virsh-util.c | 11 +
tools/virsh.c | 2 +
tools/virsh.pod | 238 +++++-
13 files changed, 1745 insertions(+), 10 deletions(-)
create mode 100644 tools/virsh-checkpoint.h
create mode 100644 tools/virsh-checkpoint.c
+static bool
+cmdCheckpointInfo(vshControl *ctl,
+ const vshCmd *cmd)
+{
+ virDomainPtr dom;
+ virDomainCheckpointPtr checkpoint = NULL;
+ const char *name;
+ char *parent = NULL;
+ bool ret = false;
+ int count;
+ unsigned int flags;
+ int current;
+ int metadata;
+
+ dom = virshCommandOptDomain(ctl, cmd, NULL);
+ if (dom == NULL)
+ return false;
+
+ if (virshLookupCheckpoint(ctl, cmd, "checkpointname", true, dom,
+ &checkpoint, &name) < 0)
+ goto cleanup;
+
+ vshPrint(ctl, "%-15s %s\n", _("Name:"), name);
+ vshPrint(ctl, "%-15s %s\n", _("Domain:"), virDomainGetName(dom));
We have vshTableNew and vshTableRowAppend that can compute the
indentation at run-time regardless of the locale.
+
+ /* Determine if checkpoint is current. */
+ current = virDomainCheckpointIsCurrent(checkpoint, 0);
+ if (current < 0) {
+ vshError(ctl, "%s",
+ _("unexpected problem querying checkpoint state"));
+ 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);
+
+ /* Metadata. */
+ metadata = virDomainCheckpointHasMetadata(checkpoint, 0);
+ if (metadata >= 0)
+ vshPrint(ctl, "%-15s %s\n", _("Metadata:"),
+ metadata ? _("yes") : _("no"));
+
+ ret = true;
+
+ cleanup:
+ VIR_FREE(parent);
+ virshDomainCheckpointFree(checkpoint);
+ virshDomainFree(dom);
+ return ret;
+}
[...]
+
+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 chklist = 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);
+#undef FILTER
+
+ if (vshCommandOptBool(cmd, "topological"))
+ flags |= VIR_DOMAIN_CHECKPOINT_LIST_TOPOLOGICAL;
+
+ if (roots)
+ flags |= VIR_DOMAIN_CHECKPOINT_LIST_ROOTS;
+
+ if (vshCommandOptBool(cmd, "metadata"))
+ flags |= VIR_DOMAIN_CHECKPOINT_LIST_METADATA;
+
+ if (vshCommandOptBool(cmd, "no-metadata"))
+ flags |= VIR_DOMAIN_CHECKPOINT_LIST_NO_METADATA;
+
+ 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", true, dom, &start,
&from_chk) < 0)
+ goto cleanup;
+
+ if (!(chklist = virshCheckpointListCollect(ctl, dom, start, flags, tree)))
+ goto cleanup;
+
+ if (!tree && !name) {
+ if (parent)
+ vshPrintExtra(ctl, " %-20s %-25s %s",
+ _("Name"), _("Creation Time"),
_("Parent"));
+ else
+ vshPrintExtra(ctl, " %-20s %-25s",
+ _("Name"), _("Creation Time"));
vshTableNew
+ vshPrintExtra(ctl, "\n"
+ "------------------------------"
+ "--------------\n");
+ }
+
+ if (tree) {
+ for (i = 0; i < chklist->nchks; i++) {
+ if (!chklist->chks[i].parent &&
+ vshTreePrint(ctl, virshCheckpointListLookup, chklist,
+ chklist->nchks, i) < 0)
+ goto cleanup;
+ }
+ ret = true;
+ goto cleanup;
+ }
+
+ for (i = 0; i < chklist->nchks; i++) {
Putting this whole loop in a separate function would make this function
more readable - both tree and non-tree would share the same 'ret = true'
assignment and, more importantly, all the XML parsing stuff is only
needed for non-tree printing.
+ const char *chk_name;
+
+ /* free up memory from previous iterations of the loop */
Sounds like a job for VIR_AUTOFREE
+ VIR_FREE(parent_chk);
+ xmlXPathFreeContext(ctxt);
+ xmlFreeDoc(xml);
+ VIR_FREE(doc);
+
+ checkpoint = chklist->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)
+ vshPrint(ctl, " %-20s %-25s %s\n",
+ chk_name, timestr, parent_chk ?: "-");
vshTableRowApend
+ else
+ vshPrint(ctl, " %-20s %-25s\n", chk_name, timestr);
+ }
+
+ ret = true;
+
+ cleanup:
+ /* this frees up memory from the last iteration of the loop */
+ virshCheckpointListFree(chklist);
+ VIR_FREE(parent_chk);
+ virshDomainCheckpointFree(start);
+ xmlXPathFreeContext(ctxt);
+ xmlFreeDoc(xml);
+ VIR_FREE(doc);
+ virshDomainFree(dom);
+
+ 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_DATA,
+ .flags = VSH_OFLAG_REQ,
+ .help = N_("checkpoint name"),
+ .completer = virshCheckpointNameCompleter,
+ },
+ {.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 (vshCommandOptStringReq(ctl, cmd, "checkpointname", &name) < 0)
+ return false;
+
+ if (!(dom = virshCommandOptDomain(ctl, cmd, NULL)))
+ return false;
+
+ if (!(checkpoint = virDomainCheckpointLookupByName(dom, 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", true, 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", true, 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-current",
+ .handler = cmdCheckpointCurrent,
+ .opts = opts_checkpoint_current,
+ .info = info_checkpoint_current,
+ .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 c4adbb70d0..ed5e1015c9 100644
--- a/tools/virsh-completer.c
+++ b/tools/virsh-completer.c
@@ -1,7 +1,7 @@
/*
* virsh-completer.c: virsh completer callbacks
*
- * Copyright (C) 2017 Red Hat, Inc.
+ * Copyright (C) 2017-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
@@ -623,6 +623,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)
I recommend adding an 'int rc' variable and only assigning the result to
ncheckpoints if it's non-negative,
+ 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]);
otherwise this loop will try to run for a long time.
Also, given that we free these on success too, having a shared 'cleanup'
path would be nicer.
+ 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,
Jano