From nobody Tue Apr 30 15:45:31 2024 Delivered-To: importer@patchew.org Received-SPF: pass (zoho.com: domain of gnu.org designates 208.118.235.17 as permitted sender) client-ip=208.118.235.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Authentication-Results: mx.zohomail.com; dkim=fail; spf=pass (zoho.com: domain of gnu.org designates 208.118.235.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org Return-Path: Received: from lists.gnu.org (lists.gnu.org [208.118.235.17]) by mx.zohomail.com with SMTPS id 1500151480413737.4964693271899; Sat, 15 Jul 2017 13:44:40 -0700 (PDT) Received: from localhost ([::1]:43212 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1dWTva-0000GZ-3k for importer@patchew.org; Sat, 15 Jul 2017 16:44:38 -0400 Received: from eggs.gnu.org ([2001:4830:134:3::10]:46225) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1dWTup-0008Pi-SE for qemu-devel@nongnu.org; Sat, 15 Jul 2017 16:43:53 -0400 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1dWTul-0000Xo-3Y for qemu-devel@nongnu.org; Sat, 15 Jul 2017 16:43:51 -0400 Received: from research.iiit.ac.in ([196.12.53.8]:42706) by eggs.gnu.org with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32) (Exim 4.71) (envelope-from ) id 1dWTuk-0000T1-3F for qemu-devel@nongnu.org; Sat, 15 Jul 2017 16:43:47 -0400 Received: from localhost (localhost [127.0.0.1]) by research.iiit.ac.in (Postfix) with ESMTP id 67B817452E2; Sun, 16 Jul 2017 02:13:40 +0530 (IST) Received: from research.iiit.ac.in ([127.0.0.1]) by localhost (research.iiit.ac.in [127.0.0.1]) (amavisd-new, port 10032) with ESMTP id tKdGNXtD6BWK; Sun, 16 Jul 2017 02:13:35 +0530 (IST) Received: from localhost (localhost [127.0.0.1]) by research.iiit.ac.in (Postfix) with ESMTP id 862037452F8; Sun, 16 Jul 2017 02:13:35 +0530 (IST) Received: from research.iiit.ac.in ([127.0.0.1]) by localhost (research.iiit.ac.in [127.0.0.1]) (amavisd-new, port 10026) with ESMTP id MMusNDIK_AFP; Sun, 16 Jul 2017 02:13:35 +0530 (IST) Received: from ishani-Inspiron-5558.iiit.ac.in (unknown [10.1.99.89]) by research.iiit.ac.in (Postfix) with ESMTPSA id 5CC417452E2; Sun, 16 Jul 2017 02:13:35 +0530 (IST) DKIM-Filter: OpenDKIM Filter v2.9.2 research.iiit.ac.in 862037452F8 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=research.iiit.ac.in; s=4E8815E6-5B55-11E4-B758-8D4964374E96; t=1500151415; bh=rd1YSJT4aLIyx01bJNQPOfshejJrL9CFmZmRkTXmXvw=; h=From:To:Subject:Date:Message-Id; b=PIcVUaw47aKUI9ENnJ9SBzp2c8sym3rNznyxN97z2c9bcuygOqsrSa9w2/xrG/K7s EErdXaklqnm5HP+4l9PG5uvOw4TD6r8aU9j0Qn5MJYD1tfgn1e5wn3OongMMSQg16d Dt5pAPvmam5oxRMiMzulq7dZIWSgewsGi3iRYSMk= X-Virus-Scanned: amavisd-new at research.iiit.ac.in From: Ishani Chugh To: qemu-devel@nongnu.org Date: Sun, 16 Jul 2017 02:13:21 +0530 Message-Id: <1500151401-30863-1-git-send-email-chugh.ishani@research.iiit.ac.in> X-Mailer: git-send-email 2.7.4 X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] [fuzzy] X-Received-From: 196.12.53.8 Subject: [Qemu-devel] [RFC] RFC on Backup tool X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Ishani Chugh , jsnow@redhat.com, stefanha@redhat.com Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: "Qemu-devel" X-ZohoMail-DKIM: fail (Header signature does not verify) X-ZohoMail: RDKM_2 RSF_0 Z_629925259 SPT_0 Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" This is a Request For Comments patch for qemu backup tool. As an Outreachy intern, I am assigned to the project for creating a backup tool. 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 usage is as follows: Add a guest python qemu-backup.py guest add --guest --qmp [-= -tcp] Add a drive for backup in a specified guest python qemu-backup.py drive add --guest --id [--tar= get ] Create backup of the added drives: python qemu-backup.py backup --guest List all guest configs in configuration file: python qemu-backup.py guest list I will be obliged by any feedback. Signed-off-by: Ishani Chugh --- contrib/backup/qemu-backup.py | 244 ++++++++++++++++++++++++++++++++++++++= ++++ 1 file changed, 244 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..9c3dc53 --- /dev/null +++ b/contrib/backup/qemu-backup.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +This file is an implementation of backup tool +""" +from argparse import ArgumentParser +import os +import errno +from socket import error as socket_error +import configparser +import sys +sys.path.append('../../scripts/qmp') +from qmp import QEMUMonitorProtocol + + +class BackupTool(object): + """BackupTool Class""" + def __init__(self, config_file=3D'backup.ini'): + self.config_file =3D config_file + self.config =3D configparser.ConfigParser() + self.config.read(self.config_file) + + def write_config(self): + """ + Writes configuration to ini file. + """ + with open(self.config_file, 'w') as config_file: + self.config.write(config_file) + + def get_socket_path(self, socket_path, tcp): + """ + Return Socket address in form of string or tuple + """ + if tcp is False: + return os.path.abspath(socket_path) + return (socket_path.split(':')[0], int(socket_path.split(':')[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") + return + if self.is_guest_running(guest_name, self.config[guest_name]['qmp'= ], + self.config[guest_name]['tcp']) is False: + return + connection =3D QEMUMonitorProtocol( + self.get_socket_path( + self.config[guest_name]['qmp'= ], + self.config[guest_name]['tcp'= ])) + connection.connect() + cmd =3D {"execute": "transaction", "arguments": {"actions": []}} + for key in self.config[guest_name]: + if key.startswith("drive_"): + drive =3D key[key.index('_')+1:] + target =3D self.config[guest_name][key] + sub_cmd =3D {"type": "drive-backup", "data": {"device": dr= ive, + "target": targ= et, + "sync": "full"= }} + cmd['arguments']['actions'].append(sub_cmd) + print (connection.cmd_obj(cmd)) + + def __drive_add(self, drive_id, guest_name, target=3DNone): + """ + Adds drive for backup + """ + if target is None: + target =3D os.path.abspath(drive_id) + ".img" + + if guest_name not in self.config.sections(): + print ("Cannot find specified guest") + return + + if "drive_"+drive_id in self.config[guest_name]: + print ("Drive already marked for backup") + return + + if self.is_guest_running(guest_name, self.config[guest_name]['qmp'= ], + self.config[guest_name]['tcp']) is False: + return + + connection =3D QEMUMonitorProtocol( + self.get_socket_path( + self.config[guest_name]['qmp'= ], + self.config[guest_name]['tcp'= ])) + connection.connect() + cmd =3D {'execute': 'query-block'} + returned_json =3D connection.cmd_obj(cmd) + device_present =3D False + for device in returned_json['return']: + if device['device'] =3D=3D drive_id: + device_present =3D True + break + + if device_present is False: + print ("No such drive in guest") + return + + drive_id =3D "drive_" + drive_id + for id in self.config[guest_name]: + if self.config[guest_name][id] =3D=3D target: + print ("Please choose different target") + return + self.config.set(guest_name, drive_id, target) + self.write_config() + print("Successfully Added Drive") + + def is_guest_running(self, guest_name, socket_path, tcp): + """ + Checks whether specified guest is running or not + """ + try: + connection =3D QEMUMonitorProtocol( + self.get_socket_path( + socket_path, tcp)) + connection.connect() + except socket_error: + if socket_error.errno !=3D errno.ECONNREFUSED: + print ("Connection to guest refused") + return False + except: + print ("Unable to connect to guest") + return False + return True + + def __guest_add(self, guest_name, socket_path, tcp): + """ + Adds a guest to the config file + """ + if self.is_guest_running(guest_name, socket_path, tcp) is False: + return + + if guest_name in self.config.sections(): + print ("ID already exists. Please choose a different guestname= ") + return + + self.config[guest_name] =3D {'qmp': socket_path} + self.config.set(guest_name, 'tcp', str(tcp)) + 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") + return + self.config.remove_section(guest_name) + print("Guest successfully deleted") + + def guest_remove_wrapper(self, args): + """ + Wrapper for __guest_remove method. + """ + guest_name =3D 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 + """ + if args.tcp is False: + self.__guest_add(args.guest, args.qmp, False) + else: + self.__guest_add(args.guest, args.qmp, True) + + 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 main(): + backup_tool =3D BackupTool() + parser =3D ArgumentParser() + subparsers =3D parser.add_subparsers(title=3D'Subcommands', + description=3D'Valid Subcommands', + help=3D'Subcommand help') + guest_parser =3D subparsers.add_parser('guest', help=3D'Adds or \ + removes and lists guest= (s)') + guest_subparsers =3D guest_parser.add_subparsers(title=3D'Guest Subpar= ser') + guest_list_parser =3D guest_subparsers.add_parser('list', + help=3D'Lists all gues= ts') + guest_list_parser.set_defaults(func=3Dbackup_tool.list) + + guest_add_parser =3D guest_subparsers.add_parser('add', help=3D'Adds a= guest') + guest_add_parser.add_argument('--guest', action=3D'store', type=3Dstr, + help=3D'Name of the guest') + guest_add_parser.add_argument('--qmp', action=3D'store', type=3Dstr, + help=3D'Path of socket') + guest_add_parser.add_argument('--tcp', nargs=3D'?', type=3Dbool, + default=3DFalse, + help=3D'Specify if socket is tcp') + guest_add_parser.set_defaults(func=3Dbackup_tool.guest_add_wrapper) + + guest_remove_parser =3D guest_subparsers.add_parser('remove', + help=3D'removes a gu= est') + guest_remove_parser.add_argument('--guest', action=3D'store', type=3Ds= tr, + help=3D'Name of the guest') + guest_remove_parser.set_defaults(func=3Dbackup_tool.guest_remove_wrapp= er) + + drive_parser =3D subparsers.add_parser('drive', + help=3D'Adds drive(s) for backup') + drive_subparsers =3D drive_parser.add_subparsers(title=3D'Add subparse= r', + description=3D'Drive \ + subparser') + drive_add_parser =3D drive_subparsers.add_parser('add', + help=3D'Adds new \ + drive for backup') + drive_add_parser.add_argument('--guest', action=3D'store', + type=3Dstr, help=3D'Name of the guest') + drive_add_parser.add_argument('--id', action=3D'store', + type=3Dstr, help=3D'Drive ID') + drive_add_parser.add_argument('--target', nargs=3D'?', + default=3DNone, help=3D'Destination path= ') + drive_add_parser.set_defaults(func=3Dbackup_tool.drive_add_wrapper) + + backup_parser =3D subparsers.add_parser('backup', help=3D'Creates back= up') + backup_parser.add_argument('--guest', action=3D'store', + type=3Dstr, help=3D'Name of the guest') + backup_parser.set_defaults(func=3Dbackup_tool.fullbackup_wrapper) + + args =3D parser.parse_args() + args.func(args) + +if __name__ =3D=3D '__main__': + main() --=20 2.7.4