file.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  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 program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation; either version 2 of the License, or
  12. # (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 along
  20. # with this program; if not, write to the Free Software Foundation, Inc.,
  21. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  22. #
  23. ''' This module contains pool implementations backed by file images'''
  24. from __future__ import absolute_import
  25. import os
  26. import os.path
  27. import re
  28. import subprocess
  29. import qubes.storage
  30. BLKSIZE = 512
  31. class FilePool(qubes.storage.Pool):
  32. ''' File based 'original' disk implementation
  33. ''' # pylint: disable=protected-access
  34. driver = 'file'
  35. def __init__(self, revisions_to_keep=1, dir_path=None, **kwargs):
  36. super(FilePool, self).__init__(revisions_to_keep=revisions_to_keep,
  37. **kwargs)
  38. assert dir_path, "No pool dir_path specified"
  39. self.dir_path = os.path.normpath(dir_path)
  40. self._volumes = []
  41. @property
  42. def config(self):
  43. return {
  44. 'name': self.name,
  45. 'dir_path': self.dir_path,
  46. 'driver': FilePool.driver,
  47. 'revisions_to_keep': self.revisions_to_keep
  48. }
  49. def init_volume(self, vm, volume_config):
  50. volume_config['dir_path'] = self.dir_path
  51. if os.path.join(self.dir_path, self._vid_prefix(vm)) == vm.dir_path:
  52. volume_config['backward_comp'] = True
  53. if 'vid' not in volume_config:
  54. volume_config['vid'] = os.path.join(
  55. self._vid_prefix(vm), volume_config['name'])
  56. try:
  57. if volume_config['reset_on_start']:
  58. volume_config['revisions_to_keep'] = 0
  59. except KeyError:
  60. pass
  61. finally:
  62. if 'revisions_to_keep' not in volume_config:
  63. volume_config['revisions_to_keep'] = self.revisions_to_keep
  64. volume_config['pool'] = self
  65. volume = FileVolume(**volume_config)
  66. self._volumes += [volume]
  67. return volume
  68. def destroy(self):
  69. pass
  70. def setup(self):
  71. create_dir_if_not_exists(self.dir_path)
  72. appvms_path = os.path.join(self.dir_path, 'appvms')
  73. create_dir_if_not_exists(appvms_path)
  74. vm_templates_path = os.path.join(self.dir_path, 'vm-templates')
  75. create_dir_if_not_exists(vm_templates_path)
  76. @staticmethod
  77. def _vid_prefix(vm):
  78. ''' Helper to create a prefix for the vid for volume
  79. ''' # FIX Remove this if we drop the file backend
  80. import qubes.vm.templatevm # pylint: disable=redefined-outer-name
  81. import qubes.vm.dispvm # pylint: disable=redefined-outer-name
  82. if isinstance(vm, qubes.vm.templatevm.TemplateVM):
  83. subdir = 'vm-templates'
  84. else:
  85. subdir = 'appvms'
  86. return os.path.join(subdir, vm.name)
  87. def target_dir(self, vm):
  88. """ Returns the path to vmdir depending on the type of the VM.
  89. The default QubesOS file storage saves the vm images in three
  90. different directories depending on the ``QubesVM`` type:
  91. * ``appvms`` for ``QubesAppVm`` or ``QubesHvm``
  92. * ``vm-templates`` for ``QubesTemplateVm`` or ``QubesTemplateHvm``
  93. Args:
  94. vm: a QubesVM
  95. pool_dir: the root directory of the pool
  96. Returns:
  97. string (str) absolute path to the directory where the vm files
  98. are stored
  99. """
  100. return os.path.join(self.dir_path, self._vid_prefix(vm))
  101. def list_volumes(self):
  102. return self._volumes
  103. class FileVolume(qubes.storage.Volume):
  104. ''' Parent class for the xen volumes implementation which expects a
  105. `target_dir` param on initialization. '''
  106. def __init__(self, dir_path, backward_comp=False, **kwargs):
  107. self.dir_path = dir_path
  108. self.backward_comp = backward_comp
  109. assert self.dir_path, "dir_path not specified"
  110. super(FileVolume, self).__init__(**kwargs)
  111. if self.snap_on_start and self.source is None:
  112. msg = "snap_on_start specified on {!r} but no volume source set"
  113. msg = msg.format(self.name)
  114. raise qubes.storage.StoragePoolException(msg)
  115. elif not self.snap_on_start and self.source is not None:
  116. msg = "source specified on {!r} but no snap_on_start set"
  117. msg = msg.format(self.name)
  118. raise qubes.storage.StoragePoolException(msg)
  119. if self._is_snapshot:
  120. img_name = self.source.vid + '-cow.img'
  121. self.path_source_cow = os.path.join(self.dir_path, img_name)
  122. elif self._is_volume or self._is_volatile:
  123. pass
  124. elif self._is_origin:
  125. pass
  126. else:
  127. assert False, 'This should not happen'
  128. def create(self):
  129. assert isinstance(self.size, int) and self.size > 0, \
  130. 'Volatile volume size must be > 0'
  131. if self._is_origin:
  132. create_sparse_file(self.path, self.size)
  133. create_sparse_file(self.path_cow, self.size)
  134. elif not self._is_snapshot:
  135. if self.source is not None:
  136. source_path = os.path.join(self.dir_path,
  137. self.source.vid + '.img')
  138. copy_file(source_path, self.path)
  139. elif self._is_volatile:
  140. pass
  141. else:
  142. create_sparse_file(self.path, self.size)
  143. def remove(self):
  144. if not self.internal:
  145. return # do not remove random attached file volumes
  146. elif self._is_snapshot:
  147. return # no need to remove, because it's just a snapshot
  148. else:
  149. _remove_if_exists(self.path)
  150. if self._is_origin:
  151. _remove_if_exists(self.path_cow)
  152. def is_dirty(self):
  153. return False # TODO: How to implement this?
  154. def resize(self, size):
  155. ''' Expands volume, throws
  156. :py:class:`qubst.storage.qubes.storage.StoragePoolException` if
  157. given size is less than current_size
  158. ''' # pylint: disable=no-self-use
  159. if not self.rw:
  160. msg = 'Can not resize reađonly volume {!s}'.format(self)
  161. raise qubes.storage.StoragePoolException(msg)
  162. if size <= self.size:
  163. raise qubes.storage.StoragePoolException(
  164. 'For your own safety, shrinking of %s is'
  165. ' disabled. If you really know what you'
  166. ' are doing, use `truncate` on %s manually.' %
  167. (self.name, self.vid))
  168. with open(self.path, 'a+b') as fd:
  169. fd.truncate(size)
  170. p = subprocess.Popen(['sudo', 'losetup', '--associated', self.path],
  171. stdout=subprocess.PIPE)
  172. result = p.communicate()
  173. m = re.match(r'^(/dev/loop\d+):\s', result[0].decode())
  174. if m is not None:
  175. loop_dev = m.group(1)
  176. # resize loop device
  177. subprocess.check_call(['sudo', 'losetup', '--set-capacity',
  178. loop_dev])
  179. self.size = size
  180. def commit(self):
  181. msg = 'Tried to commit a non commitable volume {!r}'.format(self)
  182. assert (self._is_origin or self._is_volume) and self.rw, msg
  183. if self._is_volume:
  184. return self
  185. if os.path.exists(self.path_cow):
  186. old_path = self.path_cow + '.old'
  187. os.rename(self.path_cow, old_path)
  188. old_umask = os.umask(0o002)
  189. with open(self.path_cow, 'w') as f_cow:
  190. f_cow.truncate(self.size)
  191. os.umask(old_umask)
  192. return self
  193. def export(self):
  194. return self.path
  195. def import_volume(self, src_volume):
  196. msg = "Can not import snapshot volume {!s} in to pool {!s} "
  197. msg = msg.format(src_volume, self)
  198. assert not src_volume.snap_on_start, msg
  199. if self.save_on_stop:
  200. _remove_if_exists(self.path)
  201. copy_file(src_volume.export(), self.path)
  202. return self
  203. def import_data(self):
  204. return self.path
  205. def reset(self):
  206. ''' Remove and recreate a volatile volume '''
  207. assert self._is_volatile, "Not a volatile volume"
  208. assert isinstance(self.size, int) and self.size > 0, \
  209. 'Volatile volume size must be > 0'
  210. _remove_if_exists(self.path)
  211. with open(self.path, "w") as f_volatile:
  212. f_volatile.truncate(self.size)
  213. return self
  214. def revert(self, revision=None):
  215. if revision is not None:
  216. try:
  217. return self.revisions[revision]
  218. except KeyError:
  219. msg = "Volume {!r} does not have revision {!s}"
  220. msg = msg.format(self, revision)
  221. raise qubes.storage.StoragePoolException(msg)
  222. else:
  223. try:
  224. old_path = self.revisions.values().pop()
  225. os.rename(old_path, self.path_cow)
  226. except IndexError:
  227. msg = "Volume {!r} does not have old revisions".format(self)
  228. raise qubes.storage.StoragePoolException(msg)
  229. def start(self):
  230. if self._is_volatile:
  231. self.reset()
  232. else:
  233. _check_path(self.path)
  234. if self.snap_on_start:
  235. if not self.save_on_stop:
  236. # make sure previous snapshot is removed - even if VM
  237. # shutdown routing wasn't called (power interrupt or so)
  238. _remove_if_exists(self.path_cow)
  239. try:
  240. _check_path(self.path_cow)
  241. except qubes.storage.StoragePoolException:
  242. create_sparse_file(self.path_cow, self.size)
  243. _check_path(self.path_cow)
  244. if hasattr(self, 'path_source_cow'):
  245. try:
  246. _check_path(self.path_source_cow)
  247. except qubes.storage.StoragePoolException:
  248. create_sparse_file(self.path_source_cow, self.size)
  249. _check_path(self.path_source_cow)
  250. return self
  251. def stop(self):
  252. if self.save_on_stop:
  253. self.commit()
  254. elif self.snap_on_start:
  255. _remove_if_exists(self.path_cow)
  256. else:
  257. _remove_if_exists(self.path)
  258. return self
  259. @property
  260. def path(self):
  261. if self._is_snapshot:
  262. return os.path.join(self.dir_path, self.source.vid + '.img')
  263. return os.path.join(self.dir_path, self.vid + '.img')
  264. @property
  265. def path_cow(self):
  266. img_name = self.vid + '-cow.img'
  267. return os.path.join(self.dir_path, img_name)
  268. def verify(self):
  269. ''' Verifies the volume. '''
  270. if not os.path.exists(self.path) and not self._is_volatile:
  271. msg = 'Missing image file: {!s}.'.format(self.path)
  272. raise qubes.storage.StoragePoolException(msg)
  273. return True
  274. @property
  275. def script(self):
  276. if self._is_volume or self._is_volatile:
  277. return None
  278. elif self._is_origin:
  279. return 'block-origin'
  280. elif self._is_origin_snapshot or self._is_snapshot:
  281. return 'block-snapshot'
  282. def block_device(self):
  283. ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
  284. the libvirt XML template as <disk>.
  285. '''
  286. path = self.path
  287. if self._is_snapshot:
  288. path += ":" + self.path_source_cow
  289. if self._is_origin or self._is_snapshot:
  290. path += ":" + self.path_cow
  291. return qubes.storage.BlockDevice(path, self.name, self.script, self.rw,
  292. self.domain, self.devtype)
  293. @property
  294. def revisions(self):
  295. if not hasattr(self, 'path_cow'):
  296. return {}
  297. old_revision = self.path_cow + '.old' # pylint: disable=no-member
  298. if not os.path.exists(old_revision):
  299. return {}
  300. seconds = os.path.getctime(old_revision)
  301. iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
  302. return {iso_date: old_revision}
  303. @property
  304. def usage(self):
  305. ''' Returns the actualy used space '''
  306. return get_disk_usage(self.vid)
  307. @property
  308. def _is_volatile(self):
  309. ''' Internal helper. Useful for differentiating volume handling '''
  310. return not self.snap_on_start and not self.save_on_stop
  311. @property
  312. def _is_origin(self):
  313. ''' Internal helper. Useful for differentiating volume handling '''
  314. # pylint: disable=line-too-long
  315. return self.save_on_stop and self.revisions_to_keep > 0 # NOQA
  316. @property
  317. def _is_snapshot(self):
  318. ''' Internal helper. Useful for differentiating volume handling '''
  319. return self.snap_on_start and not self.save_on_stop
  320. @property
  321. def _is_origin_snapshot(self):
  322. ''' Internal helper. Useful for differentiating volume handling '''
  323. return self.snap_on_start and self.save_on_stop
  324. @property
  325. def _is_volume(self):
  326. ''' Internal helper. Usefull for differentiating volume handling '''
  327. # pylint: disable=line-too-long
  328. return not self.snap_on_start and self.save_on_stop and self.revisions_to_keep == 0 # NOQA
  329. def create_sparse_file(path, size):
  330. ''' Create an empty sparse file '''
  331. if os.path.exists(path):
  332. raise IOError("Volume %s already exists", path)
  333. parent_dir = os.path.dirname(path)
  334. if not os.path.exists(parent_dir):
  335. os.makedirs(parent_dir)
  336. with open(path, 'a+b') as fh:
  337. fh.truncate(size)
  338. def get_disk_usage_one(st):
  339. '''Extract disk usage of one inode from its stat_result struct.
  340. If known, get real disk usage, as written to device by filesystem, not
  341. logical file size. Those values may be different for sparse files.
  342. :param os.stat_result st: stat result
  343. :returns: disk usage
  344. '''
  345. try:
  346. return st.st_blocks * BLKSIZE
  347. except AttributeError:
  348. return st.st_size
  349. def get_disk_usage(path):
  350. '''Get real disk usage of given path (file or directory).
  351. When *path* points to directory, then it is evaluated recursively.
  352. This function tries estiate real disk usage. See documentation of
  353. :py:func:`get_disk_usage_one`.
  354. :param str path: path to evaluate
  355. :returns: disk usage
  356. '''
  357. try:
  358. st = os.lstat(path)
  359. except OSError:
  360. return 0
  361. ret = get_disk_usage_one(st)
  362. # if path is not a directory, this is skipped
  363. for dirpath, dirnames, filenames in os.walk(path):
  364. for name in dirnames + filenames:
  365. ret += get_disk_usage_one(os.lstat(os.path.join(dirpath, name)))
  366. return ret
  367. def create_dir_if_not_exists(path):
  368. """ Check if a directory exists in if not create it.
  369. This method does not create any parent directories.
  370. """
  371. if not os.path.exists(path):
  372. os.mkdir(path)
  373. def copy_file(source, destination):
  374. '''Effective file copy, preserving sparse files etc.'''
  375. # We prefer to use Linux's cp, because it nicely handles sparse files
  376. assert os.path.exists(source), \
  377. "Missing the source %s to copy from" % source
  378. assert not os.path.exists(destination), \
  379. "Destination %s already exists" % destination
  380. parent_dir = os.path.dirname(destination)
  381. if not os.path.exists(parent_dir):
  382. os.makedirs(parent_dir)
  383. try:
  384. cmd = ['cp', '--sparse=auto',
  385. '--reflink=auto', source, destination]
  386. subprocess.check_call(cmd)
  387. except subprocess.CalledProcessError:
  388. raise IOError('Error while copying {!r} to {!r}'.format(source,
  389. destination))
  390. def _remove_if_exists(path):
  391. ''' Removes a file if it exist, silently succeeds if file does not exist '''
  392. if os.path.exists(path):
  393. os.remove(path)
  394. def _check_path(path):
  395. ''' Raise an StoragePoolException if ``path`` does not exist'''
  396. if not os.path.exists(path):
  397. msg = 'Missing image file: %s' % path
  398. raise qubes.storage.StoragePoolException(msg)