backup.py 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # The Qubes OS Project, http://www.qubes-os.org
  5. #
  6. # Copyright (C) 2013 Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
  7. # Copyright (C) 2013 Olivier Médoc <o_medoc@yahoo.fr>
  8. #
  9. # This program is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU General Public License
  11. # as published by the Free Software Foundation; either version 2
  12. # of the License, or (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with this program; if not, write to the Free Software
  21. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. #
  23. #
  24. from qubes import QubesException,QubesVmCollection
  25. from qubes import QubesVmClasses
  26. from qubes import system_path,vm_files
  27. from qubesutils import size_to_human, print_stdout, print_stderr
  28. import sys
  29. import os
  30. import subprocess
  31. import re
  32. import shutil
  33. import tempfile
  34. import time
  35. import grp,pwd
  36. from multiprocessing import Queue,Process
  37. BACKUP_DEBUG = False
  38. def get_disk_usage(file_or_dir):
  39. if not os.path.exists(file_or_dir):
  40. return 0
  41. p = subprocess.Popen (["du", "-s", "--block-size=1", file_or_dir],
  42. stdout=subprocess.PIPE)
  43. result = p.communicate()
  44. m = re.match(r"^(\d+)\s.*", result[0])
  45. sz = int(m.group(1)) if m is not None else 0
  46. return sz
  47. def file_to_backup (file_path, subdir = None):
  48. sz = get_disk_usage (file_path)
  49. if subdir is None:
  50. abs_file_path = os.path.abspath (file_path)
  51. abs_base_dir = os.path.abspath (system_path["qubes_base_dir"]) + '/'
  52. abs_file_dir = os.path.dirname (abs_file_path) + '/'
  53. (nothing, dir, subdir) = abs_file_dir.partition (abs_base_dir)
  54. assert nothing == ""
  55. assert dir == abs_base_dir
  56. else:
  57. if len(subdir) > 0 and not subdir.endswith('/'):
  58. subdir += '/'
  59. return [ { "path" : file_path, "size": sz, "subdir": subdir} ]
  60. def backup_prepare(vms_list = None, exclude_list = [],
  61. print_callback = print_stdout, hide_vm_names=True):
  62. """If vms = None, include all (sensible) VMs; exclude_list is always applied"""
  63. files_to_backup = file_to_backup (system_path["qubes_store_filename"])
  64. if exclude_list is None:
  65. exclude_list = []
  66. qvm_collection = QubesVmCollection()
  67. qvm_collection.lock_db_for_writing()
  68. qvm_collection.load()
  69. if vms_list is None:
  70. all_vms = [vm for vm in qvm_collection.values()]
  71. selected_vms = [vm for vm in all_vms if vm.include_in_backups]
  72. appvms_to_backup = [vm for vm in selected_vms if vm.is_appvm() and not vm.internal]
  73. netvms_to_backup = [vm for vm in selected_vms if vm.is_netvm() and not vm.qid == 0]
  74. template_vms_worth_backingup = [vm for vm in selected_vms if (vm.is_template() and not vm.installed_by_rpm)]
  75. vms_list = appvms_to_backup + netvms_to_backup + template_vms_worth_backingup
  76. vms_for_backup = vms_list
  77. # Apply exclude list
  78. if exclude_list:
  79. vms_for_backup = [vm for vm in vms_list if vm.name not in exclude_list]
  80. no_vms = len (vms_for_backup)
  81. there_are_running_vms = False
  82. fields_to_display = [
  83. { "name": "VM", "width": 16},
  84. { "name": "type","width": 12 },
  85. { "name": "size", "width": 12}
  86. ]
  87. # Display the header
  88. s = ""
  89. for f in fields_to_display:
  90. fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
  91. s += fmt.format('-')
  92. print_callback(s)
  93. s = ""
  94. for f in fields_to_display:
  95. fmt="{{0:>{0}}} |".format(f["width"] + 1)
  96. s += fmt.format(f["name"])
  97. print_callback(s)
  98. s = ""
  99. for f in fields_to_display:
  100. fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
  101. s += fmt.format('-')
  102. print_callback(s)
  103. for vm in vms_for_backup:
  104. if vm.is_template():
  105. # handle templates later
  106. continue
  107. if hide_vm_names:
  108. subdir = 'vm%d/' % vm.qid
  109. else:
  110. subdir = None
  111. if vm.private_img is not None:
  112. files_to_backup += file_to_backup(vm.private_img, subdir)
  113. if vm.is_appvm():
  114. files_to_backup += file_to_backup(vm.icon_path, subdir)
  115. if vm.updateable:
  116. if os.path.exists(vm.dir_path + "/apps.templates"):
  117. # template
  118. files_to_backup += file_to_backup(vm.dir_path + "/apps.templates", subdir)
  119. else:
  120. # standaloneVM
  121. files_to_backup += file_to_backup(vm.dir_path + "/apps", subdir)
  122. if os.path.exists(vm.dir_path + "/kernels"):
  123. files_to_backup += file_to_backup(vm.dir_path + "/kernels", subdir)
  124. if os.path.exists (vm.firewall_conf):
  125. files_to_backup += file_to_backup(vm.firewall_conf, subdir)
  126. if 'appmenus_whitelist' in vm_files and \
  127. os.path.exists(os.path.join(vm.dir_path, vm_files['appmenus_whitelist'])):
  128. files_to_backup += file_to_backup(
  129. os.path.join(vm.dir_path, vm_files['appmenus_whitelist']),
  130. subdir)
  131. if vm.updateable:
  132. files_to_backup += file_to_backup(vm.root_img, subdir)
  133. s = ""
  134. fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
  135. s += fmt.format(vm.name)
  136. fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
  137. if vm.is_netvm():
  138. s += fmt.format("NetVM" + (" + Sys" if vm.updateable else ""))
  139. else:
  140. s += fmt.format("AppVM" + (" + Sys" if vm.updateable else ""))
  141. fmt="{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
  142. s += fmt.format(size_to_human(vm.get_disk_utilization()))
  143. if vm.is_running():
  144. s += " <-- The VM is running, please shut it down before proceeding with the backup!"
  145. there_are_running_vms = True
  146. print_callback(s)
  147. for vm in vms_for_backup:
  148. if not vm.is_template():
  149. # already handled
  150. continue
  151. vm_sz = vm.get_disk_utilization()
  152. if hide_vm_names:
  153. template_subdir = 'vm%d/' % vm.qid
  154. else:
  155. template_subdir = os.path.relpath(
  156. vm.dir_path,
  157. system_path["qubes_base_dir"]) + '/'
  158. template_to_backup = [ {
  159. "path": vm.dir_path + '/.',
  160. "size": vm_sz,
  161. "subdir": template_subdir } ]
  162. files_to_backup += template_to_backup
  163. s = ""
  164. fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
  165. s += fmt.format(vm.name)
  166. fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
  167. s += fmt.format("Template VM")
  168. fmt="{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
  169. s += fmt.format(size_to_human(vm_sz))
  170. if vm.is_running():
  171. s += " <-- The VM is running, please shut it down before proceeding with the backup!"
  172. there_are_running_vms = True
  173. print_callback(s)
  174. # Initialize backup flag on all VMs
  175. vms_for_backup_qid = [vm.qid for vm in vms_for_backup]
  176. for vm in qvm_collection.values():
  177. vm.backup_content = False
  178. if vm.qid in vms_for_backup_qid:
  179. vm.backup_content = True
  180. vm.backup_size = vm.get_disk_utilization()
  181. if hide_vm_names:
  182. vm.backup_path = 'vm%d' % vm.qid
  183. else:
  184. vm.backup_path = os.path.relpath(vm.dir_path, system_path["qubes_base_dir"])
  185. # Dom0 user home
  186. if not 'dom0' in exclude_list:
  187. local_user = grp.getgrnam('qubes').gr_mem[0]
  188. home_dir = pwd.getpwnam(local_user).pw_dir
  189. # Home dir should have only user-owned files, so fix it now to prevent
  190. # permissions problems - some root-owned files can left after
  191. # 'sudo bash' and similar commands
  192. subprocess.check_call(['sudo', 'chown', '-R', local_user, home_dir])
  193. home_sz = get_disk_usage(home_dir)
  194. home_to_backup = [ { "path" : home_dir, "size": home_sz, "subdir": 'dom0-home/'} ]
  195. files_to_backup += home_to_backup
  196. vm = qvm_collection[0]
  197. vm.backup_content = True
  198. vm.backup_size = home_sz
  199. vm.backup_path = os.path.join('dom0-home', os.path.basename(home_dir))
  200. s = ""
  201. fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
  202. s += fmt.format('Dom0')
  203. fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
  204. s += fmt.format("User home")
  205. fmt="{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
  206. s += fmt.format(size_to_human(home_sz))
  207. print_callback(s)
  208. qvm_collection.save()
  209. # FIXME: should be after backup completed
  210. qvm_collection.unlock_db()
  211. total_backup_sz = 0
  212. for file in files_to_backup:
  213. total_backup_sz += file["size"]
  214. s = ""
  215. for f in fields_to_display:
  216. fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
  217. s += fmt.format('-')
  218. print_callback(s)
  219. s = ""
  220. fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
  221. s += fmt.format("Total size:")
  222. fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1 + 2 + fields_to_display[2]["width"] + 1)
  223. s += fmt.format(size_to_human(total_backup_sz))
  224. print_callback(s)
  225. s = ""
  226. for f in fields_to_display:
  227. fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
  228. s += fmt.format('-')
  229. print_callback(s)
  230. if (there_are_running_vms):
  231. raise QubesException("Please shutdown all VMs before proceeding.")
  232. for fileinfo in files_to_backup:
  233. assert len(fileinfo["subdir"]) == 0 or fileinfo["subdir"][-1] == '/', \
  234. "'subdir' must ends with a '/': %s" % str(fileinfo)
  235. return files_to_backup
  236. class Send_Worker(Process):
  237. def __init__(self, queue, base_dir, backup_stdout):
  238. super(Send_Worker, self).__init__()
  239. self.queue = queue
  240. self.base_dir = base_dir
  241. self.backup_stdout = backup_stdout
  242. def run(self):
  243. if BACKUP_DEBUG:
  244. print "Started sending thread"
  245. if BACKUP_DEBUG:
  246. print "Moving to temporary dir", self.base_dir
  247. os.chdir(self.base_dir)
  248. for filename in iter(self.queue.get,None):
  249. if filename == "FINISHED":
  250. break
  251. if BACKUP_DEBUG:
  252. print "Sending file", filename
  253. # This tar used for sending data out need to be as simple, as
  254. # simple, as featureless as possible. It will not be
  255. # verified before untaring.
  256. tar_final_cmd = ["tar", "-cO", "--posix",
  257. "-C", self.base_dir, filename]
  258. final_proc = subprocess.Popen (tar_final_cmd,
  259. stdin=subprocess.PIPE, stdout=self.backup_stdout)
  260. if final_proc.wait() >= 2:
  261. # handle only exit code 2 (tar fatal error) or greater (call failed?)
  262. raise QubesException("ERROR: Failed to write the backup, out of disk space? "
  263. "Check console output or ~/.xsession-errors for details.")
  264. # Delete the file as we don't need it anymore
  265. if BACKUP_DEBUG:
  266. print "Removing file", filename
  267. os.remove(filename)
  268. if BACKUP_DEBUG:
  269. print "Finished sending thread"
  270. def backup_do(base_backup_dir, files_to_backup, passphrase,
  271. progress_callback = None, encrypt=False, appvm=None,
  272. compress = False):
  273. total_backup_sz = 0
  274. for file in files_to_backup:
  275. total_backup_sz += file["size"]
  276. vmproc = None
  277. if appvm != None:
  278. # Prepare the backup target (Qubes service call)
  279. backup_target = "QUBESRPC qubes.Backup none"
  280. # If APPVM, STDOUT is a PIPE
  281. vmproc = appvm.run(command = backup_target, passio_popen = True)
  282. vmproc.stdin.write(base_backup_dir.\
  283. replace("\r","").replace("\n","")+"\n")
  284. backup_stdout = vmproc.stdin
  285. else:
  286. # Prepare the backup target (local file)
  287. backup_target = base_backup_dir + "/qubes-{0}".\
  288. format (time.strftime("%Y-%m-%d-%H%M%S"))
  289. # Create the target directory
  290. if not os.path.exists (base_backup_dir):
  291. raise QubesException(
  292. "ERROR: the backup directory {0} does not exists".\
  293. format(base_backup_dir))
  294. # If not APPVM, STDOUT is a local file
  295. backup_stdout = open(backup_target,'wb')
  296. global blocks_backedup
  297. blocks_backedup = 0
  298. progress = blocks_backedup * 11 / total_backup_sz
  299. progress_callback(progress)
  300. feedback_file = tempfile.NamedTemporaryFile()
  301. backup_tmpdir = tempfile.mkdtemp(prefix="/var/tmp/backup_")
  302. # Tar with tapelength does not deals well with stdout (close stdout between
  303. # two tapes)
  304. # For this reason, we will use named pipes instead
  305. if BACKUP_DEBUG:
  306. print "Working in", backup_tmpdir
  307. backup_pipe = os.path.join(backup_tmpdir,"backup_pipe")
  308. if BACKUP_DEBUG:
  309. print "Creating pipe in:", backup_pipe
  310. os.mkfifo(backup_pipe)
  311. if BACKUP_DEBUG:
  312. print "Will backup:", files_to_backup
  313. # Setup worker to send encrypted data chunks to the backup_target
  314. def compute_progress(new_size, total_backup_sz):
  315. global blocks_backedup
  316. blocks_backedup += new_size
  317. progress = blocks_backedup / float(total_backup_sz)
  318. progress_callback(int(round(progress*100,2)))
  319. to_send = Queue(10)
  320. send_proc = Send_Worker(to_send, backup_tmpdir, backup_stdout)
  321. send_proc.start()
  322. for filename in files_to_backup:
  323. if BACKUP_DEBUG:
  324. print "Backing up", filename
  325. backup_tempfile = os.path.join(backup_tmpdir,
  326. filename["subdir"],
  327. os.path.basename(filename["path"]))
  328. if BACKUP_DEBUG:
  329. print "Using temporary location:", backup_tempfile
  330. # Ensure the temporary directory exists
  331. if not os.path.isdir(os.path.dirname(backup_tempfile)):
  332. os.makedirs(os.path.dirname(backup_tempfile))
  333. # The first tar cmd can use any complex feature as we want. Files will
  334. # be verified before untaring this.
  335. # Prefix the path in archive with filename["subdir"] to have it verified during untar
  336. tar_cmdline = ["tar", "-Pc", '--sparse',
  337. "-f", backup_pipe,
  338. '--tape-length', str(100000),
  339. '-C', os.path.dirname(filename["path"]),
  340. '--xform', 's:^[^/]:%s\\0:' % filename["subdir"],
  341. os.path.basename(filename["path"])
  342. ]
  343. if BACKUP_DEBUG:
  344. print " ".join(tar_cmdline)
  345. # Tips: Popen(bufsize=0)
  346. # Pipe: tar-sparse | encryptor [| hmac] | tar | backup_target
  347. # Pipe: tar-sparse [| hmac] | tar | backup_target
  348. tar_sparse = subprocess.Popen (tar_cmdline, stdin=subprocess.PIPE,
  349. stderr=(open(os.devnull, 'w') if not BACKUP_DEBUG else None))
  350. # Wait for compressor (tar) process to finish or for any error of other
  351. # subprocesses
  352. i = 0
  353. run_error = "paused"
  354. running = []
  355. while run_error == "paused":
  356. pipe = open(backup_pipe,'rb')
  357. # Start HMAC
  358. hmac = subprocess.Popen (["openssl", "dgst", "-hmac", passphrase],
  359. stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  360. # Prepare a first chunk
  361. chunkfile = backup_tempfile + "." + "%03d" % i
  362. i += 1
  363. chunkfile_p = open(chunkfile,'wb')
  364. common_args = {
  365. 'backup_target': chunkfile_p,
  366. 'total_backup_sz': total_backup_sz,
  367. 'hmac': hmac,
  368. 'vmproc': vmproc,
  369. 'addproc': tar_sparse
  370. }
  371. if encrypt:
  372. # Start encrypt
  373. # If no cipher is provided, the data is forwarded unencrypted !!!
  374. encryptor = subprocess.Popen (["openssl", "enc",
  375. "-e", "-aes-256-cbc",
  376. "-pass", "pass:"+passphrase] +
  377. (["-z"] if compress else []),
  378. stdin=pipe, stdout=subprocess.PIPE)
  379. run_error = wait_backup_feedback(
  380. progress_callback=compute_progress,
  381. in_stream=encryptor.stdout, streamproc=encryptor,
  382. **common_args)
  383. elif compress:
  384. compressor = subprocess.Popen (["gzip"],
  385. stdin=pipe, stdout=subprocess.PIPE)
  386. run_error = wait_backup_feedback(
  387. progress_callback=compute_progress,
  388. in_stream=compressor.stdout, streamproc=compressor,
  389. **common_args)
  390. else:
  391. run_error = wait_backup_feedback(
  392. progress_callback=compute_progress,
  393. in_stream=pipe, streamproc=None,
  394. **common_args)
  395. chunkfile_p.close()
  396. if BACKUP_DEBUG:
  397. print "Wait_backup_feedback returned:", run_error
  398. if len(run_error) > 0:
  399. send_proc.terminate()
  400. raise QubesException("Failed to perform backup: error with "+ \
  401. run_error)
  402. # Send the chunk to the backup target
  403. to_send.put(os.path.relpath(chunkfile, backup_tmpdir))
  404. # Close HMAC
  405. hmac.stdin.close()
  406. hmac.wait()
  407. if BACKUP_DEBUG:
  408. print "HMAC proc return code:", hmac.poll()
  409. # Write HMAC data next to the chunk file
  410. hmac_data = hmac.stdout.read()
  411. if BACKUP_DEBUG:
  412. print "Writing hmac to", chunkfile+".hmac"
  413. hmac_file = open(chunkfile+".hmac",'w')
  414. hmac_file.write(hmac_data)
  415. hmac_file.flush()
  416. hmac_file.close()
  417. pipe.close()
  418. # Send the HMAC to the backup target
  419. to_send.put(os.path.relpath(chunkfile, backup_tmpdir)+".hmac")
  420. if tar_sparse.poll() == None:
  421. # Release the next chunk
  422. if BACKUP_DEBUG:
  423. print "Release next chunk for process:", tar_sparse.poll()
  424. #tar_sparse.stdout = subprocess.PIPE
  425. tar_sparse.stdin.write("\n")
  426. tar_sparse.stdin.flush()
  427. run_error="paused"
  428. else:
  429. if BACKUP_DEBUG:
  430. print "Finished tar sparse with error", tar_sparse.poll()
  431. to_send.put("FINISHED")
  432. send_proc.join()
  433. if send_proc.exitcode != 0:
  434. raise QubesException("Failed to send backup: error in the sending process")
  435. if vmproc:
  436. if BACKUP_DEBUG:
  437. print "VMProc1 proc return code:", vmproc.poll()
  438. print "Sparse1 proc return code:", tar_sparse.poll()
  439. vmproc.stdin.close()
  440. shutil.rmtree(backup_tmpdir)
  441. '''
  442. ' Wait for backup chunk to finish
  443. ' - Monitor all the processes (streamproc, hmac, vmproc, addproc) for errors
  444. ' - Copy stdout of streamproc to backup_target and hmac stdin if available
  445. ' - Compute progress based on total_backup_sz and send progress to
  446. ' progress_callback function
  447. ' - Returns if
  448. ' - one of the monitored processes error out (streamproc, hmac, vmproc,
  449. ' addproc), along with the processe that failed
  450. ' - all of the monitored processes except vmproc finished successfully
  451. ' (vmproc termination is controlled by the python script)
  452. ' - streamproc does not delivers any data anymore (return with the error
  453. ' "")
  454. '''
  455. def wait_backup_feedback(progress_callback, in_stream, streamproc,
  456. backup_target, total_backup_sz, hmac=None, vmproc=None, addproc=None,
  457. remove_trailing_bytes=0):
  458. buffer_size = 409600
  459. run_error = None
  460. run_count = 1
  461. blocks_backedup = 0
  462. while run_count > 0 and run_error == None:
  463. buffer = in_stream.read(buffer_size)
  464. progress_callback(len(buffer), total_backup_sz)
  465. run_count = 0
  466. if hmac:
  467. retcode=hmac.poll()
  468. if retcode != None:
  469. if retcode != 0:
  470. run_error = "hmac"
  471. else:
  472. run_count += 1
  473. if addproc:
  474. retcode=addproc.poll()
  475. if retcode != None:
  476. if retcode != 0:
  477. run_error = "addproc"
  478. else:
  479. run_count += 1
  480. if vmproc:
  481. retcode = vmproc.poll()
  482. if retcode != None:
  483. if retcode != 0:
  484. run_error = "VM"
  485. if BACKUP_DEBUG:
  486. print vmproc.stdout.read()
  487. else:
  488. # VM should run until the end
  489. pass
  490. if streamproc:
  491. retcode=streamproc.poll()
  492. if retcode != None:
  493. if retcode != 0:
  494. run_error = "streamproc"
  495. break
  496. elif retcode == 0 and len(buffer) <= 0:
  497. return ""
  498. run_count += 1
  499. else:
  500. if len(buffer) <= 0:
  501. return ""
  502. backup_target.write(buffer)
  503. if hmac:
  504. hmac.stdin.write(buffer)
  505. return run_error
  506. def verify_hmac(filename, hmacfile, passphrase):
  507. if BACKUP_DEBUG:
  508. print "Verifying file "+filename
  509. if hmacfile != filename + ".hmac":
  510. raise QubesException(
  511. "ERROR: expected hmac for {}, but got {}".\
  512. format(filename, hmacfile))
  513. hmac_proc = subprocess.Popen (["openssl", "dgst", "-hmac", passphrase],
  514. stdin=open(filename,'rb'),
  515. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  516. hmac_stdout, hmac_stderr = hmac_proc.communicate()
  517. if len(hmac_stderr) > 0:
  518. raise QubesException("ERROR: verify file {0}: {1}".format((filename, hmac_stderr)))
  519. else:
  520. if BACKUP_DEBUG:
  521. print "Loading hmac for file " + filename
  522. hmac = load_hmac(open(hmacfile,'r').read())
  523. if len(hmac) > 0 and load_hmac(hmac_stdout) == hmac:
  524. os.unlink(hmacfile)
  525. if BACKUP_DEBUG:
  526. print "File verification OK -> Sending file " + filename
  527. return True
  528. else:
  529. raise QubesException(
  530. "ERROR: invalid hmac for file {0}: {1}. " \
  531. "Is the passphrase correct?".\
  532. format(filename, load_hmac(hmac_stdout)))
  533. # Not reachable
  534. return False
  535. class Extract_Worker(Process):
  536. def __init__(self, queue, base_dir, passphrase, encrypted, total_size,
  537. print_callback, error_callback, progress_callback, vmproc=None,
  538. compressed = False):
  539. super(Extract_Worker, self).__init__()
  540. self.queue = queue
  541. self.base_dir = base_dir
  542. self.passphrase = passphrase
  543. self.encrypted = encrypted
  544. self.compressed = compressed
  545. self.total_size = total_size
  546. self.blocks_backedup = 0
  547. self.tar2_command = None
  548. self.tar2_current_file = None
  549. self.print_callback = print_callback
  550. self.error_callback = error_callback
  551. self.progress_callback = progress_callback
  552. self.vmproc = vmproc
  553. self.restore_pipe = os.path.join(self.base_dir,"restore_pipe")
  554. if BACKUP_DEBUG:
  555. print "Creating pipe in:", self.restore_pipe
  556. os.mkfifo(self.restore_pipe)
  557. def compute_progress(self, new_size, total_size):
  558. self.blocks_backedup += new_size
  559. progress = self.blocks_backedup / float(self.total_size)
  560. progress = int(round(progress*100,2))
  561. self.progress_callback(progress)
  562. def run(self):
  563. if BACKUP_DEBUG:
  564. self.print_callback("Started sending thread")
  565. self.print_callback("Moving to dir "+self.base_dir)
  566. os.chdir(self.base_dir)
  567. for filename in iter(self.queue.get,None):
  568. if filename == "FINISHED":
  569. break
  570. if BACKUP_DEBUG:
  571. self.print_callback("Extracting file "+filename)
  572. if filename.endswith('.000'):
  573. # next file
  574. if self.tar2_command != None:
  575. if self.tar2_command.wait() != 0:
  576. raise QubesException(
  577. "ERROR: unable to extract files for {0}.".\
  578. format(self.tar2_current_file))
  579. else:
  580. # Finished extracting the tar file
  581. self.tar2_command = None
  582. self.tar2_current_file = None
  583. tar2_cmdline = ['tar',
  584. '-xMk%sf' % ("v" if BACKUP_DEBUG else ""), self.restore_pipe,
  585. os.path.relpath(filename.rstrip('.000'))]
  586. if BACKUP_DEBUG:
  587. self.print_callback("Running command "+str(tar2_cmdline))
  588. self.tar2_command = subprocess.Popen(tar2_cmdline,
  589. stdin=subprocess.PIPE,
  590. stderr=(None if BACKUP_DEBUG else open('/dev/null', 'w')))
  591. else:
  592. if BACKUP_DEBUG:
  593. self.print_callback("Releasing next chunck")
  594. self.tar2_command.stdin.write("\n")
  595. self.tar2_command.stdin.flush()
  596. self.tar2_current_file = filename
  597. pipe = open(self.restore_pipe,'wb')
  598. common_args = {
  599. 'backup_target': pipe,
  600. 'total_backup_sz': self.total_size,
  601. 'hmac': None,
  602. 'vmproc': self.vmproc,
  603. 'addproc': self.tar2_command
  604. }
  605. if self.encrypted:
  606. # Start decrypt
  607. encryptor = subprocess.Popen (["openssl", "enc",
  608. "-d", "-aes-256-cbc",
  609. "-pass", "pass:"+self.passphrase],
  610. (["-z"] if compressed else []),
  611. stdin=open(filename,'rb'),
  612. stdout=subprocess.PIPE)
  613. run_error = wait_backup_feedback(
  614. progress_callback=self.compute_progress,
  615. in_stream=encryptor.stdout, streamproc=encryptor,
  616. **common_args)
  617. elif self.compressed:
  618. compressor = subprocess.Popen (["gzip", "-d"],
  619. stdin=open(filename,'rb'),
  620. stdout=subprocess.PIPE)
  621. run_error = wait_backup_feedback(
  622. progress_callback=self.compute_progress,
  623. in_stream=compressor.stdout, streamproc=compressor,
  624. **common_args)
  625. else:
  626. run_error = wait_backup_feedback(
  627. progress_callback=self.compute_progress,
  628. in_stream=open(filename,"rb"), streamproc=None,
  629. **common_args)
  630. pipe.close()
  631. # Delete the file as we don't need it anymore
  632. if BACKUP_DEBUG:
  633. self.print_callback("Removing file "+filename)
  634. os.remove(filename)
  635. if self.tar2_command != None:
  636. if self.tar2_command.wait() != 0:
  637. raise QubesException(
  638. "ERROR: unable to extract files for {0}.".\
  639. format(self.tar2_current_file))
  640. else:
  641. # Finished extracting the tar file
  642. self.tar2_command = None
  643. os.unlink(self.restore_pipe)
  644. if BACKUP_DEBUG:
  645. self.print_callback("Finished extracting thread")
  646. def restore_vm_dirs (backup_source, restore_tmpdir, passphrase, vms_dirs, vms,
  647. vms_size, print_callback=None, error_callback=None,
  648. progress_callback=None, encrypted=False, appvm=None,
  649. compressed = False):
  650. # Setup worker to extract encrypted data chunks to the restore dirs
  651. if progress_callback == None:
  652. def progress_callback(data):
  653. pass
  654. to_extract = Queue()
  655. extract_proc = Extract_Worker(queue=to_extract,
  656. base_dir=restore_tmpdir,
  657. passphrase=passphrase,
  658. encrypted=encrypted,
  659. compressed=compressed,
  660. total_size=vms_size,
  661. print_callback=print_callback,
  662. error_callback=error_callback,
  663. progress_callback=progress_callback)
  664. extract_proc.start()
  665. if BACKUP_DEBUG:
  666. print_callback("Working in temporary dir:"+restore_tmpdir)
  667. print_callback("Extracting data: " + size_to_human(vms_size)+" to restore")
  668. vmproc = None
  669. if appvm != None:
  670. # Prepare the backup target (Qubes service call)
  671. backup_target = "QUBESRPC qubes.Restore dom0"
  672. # If APPVM, STDOUT is a PIPE
  673. vmproc = appvm.run(command = backup_target, passio_popen = True, passio_stderr=True)
  674. vmproc.stdin.write(backup_source.replace("\r","").replace("\n","")+"\n")
  675. # Send to tar2qfile the VMs that should be extracted
  676. vmproc.stdin.write(" ".join(vms_dirs)+"\n")
  677. backup_stdin = vmproc.stdout
  678. tar1_command = ['/usr/libexec/qubes/qfile-dom0-unpacker',
  679. str(os.getuid()), restore_tmpdir, '-v']
  680. else:
  681. backup_stdin = open(backup_source,'rb')
  682. tar1_command = ['tar',
  683. '-ixvf', backup_source,
  684. '-C', restore_tmpdir] + vms_dirs
  685. tar1_env = os.environ.copy()
  686. # TODO: add some safety margin?
  687. tar1_env['UPDATES_MAX_BYTES'] = str(vms_size)
  688. # Restoring only header
  689. if vms_dirs and vms_dirs[0] == 'qubes.xml.000':
  690. tar1_env['UPDATES_MAX_FILES'] = '2'
  691. else:
  692. tar1_env['UPDATES_MAX_FILES'] = '0'
  693. if BACKUP_DEBUG:
  694. print_callback("Run command"+str(tar1_command))
  695. command = subprocess.Popen(tar1_command,
  696. stdin=backup_stdin,
  697. stdout=vmproc.stdin if vmproc else subprocess.PIPE,
  698. stderr=subprocess.PIPE,
  699. env=tar1_env)
  700. # qfile-dom0-unpacker output filelist on stderr (and have stdout connected
  701. # to the VM), while tar output filelist on stdout
  702. if appvm:
  703. filelist_pipe = command.stderr
  704. else:
  705. filelist_pipe = command.stdout
  706. while True:
  707. filename = filelist_pipe.readline().strip(" \t\r\n")
  708. if BACKUP_DEBUG:
  709. print_callback("Getting new file:"+filename)
  710. if not filename or filename=="EOF":
  711. break
  712. hmacfile = filelist_pipe.readline().strip(" \t\r\n")
  713. if BACKUP_DEBUG:
  714. print_callback("Getting hmac:"+hmacfile)
  715. if not any(map(lambda x: filename.startswith(x), vms_dirs)):
  716. if BACKUP_DEBUG:
  717. print_callback("Ignoring VM not selected for restore")
  718. os.unlink(os.path.join(restore_tmpdir, filename))
  719. os.unlink(os.path.join(restore_tmpdir, hmacfile))
  720. continue
  721. if verify_hmac(os.path.join(restore_tmpdir,filename),
  722. os.path.join(restore_tmpdir,hmacfile),
  723. passphrase):
  724. to_extract.put(os.path.join(restore_tmpdir, filename))
  725. if command.wait() != 0:
  726. raise QubesException(
  727. "ERROR: unable to read the qubes backup file {0} ({1}). " \
  728. "Is it really a backup?".format(backup_source, command.wait()))
  729. if vmproc:
  730. if vmproc.wait() != 0:
  731. raise QubesException(
  732. "ERROR: unable to read the qubes backup {0} " \
  733. "because of a VM error: {1}".format(
  734. backup_source, vmproc.stderr.read()))
  735. to_extract.put("FINISHED")
  736. if BACKUP_DEBUG:
  737. print_callback("Waiting for the extraction process to finish...")
  738. extract_proc.join()
  739. if BACKUP_DEBUG:
  740. print_callback("Extraction process finished with code:" + \
  741. str(extract_proc.exitcode))
  742. if extract_proc.exitcode != 0:
  743. raise QubesException(
  744. "ERROR: unable to extract the qubes backup. " \
  745. "Check extracting process errors.")
  746. def backup_restore_set_defaults(options):
  747. if 'use-default-netvm' not in options:
  748. options['use-default-netvm'] = False
  749. if 'use-none-netvm' not in options:
  750. options['use-none-netvm'] = False
  751. if 'use-default-template' not in options:
  752. options['use-default-template'] = False
  753. if 'dom0-home' not in options:
  754. options['dom0-home'] = True
  755. if 'replace-template' not in options:
  756. options['replace-template'] = []
  757. return options
  758. def load_hmac(hmac):
  759. hmac = hmac.strip(" \t\r\n").split("=")
  760. if len(hmac) > 1:
  761. hmac = hmac[1].strip()
  762. else:
  763. raise QubesException("ERROR: invalid hmac file content")
  764. return hmac
  765. def backup_detect_format_version(backup_location):
  766. if os.path.exists(os.path.join(backup_location, 'qubes.xml')):
  767. return 1
  768. else:
  769. return 2
  770. def backup_restore_header(source, passphrase,
  771. print_callback = print_stdout, error_callback = print_stderr,
  772. encrypted=False, appvm=None, compressed = False, format_version = None):
  773. vmproc = None
  774. feedback_file = tempfile.NamedTemporaryFile()
  775. restore_tmpdir = tempfile.mkdtemp(prefix="/var/tmp/restore_")
  776. if format_version == None:
  777. format_version = backup_detect_format_version(source)
  778. if format_version == 1:
  779. return (restore_tmpdir, os.path.join(source, 'qubes.xml'))
  780. os.chdir(restore_tmpdir)
  781. if BACKUP_DEBUG:
  782. print "Working in", restore_tmpdir
  783. # tar2qfile matches only beginnings, while tar full path
  784. if appvm:
  785. extract_filter = ['qubes.xml.000']
  786. else:
  787. extract_filter = ['qubes.xml.000', 'qubes.xml.000.hmac']
  788. restore_vm_dirs (source,
  789. restore_tmpdir,
  790. passphrase=passphrase,
  791. vms_dirs=extract_filter,
  792. vms=None,
  793. vms_size=40000,
  794. print_callback=print_callback,
  795. error_callback=error_callback,
  796. progress_callback=None,
  797. encrypted=encrypted,
  798. compressed=compressed,
  799. appvm=appvm)
  800. return (restore_tmpdir, "qubes.xml")
  801. def backup_restore_prepare(backup_location, qubes_xml, passphrase, options = {},
  802. host_collection = None, encrypt=False, appvm=None, format_version=None):
  803. # Defaults
  804. backup_restore_set_defaults(options)
  805. #### Private functions begin
  806. def is_vm_included_in_backup_v1 (backup_dir, vm):
  807. if vm.qid == 0:
  808. return os.path.exists(os.path.join(backup_dir,'dom0-home'))
  809. backup_vm_dir_path = vm.dir_path.replace (system_path["qubes_base_dir"], backup_dir)
  810. if os.path.exists (backup_vm_dir_path):
  811. return True
  812. else:
  813. return False
  814. def is_vm_included_in_backup_v2 (backup_dir, vm):
  815. if vm.backup_content:
  816. return True
  817. else:
  818. return False
  819. def find_template_name(template, replaces):
  820. rx_replace = re.compile("(.*):(.*)")
  821. for r in replaces:
  822. m = rx_replace.match(r)
  823. if m.group(1) == template:
  824. return m.group(2)
  825. return template
  826. #### Private functions end
  827. # Format versions:
  828. # 1 - Qubes R1, Qubes R2 beta1, beta2
  829. # 2 - Qubes R2 beta3
  830. if format_version is None:
  831. format_version = backup_detect_format_version(backup_location)
  832. if format_version == 1:
  833. is_vm_included_in_backup = is_vm_included_in_backup_v1
  834. elif format_version == 2:
  835. is_vm_included_in_backup = is_vm_included_in_backup_v2
  836. else:
  837. raise QubesException("Unknown backup format version: %s" % str(format_version))
  838. if BACKUP_DEBUG:
  839. print "Loading file", qubes_xml
  840. backup_collection = QubesVmCollection(store_filename = qubes_xml)
  841. backup_collection.lock_db_for_reading()
  842. backup_collection.load()
  843. if host_collection is None:
  844. host_collection = QubesVmCollection()
  845. host_collection.lock_db_for_reading()
  846. host_collection.load()
  847. host_collection.unlock_db()
  848. backup_vms_list = [vm for vm in backup_collection.values()]
  849. host_vms_list = [vm for vm in host_collection.values()]
  850. vms_to_restore = {}
  851. there_are_conflicting_vms = False
  852. there_are_missing_templates = False
  853. there_are_missing_netvms = False
  854. dom0_username_mismatch = False
  855. restore_home = False
  856. # ... and the actual data
  857. for vm in backup_vms_list:
  858. if vm.qid == 0:
  859. # Handle dom0 as special case later
  860. continue
  861. if is_vm_included_in_backup (backup_location, vm):
  862. if BACKUP_DEBUG:
  863. print vm.name,"is included in backup"
  864. vms_to_restore[vm.name] = {}
  865. vms_to_restore[vm.name]['vm'] = vm;
  866. if 'exclude' in options.keys():
  867. vms_to_restore[vm.name]['excluded'] = vm.name in options['exclude']
  868. if vms_to_restore[vm.name]['excluded']:
  869. vms_to_restore[vm.name]['good-to-go'] = False
  870. if host_collection.get_vm_by_name (vm.name) is not None:
  871. vms_to_restore[vm.name]['already-exists'] = True
  872. vms_to_restore[vm.name]['good-to-go'] = False
  873. if vm.template is None:
  874. vms_to_restore[vm.name]['template'] = None
  875. else:
  876. templatevm_name = find_template_name(vm.template.name, options['replace-template'])
  877. vms_to_restore[vm.name]['template'] = templatevm_name
  878. template_vm_on_host = host_collection.get_vm_by_name (templatevm_name)
  879. # No template on the host?
  880. if not ((template_vm_on_host is not None) and template_vm_on_host.is_template()):
  881. # Maybe the (custom) template is in the backup?
  882. template_vm_on_backup = backup_collection.get_vm_by_name (templatevm_name)
  883. if template_vm_on_backup is None or not \
  884. (is_vm_included_in_backup(backup_location, template_vm_on_backup) and \
  885. template_vm_on_backup.is_template()):
  886. if options['use-default-template']:
  887. vms_to_restore[vm.name]['orig-template'] = templatevm_name
  888. vms_to_restore[vm.name]['template'] = host_collection.get_default_template().name
  889. else:
  890. vms_to_restore[vm.name]['missing-template'] = True
  891. vms_to_restore[vm.name]['good-to-go'] = False
  892. if vm.netvm is None:
  893. vms_to_restore[vm.name]['netvm'] = None
  894. else:
  895. netvm_name = vm.netvm.name
  896. vms_to_restore[vm.name]['netvm'] = netvm_name
  897. # Set to None to not confuse QubesVm object from backup
  898. # collection with host collection (further in clone_attrs). Set
  899. # directly _netvm to suppress setter action, especially
  900. # modifying firewall
  901. vm._netvm = None
  902. netvm_on_host = host_collection.get_vm_by_name (netvm_name)
  903. # No netvm on the host?
  904. if not ((netvm_on_host is not None) and netvm_on_host.is_netvm()):
  905. # Maybe the (custom) netvm is in the backup?
  906. netvm_on_backup = backup_collection.get_vm_by_name (netvm_name)
  907. if not ((netvm_on_backup is not None) and \
  908. netvm_on_backup.is_netvm() and \
  909. is_vm_included_in_backup(backup_location, netvm_on_backup)):
  910. if options['use-default-netvm']:
  911. vms_to_restore[vm.name]['netvm'] = host_collection.get_default_netvm().name
  912. vm.uses_default_netvm = True
  913. elif options['use-none-netvm']:
  914. vms_to_restore[vm.name]['netvm'] = None
  915. else:
  916. vms_to_restore[vm.name]['missing-netvm'] = True
  917. vms_to_restore[vm.name]['good-to-go'] = False
  918. if 'good-to-go' not in vms_to_restore[vm.name].keys():
  919. vms_to_restore[vm.name]['good-to-go'] = True
  920. # ...and dom0 home
  921. if options['dom0-home'] and \
  922. is_vm_included_in_backup(backup_location, backup_collection[0]):
  923. vm = backup_collection[0]
  924. vms_to_restore['dom0'] = {}
  925. if format_version == 1:
  926. vms_to_restore['dom0']['subdir'] = \
  927. os.listdir(os.path.join(backup_location, 'dom0-home'))[0]
  928. vms_to_restore['dom0']['size'] = 0 # unknown
  929. else:
  930. vms_to_restore['dom0']['subdir'] = vm.backup_path
  931. vms_to_restore['dom0']['size'] = vm.backup_size
  932. local_user = grp.getgrnam('qubes').gr_mem[0]
  933. dom0_home = vms_to_restore['dom0']['subdir']
  934. vms_to_restore['dom0']['username'] = os.path.basename(dom0_home)
  935. if vms_to_restore['dom0']['username'] != local_user:
  936. vms_to_restore['dom0']['username-mismatch'] = True
  937. if not options['ignore-dom0-username-mismatch']:
  938. vms_to_restore['dom0']['good-to-go'] = False
  939. if 'good-to-go' not in vms_to_restore['dom0']:
  940. vms_to_restore['dom0']['good-to-go'] = True
  941. # Not needed - all the data stored in vms_to_restore
  942. if format_version == 2:
  943. os.unlink(qubes_xml)
  944. return vms_to_restore
  945. def backup_restore_print_summary(restore_info, print_callback = print_stdout):
  946. fields = {
  947. "qid": {"func": "vm.qid"},
  948. "name": {"func": "('[' if vm.is_template() else '')\
  949. + ('{' if vm.is_netvm() else '')\
  950. + vm.name \
  951. + (']' if vm.is_template() else '')\
  952. + ('}' if vm.is_netvm() else '')"},
  953. "type": {"func": "'Tpl' if vm.is_template() else \
  954. 'HVM' if vm.type == 'HVM' else \
  955. vm.type.replace('VM','')"},
  956. "updbl" : {"func": "'Yes' if vm.updateable else ''"},
  957. "template": {"func": "'n/a' if vm.is_template() or vm.template is None else\
  958. vm_info['template']"},
  959. "netvm": {"func": "'n/a' if vm.is_netvm() and not vm.is_proxyvm() else\
  960. ('*' if vm.uses_default_netvm else '') +\
  961. vm_info['netvm'] if vm_info['netvm'] is not None else '-'"},
  962. "label" : {"func" : "vm.label.name"},
  963. }
  964. fields_to_display = ["name", "type", "template", "updbl", "netvm", "label" ]
  965. # First calculate the maximum width of each field we want to display
  966. total_width = 0;
  967. for f in fields_to_display:
  968. fields[f]["max_width"] = len(f)
  969. for vm_info in restore_info.values():
  970. if 'vm' in vm_info.keys():
  971. vm = vm_info['vm']
  972. l = len(str(eval(fields[f]["func"])))
  973. if l > fields[f]["max_width"]:
  974. fields[f]["max_width"] = l
  975. total_width += fields[f]["max_width"]
  976. print_callback("")
  977. print_callback("The following VMs are included in the backup:")
  978. print_callback("")
  979. # Display the header
  980. s = ""
  981. for f in fields_to_display:
  982. fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
  983. s += fmt.format('-')
  984. print_callback(s)
  985. s = ""
  986. for f in fields_to_display:
  987. fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
  988. s += fmt.format(f)
  989. print_callback(s)
  990. s = ""
  991. for f in fields_to_display:
  992. fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
  993. s += fmt.format('-')
  994. print_callback(s)
  995. for vm_info in restore_info.values():
  996. # Skip non-VM here
  997. if not 'vm' in vm_info:
  998. continue
  999. vm = vm_info['vm']
  1000. s = ""
  1001. for f in fields_to_display:
  1002. fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
  1003. s += fmt.format(eval(fields[f]["func"]))
  1004. if 'excluded' in vm_info and vm_info['excluded']:
  1005. s += " <-- Excluded from restore"
  1006. elif 'already-exists' in vm_info:
  1007. s += " <-- A VM with the same name already exists on the host!"
  1008. elif 'missing-template' in vm_info:
  1009. s += " <-- No matching template on the host or in the backup found!"
  1010. elif 'missing-netvm' in vm_info:
  1011. s += " <-- No matching netvm on the host or in the backup found!"
  1012. elif 'orig-template' in vm_info:
  1013. s += " <-- Original template was '%s'" % (vm_info['orig-template'])
  1014. print_callback(s)
  1015. if 'dom0' in restore_info.keys():
  1016. s = ""
  1017. for f in fields_to_display:
  1018. fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
  1019. if f == "name":
  1020. s += fmt.format("Dom0")
  1021. elif f == "type":
  1022. s += fmt.format("Home")
  1023. else:
  1024. s += fmt.format("")
  1025. if 'username-mismatch' in restore_info['dom0']:
  1026. s += " <-- username in backup and dom0 mismatch"
  1027. print_callback(s)
  1028. def backup_restore_do(backup_location, restore_tmpdir, passphrase, restore_info,
  1029. host_collection = None, print_callback = print_stdout,
  1030. error_callback = print_stderr, progress_callback = None,
  1031. encrypted=False, appvm=None, compressed = False, format_version = None):
  1032. ### Private functions begin
  1033. def restore_vm_dir_v1 (backup_dir, src_dir, dst_dir):
  1034. backup_src_dir = src_dir.replace (system_path["qubes_base_dir"], backup_dir)
  1035. # We prefer to use Linux's cp, because it nicely handles sparse files
  1036. retcode = subprocess.call (["cp", "-rp", backup_src_dir, dst_dir])
  1037. if retcode != 0:
  1038. raise QubesException("*** Error while copying file {0} to {1}".format(backup_src_dir, dest_dir))
  1039. ### Private functions end
  1040. if format_version is None:
  1041. format_version = backup_detect_format_version(backup_location)
  1042. lock_obtained = False
  1043. if host_collection is None:
  1044. host_collection = QubesVmCollection()
  1045. host_collection.lock_db_for_writing()
  1046. host_collection.load()
  1047. lock_obtained = True
  1048. # Perform VM restoration in backup order
  1049. if format_version == 2:
  1050. vms_dirs = []
  1051. vms_size = 0
  1052. vms = {}
  1053. for vm_info in restore_info.values():
  1054. if not vm_info['good-to-go']:
  1055. continue
  1056. if 'vm' not in vm_info:
  1057. continue
  1058. vm = vm_info['vm']
  1059. vms_size += vm.backup_size
  1060. vms_dirs.append(vm.backup_path)
  1061. vms[vm.name] = vm
  1062. if 'dom0' in restore_info.keys() and restore_info['dom0']['good-to-go']:
  1063. vms_dirs.append('dom0-home')
  1064. vms_size += restore_info['dom0']['size']
  1065. restore_vm_dirs (backup_location,
  1066. restore_tmpdir,
  1067. passphrase=passphrase,
  1068. vms_dirs=vms_dirs,
  1069. vms=vms,
  1070. vms_size=vms_size,
  1071. print_callback=print_callback,
  1072. error_callback=error_callback,
  1073. progress_callback=progress_callback,
  1074. encrypted=encrypted,
  1075. compressed=compressed,
  1076. appvm=appvm)
  1077. # Add VM in right order
  1078. for (vm_class_name, vm_class) in sorted(QubesVmClasses.items(),
  1079. key=lambda _x: _x[1].load_order):
  1080. for vm_info in restore_info.values():
  1081. if not vm_info['good-to-go']:
  1082. continue
  1083. if 'vm' not in vm_info:
  1084. continue
  1085. vm = vm_info['vm']
  1086. if not vm.__class__ == vm_class:
  1087. continue
  1088. print_callback("-> Restoring {type} {0}...".format(vm.name, type=vm_class_name))
  1089. retcode = subprocess.call (["mkdir", "-p", os.path.dirname(vm.dir_path)])
  1090. if retcode != 0:
  1091. error_callback("*** Cannot create directory: {0}?!".format(dest_dir))
  1092. error_callback("Skipping...")
  1093. continue
  1094. template = None
  1095. if vm.template is not None:
  1096. template_name = vm_info['template']
  1097. template = host_collection.get_vm_by_name(template_name)
  1098. new_vm = None
  1099. try:
  1100. new_vm = host_collection.add_new_vm(vm_class_name, name=vm.name,
  1101. conf_file=vm.conf_file,
  1102. dir_path=vm.dir_path,
  1103. template=template,
  1104. installed_by_rpm=False)
  1105. if format_version == 1:
  1106. restore_vm_dir_v1(backup_location,
  1107. vm.dir_path,
  1108. os.path.dirname(new_vm.dir_path))
  1109. elif format_version == 2:
  1110. shutil.move(os.path.join(restore_tmpdir, vm.backup_path),
  1111. new_vm.dir_path)
  1112. new_vm.verify_files()
  1113. except Exception as err:
  1114. error_callback("ERROR: {0}".format(err))
  1115. error_callback("*** Skipping VM: {0}".format(vm.name))
  1116. if new_vm:
  1117. host_collection.pop(new_vm.qid)
  1118. continue
  1119. try:
  1120. new_vm.clone_attrs(vm)
  1121. except Exception as err:
  1122. error_callback("ERROR: {0}".format(err))
  1123. error_callback("*** Some VM property will not be restored")
  1124. try:
  1125. new_vm.appmenus_create(verbose=True)
  1126. except Exception as err:
  1127. error_callback("ERROR during appmenu restore: {0}".format(err))
  1128. error_callback("*** VM '{0}' will not have appmenus".format(vm.name))
  1129. # Set network dependencies - only non-default netvm setting
  1130. for vm_info in restore_info.values():
  1131. if not vm_info['good-to-go']:
  1132. continue
  1133. if 'vm' not in vm_info:
  1134. continue
  1135. vm = vm_info['vm']
  1136. host_vm = host_collection.get_vm_by_name(vm.name)
  1137. if host_vm is None:
  1138. # Failed/skipped VM
  1139. continue
  1140. if not vm.uses_default_netvm:
  1141. host_vm.netvm = host_collection.get_vm_by_name (vm_info['netvm']) if vm_info['netvm'] is not None else None
  1142. host_collection.save()
  1143. if lock_obtained:
  1144. host_collection.unlock_db()
  1145. # ... and dom0 home as last step
  1146. if 'dom0' in restore_info.keys() and restore_info['dom0']['good-to-go']:
  1147. backup_path = restore_info['dom0']['subdir']
  1148. local_user = grp.getgrnam('qubes').gr_mem[0]
  1149. home_dir = pwd.getpwnam(local_user).pw_dir
  1150. if format_version == 1:
  1151. backup_dom0_home_dir = os.path.join(backup_location, backup_path)
  1152. else:
  1153. backup_dom0_home_dir = os.path.join(restore_tmpdir, backup_path)
  1154. restore_home_backupdir = "home-pre-restore-{0}".format (time.strftime("%Y-%m-%d-%H%M%S"))
  1155. print_callback("-> Restoring home of user '{0}'...".format(local_user))
  1156. print_callback("--> Existing files/dirs backed up in '{0}' dir".format(restore_home_backupdir))
  1157. os.mkdir(home_dir + '/' + restore_home_backupdir)
  1158. for f in os.listdir(backup_dom0_home_dir):
  1159. home_file = home_dir + '/' + f
  1160. if os.path.exists(home_file):
  1161. os.rename(home_file, home_dir + '/' + restore_home_backupdir + '/' + f)
  1162. if format_version == 1:
  1163. retcode = subprocess.call (["cp", "-nrp", backup_dom0_home_dir + '/' + f, home_file])
  1164. elif format_version == 2:
  1165. shutil.move(backup_dom0_home_dir + '/' + f, home_file)
  1166. retcode = subprocess.call(['sudo', 'chown', '-R', local_user, home_dir])
  1167. if retcode != 0:
  1168. error_callback("*** Error while setting home directory owner")
  1169. shutil.rmtree(restore_tmpdir)
  1170. # vim:sw=4:et: