backup.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2014-2015
  5. # Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
  6. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  7. #
  8. # This library is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU Lesser General Public
  10. # License as published by the Free Software Foundation; either
  11. # version 2.1 of the License, or (at your option) any later version.
  12. #
  13. # This library 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 GNU
  16. # Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public
  19. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  20. #
  21. import hashlib
  22. import logging
  23. import multiprocessing
  24. import os
  25. import shutil
  26. import sys
  27. import asyncio
  28. import qubes
  29. import qubes.backup
  30. import qubes.exc
  31. import qubes.storage.lvm
  32. import qubes.tests
  33. import qubes.tests.storage_lvm
  34. import qubes.utils
  35. import qubes.vm
  36. import qubes.vm.appvm
  37. import qubes.vm.templatevm
  38. import qubes.vm.qubesvm
  39. try:
  40. import qubesadmin.backup.restore
  41. import qubesadmin.exc
  42. restore_available = True
  43. except ImportError:
  44. restore_available = False
  45. # noinspection PyAttributeOutsideInit
  46. class BackupTestsMixin(object):
  47. class BackupErrorHandler(logging.Handler):
  48. def __init__(self, errors_queue, level=logging.NOTSET):
  49. super(BackupTestsMixin.BackupErrorHandler, self).__init__(level)
  50. self.errors_queue = errors_queue
  51. def emit(self, record):
  52. self.errors_queue.put(record.getMessage())
  53. def setUp(self):
  54. super(BackupTestsMixin, self).setUp()
  55. try:
  56. self.init_default_template(self.template)
  57. except AttributeError:
  58. self.init_default_template()
  59. self.error_detected = multiprocessing.Queue()
  60. self.log.debug("Creating backupvm")
  61. self.backupdir = os.path.join(os.environ["HOME"], "test-backup")
  62. if os.path.exists(self.backupdir):
  63. shutil.rmtree(self.backupdir)
  64. os.mkdir(self.backupdir)
  65. self.error_handler = self.BackupErrorHandler(self.error_detected,
  66. level=logging.WARNING)
  67. backup_log = logging.getLogger('qubesadmin.backup')
  68. backup_log.addHandler(self.error_handler)
  69. def tearDown(self):
  70. shutil.rmtree(self.backupdir)
  71. backup_log = logging.getLogger('qubes.backup')
  72. backup_log.removeHandler(self.error_handler)
  73. super(BackupTestsMixin, self).tearDown()
  74. def fill_image(self, path, size=None, sparse=False):
  75. block_size = 4096
  76. self.log.debug("Filling %s" % path)
  77. try:
  78. f = open(path, 'rb+')
  79. except FileNotFoundError:
  80. f = open(path, 'wb+')
  81. if size is None:
  82. f.seek(0, 2)
  83. size = f.tell()
  84. f.seek(0)
  85. for block_num in range(int(size/block_size)):
  86. if sparse:
  87. f.seek(block_size, 1)
  88. f.write(b'a' * block_size)
  89. f.close()
  90. def fill_image_vm(self, vm, volume, size=None, sparse=False):
  91. path = self.loop.run_until_complete(vm.storage.export(volume))
  92. try:
  93. self.fill_image(path, size=size, sparse=sparse)
  94. finally:
  95. self.loop.run_until_complete(vm.storage.export_end(volume, path))
  96. # NOTE: this was create_basic_vms
  97. def create_backup_vms(self, pool=None):
  98. template = self.app.default_template
  99. vms = []
  100. vmname = self.make_vm_name('test-net')
  101. self.log.debug("Creating %s" % vmname)
  102. testnet = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  103. name=vmname, template=template, provides_network=True,
  104. label='red')
  105. self.loop.run_until_complete(
  106. testnet.create_on_disk(pool=pool))
  107. testnet.features['service.ntpd'] = True
  108. vms.append(testnet)
  109. self.fill_image_vm(testnet, 'private', 20*1024*1024)
  110. vmname = self.make_vm_name('test1')
  111. self.log.debug("Creating %s" % vmname)
  112. testvm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  113. name=vmname, template=template, label='red')
  114. testvm1.netvm = testnet
  115. self.loop.run_until_complete(
  116. testvm1.create_on_disk(pool=pool))
  117. vms.append(testvm1)
  118. self.fill_image_vm(testvm1, 'private', 100 * 1024 * 1024)
  119. vmname = self.make_vm_name('testhvm1')
  120. self.log.debug("Creating %s" % vmname)
  121. testvm2 = self.app.add_new_vm(qubes.vm.standalonevm.StandaloneVM,
  122. name=vmname,
  123. virt_mode='hvm',
  124. label='red')
  125. self.loop.run_until_complete(
  126. testvm2.create_on_disk(pool=pool))
  127. self.fill_image_vm(testvm2, 'root', 1024 * 1024 * 1024, True)
  128. vms.append(testvm2)
  129. vmname = self.make_vm_name('template')
  130. self.log.debug("Creating %s" % vmname)
  131. testvm3 = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
  132. name=vmname, label='red')
  133. self.loop.run_until_complete(
  134. testvm3.create_on_disk(pool=pool))
  135. self.fill_image_vm(testvm3, 'root', 100 * 1024 * 1024, True)
  136. vms.append(testvm3)
  137. vmname = self.make_vm_name('custom')
  138. self.log.debug("Creating %s" % vmname)
  139. testvm4 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  140. name=vmname, template=testvm3, label='red')
  141. self.loop.run_until_complete(
  142. testvm4.create_on_disk(pool=pool))
  143. vms.append(testvm4)
  144. self.app.save()
  145. return vms
  146. def make_backup(self, vms, target=None, expect_failure=False, **kwargs):
  147. if target is None:
  148. target = self.backupdir
  149. try:
  150. backup = qubes.backup.Backup(self.app, vms, **kwargs)
  151. except qubes.exc.QubesException as e:
  152. if not expect_failure:
  153. self.fail("QubesException during backup_prepare: %s" % str(e))
  154. else:
  155. raise
  156. if 'passphrase' not in kwargs:
  157. backup.passphrase = 'qubes'
  158. backup.target_dir = target
  159. try:
  160. self.loop.run_until_complete(backup.backup_do())
  161. except qubes.exc.QubesException as e:
  162. if not expect_failure:
  163. self.fail("QubesException during backup_do: %s" % str(e))
  164. else:
  165. raise
  166. def remove_vms(self, vms):
  167. vms = list(vms)
  168. for vm in vms:
  169. vm.netvm = None
  170. vm.default_dispvm = None
  171. super(BackupTestsMixin, self).remove_vms(vms)
  172. def restore_backup(self, source=None, appvm=None, options=None,
  173. expect_errors=None, manipulate_restore_info=None,
  174. passphrase='qubes'):
  175. if not restore_available:
  176. self.skipTest('qubesadmin module not available')
  177. if source is None:
  178. backupfile = os.path.join(self.backupdir,
  179. sorted(os.listdir(self.backupdir))[-1])
  180. else:
  181. backupfile = source
  182. client_app = qubesadmin.Qubes()
  183. if appvm:
  184. appvm = self.loop.run_until_complete(
  185. self.loop.run_in_executor(None,
  186. client_app.domains.__getitem__, appvm.name))
  187. with self.assertNotRaises(qubesadmin.exc.QubesException):
  188. restore_op = self.loop.run_until_complete(
  189. self.loop.run_in_executor(None,
  190. qubesadmin.backup.restore.BackupRestore,
  191. client_app, backupfile, appvm, passphrase))
  192. if options:
  193. for key, value in options.items():
  194. setattr(restore_op.options, key, value)
  195. restore_info = self.loop.run_until_complete(
  196. self.loop.run_in_executor(None,
  197. restore_op.get_restore_info))
  198. if callable(manipulate_restore_info):
  199. restore_info = manipulate_restore_info(restore_info)
  200. self.log.debug(restore_op.get_restore_summary(restore_info))
  201. with self.assertNotRaises(qubesadmin.exc.QubesException):
  202. self.loop.run_until_complete(
  203. self.loop.run_in_executor(None,
  204. restore_op.restore_do, restore_info))
  205. errors = []
  206. if expect_errors is None:
  207. expect_errors = []
  208. else:
  209. self.assertFalse(self.error_detected.empty(),
  210. "Restore errors expected, but none detected")
  211. while not self.error_detected.empty():
  212. current_error = self.error_detected.get()
  213. if any(map(current_error.startswith, expect_errors)):
  214. continue
  215. errors.append(current_error)
  216. self.assertTrue(len(errors) == 0,
  217. "Error(s) detected during backup_restore_do: %s" %
  218. '\n'.join(errors))
  219. if not appvm and not os.path.isdir(backupfile):
  220. os.unlink(backupfile)
  221. def create_sparse(self, path, size):
  222. f = open(path, "w")
  223. f.truncate(size)
  224. f.close()
  225. def vm_checksum(self, vms):
  226. hashes = {}
  227. for vm in vms:
  228. assert isinstance(vm, qubes.vm.qubesvm.QubesVM)
  229. hashes[vm.name] = {}
  230. for name, volume in vm.volumes.items():
  231. if not volume.rw or not volume.save_on_stop:
  232. continue
  233. vol_path = self.loop.run_until_complete(
  234. qubes.utils.coro_maybe(volume.export()))
  235. hasher = hashlib.sha1()
  236. with open(vol_path, 'rb') as afile:
  237. for buf in iter(lambda: afile.read(4096000), b''):
  238. hasher.update(buf)
  239. self.loop.run_until_complete(
  240. qubes.utils.coro_maybe(volume.export_end(vol_path)))
  241. hashes[vm.name][name] = hasher.hexdigest()
  242. return hashes
  243. def get_vms_info(self, vms):
  244. ''' Get VM metadata, for comparing VM later without holding actual
  245. reference to the old object.'''
  246. vms_info = {}
  247. for vm in vms:
  248. vm_info = {
  249. 'properties': {},
  250. 'default': {},
  251. 'devices': {},
  252. }
  253. for prop in ('name', 'kernel',
  254. 'memory', 'maxmem', 'kernelopts',
  255. 'services', 'vcpus', 'features'
  256. 'include_in_backups', 'default_user', 'qrexec_timeout',
  257. 'autostart', 'pci_strictreset', 'debug',
  258. 'internal', 'netvm', 'template', 'label'):
  259. if not hasattr(vm, prop):
  260. continue
  261. vm_info['properties'][prop] = str(getattr(vm, prop))
  262. vm_info['default'][prop] = vm.property_is_default(prop)
  263. for dev_class in vm.devices.keys():
  264. vm_info['devices'][dev_class] = {}
  265. for dev_ass in vm.devices[dev_class].assignments():
  266. vm_info['devices'][dev_class][str(dev_ass.device)] = \
  267. dev_ass.options
  268. vms_info[vm.name] = vm_info
  269. return vms_info
  270. def assertCorrectlyRestored(self, vms_info, orig_hashes):
  271. ''' Verify if restored VMs are identical to those before backup.
  272. :param orig_vms: collection of original QubesVM objects
  273. :param orig_hashes: result of :py:meth:`vm_checksum` on original VMs,
  274. before backup
  275. :return:
  276. '''
  277. for vm_name in vms_info:
  278. vm_info = vms_info[vm_name]
  279. self.assertIn(vm_name, self.app.domains)
  280. restored_vm = self.app.domains[vm_name]
  281. for prop in vm_info['properties']:
  282. self.assertEqual(
  283. vm_info['properties'][prop],
  284. str(getattr(restored_vm, prop)),
  285. "VM {} - property {} not properly restored".format(
  286. vm_name, prop))
  287. self.assertEqual(
  288. vm_info['default'][prop],
  289. restored_vm.property_is_default(prop),
  290. "VM {} - property {} differs in being default".format(
  291. vm_name, prop))
  292. for dev_class in vm_info['devices']:
  293. for dev in vm_info['devices'][dev_class]:
  294. found = False
  295. for restored_dev_ass in restored_vm.devices[
  296. dev_class].assignments():
  297. if str(restored_dev_ass.device) == dev:
  298. found = True
  299. self.assertEqual(vm_info['devices'][dev_class][dev],
  300. restored_dev_ass.options,
  301. 'VM {} - {} device {} options mismatch'.format(
  302. vm_name, dev_class, str(dev)))
  303. self.assertTrue(found,
  304. 'VM {} - {} device {} not restored'.format(
  305. vm_name, dev_class, dev))
  306. if orig_hashes:
  307. hashes = self.vm_checksum([restored_vm])[restored_vm.name]
  308. self.assertEqual(orig_hashes[vm_name], hashes,
  309. "VM {} - disk images are not properly restored".format(
  310. vm_name))
  311. class TC_00_Backup(BackupTestsMixin, qubes.tests.SystemTestCase):
  312. def test_000_basic_backup(self):
  313. vms = self.create_backup_vms()
  314. try:
  315. orig_hashes = self.vm_checksum(vms)
  316. vms_info = self.get_vms_info(vms)
  317. self.make_backup(vms)
  318. self.remove_vms(reversed(vms))
  319. finally:
  320. del vms
  321. self.restore_backup()
  322. self.assertCorrectlyRestored(vms_info, orig_hashes)
  323. def test_001_compressed_backup(self):
  324. vms = self.create_backup_vms()
  325. try:
  326. orig_hashes = self.vm_checksum(vms)
  327. vms_info = self.get_vms_info(vms)
  328. self.make_backup(vms, compressed=True)
  329. self.remove_vms(reversed(vms))
  330. finally:
  331. del vms
  332. self.restore_backup()
  333. self.assertCorrectlyRestored(vms_info, orig_hashes)
  334. def test_004_sparse_multipart(self):
  335. vms = []
  336. try:
  337. vmname = self.make_vm_name('testhvm2')
  338. self.log.debug("Creating %s" % vmname)
  339. self.hvmtemplate = self.app.add_new_vm(
  340. qubes.vm.templatevm.TemplateVM, name=vmname, virt_mode='hvm', label='red')
  341. self.loop.run_until_complete(self.hvmtemplate.create_on_disk())
  342. self.fill_image(
  343. os.path.join(self.hvmtemplate.dir_path, '00file'),
  344. 195 * 1024 * 1024 - 4096 * 3)
  345. self.fill_image_vm(self.hvmtemplate, 'private',
  346. 195 * 1024 * 1024 - 4096 * 3)
  347. self.fill_image_vm(self.hvmtemplate, 'root', 1024 * 1024 * 1024,
  348. sparse=True)
  349. vms.append(self.hvmtemplate)
  350. self.app.save()
  351. orig_hashes = self.vm_checksum(vms)
  352. vms_info = self.get_vms_info(vms)
  353. self.make_backup(vms)
  354. self.remove_vms(reversed(vms))
  355. self.restore_backup()
  356. self.assertCorrectlyRestored(vms_info, orig_hashes)
  357. # TODO check vm.backup_timestamp
  358. finally:
  359. del vms
  360. def test_005_compressed_custom(self):
  361. vms = self.create_backup_vms()
  362. try:
  363. orig_hashes = self.vm_checksum(vms)
  364. vms_info = self.get_vms_info(vms)
  365. self.make_backup(vms, compression_filter="bzip2")
  366. self.remove_vms(reversed(vms))
  367. self.restore_backup()
  368. self.assertCorrectlyRestored(vms_info, orig_hashes)
  369. finally:
  370. del vms
  371. def test_010_selective_restore(self):
  372. # create backup with internal dependencies (template, netvm etc)
  373. # try restoring only AppVMs (but not templates, netvms) - should
  374. # handle according to options set
  375. exclude = [
  376. self.make_vm_name('test-net'),
  377. self.make_vm_name('template')
  378. ]
  379. def exclude_some(restore_info):
  380. for name in exclude:
  381. restore_info.pop(name)
  382. return restore_info
  383. vms = self.create_backup_vms()
  384. try:
  385. orig_hashes = self.vm_checksum(vms)
  386. vms_info = self.get_vms_info(vms)
  387. self.make_backup(vms, compression_filter="bzip2")
  388. self.remove_vms(reversed(vms))
  389. self.restore_backup(manipulate_restore_info=exclude_some)
  390. for vm_name in vms_info:
  391. if vm_name == self.make_vm_name('test1'):
  392. # netvm was set to 'test-inst-test-net' - excluded
  393. vms_info[vm_name]['properties']['netvm'] = \
  394. str(self.app.default_netvm)
  395. vms_info[vm_name]['default']['netvm'] = True
  396. elif vm_name == self.make_vm_name('custom'):
  397. # template was set to 'test-inst-template' - excluded
  398. vms_info[vm_name]['properties']['template'] = \
  399. str(self.app.default_template)
  400. for excluded in exclude:
  401. vms_info.pop(excluded, None)
  402. self.assertCorrectlyRestored(vms_info, orig_hashes)
  403. finally:
  404. del vms
  405. def test_020_encrypted_backup_non_ascii(self):
  406. vms = self.create_backup_vms()
  407. try:
  408. orig_hashes = self.vm_checksum(vms)
  409. vms_info = self.get_vms_info(vms)
  410. self.make_backup(vms, passphrase=u'zażółć gęślą jaźń')
  411. self.remove_vms(reversed(vms))
  412. self.restore_backup(passphrase=u'zażółć gęślą jaźń')
  413. self.assertCorrectlyRestored(vms_info, orig_hashes)
  414. finally:
  415. del vms
  416. def test_100_backup_dom0_no_restore(self):
  417. # do not write it into dom0 home itself...
  418. os.mkdir('/var/tmp/test-backup')
  419. self.backupdir = '/var/tmp/test-backup'
  420. self.make_backup([self.app.domains[0]])
  421. # TODO: think of some safe way to test restore...
  422. def test_200_restore_over_existing_directory(self):
  423. """
  424. Regression test for #1386
  425. :return:
  426. """
  427. vms = self.create_backup_vms()
  428. try:
  429. orig_hashes = self.vm_checksum(vms)
  430. vms_info = self.get_vms_info(vms)
  431. self.make_backup(vms)
  432. test_dir = vms[0].dir_path
  433. self.remove_vms(reversed(vms))
  434. os.mkdir(test_dir)
  435. with open(os.path.join(test_dir, 'some-file.txt'), 'w') as f:
  436. f.write('test file\n')
  437. self.restore_backup()
  438. self.assertCorrectlyRestored(vms_info, orig_hashes)
  439. finally:
  440. del vms
  441. def test_210_auto_rename(self):
  442. """
  443. Test for #869
  444. :return:
  445. """
  446. vms = self.create_backup_vms()
  447. vms_info = self.get_vms_info(vms)
  448. try:
  449. self.make_backup(vms)
  450. self.restore_backup(options={
  451. 'rename_conflicting': True
  452. })
  453. for vm_name in vms_info:
  454. with self.assertNotRaises(
  455. (qubes.exc.QubesVMNotFoundError, KeyError)):
  456. restored_vm = self.app.domains[vm_name + '1']
  457. if vms_info[vm_name]['properties']['netvm'] and \
  458. not vms_info[vm_name]['default']['netvm']:
  459. self.assertEqual(restored_vm.netvm.name,
  460. vms_info[vm_name]['properties']['netvm'] + '1')
  461. finally:
  462. del vms
  463. def _find_pool(self, volume_group, thin_pool):
  464. ''' Returns the pool matching the specified ``volume_group`` &
  465. ``thin_pool``, or None.
  466. '''
  467. pools = [p for p in self.app.pools
  468. if issubclass(p.__class__, qubes.storage.lvm.ThinPool)]
  469. for pool in pools:
  470. if pool.volume_group == volume_group \
  471. and pool.thin_pool == thin_pool:
  472. return pool
  473. return None
  474. @qubes.tests.storage_lvm.skipUnlessLvmPoolExists
  475. def test_300_backup_lvm(self):
  476. volume_group, thin_pool = \
  477. qubes.tests.storage_lvm.DEFAULT_LVM_POOL.split('/', 1)
  478. self.pool = self._find_pool(volume_group, thin_pool)
  479. if not self.pool:
  480. self.pool = self.loop.run_until_complete(
  481. self.app.add_pool(
  482. **qubes.tests.storage_lvm.POOL_CONF))
  483. self.created_pool = True
  484. vms = self.create_backup_vms(pool=self.pool)
  485. try:
  486. orig_hashes = self.vm_checksum(vms)
  487. vms_info = self.get_vms_info(vms)
  488. self.make_backup(vms)
  489. self.remove_vms(reversed(vms))
  490. self.restore_backup()
  491. self.assertCorrectlyRestored(vms_info, orig_hashes)
  492. finally:
  493. del vms
  494. @qubes.tests.storage_lvm.skipUnlessLvmPoolExists
  495. def test_301_restore_to_lvm(self):
  496. volume_group, thin_pool = \
  497. qubes.tests.storage_lvm.DEFAULT_LVM_POOL.split('/', 1)
  498. self.pool = self._find_pool(volume_group, thin_pool)
  499. if not self.pool:
  500. self.pool = self.loop.run_until_complete(
  501. self.app.add_pool(
  502. **qubes.tests.storage_lvm.POOL_CONF))
  503. self.created_pool = True
  504. vms = self.create_backup_vms()
  505. try:
  506. orig_hashes = self.vm_checksum(vms)
  507. vms_info = self.get_vms_info(vms)
  508. self.make_backup(vms)
  509. self.remove_vms(reversed(vms))
  510. self.restore_backup(options={'override_pool': self.pool.name})
  511. self.assertCorrectlyRestored(vms_info, orig_hashes)
  512. for vm_name in vms_info:
  513. vm = self.app.domains[vm_name]
  514. for volume in vm.volumes.values():
  515. if volume.save_on_stop:
  516. self.assertEqual(volume.pool, self.pool.name)
  517. finally:
  518. del vms
  519. class TC_10_BackupVMMixin(BackupTestsMixin):
  520. def setUp(self):
  521. super(TC_10_BackupVMMixin, self).setUp()
  522. self.backupvm = self.app.add_new_vm(
  523. qubes.vm.appvm.AppVM,
  524. label='red',
  525. name=self.make_vm_name('backupvm'),
  526. template=self.template
  527. )
  528. self.loop.run_until_complete(self.backupvm.create_on_disk())
  529. def test_100_send_to_vm_file_with_spaces(self):
  530. vms = self.create_backup_vms()
  531. orig_hashes = self.vm_checksum(vms)
  532. vms_info = self.get_vms_info(vms)
  533. try:
  534. self.loop.run_until_complete(self.backupvm.start())
  535. self.loop.run_until_complete(self.backupvm.run_for_stdio(
  536. "mkdir '/var/tmp/backup directory'"))
  537. self.make_backup(vms, target_vm=self.backupvm,
  538. compressed=True,
  539. target='/var/tmp/backup directory')
  540. self.remove_vms(reversed(vms))
  541. (backup_path, _) = self.loop.run_until_complete(
  542. self.backupvm.run_for_stdio("ls /var/tmp/backup*/qubes-backup*"))
  543. backup_path = backup_path.decode().strip()
  544. self.restore_backup(source=backup_path,
  545. appvm=self.backupvm)
  546. self.assertCorrectlyRestored(vms_info, orig_hashes)
  547. finally:
  548. del vms
  549. def test_110_send_to_vm_command(self):
  550. vms = self.create_backup_vms()
  551. orig_hashes = self.vm_checksum(vms)
  552. vms_info = self.get_vms_info(vms)
  553. try:
  554. self.loop.run_until_complete(self.backupvm.start())
  555. self.make_backup(vms, target_vm=self.backupvm,
  556. compressed=True,
  557. target='dd of=/var/tmp/backup-test')
  558. self.remove_vms(reversed(vms))
  559. self.restore_backup(source='dd if=/var/tmp/backup-test',
  560. appvm=self.backupvm)
  561. self.assertCorrectlyRestored(vms_info, orig_hashes)
  562. finally:
  563. del vms
  564. def test_110_send_to_vm_no_space(self):
  565. """
  566. Check whether backup properly report failure when no enough space is
  567. available
  568. :return:
  569. """
  570. vms = self.create_backup_vms()
  571. try:
  572. self.loop.run_until_complete(self.backupvm.start())
  573. self.loop.run_until_complete(self.backupvm.run_for_stdio(
  574. # Debian 7 has too old losetup to handle loop-control device
  575. "mknod /dev/loop0 b 7 0;"
  576. "truncate -s 50M /home/user/backup.img && "
  577. "mkfs.ext4 -F /home/user/backup.img && "
  578. "mkdir /home/user/backup && "
  579. "mount /home/user/backup.img /home/user/backup -o loop &&"
  580. "chmod 777 /home/user/backup",
  581. user="root"))
  582. with self.assertRaises(qubes.exc.QubesException):
  583. self.make_backup(vms, target_vm=self.backupvm,
  584. compressed=False,
  585. target='/home/user/backup',
  586. expect_failure=True)
  587. finally:
  588. del vms
  589. def create_testcases_for_templates():
  590. return qubes.tests.create_testcases_for_templates('TC_10_BackupVM',
  591. TC_10_BackupVMMixin, qubes.tests.SystemTestCase,
  592. module=sys.modules[__name__])
  593. def load_tests(loader, tests, pattern):
  594. tests.addTests(loader.loadTestsFromNames(
  595. create_testcases_for_templates()))
  596. return tests
  597. qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)