From patchwork Thu Feb 21 11:27:49 2013 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Dietmar Maurer X-Patchwork-Id: 222271 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.gnu.org (lists.gnu.org [208.118.235.17]) (using TLSv1 with cipher AES256-SHA (256/256 bits)) (Client did not present a certificate) by ozlabs.org (Postfix) with ESMTPS id D298D2C0082 for ; Thu, 21 Feb 2013 23:09:18 +1100 (EST) Received: from localhost ([::1]:55514 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1U8UKY-0004sk-U1 for incoming@patchwork.ozlabs.org; Thu, 21 Feb 2013 06:28:50 -0500 Received: from eggs.gnu.org ([208.118.235.92]:35182) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1U8UJq-0003ff-5w for qemu-devel@nongnu.org; Thu, 21 Feb 2013 06:28:12 -0500 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1U8UJh-0001qs-LZ for qemu-devel@nongnu.org; Thu, 21 Feb 2013 06:28:06 -0500 Received: from www.maurer-it.com ([213.129.239.114]:52130 helo=maui.maurer-it.com) by eggs.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1U8UJh-0001ov-1w for qemu-devel@nongnu.org; Thu, 21 Feb 2013 06:27:57 -0500 Received: by maui.maurer-it.com (Postfix, from userid 0) id 41C523B08BB; Thu, 21 Feb 2013 12:27:55 +0100 (CET) From: Dietmar Maurer To: qemu-devel@nongnu.org Date: Thu, 21 Feb 2013 12:27:49 +0100 Message-Id: <1361446072-601980-4-git-send-email-dietmar@proxmox.com> X-Mailer: git-send-email 1.7.2.5 In-Reply-To: <1361446072-601980-1-git-send-email-dietmar@proxmox.com> References: <1361446072-601980-1-git-send-email-dietmar@proxmox.com> X-detected-operating-system: by eggs.gnu.org: GNU/Linux 3.x X-Received-From: 213.129.239.114 Cc: Dietmar Maurer Subject: [Qemu-devel] [PATCH v5 3/6] add backup related monitor commands X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.14 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org Sender: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org We use a generic BackupDriver struct to encapsulate all archive format related function. Another option would be to simply dump 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 --- backup.h | 15 ++ blockdev.c | 423 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ hmp-commands.hx | 31 ++++ hmp.c | 63 ++++++++ hmp.h | 3 + monitor.c | 7 + qapi-schema.json | 95 ++++++++++++ qmp-commands.hx | 27 ++++ 8 files changed, 664 insertions(+), 0 deletions(-) diff --git a/backup.h b/backup.h index 9b1ea1c..22598a6 100644 --- a/backup.h +++ b/backup.h @@ -14,6 +14,9 @@ #ifndef QEMU_BACKUP_H #define QEMU_BACKUP_H +#include +#include "block/block.h" + #define BACKUP_CLUSTER_BITS 16 #define BACKUP_CLUSTER_SIZE (1<dump); + + size_t zero_bytes = 0; + int bytes = backup_state.driver->dump(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(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); + assert(backup_state.driver->close); + + bcb->completed = true; + + backup_state.driver->complete(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 *backup_file, bool has_format, BackupFormat format, + bool has_config_file, const char *config_file, + 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(backup_file, 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(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_file) { + char *cdata = NULL; + gsize clen = 0; + GError *err = NULL; + if (!g_file_get_contents(config_file, &cdata, &clen, &err)) { + error_setg(errp, "unable to read file '%s'", config_file); + goto err; + } + + const char *basename = g_path_get_basename(config_file); + if (driver->register_config(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.backup_file) { + g_free(backup_state.backup_file); + } + backup_state.backup_file = g_strdup(backup_file); + + 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(backup_file); + if (driver) { + Error *err = NULL; + driver->close(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.backup_file) { + info->has_backup_file = true; + info->backup_file = g_strdup(backup_state.backup_file); + } + + 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..b2c1f23 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_backup_file) { + 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->backup_file); + 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 *backup_file = qdict_get_str(qdict, "backup-file"); + const char *devlist = qdict_get_try_str(qdict, "devlist"); + int64_t speed = qdict_get_try_int(qdict, "speed", 0); + + Error *errp = NULL; + + qmp_backup(backup_file, 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..09ca8ef 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', + '*backup-file': 'str', '*uuid': 'str' } } + +## # @query-events: # # Return a list of supported QMP events by this server @@ -1824,6 +1857,68 @@ '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. +# +# @backup-file: 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 +# +# @devlist: #optional list of block device names (separated by ',', ';' +# or ':'). By default the backup includes all writable block devices. +# +# Returns: the uuid of the backup job +# +# Since: 1.5.0 +## +{ 'command': 'backup', 'data': { 'backup-file': 'str', + '*format': 'BackupFormat', + '*config-file': '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..17e381b 100644 --- a/qmp-commands.hx +++ b/qmp-commands.hx @@ -889,6 +889,18 @@ EQMP }, { + .name = "backup", + .args_type = "backup-file:s,format:s?,config-file: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 -------