Merge branch 'new-backups'

Conflicts:
	core-modules/000QubesVm.py
This commit is contained in:
Marek Marczykowski-Górecki 2013-11-29 04:00:58 +01:00
commit 27f6f0e64e
8 changed files with 1490 additions and 643 deletions

View File

@ -123,6 +123,9 @@ class QubesVm(object):
"default_user": { "default": "user" }, "default_user": { "default": "user" },
"qrexec_timeout": { "default": 60 }, "qrexec_timeout": { "default": 60 },
"autostart": { "default": False, "attr": "_autostart" }, "autostart": { "default": False, "attr": "_autostart" },
"backup_content" : { 'default': False },
"backup_size" : { 'default': 0, "eval": "int(value)" },
"backup_path" : { 'default': "" },
##### Internal attributes - will be overriden in __init__ regardless of args ##### Internal attributes - will be overriden in __init__ regardless of args
"config_file_template": { "eval": 'system_path["config_template_pv"]' }, "config_file_template": { "eval": 'system_path["config_template_pv"]' },
"icon_path": { "eval": 'os.path.join(self.dir_path, "icon.png") if self.dir_path is not None else None' }, "icon_path": { "eval": 'os.path.join(self.dir_path, "icon.png") if self.dir_path is not None else None' },
@ -140,7 +143,9 @@ class QubesVm(object):
'uses_default_kernel', 'kernel', 'uses_default_kernelopts',\ 'uses_default_kernel', 'kernel', 'uses_default_kernelopts',\
'kernelopts', 'services', 'installed_by_rpm',\ 'kernelopts', 'services', 'installed_by_rpm',\
'uses_default_netvm', 'include_in_backups', 'debug',\ 'uses_default_netvm', 'include_in_backups', 'debug',\
'default_user', 'qrexec_timeout', 'autostart' ]: 'default_user', 'qrexec_timeout', 'autostart',
'default_user', 'qrexec_timeout',
'backup_content', 'backup_size', 'backup_path' ]:
attrs[prop]['save'] = 'str(self.%s)' % prop attrs[prop]['save'] = 'str(self.%s)' % prop
# Simple paths # Simple paths
for prop in ['conf_file', 'root_img', 'volatile_img', 'private_img']: for prop in ['conf_file', 'root_img', 'volatile_img', 'private_img']:

View File

@ -25,9 +25,13 @@ from qubes.qubes import QubesNetVm,register_qubes_vm_class,xl_ctx,xc
from qubes.qubes import defaults from qubes.qubes import defaults
from qubes.qubes import QubesException,dry_run from qubes.qubes import QubesException,dry_run
class QubesDom0NetVm(QubesNetVm): class QubesAdminVm(QubesNetVm):
# In which order load this VM type from qubes.xml
load_order = 10
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(QubesDom0NetVm, self).__init__(qid=0, name="dom0", netid=0, super(QubesAdminVm, self).__init__(qid=0, name="dom0", netid=0,
dir_path=None, dir_path=None,
private_img = None, private_img = None,
template = None, template = None,
@ -35,6 +39,10 @@ class QubesDom0NetVm(QubesNetVm):
**kwargs) **kwargs)
self.xid = 0 self.xid = 0
@property
def type(self):
return "AdminVM"
def is_running(self): def is_running(self):
return True return True
@ -83,10 +91,7 @@ class QubesDom0NetVm(QubesNetVm):
domains = xc.domain_getinfo(0, 1) domains = xc.domain_getinfo(0, 1)
return domains[0] return domains[0]
def create_xml_element(self):
return None
def verify_files(self): def verify_files(self):
return True return True
register_qubes_vm_class(QubesDom0NetVm) register_qubes_vm_class(QubesAdminVm)

