backup.py 104 KB

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