qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Qemu-devel] [RFC] RFC on Backup tool


From: Ishani
Subject: Re: [Qemu-devel] [RFC] RFC on Backup tool
Date: Fri, 21 Jul 2017 02:48:45 +0530 (IST)


----- On Jul 19, 2017, at 2:59 AM, jsnow address@hidden wrote:

> On 07/17/2017 03:37 PM, Ishani wrote:
>> ----- On Jul 17, 2017, at 12:48 PM, Fam Zheng address@hidden wrote:
>>> On Sun, 07/16 02:13, Ishani Chugh wrote:
> 
> [...]
> 
>>> Only full backup is implemented in this patch, is the plan to add 
>>> incremental
>>> backup on top?  I'm curious because you have target file path set during 
>>> drive
>>> add, but in the incremental case, it should be possible that each backup 
>>> creates
>>> a new target file that chains up with earlier ones, so I think the target 
>>> file
>>> should be an option for "backup" command as well.
>> 
>> Yes. Incremental backup is to be added. I am still in learning phase with
>> respect to incremental backups. I will modify the arguments and workflow
>> accordingly.
>> 
> 
> You may consider solidifying the backup target *pattern* during drive
> add as an alternative, such as:
> 
> .../path/to/backup/%VM%/%DRIVE%/%yyyy%-%mm%-%dd%.qcow2
> 
> Or some such scheme. Simple numerals work well, too:
> 
> myvm/sda/incr.0.qcow2
> myvm/sda/incr.1.qcow2
> 
> Simple numerals offer the benefit that it is easier to reconstruct the
> chain if you lose your metadata in the python script.
> 
> Also consider that even for non-incremental backups, we want full
> backups made subsequently to not, in general, overwrite the previous
> full backup, so the TARGET is more of a "living entity" than a fixed
> thing, even in the simple case.

Okay. Thats a great suggestion. But, it might not work when the entire chain
of backups is not present at the same location. Can a case like this arise?

>>>> 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 <guest_name> --qmp <socket_path> 
>>>> [--tcp]
>>>>
>>>> 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
>>>
>>> Please put these examples into the help output of the command, this way 
>>> users
>>> can read it as well.
>> 
>> I have created a manpage documentation for the tool.
>> http://lists.nongnu.org/archive/html/qemu-devel/2017-07/msg05241.html
>> It is to be updated continuously as the development progresses.
>> 
> 
> You can include this (or a similar link) in future cover letters for the
> benefit of reviewers.

Okay. Will update in next revision.

>>>>
>>>> I will be obliged by any feedback.
>>>
>>> Thanks for doing this!
>>>
> 
> Yes, thank you :)
> 

Thanks for review. :)

>>>>
>>>> Signed-off-by: Ishani Chugh <address@hidden>
>>>> ---
>>>>  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 -*-
>>>
>>> You need a copyright header here (the default choice for QEMU is GPLv2 but 
>>> there
>>> is no strict restrictions for scripts). See examples in other *.py files.
>> 
>> Thanks. Will update in next revision.
>>  
> 
> Yes, up to you. Files without a copyright default to GPLv2 in the QEMU
> project, but if you feel strongly one way or another you can argue for
> that license in upstream review.
> 
> (For instance, documentation and other non-code documents can sometimes
> be better served by different licenses.)
> 
> We tend to avoid the implicit copyright when possible, so including an
> explicit GPLv2 license is preferable to declare intent.
> 
> QEMU as a whole is GPLv2, so this is a good license to use if you don't
> have any strong feelings on the matter, but please take a moment to read
> the implications of the license as a new contributor to our project:
> 
> https://tldrlegal.com/license/gnu-general-public-license-v2
> 
> And the full, legal text:
> 
> https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
> 

I will go through the licenses and update in next revision. Thanks.

