@@ -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 */
@@ -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;
@@ -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
@@ -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;
@@ -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);
@@ -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 = "",
@@ -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:
#
@@ -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
-------
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(-)