diff mbox

[v2,3/6] add backup related monitor commands

Message ID 1354267354-1028712-3-git-send-email-dietmar@proxmox.com
State New
Headers show

Commit Message

Dietmar Maurer Nov. 30, 2012, 9:22 a.m. UTC
We use a generic BackupDriver struct to encaplulated all archive format
related function.

Another option would be to simply dump <devid,cluster_num,cluster_data> to
the output fh (pipe), and an external binary saves the data. That way we
could move the whole archive format related code out of qemu.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
 backup.h         |   14 +++
 blockdev.c       |  337 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 hmp-commands.hx  |   31 +++++
 hmp.c            |   63 ++++++++++
 hmp.h            |    3 +
 monitor.c        |    7 +
 qapi-schema.json |   81 +++++++++++++
 qmp-commands.hx  |   27 +++++
 8 files changed, 563 insertions(+), 0 deletions(-)

Comments

Eric Blake Dec. 12, 2012, 5:47 p.m. UTC | #1
On 11/30/2012 02:22 AM, Dietmar Maurer wrote:
> We use a generic BackupDriver struct to encaplulated all archive format

s/encaplulated/encapsulated/

> related function.
> 
> Another option would be to simply dump <devid,cluster_num,cluster_data> to
> the output fh (pipe), and an external binary saves the data. That way we
> could move the whole archive format related code out of qemu.

I'm coming into this review late, so I'm not sure what your series is
adding that cannot already be done with external snapshots and migration
to file.  But any time someone proposes new QMP commands that libvirt
might have to interact with, I try to chime in:

> 
> Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
> ---
>  backup.h         |   14 +++
>  blockdev.c       |  337 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  hmp-commands.hx  |   31 +++++
>  hmp.c            |   63 ++++++++++
>  hmp.h            |    3 +
>  monitor.c        |    7 +
>  qapi-schema.json |   81 +++++++++++++
>  qmp-commands.hx  |   27 +++++
>  8 files changed, 563 insertions(+), 0 deletions(-)
> 

> +++ b/qapi-schema.json
> @@ -358,6 +358,39 @@
>  { 'type': 'EventInfo', 'data': {'name': 'str'} }
>  
>  ##
> +# @BackupStatus:
> +#
> +# Detailed backup status.
> +#
> +# @status: #optional string describing the current backup status.
> +#          Tthis can be 'active', 'done', 'error'. If this field is not

s/Tthis/This/

> +#          returned, no backup process has been initiated
> +#
> +# @errmsg: #optional error message (only returned if status is 'error')
> +#
> +# @total: #optional total amount of bytes involved in the backup process
> +#
> +# @transferred: #optional amount of bytes already backed up.
> +#
> +# @zero-bytes: #optional amount of 'zero' bytes detected.
> +#
> +# @start-time: #optional time (epoch) when backup job started.

Should you account for sub-second resolution here

> +#
> +# @end-time: #optional time (epoch) when backup job finished.

and here?

> +#
> +# @backupfile: #optional backup file name
> +#
> +# @uuid: #optional uuid for this backup job
> +#
> +# Since: 1.4.0
> +##
> +{ 'type': 'BackupStatus',
> +  'data': {'*status': 'str', '*errmsg': 'str', '*total': 'int',
> +           '*transferred': 'int', '*zero-bytes': 'int',
> +           '*start-time': 'int', '*end-time': 'int',
> +           '*backupfile': 'str', '*uuid': 'str' } }
> +
> +##
>  # @query-events:
>  #
>  # Return a list of supported QMP events by this server
> @@ -1764,6 +1797,54 @@
>    'data': { 'path': 'str' },
>    'returns': [ 'ObjectPropertyInfo' ] }
>  
> +
> +##
> +# @backup:
> +#
> +# Starts a VM backup.
> +#
> +# @backupfile: the backup file name

I guess the fdset mechanism is how libvirt would pass in a pipe fd
rather than supplying an actual file name.  Does your implementation
allow for pipes, or must it be seekable?

> +#
> +# @format: format of the backup file

Rather than letting this be a free-form string, it would be nicer as an
enum of actually supported formats.

