backup.py 104 KB

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