restore.py 84 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation; either version 2.1 of the License, or
  11. # (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 Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. '''Backup restore module'''
  21. import contextlib
  22. import errno
  23. import fcntl
  24. import functools
  25. import getpass
  26. import grp
  27. import logging
  28. import multiprocessing
  29. from multiprocessing import Queue, Process
  30. import os
  31. import pwd
  32. import re
  33. import shutil
  34. import subprocess
  35. import sys
  36. import tempfile
  37. import termios
  38. import time
  39. import collections
  40. import qubesadmin
  41. import qubesadmin.vm
  42. from qubesadmin.backup import BackupVM
  43. from qubesadmin.backup.core2 import Core2Qubes
  44. from qubesadmin.backup.core3 import Core3Qubes
  45. from qubesadmin.devices import DeviceAssignment
  46. from qubesadmin.exc import QubesException
  47. from qubesadmin.utils import size_to_human
  48. # must be picklable
  49. QUEUE_FINISHED = "!!!FINISHED"
  50. QUEUE_ERROR = "!!!ERROR"
  51. HEADER_FILENAME = 'backup-header'
  52. DEFAULT_CRYPTO_ALGORITHM = 'aes-256-cbc'
  53. # 'scrypt' is not exactly HMAC algorithm, but a tool we use to
  54. # integrity-protect the data
  55. DEFAULT_HMAC_ALGORITHM = 'scrypt'
  56. DEFAULT_COMPRESSION_FILTER = 'gzip'
  57. KNOWN_COMPRESSION_FILTERS = ('gzip', 'bzip2', 'xz')
  58. # lazy loaded
  59. KNOWN_CRYPTO_ALGORITHMS = []
  60. # lazy loaded
  61. KNOWN_HMAC_ALGORITHMS = []
  62. # Maximum size of error message get from process stderr (including VM process)
  63. MAX_STDERR_BYTES = 1024
  64. # header + qubes.xml max size
  65. HEADER_QUBES_XML_MAX_SIZE = 1024 * 1024
  66. # hmac file max size - regardless of backup format version!
  67. HMAC_MAX_SIZE = 4096
  68. BLKSIZE = 512
  69. _re_alphanum = re.compile(r'^[A-Za-z0-9-]*$')
  70. _tar_msg_re = re.compile(r".*#[0-9].*restore_pipe")
  71. _tar_file_size_re = re.compile(r"^[^ ]+ [^ ]+/[^ ]+ *([0-9]+) .*")
  72. class BackupCanceledError(QubesException):
  73. '''Exception raised when backup/restore was cancelled'''
  74. def __init__(self, msg, tmpdir=None):
  75. super().__init__(msg)
  76. self.tmpdir = tmpdir
  77. def init_supported_hmac_and_crypto():
  78. """Collect supported hmac and crypto algorithms.
  79. This calls openssl to list actual supported algos.
  80. """
  81. if not KNOWN_HMAC_ALGORITHMS:
  82. KNOWN_HMAC_ALGORITHMS.extend(get_supported_hmac_algo())
  83. if not KNOWN_CRYPTO_ALGORITHMS:
  84. KNOWN_CRYPTO_ALGORITHMS.extend(get_supported_crypto_algo())
  85. class BackupHeader(object):
  86. '''Structure describing backup-header file included as the first file in
  87. backup archive
  88. '''
  89. Header = collections.namedtuple('Header', ['field', 't', 'validator'])
  90. known_headers = {
  91. 'version': Header(field='version', t=int,
  92. validator=lambda x: 1 <= x <= 4),
  93. 'encrypted': Header(field='encrypted', t=bool,
  94. validator=lambda x: True),
  95. 'compressed': Header(field='compressed', t=bool,
  96. validator=lambda x: True),
  97. 'compression-filter': Header(
  98. field='compression_filter',
  99. t=str,
  100. validator=lambda x: x in KNOWN_COMPRESSION_FILTERS),
  101. 'crypto-algorithm': Header(
  102. field='crypto_algorithm',
  103. t=str,
  104. validator=lambda x: x.lower() in KNOWN_CRYPTO_ALGORITHMS),
  105. 'hmac-algorithm': Header(
  106. field='hmac_algorithm',
  107. t=str,
  108. validator=lambda x: x.lower() in KNOWN_HMAC_ALGORITHMS),
  109. 'backup-id': Header(
  110. field='backup_id',
  111. t=str,
  112. validator=lambda x: not x.startswith('-') and x != ''),
  113. }
  114. def __init__(self,
  115. header_data=None,
  116. version=None,
  117. encrypted=None,
  118. compressed=None,
  119. compression_filter=None,
  120. hmac_algorithm=None,
  121. crypto_algorithm=None,
  122. backup_id=None):
  123. # repeat the list to help code completion...
  124. self.version = version
  125. self.encrypted = encrypted
  126. self.compressed = compressed
  127. # Options introduced in backup format 3+, which always have a header,
  128. # so no need for fallback in function parameter
  129. self.compression_filter = compression_filter
  130. self.hmac_algorithm = hmac_algorithm
  131. self.crypto_algorithm = crypto_algorithm
  132. self.backup_id = backup_id
  133. init_supported_hmac_and_crypto()
  134. if header_data is not None:
  135. self.load(header_data)
  136. def load(self, untrusted_header_text):
  137. """Parse backup header file.
  138. :param untrusted_header_text: header content
  139. :type untrusted_header_text: basestring
  140. .. warning::
  141. This function may be exposed to not yet verified header,
  142. so is security critical.
  143. """
  144. try:
  145. untrusted_header_text = untrusted_header_text.decode('ascii')
  146. except UnicodeDecodeError:
  147. raise QubesException(
  148. "Non-ASCII characters in backup header")
  149. seen = set()
  150. for untrusted_line in untrusted_header_text.splitlines():
  151. if untrusted_line.count('=') != 1:
  152. raise QubesException("Invalid backup header")
  153. key, value = untrusted_line.strip().split('=', 1)
  154. if not _re_alphanum.match(key):
  155. raise QubesException("Invalid backup header (key)")
  156. if key not in self.known_headers:
  157. # Ignoring unknown option
  158. continue
  159. header = self.known_headers[key]
  160. if key in seen:
  161. raise QubesException("Duplicated header line: {}".format(key))
  162. seen.add(key)
  163. if getattr(self, header.field, None) is not None:
  164. # ignore options already set (potentially forced values)
  165. continue
  166. if not _re_alphanum.match(value):
  167. raise QubesException("Invalid backup header (value)")
  168. if header.t is bool:
  169. value = value.lower() in ["1", "true", "yes"]
  170. elif header.t is int:
  171. value = int(value)
  172. elif header.t is str:
  173. pass
  174. else:
  175. raise QubesException("Unrecognized header type")
  176. if not header.validator(value):
  177. if key == 'compression-filter':
  178. raise QubesException(
  179. "Unusual compression filter '{f}' found. Use "
  180. "--compression-filter={f} to use it anyway.".format(
  181. f=value))
  182. raise QubesException("Invalid value for header: {}".format(key))
  183. setattr(self, header.field, value)
  184. self.validate()
  185. def validate(self):
  186. '''Validate header data, according to header version'''
  187. if self.version == 1:
  188. # header not really present
  189. pass
  190. elif self.version in [2, 3, 4]:
  191. expected_attrs = ['version', 'encrypted', 'compressed',
  192. 'hmac_algorithm']
  193. if self.encrypted and self.version < 4:
  194. expected_attrs += ['crypto_algorithm']
  195. if self.version >= 3 and self.compressed:
  196. expected_attrs += ['compression_filter']
  197. if self.version >= 4:
  198. expected_attrs += ['backup_id']
  199. for key in expected_attrs:
  200. if getattr(self, key) is None:
  201. raise QubesException(
  202. "Backup header lack '{}' info".format(key))
  203. else:
  204. raise QubesException(
  205. "Unsupported backup version {}".format(self.version))
  206. def save(self, filename):
  207. '''Save backup header into a file'''
  208. with open(filename, "w") as f_header:
  209. # make sure 'version' is the first key
  210. f_header.write('version={}\n'.format(self.version))
  211. for key, header in self.known_headers.items():
  212. if key == 'version':
  213. continue
  214. attr = header.field
  215. if getattr(self, attr) is None:
  216. continue
  217. f_header.write("{!s}={!s}\n".format(key, getattr(self, attr)))
  218. def launch_proc_with_pty(args, stdin=None, stdout=None, stderr=None, echo=True):
  219. """Similar to pty.fork, but handle stdin/stdout according to parameters
  220. instead of connecting to the pty
  221. :return tuple (subprocess.Popen, pty_master)
  222. """
  223. def set_ctty(ctty_fd, master_fd):
  224. '''Set controlling terminal'''
  225. os.setsid()
  226. os.close(master_fd)
  227. fcntl.ioctl(ctty_fd, termios.TIOCSCTTY, 0)
  228. if not echo:
  229. termios_p = termios.tcgetattr(ctty_fd)
  230. # termios_p.c_lflags
  231. termios_p[3] &= ~termios.ECHO
  232. termios.tcsetattr(ctty_fd, termios.TCSANOW, termios_p)
  233. (pty_master, pty_slave) = os.openpty()
  234. # pylint: disable=subprocess-popen-preexec-fn
  235. p = subprocess.Popen(args, stdin=stdin, stdout=stdout,
  236. stderr=stderr,
  237. preexec_fn=lambda: set_ctty(pty_slave, pty_master))
  238. os.close(pty_slave)
  239. return p, open(pty_master, 'wb+', buffering=0)
  240. def launch_scrypt(action, input_name, output_name, passphrase):
  241. '''
  242. Launch 'scrypt' process, pass passphrase to it and return
  243. subprocess.Popen object.
  244. :param action: 'enc' or 'dec'
  245. :param input_name: input path or '-' for stdin
  246. :param output_name: output path or '-' for stdout
  247. :param passphrase: passphrase
  248. :return: subprocess.Popen object
  249. '''
  250. command_line = ['scrypt', action, '-f', input_name, output_name]
  251. (p, pty) = launch_proc_with_pty(command_line,
  252. stdin=subprocess.PIPE if input_name == '-' else None,
  253. stdout=subprocess.PIPE if output_name == '-' else None,
  254. stderr=subprocess.PIPE,
  255. echo=False)
  256. if action == 'enc':
  257. prompts = (b'Please enter passphrase: ', b'Please confirm passphrase: ')
  258. else:
  259. prompts = (b'Please enter passphrase: ',)
  260. for prompt in prompts:
  261. actual_prompt = p.stderr.read(len(prompt))
  262. if actual_prompt != prompt:
  263. raise QubesException(
  264. 'Unexpected prompt from scrypt: {}'.format(actual_prompt))
  265. pty.write(passphrase.encode('utf-8') + b'\n')
  266. pty.flush()
  267. # save it here, so garbage collector would not close it (which would kill
  268. # the child)
  269. p.pty = pty
  270. return p
  271. def _fix_logging_lock_after_fork():
  272. """
  273. HACK:
  274. This is running in a child process, parent might hold some lock
  275. while fork was called (but will be released only in a parent
  276. process). This specifically applies to a logging module and
  277. results in a deadlock (if one is unlucky). "Fix" this by
  278. reinitialize a lock on all registered logging handlers
  279. just after a fork() call, until fixed upstream:
  280. https://bugs.python.org/issue6721
  281. """
  282. if not hasattr(logging, '_handlerList'):
  283. return
  284. # pylint: disable=protected-access
  285. for handler_ref in logging._handlerList:
  286. handler = handler_ref()
  287. if handler is None:
  288. continue
  289. if handler.lock:
  290. handler.lock = type(handler.lock)()
  291. class ExtractWorker3(Process):
  292. '''Process for handling inner tar layer of backup archive'''
  293. # pylint: disable=too-many-instance-attributes
  294. def __init__(self, queue, base_dir, passphrase, encrypted,
  295. progress_callback, vmproc=None,
  296. compressed=False, crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
  297. compression_filter=None, verify_only=False, handlers=None):
  298. '''Start inner tar extraction worker
  299. The purpose of this class is to process files extracted from outer
  300. archive layer and pass to appropriate handlers. Input files are given
  301. through a queue. Insert :py:obj:`QUEUE_FINISHED` or
  302. :py:obj:`QUEUE_ERROR` to end data processing (either cleanly,
  303. or forcefully).
  304. Handlers are given as a map filename -> (data_func, size_func),
  305. where data_func is called with file-like object to process,
  306. and size_func is called with file size as argument. Note that
  307. data_func and size_func may be called simultaneusly, in a different
  308. processes.
  309. :param multiprocessing.Queue queue: a queue with filenames to
  310. process; those files needs to be given as full path, inside *base_dir*
  311. :param str base_dir: directory where all files to process live
  312. :param str passphrase: passphrase to decrypt the data
  313. :param bool encrypted: is encryption applied?
  314. :param callable progress_callback: report extraction progress
  315. :param subprocess.Popen vmproc: process extracting outer layer,
  316. given here to monitor
  317. it for failures (when it exits with non-zero exit code, inner layer
  318. processing is stopped)
  319. :param bool compressed: is the data compressed?
  320. :param str crypto_algorithm: encryption algorithm, either `scrypt` or an
  321. algorithm supported by openssl
  322. :param str compression_filter: compression program, `gzip` by default
  323. :param bool verify_only: only verify data integrity, do not extract
  324. :param dict handlers: handlers for actual data
  325. '''
  326. super().__init__()
  327. #: queue with files to extract
  328. self.queue = queue
  329. #: paths on the queue are relative to this dir
  330. self.base_dir = base_dir
  331. #: passphrase to decrypt/authenticate data
  332. self.passphrase = passphrase
  333. #: handlers for files; it should be dict filename -> (data_function,
  334. # size_function),
  335. # where data_function will get file-like object as the only argument and
  336. # might be called in a separate process (multiprocessing.Process),
  337. # and size_function will get file size (when known) in bytes
  338. self.handlers = handlers
  339. #: is the backup encrypted?
  340. self.encrypted = encrypted
  341. #: is the backup compressed?
  342. self.compressed = compressed
  343. #: what crypto algorithm is used for encryption?
  344. self.crypto_algorithm = crypto_algorithm
  345. #: only verify integrity, don't extract anything
  346. self.verify_only = verify_only
  347. #: progress
  348. self.blocks_backedup = 0
  349. #: inner tar layer extraction (subprocess.Popen instance)
  350. self.tar2_process = None
  351. #: current inner tar archive name
  352. self.tar2_current_file = None
  353. #: cat process feeding tar2_process
  354. self.tar2_feeder = None
  355. #: decompressor subprocess.Popen instance
  356. self.decompressor_process = None
  357. #: decryptor subprocess.Popen instance
  358. self.decryptor_process = None
  359. #: data import multiprocessing.Process instance
  360. self.import_process = None
  361. #: callback reporting progress to UI
  362. self.progress_callback = progress_callback
  363. #: process (subprocess.Popen instance) feeding the data into
  364. # extraction tool
  365. self.vmproc = vmproc
  366. self.log = logging.getLogger('qubesadmin.backup.extract')
  367. self.stderr_encoding = sys.stderr.encoding or 'utf-8'
  368. self.tar2_stderr = []
  369. self.compression_filter = compression_filter
  370. def collect_tar_output(self):
  371. '''Retrieve tar stderr and handle it appropriately
  372. Log errors, process file size if requested.
  373. This use :py:attr:`tar2_process`.
  374. '''
  375. if not self.tar2_process.stderr:
  376. return
  377. if self.tar2_process.poll() is None:
  378. try:
  379. new_lines = self.tar2_process.stderr \
  380. .read(MAX_STDERR_BYTES).splitlines()
  381. except IOError as e:
  382. if e.errno == errno.EAGAIN:
  383. return
  384. raise
  385. else:
  386. new_lines = self.tar2_process.stderr.readlines()
  387. new_lines = [x.decode(self.stderr_encoding) for x in new_lines]
  388. debug_msg = [msg for msg in new_lines if _tar_msg_re.match(msg)]
  389. self.log.debug('tar2_stderr: %s', '\n'.join(debug_msg))
  390. new_lines = [msg for msg in new_lines if not _tar_msg_re.match(msg)]
  391. self.tar2_stderr += new_lines
  392. def run(self):
  393. try:
  394. _fix_logging_lock_after_fork()
  395. self.__run__()
  396. except Exception:
  397. # Cleanup children
  398. for process in [self.decompressor_process,
  399. self.decryptor_process,
  400. self.tar2_process]:
  401. if process:
  402. try:
  403. process.terminate()
  404. except OSError:
  405. pass
  406. process.wait()
  407. self.log.exception('ERROR')
  408. raise
  409. def handle_dir(self, dirname):
  410. ''' Relocate files in given director when it's already extracted
  411. :param dirname: directory path to handle (relative to backup root),
  412. without trailing slash
  413. '''
  414. for fname, (data_func, size_func) in self.handlers.items():
  415. if not fname.startswith(dirname + '/'):
  416. continue
  417. if not os.path.exists(fname):
  418. # for example firewall.xml
  419. continue
  420. if size_func is not None:
  421. size_func(os.path.getsize(fname))
  422. with open(fname, 'rb') as input_file:
  423. data_func(input_file)
  424. os.unlink(fname)
  425. shutil.rmtree(dirname)
  426. def cleanup_tar2(self, wait=True, terminate=False):
  427. '''Cleanup running :py:attr:`tar2_process`
  428. :param wait: wait for it termination, otherwise method exit early if
  429. process is still running
  430. :param terminate: terminate the process if still running
  431. '''
  432. if self.tar2_process is None:
  433. return
  434. if terminate:
  435. if self.import_process is not None:
  436. self.tar2_process.terminate()
  437. self.import_process.terminate()
  438. if wait:
  439. self.tar2_process.wait()
  440. if self.import_process is not None:
  441. self.import_process.join()
  442. elif self.tar2_process.poll() is None:
  443. return
  444. self.collect_tar_output()
  445. if self.tar2_process.stderr:
  446. self.tar2_process.stderr.close()
  447. if self.tar2_process.returncode != 0:
  448. self.log.error(
  449. "ERROR: unable to extract files for %s, tar "
  450. "output:\n %s",
  451. self.tar2_current_file,
  452. "\n ".join(self.tar2_stderr))
  453. else:
  454. # Finished extracting the tar file
  455. # if that was whole-directory archive, handle
  456. # relocated files now
  457. inner_name = self.tar2_current_file.rsplit('.', 1)[0] \
  458. .replace(self.base_dir + '/', '')
  459. if os.path.basename(inner_name) == '.':
  460. self.handle_dir(
  461. os.path.dirname(inner_name))
  462. self.tar2_current_file = None
  463. self.tar2_process = None
  464. def _data_import_wrapper(self, close_fds, data_func, size_func,
  465. tar2_process):
  466. '''Close not needed file descriptors, handle output size reported
  467. by tar (if needed) then call data_func(tar2_process.stdout).
  468. This is to prevent holding write end of a pipe in subprocess,
  469. preventing EOF transfer.
  470. '''
  471. for fd in close_fds:
  472. if fd in (tar2_process.stdout.fileno(),
  473. tar2_process.stderr.fileno()):
  474. continue
  475. try:
  476. os.close(fd)
  477. except OSError:
  478. pass
  479. # retrieve file size from tar's stderr; warning: we do
  480. # not read data from tar's stdout at this point, it will
  481. # hang if it tries to output file content before
  482. # reporting its size on stderr first
  483. if size_func:
  484. # process lines on stderr until we get file size
  485. # search for first file size reported by tar -
  486. # this is used only when extracting single-file archive, so don't
  487. # bother with checking file name
  488. # Also, this needs to be called before anything is retrieved
  489. # from tar stderr, otherwise the process may deadlock waiting for
  490. # size (at this point nothing is retrieving data from tar stdout
  491. # yet, so it will hang on write() when the output pipe fill up).
  492. while True:
  493. line = tar2_process.stderr.readline()
  494. if not line:
  495. self.log.warning('EOF from tar before got file size info')
  496. break
  497. line = line.decode()
  498. if _tar_msg_re.match(line):
  499. self.log.debug('tar2_stderr: %s', line)
  500. else:
  501. match = _tar_file_size_re.match(line)
  502. if match:
  503. file_size = int(match.groups()[0])
  504. size_func(file_size)
  505. break
  506. self.log.warning(
  507. 'unexpected tar output (no file size report): %s', line)
  508. return data_func(tar2_process.stdout)
  509. def feed_tar2(self, filename, input_pipe):
  510. '''Feed data from *filename* to *input_pipe*
  511. Start a cat process to do that (do not block this process). Cat
  512. subprocess instance will be in :py:attr:`tar2_feeder`
  513. '''
  514. assert self.tar2_feeder is None
  515. self.tar2_feeder = subprocess.Popen(['cat', filename],
  516. stdout=input_pipe)
  517. def check_processes(self, processes):
  518. '''Check if any process failed.
  519. And if so, wait for other relevant processes to cleanup.
  520. '''
  521. run_error = None
  522. for name, proc in processes.items():
  523. if proc is None:
  524. continue
  525. if isinstance(proc, Process):
  526. if not proc.is_alive() and proc.exitcode != 0:
  527. run_error = name
  528. break
  529. elif proc.poll():
  530. run_error = name
  531. break
  532. if run_error:
  533. if run_error == "target":
  534. self.collect_tar_output()
  535. details = "\n".join(self.tar2_stderr)
  536. else:
  537. details = "%s failed" % run_error
  538. if self.decryptor_process:
  539. self.decryptor_process.terminate()
  540. self.decryptor_process.wait()
  541. self.decryptor_process = None
  542. self.log.error('Error while processing \'%s\': %s',
  543. self.tar2_current_file, details)
  544. self.cleanup_tar2(wait=True, terminate=True)
  545. def __run__(self):
  546. self.log.debug("Started sending thread")
  547. self.log.debug("Moving to dir %s", self.base_dir)
  548. os.chdir(self.base_dir)
  549. filename = None
  550. input_pipe = None
  551. for filename in iter(self.queue.get, None):
  552. if filename in (QUEUE_FINISHED, QUEUE_ERROR):
  553. break
  554. assert isinstance(filename, str)
  555. self.log.debug("Extracting file %s", filename)
  556. if filename.endswith('.000'):
  557. # next file
  558. if self.tar2_process is not None:
  559. input_pipe.close()
  560. self.cleanup_tar2(wait=True, terminate=False)
  561. inner_name = filename[:-len('.000')].replace(
  562. self.base_dir + '/', '')
  563. redirect_stdout = None
  564. if os.path.basename(inner_name) == '.':
  565. if (inner_name in self.handlers or
  566. any(x.startswith(os.path.dirname(inner_name) + '/')
  567. for x in self.handlers)):
  568. tar2_cmdline = ['tar',
  569. '-%s' % ("t" if self.verify_only else "x"),
  570. inner_name]
  571. else:
  572. # ignore this directory
  573. tar2_cmdline = None
  574. elif os.path.dirname(inner_name) == "dom0-home":
  575. tar2_cmdline = ['cat']
  576. redirect_stdout = subprocess.PIPE
  577. elif inner_name in self.handlers:
  578. tar2_cmdline = ['tar',
  579. '-%svvO' % ("t" if self.verify_only else "x"),
  580. inner_name]
  581. redirect_stdout = subprocess.PIPE
  582. else:
  583. # no handlers for this file, ignore it
  584. tar2_cmdline = None
  585. if tar2_cmdline is None:
  586. # ignore the file
  587. os.remove(filename)
  588. continue
  589. tar_compress_cmd = None
  590. if self.compressed:
  591. if self.compression_filter:
  592. tar_compress_cmd = self.compression_filter
  593. else:
  594. tar_compress_cmd = DEFAULT_COMPRESSION_FILTER
  595. if os.path.dirname(inner_name) == "dom0-home":
  596. # Replaces 'cat' for compressed dom0-home!
  597. tar2_cmdline = [tar_compress_cmd, "-d"]
  598. else:
  599. tar2_cmdline.insert(-1, "--use-compress-program=%s " %
  600. tar_compress_cmd)
  601. self.log.debug("Running command %s", str(tar2_cmdline))
  602. if self.encrypted:
  603. # Start decrypt
  604. self.decryptor_process = subprocess.Popen(
  605. ["openssl", "enc",
  606. "-d",
  607. "-" + self.crypto_algorithm,
  608. "-pass",
  609. "pass:" + self.passphrase],
  610. stdin=subprocess.PIPE,
  611. stdout=subprocess.PIPE)
  612. self.tar2_process = subprocess.Popen(
  613. tar2_cmdline,
  614. stdin=self.decryptor_process.stdout,
  615. stdout=redirect_stdout,
  616. stderr=subprocess.PIPE)
  617. self.decryptor_process.stdout.close()
  618. input_pipe = self.decryptor_process.stdin
  619. else:
  620. self.tar2_process = subprocess.Popen(
  621. tar2_cmdline,
  622. stdin=subprocess.PIPE,
  623. stdout=redirect_stdout,
  624. stderr=subprocess.PIPE)
  625. input_pipe = self.tar2_process.stdin
  626. self.feed_tar2(filename, input_pipe)
  627. if inner_name in self.handlers:
  628. assert redirect_stdout is subprocess.PIPE
  629. data_func, size_func = self.handlers[inner_name]
  630. self.import_process = multiprocessing.Process(
  631. target=self._data_import_wrapper,
  632. args=([input_pipe.fileno()],
  633. data_func, size_func, self.tar2_process))
  634. self.import_process.start()
  635. self.tar2_process.stdout.close()
  636. self.tar2_stderr = []
  637. elif not self.tar2_process:
  638. # Extracting of the current archive failed, skip to the next
  639. # archive
  640. os.remove(filename)
  641. continue
  642. else:
  643. # os.path.splitext fails to handle 'something/..000'
  644. (basename, ext) = self.tar2_current_file.rsplit('.', 1)
  645. previous_chunk_number = int(ext)
  646. expected_filename = basename + '.%03d' % (
  647. previous_chunk_number+1)
  648. if expected_filename != filename:
  649. self.cleanup_tar2(wait=True, terminate=True)
  650. self.log.error(
  651. 'Unexpected file in archive: %s, expected %s',
  652. filename, expected_filename)
  653. os.remove(filename)
  654. continue
  655. self.log.debug("Releasing next chunk")
  656. self.feed_tar2(filename, input_pipe)
  657. self.tar2_current_file = filename
  658. self.tar2_feeder.wait()
  659. # check if any process failed
  660. processes = {
  661. 'target': self.tar2_feeder,
  662. 'vmproc': self.vmproc,
  663. 'addproc': self.tar2_process,
  664. 'data_import': self.import_process,
  665. 'decryptor': self.decryptor_process,
  666. }
  667. self.check_processes(processes)
  668. self.tar2_feeder = None
  669. if callable(self.progress_callback):
  670. self.progress_callback(os.path.getsize(filename))
  671. # Delete the file as we don't need it anymore
  672. self.log.debug('Removing file %s', filename)
  673. os.remove(filename)
  674. if self.tar2_process is not None:
  675. input_pipe.close()
  676. if filename == QUEUE_ERROR:
  677. if self.decryptor_process:
  678. self.decryptor_process.terminate()
  679. self.decryptor_process.wait()
  680. self.decryptor_process = None
  681. self.cleanup_tar2(terminate=(filename == QUEUE_ERROR))
  682. self.log.debug('Finished extracting thread')
  683. def get_supported_hmac_algo(hmac_algorithm=None):
  684. '''Generate a list of supported hmac algorithms
  685. :param hmac_algorithm: default algorithm, if given, it is placed as a
  686. first element
  687. '''
  688. # Start with provided default
  689. if hmac_algorithm:
  690. yield hmac_algorithm
  691. if hmac_algorithm != 'scrypt':
  692. yield 'scrypt'
  693. proc = subprocess.Popen(
  694. 'openssl list-message-digest-algorithms || '
  695. 'openssl list -digest-algorithms',
  696. shell=True,
  697. stdout=subprocess.PIPE,
  698. stderr=subprocess.DEVNULL)
  699. try:
  700. for algo in proc.stdout.readlines():
  701. algo = algo.decode('ascii')
  702. if '=>' in algo:
  703. continue
  704. yield algo.strip().lower()
  705. finally:
  706. proc.terminate()
  707. proc.wait()
  708. proc.stdout.close()
  709. def get_supported_crypto_algo(crypto_algorithm=None):
  710. '''Generate a list of supported hmac algorithms
  711. :param crypto_algorithm: default algorithm, if given, it is placed as a
  712. first element
  713. '''
  714. # Start with provided default
  715. if crypto_algorithm:
  716. yield crypto_algorithm
  717. if crypto_algorithm != 'scrypt':
  718. yield 'scrypt'
  719. proc = subprocess.Popen(
  720. 'openssl list-cipher-algorithms || '
  721. 'openssl list -cipher-algorithms',
  722. shell=True,
  723. stdout=subprocess.PIPE,
  724. stderr=subprocess.DEVNULL)
  725. try:
  726. for algo in proc.stdout.readlines():
  727. algo = algo.decode('ascii')
  728. if '=>' in algo:
  729. continue
  730. yield algo.strip().lower()
  731. finally:
  732. proc.terminate()
  733. proc.wait()
  734. proc.stdout.close()
  735. class BackupRestoreOptions(object):
  736. '''Options for restore operation'''
  737. # pylint: disable=too-few-public-methods
  738. def __init__(self):
  739. #: use default NetVM if the one referenced in backup do not exists on
  740. # the host
  741. self.use_default_netvm = True
  742. #: set NetVM to "none" if the one referenced in backup do not exists
  743. # on the host
  744. self.use_none_netvm = False
  745. #: set template to default if the one referenced in backup do not
  746. # exists on the host
  747. self.use_default_template = True
  748. #: use default kernel if the one referenced in backup do not exists
  749. # on the host
  750. self.use_default_kernel = True
  751. #: restore dom0 home
  752. self.dom0_home = True
  753. #: restore dom0 home even if username is different
  754. self.ignore_username_mismatch = False
  755. #: do not restore data, only verify backup integrity
  756. self.verify_only = False
  757. #: automatically rename VM during restore, when it would conflict
  758. # with existing one
  759. self.rename_conflicting = True
  760. #: list of VM names to exclude
  761. self.exclude = []
  762. #: restore VMs into selected storage pool
  763. self.override_pool = None
  764. #: ignore size limit calculated from backup metadata
  765. self.ignore_size_limit = False
  766. class BackupRestore(object):
  767. """Usage:
  768. >>> restore_op = BackupRestore(...)
  769. >>> # adjust restore_op.options here
  770. >>> restore_info = restore_op.get_restore_info()
  771. >>> # manipulate restore_info to select VMs to restore here
  772. >>> restore_op.restore_do(restore_info)
  773. """
  774. class VMToRestore(object):
  775. '''Information about a single VM to be restored'''
  776. # pylint: disable=too-few-public-methods
  777. #: VM excluded from restore by user
  778. EXCLUDED = object()
  779. #: VM with such name already exists on the host
  780. ALREADY_EXISTS = object()
  781. #: NetVM used by the VM does not exists on the host
  782. MISSING_NETVM = object()
  783. #: TemplateVM used by the VM does not exists on the host
  784. MISSING_TEMPLATE = object()
  785. #: Kernel used by the VM does not exists on the host
  786. MISSING_KERNEL = object()
  787. def __init__(self, vm):
  788. assert isinstance(vm, BackupVM)
  789. self.vm = vm
  790. self.name = vm.name
  791. self.subdir = vm.backup_path
  792. self.size = vm.size
  793. self.problems = set()
  794. self.template = vm.template
  795. if vm.properties.get('netvm', None):
  796. self.netvm = vm.properties['netvm']
  797. else:
  798. self.netvm = None
  799. self.orig_template = None
  800. self.restored_vm = None
  801. @property
  802. def good_to_go(self):
  803. '''Is the VM ready for restore?'''
  804. return len(self.problems) == 0
  805. class Dom0ToRestore(VMToRestore):
  806. '''Information about dom0 home to restore'''
  807. # pylint: disable=too-few-public-methods
  808. #: backup was performed on system with different dom0 username
  809. USERNAME_MISMATCH = object()
  810. def __init__(self, vm, subdir=None):
  811. super().__init__(vm)
  812. if subdir:
  813. self.subdir = subdir
  814. self.username = os.path.basename(subdir)
  815. def __init__(self, app, backup_location, backup_vm, passphrase,
  816. location_is_service=False, force_compression_filter=None,
  817. tmpdir="/var/tmp"):
  818. super().__init__()
  819. #: qubes.Qubes instance
  820. self.app = app
  821. #: options how the backup should be restored
  822. self.options = BackupRestoreOptions()
  823. #: VM from which backup should be retrieved
  824. self.backup_vm = backup_vm
  825. if backup_vm and backup_vm.name == 'dom0':
  826. self.backup_vm = None
  827. #: backup path, inside VM pointed by :py:attr:`backup_vm`
  828. self.backup_location = backup_location
  829. #: use alternative qrexec service to retrieve backup data, instead of
  830. #: ``qubes.Restore`` with *backup_location* given on stdin
  831. self.location_is_service = location_is_service
  832. #: force using specific application for (de)compression, instead of
  833. #: the one named in the backup header
  834. self.force_compression_filter = force_compression_filter
  835. #: passphrase protecting backup integrity and optionally decryption
  836. self.passphrase = passphrase
  837. #: temporary directory used to extract the data before moving to the
  838. # final location
  839. self.tmpdir = tempfile.mkdtemp(prefix="restore", dir=tmpdir)
  840. #: list of processes (Popen objects) to kill on cancel
  841. self.processes_to_kill_on_cancel = []
  842. #: is the backup operation canceled
  843. self.canceled = False
  844. #: report restore progress, called with one argument - percents of
  845. # data restored
  846. # FIXME: convert to float [0,1]
  847. self.progress_callback = None
  848. self.log = logging.getLogger('qubesadmin.backup')
  849. #: basic information about the backup
  850. self.header_data = self._retrieve_backup_header()
  851. #: VMs included in the backup
  852. self.backup_app = self._process_qubes_xml()
  853. def _start_retrieval_process(self, filelist, limit_count, limit_bytes):
  854. """Retrieve backup stream and extract it to :py:attr:`tmpdir`
  855. :param filelist: list of files to extract; listing directory name
  856. will extract the whole directory; use empty list to extract the whole
  857. archive
  858. :param limit_count: maximum number of files to extract
  859. :param limit_bytes: maximum size of extracted data
  860. :return: a touple of (Popen object of started process, file-like
  861. object for reading extracted files list, file-like object for reading
  862. errors)
  863. """
  864. vmproc = None
  865. if self.backup_vm is not None:
  866. # If APPVM, STDOUT is a PIPE
  867. if self.location_is_service:
  868. vmproc = self.backup_vm.run_service(self.backup_location)
  869. else:
  870. vmproc = self.backup_vm.run_service('qubes.Restore')
  871. vmproc.stdin.write(
  872. (self.backup_location.replace("\r", "").replace("\n",
  873. "") + "\n").encode())
  874. vmproc.stdin.flush()
  875. # Send to tar2qfile the VMs that should be extracted
  876. vmproc.stdin.write((" ".join(filelist) + "\n").encode())
  877. vmproc.stdin.flush()
  878. self.processes_to_kill_on_cancel.append(vmproc)
  879. backup_stdin = vmproc.stdout
  880. if isinstance(self.app, qubesadmin.app.QubesRemote):
  881. qfile_unpacker_path = '/usr/lib/qubes/qfile-unpacker'
  882. else:
  883. qfile_unpacker_path = '/usr/libexec/qubes/qfile-dom0-unpacker'
  884. # keep at least 500M free for decryption of a previous chunk
  885. tar1_command = [qfile_unpacker_path,
  886. str(os.getuid()), self.tmpdir, '-v',
  887. '-w', str(500 * 1024 * 1024)]
  888. else:
  889. backup_stdin = open(self.backup_location, 'rb')
  890. block_size = os.statvfs(self.tmpdir).f_frsize
  891. # slow down the restore if there is too little space in the temp dir
  892. checkpoint_command = \
  893. 'while [ $(stat -f --format=%a "{}") -lt {} ]; ' \
  894. 'do sleep 1; done' \
  895. .format(self.tmpdir, 500 * 1024 * 1024 // block_size)
  896. tar1_command = ['tar',
  897. '-ixv',
  898. '--checkpoint=10000',
  899. '--checkpoint-action=exec=' + checkpoint_command,
  900. '--occurrence=1',
  901. '-C', self.tmpdir] + filelist
  902. tar1_env = os.environ.copy()
  903. tar1_env['UPDATES_MAX_BYTES'] = str(limit_bytes)
  904. tar1_env['UPDATES_MAX_FILES'] = str(limit_count)
  905. self.log.debug("Run command %s", str(tar1_command))
  906. command = subprocess.Popen(
  907. tar1_command,
  908. stdin=backup_stdin,
  909. stdout=vmproc.stdin if vmproc else subprocess.PIPE,
  910. stderr=subprocess.PIPE,
  911. env=tar1_env)
  912. backup_stdin.close()
  913. self.processes_to_kill_on_cancel.append(command)
  914. # qfile-dom0-unpacker output filelist on stderr
  915. # and have stdout connected to the VM), while tar output filelist
  916. # on stdout
  917. if self.backup_vm:
  918. filelist_pipe = command.stderr
  919. # let qfile-dom0-unpacker hold the only open FD to the write end of
  920. # pipe, otherwise qrexec-client will not receive EOF when
  921. # qfile-dom0-unpacker terminates
  922. vmproc.stdin.close()
  923. else:
  924. filelist_pipe = command.stdout
  925. if self.backup_vm:
  926. error_pipe = vmproc.stderr
  927. else:
  928. error_pipe = command.stderr
  929. return command, filelist_pipe, error_pipe
  930. def _verify_hmac(self, filename, hmacfile, algorithm=None):
  931. '''Verify hmac of a file using given algorithm.
  932. If algorithm is not specified, use the one from backup header (
  933. :py:attr:`header_data`).
  934. Raise :py:exc:`QubesException` on failure, return :py:obj:`True` on
  935. success.
  936. 'scrypt' algorithm is supported only for header file; hmac file is
  937. encrypted (and integrity protected) version of plain header.
  938. :param filename: path to file to be verified
  939. :param hmacfile: path to hmac file for *filename*
  940. :param algorithm: override algorithm
  941. '''
  942. def load_hmac(hmac_text):
  943. '''Parse hmac output by openssl.
  944. Return just hmac, without filename and other metadata.
  945. '''
  946. if any(ord(x) not in range(128) for x in hmac_text):
  947. raise QubesException(
  948. "Invalid content of {}".format(hmacfile))
  949. hmac_text = hmac_text.strip().split("=")
  950. if len(hmac_text) > 1:
  951. hmac_text = hmac_text[1].strip()
  952. else:
  953. raise QubesException(
  954. "ERROR: invalid hmac file content")
  955. return hmac_text
  956. if algorithm is None:
  957. algorithm = self.header_data.hmac_algorithm
  958. passphrase = self.passphrase.encode('utf-8')
  959. self.log.debug("Verifying file %s", filename)
  960. if os.stat(os.path.join(self.tmpdir, hmacfile)).st_size > \
  961. HMAC_MAX_SIZE:
  962. raise QubesException('HMAC file {} too large'.format(
  963. hmacfile))
  964. if hmacfile != filename + ".hmac":
  965. raise QubesException(
  966. "ERROR: expected hmac for {}, but got {}".
  967. format(filename, hmacfile))
  968. if algorithm == 'scrypt':
  969. # in case of 'scrypt' _verify_hmac is only used for backup header
  970. assert filename == HEADER_FILENAME
  971. self._verify_and_decrypt(hmacfile, HEADER_FILENAME + '.dec')
  972. f_name = os.path.join(self.tmpdir, filename)
  973. with open(f_name, 'rb') as f_one:
  974. with open(f_name + '.dec', 'rb') as f_two:
  975. if f_one.read() != f_two.read():
  976. raise QubesException(
  977. 'Invalid hmac on {}'.format(filename))
  978. return True
  979. with open(os.path.join(self.tmpdir, filename), 'rb') as f_input:
  980. hmac_proc = subprocess.Popen(
  981. ["openssl", "dgst", "-" + algorithm, "-hmac", passphrase],
  982. stdin=f_input,
  983. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  984. hmac_stdout, hmac_stderr = hmac_proc.communicate()
  985. if hmac_stderr:
  986. raise QubesException(
  987. "ERROR: verify file {0}: {1}".format(filename, hmac_stderr))
  988. self.log.debug("Loading hmac for file %s", filename)
  989. try:
  990. with open(os.path.join(self.tmpdir, hmacfile), 'r',
  991. encoding='ascii') as f_hmac:
  992. hmac = load_hmac(f_hmac.read())
  993. except UnicodeDecodeError as err:
  994. raise QubesException('Cannot load hmac file: ' + str(err))
  995. if hmac and load_hmac(hmac_stdout.decode('ascii')) == hmac:
  996. os.unlink(os.path.join(self.tmpdir, hmacfile))
  997. self.log.debug(
  998. "File verification OK -> Sending file %s", filename)
  999. return True
  1000. raise QubesException(
  1001. "ERROR: invalid hmac for file {0}: {1}. "
  1002. "Is the passphrase correct?".
  1003. format(filename, load_hmac(hmac_stdout.decode('ascii'))))
  1004. def _verify_and_decrypt(self, filename, output=None):
  1005. '''Handle scrypt-wrapped file
  1006. Decrypt the file, and verify its integrity - both tasks handled by
  1007. 'scrypt' tool. Filename (without extension) is also validated.
  1008. :param filename: Input file name (relative to :py:attr:`tmpdir`),
  1009. needs to have `.enc` or `.hmac` extension
  1010. :param output: Output file name (relative to :py:attr:`tmpdir`),
  1011. use :py:obj:`None` to use *filename* without extension
  1012. :return: *filename* without extension
  1013. '''
  1014. assert filename.endswith('.enc') or filename.endswith('.hmac')
  1015. fullname = os.path.join(self.tmpdir, filename)
  1016. (origname, _) = os.path.splitext(filename)
  1017. if output:
  1018. fulloutput = os.path.join(self.tmpdir, output)
  1019. else:
  1020. fulloutput = os.path.join(self.tmpdir, origname)
  1021. if origname == HEADER_FILENAME:
  1022. passphrase = u'{filename}!{passphrase}'.format(
  1023. filename=origname,
  1024. passphrase=self.passphrase)
  1025. else:
  1026. passphrase = u'{backup_id}!{filename}!{passphrase}'.format(
  1027. backup_id=self.header_data.backup_id,
  1028. filename=origname,
  1029. passphrase=self.passphrase)
  1030. try:
  1031. p = launch_scrypt('dec', fullname, fulloutput, passphrase)
  1032. except OSError as err:
  1033. raise QubesException('failed to decrypt {}: {!s}'.format(
  1034. fullname, err))
  1035. (_, stderr) = p.communicate()
  1036. if hasattr(p, 'pty'):
  1037. p.pty.close()
  1038. if p.returncode != 0:
  1039. os.unlink(fulloutput)
  1040. raise QubesException('failed to decrypt {}: {}'.format(
  1041. fullname, stderr))
  1042. # encrypted file is no longer needed
  1043. os.unlink(fullname)
  1044. return origname
  1045. def _retrieve_backup_header_files(self, files, allow_none=False):
  1046. '''Retrieve backup header.
  1047. Start retrieval process (possibly involving network access from
  1048. another VM). Returns a collection of retrieved file paths.
  1049. '''
  1050. (retrieve_proc, filelist_pipe, error_pipe) = \
  1051. self._start_retrieval_process(
  1052. files, len(files), 1024 * 1024)
  1053. filelist = filelist_pipe.read()
  1054. filelist_pipe.close()
  1055. retrieve_proc_returncode = retrieve_proc.wait()
  1056. if retrieve_proc in self.processes_to_kill_on_cancel:
  1057. self.processes_to_kill_on_cancel.remove(retrieve_proc)
  1058. extract_stderr = error_pipe.read(MAX_STDERR_BYTES)
  1059. error_pipe.close()
  1060. # wait for other processes (if any)
  1061. for proc in self.processes_to_kill_on_cancel:
  1062. if proc.wait() != 0:
  1063. raise QubesException(
  1064. "Backup header retrieval failed (exit code {})".format(
  1065. proc.wait())
  1066. )
  1067. if retrieve_proc_returncode != 0:
  1068. if not filelist and 'Not found in archive' in extract_stderr:
  1069. if allow_none:
  1070. return None
  1071. raise QubesException(
  1072. "unable to read the qubes backup file {0} ({1}): {2}".
  1073. format(
  1074. self.backup_location,
  1075. retrieve_proc.wait(),
  1076. extract_stderr
  1077. ))
  1078. actual_files = filelist.decode('ascii').splitlines()
  1079. if sorted(actual_files) != sorted(files):
  1080. raise QubesException(
  1081. 'unexpected files in archive: got {!r}, expected {!r}'.format(
  1082. actual_files, files
  1083. ))
  1084. for fname in files:
  1085. if not os.path.exists(os.path.join(self.tmpdir, fname)):
  1086. if allow_none:
  1087. return None
  1088. raise QubesException(
  1089. 'Unable to retrieve file {} from backup {}: {}'.format(
  1090. fname, self.backup_location, extract_stderr
  1091. )
  1092. )
  1093. return files
  1094. def _retrieve_backup_header(self):
  1095. """Retrieve backup header and qubes.xml. Only backup header is
  1096. analyzed, qubes.xml is left as-is
  1097. (not even verified/decrypted/uncompressed)
  1098. :return header_data
  1099. :rtype :py:class:`BackupHeader`
  1100. """
  1101. if not self.backup_vm and os.path.exists(
  1102. os.path.join(self.backup_location, 'qubes.xml')):
  1103. # backup format version 1 doesn't have header
  1104. header_data = BackupHeader()
  1105. header_data.version = 1
  1106. return header_data
  1107. header_files = self._retrieve_backup_header_files(
  1108. ['backup-header', 'backup-header.hmac'], allow_none=True)
  1109. if not header_files:
  1110. # R2-Beta3 didn't have backup header, so if none is found,
  1111. # assume it's version=2 and use values present at that time
  1112. header_data = BackupHeader(
  1113. version=2,
  1114. # place explicitly this value, because it is what format_version
  1115. # 2 have
  1116. hmac_algorithm='SHA1',
  1117. crypto_algorithm='aes-256-cbc',
  1118. # TODO: set encrypted to something...
  1119. )
  1120. else:
  1121. filename = HEADER_FILENAME
  1122. hmacfile = HEADER_FILENAME + '.hmac'
  1123. self.log.debug("Got backup header and hmac: %s, %s",
  1124. filename, hmacfile)
  1125. file_ok = False
  1126. hmac_algorithm = DEFAULT_HMAC_ALGORITHM
  1127. for hmac_algo in get_supported_hmac_algo(hmac_algorithm):
  1128. try:
  1129. if self._verify_hmac(filename, hmacfile, hmac_algo):
  1130. file_ok = True
  1131. break
  1132. except QubesException as err:
  1133. self.log.debug(
  1134. 'Failed to verify %s using %s: %r',
  1135. hmacfile, hmac_algo, err)
  1136. # Ignore exception here, try the next algo
  1137. if not file_ok:
  1138. raise QubesException(
  1139. "Corrupted backup header (hmac verification "
  1140. "failed). Is the password correct?")
  1141. filename = os.path.join(self.tmpdir, filename)
  1142. with open(filename, 'rb') as f_header:
  1143. header_data = BackupHeader(
  1144. f_header.read(),
  1145. compression_filter=self.force_compression_filter)
  1146. os.unlink(filename)
  1147. return header_data
  1148. def _start_inner_extraction_worker(self, queue, handlers):
  1149. """Start a worker process, extracting inner layer of bacup archive,
  1150. extract them to :py:attr:`tmpdir`.
  1151. End the data by pushing QUEUE_FINISHED or QUEUE_ERROR to the queue.
  1152. :param queue :py:class:`Queue` object to handle files from
  1153. """
  1154. # Setup worker to extract encrypted data chunks to the restore dirs
  1155. # Create the process here to pass it options extracted from
  1156. # backup header
  1157. extractor_params = {
  1158. 'queue': queue,
  1159. 'base_dir': self.tmpdir,
  1160. 'passphrase': self.passphrase,
  1161. 'encrypted': self.header_data.encrypted,
  1162. 'compressed': self.header_data.compressed,
  1163. 'crypto_algorithm': self.header_data.crypto_algorithm,
  1164. 'verify_only': self.options.verify_only,
  1165. 'progress_callback': self.progress_callback,
  1166. 'handlers': handlers,
  1167. }
  1168. self.log.debug(
  1169. 'Starting extraction worker in %s, file handlers map: %s',
  1170. self.tmpdir, repr(handlers))
  1171. format_version = self.header_data.version
  1172. if format_version in [3, 4]:
  1173. extractor_params['compression_filter'] = \
  1174. self.header_data.compression_filter
  1175. if format_version == 4:
  1176. # encryption already handled
  1177. extractor_params['encrypted'] = False
  1178. extract_proc = ExtractWorker3(**extractor_params)
  1179. else:
  1180. raise NotImplementedError(
  1181. "Backup format version %d not supported" % format_version)
  1182. extract_proc.start()
  1183. return extract_proc
  1184. @staticmethod
  1185. def _save_qubes_xml(path, stream):
  1186. '''Handler for qubes.xml.000 content - just save the data to a file'''
  1187. with open(path, 'wb') as f_qubesxml:
  1188. f_qubesxml.write(stream.read())
  1189. def _process_qubes_xml(self):
  1190. """Verify, unpack and load qubes.xml. Possibly convert its format if
  1191. necessary. It expect that :py:attr:`header_data` is already populated,
  1192. and :py:meth:`retrieve_backup_header` was called.
  1193. """
  1194. if self.header_data.version == 1:
  1195. raise NotImplementedError('Backup format version 1 not supported')
  1196. if self.header_data.version in [2, 3]:
  1197. self._retrieve_backup_header_files(
  1198. ['qubes.xml.000', 'qubes.xml.000.hmac'])
  1199. self._verify_hmac("qubes.xml.000", "qubes.xml.000.hmac")
  1200. else:
  1201. self._retrieve_backup_header_files(['qubes.xml.000.enc'])
  1202. self._verify_and_decrypt('qubes.xml.000.enc')
  1203. queue = Queue()
  1204. queue.put("qubes.xml.000")
  1205. queue.put(QUEUE_FINISHED)
  1206. qubes_xml_path = os.path.join(self.tmpdir, 'qubes-restored.xml')
  1207. handlers = {
  1208. 'qubes.xml': (
  1209. functools.partial(self._save_qubes_xml, qubes_xml_path),
  1210. None)
  1211. }
  1212. extract_proc = self._start_inner_extraction_worker(queue, handlers)
  1213. extract_proc.join()
  1214. if extract_proc.exitcode != 0:
  1215. raise QubesException(
  1216. "unable to extract the qubes backup. "
  1217. "Check extracting process errors.")
  1218. if self.header_data.version in [2, 3]:
  1219. backup_app = Core2Qubes(qubes_xml_path)
  1220. elif self.header_data.version in [4]:
  1221. backup_app = Core3Qubes(qubes_xml_path)
  1222. else:
  1223. raise QubesException(
  1224. 'Unsupported qubes.xml format version: {}'.format(
  1225. self.header_data.version))
  1226. # Not needed anymore - all the data stored in backup_app
  1227. os.unlink(qubes_xml_path)
  1228. return backup_app
  1229. def _restore_vm_data(self, vms_dirs, vms_size, handlers):
  1230. '''Restore data of VMs
  1231. :param vms_dirs: list of directories to extract (skip others)
  1232. :param vms_size: expected size (abort if source stream exceed this
  1233. value)
  1234. :param handlers: handlers for restored files - see
  1235. :py:class:`ExtractWorker3` for details
  1236. '''
  1237. # Currently each VM consists of at most 7 archives (count
  1238. # file_to_backup calls in backup_prepare()), but add some safety
  1239. # margin for further extensions. Each archive is divided into 100MB
  1240. # chunks. Additionally each file have own hmac file. So assume upper
  1241. # limit as 2*(10*COUNT_OF_VMS+TOTAL_SIZE/100MB)
  1242. limit_count = str(2 * (10 * len(vms_dirs) +
  1243. int(vms_size / (100 * 1024 * 1024))))
  1244. if self.options.ignore_size_limit:
  1245. limit_count = '0'
  1246. vms_size = 0
  1247. self.log.debug("Working in temporary dir: %s", self.tmpdir)
  1248. self.log.info("Extracting data: %s to restore", size_to_human(vms_size))
  1249. # retrieve backup from the backup stream (either VM, or dom0 file)
  1250. (retrieve_proc, filelist_pipe, error_pipe) = \
  1251. self._start_retrieval_process(
  1252. vms_dirs, limit_count, vms_size)
  1253. to_extract = Queue()
  1254. # extract data retrieved by retrieve_proc
  1255. extract_proc = self._start_inner_extraction_worker(
  1256. to_extract, handlers)
  1257. try:
  1258. filename = None
  1259. hmacfile = None
  1260. nextfile = None
  1261. while True:
  1262. if self.canceled:
  1263. break
  1264. if not extract_proc.is_alive():
  1265. retrieve_proc.terminate()
  1266. retrieve_proc.wait()
  1267. if retrieve_proc in self.processes_to_kill_on_cancel:
  1268. self.processes_to_kill_on_cancel.remove(retrieve_proc)
  1269. # wait for other processes (if any)
  1270. for proc in self.processes_to_kill_on_cancel:
  1271. proc.wait()
  1272. break
  1273. if nextfile is not None:
  1274. filename = nextfile
  1275. else:
  1276. filename = filelist_pipe.readline().decode('ascii').strip()
  1277. self.log.debug("Getting new file: %s", filename)
  1278. if not filename or filename == "EOF":
  1279. break
  1280. # if reading archive directly with tar, wait for next filename -
  1281. # tar prints filename before processing it, so wait for
  1282. # the next one to be sure that whole file was extracted
  1283. if not self.backup_vm:
  1284. nextfile = filelist_pipe.readline().decode('ascii').strip()
  1285. if self.header_data.version in [2, 3]:
  1286. if not self.backup_vm:
  1287. hmacfile = nextfile
  1288. nextfile = filelist_pipe.readline().\
  1289. decode('ascii').strip()
  1290. else:
  1291. hmacfile = filelist_pipe.readline().\
  1292. decode('ascii').strip()
  1293. if self.canceled:
  1294. break
  1295. self.log.debug("Getting hmac: %s", hmacfile)
  1296. if not hmacfile or hmacfile == "EOF":
  1297. # Premature end of archive, either of tar1_command or
  1298. # vmproc exited with error
  1299. break
  1300. else: # self.header_data.version == 4
  1301. if not filename.endswith('.enc'):
  1302. raise qubesadmin.exc.QubesException(
  1303. 'Invalid file extension found in archive: {}'.
  1304. format(filename))
  1305. if not any(filename.startswith(x) for x in vms_dirs):
  1306. self.log.debug("Ignoring VM not selected for restore")
  1307. os.unlink(os.path.join(self.tmpdir, filename))
  1308. if hmacfile:
  1309. os.unlink(os.path.join(self.tmpdir, hmacfile))
  1310. continue
  1311. if self.header_data.version in [2, 3]:
  1312. self._verify_hmac(filename, hmacfile)
  1313. else:
  1314. # _verify_and_decrypt will write output to a file with
  1315. # '.enc' extension cut off. This is safe because:
  1316. # - `scrypt` tool will override output, so if the file was
  1317. # already there (received from the VM), it will be removed
  1318. # - incoming archive extraction will refuse to override
  1319. # existing file, so if `scrypt` already created one,
  1320. # it can not be manipulated by the VM
  1321. # - when the file is retrieved from the VM, it appears at
  1322. # the final form - if it's visible, VM have no longer
  1323. # influence over its content
  1324. #
  1325. # This all means that if the file was correctly verified
  1326. # + decrypted, we will surely access the right file
  1327. filename = self._verify_and_decrypt(filename)
  1328. if not self.options.verify_only:
  1329. to_extract.put(os.path.join(self.tmpdir, filename))
  1330. else:
  1331. os.unlink(os.path.join(self.tmpdir, filename))
  1332. if self.canceled:
  1333. raise BackupCanceledError("Restore canceled",
  1334. tmpdir=self.tmpdir)
  1335. if retrieve_proc.wait() != 0:
  1336. if retrieve_proc.returncode == errno.EDQUOT:
  1337. raise QubesException(
  1338. 'retrieved backup size exceed expected size, if you '
  1339. 'believe this is ok, use --ignore-size-limit option')
  1340. raise QubesException(
  1341. "unable to read the qubes backup file {} ({}): {}"
  1342. .format(self.backup_location,
  1343. retrieve_proc.returncode, error_pipe.read(
  1344. MAX_STDERR_BYTES)))
  1345. # wait for other processes (if any)
  1346. for proc in self.processes_to_kill_on_cancel:
  1347. proc.wait()
  1348. if proc.returncode != 0:
  1349. raise QubesException(
  1350. "Backup completed, "
  1351. "but VM sending it reported an error (exit code {})".
  1352. format(proc.returncode))
  1353. if filename and filename != "EOF":
  1354. raise QubesException(
  1355. "Premature end of archive, the last file was %s" % filename)
  1356. except:
  1357. with contextlib.suppress(ProcessLookupError):
  1358. retrieve_proc.terminate()
  1359. retrieve_proc.wait()
  1360. to_extract.put(QUEUE_ERROR)
  1361. extract_proc.join()
  1362. raise
  1363. else:
  1364. to_extract.put(QUEUE_FINISHED)
  1365. finally:
  1366. error_pipe.close()
  1367. filelist_pipe.close()
  1368. self.log.debug("Waiting for the extraction process to finish...")
  1369. extract_proc.join()
  1370. self.log.debug("Extraction process finished with code: %s",
  1371. extract_proc.exitcode)
  1372. if extract_proc.exitcode != 0:
  1373. raise QubesException(
  1374. "unable to extract the qubes backup. "
  1375. "Check extracting process errors.")
  1376. def new_name_for_conflicting_vm(self, orig_name, restore_info):
  1377. '''Generate new name for conflicting VM
  1378. Add a number suffix, until the name is unique. If no unique name can
  1379. be found using this strategy, return :py:obj:`None`
  1380. '''
  1381. number = 1
  1382. if len(orig_name) > 29:
  1383. orig_name = orig_name[0:29]
  1384. new_name = orig_name
  1385. while (new_name in restore_info.keys() or
  1386. new_name in [x.name for x in restore_info.values()] or
  1387. new_name in self.app.domains):
  1388. new_name = str('{}{}'.format(orig_name, number))
  1389. number += 1
  1390. if number == 100:
  1391. # give up
  1392. return None
  1393. return new_name
  1394. def restore_info_verify(self, restore_info):
  1395. '''Verify restore info - validate VM dependencies, name conflicts
  1396. etc.
  1397. '''
  1398. for vm in restore_info.keys():
  1399. if vm in ['dom0']:
  1400. continue
  1401. vm_info = restore_info[vm]
  1402. assert isinstance(vm_info, self.VMToRestore)
  1403. vm_info.problems.clear()
  1404. if vm in self.options.exclude:
  1405. vm_info.problems.add(self.VMToRestore.EXCLUDED)
  1406. if not self.options.verify_only and \
  1407. vm_info.name in self.app.domains:
  1408. if self.options.rename_conflicting:
  1409. new_name = self.new_name_for_conflicting_vm(
  1410. vm, restore_info
  1411. )
  1412. if new_name is not None:
  1413. vm_info.name = new_name
  1414. else:
  1415. vm_info.problems.add(self.VMToRestore.ALREADY_EXISTS)
  1416. else:
  1417. vm_info.problems.add(self.VMToRestore.ALREADY_EXISTS)
  1418. # check template
  1419. if vm_info.template:
  1420. present_on_host = False
  1421. if vm_info.template in self.app.domains:
  1422. host_tpl = self.app.domains[vm_info.template]
  1423. if vm_info.vm.klass == 'DispVM':
  1424. present_on_host = (
  1425. getattr(host_tpl, 'template_for_dispvms', False))
  1426. else:
  1427. present_on_host = host_tpl.klass == 'TemplateVM'
  1428. present_in_backup = False
  1429. if vm_info.template in restore_info:
  1430. bak_tpl = restore_info[vm_info.template]
  1431. if bak_tpl.good_to_go:
  1432. if vm_info.vm.klass == 'DispVM':
  1433. present_in_backup = (
  1434. bak_tpl.vm.properties.get(
  1435. 'template_for_dispvms', False))
  1436. else:
  1437. present_in_backup = (
  1438. bak_tpl.vm.klass == 'TemplateVM')
  1439. self.log.debug(
  1440. "vm=%s template=%s on_host=%s in_backup=%s",
  1441. vm_info.name, vm_info.template,
  1442. present_on_host, present_in_backup)
  1443. if not present_on_host and not present_in_backup:
  1444. if vm_info.vm.klass == 'DispVM':
  1445. default_template = self.app.default_dispvm
  1446. else:
  1447. default_template = self.app.default_template
  1448. if (self.options.use_default_template
  1449. and default_template is not None):
  1450. if vm_info.orig_template is None:
  1451. vm_info.orig_template = vm_info.template
  1452. vm_info.template = default_template.name
  1453. self.log.debug(
  1454. "vm=%s orig_template=%s -> default_template=%s",
  1455. vm_info.name, vm_info.orig_template,
  1456. default_template.name)
  1457. else:
  1458. vm_info.problems.add(self.VMToRestore.MISSING_TEMPLATE)
  1459. # check netvm
  1460. if vm_info.vm.properties.get('netvm', None) is not None:
  1461. netvm_name = vm_info.netvm
  1462. try:
  1463. netvm_on_host = self.app.domains[netvm_name]
  1464. except KeyError:
  1465. netvm_on_host = None
  1466. present_on_host = (netvm_on_host is not None
  1467. and netvm_on_host.provides_network)
  1468. present_in_backup = (netvm_name in restore_info.keys() and
  1469. restore_info[netvm_name].good_to_go and
  1470. restore_info[netvm_name].vm.properties.get(
  1471. 'provides_network', False))
  1472. if not present_on_host and not present_in_backup:
  1473. if self.options.use_default_netvm:
  1474. del vm_info.vm.properties['netvm']
  1475. elif self.options.use_none_netvm:
  1476. vm_info.netvm = None
  1477. else:
  1478. vm_info.problems.add(self.VMToRestore.MISSING_NETVM)
  1479. return restore_info
  1480. def get_restore_info(self):
  1481. '''Get restore info
  1482. Return information about what is included in the backup.
  1483. That dictionary can be adjusted to select what VM should be restore.
  1484. '''
  1485. # Format versions:
  1486. # 1 - Qubes R1, Qubes R2 beta1, beta2
  1487. # 2 - Qubes R2 beta3+
  1488. # 3 - Qubes R2+
  1489. # 4 - Qubes R4+
  1490. vms_to_restore = {}
  1491. for vm in self.backup_app.domains.values():
  1492. if vm.klass == 'AdminVM':
  1493. # Handle dom0 as special case later
  1494. continue
  1495. if vm.included_in_backup:
  1496. self.log.debug("%s is included in backup", vm.name)
  1497. vms_to_restore[vm.name] = self.VMToRestore(vm)
  1498. if vm.template is not None:
  1499. templatevm_name = vm.template
  1500. vms_to_restore[vm.name].template = templatevm_name
  1501. vms_to_restore = self.restore_info_verify(vms_to_restore)
  1502. # ...and dom0 home
  1503. if self.options.dom0_home and \
  1504. self.backup_app.domains['dom0'].included_in_backup:
  1505. vm = self.backup_app.domains['dom0']
  1506. vms_to_restore['dom0'] = self.Dom0ToRestore(vm,
  1507. self.backup_app.domains['dom0'].backup_path)
  1508. try:
  1509. local_user = grp.getgrnam('qubes').gr_mem[0]
  1510. except KeyError:
  1511. # if no qubes group is present, assume username matches
  1512. local_user = vms_to_restore['dom0'].username
  1513. if vms_to_restore['dom0'].username != local_user:
  1514. if not self.options.ignore_username_mismatch:
  1515. vms_to_restore['dom0'].problems.add(
  1516. self.Dom0ToRestore.USERNAME_MISMATCH)
  1517. return vms_to_restore
  1518. @staticmethod
  1519. def get_restore_summary(restore_info):
  1520. '''Return a ASCII formatted table with restore info summary'''
  1521. fields = {
  1522. "name": {'func': lambda vm: vm.name},
  1523. "type": {'func': lambda vm: vm.klass},
  1524. "template": {'func': lambda vm:
  1525. 'n/a' if vm.template is None else vm.template},
  1526. "netvm": {'func': lambda vm:
  1527. '(default)' if 'netvm' not in vm.properties else
  1528. '-' if vm.properties['netvm'] is None else
  1529. vm.properties['netvm']},
  1530. "label": {'func': lambda vm: vm.label},
  1531. }
  1532. fields_to_display = ['name', 'type', 'template',
  1533. 'netvm', 'label']
  1534. # First calculate the maximum width of each field we want to display
  1535. total_width = 0
  1536. for field in fields_to_display:
  1537. fields[field]['max_width'] = len(field)
  1538. for vm_info in restore_info.values():
  1539. if vm_info.vm:
  1540. # noinspection PyUnusedLocal
  1541. field_len = len(str(fields[field]["func"](vm_info.vm)))
  1542. if field_len > fields[field]['max_width']:
  1543. fields[field]['max_width'] = field_len
  1544. total_width += fields[field]['max_width']
  1545. summary = ""
  1546. summary += "The following VMs are included in the backup:\n"
  1547. summary += "\n"
  1548. # Display the header
  1549. for field in fields_to_display:
  1550. # noinspection PyTypeChecker
  1551. fmt = "{{0:-^{0}}}-+".format(fields[field]["max_width"] + 1)
  1552. summary += fmt.format('-')
  1553. summary += "\n"
  1554. for field in fields_to_display:
  1555. # noinspection PyTypeChecker
  1556. fmt = "{{0:>{0}}} |".format(fields[field]["max_width"] + 1)
  1557. summary += fmt.format(field)
  1558. summary += "\n"
  1559. for field in fields_to_display:
  1560. # noinspection PyTypeChecker
  1561. fmt = "{{0:-^{0}}}-+".format(fields[field]["max_width"] + 1)
  1562. summary += fmt.format('-')
  1563. summary += "\n"
  1564. for vm_info in restore_info.values():
  1565. assert isinstance(vm_info, BackupRestore.VMToRestore)
  1566. # Skip non-VM here
  1567. if not vm_info.vm:
  1568. continue
  1569. # noinspection PyUnusedLocal
  1570. summary_line = ""
  1571. for field in fields_to_display:
  1572. # noinspection PyTypeChecker
  1573. fmt = "{{0:>{0}}} |".format(fields[field]["max_width"] + 1)
  1574. summary_line += fmt.format(fields[field]["func"](vm_info.vm))
  1575. if BackupRestore.VMToRestore.EXCLUDED in vm_info.problems:
  1576. summary_line += " <-- Excluded from restore"
  1577. elif BackupRestore.VMToRestore.ALREADY_EXISTS in vm_info.problems:
  1578. summary_line += \
  1579. " <-- A VM with the same name already exists on the host!"
  1580. elif BackupRestore.VMToRestore.MISSING_TEMPLATE in \
  1581. vm_info.problems:
  1582. summary_line += " <-- No matching template on the host " \
  1583. "or in the backup found!"
  1584. elif BackupRestore.VMToRestore.MISSING_NETVM in \
  1585. vm_info.problems:
  1586. summary_line += " <-- No matching netvm on the host " \
  1587. "or in the backup found!"
  1588. elif vm_info.name == "dom0" and \
  1589. BackupRestore.Dom0ToRestore.USERNAME_MISMATCH in \
  1590. restore_info['dom0'].problems:
  1591. summary_line += " <-- username in backup and dom0 mismatch"
  1592. else:
  1593. if vm_info.template != vm_info.vm.template:
  1594. summary_line += " <-- Template change to '{}'".format(
  1595. vm_info.template)
  1596. if vm_info.name != vm_info.vm.name:
  1597. summary_line += " <-- Will be renamed to '{}'".format(
  1598. vm_info.name)
  1599. summary += summary_line + "\n"
  1600. return summary
  1601. @staticmethod
  1602. def _templates_first(vms):
  1603. '''Sort templates before other VM types'''
  1604. def key_function(instance):
  1605. '''Key function for :py:func:`sorted`'''
  1606. if isinstance(instance, BackupVM):
  1607. if instance.klass == 'TemplateVM':
  1608. return 0
  1609. if instance.properties.get('template_for_dispvms', False):
  1610. return 1
  1611. return 2
  1612. if hasattr(instance, 'vm'):
  1613. return key_function(instance.vm)
  1614. return 9
  1615. return sorted(vms, key=key_function)
  1616. def _handle_dom0(self, stream):
  1617. '''Extract dom0 home'''
  1618. try:
  1619. local_user = grp.getgrnam('qubes').gr_mem[0]
  1620. home_dir = pwd.getpwnam(local_user).pw_dir
  1621. except KeyError:
  1622. home_dir = os.path.expanduser('~')
  1623. local_user = getpass.getuser()
  1624. restore_home_backupdir = "home-restore-{0}".format(
  1625. time.strftime("%Y-%m-%d-%H%M%S"))
  1626. self.log.info("Restoring home of user '%s' to '%s' directory...",
  1627. local_user, restore_home_backupdir)
  1628. os.mkdir(os.path.join(home_dir, restore_home_backupdir))
  1629. tar3_cmdline = ['tar', '-C',
  1630. os.path.join(home_dir, restore_home_backupdir), '-x']
  1631. retcode = subprocess.call(tar3_cmdline, stdin=stream)
  1632. if retcode != 0:
  1633. raise QubesException("Inner tar error for dom0-home")
  1634. retcode = subprocess.call(['sudo', 'chown', '-R',
  1635. local_user, os.path.join(home_dir, restore_home_backupdir)])
  1636. if retcode != 0:
  1637. self.log.error("*** Error while setting restore directory owner")
  1638. def _handle_appmenus_list(self, vm, stream):
  1639. '''Handle whitelisted-appmenus.list file'''
  1640. try:
  1641. subprocess.check_call(
  1642. ['qvm-appmenus', '--set-whitelist=-', vm.name],
  1643. stdin=stream)
  1644. except (subprocess.CalledProcessError, FileNotFoundError):
  1645. self.log.error('Failed to set application list for %s', vm.name)
  1646. def _handle_volume_data(self, vm, volume, stream):
  1647. '''Wrap volume data import with logging'''
  1648. try:
  1649. volume.import_data(stream)
  1650. except Exception as err: # pylint: disable=broad-except
  1651. self.log.error('Failed to restore volume %s of VM %s: %s',
  1652. volume.name, vm.name, err)
  1653. def _handle_volume_size(self, vm, volume, size):
  1654. '''Wrap volume resize with logging'''
  1655. try:
  1656. if volume.size < size:
  1657. volume.resize(size)
  1658. except Exception as err: # pylint: disable=broad-except
  1659. self.log.error('Failed to resize volume %s of VM %s to %d: %s',
  1660. volume.name, vm.name, size, err)
  1661. def check_disk_space(self):
  1662. """
  1663. Check if there is enough disk space to restore the backup.
  1664. Currently it checks only for the space in temporary directory,
  1665. not the target storage pools.
  1666. :return:
  1667. """
  1668. statvfs = os.statvfs(self.tmpdir)
  1669. # require 1GB in /var/tmp
  1670. if statvfs.f_frsize * statvfs.f_bavail < 1024 ** 3:
  1671. raise QubesException("Too little space in {}, needs at least 1GB".
  1672. format(self.tmpdir))
  1673. def restore_do(self, restore_info):
  1674. '''
  1675. High level workflow:
  1676. 1. Create VMs object in host collection (qubes.xml)
  1677. 2. Create them on disk (vm.create_on_disk)
  1678. 3. Restore VM data, overriding/converting VM files
  1679. 4. Apply possible fixups and save qubes.xml
  1680. :param restore_info:
  1681. :return:
  1682. '''
  1683. if self.header_data.version == 1:
  1684. raise NotImplementedError('Backup format version 1 not supported')
  1685. self.check_disk_space()
  1686. restore_info = self.restore_info_verify(restore_info)
  1687. self._restore_vms_metadata(restore_info)
  1688. # Perform VM restoration in backup order
  1689. vms_dirs = []
  1690. handlers = {}
  1691. vms_size = 0
  1692. for vm_info in self._templates_first(restore_info.values()):
  1693. vm = vm_info.restored_vm
  1694. if vm and vm_info.subdir:
  1695. if isinstance(vm_info, self.Dom0ToRestore) and \
  1696. vm_info.good_to_go:
  1697. vms_dirs.append(os.path.dirname(vm_info.subdir))
  1698. vms_size += int(vm_info.size)
  1699. if self.options.verify_only:
  1700. continue
  1701. handlers[vm_info.subdir] = (self._handle_dom0, None)
  1702. else:
  1703. vms_size += int(vm_info.size)
  1704. vms_dirs.append(vm_info.subdir)
  1705. if self.options.verify_only:
  1706. continue
  1707. for name, volume in vm.volumes.items():
  1708. if not volume.save_on_stop:
  1709. continue
  1710. data_func = functools.partial(
  1711. self._handle_volume_data, vm, volume)
  1712. size_func = functools.partial(
  1713. self._handle_volume_size, vm, volume)
  1714. img_path = os.path.join(vm_info.subdir, name + '.img')
  1715. handlers[img_path] = (data_func, size_func)
  1716. handlers[os.path.join(vm_info.subdir, 'firewall.xml')] = (
  1717. functools.partial(vm_info.vm.handle_firewall_xml, vm),
  1718. None)
  1719. handlers[os.path.join(vm_info.subdir,
  1720. 'whitelisted-appmenus.list')] = (
  1721. functools.partial(self._handle_appmenus_list, vm), None)
  1722. try:
  1723. self._restore_vm_data(vms_dirs=vms_dirs, vms_size=vms_size,
  1724. handlers=handlers)
  1725. except QubesException as err:
  1726. if self.options.verify_only:
  1727. raise
  1728. self.log.error('Error extracting data: %s', str(err))
  1729. finally:
  1730. if self.log.getEffectiveLevel() > logging.DEBUG:
  1731. shutil.rmtree(self.tmpdir)
  1732. if self.canceled:
  1733. raise BackupCanceledError("Restore canceled",
  1734. tmpdir=self.tmpdir)
  1735. self.log.info("-> Done.")
  1736. if not self.options.verify_only:
  1737. self.log.info("-> Please install updates for all the restored "
  1738. "templates.")
  1739. def _restore_property(self, vm, prop, value):
  1740. '''Restore a single VM property, logging exceptions'''
  1741. try:
  1742. setattr(vm, prop, value)
  1743. except Exception as err: # pylint: disable=broad-except
  1744. self.log.error('Error setting %s.%s to %s: %s',
  1745. vm.name, prop, value, err)
  1746. def _restore_vms_metadata(self, restore_info):
  1747. '''Restore VM metadata
  1748. Create VMs, set their properties etc.
  1749. '''
  1750. vms = {}
  1751. for vm_info in restore_info.values():
  1752. assert isinstance(vm_info, self.VMToRestore)
  1753. if not vm_info.vm:
  1754. continue
  1755. if not vm_info.good_to_go:
  1756. continue
  1757. vm = vm_info.vm
  1758. vms[vm.name] = vm
  1759. # First load templates, then other VMs
  1760. for vm in self._templates_first(vms.values()):
  1761. if self.canceled:
  1762. return
  1763. if self.options.verify_only:
  1764. self.log.info("-> Verifying %s...", vm.name)
  1765. else:
  1766. self.log.info("-> Restoring %s...", vm.name)
  1767. kwargs = {}
  1768. if vm.template:
  1769. template = restore_info[vm.name].template
  1770. # handle potentially renamed template
  1771. if template in restore_info \
  1772. and restore_info[template].good_to_go:
  1773. template = restore_info[template].name
  1774. kwargs['template'] = template
  1775. new_vm = None
  1776. vm_name = restore_info[vm.name].name
  1777. if self.options.verify_only or vm.name == 'dom0':
  1778. # can't create vm, but need backup info
  1779. new_vm = self.backup_app.domains[vm_name]
  1780. else:
  1781. try:
  1782. # first only create VMs, later setting may require other VMs
  1783. # be already created
  1784. new_vm = self.app.add_new_vm(
  1785. vm.klass,
  1786. name=vm_name,
  1787. label=vm.label,
  1788. pool=self.options.override_pool,
  1789. **kwargs)
  1790. except Exception as err: # pylint: disable=broad-except
  1791. self.log.error('Error restoring VM %s, skipping: %s',
  1792. vm.name, err)
  1793. if new_vm:
  1794. del self.app.domains[new_vm.name]
  1795. continue
  1796. # restore this property early to be ready for dependent DispVMs
  1797. prop = 'template_for_dispvms'
  1798. value = vm.properties.get(prop, None)
  1799. if value is not None:
  1800. self._restore_property(new_vm, prop, value)
  1801. restore_info[vm.name].restored_vm = new_vm
  1802. for vm in vms.values():
  1803. if self.canceled:
  1804. return
  1805. new_vm = restore_info[vm.name].restored_vm
  1806. if not new_vm:
  1807. # skipped/failed
  1808. continue
  1809. for prop, value in vm.properties.items():
  1810. # can't reset the first; already handled the second
  1811. if prop in ['dispid', 'template_for_dispvms']:
  1812. continue
  1813. # exclude VM references - handled manually according to
  1814. # restore options
  1815. if prop in ['template', 'netvm', 'default_dispvm']:
  1816. continue
  1817. # exclude as this only applied before restoring
  1818. if prop in ['installed_by_rpm']:
  1819. continue
  1820. self._restore_property(new_vm, prop, value)
  1821. for feature, value in vm.features.items():
  1822. try:
  1823. new_vm.features[feature] = value
  1824. except Exception as err: # pylint: disable=broad-except
  1825. self.log.error('Error setting %s.features[%s] to %s: %s',
  1826. vm.name, feature, value, err)
  1827. for tag in vm.tags:
  1828. try:
  1829. new_vm.tags.add(tag)
  1830. except Exception as err: # pylint: disable=broad-except
  1831. if tag not in new_vm.tags:
  1832. self.log.error('Error adding tag %s to %s: %s',
  1833. tag, vm.name, err)
  1834. for bus in vm.devices:
  1835. for backend_domain, ident in vm.devices[bus]:
  1836. options = vm.devices[bus][(backend_domain, ident)]
  1837. assignment = DeviceAssignment(
  1838. backend_domain=backend_domain,
  1839. ident=ident,
  1840. options=options,
  1841. persistent=True)
  1842. try:
  1843. if not self.options.verify_only:
  1844. new_vm.devices[bus].attach(assignment)
  1845. except Exception as err: # pylint: disable=broad-except
  1846. self.log.error('Error attaching device %s:%s to %s: %s',
  1847. bus, ident, vm.name, err)
  1848. # Set VM dependencies - only non-default setting
  1849. for vm in vms.values():
  1850. vm_info = restore_info[vm.name]
  1851. vm_name = vm_info.name
  1852. try:
  1853. host_vm = self.app.domains[vm_name]
  1854. except KeyError:
  1855. # Failed/skipped VM
  1856. continue
  1857. if 'netvm' in vm.properties:
  1858. if vm_info.netvm in restore_info:
  1859. value = restore_info[vm_info.netvm].name
  1860. else:
  1861. value = vm_info.netvm
  1862. try:
  1863. host_vm.netvm = value
  1864. except Exception as err: # pylint: disable=broad-except
  1865. self.log.error('Error setting %s.%s to %s: %s',
  1866. vm.name, 'netvm', value, err)
  1867. if 'default_dispvm' in vm.properties:
  1868. if vm.properties['default_dispvm'] in restore_info:
  1869. value = restore_info[vm.properties[
  1870. 'default_dispvm']].name
  1871. else:
  1872. value = vm.properties['default_dispvm']
  1873. try:
  1874. host_vm.default_dispvm = value
  1875. except Exception as err: # pylint: disable=broad-except
  1876. self.log.error('Error setting %s.%s to %s: %s',
  1877. vm.name, 'default_dispvm', value, err)