file.py 17 KB

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