diff --git a/doc/manpages/qvm-backup-restore.rst b/doc/manpages/qvm-backup-restore.rst index c3ff451..0407174 100644 --- a/doc/manpages/qvm-backup-restore.rst +++ b/doc/manpages/qvm-backup-restore.rst @@ -53,11 +53,6 @@ Options Restore VMs that are already present on the host under different names -.. option:: --replace-template=REPLACE_TEMPLATE - - Restore VMs using another template, syntax: - ``old-template-name:new-template-name`` (might be repeated) - .. option:: --exclude=EXCLUDE, -x EXCLUDE Skip restore of specified VM (might be repeated) diff --git a/qubesadmin/backup/restore.py b/qubesadmin/backup/restore.py index 3049bdf..c1d092a 100644 --- a/qubesadmin/backup/restore.py +++ b/qubesadmin/backup/restore.py @@ -1472,26 +1472,50 @@ class BackupRestore(object): # check template if vm_info.template: - template_name = vm_info.template - try: - host_template = self.app.domains[template_name] - except KeyError: - host_template = None - present_on_host = (host_template and - host_template.klass == 'TemplateVM') - present_in_backup = (template_name in restore_info.keys() and - restore_info[template_name].good_to_go and - restore_info[template_name].vm.klass == - 'TemplateVM') - if not present_on_host and not present_in_backup: - if self.options.use_default_template and \ - self.app.default_template: - if vm_info.orig_template is None: - vm_info.orig_template = template_name - vm_info.template = self.app.default_template.name + present_on_host = False + if vm_info.template in self.app.domains: + host_tpl = self.app.domains[vm_info.template] + if vm_info.vm.klass == 'DispVM': + present_on_host = ( + getattr(host_tpl, 'template_for_dispvms', False)) else: - vm_info.problems.add( - self.VMToRestore.MISSING_TEMPLATE) + present_on_host = host_tpl.klass == 'TemplateVM' + + present_in_backup = False + if vm_info.template in restore_info: + bak_tpl = restore_info[vm_info.template] + if bak_tpl.good_to_go: + if vm_info.vm.klass == 'DispVM': + present_in_backup = ( + bak_tpl.vm.properties.get( + 'template_for_dispvms', False)) + else: + present_in_backup = ( + bak_tpl.vm.klass == 'TemplateVM') + + self.log.debug( + "vm=%s template=%s on_host=%s in_backup=%s", + vm_info.name, vm_info.template, + present_on_host, present_in_backup) + + if not present_on_host and not present_in_backup: + if vm_info.vm.klass == 'DispVM': + default_template = self.app.default_dispvm + else: + default_template = self.app.default_template + + if (self.options.use_default_template + and default_template is not None): + if vm_info.orig_template is None: + vm_info.orig_template = vm_info.template + vm_info.template = default_template.name + + self.log.debug( + "vm=%s orig_template=%s -> default_template=%s", + vm_info.name, vm_info.orig_template, + default_template.name) + else: + vm_info.problems.add(self.VMToRestore.MISSING_TEMPLATE) # check netvm if vm_info.vm.properties.get('netvm', None) is not None: @@ -1664,18 +1688,19 @@ class BackupRestore(object): @staticmethod def _templates_first(vms): - '''Sort templates befor other VM types (AppVM etc)''' + '''Sort templates before other VM types''' def key_function(instance): '''Key function for :py:func:`sorted`''' if isinstance(instance, BackupVM): - return instance.klass == 'TemplateVM' + if instance.klass == 'TemplateVM': + return 0 + elif instance.properties.get('template_for_dispvms', False): + return 1 + return 2 elif hasattr(instance, 'vm'): return key_function(instance.vm) - return 0 - return sorted(vms, - key=key_function, - reverse=True) - + return 9 + return sorted(vms, key=key_function) def _handle_dom0(self, stream): '''Extract dom0 home''' @@ -1805,6 +1830,14 @@ class BackupRestore(object): self.log.info("-> Please install updates for all the restored " "templates.") + def _restore_property(self, vm, prop, value): + '''Restore a single VM property, logging exceptions''' + try: + setattr(vm, prop, value) + except Exception as err: # pylint: disable=broad-except + self.log.error('Error setting %s.%s to %s: %s', + vm.name, prop, value, err) + def _restore_vms_metadata(self, restore_info): '''Restore VM metadata @@ -1860,6 +1893,12 @@ class BackupRestore(object): del self.app.domains[new_vm.name] continue + # restore this property early to be ready for dependent DispVMs + prop = 'template_for_dispvms' + value = vm.properties.get(prop, None) + if value is not None: + self._restore_property(new_vm, prop, value) + restore_info[vm.name].restored_vm = new_vm for vm in vms.values(): @@ -1872,15 +1911,14 @@ class BackupRestore(object): continue for prop, value in vm.properties.items(): + # can't reset the first; already handled the second + if prop in ['dispid', 'template_for_dispvms']: + continue # exclude VM references - handled manually according to # restore options if prop in ['template', 'netvm', 'default_dispvm']: continue - try: - setattr(new_vm, prop, value) - except Exception as err: # pylint: disable=broad-except - self.log.error('Error setting %s.%s to %s: %s', - vm.name, prop, value, err) + self._restore_property(new_vm, prop, value) for feature, value in vm.features.items(): try: diff --git a/qubesadmin/tests/tools/qvm_backup.py b/qubesadmin/tests/tools/qvm_backup.py index af2573b..48700a4 100644 --- a/qubesadmin/tests/tools/qvm_backup.py +++ b/qubesadmin/tests/tools/qvm_backup.py @@ -33,10 +33,7 @@ class TC_00_qvm_backup(qubesadmin.tests.QubesTestCase): profile = io.StringIO() qvm_backup.write_backup_profile(profile, args) expected_profile = ( - 'destination_path: /var/tmp\n' - 'destination_vm: dom0\n' - 'include: [\'$type:AppVM\', \'$type:TemplateVM\', ' - '\'$type:StandaloneVM\']\n' + '{destination_path: /var/tmp, destination_vm: dom0, include: null}\n' ) self.assertEqual(profile.getvalue(), expected_profile) @@ -67,8 +64,7 @@ class TC_00_qvm_backup(qubesadmin.tests.QubesTestCase): 'destination_path: /var/tmp\n' 'destination_vm: dom0\n' 'exclude: [vm1, vm2]\n' - 'include: [\'$type:AppVM\', \'$type:TemplateVM\', ' - '\'$type:StandaloneVM\']\n' + 'include: null\n' ) self.assertEqual(profile.getvalue(), expected_profile) @@ -77,11 +73,7 @@ class TC_00_qvm_backup(qubesadmin.tests.QubesTestCase): profile = io.StringIO() qvm_backup.write_backup_profile(profile, args, passphrase='test123') expected_profile = ( - 'destination_path: /var/tmp\n' - 'destination_vm: dom0\n' - 'include: [\'$type:AppVM\', \'$type:TemplateVM\', ' - '\'$type:StandaloneVM\']\n' - 'passphrase_text: test123\n' + '{destination_path: /var/tmp, destination_vm: dom0, include: null, passphrase_text: test123}\n' ) self.assertEqual(profile.getvalue(), expected_profile) @@ -101,10 +93,7 @@ class TC_00_qvm_backup(qubesadmin.tests.QubesTestCase): qvm_backup.main(['--save-profile', 'test-profile', '/var/tmp'], app=self.app) expected_profile = ( - 'destination_path: /var/tmp\n' - 'destination_vm: dom0\n' - 'include: [\'$type:AppVM\', \'$type:TemplateVM\', ' - '\'$type:StandaloneVM\']\n' + '{destination_path: /var/tmp, destination_vm: dom0, include: null}\n' ) with open(profile_path) as f: self.assertEqual(expected_profile, f.read()) @@ -130,11 +119,8 @@ class TC_00_qvm_backup(qubesadmin.tests.QubesTestCase): qvm_backup.main(['--save-profile', 'test-profile', '/var/tmp'], app=self.app) expected_profile = ( - 'destination_path: /var/tmp\n' - 'destination_vm: dom0\n' - 'include: [\'$type:AppVM\', \'$type:TemplateVM\', ' - '\'$type:StandaloneVM\']\n' - 'passphrase_text: some password\n' + '{destination_path: /var/tmp, destination_vm: dom0, include: null, passphrase_text: some\n' + ' password}\n' ) with open(profile_path) as f: self.assertEqual(expected_profile, f.read()) @@ -194,8 +180,7 @@ class TC_00_qvm_backup(qubesadmin.tests.QubesTestCase): 'destination_path: /var/tmp\n' 'destination_vm: dom0\n' 'exclude: [vm1]\n' - 'include: [\'$type:AppVM\', \'$type:TemplateVM\', ' - '\'$type:StandaloneVM\']\n' + 'include: null\n' '# specify backup passphrase below\n' 'passphrase_text: ...\n' ) @@ -225,11 +210,8 @@ class TC_00_qvm_backup(qubesadmin.tests.QubesTestCase): 'test-profile', '/var/tmp'], app=self.app) expected_profile = ( - 'destination_path: /var/tmp\n' - 'destination_vm: dom0\n' - 'include: [\'$type:AppVM\', \'$type:TemplateVM\', ' - '\'$type:StandaloneVM\']\n' - 'passphrase_text: other passphrase\n' + '{destination_path: /var/tmp, destination_vm: dom0, include: null, passphrase_text: other\n' + ' passphrase}\n' ) with open(profile_path) as f: self.assertEqual(expected_profile, f.read()) diff --git a/qubesadmin/tests/tools/qvm_backup_restore.py b/qubesadmin/tests/tools/qvm_backup_restore.py index a3cfab6..534fb10 100644 --- a/qubesadmin/tests/tools/qvm_backup_restore.py +++ b/qubesadmin/tests/tools/qvm_backup_restore.py @@ -93,7 +93,7 @@ class TC_00_qvm_backup_restore(qubesadmin.tests.QubesTestCase): app=self.app) mock_backup.assert_called_once_with( self.app, '/some/path', None, 'testpass') - self.assertEqual(exclude_list, ['test-vm2']) + self.assertEqual(mock_backup.return_value.options.exclude, ['test-vm2']) self.assertAllCalled() def test_010_handle_broken_no_problems(self): diff --git a/qubesadmin/tools/qvm_backup.py b/qubesadmin/tools/qvm_backup.py index 0535dc1..088ce7d 100644 --- a/qubesadmin/tools/qvm_backup.py +++ b/qubesadmin/tools/qvm_backup.py @@ -94,11 +94,7 @@ def write_backup_profile(output_stream, args, passphrase=None): ''' profile_data = {} - if args.vms: - profile_data['include'] = args.vms - else: - profile_data['include'] = [ - '$type:AppVM', '$type:TemplateVM', '$type:StandaloneVM'] + profile_data['include'] = args.vms or None if args.exclude_list: profile_data['exclude'] = args.exclude_list if passphrase: diff --git a/qubesadmin/tools/qvm_backup_restore.py b/qubesadmin/tools/qvm_backup_restore.py index 06a7b0c..80898a2 100644 --- a/qubesadmin/tools/qvm_backup_restore.py +++ b/qubesadmin/tools/qvm_backup_restore.py @@ -55,12 +55,6 @@ parser.add_argument("--rename-conflicting", action="store_true", help="Restore VMs that are already present on the host " "under different names") -parser.add_argument("--replace-template", action="append", - dest="replace_template", default=[], - help="Restore VMs using another TemplateVM; syntax: " - "old-template-name:new-template-name (may be " - "repeated)") - parser.add_argument("-x", "--exclude", action="append", dest="exclude", default=[], help="Skip restore of specified VM (may be repeated)") @@ -229,20 +223,12 @@ def main(args=None, app=None): if args.ignore_missing: backup.options.use_default_template = True backup.options.use_default_netvm = True - if args.replace_template: - backup.options.replace_template = args.replace_template - if args.rename_conflicting: - backup.options.rename_conflicting = True - if not args.dom0_home: - backup.options.dom0_home = False - if args.ignore_username_mismatch: - backup.options.ignore_username_mismatch = True - if args.ignore_size_limit: - backup.options.ignore_size_limit = True - if args.exclude: - backup.options.exclude = args.exclude - if args.verify_only: - backup.options.verify_only = True + backup.options.rename_conflicting = args.rename_conflicting + backup.options.dom0_home = args.dom0_home + backup.options.ignore_username_mismatch = args.ignore_username_mismatch + backup.options.ignore_size_limit = args.ignore_size_limit + backup.options.exclude = args.exclude + backup.options.verify_only = args.verify_only restore_info = None try: