xen.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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. from __future__ import absolute_import
  26. import os
  27. import os.path
  28. import re
  29. import subprocess
  30. from qubes.storage import Pool, StoragePoolException, Volume
  31. BLKSIZE = 512
  32. class XenPool(Pool):
  33. ''' File based 'original' disk implementation '''
  34. driver = 'xen'
  35. def __init__(self, name=None, dir_path=None):
  36. super(XenPool, self).__init__(name=name)
  37. assert dir_path, "No pool dir_path specified"
  38. self.dir_path = os.path.normpath(dir_path)
  39. create_dir_if_not_exists(self.dir_path)
  40. appvms_path = os.path.join(self.dir_path, 'appvms')
  41. create_dir_if_not_exists(appvms_path)
  42. vm_templates_path = os.path.join(self.dir_path, 'vm-templates')
  43. create_dir_if_not_exists(vm_templates_path)
  44. def clone(self, source, target):
  45. ''' Clones the volume if the `source.pool` if the source is a
  46. :py:class:`XenVolume`.
  47. '''
  48. if issubclass(XenVolume, source.__class__):
  49. raise StoragePoolException('Volumes %s and %s use different pools'
  50. % (source.__class__, target.__class__))
  51. if source.volume_type not in ['origin', 'read-write']:
  52. return target
  53. copy_file(source.vid, target.vid)
  54. return target
  55. def create(self, volume, source_volume=None):
  56. _type = volume.volume_type
  57. size = volume.size
  58. if _type == 'origin':
  59. create_sparse_file(volume.path_origin, size)
  60. create_sparse_file(volume.path_cow, size)
  61. elif _type in ['read-write'] and source_volume:
  62. copy_file(source_volume.path, volume.path)
  63. elif _type in ['read-write', 'volatile']:
  64. create_sparse_file(volume.path, size)
  65. return volume
  66. @property
  67. def config(self):
  68. return {
  69. 'name': self.name,
  70. 'dir_path': self.dir_path,
  71. 'driver': XenPool.driver,
  72. }
  73. def resize(self, volume, size):
  74. ''' Expands volume, throws
  75. :py:class:`qubst.storage.StoragePoolException` if given size is
  76. less than current_size
  77. '''
  78. _type = volume.volume_type
  79. if _type not in ['origin', 'read-write', 'volatile']:
  80. raise StoragePoolException('Can not resize a %s volume %s' %
  81. (_type, volume.vid))
  82. if size <= volume.size:
  83. raise StoragePoolException(
  84. 'For your own safety, shrinking of %s is'
  85. ' disabled. If you really know what you'
  86. ' are doing, use `truncate` on %s manually.' %
  87. (volume.name, volume.vid))
  88. if _type == 'origin':
  89. path = volume.path_origin
  90. elif _type in ['read-write', 'volatile']:
  91. path = volume.path
  92. with open(path, 'a+b') as fd:
  93. fd.truncate(size)
  94. self._resize_loop_device(path)
  95. def remove(self, volume):
  96. if volume.volume_type in ['read-write', 'volatile']:
  97. _remove_if_exists(volume.vid)
  98. elif volume.volume_type == 'origin':
  99. _remove_if_exists(volume.vid)
  100. _remove_if_exists(volume.path_cow)
  101. def _resize_loop_device(self, path):
  102. # find loop device if any
  103. p = subprocess.Popen(
  104. ['sudo', 'losetup', '--associated', path],
  105. stdout=subprocess.PIPE)
  106. result = p.communicate()
  107. m = re.match(r'^(/dev/loop\d+):\s', result[0])
  108. if m is not None:
  109. loop_dev = m.group(1)
  110. # resize loop device
  111. subprocess.check_call(['sudo', 'losetup', '--set-capacity',
  112. loop_dev])
  113. def commit_template_changes(self, volume):
  114. if volume.volume_type != 'origin':
  115. return volume
  116. if os.path.exists(volume.path_cow):
  117. os.rename(volume.path_cow, volume.path_cow + '.old')
  118. old_umask = os.umask(002)
  119. with open(volume.path_cow, 'w') as f_cow:
  120. f_cow.truncate(volume.size)
  121. os.umask(old_umask)
  122. return volume
  123. def start(self, volume):
  124. if volume.volume_type == 'volatile':
  125. self._reset_volume(volume)
  126. if volume.volume_type in ['origin', 'snapshot']:
  127. _check_path(volume.path_origin)
  128. _check_path(volume.path_cow)
  129. else:
  130. _check_path(volume.path)
  131. return volume
  132. def stop(self, volume):
  133. pass
  134. def _reset_volume(self, volume):
  135. ''' Remove and recreate a volatile volume '''
  136. assert volume.volume_type == 'volatile', "Not a volatile volume"
  137. assert volume.size
  138. _remove_if_exists(volume)
  139. with open(volume.path, "w") as f_volatile:
  140. f_volatile.truncate(volume.size)
  141. return volume
  142. def target_dir(self, vm):
  143. """ Returns the path to vmdir depending on the type of the VM.
  144. The default QubesOS file storage saves the vm images in three
  145. different directories depending on the ``QubesVM`` type:
  146. * ``appvms`` for ``QubesAppVm`` or ``QubesHvm``
  147. * ``vm-templates`` for ``QubesTemplateVm`` or ``QubesTemplateHvm``
  148. Args:
  149. vm: a QubesVM
  150. pool_dir: the root directory of the pool
  151. Returns:
  152. string (str) absolute path to the directory where the vm files
  153. are stored
  154. """
  155. if vm.is_template():
  156. subdir = 'vm-templates'
  157. elif vm.is_disposablevm():
  158. subdir = 'appvms'
  159. return os.path.join(self.dir_path, subdir,
  160. vm.template.name + '-dvm')
  161. else:
  162. subdir = 'appvms'
  163. return os.path.join(self.dir_path, subdir, vm.name)
  164. def init_volume(self, vm, volume_config):
  165. assert 'volume_type' in volume_config, "Volume type missing " \
  166. + str(volume_config)
  167. volume_type = volume_config['volume_type']
  168. known_types = {
  169. 'read-write': ReadWriteFile,
  170. 'read-only': ReadOnlyFile,
  171. 'origin': OriginFile,
  172. 'snapshot': SnapshotFile,
  173. 'volatile': VolatileFile,
  174. }
  175. if volume_type not in known_types:
  176. raise StoragePoolException("Unknown volume type " + volume_type)
  177. if volume_type in ['snapshot', 'read-only']:
  178. origin_pool = vm.app.get_pool(volume_config['pool'])
  179. assert isinstance(origin_pool,
  180. XenPool), 'Origin volume not a xen volume'
  181. volume_config['target_dir'] = origin_pool.target_dir(vm.template)
  182. name = volume_config['name']
  183. volume_config['size'] = vm.template.volume_config[name]['size']
  184. else:
  185. volume_config['target_dir'] = self.target_dir(vm)
  186. return known_types[volume_type](**volume_config)
  187. class XenVolume(Volume):
  188. ''' Parent class for the xen volumes implementation which expects a
  189. `target_dir` param on initialization.
  190. '''
  191. def __init__(self, target_dir, **kwargs):
  192. self.target_dir = target_dir
  193. assert self.target_dir, "target_dir not specified"
  194. super(XenVolume, self).__init__(**kwargs)
  195. class SizeMixIn(XenVolume):
  196. ''' A mix in which expects a `size` param to be > 0 on initialization and
  197. provides a usage property wrapper.
  198. '''
  199. def __init__(self, size=0, **kwargs):
  200. super(SizeMixIn, self).__init__(size=int(size), **kwargs)
  201. assert size, 'Empty size provided'
  202. assert size > 0, 'Size for volume ' + kwargs['name'] + ' is <=0'
  203. @property
  204. def usage(self):
  205. ''' Returns the actualy used space '''
  206. return get_disk_usage(self.vid)
  207. @property
  208. def config(self):
  209. ''' return config data for serialization to qubes.xml '''
  210. return {'name': self.name,
  211. 'pool': self.pool,
  212. 'size': str(self.size),
  213. 'volume_type': self.volume_type}
  214. class ReadWriteFile(SizeMixIn):
  215. # :pylint: disable=missing-docstring
  216. def __init__(self, **kwargs):
  217. super(ReadWriteFile, self).__init__(**kwargs)
  218. self.path = os.path.join(self.target_dir, self.name + '.img')
  219. self.vid = self.path
  220. class ReadOnlyFile(XenVolume):
  221. # :pylint: disable=missing-docstring
  222. usage = 0
  223. def __init__(self, size=0, **kwargs):
  224. # :pylint: disable=unused-argument
  225. super(ReadOnlyFile, self).__init__(size=int(size), **kwargs)
  226. self.path = self.vid
  227. class OriginFile(SizeMixIn):
  228. # :pylint: disable=missing-docstring
  229. script = 'block-origin'
  230. def __init__(self, **kwargs):
  231. super(OriginFile, self).__init__(**kwargs)
  232. self.path_origin = os.path.join(self.target_dir, self.name + '.img')
  233. self.path_cow = os.path.join(self.target_dir, self.name + '-cow.img')
  234. self.path = '%s:%s' % (self.path_origin, self.path_cow)
  235. self.vid = self.path_origin
  236. def commit(self):
  237. raise NotImplementedError
  238. @property
  239. def usage(self):
  240. result = 0
  241. if os.path.exists(self.path_origin):
  242. result += get_disk_usage(self.path_origin)
  243. if os.path.exists(self.path_cow):
  244. result += get_disk_usage(self.path_cow)
  245. return result
  246. class SnapshotFile(XenVolume):
  247. # :pylint: disable=missing-docstring
  248. script = 'block-snapshot'
  249. rw = False
  250. usage = 0
  251. def __init__(self, name=None, size=None, **kwargs):
  252. assert size
  253. super(SnapshotFile, self).__init__(name=name, size=int(size), **kwargs)
  254. self.path_origin = os.path.join(self.target_dir, name + '.img')
  255. self.path_cow = os.path.join(self.target_dir, name + '-cow.img')
  256. self.path = '%s:%s' % (self.path_origin, self.path_cow)
  257. self.vid = self.path_origin
  258. @property
  259. def created(self):
  260. return os.path.exists(self.path_origin) and os.path.exists(
  261. self.path_cow)
  262. class VolatileFile(SizeMixIn):
  263. # :pylint: disable=missing-docstring
  264. def __init__(self, **kwargs):
  265. super(VolatileFile, self).__init__(**kwargs)
  266. self.path = os.path.join(self.target_dir, 'volatile.img')
  267. self.vid = self.path
  268. def create_sparse_file(path, size):
  269. ''' Create an empty sparse file '''
  270. if os.path.exists(path):
  271. raise IOError("Volume %s already exists", path)
  272. parent_dir = os.path.dirname(path)
  273. if not os.path.exists(parent_dir):
  274. os.makedirs(parent_dir)
  275. with open(path, 'a+b') as fh:
  276. fh.truncate(size)
  277. def get_disk_usage_one(st):
  278. '''Extract disk usage of one inode from its stat_result struct.
  279. If known, get real disk usage, as written to device by filesystem, not
  280. logical file size. Those values may be different for sparse files.
  281. :param os.stat_result st: stat result
  282. :returns: disk usage
  283. '''
  284. try:
  285. return st.st_blocks * BLKSIZE
  286. except AttributeError:
  287. return st.st_size
  288. def get_disk_usage(path):
  289. '''Get real disk usage of given path (file or directory).
  290. When *path* points to directory, then it is evaluated recursively.
  291. This function tries estiate real disk usage. See documentation of
  292. :py:func:`get_disk_usage_one`.
  293. :param str path: path to evaluate
  294. :returns: disk usage
  295. '''
  296. try:
  297. st = os.lstat(path)
  298. except OSError:
  299. return 0
  300. ret = get_disk_usage_one(st)
  301. # if path is not a directory, this is skipped
  302. for dirpath, dirnames, filenames in os.walk(path):
  303. for name in dirnames + filenames:
  304. ret += get_disk_usage_one(os.lstat(os.path.join(dirpath, name)))
  305. return ret
  306. def create_dir_if_not_exists(path):
  307. """ Check if a directory exists in if not create it.
  308. This method does not create any parent directories.
  309. """
  310. if not os.path.exists(path):
  311. os.mkdir(path)
  312. def copy_file(source, destination):
  313. '''Effective file copy, preserving sparse files etc.
  314. '''
  315. # TODO: Windows support
  316. # We prefer to use Linux's cp, because it nicely handles sparse files
  317. assert os.path.exists(source), \
  318. "Missing the source %s to copy from" % source
  319. assert not os.path.exists(destination), \
  320. "Destination %s already exists" % destination
  321. parent_dir = os.path.dirname(destination)
  322. if not os.path.exists(parent_dir):
  323. os.makedirs(parent_dir)
  324. try:
  325. subprocess.check_call(['cp', '--reflink=auto', source, destination])
  326. except subprocess.CalledProcessError:
  327. raise IOError('Error while copying {!r} to {!r}'.format(source,
  328. destination))
  329. def _remove_if_exists(volume):
  330. if os.path.exists(volume.path):
  331. os.remove(volume.path)
  332. def _check_path(path):
  333. ''' Raise an StoragePoolException if ``path`` does not exist'''
  334. if not os.path.exists(path):
  335. raise StoragePoolException('Missing image file: %s' % path)