backup.py 88 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # The Qubes OS Project, http://www.qubes-os.org
  5. #
  6. # Copyright (C) 2013-2015 Marek Marczykowski-Górecki
  7. # <marmarek@invisiblethingslab.com>
  8. # Copyright (C) 2013 Olivier Médoc <o_medoc@yahoo.fr>
  9. #
  10. # This program is free software; you can redistribute it and/or
  11. # modify it under the terms of the GNU General Public License
  12. # as published by the Free Software Foundation; either version 2
  13. # of the License, or (at your option) any later version.
  14. #
  15. # This program is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. # GNU General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU General Public License
  21. # along with this program. If not, see <http://www.gnu.org/licenses/>
  22. #
  23. #
  24. from __future__ import unicode_literals
  25. import itertools
  26. import logging
  27. from qubes.utils import size_to_human
  28. import sys
  29. import os
  30. import fcntl
  31. import subprocess
  32. import re
  33. import shutil
  34. import tempfile
  35. import time
  36. import grp
  37. import pwd
  38. import errno
  39. import datetime
  40. from multiprocessing import Queue, Process
  41. import qubes
  42. import qubes.core2migration
  43. import qubes.storage
  44. import qubes.storage.file
  45. QUEUE_ERROR = "ERROR"
  46. QUEUE_FINISHED = "FINISHED"
  47. HEADER_FILENAME = 'backup-header'
  48. DEFAULT_CRYPTO_ALGORITHM = 'aes-256-cbc'
  49. DEFAULT_HMAC_ALGORITHM = 'SHA512'
  50. DEFAULT_COMPRESSION_FILTER = 'gzip'
  51. CURRENT_BACKUP_FORMAT_VERSION = '4'
  52. # Maximum size of error message get from process stderr (including VM process)
  53. MAX_STDERR_BYTES = 1024
  54. # header + qubes.xml max size
  55. HEADER_QUBES_XML_MAX_SIZE = 1024 * 1024
  56. BLKSIZE = 512
  57. _re_alphanum = re.compile(r'^[A-Za-z0-9-]*$')
  58. class BackupCanceledError(qubes.exc.QubesException):
  59. def __init__(self, msg, tmpdir=None):
  60. super(BackupCanceledError, self).__init__(msg)
  61. self.tmpdir = tmpdir
  62. class BackupHeader(object):
  63. header_keys = {
  64. 'version': 'version',
  65. 'encrypted': 'encrypted',
  66. 'compressed': 'compressed',
  67. 'compression-filter': 'compression_filter',
  68. 'crypto-algorithm': 'crypto_algorithm',
  69. 'hmac-algorithm': 'hmac_algorithm',
  70. }
  71. bool_options = ['encrypted', 'compressed']
  72. int_options = ['version']
  73. def __init__(self,
  74. header_data=None,
  75. version=None,
  76. encrypted=None,
  77. compressed=None,
  78. compression_filter=None,
  79. hmac_algorithm=None,
  80. crypto_algorithm=None):
  81. # repeat the list to help code completion...
  82. self.version = version
  83. self.encrypted = encrypted
  84. self.compressed = compressed
  85. # Options introduced in backup format 3+, which always have a header,
  86. # so no need for fallback in function parameter
  87. self.compression_filter = compression_filter
  88. self.hmac_algorithm = hmac_algorithm
  89. self.crypto_algorithm = crypto_algorithm
  90. if header_data is not None:
  91. self.load(header_data)
  92. def load(self, untrusted_header_text):
  93. """Parse backup header file.
  94. :param untrusted_header_text: header content
  95. :type untrusted_header_text: basestring
  96. .. warning::
  97. This function may be exposed to not yet verified header,
  98. so is security critical.
  99. """
  100. try:
  101. untrusted_header_text = untrusted_header_text.decode('ascii')
  102. except UnicodeDecodeError:
  103. raise qubes.exc.QubesException(
  104. "Non-ASCII characters in backup header")
  105. for untrusted_line in untrusted_header_text.splitlines():
  106. if untrusted_line.count('=') != 1:
  107. raise qubes.exc.QubesException("Invalid backup header")
  108. key, value = untrusted_line.strip().split('=', 1)
  109. if not _re_alphanum.match(key):
  110. raise qubes.exc.QubesException("Invalid backup header (key)")
  111. if key not in self.header_keys.keys():
  112. # Ignoring unknown option
  113. continue
  114. if not _re_alphanum.match(value):
  115. raise qubes.exc.QubesException("Invalid backup header (value)")
  116. if getattr(self, self.header_keys[key]) is not None:
  117. raise qubes.exc.QubesException(
  118. "Duplicated header line: {}".format(key))
  119. if key in self.bool_options:
  120. value = value.lower() in ["1", "true", "yes"]
  121. elif key in self.int_options:
  122. value = int(value)
  123. setattr(self, self.header_keys[key], value)
  124. self.validate()
  125. def validate(self):
  126. if self.version == 1:
  127. # header not really present
  128. pass
  129. elif self.version in [2, 3, 4]:
  130. expected_attrs = ['version', 'encrypted', 'compressed',
  131. 'hmac_algorithm']
  132. if self.encrypted:
  133. expected_attrs += ['crypto_algorithm']
  134. if self.version >= 3 and self.compressed:
  135. expected_attrs += ['compression_filter']
  136. for key in expected_attrs:
  137. if getattr(self, key) is None:
  138. raise qubes.exc.QubesException(
  139. "Backup header lack '{}' info".format(key))
  140. else:
  141. raise qubes.exc.QubesException(
  142. "Unsupported backup version {}".format(self.version))
  143. def save(self, filename):
  144. with open(filename, "w") as f:
  145. # make sure 'version' is the first key
  146. f.write('version={}\n'.format(self.version))
  147. for key, attr in self.header_keys.iteritems():
  148. if key == 'version':
  149. continue
  150. if getattr(self, attr) is None:
  151. continue
  152. f.write("{!s}={!s}\n".format(key, getattr(self, attr)))
  153. class SendWorker(Process):
  154. def __init__(self, queue, base_dir, backup_stdout):
  155. super(SendWorker, self).__init__()
  156. self.queue = queue
  157. self.base_dir = base_dir
  158. self.backup_stdout = backup_stdout
  159. self.log = logging.getLogger('qubes.backup')
  160. def run(self):
  161. self.log.debug("Started sending thread")
  162. self.log.debug("Moving to temporary dir".format(self.base_dir))
  163. os.chdir(self.base_dir)
  164. for filename in iter(self.queue.get, None):
  165. if filename in (QUEUE_FINISHED, QUEUE_ERROR):
  166. break
  167. self.log.debug("Sending file {}".format(filename))
  168. # This tar used for sending data out need to be as simple, as
  169. # simple, as featureless as possible. It will not be
  170. # verified before untaring.
  171. tar_final_cmd = ["tar", "-cO", "--posix",
  172. "-C", self.base_dir, filename]
  173. final_proc = subprocess.Popen(tar_final_cmd,
  174. stdin=subprocess.PIPE,
  175. stdout=self.backup_stdout)
  176. if final_proc.wait() >= 2:
  177. if self.queue.full():
  178. # if queue is already full, remove some entry to wake up
  179. # main thread, so it will be able to notice error
  180. self.queue.get()
  181. # handle only exit code 2 (tar fatal error) or
  182. # greater (call failed?)
  183. raise qubes.exc.QubesException(
  184. "ERROR: Failed to write the backup, out of disk space? "
  185. "Check console output or ~/.xsession-errors for details.")
  186. # Delete the file as we don't need it anymore
  187. self.log.debug("Removing file {}".format(filename))
  188. os.remove(filename)
  189. self.log.debug("Finished sending thread")
  190. class Backup(object):
  191. class FileToBackup(object):
  192. def __init__(self, file_path, subdir=None):
  193. sz = qubes.storage.file.get_disk_usage(file_path)
  194. if subdir is None:
  195. abs_file_path = os.path.abspath(file_path)
  196. abs_base_dir = os.path.abspath(
  197. qubes.config.system_path["qubes_base_dir"]) + '/'
  198. abs_file_dir = os.path.dirname(abs_file_path) + '/'
  199. (nothing, directory, subdir) = abs_file_dir.partition(abs_base_dir)
  200. assert nothing == ""
  201. assert directory == abs_base_dir
  202. else:
  203. if len(subdir) > 0 and not subdir.endswith('/'):
  204. subdir += '/'
  205. self.path = file_path
  206. self.size = sz
  207. self.subdir = subdir
  208. class VMToBackup(object):
  209. def __init__(self, vm, files, subdir):
  210. self.vm = vm
  211. self.files = files
  212. self.subdir = subdir
  213. @property
  214. def size(self):
  215. return reduce(lambda x, y: x + y.size, self.files, 0)
  216. def __init__(self, app, vms_list=None, exclude_list=None, **kwargs):
  217. """
  218. If vms = None, include all (sensible) VMs;
  219. exclude_list is always applied
  220. """
  221. super(Backup, self).__init__()
  222. #: progress of the backup - bytes handled of the current VM
  223. self.chunk_size = 100 * 1024 * 1024
  224. self._current_vm_bytes = 0
  225. #: progress of the backup - bytes handled of finished VMs
  226. self._done_vms_bytes = 0
  227. #: total backup size (set by :py:meth:`get_files_to_backup`)
  228. self.total_backup_bytes = 0
  229. #: application object
  230. self.app = app
  231. #: directory for temporary files - set after creating the directory
  232. self.tmpdir = None
  233. # Backup settings - defaults
  234. #: should the backup be encrypted?
  235. self.encrypted = True
  236. #: should the backup be compressed?
  237. self.compressed = True
  238. #: what passphrase should be used to intergrity protect (and encrypt)
  239. #: the backup; required
  240. self.passphrase = None
  241. #: custom hmac algorithm
  242. self.hmac_algorithm = DEFAULT_HMAC_ALGORITHM
  243. #: custom encryption algorithm
  244. self.crypto_algorithm = DEFAULT_CRYPTO_ALGORITHM
  245. #: custom compression filter; a program which process stdin to stdout
  246. self.compression_filter = DEFAULT_COMPRESSION_FILTER
  247. #: VM to which backup should be sent (if any)
  248. self.target_vm = None
  249. #: directory to save backup in (either in dom0 or target VM,
  250. #: depending on :py:attr:`target_vm`
  251. self.target_dir = None
  252. #: callback for progress reporting. Will be called with one argument
  253. #: - progress in percents
  254. self.progress_callback = None
  255. for key, value in kwargs.iteritems():
  256. if hasattr(self, key):
  257. setattr(self, key, value)
  258. else:
  259. raise AttributeError(key)
  260. #: whether backup was canceled
  261. self.canceled = False
  262. #: list of PIDs to kill on backup cancel
  263. self.processes_to_kill_on_cancel = []
  264. self.log = logging.getLogger('qubes.backup')
  265. self.compression_filter = DEFAULT_COMPRESSION_FILTER
  266. if exclude_list is None:
  267. exclude_list = []
  268. if vms_list is None:
  269. vms_list = [vm for vm in app.domains if vm.include_in_backups]
  270. # Apply exclude list
  271. self.vms_for_backup = [vm for vm in vms_list
  272. if vm.name not in exclude_list]
  273. self._files_to_backup = self.get_files_to_backup()
  274. def __del__(self):
  275. if self.tmpdir and os.path.exists(self.tmpdir):
  276. shutil.rmtree(self.tmpdir)
  277. def cancel(self):
  278. """Cancel running backup operation. Can be called from another thread.
  279. """
  280. self.canceled = True
  281. for proc in self.processes_to_kill_on_cancel:
  282. try:
  283. proc.terminate()
  284. except OSError:
  285. pass
  286. def get_files_to_backup(self):
  287. files_to_backup = {}
  288. for vm in self.vms_for_backup:
  289. if vm.qid == 0:
  290. # handle dom0 later
  291. continue
  292. if self.encrypted:
  293. subdir = 'vm%d/' % vm.qid
  294. else:
  295. subdir = None
  296. vm_files = []
  297. if vm.private_img is not None:
  298. vm_files.append(self.FileToBackup(vm.private_img, subdir))
  299. vm_files.append(self.FileToBackup(vm.icon_path, subdir))
  300. vm_files.extend(self.FileToBackup(i, subdir)
  301. for i in vm.fire_event('backup-get-files'))
  302. # TODO: drop after merging firewall.xml into qubes.xml
  303. firewall_conf = os.path.join(vm.dir_path, vm.firewall_conf)
  304. if os.path.exists(firewall_conf):
  305. vm_files.append(self.FileToBackup(firewall_conf, subdir))
  306. if vm.updateable:
  307. vm_files.append(self.FileToBackup(vm.root_img, subdir))
  308. files_to_backup[vm.qid] = self.VMToBackup(vm, vm_files, subdir)
  309. # Dom0 user home
  310. if 0 in [vm.qid for vm in self.vms_for_backup]:
  311. local_user = grp.getgrnam('qubes').gr_mem[0]
  312. home_dir = pwd.getpwnam(local_user).pw_dir
  313. # Home dir should have only user-owned files, so fix it now
  314. # to prevent permissions problems - some root-owned files can
  315. # left after 'sudo bash' and similar commands
  316. subprocess.check_call(['sudo', 'chown', '-R', local_user, home_dir])
  317. home_to_backup = [
  318. self.FileToBackup(home_dir, 'dom0-home/')]
  319. vm_files = home_to_backup
  320. files_to_backup[0] = self.VMToBackup(self.app.domains[0],
  321. vm_files,
  322. os.path.join('dom0-home', os.path.basename(home_dir)))
  323. self.total_backup_bytes = reduce(
  324. lambda x, y: x + y.size, files_to_backup.values(), 0)
  325. return files_to_backup
  326. def get_backup_summary(self):
  327. summary = ""
  328. fields_to_display = [
  329. {"name": "VM", "width": 16},
  330. {"name": "type", "width": 12},
  331. {"name": "size", "width": 12}
  332. ]
  333. # Display the header
  334. for f in fields_to_display:
  335. fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
  336. summary += fmt.format('-')
  337. summary += "\n"
  338. for f in fields_to_display:
  339. fmt = "{{0:>{0}}} |".format(f["width"] + 1)
  340. summary += fmt.format(f["name"])
  341. summary += "\n"
  342. for f in fields_to_display:
  343. fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
  344. summary += fmt.format('-')
  345. summary += "\n"
  346. files_to_backup = self._files_to_backup
  347. for qid, vm_info in files_to_backup.iteritems():
  348. s = ""
  349. fmt = "{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
  350. s += fmt.format(vm_info['vm'].name)
  351. fmt = "{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
  352. if qid == 0:
  353. s += fmt.format("User home")
  354. elif vm_info['vm'].is_template():
  355. s += fmt.format("Template VM")
  356. else:
  357. s += fmt.format("VM" + (" + Sys" if vm_info['vm'].updateable
  358. else ""))
  359. vm_size = vm_info['size']
  360. fmt = "{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
  361. s += fmt.format(size_to_human(vm_size))
  362. if qid != 0 and vm_info['vm'].is_running():
  363. s += " <-- The VM is running, please shut it down before proceeding " \
  364. "with the backup!"
  365. summary += s + "\n"
  366. for f in fields_to_display:
  367. fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
  368. summary += fmt.format('-')
  369. summary += "\n"
  370. fmt = "{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
  371. summary += fmt.format("Total size:")
  372. fmt = "{{0:>{0}}} |".format(
  373. fields_to_display[1]["width"] + 1 + 2 + fields_to_display[2][
  374. "width"] + 1)
  375. summary += fmt.format(size_to_human(self.total_backup_bytes))
  376. summary += "\n"
  377. for f in fields_to_display:
  378. fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
  379. summary += fmt.format('-')
  380. summary += "\n"
  381. vms_not_for_backup = [vm.name for vm in self.app.domains
  382. if vm not in self.vms_for_backup]
  383. summary += "VMs not selected for backup:\n - " + "\n - ".join(
  384. sorted(vms_not_for_backup))
  385. return summary
  386. def prepare_backup_header(self):
  387. header_file_path = os.path.join(self.tmpdir, HEADER_FILENAME)
  388. backup_header = BackupHeader(
  389. version=CURRENT_BACKUP_FORMAT_VERSION,
  390. hmac_algorithm=self.hmac_algorithm,
  391. crypto_algorithm=self.crypto_algorithm,
  392. encrypted=self.encrypted,
  393. compressed=self.compressed,
  394. compression_filter=self.compression_filter,
  395. )
  396. backup_header.save(header_file_path)
  397. hmac = subprocess.Popen(
  398. ["openssl", "dgst", "-" + self.hmac_algorithm,
  399. "-hmac", self.passphrase],
  400. stdin=open(header_file_path, "r"),
  401. stdout=open(header_file_path + ".hmac", "w"))
  402. if hmac.wait() != 0:
  403. raise qubes.exc.QubesException(
  404. "Failed to compute hmac of header file")
  405. return HEADER_FILENAME, HEADER_FILENAME + ".hmac"
  406. @staticmethod
  407. def _queue_put_with_check(proc, vmproc, queue, element):
  408. if queue.full():
  409. if not proc.is_alive():
  410. if vmproc:
  411. message = ("Failed to write the backup, VM output:\n" +
  412. vmproc.stderr.read())
  413. else:
  414. message = "Failed to write the backup. Out of disk space?"
  415. raise qubes.exc.QubesException(message)
  416. queue.put(element)
  417. def _send_progress_update(self):
  418. if callable(self.progress_callback):
  419. progress = (
  420. 100 * (self._done_vms_bytes + self._current_vm_bytes) /
  421. self.total_backup_bytes)
  422. self.progress_callback(progress)
  423. def _add_vm_progress(self, bytes_done):
  424. self._current_vm_bytes += bytes_done
  425. self._send_progress_update()
  426. def backup_do(self):
  427. if self.passphrase is None:
  428. raise qubes.exc.QubesException("No passphrase set")
  429. qubes_xml = self.app.store
  430. self.tmpdir = tempfile.mkdtemp()
  431. shutil.copy(qubes_xml, os.path.join(self.tmpdir, 'qubes.xml'))
  432. qubes_xml = os.path.join(self.tmpdir, 'qubes.xml')
  433. backup_app = qubes.Qubes(qubes_xml)
  434. files_to_backup = self._files_to_backup
  435. # make sure backup_content isn't set initially
  436. for vm in backup_app.domains:
  437. vm.features['backup-content'] = False
  438. for qid, vm_info in files_to_backup.iteritems():
  439. if qid != 0 and vm_info.vm.is_running():
  440. raise qubes.exc.QubesVMNotHaltedError(vm_info.vm)
  441. # VM is included in the backup
  442. backup_app.domains[qid].features['backup-content'] = True
  443. backup_app.domains[qid].features['backup-path'] = vm_info.subdir
  444. backup_app.domains[qid].features['backup-size'] = vm_info.size
  445. backup_app.save()
  446. passphrase = self.passphrase.encode('utf-8')
  447. vmproc = None
  448. tar_sparse = None
  449. if self.target_vm is not None:
  450. # Prepare the backup target (Qubes service call)
  451. # If APPVM, STDOUT is a PIPE
  452. vmproc = self.target_vm.run_service('qubes.Backup',
  453. passio_popen=True, passio_stderr=True)
  454. vmproc.stdin.write(self.target_dir.
  455. replace("\r", "").replace("\n", "") + "\n")
  456. backup_stdout = vmproc.stdin
  457. self.processes_to_kill_on_cancel.append(vmproc)
  458. else:
  459. # Prepare the backup target (local file)
  460. if os.path.isdir(self.target_dir):
  461. backup_target = self.target_dir + "/qubes-{0}". \
  462. format(time.strftime("%Y-%m-%dT%H%M%S"))
  463. else:
  464. backup_target = self.target_dir
  465. # Create the target directory
  466. if not os.path.exists(os.path.dirname(self.target_dir)):
  467. raise qubes.exc.QubesException(
  468. "ERROR: the backup directory for {0} does not exists".
  469. format(self.target_dir))
  470. # If not APPVM, STDOUT is a local file
  471. backup_stdout = open(backup_target, 'wb')
  472. # Tar with tape length does not deals well with stdout
  473. # (close stdout between two tapes)
  474. # For this reason, we will use named pipes instead
  475. self.log.debug("Working in {}".format(self.tmpdir))
  476. backup_pipe = os.path.join(self.tmpdir, "backup_pipe")
  477. self.log.debug("Creating pipe in: {}".format(backup_pipe))
  478. os.mkfifo(backup_pipe)
  479. self.log.debug("Will backup: {}".format(files_to_backup))
  480. header_files = self.prepare_backup_header()
  481. # Setup worker to send encrypted data chunks to the backup_target
  482. to_send = Queue(10)
  483. send_proc = SendWorker(to_send, self.tmpdir, backup_stdout)
  484. send_proc.start()
  485. for f in header_files:
  486. to_send.put(f)
  487. qubes_xml_info = self.VMToBackup(
  488. None,
  489. [self.FileToBackup(qubes_xml, '')],
  490. ''
  491. )
  492. for vm_info in itertools.chain([qubes_xml_info],
  493. files_to_backup.itervalues()):
  494. for file_info in vm_info.files:
  495. self.log.debug("Backing up {}".format(file_info))
  496. backup_tempfile = os.path.join(
  497. self.tmpdir, file_info.subdir,
  498. os.path.basename(file_info.path))
  499. self.log.debug("Using temporary location: {}".format(
  500. backup_tempfile))
  501. # Ensure the temporary directory exists
  502. if not os.path.isdir(os.path.dirname(backup_tempfile)):
  503. os.makedirs(os.path.dirname(backup_tempfile))
  504. # The first tar cmd can use any complex feature as we want.
  505. # Files will be verified before untaring this.
  506. # Prefix the path in archive with filename["subdir"] to have it
  507. # verified during untar
  508. tar_cmdline = (["tar", "-Pc", '--sparse',
  509. "-f", backup_pipe,
  510. '-C', os.path.dirname(file_info.path)] +
  511. (['--dereference'] if
  512. file_info.subdir != "dom0-home/" else []) +
  513. ['--xform', 's:^%s:%s\\0:' % (
  514. os.path.basename(file_info.path),
  515. file_info.subdir),
  516. os.path.basename(file_info.path)
  517. ])
  518. if self.compressed:
  519. tar_cmdline.insert(-1,
  520. "--use-compress-program=%s" % self.compression_filter)
  521. self.log.debug(" ".join(tar_cmdline))
  522. # Tips: Popen(bufsize=0)
  523. # Pipe: tar-sparse | encryptor [| hmac] | tar | backup_target
  524. # Pipe: tar-sparse [| hmac] | tar | backup_target
  525. # TODO: log handle stderr
  526. tar_sparse = subprocess.Popen(
  527. tar_cmdline, stdin=subprocess.PIPE)
  528. self.processes_to_kill_on_cancel.append(tar_sparse)
  529. # Wait for compressor (tar) process to finish or for any
  530. # error of other subprocesses
  531. i = 0
  532. run_error = "paused"
  533. encryptor = None
  534. if self.encrypted:
  535. # Start encrypt
  536. # If no cipher is provided,
  537. # the data is forwarded unencrypted !!!
  538. encryptor = subprocess.Popen([
  539. "openssl", "enc",
  540. "-e", "-" + self.crypto_algorithm,
  541. "-pass", "pass:" + passphrase],
  542. stdin=open(backup_pipe, 'rb'),
  543. stdout=subprocess.PIPE)
  544. pipe = encryptor.stdout
  545. else:
  546. pipe = open(backup_pipe, 'rb')
  547. while run_error == "paused":
  548. # Start HMAC
  549. hmac = subprocess.Popen([
  550. "openssl", "dgst", "-" + self.hmac_algorithm,
  551. "-hmac", passphrase],
  552. stdin=subprocess.PIPE,
  553. stdout=subprocess.PIPE)
  554. # Prepare a first chunk
  555. chunkfile = backup_tempfile + "." + "%03d" % i
  556. i += 1
  557. chunkfile_p = open(chunkfile, 'wb')
  558. common_args = {
  559. 'backup_target': chunkfile_p,
  560. 'hmac': hmac,
  561. 'vmproc': vmproc,
  562. 'addproc': tar_sparse,
  563. 'progress_callback': self._add_vm_progress,
  564. 'size_limit': self.chunk_size,
  565. }
  566. run_error = wait_backup_feedback(
  567. in_stream=pipe, streamproc=encryptor,
  568. **common_args)
  569. chunkfile_p.close()
  570. self.log.debug(
  571. "Wait_backup_feedback returned: {}".format(run_error))
  572. if self.canceled:
  573. try:
  574. tar_sparse.terminate()
  575. except OSError:
  576. pass
  577. try:
  578. hmac.terminate()
  579. except OSError:
  580. pass
  581. tar_sparse.wait()
  582. hmac.wait()
  583. to_send.put(QUEUE_ERROR)
  584. send_proc.join()
  585. shutil.rmtree(self.tmpdir)
  586. raise BackupCanceledError("Backup canceled")
  587. if run_error and run_error != "size_limit":
  588. send_proc.terminate()
  589. if run_error == "VM" and vmproc:
  590. raise qubes.exc.QubesException(
  591. "Failed to write the backup, VM output:\n" +
  592. vmproc.stderr.read(MAX_STDERR_BYTES))
  593. else:
  594. raise qubes.exc.QubesException(
  595. "Failed to perform backup: error in " +
  596. run_error)
  597. # Send the chunk to the backup target
  598. self._queue_put_with_check(
  599. send_proc, vmproc, to_send,
  600. os.path.relpath(chunkfile, self.tmpdir))
  601. # Close HMAC
  602. hmac.stdin.close()
  603. hmac.wait()
  604. self.log.debug("HMAC proc return code: {}".format(
  605. hmac.poll()))
  606. # Write HMAC data next to the chunk file
  607. hmac_data = hmac.stdout.read()
  608. self.log.debug(
  609. "Writing hmac to {}.hmac".format(chunkfile))
  610. with open(chunkfile + ".hmac", 'w') as hmac_file:
  611. hmac_file.write(hmac_data)
  612. # Send the HMAC to the backup target
  613. self._queue_put_with_check(
  614. send_proc, vmproc, to_send,
  615. os.path.relpath(chunkfile, self.tmpdir) + ".hmac")
  616. if tar_sparse.poll() is None or run_error == "size_limit":
  617. run_error = "paused"
  618. else:
  619. self.processes_to_kill_on_cancel.remove(tar_sparse)
  620. self.log.debug(
  621. "Finished tar sparse with exit code {}".format(
  622. tar_sparse.poll()))
  623. pipe.close()
  624. # This VM done, update progress
  625. self._done_vms_bytes += vm_info.size
  626. self._current_vm_bytes = 0
  627. self._send_progress_update()
  628. # Save date of last backup
  629. if vm_info.vm:
  630. vm_info.vm.backup_timestamp = datetime.datetime.now()
  631. self._queue_put_with_check(send_proc, vmproc, to_send, QUEUE_FINISHED)
  632. send_proc.join()
  633. shutil.rmtree(self.tmpdir)
  634. if self.canceled:
  635. raise BackupCanceledError("Backup canceled")
  636. if send_proc.exitcode != 0:
  637. raise qubes.exc.QubesException(
  638. "Failed to send backup: error in the sending process")
  639. if vmproc:
  640. self.log.debug("VMProc1 proc return code: {}".format(vmproc.poll()))
  641. if tar_sparse is not None:
  642. self.log.debug("Sparse1 proc return code: {}".format(
  643. tar_sparse.poll()))
  644. vmproc.stdin.close()
  645. self.app.save()
  646. def wait_backup_feedback(progress_callback, in_stream, streamproc,
  647. backup_target, hmac=None, vmproc=None,
  648. addproc=None,
  649. size_limit=None):
  650. '''
  651. Wait for backup chunk to finish
  652. - Monitor all the processes (streamproc, hmac, vmproc, addproc) for errors
  653. - Copy stdout of streamproc to backup_target and hmac stdin if available
  654. - Compute progress based on total_backup_sz and send progress to
  655. progress_callback function
  656. - Returns if
  657. - one of the monitored processes error out (streamproc, hmac, vmproc,
  658. addproc), along with the processe that failed
  659. - all of the monitored processes except vmproc finished successfully
  660. (vmproc termination is controlled by the python script)
  661. - streamproc does not delivers any data anymore (return with the error
  662. "")
  663. - size_limit is provided and is about to be exceeded
  664. '''
  665. buffer_size = 409600
  666. run_error = None
  667. run_count = 1
  668. bytes_copied = 0
  669. log = logging.getLogger('qubes.backup')
  670. while run_count > 0 and run_error is None:
  671. if size_limit and bytes_copied + buffer_size > size_limit:
  672. return "size_limit"
  673. buf = in_stream.read(buffer_size)
  674. if callable(progress_callback):
  675. progress_callback(len(buf))
  676. bytes_copied += len(buf)
  677. run_count = 0
  678. if hmac:
  679. retcode = hmac.poll()
  680. if retcode is not None:
  681. if retcode != 0:
  682. run_error = "hmac"
  683. else:
  684. run_count += 1
  685. if addproc:
  686. retcode = addproc.poll()
  687. if retcode is not None:
  688. if retcode != 0:
  689. run_error = "addproc"
  690. else:
  691. run_count += 1
  692. if vmproc:
  693. retcode = vmproc.poll()
  694. if retcode is not None:
  695. if retcode != 0:
  696. run_error = "VM"
  697. log.debug(vmproc.stdout.read())
  698. else:
  699. # VM should run until the end
  700. pass
  701. if streamproc:
  702. retcode = streamproc.poll()
  703. if retcode is not None:
  704. if retcode != 0:
  705. run_error = "streamproc"
  706. break
  707. elif retcode == 0 and len(buf) <= 0:
  708. return ""
  709. run_count += 1
  710. else:
  711. if len(buf) <= 0:
  712. return ""
  713. try:
  714. backup_target.write(buf)
  715. except IOError as e:
  716. if e.errno == errno.EPIPE:
  717. run_error = "target"
  718. else:
  719. raise
  720. if hmac:
  721. hmac.stdin.write(buf)
  722. return run_error
  723. class ExtractWorker2(Process):
  724. def __init__(self, queue, base_dir, passphrase, encrypted,
  725. progress_callback, vmproc=None,
  726. compressed=False, crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
  727. verify_only=False):
  728. super(ExtractWorker2, self).__init__()
  729. self.queue = queue
  730. self.base_dir = base_dir
  731. self.passphrase = passphrase
  732. self.encrypted = encrypted
  733. self.compressed = compressed
  734. self.crypto_algorithm = crypto_algorithm
  735. self.verify_only = verify_only
  736. self.blocks_backedup = 0
  737. self.tar2_process = None
  738. self.tar2_current_file = None
  739. self.decompressor_process = None
  740. self.decryptor_process = None
  741. self.progress_callback = progress_callback
  742. self.vmproc = vmproc
  743. self.restore_pipe = os.path.join(self.base_dir, "restore_pipe")
  744. self.log = logging.getLogger('qubes.backup.extract')
  745. self.log.debug("Creating pipe in: {}".format(self.restore_pipe))
  746. os.mkfifo(self.restore_pipe)
  747. self.stderr_encoding = sys.stderr.encoding or 'utf-8'
  748. def collect_tar_output(self):
  749. if not self.tar2_process.stderr:
  750. return
  751. if self.tar2_process.poll() is None:
  752. try:
  753. new_lines = self.tar2_process.stderr \
  754. .read(MAX_STDERR_BYTES).splitlines()
  755. except IOError as e:
  756. if e.errno == errno.EAGAIN:
  757. return
  758. else:
  759. raise
  760. else:
  761. new_lines = self.tar2_process.stderr.readlines()
  762. new_lines = map(lambda x: x.decode(self.stderr_encoding), new_lines)
  763. msg_re = re.compile(r".*#[0-9].*restore_pipe")
  764. debug_msg = filter(msg_re.match, new_lines)
  765. self.log.debug('tar2_stderr: {}'.format('\n'.join(debug_msg)))
  766. new_lines = filter(lambda x: not msg_re.match(x), new_lines)
  767. self.tar2_stderr += new_lines
  768. def run(self):
  769. try:
  770. self.__run__()
  771. except Exception as e:
  772. exc_type, exc_value, exc_traceback = sys.exc_info()
  773. # Cleanup children
  774. for process in [self.decompressor_process,
  775. self.decryptor_process,
  776. self.tar2_process]:
  777. if process:
  778. try:
  779. process.terminate()
  780. except OSError:
  781. pass
  782. process.wait()
  783. self.log.error("ERROR: " + unicode(e))
  784. raise e, None, exc_traceback
  785. def __run__(self):
  786. self.log.debug("Started sending thread")
  787. self.log.debug("Moving to dir " + self.base_dir)
  788. os.chdir(self.base_dir)
  789. filename = None
  790. for filename in iter(self.queue.get, None):
  791. if filename in (QUEUE_FINISHED, QUEUE_ERROR):
  792. break
  793. self.log.debug("Extracting file " + filename)
  794. if filename.endswith('.000'):
  795. # next file
  796. if self.tar2_process is not None:
  797. if self.tar2_process.wait() != 0:
  798. self.collect_tar_output()
  799. self.log.error(
  800. "ERROR: unable to extract files for {0}, tar "
  801. "output:\n {1}".
  802. format(self.tar2_current_file,
  803. "\n ".join(self.tar2_stderr)))
  804. else:
  805. # Finished extracting the tar file
  806. self.tar2_process = None
  807. self.tar2_current_file = None
  808. tar2_cmdline = ['tar',
  809. '-%sMkvf' % ("t" if self.verify_only else "x"),
  810. self.restore_pipe,
  811. os.path.relpath(filename.rstrip('.000'))]
  812. self.log.debug("Running command " + unicode(tar2_cmdline))
  813. self.tar2_process = subprocess.Popen(tar2_cmdline,
  814. stdin=subprocess.PIPE,
  815. stderr=subprocess.PIPE)
  816. fcntl.fcntl(self.tar2_process.stderr.fileno(), fcntl.F_SETFL,
  817. fcntl.fcntl(self.tar2_process.stderr.fileno(),
  818. fcntl.F_GETFL) | os.O_NONBLOCK)
  819. self.tar2_stderr = []
  820. elif not self.tar2_process:
  821. # Extracting of the current archive failed, skip to the next
  822. # archive
  823. os.remove(filename)
  824. continue
  825. else:
  826. self.collect_tar_output()
  827. self.log.debug("Releasing next chunck")
  828. self.tar2_process.stdin.write("\n")
  829. self.tar2_process.stdin.flush()
  830. self.tar2_current_file = filename
  831. pipe = open(self.restore_pipe, 'wb')
  832. common_args = {
  833. 'backup_target': pipe,
  834. 'hmac': None,
  835. 'vmproc': self.vmproc,
  836. 'addproc': self.tar2_process
  837. }
  838. if self.encrypted:
  839. # Start decrypt
  840. self.decryptor_process = subprocess.Popen(
  841. ["openssl", "enc",
  842. "-d",
  843. "-" + self.crypto_algorithm,
  844. "-pass",
  845. "pass:" + self.passphrase] +
  846. (["-z"] if self.compressed else []),
  847. stdin=open(filename, 'rb'),
  848. stdout=subprocess.PIPE)
  849. run_error = wait_backup_feedback(
  850. progress_callback=self.progress_callback,
  851. in_stream=self.decryptor_process.stdout,
  852. streamproc=self.decryptor_process,
  853. **common_args)
  854. elif self.compressed:
  855. self.decompressor_process = subprocess.Popen(
  856. ["gzip", "-d"],
  857. stdin=open(filename, 'rb'),
  858. stdout=subprocess.PIPE)
  859. run_error = wait_backup_feedback(
  860. progress_callback=self.progress_callback,
  861. in_stream=self.decompressor_process.stdout,
  862. streamproc=self.decompressor_process,
  863. **common_args)
  864. else:
  865. run_error = wait_backup_feedback(
  866. progress_callback=self.progress_callback,
  867. in_stream=open(filename, "rb"), streamproc=None,
  868. **common_args)
  869. try:
  870. pipe.close()
  871. except IOError as e:
  872. if e.errno == errno.EPIPE:
  873. self.log.debug(
  874. "Got EPIPE while closing pipe to "
  875. "the inner tar process")
  876. # ignore the error
  877. else:
  878. raise
  879. if len(run_error):
  880. if run_error == "target":
  881. self.collect_tar_output()
  882. details = "\n".join(self.tar2_stderr)
  883. else:
  884. details = "%s failed" % run_error
  885. self.tar2_process.terminate()
  886. self.tar2_process.wait()
  887. self.tar2_process = None
  888. self.log.error("Error while processing '{}': {}".format(
  889. self.tar2_current_file, details))
  890. # Delete the file as we don't need it anymore
  891. self.log.debug("Removing file " + filename)
  892. os.remove(filename)
  893. os.unlink(self.restore_pipe)
  894. if self.tar2_process is not None:
  895. if filename == QUEUE_ERROR:
  896. self.tar2_process.terminate()
  897. self.tar2_process.wait()
  898. elif self.tar2_process.wait() != 0:
  899. self.collect_tar_output()
  900. raise qubes.exc.QubesException(
  901. "unable to extract files for {0}.{1} Tar command "
  902. "output: %s".
  903. format(self.tar2_current_file,
  904. (" Perhaps the backup is encrypted?"
  905. if not self.encrypted else "",
  906. "\n".join(self.tar2_stderr))))
  907. else:
  908. # Finished extracting the tar file
  909. self.tar2_process = None
  910. self.log.debug("Finished extracting thread")
  911. class ExtractWorker3(ExtractWorker2):
  912. def __init__(self, queue, base_dir, passphrase, encrypted,
  913. progress_callback, vmproc=None,
  914. compressed=False, crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
  915. compression_filter=None, verify_only=False):
  916. super(ExtractWorker3, self).__init__(queue, base_dir, passphrase,
  917. encrypted,
  918. progress_callback, vmproc,
  919. compressed, crypto_algorithm,
  920. verify_only)
  921. self.compression_filter = compression_filter
  922. os.unlink(self.restore_pipe)
  923. def __run__(self):
  924. self.log.debug("Started sending thread")
  925. self.log.debug("Moving to dir " + self.base_dir)
  926. os.chdir(self.base_dir)
  927. filename = None
  928. input_pipe = None
  929. for filename in iter(self.queue.get, None):
  930. if filename in (QUEUE_FINISHED, QUEUE_ERROR):
  931. break
  932. self.log.debug("Extracting file " + filename)
  933. if filename.endswith('.000'):
  934. # next file
  935. if self.tar2_process is not None:
  936. input_pipe.close()
  937. if self.tar2_process.wait() != 0:
  938. self.collect_tar_output()
  939. self.log.error(
  940. "ERROR: unable to extract files for {0}, tar "
  941. "output:\n {1}".
  942. format(self.tar2_current_file,
  943. "\n ".join(self.tar2_stderr)))
  944. else:
  945. # Finished extracting the tar file
  946. self.tar2_process = None
  947. self.tar2_current_file = None
  948. tar2_cmdline = ['tar',
  949. '-%sk' % ("t" if self.verify_only else "x"),
  950. os.path.relpath(filename.rstrip('.000'))]
  951. if self.compressed:
  952. if self.compression_filter:
  953. tar2_cmdline.insert(-1,
  954. "--use-compress-program=%s" %
  955. self.compression_filter)
  956. else:
  957. tar2_cmdline.insert(-1, "--use-compress-program=%s" %
  958. DEFAULT_COMPRESSION_FILTER)
  959. self.log.debug("Running command " + unicode(tar2_cmdline))
  960. if self.encrypted:
  961. # Start decrypt
  962. self.decryptor_process = subprocess.Popen(
  963. ["openssl", "enc",
  964. "-d",
  965. "-" + self.crypto_algorithm,
  966. "-pass",
  967. "pass:" + self.passphrase],
  968. stdin=subprocess.PIPE,
  969. stdout=subprocess.PIPE)
  970. self.tar2_process = subprocess.Popen(
  971. tar2_cmdline,
  972. stdin=self.decryptor_process.stdout,
  973. stderr=subprocess.PIPE)
  974. input_pipe = self.decryptor_process.stdin
  975. else:
  976. self.tar2_process = subprocess.Popen(
  977. tar2_cmdline,
  978. stdin=subprocess.PIPE,
  979. stderr=subprocess.PIPE)
  980. input_pipe = self.tar2_process.stdin
  981. fcntl.fcntl(self.tar2_process.stderr.fileno(), fcntl.F_SETFL,
  982. fcntl.fcntl(self.tar2_process.stderr.fileno(),
  983. fcntl.F_GETFL) | os.O_NONBLOCK)
  984. self.tar2_stderr = []
  985. elif not self.tar2_process:
  986. # Extracting of the current archive failed, skip to the next
  987. # archive
  988. os.remove(filename)
  989. continue
  990. else:
  991. self.log.debug("Releasing next chunck")
  992. self.tar2_current_file = filename
  993. common_args = {
  994. 'backup_target': input_pipe,
  995. 'hmac': None,
  996. 'vmproc': self.vmproc,
  997. 'addproc': self.tar2_process
  998. }
  999. run_error = wait_backup_feedback(
  1000. progress_callback=self.progress_callback,
  1001. in_stream=open(filename, "rb"), streamproc=None,
  1002. **common_args)
  1003. if len(run_error):
  1004. if run_error == "target":
  1005. self.collect_tar_output()
  1006. details = "\n".join(self.tar2_stderr)
  1007. else:
  1008. details = "%s failed" % run_error
  1009. if self.decryptor_process:
  1010. self.decryptor_process.terminate()
  1011. self.decryptor_process.wait()
  1012. self.decryptor_process = None
  1013. self.tar2_process.terminate()
  1014. self.tar2_process.wait()
  1015. self.tar2_process = None
  1016. self.log.error("Error while processing '{}': {}".format(
  1017. self.tar2_current_file, details))
  1018. # Delete the file as we don't need it anymore
  1019. self.log.debug("Removing file " + filename)
  1020. os.remove(filename)
  1021. if self.tar2_process is not None:
  1022. input_pipe.close()
  1023. if filename == QUEUE_ERROR:
  1024. if self.decryptor_process:
  1025. self.decryptor_process.terminate()
  1026. self.decryptor_process.wait()
  1027. self.decryptor_process = None
  1028. self.tar2_process.terminate()
  1029. self.tar2_process.wait()
  1030. elif self.tar2_process.wait() != 0:
  1031. self.collect_tar_output()
  1032. raise qubes.exc.QubesException(
  1033. "unable to extract files for {0}.{1} Tar command "
  1034. "output: %s".
  1035. format(self.tar2_current_file,
  1036. (" Perhaps the backup is encrypted?"
  1037. if not self.encrypted else "",
  1038. "\n".join(self.tar2_stderr))))
  1039. else:
  1040. # Finished extracting the tar file
  1041. self.tar2_process = None
  1042. self.log.debug("Finished extracting thread")
  1043. def get_supported_hmac_algo(hmac_algorithm=None):
  1044. # Start with provided default
  1045. if hmac_algorithm:
  1046. yield hmac_algorithm
  1047. proc = subprocess.Popen(['openssl', 'list-message-digest-algorithms'],
  1048. stdout=subprocess.PIPE)
  1049. for algo in proc.stdout.readlines():
  1050. if '=>' in algo:
  1051. continue
  1052. yield algo.strip()
  1053. proc.wait()
  1054. class BackupRestoreOptions(object):
  1055. def __init__(self):
  1056. #: use default NetVM if the one referenced in backup do not exists on
  1057. # the host
  1058. self.use_default_netvm = True
  1059. #: set NetVM to "none" if the one referenced in backup do not exists
  1060. # on the host
  1061. self.use_none_netvm = False
  1062. #: set template to default if the one referenced in backup do not
  1063. # exists on the host
  1064. self.use_default_template = True
  1065. #: use default kernel if the one referenced in backup do not exists
  1066. # on the host
  1067. self.use_default_kernel = True
  1068. #: restore dom0 home
  1069. self.dom0_home = True
  1070. #: dictionary how what templates should be used instead of those
  1071. # referenced in backup
  1072. self.replace_template = {}
  1073. #: restore dom0 home even if username is different
  1074. self.ignore_username_mismatch = False
  1075. #: do not restore data, only verify backup integrity
  1076. self.verify_only = False
  1077. #: automatically rename VM during restore, when it would conflict
  1078. # with existing one
  1079. self.rename_conflicting = True
  1080. #: list of VM names to exclude
  1081. self.exclude = []
  1082. class BackupRestore(object):
  1083. """Usage:
  1084. >>> restore_op = BackupRestore(...)
  1085. >>> # adjust restore_op.options here
  1086. >>> restore_info = restore_op.get_restore_info()
  1087. >>> # manipulate restore_info to select VMs to restore here
  1088. >>> restore_op.restore_do(restore_info)
  1089. """
  1090. class VMToRestore(object):
  1091. #: VM excluded from restore by user
  1092. EXCLUDED = object()
  1093. #: VM with such name already exists on the host
  1094. ALREADY_EXISTS = object()
  1095. #: NetVM used by the VM does not exists on the host
  1096. MISSING_NETVM = object()
  1097. #: TemplateVM used by the VM does not exists on the host
  1098. MISSING_TEMPLATE = object()
  1099. #: Kernel used by the VM does not exists on the host
  1100. MISSING_KERNEL = object()
  1101. def __init__(self, vm):
  1102. self.vm = vm
  1103. if 'backup-path' in vm.features:
  1104. self.subdir = vm.features['backup-path']
  1105. else:
  1106. self.subdir = None
  1107. if 'backup-size' in vm.features and vm.features['backup-size']:
  1108. self.size = int(vm.features['backup-size'])
  1109. else:
  1110. self.size = 0
  1111. self.problems = set()
  1112. if hasattr(vm, 'template') and vm.template:
  1113. self.template = vm.template.name
  1114. else:
  1115. self.template = None
  1116. if vm.netvm:
  1117. self.netvm = vm.netvm.name
  1118. else:
  1119. self.netvm = None
  1120. self.name = vm.name
  1121. self.orig_template = None
  1122. @property
  1123. def good_to_go(self):
  1124. return len(self.problems) == 0
  1125. class Dom0ToRestore(VMToRestore):
  1126. #: backup was performed on system with different dom0 username
  1127. USERNAME_MISMATCH = object()
  1128. def __init__(self, vm, subdir=None):
  1129. super(BackupRestore.Dom0ToRestore, self).__init__(vm)
  1130. if subdir:
  1131. self.subdir = subdir
  1132. self.username = os.path.basename(subdir)
  1133. def __init__(self, app, backup_location, backup_vm, passphrase):
  1134. super(BackupRestore, self).__init__()
  1135. #: qubes.Qubes instance
  1136. self.app = app
  1137. #: options how the backup should be restored
  1138. self.options = BackupRestoreOptions()
  1139. #: VM from which backup should be retrieved
  1140. self.backup_vm = backup_vm
  1141. if backup_vm and backup_vm.qid == 0:
  1142. self.backup_vm = None
  1143. #: backup path, inside VM pointed by :py:attr:`backup_vm`
  1144. self.backup_location = backup_location
  1145. #: passphrase protecting backup integrity and optionally decryption
  1146. self.passphrase = passphrase
  1147. #: temporary directory used to extract the data before moving to the
  1148. # final location; should be on the same filesystem as /var/lib/qubes
  1149. self.tmpdir = tempfile.mkdtemp(prefix="restore", dir="/var/tmp")
  1150. #: list of processes (Popen objects) to kill on cancel
  1151. self.processes_to_kill_on_cancel = []
  1152. #: is the backup operation canceled
  1153. self.canceled = False
  1154. #: report restore progress, called with one argument - percents of
  1155. # data restored
  1156. # FIXME: convert to float [0,1]
  1157. self.progress_callback = None
  1158. self.log = logging.getLogger('qubes.backup')
  1159. #: basic information about the backup
  1160. self.header_data = self._retrieve_backup_header()
  1161. #: VMs included in the backup
  1162. self.backup_app = self._process_qubes_xml()
  1163. def cancel(self):
  1164. """Cancel running backup operation. Can be called from another thread.
  1165. """
  1166. self.canceled = True
  1167. for proc in self.processes_to_kill_on_cancel:
  1168. try:
  1169. proc.terminate()
  1170. except OSError:
  1171. pass
  1172. def _start_retrieval_process(self, filelist, limit_count, limit_bytes):
  1173. """Retrieve backup stream and extract it to :py:attr:`tmpdir`
  1174. :param filelist: list of files to extract; listing directory name
  1175. will extract the whole directory; use empty list to extract the whole
  1176. archive
  1177. :param limit_count: maximum number of files to extract
  1178. :param limit_bytes: maximum size of extracted data
  1179. :return: a touple of (Popen object of started process, file-like
  1180. object for reading extracted files list, file-like object for reading
  1181. errors)
  1182. """
  1183. vmproc = None
  1184. if self.backup_vm is not None:
  1185. # If APPVM, STDOUT is a PIPE
  1186. vmproc = self.backup_vm.run_service('qubes.Restore',
  1187. passio_popen=True, passio_stderr=True)
  1188. vmproc.stdin.write(
  1189. self.backup_location.replace("\r", "").replace("\n", "") + "\n")
  1190. # Send to tar2qfile the VMs that should be extracted
  1191. vmproc.stdin.write(" ".join(filelist) + "\n")
  1192. self.processes_to_kill_on_cancel.append(vmproc)
  1193. backup_stdin = vmproc.stdout
  1194. tar1_command = ['/usr/libexec/qubes/qfile-dom0-unpacker',
  1195. str(os.getuid()), self.tmpdir, '-v']
  1196. else:
  1197. backup_stdin = open(self.backup_location, 'rb')
  1198. tar1_command = ['tar',
  1199. '-ixv',
  1200. '-C', self.tmpdir] + filelist
  1201. tar1_env = os.environ.copy()
  1202. tar1_env['UPDATES_MAX_BYTES'] = str(limit_bytes)
  1203. tar1_env['UPDATES_MAX_FILES'] = str(limit_count)
  1204. self.log.debug("Run command" + unicode(tar1_command))
  1205. command = subprocess.Popen(
  1206. tar1_command,
  1207. stdin=backup_stdin,
  1208. stdout=vmproc.stdin if vmproc else subprocess.PIPE,
  1209. stderr=subprocess.PIPE,
  1210. env=tar1_env)
  1211. self.processes_to_kill_on_cancel.append(command)
  1212. # qfile-dom0-unpacker output filelist on stderr
  1213. # and have stdout connected to the VM), while tar output filelist
  1214. # on stdout
  1215. if self.backup_vm:
  1216. filelist_pipe = command.stderr
  1217. # let qfile-dom0-unpacker hold the only open FD to the write end of
  1218. # pipe, otherwise qrexec-client will not receive EOF when
  1219. # qfile-dom0-unpacker terminates
  1220. vmproc.stdin.close()
  1221. else:
  1222. filelist_pipe = command.stdout
  1223. if self.backup_vm:
  1224. error_pipe = vmproc.stderr
  1225. else:
  1226. error_pipe = command.stderr
  1227. return command, filelist_pipe, error_pipe
  1228. def _verify_hmac(self, filename, hmacfile, algorithm=None):
  1229. def load_hmac(hmac_text):
  1230. hmac_text = hmac_text.strip().split("=")
  1231. if len(hmac_text) > 1:
  1232. hmac_text = hmac_text[1].strip()
  1233. else:
  1234. raise qubes.exc.QubesException(
  1235. "ERROR: invalid hmac file content")
  1236. return hmac_text
  1237. if algorithm is None:
  1238. algorithm = self.header_data.hmac_algorithm
  1239. passphrase = self.passphrase.encode('utf-8')
  1240. self.log.debug("Verifying file {}".format(filename))
  1241. if hmacfile != filename + ".hmac":
  1242. raise qubes.exc.QubesException(
  1243. "ERROR: expected hmac for {}, but got {}".
  1244. format(filename, hmacfile))
  1245. hmac_proc = subprocess.Popen(
  1246. ["openssl", "dgst", "-" + algorithm, "-hmac", passphrase],
  1247. stdin=open(os.path.join(self.tmpdir, filename), 'rb'),
  1248. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  1249. hmac_stdout, hmac_stderr = hmac_proc.communicate()
  1250. if len(hmac_stderr) > 0:
  1251. raise qubes.exc.QubesException(
  1252. "ERROR: verify file {0}: {1}".format(filename, hmac_stderr))
  1253. else:
  1254. self.log.debug("Loading hmac for file {}".format(filename))
  1255. hmac = load_hmac(open(os.path.join(self.tmpdir, hmacfile),
  1256. 'r').read())
  1257. if len(hmac) > 0 and load_hmac(hmac_stdout) == hmac:
  1258. os.unlink(os.path.join(self.tmpdir, hmacfile))
  1259. self.log.debug(
  1260. "File verification OK -> Sending file {}".format(filename))
  1261. return True
  1262. else:
  1263. raise qubes.exc.QubesException(
  1264. "ERROR: invalid hmac for file {0}: {1}. "
  1265. "Is the passphrase correct?".
  1266. format(filename, load_hmac(hmac_stdout)))
  1267. def _retrieve_backup_header(self):
  1268. """Retrieve backup header and qubes.xml. Only backup header is
  1269. analyzed, qubes.xml is left as-is
  1270. (not even verified/decrypted/uncompressed)
  1271. :return header_data
  1272. :rtype :py:class:`BackupHeader`
  1273. """
  1274. if not self.backup_vm and os.path.exists(
  1275. os.path.join(self.backup_location, 'qubes.xml')):
  1276. # backup format version 1 doesn't have header
  1277. header_data = BackupHeader()
  1278. header_data.version = 1
  1279. return header_data
  1280. (retrieve_proc, filelist_pipe, error_pipe) = \
  1281. self._start_retrieval_process(
  1282. ['backup-header', 'backup-header.hmac',
  1283. 'qubes.xml.000', 'qubes.xml.000.hmac'], 4, 1024 * 1024)
  1284. expect_tar_error = False
  1285. filename = filelist_pipe.readline().strip()
  1286. hmacfile = filelist_pipe.readline().strip()
  1287. # tar output filename before actually extracting it, so wait for the
  1288. # next one before trying to access it
  1289. if not self.backup_vm:
  1290. filelist_pipe.readline().strip()
  1291. self.log.debug("Got backup header and hmac: {}, {}".format(
  1292. filename, hmacfile))
  1293. if not filename or filename == "EOF" or \
  1294. not hmacfile or hmacfile == "EOF":
  1295. retrieve_proc.wait()
  1296. proc_error_msg = error_pipe.read(MAX_STDERR_BYTES)
  1297. raise qubes.exc.QubesException(
  1298. "Premature end of archive while receiving "
  1299. "backup header. Process output:\n" + proc_error_msg)
  1300. file_ok = False
  1301. hmac_algorithm = DEFAULT_HMAC_ALGORITHM
  1302. for hmac_algo in get_supported_hmac_algo(hmac_algorithm):
  1303. try:
  1304. if self._verify_hmac(filename, hmacfile, hmac_algo):
  1305. file_ok = True
  1306. hmac_algorithm = hmac_algo
  1307. break
  1308. except qubes.exc.QubesException:
  1309. # Ignore exception here, try the next algo
  1310. pass
  1311. if not file_ok:
  1312. raise qubes.exc.QubesException(
  1313. "Corrupted backup header (hmac verification "
  1314. "failed). Is the password correct?")
  1315. if os.path.basename(filename) == HEADER_FILENAME:
  1316. filename = os.path.join(self.tmpdir, filename)
  1317. header_data = BackupHeader(open(filename, 'r').read())
  1318. os.unlink(filename)
  1319. else:
  1320. # if no header found, create one with guessed HMAC algo
  1321. header_data = BackupHeader(
  1322. version=2,
  1323. hmac_algorithm=hmac_algorithm,
  1324. # place explicitly this value, because it is what format_version
  1325. # 2 have
  1326. crypto_algorithm='aes-256-cbc',
  1327. # TODO: set encrypted to something...
  1328. )
  1329. # when tar do not find expected file in archive, it exit with
  1330. # code 2. This will happen because we've requested backup-header
  1331. # file, but the archive do not contain it. Ignore this particular
  1332. # error.
  1333. if not self.backup_vm:
  1334. expect_tar_error = True
  1335. if retrieve_proc.wait() != 0 and not expect_tar_error:
  1336. raise qubes.exc.QubesException(
  1337. "unable to read the qubes backup file {0} ({1}): {2}".format(
  1338. self.backup_location,
  1339. retrieve_proc.wait(),
  1340. error_pipe.read(MAX_STDERR_BYTES)
  1341. ))
  1342. if retrieve_proc in self.processes_to_kill_on_cancel:
  1343. self.processes_to_kill_on_cancel.remove(retrieve_proc)
  1344. # wait for other processes (if any)
  1345. for proc in self.processes_to_kill_on_cancel:
  1346. if proc.wait() != 0:
  1347. raise qubes.exc.QubesException(
  1348. "Backup header retrieval failed (exit code {})".format(
  1349. proc.wait())
  1350. )
  1351. return header_data
  1352. def _start_inner_extraction_worker(self, queue):
  1353. """Start a worker process, extracting inner layer of bacup archive,
  1354. extract them to :py:attr:`tmpdir`.
  1355. End the data by pushing QUEUE_FINISHED or QUEUE_ERROR to the queue.
  1356. :param queue :py:class:`Queue` object to handle files from
  1357. """
  1358. # Setup worker to extract encrypted data chunks to the restore dirs
  1359. # Create the process here to pass it options extracted from
  1360. # backup header
  1361. extractor_params = {
  1362. 'queue': queue,
  1363. 'base_dir': self.tmpdir,
  1364. 'passphrase': self.passphrase,
  1365. 'encrypted': self.header_data.encrypted,
  1366. 'compressed': self.header_data.compressed,
  1367. 'crypto_algorithm': self.header_data.crypto_algorithm,
  1368. 'verify_only': self.options.verify_only,
  1369. 'progress_callback': self.progress_callback,
  1370. }
  1371. format_version = self.header_data.version
  1372. if format_version == 2:
  1373. extract_proc = ExtractWorker2(**extractor_params)
  1374. elif format_version in [3, 4]:
  1375. extractor_params['compression_filter'] = \
  1376. self.header_data.compression_filter
  1377. extract_proc = ExtractWorker3(**extractor_params)
  1378. else:
  1379. raise NotImplementedError(
  1380. "Backup format version %d not supported" % format_version)
  1381. extract_proc.start()
  1382. return extract_proc
  1383. def _process_qubes_xml(self):
  1384. """Verify, unpack and load qubes.xml. Possibly convert its format if
  1385. necessary. It expect that :py:attr:`header_data` is already populated,
  1386. and :py:meth:`retrieve_backup_header` was called.
  1387. """
  1388. if self.header_data.version == 1:
  1389. backup_app = qubes.core2migration.Core2Qubes(
  1390. os.path.join(self.backup_location, 'qubes.xml'))
  1391. return backup_app
  1392. else:
  1393. self._verify_hmac("qubes.xml.000", "qubes.xml.000.hmac")
  1394. queue = Queue()
  1395. queue.put("qubes.xml.000")
  1396. queue.put(QUEUE_FINISHED)
  1397. extract_proc = self._start_inner_extraction_worker(queue)
  1398. extract_proc.join()
  1399. if extract_proc.exitcode != 0:
  1400. raise qubes.exc.QubesException(
  1401. "unable to extract the qubes backup. "
  1402. "Check extracting process errors.")
  1403. if self.header_data.version in [2, 3]:
  1404. backup_app = qubes.core2migration.Core2Qubes(
  1405. os.path.join(self.tmpdir, 'qubes.xml'))
  1406. else:
  1407. backup_app = qubes.Qubes(os.path.join(self.tmpdir, 'qubes.xml'))
  1408. # Not needed anymore - all the data stored in backup_app
  1409. os.unlink(os.path.join(self.tmpdir, 'qubes.xml'))
  1410. return backup_app
  1411. def _restore_vm_dirs(self, vms_dirs, vms_size):
  1412. # Currently each VM consists of at most 7 archives (count
  1413. # file_to_backup calls in backup_prepare()), but add some safety
  1414. # margin for further extensions. Each archive is divided into 100MB
  1415. # chunks. Additionally each file have own hmac file. So assume upper
  1416. # limit as 2*(10*COUNT_OF_VMS+TOTAL_SIZE/100MB)
  1417. limit_count = str(2 * (10 * len(vms_dirs) +
  1418. int(vms_size / (100 * 1024 * 1024))))
  1419. self.log.debug("Working in temporary dir:" + self.tmpdir)
  1420. self.log.info(
  1421. "Extracting data: " + size_to_human(vms_size) + " to restore")
  1422. # retrieve backup from the backup stream (either VM, or dom0 file)
  1423. (retrieve_proc, filelist_pipe, error_pipe) = \
  1424. self._start_retrieval_process(vms_dirs, limit_count, vms_size)
  1425. to_extract = Queue()
  1426. # extract data retrieved by retrieve_proc
  1427. extract_proc = self._start_inner_extraction_worker(to_extract)
  1428. try:
  1429. filename = None
  1430. nextfile = None
  1431. while True:
  1432. if self.canceled:
  1433. break
  1434. if not extract_proc.is_alive():
  1435. retrieve_proc.terminate()
  1436. retrieve_proc.wait()
  1437. if retrieve_proc in self.processes_to_kill_on_cancel:
  1438. self.processes_to_kill_on_cancel.remove(retrieve_proc)
  1439. # wait for other processes (if any)
  1440. for proc in self.processes_to_kill_on_cancel:
  1441. proc.wait()
  1442. break
  1443. if nextfile is not None:
  1444. filename = nextfile
  1445. else:
  1446. filename = filelist_pipe.readline().strip()
  1447. self.log.debug("Getting new file:" + filename)
  1448. if not filename or filename == "EOF":
  1449. break
  1450. hmacfile = filelist_pipe.readline().strip()
  1451. if self.canceled:
  1452. break
  1453. # if reading archive directly with tar, wait for next filename -
  1454. # tar prints filename before processing it, so wait for
  1455. # the next one to be sure that whole file was extracted
  1456. if not self.backup_vm:
  1457. nextfile = filelist_pipe.readline().strip()
  1458. self.log.debug("Getting hmac:" + hmacfile)
  1459. if not hmacfile or hmacfile == "EOF":
  1460. # Premature end of archive, either of tar1_command or
  1461. # vmproc exited with error
  1462. break
  1463. if not any(map(lambda x: filename.startswith(x), vms_dirs)):
  1464. self.log.debug("Ignoring VM not selected for restore")
  1465. os.unlink(os.path.join(self.tmpdir, filename))
  1466. os.unlink(os.path.join(self.tmpdir, hmacfile))
  1467. continue
  1468. if self._verify_hmac(filename, hmacfile):
  1469. to_extract.put(os.path.join(self.tmpdir, filename))
  1470. if self.canceled:
  1471. raise BackupCanceledError("Restore canceled",
  1472. tmpdir=self.tmpdir)
  1473. if retrieve_proc.wait() != 0:
  1474. raise qubes.exc.QubesException(
  1475. "unable to read the qubes backup file {0} ({1}): {2}"
  1476. .format(self.backup_location, error_pipe.read(
  1477. MAX_STDERR_BYTES)))
  1478. # wait for other processes (if any)
  1479. for proc in self.processes_to_kill_on_cancel:
  1480. proc.wait()
  1481. if vmproc.returncode != 0:
  1482. raise qubes.exc.QubesException(
  1483. "Backup completed, but VM receiving it reported an error "
  1484. "(exit code {})".format(vmproc.returncode))
  1485. if filename and filename != "EOF":
  1486. raise qubes.exc.QubesException(
  1487. "Premature end of archive, the last file was %s" % filename)
  1488. except:
  1489. to_extract.put(QUEUE_ERROR)
  1490. extract_proc.join()
  1491. raise
  1492. else:
  1493. to_extract.put(QUEUE_FINISHED)
  1494. self.log.debug("Waiting for the extraction process to finish...")
  1495. extract_proc.join()
  1496. self.log.debug("Extraction process finished with code: {}".format(
  1497. extract_proc.exitcode))
  1498. if extract_proc.exitcode != 0:
  1499. raise qubes.exc.QubesException(
  1500. "unable to extract the qubes backup. "
  1501. "Check extracting process errors.")
  1502. def generate_new_name_for_conflicting_vm(self, orig_name, restore_info):
  1503. number = 1
  1504. if len(orig_name) > 29:
  1505. orig_name = orig_name[0:29]
  1506. new_name = orig_name
  1507. while (new_name in restore_info.keys() or
  1508. new_name in map(lambda x: x.name,
  1509. restore_info.values()) or
  1510. new_name in self.app.domains):
  1511. new_name = str('{}{}'.format(orig_name, number))
  1512. number += 1
  1513. if number == 100:
  1514. # give up
  1515. return None
  1516. return new_name
  1517. def restore_info_verify(self, restore_info):
  1518. for vm in restore_info.keys():
  1519. if vm in ['dom0']:
  1520. continue
  1521. vm_info = restore_info[vm]
  1522. assert isinstance(vm_info, self.VMToRestore)
  1523. vm_info.problems.clear()
  1524. if vm in self.options.exclude:
  1525. vm_info.problems.add(self.VMToRestore.EXCLUDED)
  1526. if not self.options.verify_only and \
  1527. vm in self.app.domains:
  1528. if self.options.rename_conflicting:
  1529. new_name = self.generate_new_name_for_conflicting_vm(
  1530. vm, restore_info
  1531. )
  1532. if new_name is not None:
  1533. vm_info.name = new_name
  1534. else:
  1535. vm_info.problems.add(self.VMToRestore.ALREADY_EXISTS)
  1536. else:
  1537. vm_info.problems.add(self.VMToRestore.ALREADY_EXISTS)
  1538. # check template
  1539. if vm_info.template:
  1540. template_name = vm_info.template
  1541. try:
  1542. host_template = self.app.domains[template_name]
  1543. except KeyError:
  1544. host_template = None
  1545. if not host_template or not host_template.is_template():
  1546. # Maybe the (custom) template is in the backup?
  1547. if not (template_name in restore_info.keys() and
  1548. restore_info[template_name].good_to_go and
  1549. restore_info[template_name].vm.is_template()):
  1550. if self.options.use_default_template and \
  1551. self.app.default_template:
  1552. if vm_info.orig_template is None:
  1553. vm_info.orig_template = template_name
  1554. vm_info.template = self.app.default_template.name
  1555. else:
  1556. vm_info.problems.add(
  1557. self.VMToRestore.MISSING_TEMPLATE)
  1558. # check netvm
  1559. if not vm_info.vm.property_is_default('netvm') and vm_info.netvm:
  1560. netvm_name = vm_info.netvm
  1561. try:
  1562. netvm_on_host = self.app.domains[netvm_name]
  1563. except KeyError:
  1564. netvm_on_host = None
  1565. # No netvm on the host?
  1566. if not ((netvm_on_host is not None)
  1567. and netvm_on_host.provides_network):
  1568. # Maybe the (custom) netvm is in the backup?
  1569. if not (netvm_name in restore_info.keys() and
  1570. restore_info[netvm_name].good_to_go and
  1571. restore_info[netvm_name].vm.provides_network):
  1572. if self.options.use_default_netvm:
  1573. vm_info.vm.netvm = qubes.property.DEFAULT
  1574. elif self.options.use_none_netvm:
  1575. vm_info.netvm = None
  1576. else:
  1577. vm_info.problems.add(self.VMToRestore.MISSING_NETVM)
  1578. # check kernel
  1579. if hasattr(vm_info.vm, 'kernel'):
  1580. installed_kernels = os.listdir(os.path.join(
  1581. qubes.config.qubes_base_dir,
  1582. qubes.config.system_path['qubes_kernels_base_dir']))
  1583. if not vm_info.vm.property_is_default('kernel') \
  1584. and vm_info.vm.kernel \
  1585. and vm_info.vm.kernel not in installed_kernels:
  1586. if self.options.use_default_kernel:
  1587. vm_info.vm.kernel = qubes.property.DEFAULT
  1588. else:
  1589. vm_info.problems.add(self.VMToRestore.MISSING_KERNEL)
  1590. return restore_info
  1591. def _is_vm_included_in_backup_v1(self, check_vm):
  1592. if check_vm.qid == 0:
  1593. return os.path.exists(
  1594. os.path.join(self.backup_location, 'dom0-home'))
  1595. # DisposableVM
  1596. if check_vm.dir_path is None:
  1597. return False
  1598. backup_vm_dir_path = check_vm.dir_path.replace(
  1599. qubes.config.system_path["qubes_base_dir"], self.backup_location)
  1600. if os.path.exists(backup_vm_dir_path):
  1601. return True
  1602. else:
  1603. return False
  1604. @staticmethod
  1605. def _is_vm_included_in_backup_v2(check_vm):
  1606. if 'backup-content' in check_vm.features:
  1607. return check_vm.features['backup-content']
  1608. else:
  1609. return False
  1610. def _find_template_name(self, template):
  1611. if template in self.options.replace_template:
  1612. return self.options.replace_template[template]
  1613. return template
  1614. def _is_vm_included_in_backup(self, vm):
  1615. if self.header_data.version == 1:
  1616. return self._is_vm_included_in_backup_v1(vm)
  1617. elif self.header_data.version in [2, 3, 4]:
  1618. return self._is_vm_included_in_backup_v2(vm)
  1619. else:
  1620. raise qubes.exc.QubesException(
  1621. "Unknown backup format version: {}".format(
  1622. self.header_data.version))
  1623. def get_restore_info(self):
  1624. # Format versions:
  1625. # 1 - Qubes R1, Qubes R2 beta1, beta2
  1626. # 2 - Qubes R2 beta3+
  1627. vms_to_restore = {}
  1628. for vm in self.backup_app.domains:
  1629. if vm.qid == 0:
  1630. # Handle dom0 as special case later
  1631. continue
  1632. if self._is_vm_included_in_backup(vm):
  1633. self.log.debug("{} is included in backup".format(vm.name))
  1634. vms_to_restore[vm.name] = self.VMToRestore(vm)
  1635. if hasattr(vm, 'template'):
  1636. templatevm_name = self._find_template_name(
  1637. vm.template.name)
  1638. vms_to_restore[vm.name].template = templatevm_name
  1639. # Set to None to not confuse QubesVm object from backup
  1640. # collection with host collection (further in clone_attrs).
  1641. vm.netvm = None
  1642. vms_to_restore = self.restore_info_verify(vms_to_restore)
  1643. # ...and dom0 home
  1644. if self.options.dom0_home and \
  1645. self._is_vm_included_in_backup(self.backup_app.domains[0]):
  1646. vm = self.backup_app.domains[0]
  1647. if self.header_data.version == 1:
  1648. subdir = os.listdir(os.path.join(self.backup_location,
  1649. 'dom0-home'))[0]
  1650. else:
  1651. subdir = None
  1652. vms_to_restore['dom0'] = self.Dom0ToRestore(vm, subdir)
  1653. local_user = grp.getgrnam('qubes').gr_mem[0]
  1654. if vms_to_restore['dom0'].username != local_user:
  1655. if not self.options.ignore_username_mismatch:
  1656. vms_to_restore['dom0'].problems.add(
  1657. self.Dom0ToRestore.USERNAME_MISMATCH)
  1658. return vms_to_restore
  1659. @staticmethod
  1660. def get_restore_summary(restore_info):
  1661. fields = {
  1662. "qid": {"func": "vm.qid"},
  1663. "name": {"func": "('[' if vm.is_template() else '')\
  1664. + ('{' if vm.is_netvm() else '')\
  1665. + vm.name \
  1666. + (']' if vm.is_template() else '')\
  1667. + ('}' if vm.is_netvm() else '')"},
  1668. "type": {"func": "'Tpl' if vm.is_template() else \
  1669. 'App' if isinstance(vm, qubes.vm.appvm.AppVM) else \
  1670. vm.__class__.__name__.replace('VM','')"},
  1671. "updbl": {"func": "'Yes' if vm.updateable else ''"},
  1672. "template": {"func": "'n/a' if not hasattr(vm, 'template') is None "
  1673. "else vm_info.template"},
  1674. "netvm": {"func": "'n/a' if vm.is_netvm() and not vm.is_proxyvm() else\
  1675. ('*' if vm.property_is_default('netvm') else '') +\
  1676. vm_info.netvm if vm_info.netvm is not None "
  1677. "else '-'"},
  1678. "label": {"func": "vm.label.name"},
  1679. }
  1680. fields_to_display = ["name", "type", "template", "updbl",
  1681. "netvm", "label"]
  1682. # First calculate the maximum width of each field we want to display
  1683. total_width = 0
  1684. for f in fields_to_display:
  1685. fields[f]["max_width"] = len(f)
  1686. for vm_info in restore_info.values():
  1687. if vm_info.vm:
  1688. # noinspection PyUnusedLocal
  1689. vm = vm_info.vm
  1690. l = len(unicode(eval(fields[f]["func"])))
  1691. if l > fields[f]["max_width"]:
  1692. fields[f]["max_width"] = l
  1693. total_width += fields[f]["max_width"]
  1694. summary = ""
  1695. summary += "The following VMs are included in the backup:\n"
  1696. summary += "\n"
  1697. # Display the header
  1698. for f in fields_to_display:
  1699. # noinspection PyTypeChecker
  1700. fmt = "{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
  1701. summary += fmt.format('-')
  1702. summary += "\n"
  1703. for f in fields_to_display:
  1704. # noinspection PyTypeChecker
  1705. fmt = "{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
  1706. summary += fmt.format(f)
  1707. summary += "\n"
  1708. for f in fields_to_display:
  1709. # noinspection PyTypeChecker
  1710. fmt = "{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
  1711. summary += fmt.format('-')
  1712. summary += "\n"
  1713. for vm_info in restore_info.values():
  1714. assert isinstance(vm_info, BackupRestore.VMToRestore)
  1715. # Skip non-VM here
  1716. if not vm_info.vm:
  1717. continue
  1718. # noinspection PyUnusedLocal
  1719. vm = vm_info.vm
  1720. s = ""
  1721. for f in fields_to_display:
  1722. # noinspection PyTypeChecker
  1723. fmt = "{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
  1724. s += fmt.format(eval(fields[f]["func"]))
  1725. if BackupRestore.VMToRestore.EXCLUDED in vm_info.problems:
  1726. s += " <-- Excluded from restore"
  1727. elif BackupRestore.VMToRestore.ALREADY_EXISTS in vm_info.problems:
  1728. s += " <-- A VM with the same name already exists on the host!"
  1729. elif BackupRestore.VMToRestore.MISSING_TEMPLATE in \
  1730. vm_info.problems:
  1731. s += " <-- No matching template on the host " \
  1732. "or in the backup found!"
  1733. elif BackupRestore.VMToRestore.MISSING_NETVM in \
  1734. vm_info.problems:
  1735. s += " <-- No matching netvm on the host " \
  1736. "or in the backup found!"
  1737. else:
  1738. if vm_info.orig_template:
  1739. s += " <-- Original template was '{}'".format(
  1740. vm_info.orig_template)
  1741. if vm_info.name != vm_info.vm.name:
  1742. s += " <-- Will be renamed to '{}'".format(
  1743. vm_info.name)
  1744. summary += s + "\n"
  1745. if 'dom0' in restore_info.keys():
  1746. s = ""
  1747. for f in fields_to_display:
  1748. # noinspection PyTypeChecker
  1749. fmt = "{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
  1750. if f == "name":
  1751. s += fmt.format("Dom0")
  1752. elif f == "type":
  1753. s += fmt.format("Home")
  1754. else:
  1755. s += fmt.format("")
  1756. if BackupRestore.Dom0ToRestore.USERNAME_MISMATCH in \
  1757. restore_info['dom0'].problems:
  1758. s += " <-- username in backup and dom0 mismatch"
  1759. summary += s + "\n"
  1760. return summary
  1761. def _restore_vm_dir_v1(self, src_dir, dst_dir):
  1762. backup_src_dir = src_dir.replace(
  1763. qubes.config.system_path["qubes_base_dir"], self.backup_location)
  1764. # We prefer to use Linux's cp, because it nicely handles sparse files
  1765. cp_retcode = subprocess.call(
  1766. ["cp", "-rp", "--reflink=auto", backup_src_dir, dst_dir])
  1767. if cp_retcode != 0:
  1768. raise qubes.exc.QubesException(
  1769. "*** Error while copying file {0} to {1}".format(backup_src_dir,
  1770. dst_dir))
  1771. def restore_do(self, restore_info):
  1772. # FIXME handle locking
  1773. # Perform VM restoration in backup order
  1774. vms_dirs = []
  1775. vms_size = 0
  1776. vms = {}
  1777. for vm_info in restore_info.values():
  1778. assert isinstance(vm_info, self.VMToRestore)
  1779. if not vm_info.vm:
  1780. continue
  1781. if not vm_info.good_to_go:
  1782. continue
  1783. vm = vm_info.vm
  1784. if self.header_data.version >= 2:
  1785. if vm.features['backup-size']:
  1786. vms_size += int(vm.features['backup-size'])
  1787. vms_dirs.append(vm.features['backup-path'])
  1788. vms[vm.name] = vm
  1789. if self.header_data.version >= 2:
  1790. if 'dom0' in restore_info.keys() and \
  1791. restore_info['dom0'].good_to_go:
  1792. vms_dirs.append(os.path.dirname(restore_info['dom0'].subdir))
  1793. vms_size += restore_info['dom0'].size
  1794. try:
  1795. self._restore_vm_dirs(vms_dirs=vms_dirs, vms_size=vms_size)
  1796. except qubes.exc.QubesException:
  1797. if self.options.verify_only:
  1798. raise
  1799. else:
  1800. self.log.warning(
  1801. "Some errors occurred during data extraction, "
  1802. "continuing anyway to restore at least some "
  1803. "VMs")
  1804. else:
  1805. if self.options.verify_only:
  1806. self.log.warning(
  1807. "Backup verification not supported for this backup format.")
  1808. if self.options.verify_only:
  1809. shutil.rmtree(self.tmpdir)
  1810. return
  1811. # First load templates, then other VMs
  1812. for vm in sorted(vms.values(), key=lambda x: x.is_template(),
  1813. reverse=True):
  1814. if self.canceled:
  1815. # only break the loop to save qubes.xml
  1816. # with already restored VMs
  1817. break
  1818. self.log.info("-> Restoring {0}...".format(vm.name))
  1819. retcode = subprocess.call(
  1820. ["mkdir", "-p", os.path.dirname(vm.dir_path)])
  1821. if retcode != 0:
  1822. self.log.error("*** Cannot create directory: {0}?!".format(
  1823. vm.dir_path))
  1824. self.log.warning("Skipping VM {}...".format(vm.name))
  1825. continue
  1826. kwargs = {}
  1827. if hasattr(vm, 'template'):
  1828. template = restore_info[vm.name].template
  1829. # handle potentially renamed template
  1830. if template in restore_info \
  1831. and restore_info[template].good_to_go:
  1832. template = restore_info[template].name
  1833. kwargs['template'] = template
  1834. new_vm = None
  1835. vm_name = restore_info[vm.name].name
  1836. try:
  1837. # first only minimal set, later clone_properties
  1838. # will be called
  1839. new_vm = self.app.add_new_vm(
  1840. vm.__class__,
  1841. name=vm_name,
  1842. label=vm.label,
  1843. installed_by_rpm=False,
  1844. **kwargs)
  1845. if os.path.exists(new_vm.dir_path):
  1846. move_to_path = tempfile.mkdtemp('', os.path.basename(
  1847. new_vm.dir_path), os.path.dirname(new_vm.dir_path))
  1848. try:
  1849. os.rename(new_vm.dir_path, move_to_path)
  1850. self.log.warning(
  1851. "*** Directory {} already exists! It has "
  1852. "been moved to {}".format(new_vm.dir_path,
  1853. move_to_path))
  1854. except OSError:
  1855. self.log.error(
  1856. "*** Directory {} already exists and "
  1857. "cannot be moved!".format(new_vm.dir_path))
  1858. self.log.warning("Skipping VM {}...".format(
  1859. vm.name))
  1860. continue
  1861. if self.header_data.version == 1:
  1862. self._restore_vm_dir_v1(vm.dir_path,
  1863. os.path.dirname(new_vm.dir_path))
  1864. else:
  1865. shutil.move(os.path.join(self.tmpdir,
  1866. vm.features['backup-path']),
  1867. new_vm.dir_path)
  1868. new_vm.verify_files()
  1869. except Exception as err:
  1870. self.log.error("ERROR: {0}".format(err))
  1871. self.log.warning("*** Skipping VM: {0}".format(vm.name))
  1872. if new_vm:
  1873. del self.app.domains[new_vm.qid]
  1874. continue
  1875. # remove no longer needed backup metadata
  1876. if 'backup-content' in vm.features:
  1877. del vm.features['backup-content']
  1878. del vm.features['backup-size']
  1879. del vm.features['backup-path']
  1880. try:
  1881. # exclude VM references - handled manually according to
  1882. # restore options
  1883. proplist = [prop for prop in new_vm.property_list()
  1884. if prop.clone and prop.__name__ not in
  1885. ['template', 'netvm', 'dispvm_netvm']]
  1886. new_vm.clone_properties(vm, proplist=proplist)
  1887. except Exception as err:
  1888. self.log.error("ERROR: {0}".format(err))
  1889. self.log.warning("*** Some VM property will not be "
  1890. "restored")
  1891. try:
  1892. new_vm.fire_event('domain-restore')
  1893. except Exception as err:
  1894. self.log.error("ERROR during appmenu restore: "
  1895. "{0}".format(err))
  1896. self.log.warning(
  1897. "*** VM '{0}' will not have appmenus".format(vm.name))
  1898. # Set network dependencies - only non-default netvm setting
  1899. for vm in vms.values():
  1900. vm_info = restore_info[vm.name]
  1901. vm_name = vm_info.name
  1902. try:
  1903. host_vm = self.app.domains[vm_name]
  1904. except KeyError:
  1905. # Failed/skipped VM
  1906. continue
  1907. if not vm.property_is_default('netvm'):
  1908. if vm_info.netvm in restore_info:
  1909. host_vm.netvm = restore_info[vm_info.netvm].name
  1910. else:
  1911. host_vm.netvm = vm_info.netvm
  1912. self.app.save()
  1913. if self.canceled:
  1914. if self.header_data.version >= 2:
  1915. raise BackupCanceledError("Restore canceled",
  1916. tmpdir=self.tmpdir)
  1917. else:
  1918. raise BackupCanceledError("Restore canceled")
  1919. # ... and dom0 home as last step
  1920. if 'dom0' in restore_info.keys() and restore_info['dom0'].good_to_go:
  1921. backup_path = restore_info['dom0'].subdir
  1922. local_user = grp.getgrnam('qubes').gr_mem[0]
  1923. home_dir = pwd.getpwnam(local_user).pw_dir
  1924. if self.header_data.version == 1:
  1925. backup_dom0_home_dir = os.path.join(self.backup_location,
  1926. backup_path)
  1927. else:
  1928. backup_dom0_home_dir = os.path.join(self.tmpdir, backup_path)
  1929. restore_home_backupdir = "home-pre-restore-{0}".format(
  1930. time.strftime("%Y-%m-%d-%H%M%S"))
  1931. self.log.info(
  1932. "Restoring home of user '{0}'...".format(local_user))
  1933. self.log.info(
  1934. "Existing files/dirs backed up in '{0}' dir".format(
  1935. restore_home_backupdir))
  1936. os.mkdir(home_dir + '/' + restore_home_backupdir)
  1937. for f in os.listdir(backup_dom0_home_dir):
  1938. home_file = home_dir + '/' + f
  1939. if os.path.exists(home_file):
  1940. os.rename(home_file,
  1941. home_dir + '/' + restore_home_backupdir + '/' + f)
  1942. if self.header_data.version == 1:
  1943. subprocess.call(
  1944. ["cp", "-nrp", "--reflink=auto",
  1945. backup_dom0_home_dir + '/' + f, home_file])
  1946. elif self.header_data.version >= 2:
  1947. shutil.move(backup_dom0_home_dir + '/' + f, home_file)
  1948. retcode = subprocess.call(['sudo', 'chown', '-R',
  1949. local_user, home_dir])
  1950. if retcode != 0:
  1951. self.log.error("*** Error while setting home directory owner")
  1952. shutil.rmtree(self.tmpdir)
  1953. # vim:sw=4:et: