Patchwork [v3,3/6] add backup related monitor commands

login
register
mail settings
Submitter Dietmar Maurer
Date Feb. 19, 2013, 11:31 a.m.
Message ID <1361273503-974882-3-git-send-email-dietmar@proxmox.com>
Download mbox | patch
Permalink /patch/221691/
State New
Headers show

Comments

Dietmar Maurer - Feb. 19, 2013, 11:31 a.m.
We use a generic BackupDriver struct to encapsulate 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         |   12 ++
 blockdev.c       |  423 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 hmp-commands.hx  |   31 ++++
 hmp.c            |   63 ++++++++
 hmp.h            |    3 +
 monitor.c        |    7 +
 qapi-schema.json |   91 ++++++++++++
 qmp-commands.hx  |   27 ++++
 8 files changed, 657 insertions(+), 0 deletions(-)
Eric Blake - Feb. 19, 2013, 10:59 p.m.
On 02/19/2013 04:31 AM, Dietmar Maurer wrote:
> We use a generic BackupDriver struct to encapsulate 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>
> ---

Focusing my review on just the QMP interface:


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

This should be an enum type, not an open-coded 'str'.

> +#
> +# @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.

Is 1-second resolution good enough, or should we be accounting for
sub-second information?

> +#
> +# @backupfile: #optional backup file name
> +#
> +# @uuid: #optional uuid for this backup job
> +#
> +# Since: 1.5.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' } }

You can optional set the speed when starting a backup job, but can you
later change the job speed on the fly, and if so, with what command?
Also, shouldn't the current speed be displayed as part of BackupStatus?

> +
> +##
>  # @query-events:
>  #
>  # Return a list of supported QMP events by this server
> @@ -1824,6 +1857,64 @@
>    'data': { 'path': 'str' },
>    'returns': [ 'ObjectPropertyInfo' ] }
>  
> +
> +##
> +# @BackupFormat
> +#
> +# An enumeration of supported backup formats.
> +#
> +# @vma: Proxmox vma backup format
> +##
> +{ 'enum': 'BackupFormat',
> +  'data': [ 'vma' ] }
> +
> +##
> +# @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.

'backupfile' vs. 'config-filename' feels inconsistent; better might be
'backup-file' and 'config-file'.

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

@devlist is missing.

> +# Returns: the uuid of the backup job
> +#
> +# Since: 1.5.0
> +##
> +{ 'command': 'backup', 'data': { 'backupfile': 'str', '*format': 'BackupFormat',
> +                                 '*config-filename': 'str',
> +                                 '*devlist': 'str', '*speed': 'int' },
> +  'returns': 'str' }
> +
> +##
> +# @query-backup
> +#
> +# Returns information about current/last backup task.
> +#
> +# Returns: @BackupStatus
> +#
> +# Since: 1.5.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.5.0
> +##
> +{ 'command': 'backup-cancel' }
> +

> +++ b/qmp-commands.hx
> @@ -889,6 +889,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",

This doesn't match the spelling in the .json file.
Dietmar Maurer - Feb. 20, 2013, 8:51 a.m.
> > +++ b/qapi-schema.json

> > @@ -425,6 +425,39 @@

> >  { 'type': 'EventInfo', 'data': {'name': 'str'} }

> >

> >  ##

> > +# @BackupStatus:

> > +#

> > +# Detailed backup status.

> > +#

> > +# @status: #optional string describing the current backup status.

> > +#          This can be 'active', 'done', 'error'. If this field is not

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

> 

> This should be an enum type, not an open-coded 'str'.


@MigrationInfo also use a string for @status. I try to use the same
style as existing code.

> > +#

> > +# @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.

> 

> Is 1-second resolution good enough, or should we be accounting for sub-

> second information?


IMHO second is good enough.

> > +#

> > +# @backupfile: #optional backup file name # # @uuid: #optional uuid

> > +for this backup job # # Since: 1.5.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' } }

> 

> You can optional set the speed when starting a backup job, but can you later

> change the job speed on the fly, and if so, with what command?


no, you can't change the speed later (currently).

> Also, shouldn't the current speed be displayed as part of BackupStatus?


You can compute the actual speed from those value. 

> > +

> > +##

> >  # @query-events:

> >  #

> >  # Return a list of supported QMP events by this server @@ -1824,6

> > +1857,64 @@

> >    'data': { 'path': 'str' },

> >    'returns': [ 'ObjectPropertyInfo' ] }

> >

> > +

> > +##

> > +# @BackupFormat

> > +#

> > +# An enumeration of supported backup formats.

> > +#

> > +# @vma: Proxmox vma backup format

> > +##

> > +{ 'enum': 'BackupFormat',

> > +  'data': [ 'vma' ] }

> > +

> > +##

> > +# @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.

> 

> 'backupfile' vs. 'config-filename' feels inconsistent; better might be 'backup-

> file' and 'config-file'.


will change that.
> 

> > +#

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

> 

> @devlist is missing.

> 

> > +# Returns: the uuid of the backup job # # Since: 1.5.0 ## {

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

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

> > +                                 '*devlist': 'str', '*speed': 'int'

> > +},

> > +  'returns': 'str' }

> > +

> > +##

> > +# @query-backup

> > +#

> > +# Returns information about current/last backup task.

> > +#

> > +# Returns: @BackupStatus

> > +#

> > +# Since: 1.5.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.5.0

> > +##

> > +{ 'command': 'backup-cancel' }

> > +

> 

> > +++ b/qmp-commands.hx

> > @@ -889,6 +889,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",

> 

> This doesn't match the spelling in the .json file.


will fix that.

Patch

diff --git a/backup.h b/backup.h
index 74f04f4..02c0e97 100644
--- a/backup.h
+++ b/backup.h
@@ -29,4 +29,16 @@  int backup_job_create(BlockDriverState *bs, BackupDumpFunc *backup_dump_cb,
                       BlockDriverCompletionFunc *backup_complete_cb,
                       void *opaque, int64_t speed);
 
+typedef struct BackupDriver {
+    const char *format;
+    void *(*open_cb)(const char *filename, uuid_t uuid, 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);
+} BackupDriver;
+
 #endif /* QEMU_BACKUP_H */
diff --git a/blockdev.c b/blockdev.c
index 63e6f1e..bd7fa08 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -20,6 +20,7 @@ 
 #include "qmp-commands.h"
 #include "trace.h"
 #include "sysemu/arch_init.h"
+#include "backup.h"
 
 static QTAILQ_HEAD(drivelist, DriveInfo) drives = QTAILQ_HEAD_INITIALIZER(drives);
 
@@ -1334,6 +1335,428 @@  void qmp_drive_mirror(const char *device, const char *target,
     drive_get_ref(drive_get_by_blockdev(bs));
 }
 
+/* Backup related function */
+
+static void backup_run_next_job(void);
+
+static struct GenericBackupState {
+    Error *error;
+    bool cancel;
+    uuid_t uuid;
+    char uuid_str[37];
+    int64_t speed;
+    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;
+    bool started;
+    bool completed;
+    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_cleanup(void)
+{
+    if (backup_state.writer && backup_state.driver) {
+        backup_state.end_time = time(NULL);
+        Error *local_err = NULL;
+        backup_state.driver->close_cb(backup_state.writer, &local_err);
+        error_propagate(&backup_state.error, local_err);
+        backup_state.writer = NULL;
+    }
+
+    if (backup_state.bcb_list) {
+        GList *l = backup_state.bcb_list;
+        while (l) {
+            BackupCB *bcb = l->data;
+            l = g_list_next(l);
+            drive_put_ref_bh_schedule(drive_get_by_blockdev(bcb->bs));
+            g_free(bcb);
+        }
+        g_list_free(backup_state.bcb_list);
+        backup_state.bcb_list = NULL;
+    }
+}
+
+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);
+
+    bcb->completed = true;
+
+    backup_state.driver->complete_cb(backup_state.writer, bcb->dev_id, ret);
+
+    if (!backup_state.cancel) {
+        backup_run_next_job();
+    }
+}
+
+static void backup_cancel(void)
+{
+    backup_state.cancel = true;
+
+    if (!backup_state.error) {
+        error_setg(&backup_state.error, "backup cancelled");
+    }
+
+    /* drain all i/o (awake jobs waiting for aio) */
+    bdrv_drain_all();
+
+    int job_count = 0;
+    GList *l = backup_state.bcb_list;
+    while (l) {
+        BackupCB *bcb = l->data;
+        l = g_list_next(l);
+        BlockJob *job = bcb->bs->job;
+        if (job) {
+            job_count++;
+            if (!bcb->started) {
+                bcb->started = true;
+                backup_job_start(bcb->bs, true);
+            }
+            if (!bcb->completed) {
+                block_job_cancel_sync(job);
+            }
+        }
+    }
+
+    backup_cleanup();
+}
+
+void qmp_backup_cancel(Error **errp)
+{
+    backup_cancel();
+}
+
+static void backup_run_next_job(void)
+{
+    GList *l = backup_state.bcb_list;
+    while (l) {
+        BackupCB *bcb = l->data;
+        l = g_list_next(l);
+
+        if (bcb->bs && bcb->bs->job && !bcb->completed) {
+            if (!bcb->started) {
+                bcb->started = true;
+                bool cancel = backup_state.error || backup_state.cancel;
+                backup_job_start(bcb->bs, cancel);
+            }
+            return;
+        }
+    }
+
+    backup_cleanup();
+}
+
+static void backup_start_jobs(void)
+{
+    /* create all jobs (one for each device), start first one */
+    GList *l = backup_state.bcb_list;
+    while (l) {
+        BackupCB *bcb = l->data;
+        l = g_list_next(l);
+
+        if (backup_job_create(bcb->bs, backup_dump_cb, backup_complete_cb,
+                              bcb, backup_state.speed) != 0) {
+            error_setg(&backup_state.error, "backup_job_create failed");
+            backup_cancel();
+            return;
+        }
+    }
+
+    backup_run_next_job();
+}
+
+char *qmp_backup(const char *backupfile, bool has_format, BackupFormat 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;
+
+    if (backup_state.bcb_list) {
+        error_set(errp, ERROR_CLASS_GENERIC_ERROR,
+                  "previous backup not finished");
+        return NULL;
+    }
+
+    /* Todo: try to auto-detect format based on file name */
+    format = has_format ? format : BACKUP_FORMAT_VMA;
+
+    /* fixme: find driver for specifued format */
+    const BackupDriver *driver = NULL;
+
+    if (!driver) {
+        error_set(errp, ERROR_CLASS_GENERIC_ERROR, "unknown backup format");
+        return NULL;
+    }
+
+    if (has_devlist) {
+        devs = g_strsplit_set(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, &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 */
+
+    backup_state.cancel = false;
+
+    if (backup_state.error) {
+        error_free(backup_state.error);
+        backup_state.error = NULL;
+    }
+
+    backup_state.driver = driver;
+
+    backup_state.speed = (has_speed && speed > 0) ? speed : 0;
+
+    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;
+
+    /* Grab a reference so hotplug does not delete the
+     * BlockDriverState from underneath us.
+     */
+    l = bcblist;
+    while (l) {
+        BackupCB *bcb = l->data;
+        l = g_list_next(l);
+        drive_get_ref(drive_get_by_blockdev(bcb->bs));
+    }
+
+    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 64008a9..0f178d8 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",
@@ -1630,6 +1659,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 2f47a8a..9ac34c5 100644
--- a/hmp.c
+++ b/hmp.c
@@ -131,6 +131,38 @@  void hmp_info_mice(Monitor *mon, const QDict *qdict)
     qapi_free_MouseInfoList(mice_list);
 }
 
+void hmp_info_backup(Monitor *mon, const QDict *qdict)
+{
+    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, const QDict *qdict)
 {
     MigrationInfo *info;
@@ -998,6 +1030,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, true, BACKUP_FORMAT_VMA, 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 30b3c20..ad4cf80 100644
--- a/hmp.h
+++ b/hmp.h
@@ -28,6 +28,7 @@  void hmp_info_mice(Monitor *mon, const QDict *qdict);
 void hmp_info_migrate(Monitor *mon, const QDict *qdict);
 void hmp_info_migrate_capabilities(Monitor *mon, const QDict *qdict);
 void hmp_info_migrate_cache_size(Monitor *mon, const QDict *qdict);
+void hmp_info_backup(Monitor *mon, const QDict *qdict);
 void hmp_info_cpus(Monitor *mon, const QDict *qdict);
 void hmp_info_block(Monitor *mon, const QDict *qdict);
 void hmp_info_blockstats(Monitor *mon, const QDict *qdict);
@@ -65,6 +66,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 6a0f257..e4a810c 100644
--- a/monitor.c
+++ b/monitor.c
@@ -2666,6 +2666,13 @@  static mon_cmd_t info_cmds[] = {
     },
 #endif
     {
+        .name       = "backup",
+        .args_type  = "",
+        .params     = "",
+        .help       = "show backup status",
+        .mhandler.cmd = hmp_info_backup,
+    },
+    {
         .name       = "migrate",
         .args_type  = "",
         .params     = "",
diff --git a/qapi-schema.json b/qapi-schema.json
index 7275b5d..efcbf09 100644
--- a/qapi-schema.json
+++ b/qapi-schema.json
@@ -425,6 +425,39 @@ 
 { 'type': 'EventInfo', 'data': {'name': 'str'} }
 
 ##
+# @BackupStatus:
+#
+# Detailed backup status.
+#
+# @status: #optional string describing the current backup status.
+#          This 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.5.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
@@ -1824,6 +1857,64 @@ 
   'data': { 'path': 'str' },
   'returns': [ 'ObjectPropertyInfo' ] }
 
+
+##
+# @BackupFormat
+#
+# An enumeration of supported backup formats.
+#
+# @vma: Proxmox vma backup format
+##
+{ 'enum': 'BackupFormat',
+  'data': [ 'vma' ] }
+
+##
+# @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.5.0
+##
+{ 'command': 'backup', 'data': { 'backupfile': 'str', '*format': 'BackupFormat',
+                                 '*config-filename': 'str',
+                                 '*devlist': 'str', '*speed': 'int' },
+  'returns': 'str' }
+
+##
+# @query-backup
+#
+# Returns information about current/last backup task.
+#
+# Returns: @BackupStatus
+#
+# Since: 1.5.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.5.0
+##
+{ 'command': 'backup-cancel' }
+
 ##
 # @qom-get:
 #
diff --git a/qmp-commands.hx b/qmp-commands.hx
index 799adea..ca601b3 100644
--- a/qmp-commands.hx
+++ b/qmp-commands.hx
@@ -889,6 +889,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,
@@ -2566,6 +2578,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
 -------