file.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  5. # Copyright (C) 2013-2015 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  8. #
  9. # This library is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU Lesser General Public
  11. # License as published by the Free Software Foundation; either
  12. # version 2.1 of the License, or (at your option) any later version.
  13. #
  14. # This library 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 GNU
  17. # Lesser General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU Lesser General Public
  20. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  21. #
  22. ''' This module contains pool implementations backed by file images'''
  23. import asyncio
  24. import os
  25. import os.path
  26. import re
  27. import subprocess
  28. from contextlib import suppress
  29. import qubes.storage
  30. import qubes.utils
  31. BLKSIZE = 512
  32. # 256 KiB chunk, same as in block-snapshot script. Header created by
  33. # struct.pack('<4I', 0x70416e53, 1, 1, 256) mimicking write_header()
  34. # in linux/drivers/md/dm-snap-persistent.c
  35. EMPTY_SNAPSHOT = b'SnAp\x01\x00\x00\x00\x01\x00\x00\x00\x00\x01\x00\x00' \
  36. + bytes(262128)
  37. class FilePool(qubes.storage.Pool):
  38. ''' File based 'original' disk implementation
  39. Volumes are stored in sparse files. Additionally device-mapper is used for
  40. applying copy-on-write layer.
  41. Quick reference on device-mapper layers:
  42. snap_on_start save_on_stop layout
  43. yes yes not supported
  44. no yes snapshot-origin(volume.img, volume-cow.img)
  45. yes no snapshot(
  46. snapshot(source.img, source-cow.img),
  47. volume-cow.img)
  48. no no volume.img directly
  49. ''' # pylint: disable=protected-access
  50. driver = 'file'
  51. def __init__(self, *, name, revisions_to_keep=1, dir_path):
  52. super().__init__(name=name, revisions_to_keep=revisions_to_keep)
  53. self.dir_path = os.path.normpath(dir_path)
  54. self._volumes = []
  55. @property
  56. def config(self):
  57. return {
  58. 'name': self.name,
  59. 'dir_path': self.dir_path,
  60. 'driver': FilePool.driver,
  61. 'revisions_to_keep': self.revisions_to_keep
  62. }
  63. def init_volume(self, vm, volume_config):
  64. if volume_config.get('snap_on_start', False) and \
  65. volume_config.get('save_on_stop', False):
  66. raise NotImplementedError(
  67. 'snap_on_start + save_on_stop not supported by file driver')
  68. volume_config['dir_path'] = self.dir_path
  69. if 'vid' not in volume_config:
  70. volume_config['vid'] = os.path.join(
  71. self._vid_prefix(vm), volume_config['name'])
  72. try:
  73. if not volume_config.get('save_on_stop', False):
  74. volume_config['revisions_to_keep'] = 0
  75. except KeyError:
  76. pass
  77. if 'revisions_to_keep' not in volume_config:
  78. volume_config['revisions_to_keep'] = self.revisions_to_keep
  79. volume_config['pool'] = self
  80. volume = FileVolume(**volume_config)
  81. self._volumes += [volume]
  82. return volume
  83. @property
  84. def revisions_to_keep(self):
  85. return self._revisions_to_keep
  86. @revisions_to_keep.setter
  87. def revisions_to_keep(self, value):
  88. value = int(value)
  89. if value > 1:
  90. raise NotImplementedError(
  91. 'FilePool supports maximum 1 volume revision to keep')
  92. self._revisions_to_keep = value
  93. def destroy(self):
  94. pass
  95. def setup(self):
  96. create_dir_if_not_exists(self.dir_path)
  97. appvms_path = os.path.join(self.dir_path, 'appvms')
  98. create_dir_if_not_exists(appvms_path)
  99. vm_templates_path = os.path.join(self.dir_path, 'vm-templates')
  100. create_dir_if_not_exists(vm_templates_path)
  101. @staticmethod
  102. def _vid_prefix(vm):
  103. ''' Helper to create a prefix for the vid for volume
  104. ''' # FIX Remove this if we drop the file backend
  105. import qubes.vm.templatevm # pylint: disable=redefined-outer-name
  106. import qubes.vm.dispvm # pylint: disable=redefined-outer-name
  107. if isinstance(vm, qubes.vm.templatevm.TemplateVM):
  108. subdir = 'vm-templates'
  109. else:
  110. subdir = 'appvms'
  111. return os.path.join(subdir, vm.name)
  112. def target_dir(self, vm):
  113. """ Returns the path to vmdir depending on the type of the VM.
  114. The default QubesOS file storage saves the vm images in three
  115. different directories depending on the ``QubesVM`` type:
  116. * ``appvms`` for ``QubesAppVm`` or ``QubesHvm``
  117. * ``vm-templates`` for ``QubesTemplateVm`` or ``QubesTemplateHvm``
  118. Args:
  119. vm: a QubesVM
  120. pool_dir: the root directory of the pool
  121. Returns:
  122. string (str) absolute path to the directory where the vm files
  123. are stored
  124. """
  125. return os.path.join(self.dir_path, self._vid_prefix(vm))
  126. def list_volumes(self):
  127. return self._volumes
  128. @property
  129. def size(self):
  130. try:
  131. statvfs = os.statvfs(self.dir_path)
  132. return statvfs.f_frsize * statvfs.f_blocks
  133. except FileNotFoundError:
  134. return 0
  135. @property
  136. def usage(self):
  137. try:
  138. statvfs = os.statvfs(self.dir_path)
  139. return statvfs.f_frsize * (statvfs.f_blocks - statvfs.f_bfree)
  140. except FileNotFoundError:
  141. return 0
  142. def included_in(self, app):
  143. ''' Check if there is pool containing this one - either as a
  144. filesystem or its LVM volume'''
  145. return qubes.storage.search_pool_containing_dir(
  146. [pool for pool in app.pools.values() if pool is not self],
  147. self.dir_path)
  148. class FileVolume(qubes.storage.Volume):
  149. ''' Parent class for the xen volumes implementation which expects a
  150. `target_dir` param on initialization. '''
  151. _marker_running = object()
  152. _marker_exported = object()
  153. def __init__(self, dir_path, **kwargs):
  154. self.dir_path = dir_path
  155. assert self.dir_path, "dir_path not specified"
  156. self._revisions_to_keep = 0
  157. self._export_lock = None
  158. super().__init__(**kwargs)
  159. if self.snap_on_start:
  160. img_name = self.source.vid + '-cow.img'
  161. self.path_source_cow = os.path.join(self.dir_path, img_name)
  162. @property
  163. def revisions_to_keep(self):
  164. return self._revisions_to_keep
  165. @revisions_to_keep.setter
  166. def revisions_to_keep(self, value):
  167. if int(value) > 1:
  168. raise NotImplementedError(
  169. 'FileVolume supports maximum 1 volume revision to keep')
  170. self._revisions_to_keep = int(value)
  171. def create(self):
  172. assert isinstance(self.size, int) and self.size > 0, \
  173. 'Volume size must be > 0'
  174. if not self.snap_on_start:
  175. create_sparse_file(self.path, self.size, permissions=0o664)
  176. def remove(self):
  177. if not self.snap_on_start:
  178. _remove_if_exists(self.path)
  179. if self.snap_on_start or self.save_on_stop:
  180. _remove_if_exists(self.path_cow)
  181. _remove_if_exists(self.path_cow + '.old')
  182. def is_outdated(self):
  183. return False # avoid spamming the log with NotImplementedError
  184. def is_dirty(self):
  185. if self.save_on_stop:
  186. with suppress(FileNotFoundError), open(self.path_cow, 'rb') as cow:
  187. cow_used = os.fstat(cow.fileno()).st_blocks * BLKSIZE
  188. return (cow_used > 0 and
  189. (cow_used > len(EMPTY_SNAPSHOT) or
  190. cow.read(len(EMPTY_SNAPSHOT)) != EMPTY_SNAPSHOT or
  191. cow_used > cow.seek(0, os.SEEK_HOLE)))
  192. return False
  193. def resize(self, size):
  194. ''' Expands volume, throws
  195. :py:class:`qubst.storage.qubes.storage.StoragePoolException` if
  196. given size is less than current_size
  197. ''' # pylint: disable=no-self-use
  198. if not self.rw:
  199. msg = 'Can not resize reađonly volume {!s}'.format(self)
  200. raise qubes.storage.StoragePoolException(msg)
  201. if size < self.size:
  202. raise qubes.storage.StoragePoolException(
  203. 'For your own safety, shrinking of %s is'
  204. ' disabled. If you really know what you'
  205. ' are doing, use `truncate` on %s manually.' %
  206. (self.name, self.vid))
  207. with open(self.path, 'a+b') as fd:
  208. fd.truncate(size)
  209. p = subprocess.Popen(['losetup', '--associated', self.path],
  210. stdout=subprocess.PIPE)
  211. result = p.communicate()
  212. m = re.match(r'^(/dev/loop\d+):\s', result[0].decode())
  213. if m is not None:
  214. loop_dev = m.group(1)
  215. # resize loop device
  216. subprocess.check_call(['losetup', '--set-capacity',
  217. loop_dev])
  218. self._size = size
  219. def commit(self):
  220. msg = 'Tried to commit a non commitable volume {!r}'.format(self)
  221. assert self.save_on_stop and self.rw, msg
  222. if os.path.exists(self.path_cow):
  223. if self.revisions_to_keep:
  224. old_path = self.path_cow + '.old'
  225. os.rename(self.path_cow, old_path)
  226. else:
  227. os.unlink(self.path_cow)
  228. create_sparse_file(self.path_cow, self.size)
  229. return self
  230. def export(self):
  231. if self._export_lock is not None:
  232. assert self._export_lock is FileVolume._marker_running, \
  233. 'nested calls to export()'
  234. raise qubes.storage.StoragePoolException(
  235. 'file pool cannot export running volumes')
  236. if self.is_dirty():
  237. raise qubes.storage.StoragePoolException(
  238. 'file pool cannot export dirty volumes')
  239. self._export_lock = FileVolume._marker_exported
  240. return self.path
  241. def export_end(self, path):
  242. assert self._export_lock is not FileVolume._marker_running, \
  243. 'ending an export on a running volume?'
  244. self._export_lock = None
  245. @asyncio.coroutine
  246. def import_volume(self, src_volume):
  247. if src_volume.snap_on_start:
  248. raise qubes.storage.StoragePoolException(
  249. "Can not import snapshot volume {!s} in to pool {!s} ".format(
  250. src_volume, self))
  251. if self.save_on_stop:
  252. _remove_if_exists(self.path)
  253. path = yield from qubes.utils.coro_maybe(src_volume.export())
  254. try:
  255. copy_file(path, self.path)
  256. finally:
  257. yield from qubes.utils.coro_maybe(src_volume.export_end(path))
  258. return self
  259. def import_data(self, size):
  260. if not self.save_on_stop:
  261. raise qubes.storage.StoragePoolException(
  262. "Can not import into save_on_stop=False volume {!s}".format(
  263. self))
  264. create_sparse_file(self.path_import, size)
  265. return self.path_import
  266. def import_data_end(self, success):
  267. if success:
  268. os.rename(self.path_import, self.path)
  269. else:
  270. os.unlink(self.path_import)
  271. return self
  272. def reset(self):
  273. ''' Remove and recreate a volatile volume '''
  274. assert not self.snap_on_start and not self.save_on_stop, \
  275. "Not a volatile volume"
  276. assert isinstance(self.size, int) and self.size > 0, \
  277. 'Volatile volume size must be > 0'
  278. _remove_if_exists(self.path)
  279. create_sparse_file(self.path, self.size)
  280. return self
  281. def start(self):
  282. if self._export_lock is not None:
  283. assert self._export_lock is FileVolume._marker_exported, \
  284. 'nested calls to start()'
  285. raise qubes.storage.StoragePoolException(
  286. 'file pool cannot start a VM with an exported volume')
  287. self._export_lock = FileVolume._marker_running
  288. if not self.save_on_stop and not self.snap_on_start:
  289. self.reset()
  290. else:
  291. if not self.save_on_stop:
  292. # make sure previous snapshot is removed - even if VM
  293. # shutdown routine wasn't called (power interrupt or so)
  294. _remove_if_exists(self.path_cow)
  295. if not os.path.exists(self.path_cow):
  296. create_sparse_file(self.path_cow, self.size)
  297. if not self.snap_on_start:
  298. _check_path(self.path)
  299. if hasattr(self, 'path_source_cow'):
  300. if not os.path.exists(self.path_source_cow):
  301. create_sparse_file(self.path_source_cow, self.size)
  302. return self
  303. def stop(self):
  304. assert self._export_lock is not FileVolume._marker_exported, \
  305. 'trying to stop exported file volume?'
  306. if self.save_on_stop:
  307. self.commit()
  308. elif self.snap_on_start:
  309. _remove_if_exists(self.path_cow)
  310. else:
  311. _remove_if_exists(self.path)
  312. self._export_lock = None
  313. return self
  314. @property
  315. def path(self):
  316. if self.snap_on_start:
  317. return os.path.join(self.dir_path, self.source.vid + '.img')
  318. return os.path.join(self.dir_path, self.vid + '.img')
  319. @property
  320. def path_cow(self):
  321. img_name = self.vid + '-cow.img'
  322. return os.path.join(self.dir_path, img_name)
  323. @property
  324. def path_import(self):
  325. img_name = self.vid + '-import.img'
  326. return os.path.join(self.dir_path, img_name)
  327. def verify(self):
  328. ''' Verifies the volume. '''
  329. if not os.path.exists(self.path) and \
  330. (self.snap_on_start or self.save_on_stop):
  331. msg = 'Missing image file: {!s}.'.format(self.path)
  332. raise qubes.storage.StoragePoolException(msg)
  333. return True
  334. @property
  335. def script(self):
  336. if not self.snap_on_start and not self.save_on_stop:
  337. return None
  338. if not self.snap_on_start and self.save_on_stop:
  339. return 'block-origin'
  340. if self.snap_on_start:
  341. return 'block-snapshot'
  342. return None
  343. def block_device(self):
  344. ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
  345. the libvirt XML template as <disk>.
  346. '''
  347. path = self.path
  348. if self.snap_on_start:
  349. path += ":" + self.path_source_cow
  350. if self.snap_on_start or self.save_on_stop:
  351. path += ":" + self.path_cow
  352. return qubes.storage.BlockDevice(path, self.name, self.script, self.rw,
  353. self.domain, self.devtype)
  354. @property
  355. def revisions(self):
  356. if not hasattr(self, 'path_cow'):
  357. return {}
  358. old_revision = self.path_cow + '.old' # pylint: disable=no-member
  359. if not os.path.exists(old_revision):
  360. return {}
  361. seconds = os.path.getctime(old_revision)
  362. iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
  363. return {'old': iso_date}
  364. @property
  365. def size(self):
  366. with suppress(FileNotFoundError):
  367. self._size = os.path.getsize(self.path)
  368. return self._size
  369. @size.setter
  370. def size(self, _):
  371. raise qubes.storage.StoragePoolException(
  372. "You shouldn't use volume size setter, use resize method instead")
  373. @property
  374. def usage(self):
  375. ''' Returns the actualy used space '''
  376. usage = 0
  377. if self.save_on_stop or self.snap_on_start:
  378. usage = get_disk_usage(self.path_cow)
  379. if self.save_on_stop or not self.snap_on_start:
  380. usage += get_disk_usage(self.path)
  381. return usage
  382. def create_sparse_file(path, size, permissions=None):
  383. ''' Create an empty sparse file '''
  384. if os.path.exists(path):
  385. raise IOError("Volume %s already exists" % path)
  386. parent_dir = os.path.dirname(path)
  387. if not os.path.exists(parent_dir):
  388. os.makedirs(parent_dir)
  389. with open(path, 'a+b') as fh:
  390. if permissions is not None:
  391. os.fchmod(fh.fileno(), permissions)
  392. fh.truncate(size)
  393. def get_disk_usage_one(st):
  394. '''Extract disk usage of one inode from its stat_result struct.
  395. If known, get real disk usage, as written to device by filesystem, not
  396. logical file size. Those values may be different for sparse files.
  397. :param os.stat_result st: stat result
  398. :returns: disk usage
  399. '''
  400. try:
  401. return st.st_blocks * BLKSIZE
  402. except AttributeError:
  403. return st.st_size
  404. def get_disk_usage(path):
  405. '''Get real disk usage of given path (file or directory).
  406. When *path* points to directory, then it is evaluated recursively.
  407. This function tries estimate real disk usage. See documentation of
  408. :py:func:`get_disk_usage_one`.
  409. :param str path: path to evaluate
  410. :returns: disk usage
  411. '''
  412. try:
  413. st = os.lstat(path)
  414. except OSError:
  415. return 0
  416. ret = get_disk_usage_one(st)
  417. # if path is not a directory, this is skipped
  418. for dirpath, dirnames, filenames in os.walk(path):
  419. for name in dirnames + filenames:
  420. ret += get_disk_usage_one(os.lstat(os.path.join(dirpath, name)))
  421. return ret
  422. def create_dir_if_not_exists(path):
  423. """ Check if a directory exists in if not create it.
  424. This method does not create any parent directories.
  425. """
  426. if not os.path.exists(path):
  427. os.mkdir(path)
  428. def copy_file(source, destination):
  429. '''Effective file copy, preserving sparse files etc.'''
  430. # We prefer to use Linux's cp, because it nicely handles sparse files
  431. assert os.path.exists(source), \
  432. "Missing the source %s to copy from" % source
  433. assert not os.path.exists(destination), \
  434. "Destination %s already exists" % destination
  435. parent_dir = os.path.dirname(destination)
  436. if not os.path.exists(parent_dir):
  437. os.makedirs(parent_dir)
  438. try:
  439. cmd = ['cp', '--sparse=always',
  440. '--reflink=auto', source, destination]
  441. subprocess.check_call(cmd)
  442. except subprocess.CalledProcessError:
  443. raise IOError('Error while copying {!r} to {!r}'.format(source,
  444. destination))
  445. def _remove_if_exists(path):
  446. ''' Removes a file if it exist, silently succeeds if file does not exist '''
  447. if os.path.exists(path):
  448. os.remove(path)
  449. def _check_path(path):
  450. ''' Raise an StoragePoolException if ``path`` does not exist'''
  451. if not os.path.exists(path):
  452. msg = 'Missing image file: %s' % path
  453. raise qubes.storage.StoragePoolException(msg)