diff --git a/dom0/qvm-core/qubes.py b/dom0/qvm-core/qubes.py index fcee728d..2c7a7730 100755 --- a/dom0/qvm-core/qubes.py +++ b/dom0/qvm-core/qubes.py @@ -1042,6 +1042,9 @@ class QubesAppVm(QubesVm): print "--> Creating icon symlink: {0} -> {1}".format(self.icon_path, self.label.icon_path) os.symlink (self.label.icon_path, self.icon_path) + self.create_appmenus (verbose) + + def create_appmenus(self, verbose): subprocess.check_call ([qubes_appmenu_create_cmd, self.template_vm.appmenus_templates_dir, self.name]) def get_disk_utilization_root_img(self): @@ -1146,10 +1149,11 @@ class QubesVmCollection(dict): A collection of Qubes VMs indexed by Qubes id (qid) """ - def __init__(self): + def __init__(self, store_filename=qubes_store_filename): super(QubesVmCollection, self).__init__() self.default_netvm_qid = None self.default_template_qid = None + self.qubes_store_filename = store_filename def values(self): for qid in self.keys(): @@ -1309,23 +1313,23 @@ class QubesVmCollection(dict): def check_if_storage_exists(self): try: - f = open (qubes_store_filename, 'r') + f = open (self.qubes_store_filename, 'r') except IOError: return False f.close() return True def create_empty_storage(self): - self.qubes_store_file = open (qubes_store_filename, 'w') + self.qubes_store_file = open (self.qubes_store_filename, 'w') self.clear() self.save() def lock_db_for_reading(self): - self.qubes_store_file = open (qubes_store_filename, 'r') + self.qubes_store_file = open (self.qubes_store_filename, 'r') fcntl.lockf (self.qubes_store_file, fcntl.LOCK_SH) def lock_db_for_writing(self): - self.qubes_store_file = open (qubes_store_filename, 'r+') + self.qubes_store_file = open (self.qubes_store_filename, 'r+') fcntl.lockf (self.qubes_store_file, fcntl.LOCK_EX) def unlock_db(self): diff --git a/dom0/qvm-tools/qvm-backup b/dom0/qvm-tools/qvm-backup new file mode 100755 index 00000000..664b6050 --- /dev/null +++ b/dom0/qvm-tools/qvm-backup @@ -0,0 +1,243 @@ +#!/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 optparse import OptionParser +import os +import time +import subprocess +import sys + +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' + +def file_to_backup (file_path, sz = None): + if sz is None: + sz = os.path.getsize (qubes_store_filename) + + abs_file_path = os.path.abspath (file_path) + abs_base_dir = os.path.abspath (qubes_base_dir) + '/' + abs_file_dir = os.path.dirname (abs_file_path) + '/' + (nothing, dir, subdir) = abs_file_dir.partition (abs_base_dir) + assert nothing == "" + assert dir == abs_base_dir + return [ { "path" : file_path, "size": sz, "subdir": subdir} ] + +def main(): + usage = "usage: %prog [options] " + parser = OptionParser (usage) + + (options, args) = parser.parse_args () + + if (len (args) != 1): + print "You must specify the target backup directory (e.g. /mnt/backup)" + print "qvm-backup will create a subdirectory there for each individual backup." + exit (0) + + base_backup_dir = args[0] + + if not os.path.exists (base_backup_dir): + print "The target directory doesn't exist!" + exit(1) + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + + vms_list = [vm for vm in qvm_collection.values()] + no_vms = len (vms_list) + + files_to_backup = file_to_backup (qubes_store_filename) + + appvms_to_backup = [vm for vm in vms_list if vm.is_appvm()] + there_are_running_vms = False + + fields_to_display = [ + { "name": "VM", "width": 16}, + { "name": "type","width": 12 }, + { "name": "size", "width": 12} + ] + + # Display the header + s = "" + for f in fields_to_display: + fmt="{{0:-^{0}}}-+".format(f["width"] + 1) + s += fmt.format('-') + print s + s = "" + for f in fields_to_display: + fmt="{{0:>{0}}} |".format(f["width"] + 1) + s += fmt.format(f["name"]) + print s + s = "" + for f in fields_to_display: + fmt="{{0:-^{0}}}-+".format(f["width"] + 1) + s += fmt.format('-') + print s + + if len (appvms_to_backup): + for vm in appvms_to_backup: + + vm_sz = vm.get_disk_usage (vm.private_img) + files_to_backup += file_to_backup(vm.private_img, vm_sz ) + + files_to_backup += file_to_backup(vm.icon_path) + files_to_backup += file_to_backup(vm.conf_file) + #files_to_backup += file_to_backup(vm.dir_path + "/apps") + + if vm.is_updateable(): + sz = vm.get_disk_usage(vm.rootcow_img) + files_to_backup += file_to_backup(vm.rootcow_img, sz) + vm_sz += sz + + s = "" + fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1) + s += fmt.format(vm.name) + + fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1) + s += fmt.format("AppVM" + (" + COW" if vm.is_updateable() else "")) + + fmt="{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1) + s += fmt.format(size_to_human(vm_sz)) + + if vm.is_running(): + s += " <-- The VM is running, please shut it down before proceeding with the backup!" + there_are_running_vms = True + + print s + + template_vms_worth_backingup = [ vm for vm in vms_list if (vm.is_templete() and not vm.installed_by_rpm)] + if len (template_vms_worth_backingup): + for vm in template_vms_worth_backingup: + vm_sz = vm.get_disk_utilization() + files_to_backup += file_to_backup (vm.dir_path, vm_sz) + + s = "" + fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1) + s += fmt.format(vm.name) + + fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1) + s += fmt.format("Template VM") + + fmt="{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1) + s += fmt.format(size_to_human(vm_sz)) + + if vm.is_running(): + s += " <-- The VM is running, please shut it down before proceeding with the backup!" + there_are_running_vms = True + + print s + + + + total_backup_sz = 0 + for file in files_to_backup: + total_backup_sz += file["size"] + + s = "" + for f in fields_to_display: + fmt="{{0:-^{0}}}-+".format(f["width"] + 1) + s += fmt.format('-') + print s + + s = "" + fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1) + s += fmt.format("Total size:") + fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1 + 2 + fields_to_display[2]["width"] + 1) + s += fmt.format(size_to_human(total_backup_sz)) + print s + + s = "" + for f in fields_to_display: + fmt="{{0:-^{0}}}-+".format(f["width"] + 1) + s += fmt.format('-') + print s + + stat = os.statvfs(base_backup_dir) + backup_fs_free_sz = stat.f_bsize * stat.f_bavail + print + if (total_backup_sz > backup_fs_free_sz): + print "Not enough space avilable on the backup filesystem!" + exit (1) + + if (there_are_running_vms): + print "Please shutdown all VMs before proceeding." + exit (1) + + + backup_dir = base_backup_dir + "/qubes-{0}".format (time.strftime("%Y-%m-%d-%H%M%S")) + if os.path.exists (backup_dir): + print "ERROR: the path {0} already exists?!".format(backup_dir) + print "Aborting..." + exit (1) + + print "-> Backup dir: {0}".format (backup_dir) + print "-> Avilable space: {0}".format(size_to_human(backup_fs_free_sz)) + + prompt = raw_input ("Do you want to proceed? [y/N] ") + if not (prompt == "y" or prompt == "Y"): + exit (0) + + os.mkdir (backup_dir) + + if not os.path.exists (backup_dir): + print "ERROR: Strange: couldn't create backup dir: {0}?!".format(backup_dir) + print "Aborting..." + exit (1) + + bytes_backedup = 0 + for file in files_to_backup: + # We prefer to use Linux's cp, because it nicely handles sparse files + progress = bytes_backedup * 100 / total_backup_sz + print >> sys.stderr, "\r-> Backing up files: {0}%...".format (progress), + dest_dir = backup_dir + '/' + file["subdir"] + if file["subdir"] != "": + retcode = subprocess.call (["mkdir", "-p", dest_dir]) + if retcode != 0: + print "Cannot create directory: {0}?!".format(dest_dir) + print "Aborting..." + exit(1) + + retcode = subprocess.call (["cp", "-rp", file["path"], dest_dir]) + if retcode != 0: + print "Error while copying file {0} to {1}".format(file["path"], dest_dir) + exit (1) + + bytes_backedup += file["size"] + progress = bytes_backedup * 100 / total_backup_sz + print >> sys.stderr, "\r-> Backing up files: {0}%...".format (progress), + + print + print "-> Backup completed." + + qvm_collection.unlock_db() +main() diff --git a/dom0/qvm-tools/qvm-backup-restore b/dom0/qvm-tools/qvm-backup-restore new file mode 100755 index 00000000..228ab21f --- /dev/null +++ b/dom0/qvm-tools/qvm-backup-restore @@ -0,0 +1,351 @@ +#!/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 optparse import OptionParser + +import os +import time +import subprocess +import sys + +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_templete() else '')\ + + ('{' if vm.is_netvm() else '')\ + + vm.name \ + + (']' if vm.is_templete() else '')\ + + ('}' if vm.is_netvm() else '')"}, + + "type": {"func": "'Tpl' if vm.is_templete() else \ + (' Net' if vm.is_netvm() else 'App')"}, + + "updbl" : {"func": "'Yes' if vm.is_updateable() else ''"}, + + "template": {"func": "'n/a' if vm.is_templete() or vm.is_netvm() else\ + backup_collection[vm.template_vm.qid].name"}, + + "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", "-rp", backup_file_path, file_path]) + if retcode != 0: + print "*** Error while copying file {0} to {1}".format(backup_file_path, file_path) + return + +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") + + (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 + + if vm.is_appvm(): + templatevm_name = vm.template_vm.name + 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_templete): + + # 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_templete): + 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! ***" + print "Remove VMs with conflicting names from the host before proceeding." + print "You can use 'qvm-remove ' command for this." + print "Be careful! Think twice before typing qvm-remove!!!" + 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 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_file (backup_dir, vm.private_img) + restore_vm_file (backup_dir, vm.icon_path) + restore_vm_file (backup_dir, vm.conf_file) + + if vm.is_updateable(): + restore_vm_file (backup_dir, vm.rootcow_img) + + elif vm.is_templete(): + restore_vm_file (backup_dir, vm.dir_path + "/*"); + else: + print "ERROR: VM '{0}', type='{1}': unsupported VM type!".format(vm.name, vm.type) + + print "-> Adding to Qubes DB..." + + # Add templates... + for vm in [ vm for vm in vms_to_restore if vm.is_templete()]: + 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 = host_collection.get_vm_by_name(vm.template_vm.name) + + 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, + label=vm.label) + + vm.updateable = updateable + if not uses_default_netvm: + vm.uses_default_netvm = False + vm.netvm_vm = netvm_vm + + + vm.create_appmenus(verbose=True) + try: + vm.verify_files() + except QubesException as err: + print "ERROR: {0}".format(err) + print "*** Skiping VM: {0}".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}".vm.name + host_collection.pop(vm.qid) + + + backup_collection.unlock_db() + host_collection.save() + host_collection.unlock_db() +main()