#!/usr/bin/python2.6 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright (C) 2010 Joanna Rutkowska # # 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. # # from qubes.qubes import QubesVmCollection from qubes.qubes import QubesException from qubes.qubes import qubes_store_filename from qubes.qubes import qubes_base_dir from qubes.qubes import qubes_templates_dir from qubes.qubes import qubes_appvms_dir from optparse import OptionParser import os import time import subprocess import sys import re def size_to_human (size): if size < 1024: return str (size); elif size < 1024*1024: return str(round(size/1024.0,1)) + ' KiB' elif size < 1024*1024*1024: return str(round(size/(1024.0*1024),1)) + ' MiB' else: return str(round(size/(1024.0*1024*1024),1)) + ' GiB' fields = { "qid": {"func": "vm.qid"}, "name": {"func": "('=>' if backup_collection.get_default_template_vm() is not None\ and vm.qid == backup_collection.get_default_template_vm().qid else '')\ + ('[' if vm.is_template() else '')\ + ('{' if vm.is_netvm() else '')\ + vm.name \ + (']' if vm.is_template() else '')\ + ('}' if vm.is_netvm() else '')"}, "type": {"func": "'Tpl' if vm.is_template() else \ (' Net' if vm.is_netvm() else 'App')"}, "updbl" : {"func": "'Yes' if vm.is_updateable() else ''"}, "template": {"func": "'n/a' if vm.is_template() or vm.template_vm is None else\ find_template_name(backup_collection[vm.template_vm.qid].name,\ options.replace_template)"}, "netvm": {"func": "'n/a' if vm.is_netvm() else\ ('*' if vm.uses_default_netvm else '') +\ backup_collection[vm.netvm_vm.qid].name\ if vm.netvm_vm is not None else '-'"}, "label" : {"func" : "vm.label.name"}, } def is_vm_included_in_backup (backup_dir, vm): if vm.qid == 0: # Dom0 is not included, obviously return False backup_vm_dir_path = vm.dir_path.replace (qubes_base_dir, backup_dir) if os.path.exists (backup_vm_dir_path): return True else: return False def restore_vm_file (backup_dir, file_path): backup_file_path = file_path.replace (qubes_base_dir, backup_dir) #print "cp -rp {0} {1}".format (backup_file_path, file_path) # We prefer to use Linux's cp, because it nicely handles sparse files retcode = subprocess.call (["cp", "-p", backup_file_path, file_path]) if retcode != 0: print "*** Error while copying file {0} to {1}".format(backup_file_path, file_path) exit (1) def restore_vm_dir (backup_dir, src_dir, dst_dir): backup_src_dir = src_dir.replace (qubes_base_dir, backup_dir) # We prefer to use Linux's cp, because it nicely handles sparse files retcode = subprocess.call (["cp", "-rp", backup_src_dir, dst_dir]) if retcode != 0: print "*** Error while copying file {0} to {1}".format(backup_src_dir, dest_dir) exit (1) def find_template_name(template, replaces): rx_replace = re.compile("(.*):(.*)") for r in replaces: m = rx_replace.match(r) if m.group(1) == template: return m.group(2) return template def main(): usage = "usage: %prog [options] " parser = OptionParser (usage) parser.add_option ("--skip-broken", action="store_true", dest="skip_broken", default=False, help="Do not restore VMs that have missing templates or netvms") parser.add_option ("--ignore-missing", action="store_true", dest="ignore_missing", default=False, help="Ignore missing templates or netvms, restore VMs anyway") parser.add_option ("--skip-conflicting", action="store_true", dest="skip_conflicting", default=False, help="Do not restore VMs that are already present on the host") parser.add_option ("--recreate-conf-files", action="store_true", dest="recreate_conf", default=False, help="Recreate conf files after restore") parser.add_option ("--replace-template", action="append", dest="replace_template", default=[], help="Restore VMs using another template, syntax: old-template-name:new-template-name (might be repeated)") (options, args) = parser.parse_args () if (len (args) != 1): print "You must specify the backup directory (e.g. /mnt/backup/qubes-2010-12-01-235959)" exit (0) backup_dir = args[0] if not os.path.exists (backup_dir): print "The backup directory doesn't exist!" exit(1) backup_collection = QubesVmCollection(store_filename = backup_dir + "/qubes.xml") backup_collection.lock_db_for_reading() backup_collection.load() host_collection = QubesVmCollection() host_collection.lock_db_for_writing() host_collection.load() backup_vms_list = [vm for vm in backup_collection.values()] host_vms_list = [vm for vm in host_collection.values()] vms_to_restore = [] fields_to_display = ["name", "type", "template", "updbl", "netvm", "label" ] # First calculate the maximum width of each field we want to display total_width = 0; for f in fields_to_display: fields[f]["max_width"] = len(f) for vm in backup_vms_list: l = len(str(eval(fields[f]["func"]))) if l > fields[f]["max_width"]: fields[f]["max_width"] = l total_width += fields[f]["max_width"] print print "The following VMs are included in the backup:" print # Display the header s = "" for f in fields_to_display: fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1) s += fmt.format('-') print s s = "" for f in fields_to_display: fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1) s += fmt.format(f) print s s = "" for f in fields_to_display: fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1) s += fmt.format('-') print s there_are_conflicting_vms = False there_are_missing_templates = False there_are_missing_netvms = False # ... and the actual data for vm in backup_vms_list: if is_vm_included_in_backup (backup_dir, vm): s = "" good_to_go = True for f in fields_to_display: fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1) s += fmt.format(eval(fields[f]["func"])) if host_collection.get_vm_by_name (vm.name) is not None: s += " <-- A VM with the same name already exists on the host!" there_are_conflicting_vms = True good_to_go = False # Do not overwrite VMs on the host! if vm.template_vm is not None: templatevm_name = find_template_name(vm.template_vm.name, options.replace_template) template_vm_on_host = host_collection.get_vm_by_name (templatevm_name) # No template on the host? if not ((template_vm_on_host is not None) and template_vm_on_host.is_template()): # Maybe the (custom) template is in the backup? template_vm_on_backup = backup_collection.get_vm_by_name (templatevm_name) if not ((template_vm_on_backup is not None) and template_vm_on_backup.is_template): s += " <-- No matching template on the host or in the backup found!" there_are_missing_templates = True good_to_go = False if not (options.ignore_missing) else True if not vm.is_netvm() and vm.netvm_vm is not None: netvm_name = vm.netvm_vm.name netvm_vm_on_host = host_collection.get_vm_by_name (netvm_name) # No netv, on the host? if not ((netvm_vm_on_host is not None) and netvm_vm_on_host.is_netvm): # Maybe the (custom) netvm is in the backup? netvm_vm_on_backup = backup_collection.get_vm_by_name (netvm_name) if not ((netvm_vm_on_backup is not None) and netvm_vm_on_backup.is_netvm): s += " <-- No matching netvm on the host found!" there_are_missing_netvms = True good_to_go = False if not (options.ignore_missing) else True print s if good_to_go: vms_to_restore.append (vm) print if there_are_conflicting_vms: print "*** There VMs with conflicting names on the host! ***" if options.skip_conflicting: print "Those VMs will not be restored, the host VMs will not be overwritten!" else: print "Remove VMs with conflicting names from the host before proceeding." print "... or use --skip-conflicting to restore only those VMs that do not exist on the host." exit (1) print "The above VMs will be copied and added to your system." print "Exisiting VMs will not be removed." if there_are_missing_templates: print "*** One or more template VM is missing on the host! ***" if not (options.skip_broken or options.ignore_missing): print "Install it first, before proceeding with backup restore." print "Or pass: --skip-broken or --ignore-missing switch." exit (1) elif options.skip_broken: print "... VMs that depend on it will not be restored (--skip-broken used)" elif options.ignore_missing: print "... VMs that depend on it will be restored anyway (--ignore-missing used)" else: print "INTERNAL ERROR?!" exit (1) if there_are_missing_netvms: print "*** One or more network VM is missing on the host! ***" if not (options.skip_broken or options.ignore_missing): print "Install it first, before proceeding with backup restore." print "Or pass: --skip_broken or --ignore_missing switch." exit (1) elif options.skip_broken: print "... VMs that depend on it will not be restored (--skip-broken used)" elif options.ignore_missing: print "... VMs that depend on it be restored anyway (--ignore-missing used)" else: print "INTERNAL ERROR?!" exit (1) prompt = raw_input ("Do you want to proceed? [y/N] ") if not (prompt == "y" or prompt == "Y"): exit (0) for vm in vms_to_restore: print "-> Restoring: {0} ...".format(vm.name) retcode = subprocess.call (["mkdir", "-p", vm.dir_path]) if retcode != 0: print ("*** Cannot create directory: {0}?!".format(dest_dir)) print ("Skiping...") continue if vm.is_appvm(): restore_vm_dir (backup_dir, vm.dir_path, qubes_appvms_dir); elif vm.is_template(): restore_vm_dir (backup_dir, vm.dir_path, qubes_templates_dir); else: print "ERROR: VM '{0}', type='{1}': unsupported VM type!".format(vm.name, vm.type) # Add templates... for vm in [ vm for vm in vms_to_restore if vm.is_template()]: print "-> Adding Template VM {0}...".format(vm.name) updateable = vm.updateable vm = host_collection.add_new_templatevm(vm.name, conf_file=vm.conf_file, dir_path=vm.dir_path, installed_by_rpm=False) vm.updateable = updateable try: vm.verify_files() except QubesException as err: print "ERROR: {0}".format(err) print "*** Skiping VM: {0}".vm.name host_collection.pop(vm.qid) continue try: vm.add_to_xen_storage() except (IOError, OSError) as err: print "ERROR: {0}".format(err) print "*** Skiping VM: {0}".vm.name host_collection.pop(vm.qid) continue # ... then appvms... for vm in [ vm for vm in vms_to_restore if vm.is_appvm()]: print "-> Adding AppVM {0}...".format(vm.name) template_vm = None recreate_conf = options.recreate_conf if vm.template_vm is not None: template_name = find_template_name(vm.template_vm.name, options.replace_template) template_vm = host_collection.get_vm_by_name(template_name) if template_name != vm.template_vm.name: recreate_conf = True if not vm.uses_default_netvm: uses_default_netvm = False netvm_vm = host_collection.get_vm_by_name (vm.netvm_vm.name) if vm.netvm_vm is not None else None else: uses_default_netvm = True updateable = vm.updateable vm = host_collection.add_new_appvm(vm.name, template_vm, conf_file=vm.conf_file, dir_path=vm.dir_path, updateable=updateable, label=vm.label) if not uses_default_netvm: vm.uses_default_netvm = False vm.netvm_vm = netvm_vm if template_vm is not None and recreate_conf: print "--> Recreating config file..." vm.create_config_file() vm.create_appmenus(verbose=True) try: vm.verify_files() except QubesException as err: print "ERROR: {0}".format(err) print "*** Skiping VM: {0}".format(vm.name) host_collection.pop(vm.qid) try: vm.add_to_xen_storage() except (IOError, OSError) as err: print "ERROR: {0}".format(err) print "*** Skiping VM: {0}".format(vm.name) host_collection.pop(vm.qid) backup_collection.unlock_db() host_collection.save() host_collection.unlock_db() print "-> Done." main()