Merge branch 'new-backups'
Conflicts: core-modules/000QubesVm.py
This commit is contained in:
commit
27f6f0e64e
@ -123,6 +123,9 @@ class QubesVm(object):
|
||||
"default_user": { "default": "user" },
|
||||
"qrexec_timeout": { "default": 60 },
|
||||
"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
|
||||
"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' },
|
||||
@ -140,7 +143,9 @@ class QubesVm(object):
|
||||
'uses_default_kernel', 'kernel', 'uses_default_kernelopts',\
|
||||
'kernelopts', 'services', 'installed_by_rpm',\
|
||||
'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
|
||||
# Simple paths
|
||||
for prop in ['conf_file', 'root_img', 'volatile_img', 'private_img']:
|
||||
|
@ -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 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):
|
||||
super(QubesDom0NetVm, self).__init__(qid=0, name="dom0", netid=0,
|
||||
super(QubesAdminVm, self).__init__(qid=0, name="dom0", netid=0,
|
||||
dir_path=None,
|
||||
private_img = None,
|
||||
template = None,
|
||||
@ -35,6 +39,10 @@ class QubesDom0NetVm(QubesNetVm):
|
||||
**kwargs)
|
||||
self.xid = 0
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return "AdminVM"
|
||||
|
||||
def is_running(self):
|
||||
return True
|
||||
|
||||
@ -83,10 +91,7 @@ class QubesDom0NetVm(QubesNetVm):
|
||||
domains = xc.domain_getinfo(0, 1)
|
||||
return domains[0]
|
||||
|
||||
def create_xml_element(self):
|
||||
return None
|
||||
|
||||
def verify_files(self):
|
||||
return True
|
||||
|
||||
register_qubes_vm_class(QubesDom0NetVm)
|
||||
register_qubes_vm_class(QubesAdminVm)
|
1369
core/backup.py
Normal file
1369
core/backup.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -609,10 +609,6 @@ class QubesVmCollection(dict):
|
||||
def load(self):
|
||||
self.clear()
|
||||
|
||||
dom0vm = QubesDom0NetVm (collection=self)
|
||||
self[dom0vm.qid] = dom0vm
|
||||
self.default_netvm_qid = 0
|
||||
|
||||
try:
|
||||
tree = lxml.etree.parse(self.qubes_store_file)
|
||||
except (EnvironmentError,
|
||||
@ -665,6 +661,12 @@ class QubesVmCollection(dict):
|
||||
# using 123/udp port)
|
||||
if self.clockvm_qid is not None:
|
||||
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
|
||||
|
||||
def pop(self, qid):
|
||||
|
@ -28,6 +28,7 @@ import sys
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import grp,pwd
|
||||
from datetime import datetime
|
||||
@ -754,622 +755,4 @@ class QubesWatch(object):
|
||||
while True:
|
||||
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:
|
||||
|
@ -22,10 +22,12 @@
|
||||
|
||||
from qubes.qubes import QubesVmCollection
|
||||
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
|
||||
import os
|
||||
import sys
|
||||
import getpass
|
||||
|
||||
def print_progress(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)")
|
||||
parser.add_option ("--force-root", action="store_true", dest="force_root", default=False,
|
||||
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 ()
|
||||
|
||||
@ -66,17 +72,57 @@ def main():
|
||||
|
||||
files_to_backup = None
|
||||
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:
|
||||
print >>sys.stderr, "ERROR: %s" % str(e)
|
||||
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] ")
|
||||
if not (prompt == "y" or prompt == "Y"):
|
||||
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:
|
||||
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:
|
||||
print >>sys.stderr, "ERROR: %s" % str(e)
|
||||
exit(1)
|
||||
|
@ -22,13 +22,15 @@
|
||||
|
||||
from qubes.qubes import QubesVmCollection
|
||||
from qubes.qubes import QubesException
|
||||
from qubes.qubesutils import backup_restore_prepare
|
||||
from qubes.qubesutils import backup_restore_print_summary
|
||||
from qubes.qubesutils import backup_restore_do
|
||||
from qubes.backup import backup_restore_header
|
||||
from qubes.backup import backup_restore_prepare
|
||||
from qubes.backup import backup_restore_print_summary
|
||||
from qubes.backup import backup_restore_do
|
||||
from optparse import OptionParser
|
||||
|
||||
import os
|
||||
import sys
|
||||
import getpass
|
||||
|
||||
def main():
|
||||
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,
|
||||
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 ()
|
||||
|
||||
if (len (args) != 1):
|
||||
@ -66,9 +74,9 @@ def main():
|
||||
|
||||
backup_dir = args[0]
|
||||
|
||||
if not os.path.exists (backup_dir):
|
||||
print >> sys.stderr, "The backup directory doesn't exist!"
|
||||
exit(1)
|
||||
#if not os.path.exists (backup_dir):
|
||||
# print >> sys.stderr, "The backup directory doesn't exist!"
|
||||
# exit(1)
|
||||
|
||||
host_collection = QubesVmCollection()
|
||||
host_collection.lock_db_for_writing()
|
||||
@ -87,9 +95,28 @@ def main():
|
||||
if 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
|
||||
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:
|
||||
print >> sys.stderr, "ERROR: %s" % str(e)
|
||||
exit(1)
|
||||
@ -113,8 +140,6 @@ def main():
|
||||
if 'username-mismatch' in vm_info.keys():
|
||||
dom0_username_mismatch = True
|
||||
|
||||
print
|
||||
|
||||
if os.geteuid() == 0:
|
||||
print >> sys.stderr, "*** Running this tool as root is strongly discouraged, this will lead you in permissions problems."
|
||||
if options.force_root:
|
||||
@ -179,7 +204,14 @@ def main():
|
||||
if not (prompt == "y" or prompt == "Y"):
|
||||
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()
|
||||
|
||||
|
@ -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/guihelpers.py $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[co] $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.pyc
|
||||
%{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__.pyc
|
||||
%{python_sitearch}/qubes/__init__.pyo
|
||||
|
Loading…
Reference in New Issue
Block a user