diff --git a/doc/manpages/qvm-clone.rst b/doc/manpages/qvm-clone.rst index 98cfb5b8..cd26aac3 100644 --- a/doc/manpages/qvm-clone.rst +++ b/doc/manpages/qvm-clone.rst @@ -1,30 +1,39 @@ .. program:: qvm-clone -=========================================================================== :program:`qvm-clone` -- Clones an existing VM by copying all its disk files =========================================================================== Synopsis -======== -:command:`qvm-clone` [*options*] <*src-name*> <*new-name*> +-------- +:command:`qvm-clone` [-h] [--verbose] [--quiet] [-p *POOL:VOLUME* | -P POOL] *VMNAME* *NEWVM* Options -======= +------- .. option:: --help, -h Show this help message and exit +.. option:: -P POOL + + Pool to use for the new domain. All volumes besides snapshots volumes are + imported in to the specified POOL. ~HIS IS WHAT YOU WANT TO USE NORMALLY. + +.. option:: --pool=POOL:VOLUME, -p POOL:VOLUME + + Specify the pool to use for the specific volume + .. option:: --quiet, -q Be quiet -.. option:: --path=DIR_PATH, -p DIR_PATH +.. option:: --verbose, -v - Specify path to the template directory + Increase verbosity Authors -======= +------- | Joanna Rutkowska | Rafal Wojtczuk | Marek Marczykowski +| Bahtiar `kalkin-` Gadimov diff --git a/qubes/tools/qvm_clone.py b/qubes/tools/qvm_clone.py new file mode 100644 index 00000000..f3444d1f --- /dev/null +++ b/qubes/tools/qvm_clone.py @@ -0,0 +1,73 @@ +#!/usr/bin/python2 +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +''' Clone a domain ''' + +import sys + +from qubes.tools import QubesArgumentParser, SinglePropertyAction + +parser = QubesArgumentParser(description=__doc__, vmname_nargs=1) +parser.add_argument('new_name', + metavar='NEWVM', + action=SinglePropertyAction, + help='name of the domain to create') + +group = parser.add_mutually_exclusive_group() +group.add_argument('-P', + metavar='POOL', + dest='one_pool', + default='', + help='pool to use for the new domain') + +group.add_argument('-p', + '--pool', + action='append', + metavar='POOL:VOLUME', + help='specify the pool to use for the specific volume') + + +def main(args=None): + ''' Clones an existing VM by copying all its disk files ''' + args = parser.parse_args(args) + app = args.app + src_vm = args.domains[0] + new_name = args.properties['new_name'] + dst_vm = app.add_new_vm(src_vm.__class__, name=new_name) + dst_vm.clone_properties(src_vm) + + if args.one_pool: + dst_vm.clone_disk_files(src_vm, pool=args.one_pool) + elif hasattr(args, 'pools') and args.pools: + dst_vm.clone_disk_files(src_vm, pools=args.pools) + else: + dst_vm.clone_disk_files(src_vm) + +# try: + app.save() # HACK remove_from_disk on exception hangs for some reason +# except Exception as e: # pylint: disable=broad-except +# dst_vm.remove_from_disk() +# parser.print_error(e) +# return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index e052e186..e65e78be 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -25,6 +25,7 @@ from __future__ import absolute_import +import copy import base64 import datetime import itertools @@ -1074,6 +1075,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): os.makedirs(self.dir_path, mode=0o775) if pool or pools: + # pylint: disable=attribute-defined-outside-init self.volume_config = _patch_volume_config(self.volume_config, pool, pools) self.storage = qubes.storage.Storage(self) @@ -1096,7 +1098,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): shutil.rmtree(self.dir_path) self.storage.remove() - def clone_disk_files(self, src): + def clone_disk_files(self, src, pool=None, pools=None, ): '''Clone files from other vm. :param qubes.vm.qubesvm.QubesVM src: source VM @@ -1110,9 +1112,11 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): raise qubes.exc.QubesVMNotHaltedError( self, 'Cannot clone a running domain {!r}'.format(self.name)) - if hasattr(src, 'volume_config'): + if pool or pools: # pylint: disable=attribute-defined-outside-init - self.volume_config = src.volume_config + self.volume_config = _patch_volume_config(self.volume_config, pool, + pools) + self.storage = qubes.storage.Storage(self) self.storage.clone(src) self.storage.verify() @@ -1589,3 +1593,53 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): domain.memory_maximum = self.get_mem_static_max() * 1024 return qubes.qmemman.algo.prefmem(domain) / 1024 + + +def _clean_volume_config(config): + common_attributes = ['name', 'pool', 'size', 'internal', 'removable', + 'revisions_to_keep', 'rw', 'snap_on_start', + 'save_on_stop', 'source'] + config_copy = copy.deepcopy(config) + return {k: v for k, v in config_copy.items() if k in common_attributes} + + +def _patch_pool_config(config, pool=None, pools=None): + assert pool is not None or pools is not None + is_saveable = 'save_on_stop' in config and config['save_on_stop'] + is_resetable = not ('snap_on_start' in config and # volatile + config['snap_on_start'] and not is_saveable) + + is_exportable = is_saveable or is_resetable + + name = config['name'] + + if pool and is_exportable: + config['pool'] = str(pool) + elif pool and not is_exportable: + pass + elif pools and name in pools.keys(): + if is_exportable: + config['pool'] = str(pools[name]) + else: + msg = "Can't clone a snapshot volume {!s} to pool {!s} " \ + .format(name, pools[name]) + raise qubes.exc.QubesException(msg) + return config + +def _patch_volume_config(volume_config, pool=None, pools=None): + assert not (pool and pools), \ + 'You can not pass pool & pools parameter at same time' + assert pool or pools + + result = {} + + for name, config in volume_config.items(): + # copy only the subset of volume_config key/values + dst_config = _clean_volume_config(config) + + if pool is not None or pools is not None: + dst_config = _patch_pool_config(dst_config, pool, pools) + + result[name] = dst_config + + return result diff --git a/rpm_spec/core-dom0.spec b/rpm_spec/core-dom0.spec index b6a5927f..afca4fc8 100644 --- a/rpm_spec/core-dom0.spec +++ b/rpm_spec/core-dom0.spec @@ -247,6 +247,7 @@ fi %{python_sitelib}/qubes/tools/qvm_block.py* %{python_sitelib}/qubes/tools/qvm_create.py* %{python_sitelib}/qubes/tools/qvm_features.py* +%{python_sitelib}/qubes/tools/qvm_clone.py* %{python_sitelib}/qubes/tools/qvm_kill.py* %{python_sitelib}/qubes/tools/qvm_ls.py* %{python_sitelib}/qubes/tools/qvm_pause.py*