1369
core/backup.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -609,10 +609,6 @@ class QubesVmCollection(dict):
def load(self): def load(self):
self.clear() self.clear()
dom0vm = QubesDom0NetVm (collection=self)
self[dom0vm.qid] = dom0vm
self.default_netvm_qid = 0
try: try:
tree = lxml.etree.parse(self.qubes_store_file) tree = lxml.etree.parse(self.qubes_store_file)
except (EnvironmentError, except (EnvironmentError,
@ -665,6 +661,12 @@ class QubesVmCollection(dict):
# using 123/udp port) # using 123/udp port)
if self.clockvm_qid is not None: if self.clockvm_qid is not None:
self[self.clockvm_qid].services['ntpd'] = False self[self.clockvm_qid].services['ntpd'] = False
# Add dom0 if wasn't present in qubes.xml
if not 0 in self.keys():
dom0vm = QubesAdminVm (collection=self)
self[dom0vm.qid] = dom0vm
return True return True
def pop(self, qid): def pop(self, qid):

View File

@ -28,6 +28,7 @@ import sys
import os import os
import subprocess import subprocess
import re import re
import shutil
import time import time
import grp,pwd import grp,pwd
from datetime import datetime from datetime import datetime
@ -754,622 +755,4 @@ class QubesWatch(object):
while True: while True:
self.watch_single() self.watch_single()
######## Backups #########
def get_disk_usage(file_or_dir):
if not os.path.exists(file_or_dir):
return 0
p = subprocess.Popen (["du", "-s", "--block-size=1", file_or_dir],
stdout=subprocess.PIPE)
result = p.communicate()
m = re.match(r"^(\d+)\s.*", result[0])
sz = int(m.group(1)) if m is not None else 0
return sz
def file_to_backup (file_path, sz = None):
if sz is None:
sz = os.path.getsize (system_path["qubes_store_filename"])
abs_file_path = os.path.abspath (file_path)
abs_base_dir = os.path.abspath (system_path["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 backup_prepare(base_backup_dir, vms_list = None, exclude_list = [], print_callback = print_stdout):
"""If vms = None, include all (sensible) VMs; exclude_list is always applied"""
if not os.path.exists (base_backup_dir):
raise QubesException("The target directory doesn't exist!")
files_to_backup = file_to_backup (system_path["qubes_store_filename"])
if exclude_list is None:
exclude_list = []
if vms_list is None:
qvm_collection = QubesVmCollection()
qvm_collection.lock_db_for_reading()
qvm_collection.load()
# FIXME: should be after backup completed
qvm_collection.unlock_db()
all_vms = [vm for vm in qvm_collection.values()]
selected_vms = [vm for vm in all_vms if vm.include_in_backups]
appvms_to_backup = [vm for vm in selected_vms if vm.is_appvm() and not vm.internal]
netvms_to_backup = [vm for vm in selected_vms if vm.is_netvm() and not vm.qid == 0]
template_vms_worth_backingup = [vm for vm in selected_vms if (vm.is_template() and not vm.installed_by_rpm)]
vms_list = appvms_to_backup + netvms_to_backup + template_vms_worth_backingup
vms_for_backup = vms_list
# Apply exclude list
if exclude_list:
vms_for_backup = [vm for vm in vms_list if vm.name not in exclude_list]
no_vms = len (vms_for_backup)
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_callback(s)
s = ""
for f in fields_to_display:
fmt="{{0:>{0}}} |".format(f["width"] + 1)
s += fmt.format(f["name"])
print_callback(s)
s = ""
for f in fields_to_display:
fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
s += fmt.format('-')
print_callback(s)
for vm in vms_for_backup:
if vm.is_template():
# handle templates later
continue
if vm.private_img is not None:
vm_sz = vm.get_disk_usage (vm.private_img)
files_to_backup += file_to_backup(vm.private_img, vm_sz )
if vm.is_appvm():
files_to_backup += file_to_backup(vm.icon_path)
if vm.updateable:
if os.path.exists(vm.dir_path + "/apps.templates"):
# template
files_to_backup += file_to_backup(vm.dir_path + "/apps.templates")
else:
# standaloneVM
files_to_backup += file_to_backup(vm.dir_path + "/apps")
if os.path.exists(vm.dir_path + "/kernels"):
files_to_backup += file_to_backup(vm.dir_path + "/kernels")
if os.path.exists (vm.firewall_conf):
files_to_backup += file_to_backup(vm.firewall_conf)
if 'appmenus_whitelist' in vm_files and \
os.path.exists(vm.dir_path + vm_files['appmenus_whitelist']):
files_to_backup += file_to_backup(vm.dir_path + vm_files['appmenus_whitelist'])
if vm.updateable:
sz = vm.get_disk_usage(vm.root_img)
files_to_backup += file_to_backup(vm.root_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)
if vm.is_netvm():
s += fmt.format("NetVM" + (" + Sys" if vm.updateable else ""))
else:
s += fmt.format("AppVM" + (" + Sys" if vm.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_callback(s)
for vm in vms_for_backup:
if not vm.is_template():
# already handled
continue
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_callback(s)
# Dom0 user home
if not 'dom0' in exclude_list:
local_user = grp.getgrnam('qubes').gr_mem[0]
home_dir = pwd.getpwnam(local_user).pw_dir
# Home dir should have only user-owned files, so fix it now to prevent
# permissions problems - some root-owned files can left after
# 'sudo bash' and similar commands
subprocess.check_call(['sudo', 'chown', '-R', local_user, home_dir])
home_sz = get_disk_usage(home_dir)
home_to_backup = [ { "path" : home_dir, "size": home_sz, "subdir": 'dom0-home'} ]
files_to_backup += home_to_backup
s = ""
fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
s += fmt.format('Dom0')
fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
s += fmt.format("User home")
fmt="{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
s += fmt.format(size_to_human(home_sz))
print_callback(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_callback(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_callback(s)
s = ""
for f in fields_to_display:
fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
s += fmt.format('-')
print_callback(s)
stat = os.statvfs(base_backup_dir)
backup_fs_free_sz = stat.f_bsize * stat.f_bavail
print_callback("")
if (total_backup_sz > backup_fs_free_sz):
raise QubesException("Not enough space available on the backup filesystem!")
if (there_are_running_vms):
raise QubesException("Please shutdown all VMs before proceeding.")
print_callback("-> Available space: {0}".format(size_to_human(backup_fs_free_sz)))
return files_to_backup
def backup_do(base_backup_dir, files_to_backup, progress_callback = None):
total_backup_sz = 0
for file in files_to_backup:
total_backup_sz += file["size"]
backup_dir = base_backup_dir + "/qubes-{0}".format (time.strftime("%Y-%m-%d-%H%M%S"))
if os.path.exists (backup_dir):
raise QubesException("ERROR: the path {0} already exists?!".format(backup_dir))
os.mkdir (backup_dir)
if not os.path.exists (backup_dir):
raise QubesException("Strange: couldn't create backup dir: {0}?!".format(backup_dir))
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
progress_callback(progress)
dest_dir = backup_dir + '/' + file["subdir"]
if file["subdir"] != "":
retcode = subprocess.call (["mkdir", "-p", dest_dir])
if retcode != 0:
raise QubesException("Cannot create directory: {0}?!".format(dest_dir))
retcode = subprocess.call (["cp", "-rp", file["path"], dest_dir])
if retcode != 0:
raise QubesException("Error while copying file {0} to {1}".format(file["path"], dest_dir))
bytes_backedup += file["size"]
progress = bytes_backedup * 100 / total_backup_sz
progress_callback(progress)
def backup_restore_set_defaults(options):
if 'use-default-netvm' not in options:
options['use-default-netvm'] = False
if 'use-none-netvm' not in options:
options['use-none-netvm'] = False
if 'use-default-template' not in options:
options['use-default-template'] = False
if 'dom0-home' not in options:
options['dom0-home'] = True
if 'replace-template' not in options:
options['replace-template'] = []
return options
def backup_restore_prepare(backup_dir, options = {}, host_collection = None):
# Defaults
backup_restore_set_defaults(options)
#### Private functions begin
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 (system_path["qubes_base_dir"], backup_dir)
if os.path.exists (backup_vm_dir_path):
return True
else:
return False
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
#### Private functions end
if not os.path.exists (backup_dir):
raise QubesException("The backup directory doesn't exist!")
backup_collection = QubesVmCollection(store_filename = backup_dir + "/qubes.xml")
backup_collection.lock_db_for_reading()
backup_collection.load()
if host_collection is None:
host_collection = QubesVmCollection()
host_collection.lock_db_for_reading()
host_collection.load()
host_collection.unlock_db()
backup_vms_list = [vm for vm in backup_collection.values()]
host_vms_list = [vm for vm in host_collection.values()]
vms_to_restore = {}
there_are_conflicting_vms = False
there_are_missing_templates = False
there_are_missing_netvms = False
dom0_username_mismatch = False
restore_home = False
# ... and the actual data
for vm in backup_vms_list:
if is_vm_included_in_backup (backup_dir, vm):
vms_to_restore[vm.name] = {}
vms_to_restore[vm.name]['vm'] = vm;
if 'exclude' in options.keys():
vms_to_restore[vm.name]['excluded'] = vm.name in options['exclude']
vms_to_restore[vm.name]['good-to-go'] = False
if host_collection.get_vm_by_name (vm.name) is not None:
vms_to_restore[vm.name]['already-exists'] = True
vms_to_restore[vm.name]['good-to-go'] = False
if vm.template is None:
vms_to_restore[vm.name]['template'] = None
else:
templatevm_name = find_template_name(vm.template.name, options['replace-template'])
vms_to_restore[vm.name]['template'] = templatevm_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_template()):
# Maybe the (custom) template is in the backup?
template_vm_on_backup = backup_collection.get_vm_by_name (templatevm_name)
if template_vm_on_backup is None or not \
(is_vm_included_in_backup(backup_dir, template_vm_on_backup) and \
template_vm_on_backup.is_template()):
if options['use-default-template']:
vms_to_restore[vm.name]['orig-template'] = templatevm_name
vms_to_restore[vm.name]['template'] = host_collection.get_default_template().name
else:
vms_to_restore[vm.name]['missing-template'] = True
vms_to_restore[vm.name]['good-to-go'] = False
if vm.netvm is None:
vms_to_restore[vm.name]['netvm'] = None
else:
netvm_name = vm.netvm.name
vms_to_restore[vm.name]['netvm'] = netvm_name
# Set to None to not confuse QubesVm object from backup
# collection with host collection (further in clone_attrs). Set
# directly _netvm to suppress setter action, especially
# modifying firewall
vm._netvm = None
netvm_on_host = host_collection.get_vm_by_name (netvm_name)
# No netvm on the host?
if not ((netvm_on_host is not None) and netvm_on_host.is_netvm()):
# Maybe the (custom) netvm is in the backup?
netvm_on_backup = backup_collection.get_vm_by_name (netvm_name)
if not ((netvm_on_backup is not None) and netvm_on_backup.is_netvm() and is_vm_included_in_backup(backup_dir, netvm_on_backup)):
if options['use-default-netvm']:
vms_to_restore[vm.name]['netvm'] = host_collection.get_default_netvm().name
vm.uses_default_netvm = True
elif options['use-none-netvm']:
vms_to_restore[vm.name]['netvm'] = None
else:
vms_to_restore[vm.name]['missing-netvm'] = True
vms_to_restore[vm.name]['good-to-go'] = False
if 'good-to-go' not in vms_to_restore[vm.name].keys():
vms_to_restore[vm.name]['good-to-go'] = True
# ...and dom0 home
if options['dom0-home'] and os.path.exists(backup_dir + '/dom0-home'):
vms_to_restore['dom0'] = {}
local_user = grp.getgrnam('qubes').gr_mem[0]
dom0_homes = os.listdir(backup_dir + '/dom0-home')
if len(dom0_homes) > 1:
raise QubesException("More than one dom0 homedir in backup")
vms_to_restore['dom0']['username'] = dom0_homes[0]
if dom0_homes[0] != local_user:
vms_to_restore['dom0']['username-mismatch'] = True
if not options['ignore-dom0-username-mismatch']:
vms_to_restore['dom0']['good-to-go'] = False
if 'good-to-go' not in vms_to_restore['dom0']:
vms_to_restore['dom0']['good-to-go'] = True
return vms_to_restore
def backup_restore_print_summary(restore_info, print_callback = print_stdout):
fields = {
"qid": {"func": "vm.qid"},
"name": {"func": "('[' 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 \
'HVM' if vm.type == 'HVM' else \
vm.type.replace('VM','')"},
"updbl" : {"func": "'Yes' if vm.updateable else ''"},
"template": {"func": "'n/a' if vm.is_template() or vm.template is None else\
vm_info['template']"},
"netvm": {"func": "'n/a' if vm.is_netvm() and not vm.is_proxyvm() else\
('*' if vm.uses_default_netvm else '') +\
vm_info['netvm'] if vm_info['netvm'] is not None else '-'"},
"label" : {"func" : "vm.label.name"},
}
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_info in restore_info.values():
if 'vm' in vm_info.keys():
vm = vm_info['vm']
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_callback("")
print_callback("The following VMs are included in the backup:")
print_callback("")
# Display the header
s = ""
for f in fields_to_display:
fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
s += fmt.format('-')
print_callback(s)
s = ""
for f in fields_to_display:
fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
s += fmt.format(f)
print_callback(s)
s = ""
for f in fields_to_display:
fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
s += fmt.format('-')
print_callback(s)
for vm_info in restore_info.values():
# Skip non-VM here
if not 'vm' in vm_info:
continue
vm = vm_info['vm']
s = ""
for f in fields_to_display:
fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
s += fmt.format(eval(fields[f]["func"]))
if 'excluded' in vm_info and vm_info['excluded']:
s += " <-- Excluded from restore"
elif 'already-exists' in vm_info:
s += " <-- A VM with the same name already exists on the host!"
elif 'missing-template' in vm_info:
s += " <-- No matching template on the host or in the backup found!"
elif 'missing-netvm' in vm_info:
s += " <-- No matching netvm on the host or in the backup found!"
elif 'orig-template' in vm_info:
s += " <-- Original template was '%s'" % (vm_info['orig-template'])
print_callback(s)
if 'dom0' in restore_info.keys():
s = ""
for f in fields_to_display:
fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
if f == "name":
s += fmt.format("Dom0")
elif f == "type":
s += fmt.format("Home")
else:
s += fmt.format("")
if 'username-mismatch' in restore_info['dom0']:
s += " <-- username in backup and dom0 mismatch"
print_callback(s)
def backup_restore_do(backup_dir, restore_info, host_collection = None, print_callback = print_stdout, error_callback = print_stderr):
### Private functions begin
def restore_vm_dir (backup_dir, src_dir, dst_dir):
backup_src_dir = src_dir.replace (system_path["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:
raise QubesException("*** Error while copying file {0} to {1}".format(backup_src_dir, dest_dir))
### Private functions end
lock_obtained = False
if host_collection is None:
host_collection = QubesVmCollection()
host_collection.lock_db_for_writing()
host_collection.load()
lock_obtained = True
# Add VM in right order
for (vm_class_name, vm_class) in sorted(QubesVmClasses.items(),
key=lambda _x: _x[1].load_order):
for vm_info in restore_info.values():
if not vm_info['good-to-go']:
continue
if 'vm' not in vm_info:
continue
vm = vm_info['vm']
if not vm.__class__ == vm_class:
continue
print_callback("-> Restoring {type} {0}...".format(vm.name, type=vm_class_name))
retcode = subprocess.call (["mkdir", "-p", vm.dir_path])
if retcode != 0:
error_callback("*** Cannot create directory: {0}?!".format(dest_dir))
error_callback("Skipping...")
continue
template = None
if vm.template is not None:
template_name = vm_info['template']
template = host_collection.get_vm_by_name(template_name)
new_vm = None
try:
new_vm = host_collection.add_new_vm(vm_class_name, name=vm.name,
conf_file=vm.conf_file,
dir_path=vm.dir_path,
template=template,
installed_by_rpm=False)
restore_vm_dir (backup_dir, vm.dir_path, os.path.dirname(new_vm.dir_path));
new_vm.verify_files()
except Exception as err:
error_callback("ERROR: {0}".format(err))
error_callback("*** Skipping VM: {0}".format(vm.name))
if new_vm:
host_collection.pop(new_vm.qid)
continue
try:
new_vm.clone_attrs(vm)
except Exception as err:
error_callback("ERROR: {0}".format(err))
error_callback("*** Some VM property will not be restored")
try:
new_vm.appmenus_create(verbose=True)
except Exception as err:
error_callback("ERROR during appmenu restore: {0}".format(err))
error_callback("*** VM '{0}' will not have appmenus".format(vm.name))
# Set network dependencies - only non-default netvm setting
for vm_info in restore_info.values():
if not vm_info['good-to-go']:
continue
if 'vm' not in vm_info:
continue
vm = vm_info['vm']
host_vm = host_collection.get_vm_by_name(vm.name)
if host_vm is None:
# Failed/skipped VM
continue
if not vm.uses_default_netvm:
host_vm.netvm = host_collection.get_vm_by_name (vm_info['netvm']) if vm_info['netvm'] is not None else None
host_collection.save()
if lock_obtained:
host_collection.unlock_db()
# ... and dom0 home as last step
if 'dom0' in restore_info.keys() and restore_info['dom0']['good-to-go']:
backup_info = restore_info['dom0']
local_user = grp.getgrnam('qubes').gr_mem[0]
home_dir = pwd.getpwnam(local_user).pw_dir
backup_dom0_home_dir = backup_dir + '/dom0-home/' + backup_info['username']
restore_home_backupdir = "home-pre-restore-{0}".format (time.strftime("%Y-%m-%d-%H%M%S"))
print_callback("-> Restoring home of user '{0}'...".format(local_user))
print_callback("--> Existing files/dirs backed up in '{0}' dir".format(restore_home_backupdir))
os.mkdir(home_dir + '/' + restore_home_backupdir)
for f in os.listdir(backup_dom0_home_dir):
home_file = home_dir + '/' + f
if os.path.exists(home_file):
os.rename(home_file, home_dir + '/' + restore_home_backupdir + '/' + f)
retcode = subprocess.call (["cp", "-nrp", backup_dom0_home_dir + '/' + f, home_file])
if retcode != 0:
error_callback("*** Error while copying file {0} to {1}".format(backup_dom0_home_dir + '/' + f, home_file))
retcode = subprocess.call(['sudo', 'chown', '-R', local_user, home_dir])
if retcode != 0:
error_callback("*** Error while setting home directory owner")
# vim:sw=4:et: # vim:sw=4:et:

View File

@ -22,10 +22,12 @@
from qubes.qubes import QubesVmCollection from qubes.qubes import QubesVmCollection
from qubes.qubes import QubesException from qubes.qubes import QubesException
from qubes.qubesutils import backup_prepare, backup_do from qubes.backup import backup_prepare, backup_do
from qubes.qubesutils import size_to_human
from optparse import OptionParser from optparse import OptionParser
import os import os
import sys import sys
import getpass
def print_progress(progress): def print_progress(progress):
print >> sys.stderr, "\r-> Backing up files: {0}%...".format (progress), print >> sys.stderr, "\r-> Backing up files: {0}%...".format (progress),
@ -38,6 +40,10 @@ def main():
help="Exclude the specified VM from backup (might be repeated)") help="Exclude the specified VM from backup (might be repeated)")
parser.add_option ("--force-root", action="store_true", dest="force_root", default=False, parser.add_option ("--force-root", action="store_true", dest="force_root", default=False,
help="Force to run, even with root privileges") help="Force to run, even with root privileges")
parser.add_option ("-d", "--dest-vm", action="store", dest="appvm",
help="The AppVM to send backups to")
parser.add_option ("-e", "--encrypt", action="store_true", dest="encrypt", default=False,
help="Encrypts the backup")
(options, args) = parser.parse_args () (options, args) = parser.parse_args ()
@ -66,17 +72,57 @@ def main():
files_to_backup = None files_to_backup = None
try: try:
files_to_backup = backup_prepare(base_backup_dir, vms_list=vms, exclude_list=options.exclude_list) files_to_backup = backup_prepare(
vms_list=vms,
exclude_list=options.exclude_list,
hide_vm_names=options.encrypt)
except QubesException as e: except QubesException as e:
print >>sys.stderr, "ERROR: %s" % str(e) print >>sys.stderr, "ERROR: %s" % str(e)
exit(1) exit(1)
total_backup_sz = reduce(lambda size, file: size+file["size"],
files_to_backup, 0)
if not options.appvm:
appvm = None
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 >>sys.stderr, "ERROR: Not enough space available on the backup filesystem!"
exit(1)
print "-> Available space: {0}".format(size_to_human(backup_fs_free_sz))
else:
appvm = qvm_collection.get_vm_by_name(options.appvm)
if appvm is None:
print >>sys.stderr, "ERROR: VM {0} does not exist".format(options.appvm)
exit(1)
stat = os.statvfs('/var/tmp')
backup_fs_free_sz = stat.f_bsize * stat.f_bavail
print
if (backup_fs_free_sz < 1000000000):
print >>sys.stderr, "ERROR: Not enough space available " \
"on the local filesystem (needs 1GB for temporary files)!"
exit(1)
prompt = raw_input ("Do you want to proceed? [y/N] ") prompt = raw_input ("Do you want to proceed? [y/N] ")
if not (prompt == "y" or prompt == "Y"): if not (prompt == "y" or prompt == "Y"):
exit (0) exit (0)
passphrase = getpass.getpass("Please enter the pass phrase that will be used to encrypt/verify the backup: ")
passphrase2 = getpass.getpass("Enter again for verification: ")
if passphrase != passphrase2:
print >>sys.stderr, "ERROR: Password mismatch"
exit(1)
try: try:
backup_do(base_backup_dir, files_to_backup, progress_callback=print_progress) backup_do(base_backup_dir, files_to_backup, passphrase,
progress_callback=print_progress,
encrypt=options.encrypt,
appvm=appvm)
except QubesException as e: except QubesException as e:
print >>sys.stderr, "ERROR: %s" % str(e) print >>sys.stderr, "ERROR: %s" % str(e)
exit(1) exit(1)

View File

@ -22,13 +22,15 @@
from qubes.qubes import QubesVmCollection from qubes.qubes import QubesVmCollection
from qubes.qubes import QubesException from qubes.qubes import QubesException
from qubes.qubesutils import backup_restore_prepare from qubes.backup import backup_restore_header
from qubes.qubesutils import backup_restore_print_summary from qubes.backup import backup_restore_prepare
from qubes.qubesutils import backup_restore_do from qubes.backup import backup_restore_print_summary
from qubes.backup import backup_restore_do
from optparse import OptionParser from optparse import OptionParser
import os import os
import sys import sys
import getpass
def main(): def main():
usage = "usage: %prog [options] <backup-dir>" usage = "usage: %prog [options] <backup-dir>"
@ -58,6 +60,12 @@ def main():
parser.add_option ("--ignore-username-mismatch", action="store_true", dest="ignore_username_mismatch", default=False, parser.add_option ("--ignore-username-mismatch", action="store_true", dest="ignore_username_mismatch", default=False,
help="Ignore dom0 username mismatch while restoring homedir") help="Ignore dom0 username mismatch while restoring homedir")
parser.add_option ("-d", "--dest-vm", action="store", dest="appvm",
help="The AppVM to send backups to")
parser.add_option ("-e", "--encrypted", action="store_true", dest="decrypt", default=False,
help="The backup is encrypted")
(options, args) = parser.parse_args () (options, args) = parser.parse_args ()
if (len (args) != 1): if (len (args) != 1):
@ -66,9 +74,9 @@ def main():
backup_dir = args[0] backup_dir = args[0]
if not os.path.exists (backup_dir): #if not os.path.exists (backup_dir):
print >> sys.stderr, "The backup directory doesn't exist!" # print >> sys.stderr, "The backup directory doesn't exist!"
exit(1) # exit(1)
host_collection = QubesVmCollection() host_collection = QubesVmCollection()
host_collection.lock_db_for_writing() host_collection.lock_db_for_writing()
@ -87,9 +95,28 @@ def main():
if options.exclude: if options.exclude:
restore_options['exclude'] = options.exclude restore_options['exclude'] = options.exclude
appvm = None
if options.appvm is not None:
appvm = host_collection.get_vm_by_name(options.appvm)
if appvm is None:
print >>sys.stderr, "ERROR: VM {0} does not exist".format(options.appvm)
exit(1)
passphrase = getpass.getpass("Please enter the pass phrase that will be used to decrypt/verify the backup: ")
print >> sys.stderr, "Checking backup content..."
restore_tmpdir,qubes_xml = backup_restore_header(backup_dir, passphrase, encrypted=options.decrypt, appvm=appvm)
restore_info = None restore_info = None
try: try:
restore_info = backup_restore_prepare(backup_dir, options=restore_options, host_collection=host_collection) restore_info = backup_restore_prepare(
backup_dir,
os.path.join(restore_tmpdir, qubes_xml),
passphrase,
options=restore_options,
host_collection=host_collection,
encrypt=options.decrypt,
appvm=appvm)
except QubesException as e: except QubesException as e:
print >> sys.stderr, "ERROR: %s" % str(e) print >> sys.stderr, "ERROR: %s" % str(e)
exit(1) exit(1)
@ -113,8 +140,6 @@ def main():
if 'username-mismatch' in vm_info.keys(): if 'username-mismatch' in vm_info.keys():
dom0_username_mismatch = True dom0_username_mismatch = True
print
if os.geteuid() == 0: if os.geteuid() == 0:
print >> sys.stderr, "*** Running this tool as root is strongly discouraged, this will lead you in permissions problems." print >> sys.stderr, "*** Running this tool as root is strongly discouraged, this will lead you in permissions problems."
if options.force_root: if options.force_root:
@ -135,7 +160,7 @@ def main():
print "The above VMs will be copied and added to your system." print "The above VMs will be copied and added to your system."
print "Exisiting VMs will not be removed." print "Exisiting VMs will not be removed."
if there_are_missing_templates: if there_are_missing_templates:
print >> sys.stderr, "*** One or more template VM is missing on the host! ***" print >> sys.stderr, "*** One or more template VM is missing on the host! ***"
if not (options.skip_broken or options.ignore_missing): if not (options.skip_broken or options.ignore_missing):
@ -179,7 +204,14 @@ def main():
if not (prompt == "y" or prompt == "Y"): if not (prompt == "y" or prompt == "Y"):
exit (0) exit (0)
backup_restore_do(backup_dir, restore_info, host_collection=host_collection)
backup_restore_do(backup_dir,
restore_tmpdir,
passphrase,
restore_info,
host_collection=host_collection,
encrypted=options.decrypt,
appvm=appvm)
host_collection.unlock_db() host_collection.unlock_db()

View File

@ -110,6 +110,8 @@ cp core/qubesutils.py $RPM_BUILD_ROOT%{python_sitearch}/qubes
cp core/qubesutils.py[co] $RPM_BUILD_ROOT%{python_sitearch}/qubes cp core/qubesutils.py[co] $RPM_BUILD_ROOT%{python_sitearch}/qubes
cp core/guihelpers.py $RPM_BUILD_ROOT%{python_sitearch}/qubes cp core/guihelpers.py $RPM_BUILD_ROOT%{python_sitearch}/qubes
cp core/guihelpers.py[co] $RPM_BUILD_ROOT%{python_sitearch}/qubes cp core/guihelpers.py[co] $RPM_BUILD_ROOT%{python_sitearch}/qubes
cp core/backup.py $RPM_BUILD_ROOT%{python_sitearch}/qubes
cp core/backup.py[co] $RPM_BUILD_ROOT%{python_sitearch}/qubes
cp core/__init__.py $RPM_BUILD_ROOT%{python_sitearch}/qubes cp core/__init__.py $RPM_BUILD_ROOT%{python_sitearch}/qubes
cp core/__init__.py[co] $RPM_BUILD_ROOT%{python_sitearch}/qubes cp core/__init__.py[co] $RPM_BUILD_ROOT%{python_sitearch}/qubes
cp qmemman/qmemman*py $RPM_BUILD_ROOT%{python_sitearch}/qubes cp qmemman/qmemman*py $RPM_BUILD_ROOT%{python_sitearch}/qubes
@ -273,6 +275,9 @@ fi
%{python_sitearch}/qubes/guihelpers.py %{python_sitearch}/qubes/guihelpers.py
%{python_sitearch}/qubes/guihelpers.pyc %{python_sitearch}/qubes/guihelpers.pyc
%{python_sitearch}/qubes/guihelpers.pyo %{python_sitearch}/qubes/guihelpers.pyo
%{python_sitearch}/qubes/backup.py
%{python_sitearch}/qubes/backup.pyc
%{python_sitearch}/qubes/backup.pyo
%{python_sitearch}/qubes/__init__.py %{python_sitearch}/qubes/__init__.py
%{python_sitearch}/qubes/__init__.pyc %{python_sitearch}/qubes/__init__.pyc
%{python_sitearch}/qubes/__init__.pyo %{python_sitearch}/qubes/__init__.pyo