[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: [Qemu-devel] [PATCH 1/3] backup: QEMU Backup Tool
From: |
Ishani |
Subject: |
Re: [Qemu-devel] [PATCH 1/3] backup: QEMU Backup Tool |
Date: |
Tue, 22 Aug 2017 22:29:46 +0530 (IST) |
----- On Aug 22, 2017, at 7:43 PM, stefanha address@hidden wrote:
> On Mon, Aug 21, 2017 at 04:40:37PM +0530, Ishani Chugh wrote:
>> qemu-backup will be a command-line tool for performing full and
>> incremental disk backups on running VMs. It is intended as a
>> reference implementation for management stack and backup developers
>> to see QEMU's backup features in action. The tool writes details of
>> guest in a configuration file and the data is retrieved from the file
>> while creating a backup. The location of config file can be set as an
>> environment variable QEMU_BACKUP_CONFIG. The usage is as follows:
>>
>> Add a guest
>> python qemu-backup.py guest add --guest <guest_name> --qmp <socket_path>
>>
>> Add a drive for backup in a specified guest
>> python qemu-backup.py drive add --guest <guest_name> --id <drive_id>
>> [--target
>> <target_file_path>]
>>
>> Create backup of the added drives:
>> python qemu-backup.py backup --guest <guest_name>
>>
>> List all guest configs in configuration file:
>> python qemu-backup.py guest list
>>
>> Restore operation
>> python qemu-backup.py restore --guest <guest-name>
>>
>> Remove a guest
>> python qemu-backup.py guest remove --guest <guest_name>
>>
>> Signed-off-by: Ishani Chugh <address@hidden>
>> ---
>> contrib/backup/qemu-backup.py | 334
>> ++++++++++++++++++++++++++++++++++++++++++
>> 1 file changed, 334 insertions(+)
>> create mode 100644 contrib/backup/qemu-backup.py
>>
>> diff --git a/contrib/backup/qemu-backup.py b/contrib/backup/qemu-backup.py
>> new file mode 100644
>> index 0000000..08570f7
>> --- /dev/null
>> +++ b/contrib/backup/qemu-backup.py
>> @@ -0,0 +1,334 @@
>> +#!/usr/bin/python
>> +# -*- coding: utf-8 -*-
>> +#
>> +# Copyright (C) 2017 Ishani Chugh <address@hidden>
>> +#
>> +# This program is free software; you can redistribute it and/or modify
>> +# it under the terms of the GNU General Public License as published by
>> +# the Free Software Foundation; either version 2 of the License, or
>> +# (at your option) any later version.
>> +#
>> +# This program is distributed in the hope that it will be useful,
>> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
>> +# GNU General Public License for more details.
>> +#
>> +# You should have received a copy of the GNU General Public License
>> +# along with this program. If not, see <http://www.gnu.org/licenses/>.
>> +#
>> +
>> +"""
>> +This file is an implementation of backup tool
>> +"""
>> +from __future__ import print_function
>> +from argparse import ArgumentParser
>> +import os
>> +import errno
>> +from socket import error as socket_error
>> +try:
>> + import configparser
>> +except ImportError:
>> + import ConfigParser as configparser
>> +import sys
>> +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..',
>> + 'scripts', 'qmp'))
>> +from qmp import QEMUMonitorProtocol
>> +
>> +
>> +class BackupTool(object):
>> + """BackupTool Class"""
>> + def __init__(self, config_file=os.path.expanduser('~') +
>> + '/.config/qemu/qemu-backup-config'):
>> + if "QEMU_BACKUP_CONFIG" in os.environ:
>> + self.config_file = os.environ["QEMU_BACKUP_CONFIG"]
>> + else:
>> + self.config_file = config_file
>> + try:
>> + if not os.path.isdir(os.path.dirname(self.config_file)):
>> + os.makedirs(os.path.dirname(self.config_file))
>> + except:
>> + print("Cannot create config directory", file=sys.stderr)
>> + sys.exit(1)
>> + self.config = configparser.ConfigParser()
>> + self.config.read(self.config_file)
>> +
>> + def write_config(self):
>> + """
>> + Writes configuration to ini file.
>> + """
>> + config_file = open(self.config_file+".tmp", 'w')
>> + self.config.write(config_file)
>> + config_file.flush()
>> + os.fsync(config_file.fileno())
>> + config_file.close()
>> + os.rename(self.config_file+".tmp", self.config_file)
>> +
>> + def get_socket_address(self, socket_address):
>> + """
>> + Return Socket address in form of string or tuple
>> + """
>> + if socket_address.startswith('tcp'):
>> + return (socket_address.split(':')[1],
>> + int(socket_address.split(':')[2]))
>> + return socket_address.split(':', 2)[1]
>> +
>> + def _full_backup(self, guest_name):
>> + """
>> + Performs full backup of guest
>> + """
>> + if guest_name not in self.config.sections():
>> + print("Cannot find specified guest", file=sys.stderr)
>> + sys.exit(1)
>> +
>> + self.verify_guest_running(guest_name)
>> + connection = QEMUMonitorProtocol(
>> + self.get_socket_address(
>> +
>> self.config[guest_name]['qmp']))
>> + connection.connect()
>> + cmd = {"execute": "transaction", "arguments": {"actions": []}}
>> + drive_list = []
>> + for key in self.config[guest_name]:
>> + if key.startswith("drive_"):
>> + drive = key[len('drive_'):]
>> + drive_list.append(drive)
>> + target = self.config[guest_name][key]
>> + sub_cmd = {"type": "drive-backup", "data": {"device": drive,
>> + "target":
>> target,
>> + "sync": "full"}}
>> + cmd['arguments']['actions'].append(sub_cmd)
>> + qmp_return = connection.cmd_obj(cmd)
>> + if 'error' in qmp_return:
>> + print(qmp_return['error']['desc'], file=sys.stderr)
>> + sys.exit(1)
>> + print("Backup Started")
>> + while len(drive_list) != 0:
>> + event = connection.pull_event(wait=True)
>> + if event['event'] == 'SHUTDOWN':
>> + print("The guest was SHUT DOWN", file=sys.stderr)
>> + sys.exit(1)
>> +
>> + if event['event'] == 'RESET':
>> + print("The guest was Rebooted", file=sys.stderr)
>> + sys.exit(1)
>> +
>> + if event['data']['device'] in drive_list and \
>> + event['data']['type'] == 'backup':
>
> At this point the event[] dict might be a non-BLOCK_JOB_* event.
> Therefore event['data']['device'] will raise a KeyError exception.
>
> You must check event['event'] before assuming it contains certain
> fields.
Agreed. Will fix in next revision.
>> + if event['event'] == 'BLOCK_JOB_COMPLETED':
>> + print("*"+event['data']['device'])
>> + drive_list.remove(event['data']['device'])
>> + elif event['event'] == 'BLOCK_JOB_ERROR':
>> + print("Cannot Complete Backup", file=sys.stderr)
>> + sys.exit(1)
>> + print("Backup Complete")
>> +
>> + def _drive_add(self, drive_id, guest_name, target=None):
>> + """
>> + Adds drive for backup
>> + """
>> + if target is None:
>> + target = os.path.abspath(drive_id)
>> +
>> + if os.path.isdir(os.path.dirname(target)) is False:
>> + print("Cannot find target directory", file=sys.stderr)
>> + sys.exit(1)
>> +
>> + if guest_name not in self.config.sections():
>> + print("Cannot find specified guest", file=sys.stderr)
>> + sys.exit(1)
>> +
>> + if "drive_"+drive_id in self.config[guest_name]:
>> + print("Drive already marked for backup", file=sys.stderr)
>> + sys.exit(1)
>> +
>> + self.verify_guest_running(guest_name)
>> +
>> + connection = QEMUMonitorProtocol(
>> + self.get_socket_address(
>> +
>> self.config[guest_name]['qmp']))
>> + connection.connect()
>> + cmd = {'execute': 'query-block'}
>> + returned_json = connection.cmd_obj(cmd)
>> + device_present = False
>> + for device in returned_json['return']:
>> + if device['device'] == drive_id:
>> + device_present = True
>> + break
>> +
>> + if not device_present:
>> + print("No such drive in guest", file=sys.stderr)
>> + sys.exit(1)
>> +
>> + drive_id = "drive_" + drive_id
>> + for d_id in self.config[guest_name]:
>> + if self.config[guest_name][d_id] == target:
>> + print("Please choose different target", file=sys.stderr)
>> + sys.exit(1)
>> + self.config.set(guest_name, drive_id, target)
>> + self.write_config()
>> + print("Successfully Added Drive")
>> +
>> + def verify_guest_running(self, guest_name):
>> + """
>> + Checks whether specified guest is running or not
>> + """
>> + socket_address = self.config.get(guest_name, 'qmp')
>> + try:
>> + connection = QEMUMonitorProtocol(self.get_socket_address(
>> + socket_address))
>> + connection.connect()
>> + except socket_error:
>> + if socket_error.errno != errno.ECONNREFUSED:
>> + print("Connection to guest refused", file=sys.stderr)
>> + sys.exit(1)
>> +
>> + def _guest_add(self, guest_name, socket_address):
>> + """
>> + Adds a guest to the config file
>> + """
>> + if guest_name in self.config.sections():
>> + print("ID already exists. Please choose a different guestname",
>> + file=sys.stderr)
>> + sys.exit(1)
>> + self.config[guest_name] = {'qmp': socket_address}
>> + self.verify_guest_running(guest_name)
>> + self.write_config()
>> + print("Successfully Added Guest")
>> +
>> + def _guest_remove(self, guest_name):
>> + """
>> + Removes a guest from config file
>> + """
>> + if guest_name not in self.config.sections():
>> + print("Guest Not present", file=sys.stderr)
>> + sys.exit(1)
>> + self.config.remove_section(guest_name)
>> + print("Guest successfully deleted")
>> +
>> + def _restore(self, guest_name):
>> + """
>> + Prints Steps to perform restore operation
>> + """
>> + if guest_name not in self.config.sections():
>> + print("Cannot find specified guest", file=sys.stderr)
>> + sys.exit(1)
>> +
>> + self.verify_guest_running(guest_name)
>> + connection = QEMUMonitorProtocol(
>> + self.get_socket_address(
>> +
>> self.config[guest_name]['qmp']))
>> + connection.connect()
>> + print("To perform restore, replace:")
>> + for key in self.config[guest_name]:
>> + if key.startswith("drive_"):
>> + drive = key[len('drive_'):]
>> + target = self.config[guest_name][key]
>> + cmd = {'execute': 'query-block'}
>> + returned_json = connection.cmd_obj(cmd)
>> + device_present = False
>> + for device in returned_json['return']:
>> + if device['device'] == drive:
>> + device_present = True
>> + location = device['inserted']['image']['filename']
>> + print(location+" By "+target)
>
> I suggest print('Replace ' + location + ' with ' + target) for a
> slightly clearer message.
Will fix in next revision.
>> +
>> + if not device_present:
>> + print("No such drive in guest", file=sys.stderr)
>> + sys.exit(1)
>
> Since the command itself requires the guest to be running
> users are very likely to manipulate the disk image files while the guest
> is running.
>
> This command must prompt the user to shut down the guest before
> restoring.
The intention behind this condition is the case where user may have detached
a disk from the guest(and have rerun the guest). I will put up the message to
shut down the guest.
>> +
>> + def guest_remove_wrapper(self, args):
>> + """
>> + Wrapper for _guest_remove method.
>> + """
>> + guest_name = args.guest
>> + self._guest_remove(guest_name)
>> + self.write_config()
>> +
>> + def list(self, args):
>> + """
>> + Prints guests present in Config file
>> + """
>> + for guest_name in self.config.sections():
>> + print(guest_name)
>> +
>> + def guest_add_wrapper(self, args):
>> + """
>> + Wrapper for _quest_add method
>> + """
>> + self._guest_add(args.guest, args.qmp)
>> +
>> + def drive_add_wrapper(self, args):
>> + """
>> + Wrapper for _drive_add method
>> + """
>> + self._drive_add(args.id, args.guest, args.target)
>> +
>> + def fullbackup_wrapper(self, args):
>> + """
>> + Wrapper for _full_backup method
>> + """
>> + self._full_backup(args.guest)
>> +
>> + def restore_wrapper(self, args):
>> + """
>> + Wrapper for restore
>> + """
>> + self._restore(args.guest)
>> +
>> +
>> +def main():
>> + backup_tool = BackupTool()
>> + parser = ArgumentParser()
>> + subparsers = parser.add_subparsers(title='Subcommands',
>> + description='Valid Subcommands',
>> + help='Subcommand help')
>> + guest_parser = subparsers.add_parser('guest', help='Adds or \
>> + removes and lists
>> guest(s)')
>> + guest_subparsers = guest_parser.add_subparsers(title='Guest Subparser')
>> + guest_list_parser = guest_subparsers.add_parser('list',
>> + help='Lists all guests')
>> + guest_list_parser.set_defaults(func=backup_tool.list)
>> +
>> + guest_add_parser = guest_subparsers.add_parser('add', help='Adds a
>> guest')
>> + guest_add_parser.add_argument('--guest', action='store', type=str,
>> + help='Name of the guest')
>> + guest_add_parser.add_argument('--qmp', action='store', type=str,
>> + help='Path of socket')
>> + guest_add_parser.set_defaults(func=backup_tool.guest_add_wrapper)
>> +
>> + guest_remove_parser = guest_subparsers.add_parser('remove',
>> + help='removes a
>> guest')
>> + guest_remove_parser.add_argument('--guest', action='store', type=str,
>> + help='Name of the guest')
>> + guest_remove_parser.set_defaults(func=backup_tool.guest_remove_wrapper)
>> +
>> + drive_parser = subparsers.add_parser('drive',
>> + help='Adds drive(s) for backup')
>> + drive_subparsers = drive_parser.add_subparsers(title='Add subparser',
>> + description='Drive \
>> + subparser')
>> + drive_add_parser = drive_subparsers.add_parser('add',
>> + help='Adds new \
>> + drive for backup')
>> + drive_add_parser.add_argument('--guest', action='store',
>> + type=str, help='Name of the guest')
>> + drive_add_parser.add_argument('--id', action='store',
>> + type=str, help='Drive ID')
>> + drive_add_parser.add_argument('--target', nargs='?',
>> + default=None, help='Destination path')
>> + drive_add_parser.set_defaults(func=backup_tool.drive_add_wrapper)
>> +
>> + backup_parser = subparsers.add_parser('backup', help='Creates backup')
>> + backup_parser.add_argument('--guest', action='store',
>> + type=str, help='Name of the guest')
>> + backup_parser.set_defaults(func=backup_tool.fullbackup_wrapper)
>> +
>> + backup_parser = subparsers.add_parser('restore', help='Restores drives')
>> + backup_parser.add_argument('--guest', action='store',
>> + type=str, help='Name of the guest')
>> + backup_parser.set_defaults(func=backup_tool.restore_wrapper)
>> +
>> + args = parser.parse_args()
>> + args.func(args)
>> +
>> +if __name__ == '__main__':
>> + main()
>> --
>> 2.7.4
Thanks for review.