__init__.py 77 KB

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