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 <diet...@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(-) 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 ------- -- 1.7.2.5