>>>> +"""
>>>> +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
>>>
>>> Python2 has ConfigParser while python3 has configparser. Please be specific
>>> about the python compatibility level of this script - my system (Fedora) has
>>> python2 as /usr/bin/python, so the shebang and your example command in the
>>> commit message don't really work. "six" module can handle python 2/3
>>> differentiations, or you can use '#!/usr/bin/env python2' to specify a 
>>> python
>>> version explicitly.
>> 
>> The script is supposed to be both python2/3 compatible. I will take reference
>> from Stefan's review and fix it in next revision.
>> 
>>>> +import sys
>>>> +sys.path.append('../../scripts/qmp')
>>>
>>> This expects the script to be invoked from its local directory? It's better 
>>> to
>>> use the __file__ trick to allow arbitrary workdir:
>>>
>>> $ git grep __file__
>>> scripts/device-crash-test:sys.path.append(os.path.join(os.path.dirname(__file__),
>>> '..', 'scripts'))
>>> scripts/qemu-gdb.py:sys.path.append(os.path.dirname(__file__))
>>> tests/migration/guestperf/engine.py:sys.path.append(os.path.join(os.path.dirname(__file__),
>>> '..', '..', '..', 'scripts'))
>>> tests/qemu-iotests/iotests.py:sys.path.append(os.path.join(os.path.dirname(__file__),
>>> '..', '..', 'scripts'))
>> 
>> Thanks. Will fix it in next revision.
>> 
> 
> Thanks for the hint, Fam!
> 
>>>> +from qmp import QEMUMonitorProtocol
>>>> +
>>>> +
>>>> +class BackupTool(object):
>>>> +    """BackupTool Class"""
>>>> +    def __init__(self, config_file='backup.ini'):
>>>
>>> Is it better to put this in a more predictable place such as
>>> "$HOME/.qemu-backup.ini" and/or make it a command line option?
>> 
>> It is planned to be an optional command-line option, if not
>> provided, the default location suggested should be taken.
>> 
> 
> $HOME/.config/QEMU/backup.ini is my preference here.
> 
> Just a preference; but we may wind up needing more than one or two
> secret files, and this would be a good place to keep them.

If we require multiple files, then this is a good location. Is it okay that,
at present, since we require only config file, I put the default location in 
$HOME/.qemu-backup.ini ? 

>>>> +        self.config_file = config_file
>>>> +        self.config = 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):
>>>
>>> I think double underscore attributes are special:
>>>
>>> https://www.python.org/dev/peps/pep-0008/#id47
>>>
>>> If it's not intended, perhaps it's enough to just stick to one "_". The same
>>> applies to a few more below.
>> 
>> I used double underscores to stick with internal usage. However, single
>> underscores can be used as well for weak internal usage. Will fix in
>> next revision.
>>  
>>>> +        """
>>>> +        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 = QEMUMonitorProtocol(
>>>> +                                         self.get_socket_path(
>>>> +                                             
>>>> self.config[guest_name]['qmp'],
>>>> +                                             
>>>> self.config[guest_name]['tcp']))
>>>> +        connection.connect()
>>>> +        cmd = {"execute": "transaction", "arguments": {"actions": []}}
>>>> +        for key in self.config[guest_name]:
>>>> +            if key.startswith("drive_"):
>>>> +                drive = key[key.index('_')+1:]
>>>> +                target = self.config[guest_name][key]
>>>> +                sub_cmd = {"type": "drive-backup", "data": {"device": 
>>>> drive,
>>>> +                                                            "target": 
>>>> target,
>>>> +                                                            "sync": 
>>>> "full"}}
>>>> +                cmd['arguments']['actions'].append(sub_cmd)
>>>> +        print (connection.cmd_obj(cmd))
>>>> +
>>>> +    def __drive_add(self, drive_id, guest_name, target=None):
>>>> +        """
>>>> +        Adds drive for backup
>>>> +        """
>>>> +        if target is None:
>>>> +            target = os.path.abspath(drive_id) + ".img"
>>>> +
>>>> +        if guest_name not in self.config.sections():
>>>> +            print ("Cannot find specified guest")
>>>
>>> Error messages should go to stderr.
>> 
>> Will update in next revision.
>> 
>>>
>>>> +            return
>>>> +
>>>> +        if "drive_"+drive_id in self.config[guest_name]:
>>>
>>> Whitespace around "+"?
>>>
>> 
>> I am not sure how I missed it. I ran the code through online
>> PEP8 checker. Will fix it.
>> 
>>>> +            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 = QEMUMonitorProtocol(
>>>> +                                         self.get_socket_path(
>>>> +                                             
>>>> self.config[guest_name]['qmp'],
>>>> +                                             
>>>> self.config[guest_name]['tcp']))
>>>> +        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 device_present is False:
>>>> +            print ("No such drive in guest")
>>>> +            return
>>>> +
>>>> +        drive_id = "drive_" + drive_id
>>>> +        for id in self.config[guest_name]:
>>>
>>> I think id is a python built-in function, please avoid using it as variable
>>> name.
>>>
>> 
>> Will fix in next revision.
>> 
>>>> +            if self.config[guest_name][id] == 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 = QEMUMonitorProtocol(
>>>> +                                             self.get_socket_path(
>>>> +                                                  socket_path, tcp))
>>>> +            connection.connect()
>>>> +        except socket_error:
>>>> +            if socket_error.errno != 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")
>>>
>>> "guest name".
>> 
>> Will fix in next revision. Thanks.
>> 
>>>> +            return
>>>> +
>>>> +        self.config[guest_name] = {'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 = 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 = 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.add_argument('--tcp', nargs='?', type=bool,
>>>> +                                  default=False,
>>>> +                                  help='Specify if socket is tcp')
>>>> +    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)
>>>> +
>>>> +    args = parser.parse_args()
>>>> +    args.func(args)
>>>
>>> All exit points should report a status code (zero for success and non-zero 
>>> for
>>> failures). This way the user or upper layer software know if backup has
>>> succeeded.
>> 
>> I agree. Will update in next revision. Thanks.
>> 
>>>> +
>>>> +if __name__ == '__main__':
>>>> +    main()
>>>> --
>>>
>>> Nice patch, thanks!
>> 
>> Thanks for a deep and detailed review.
>> 
> 
> Fam's the best! :)
> 
>> Regards,
>> Ishani
>> 
> 
> Thank you both.

Thanks for review.

> --John



reply via email to

[Prev in Thread] Current Thread [Next in Thread]