> +#
> +# @config-filename: #optional name of a configuration file to include into
> +# the backup archive.
> +#
> +# @speed: #optional the maximum speed, in bytes per second
> +#
> +# Returns: the uuid of the backup job

UUID in raw byte format, or in ASCII format?  (Assuming the latter)

> +#
> +# Since: 1.4.0
> +##
> +{ 'command': 'backup', 'data': { 'backupfile': 'str', '*format': 'str',

backupfile sounds like a run-on; is it any better to name it backup-file?

> +                                 '*config-filename': 'str',

especially when compared with config-filename
Dietmar Maurer Dec. 12, 2012, 7:33 p.m. UTC | #2
> I'm coming into this review late, so I'm not sure what your series is adding

> that cannot already be done with external snapshots and migration to file.


This series try to do backup more efficient (see docu in [PATCH v2 1/6]).

> But any time someone proposes new QMP commands that libvirt might have

> to interact with, I try to chime in:


libvirt does not need to use any of those commands.

> > +#          returned, no backup process has been initiated

> > +#

> > +# @errmsg: #optional error message (only returned if status is

> > +'error') # # @total: #optional total amount of bytes involved in the

> > +backup process # # @transferred: #optional amount of bytes already

> > +backed up.

> > +#

> > +# @zero-bytes: #optional amount of 'zero' bytes detected.

> > +#

> > +# @start-time: #optional time (epoch) when backup job started.

> 

> Should you account for sub-second resolution here


Why (I doubt we can backup within a second).

> > +# @backupfile: the backup file name

> 

> I guess the fdset mechanism is how libvirt would pass in a pipe fd rather than

> supplying an actual file name.  Does your implementation allow for pipes, or

> must it be seekable?


You can pass /dev/fdset/XXX.

My implementation allows pipes.

(I will post the necessary changes for /dev/fdset/ next time).

> > +# @format: format of the backup file

> 

> Rather than letting this be a free-form string, it would be nicer as an enum of

> actually supported formats.


ok

> > +# @config-filename: #optional name of a configuration file to include

> > +into # the backup archive.

> > +#

> > +# @speed: #optional the maximum speed, in bytes per second # #

> > +Returns: the uuid of the backup job

> 

> UUID in raw byte format, or in ASCII format?  (Assuming the latter)


ASCII

> > +#

> > +# Since: 1.4.0

> > +##

> > +{ 'command': 'backup', 'data': { 'backupfile': 'str', '*format':

> > +'str',

> 

> backupfile sounds like a run-on; is it any better to name it backup-file?

> 

> > +                                 '*config-filename': 'str',

> 

> especially when compared with config-filename


ok. Many thanks for the review.
diff mbox

Patch

diff --git a/backup.h b/backup.h
index e1f0290..881353f 100644
--- a/backup.h
+++ b/backup.h
@@ -27,4 +27,18 @@  int backup_job_start(BlockDriverState *bs, BackupDumpFunc *backup_dump_cb,
                      BlockDriverCompletionFunc *backup_complete_cb,
                      void *opaque);
 
+typedef struct BackupDriver {
+    const char *format;
+    void *(*open_cb)(const char *filename, uuid_t uuid, int64_t speed,
+                     Error **errp);
+    int (*close_cb)(void *opaque, Error **errp);
+    int (*register_config_cb)(void *opaque, const char *name, gpointer data,
+                              size_t data_len);
+    int (*register_stream_cb)(void *opaque, const char *devname, size_t size);
+    int (*dump_cb)(void *opaque, uint8_t dev_id, int64_t cluster_num,
+                   unsigned char *buf, size_t *zero_bytes);
+    int (*complete_cb)(void *opaque, uint8_t dev_id, int ret);
+    void (*cancel_cb)(void *opaque);
+} BackupDriver;
+
 #endif /* QEMU_BACKUP_H */
diff --git a/blockdev.c b/blockdev.c
index e73fd6e..a64490e 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -20,6 +20,7 @@ 
 #include "qmp-commands.h"
 #include "trace.h"
 #include "arch_init.h"
+#include "backup.h"
 
 static QTAILQ_HEAD(drivelist, DriveInfo) drives = QTAILQ_HEAD_INITIALIZER(drives);
 
@@ -1321,6 +1322,342 @@  void qmp_drive_mirror(const char *device, const char *target,
     drive_get_ref(drive_get_by_blockdev(bs));
 }
 
+/* Backup related function */
+
+static struct GenericBackupState {
+    Error *error;
+    uuid_t uuid;
+    char uuid_str[37];
+    time_t start_time;
+    time_t end_time;
+    char *backupfile;
+    const BackupDriver *driver;
+    void *writer;
+    GList *bcb_list;
+    size_t total;
+    size_t transferred;
+    size_t zero_bytes;
+} backup_state;
+
+typedef struct BackupCB {
+    BlockDriverState *bs;
+    uint8_t dev_id;
+    size_t size;
+    size_t transferred;
+    size_t zero_bytes;
+} BackupCB;
+
+static int backup_dump_cb(void *opaque, BlockDriverState *bs,
+                          int64_t cluster_num, unsigned char *buf)
+{
+    BackupCB *bcb = opaque;
+
+    assert(backup_state.driver);
+    assert(backup_state.writer);
+    assert(backup_state.driver->dump_cb);
+
+    size_t zero_bytes = 0;
+    int bytes = backup_state.driver->dump_cb(backup_state.writer,
+                                             bcb->dev_id, cluster_num,
+                                             buf, &zero_bytes);
+
+    if (bytes > 0) {
+        bcb->transferred += bytes;
+        backup_state.transferred += bytes;
+        if (zero_bytes) {
+            bcb->zero_bytes += bytes;
+            backup_state.zero_bytes += zero_bytes;
+        }
+    }
+
+    return bytes;
+}
+
+static void backup_complete_cb(void *opaque, int ret)
+{
+    BackupCB *bcb = opaque;
+
+    assert(backup_state.driver);
+    assert(backup_state.writer);
+    assert(backup_state.driver->complete_cb);
+    assert(backup_state.driver->close_cb);
+
+    drive_put_ref_bh_schedule(drive_get_by_blockdev(bcb->bs));
+
+    backup_state.bcb_list = g_list_remove(backup_state.bcb_list, bcb);
+
+    backup_state.driver->complete_cb(backup_state.writer, bcb->dev_id, ret);
+
+    if (g_list_length(backup_state.bcb_list) == 0) {
+
+        backup_state.end_time = time(NULL);
+
+        backup_state.driver->close_cb(backup_state.writer, &backup_state.error);
+
+        backup_state.writer = NULL;
+    }
+
+    g_free(bcb);
+}
+
+void qmp_backup_cancel(Error **errp)
+{
+    if (backup_state.writer) {
+        assert(backup_state.driver);
+        assert(backup_state.driver->cancel_cb);
+        backup_state.driver->cancel_cb(backup_state.writer);
+    }
+}
+
+static void backup_start_jobs(void)
+{
+    /* start all jobs (one for each device) */
+    GList *l = backup_state.bcb_list;
+    while (l) {
+        BackupCB *bcb = l->data;
+        l = g_list_next(l);
+
+        if (backup_job_start(bcb->bs, backup_dump_cb, backup_complete_cb,
+                             bcb) == 0) {
+            /* Grab a reference so hotplug does not delete the
+             * BlockDriverState from underneath us.
+             */
+            drive_get_ref(drive_get_by_blockdev(bcb->bs));
+        } else {
+            backup_state.driver->cancel_cb(backup_state.writer);
+        }
+    }
+}
+
+char *qmp_backup(const char *backupfile, bool has_format, const char *format,
+                 bool has_config_filename, const char *config_filename,
+                 bool has_devlist, const char *devlist,
+                 bool has_speed, int64_t speed, Error **errp)
+{
+    BlockDriverState *bs;
+    Error *local_err = NULL;
+    uuid_t uuid;
+    void *writer = NULL;
+    gchar **devs = NULL;
+    GList *bcblist = NULL;
+
+    /* Todo: try to auto-detect format based on file name */
+    format = has_format ? format : "vma";
+
+    /* fixme: find driver for specifued format */
+    const BackupDriver *driver = NULL;
+
+    if (!driver) {
+        error_set(errp, ERROR_CLASS_GENERIC_ERROR,
+                  "no backup driver for format '%s'", format);
+        return NULL;
+    }
+
+    if (has_devlist) {
+        devs = g_strsplit(devlist, ",;:", -1);
+
+        gchar **d = devs;
+        while (d && *d) {
+            bs = bdrv_find(*d);
+            if (bs) {
+                if (bdrv_is_read_only(bs)) {
+                    error_set(errp, QERR_DEVICE_IS_READ_ONLY, *d);
+                    goto err;
+                }
+                if (!bdrv_is_inserted(bs)) {
+                    error_set(errp, QERR_DEVICE_HAS_NO_MEDIUM, *d);
+                    goto err;
+                }
+                BackupCB *bcb = g_new0(BackupCB, 1);
+                bcb->bs = bs;
+                bcblist = g_list_append(bcblist, bcb);
+            } else {
+                error_set(errp, QERR_DEVICE_NOT_FOUND, *d);
+                goto err;
+            }
+            d++;
+        }
+
+    } else {
+
+        bs = NULL;
+        while ((bs = bdrv_next(bs))) {
+
+            if (!bdrv_is_inserted(bs) || bdrv_is_read_only(bs)) {
+                continue;
+            }
+
+            BackupCB *bcb = g_new0(BackupCB, 1);
+            bcb->bs = bs;
+            bcblist = g_list_append(bcblist, bcb);
+        }
+    }
+
+    if (!bcblist) {
+        error_set(errp, ERROR_CLASS_GENERIC_ERROR, "empty device list");
+        goto err;
+    }
+
+    GList *l = bcblist;
+    while (l) {
+        BackupCB *bcb = l->data;
+        l = g_list_next(l);
+        if (bcb->bs->job) {
+            error_set(errp, QERR_DEVICE_IN_USE, bdrv_get_device_name(bcb->bs));
+            goto err;
+        }
+    }
+
+    uuid_generate(uuid);
+
+    writer = driver->open_cb(backupfile, uuid, speed, &local_err);
+    if (!writer) {
+        if (error_is_set(&local_err)) {
+            error_propagate(errp, local_err);
+        }
+        goto err;
+    }
+
+    size_t total = 0;
+
+    /* register all devices for vma writer */
+    l = bcblist;
+    while (l) {
+        BackupCB *bcb = l->data;
+        l = g_list_next(l);
+
+        int64_t size = bdrv_getlength(bcb->bs);
+        const char *devname = bdrv_get_device_name(bcb->bs);
+        bcb->dev_id = driver->register_stream_cb(writer, devname, size);
+        if (bcb->dev_id <= 0) {
+            error_set(errp, ERROR_CLASS_GENERIC_ERROR,
+                      "register_stream failed");
+            goto err;
+        }
+        bcb->size = size;
+        total += size;
+    }
+
+    /* add configuration file to archive */
+    if (has_config_filename) {
+        char *cdata = NULL;
+        gsize clen = 0;
+        GError *err = NULL;
+        if (!g_file_get_contents(config_filename, &cdata, &clen, &err)) {
+            error_setg(errp, "unable to read file '%s'", config_filename);
+            goto err;
+        }
+
+        const char *basename = g_path_get_basename(config_filename);
+        if (driver->register_config_cb(writer, basename, cdata, clen) <= 0) {
+            error_setg(errp, "register_config failed");
+            g_free(cdata);
+            goto err;
+        }
+        g_free(cdata);
+    }
+
+    /* initialize global backup_state now */
+
+    if (backup_state.error) {
+        error_free(backup_state.error);
+        backup_state.error = NULL;
+    }
+
+    backup_state.driver = driver;
+
+    backup_state.start_time = time(NULL);
+    backup_state.end_time = 0;
+
+    if (backup_state.backupfile) {
+        g_free(backup_state.backupfile);
+    }
+    backup_state.backupfile = g_strdup(backupfile);
+
+    backup_state.writer = writer;
+
+    uuid_copy(backup_state.uuid, uuid);
+    uuid_unparse_lower(uuid, backup_state.uuid_str);
+
+    backup_state.bcb_list = bcblist;
+
+    backup_state.total = total;
+    backup_state.transferred = 0;
+    backup_state.zero_bytes = 0;
+
+    backup_start_jobs();
+
+    return g_strdup(backup_state.uuid_str);
+
+err:
+
+    l = bcblist;
+    while (l) {
+        g_free(l->data);
+        l = g_list_next(l);
+    }
+    g_list_free(bcblist);
+
+    if (devs) {
+        g_strfreev(devs);
+    }
+
+    if (writer) {
+        unlink(backupfile);
+        if (driver) {
+            Error *err = NULL;
+            driver->close_cb(writer, &err);
+        }
+    }
+
+    return NULL;
+}
+
+BackupStatus *qmp_query_backup(Error **errp)
+{
+    BackupStatus *info = g_malloc0(sizeof(*info));
+
+    if (!backup_state.start_time) {
+        /* not started, return {} */
+        return info;
+    }
+
+    info->has_status = true;
+    info->has_start_time = true;
+    info->start_time = backup_state.start_time;
+
+    if (backup_state.backupfile) {
+        info->has_backupfile = true;
+        info->backupfile = g_strdup(backup_state.backupfile);
+    }
+
+    info->has_uuid = true;
+    info->uuid = g_strdup(backup_state.uuid_str);
+
+    if (backup_state.end_time) {
+        if (backup_state.error) {
+            info->status = g_strdup("error");
+            info->has_errmsg = true;
+            info->errmsg = g_strdup(error_get_pretty(backup_state.error));
+        } else {
+            info->status = g_strdup("done");
+        }
+        info->has_end_time = true;
+        info->end_time = backup_state.end_time;
+    } else {
+        info->status = g_strdup("active");
+    }
+
+    info->has_total = true;
+    info->total = backup_state.total;
+    info->has_zero_bytes = true;
+    info->zero_bytes = backup_state.zero_bytes;
+    info->has_transferred = true;
+    info->transferred = backup_state.transferred;
+
+    return info;
+}
+
 static BlockJob *find_block_job(const char *device)
 {
     BlockDriverState *bs;
diff --git a/hmp-commands.hx b/hmp-commands.hx
index 010b8c9..57be357 100644
--- a/hmp-commands.hx
+++ b/hmp-commands.hx
@@ -83,6 +83,35 @@  STEXI
 Copy data from a backing file into a block device.
 ETEXI
 
+   {
+        .name       = "backup",
+        .args_type  = "backupfile:s,speed:o?,devlist:s?",
+        .params     = "backupfile [speed [devlist]]",
+        .help       = "create a VM Backup.",
+        .mhandler.cmd = hmp_backup,
+    },
+
+STEXI
+@item backup
+@findex backup
+Create a VM backup.
+ETEXI
+
+    {
+        .name       = "backup_cancel",
+        .args_type  = "",
+        .params     = "",
+        .help       = "cancel the current VM backup",
+        .mhandler.cmd = hmp_backup_cancel,
+    },
+
+STEXI
+@item backup_cancel
+@findex backup_cancel
+Cancel the current VM backup.
+
+ETEXI
+
     {
         .name       = "block_job_set_speed",
         .args_type  = "device:B,speed:o",
@@ -1558,6 +1587,8 @@  show CPU statistics
 show user network stack connection states
 @item info migrate
 show migration status
+@item info backup
+show backup status
 @item info migrate_capabilities
 show current migration capabilities
 @item info migrate_cache_size
diff --git a/hmp.c b/hmp.c
index 180ba2b..600792f 100644
--- a/hmp.c
+++ b/hmp.c
@@ -130,6 +130,38 @@  void hmp_info_mice(Monitor *mon)
     qapi_free_MouseInfoList(mice_list);
 }
 
+void hmp_info_backup(Monitor *mon)
+{
+    BackupStatus *info;
+
+    info = qmp_query_backup(NULL);
+    if (info->has_status) {
+        if (info->has_errmsg) {
+            monitor_printf(mon, "Backup status: %s - %s\n",
+                           info->status, info->errmsg);
+        } else {
+            monitor_printf(mon, "Backup status: %s\n", info->status);
+        }
+    }
+    if (info->has_backupfile) {
+        int per = (info->has_total && info->total &&
+            info->has_transferred && info->transferred) ?
+            (info->transferred * 100)/info->total : 0;
+        int zero_per = (info->has_total && info->total &&
+                        info->has_zero_bytes && info->zero_bytes) ?
+            (info->zero_bytes * 100)/info->total : 0;
+        monitor_printf(mon, "Backup file: %s\n", info->backupfile);
+        monitor_printf(mon, "Backup uuid: %s\n", info->uuid);
+        monitor_printf(mon, "Total size: %zd\n", info->total);
+        monitor_printf(mon, "Transferred bytes: %zd (%d%%)\n",
+                       info->transferred, per);
+        monitor_printf(mon, "Zero bytes: %zd (%d%%)\n",
+                       info->zero_bytes, zero_per);
+    }
+
+    qapi_free_BackupStatus(info);
+}
+
 void hmp_info_migrate(Monitor *mon)
 {
     MigrationInfo *info;
@@ -977,6 +1009,37 @@  void hmp_block_stream(Monitor *mon, const QDict *qdict)
     hmp_handle_error(mon, &error);
 }
 
+void hmp_backup_cancel(Monitor *mon, const QDict *qdict)
+{
+    Error *errp = NULL;
+
+    qmp_backup_cancel(&errp);
+
+    if (error_is_set(&errp)) {
+        monitor_printf(mon, "%s\n", error_get_pretty(errp));
+        error_free(errp);
+        return;
+    }
+}
+
+void hmp_backup(Monitor *mon, const QDict *qdict)
+{
+    const char *backupfile = qdict_get_str(qdict, "backupfile");
+    const char *devlist = qdict_get_try_str(qdict, "devlist");
+    int64_t speed = qdict_get_try_int(qdict, "speed", 0);
+
+    Error *errp = NULL;
+
+    qmp_backup(backupfile, false, NULL, false, NULL, !!devlist, devlist,
+               qdict_haskey(qdict, "speed"), speed, &errp);
+
+    if (error_is_set(&errp)) {
+        monitor_printf(mon, "%s\n", error_get_pretty(errp));
+        error_free(errp);
+        return;
+    }
+}
+
 void hmp_block_job_set_speed(Monitor *mon, const QDict *qdict)
 {
     Error *error = NULL;
diff --git a/hmp.h b/hmp.h
index 0ab03be..20c9a62 100644
--- a/hmp.h
+++ b/hmp.h
@@ -28,6 +28,7 @@  void hmp_info_mice(Monitor *mon);
 void hmp_info_migrate(Monitor *mon);
 void hmp_info_migrate_capabilities(Monitor *mon);
 void hmp_info_migrate_cache_size(Monitor *mon);
+void hmp_info_backup(Monitor *mon);
 void hmp_info_cpus(Monitor *mon);
 void hmp_info_block(Monitor *mon);
 void hmp_info_blockstats(Monitor *mon);
@@ -63,6 +64,8 @@  void hmp_eject(Monitor *mon, const QDict *qdict);
 void hmp_change(Monitor *mon, const QDict *qdict);
 void hmp_block_set_io_throttle(Monitor *mon, const QDict *qdict);
 void hmp_block_stream(Monitor *mon, const QDict *qdict);
+void hmp_backup(Monitor *mon, const QDict *qdict);
+void hmp_backup_cancel(Monitor *mon, const QDict *qdict);
 void hmp_block_job_set_speed(Monitor *mon, const QDict *qdict);
 void hmp_block_job_cancel(Monitor *mon, const QDict *qdict);
 void hmp_block_job_pause(Monitor *mon, const QDict *qdict);
diff --git a/monitor.c b/monitor.c
index c0e32d6..85cf47e 100644
--- a/monitor.c
+++ b/monitor.c
@@ -2680,6 +2680,13 @@  static mon_cmd_t info_cmds[] = {
     },
 #endif
     {
+        .name       = "backup",
+        .args_type  = "",
+        .params     = "",
+        .help       = "show backup status",
+        .mhandler.info = hmp_info_backup,
+    },
+    {
         .name       = "migrate",
         .args_type  = "",
         .params     = "",
diff --git a/qapi-schema.json b/qapi-schema.json
index 5dfa052..71e0ed9 100644
--- a/qapi-schema.json
+++ b/qapi-schema.json
@@ -358,6 +358,39 @@ 
 { 'type': 'EventInfo', 'data': {'name': 'str'} }
 
 ##
+# @BackupStatus:
+#
+# Detailed backup status.
+#
+# @status: #optional string describing the current backup status.
+#          Tthis can be 'active', 'done', 'error'. If this field is not
+#          returned, no backup process has been initiated
+#
+# @errmsg: #optional error message (only returned if status is 'error')
+#
+# @total: #optional total amount of bytes involved in the backup process
+#
+# @transferred: #optional amount of bytes already backed up.
+#
+# @zero-bytes: #optional amount of 'zero' bytes detected.
+#
+# @start-time: #optional time (epoch) when backup job started.
+#
+# @end-time: #optional time (epoch) when backup job finished.
+#
+# @backupfile: #optional backup file name
+#
+# @uuid: #optional uuid for this backup job
+#
+# Since: 1.4.0
+##
+{ 'type': 'BackupStatus',
+  'data': {'*status': 'str', '*errmsg': 'str', '*total': 'int',
+           '*transferred': 'int', '*zero-bytes': 'int',
+           '*start-time': 'int', '*end-time': 'int',
+           '*backupfile': 'str', '*uuid': 'str' } }
+
+##
 # @query-events:
 #
 # Return a list of supported QMP events by this server
@@ -1764,6 +1797,54 @@ 
   'data': { 'path': 'str' },
   'returns': [ 'ObjectPropertyInfo' ] }
 
+
+##
+# @backup:
+#
+# Starts a VM backup.
+#
+# @backupfile: the backup file name
+#
+# @format: format of the backup file
+#
+# @config-filename: #optional name of a configuration file to include into
+# the backup archive.
+#
+# @speed: #optional the maximum speed, in bytes per second
+#
+# Returns: the uuid of the backup job
+#
+# Since: 1.4.0
+##
+{ 'command': 'backup', 'data': { 'backupfile': 'str', '*format': 'str',
+                                 '*config-filename': 'str',
+                                 '*devlist': 'str', '*speed': 'int' },
+  'returns': 'str' }
+
+##
+# @query-backup
+#
+# Returns information about current/last backup task.
+#
+# Returns: @BackupStatus
+#
+# Since: 1.4.0
+##
+{ 'command': 'query-backup', 'returns': 'BackupStatus' }
+
+##
+# @backup-cancel
+#
+# Cancel the current executing backup process.
+#
+# Returns: nothing on success
+#
+# Notes: This command succeeds even if there is no backup process running.
+#
+# Since: 1.4.0
+##
+{ 'command': 'backup-cancel' }
+
 ##
 # @qom-get:
 #
diff --git a/qmp-commands.hx b/qmp-commands.hx
index 5c692d0..c46fdc4 100644
--- a/qmp-commands.hx
+++ b/qmp-commands.hx
@@ -822,6 +822,18 @@  EQMP
     },
 
     {
+        .name       = "backup",
+        .args_type  = "backupfile:s,format:s?,config-filename:F?,speed:o?,devlist:s?",
+        .mhandler.cmd_new = qmp_marshal_input_backup,
+    },
+
+    {
+        .name       = "backup_cancel",
+        .args_type  = "",
+        .mhandler.cmd_new = qmp_marshal_input_backup_cancel,
+    },
+
+    {
         .name       = "block-job-set-speed",
         .args_type  = "device:B,speed:o",
         .mhandler.cmd_new = qmp_marshal_input_block_job_set_speed,
@@ -2491,6 +2503,21 @@  EQMP
     },
 
 SQMP
+
+query-backup
+-------------
+
+Backup status.
+
+EQMP
+
+    {
+        .name       = "query-backup",
+        .args_type  = "",
+        .mhandler.cmd_new = qmp_marshal_input_query_backup,
+    },
+
+SQMP
 migrate-set-capabilities
 -------