__init__.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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) 2013-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. """ Qubes storage system"""
  26. from __future__ import absolute_import
  27. import os
  28. import os.path
  29. import pkg_resources
  30. import lxml.etree
  31. import qubes
  32. import qubes.exc
  33. import qubes.utils
  34. import qubes.devices
  35. STORAGE_ENTRY_POINT = 'qubes.storage'
  36. class StoragePoolException(qubes.exc.QubesException):
  37. pass
  38. class Volume(object):
  39. ''' Encapsulates all data about a volume for serialization to qubes.xml and
  40. libvirt config.
  41. '''
  42. devtype = 'disk'
  43. domain = None
  44. path = None
  45. rw = True
  46. script = None
  47. usage = 0
  48. def __init__(self, name, pool, volume_type, vid=None, size=0,
  49. removable=False, **kwargs):
  50. super(Volume, self).__init__(**kwargs)
  51. self.name = str(name)
  52. self.pool = str(pool)
  53. self.vid = vid
  54. self.size = size
  55. self.volume_type = volume_type
  56. self.removable = removable
  57. def __xml__(self):
  58. return lxml.etree.Element('volume', **self.config)
  59. @property
  60. def config(self):
  61. ''' return config data for serialization to qubes.xml '''
  62. return {'name': self.name,
  63. 'pool': self.pool,
  64. 'volume_type': self.volume_type}
  65. def __repr__(self):
  66. return '{}(name={!s}, pool={!r}, vid={!r}, volume_type={!r})'.format(
  67. self.__class__.__name__, self.name, self.pool, self.vid,
  68. self.volume_type)
  69. def block_device(self):
  70. ''' Return :py:class:`qubes.devices.BlockDevice` for serialization in
  71. the libvirt XML template as <disk>.
  72. '''
  73. return qubes.devices.BlockDevice(self.path, self.name, self.script,
  74. self.rw, self.domain, self.devtype)
  75. class Storage(object):
  76. ''' Class for handling VM virtual disks.
  77. This is base class for all other implementations, mostly with Xen on Linux
  78. in mind.
  79. '''
  80. def __init__(self, vm):
  81. #: Domain for which we manage storage
  82. self.vm = vm
  83. self.log = self.vm.log
  84. #: Additional drive (currently used only by HVM)
  85. self.drive = None
  86. self.pools = {}
  87. if hasattr(vm, 'volume_config'):
  88. for name, conf in self.vm.volume_config.items():
  89. assert 'pool' in conf, "Pool missing in volume_config" % str(
  90. conf)
  91. pool = self.vm.app.get_pool(conf['pool'])
  92. self.vm.volumes[name] = pool.init_volume(self.vm, conf)
  93. self.pools[name] = pool
  94. @property
  95. def kernels_dir(self):
  96. '''Directory where kernel resides.
  97. If :py:attr:`self.vm.kernel` is :py:obj:`None`, the this points inside
  98. :py:attr:`self.vm.dir_path`
  99. '''
  100. assert 'kernel' in self.vm.volumes, "VM has no kernel pool"
  101. return self.vm.volumes['kernel'].kernels_dir
  102. def get_disk_utilization(self):
  103. ''' Returns summed up disk utilization for all domain volumes '''
  104. result = 0
  105. for volume in self.vm.volumes.values():
  106. result += volume.usage
  107. return result
  108. # TODO Remove this wrapper
  109. def get_disk_utilization_private_img(self):
  110. # pylint: disable=invalid-name,missing-docstring
  111. return self.vm.volume['private'].usage
  112. # TODO Remove this wrapper
  113. def get_private_img_sz(self):
  114. # :pylint: disable=missing-docstring
  115. return self.vm.volume['private'].size
  116. def resize(self, volume, size):
  117. ''' Resize volume '''
  118. self.get_pool(volume).resize(volume, size)
  119. # TODO rename it to create()
  120. def create_on_disk(self, source_template=None):
  121. # :pylint: disable=missing-docstring
  122. if source_template is None and hasattr(self.vm, 'template'):
  123. source_template = self.vm.template
  124. old_umask = os.umask(002)
  125. for name, volume in self.vm.volumes.items():
  126. source_volume = None
  127. if source_template and hasattr(source_template, 'volumes'):
  128. source_volume = source_template.volumes[name]
  129. self.get_pool(volume).create(volume, source_volume=source_volume)
  130. os.umask(old_umask)
  131. def clone(self, src_vm):
  132. self.vm.log.info('Creating directory: {0}'.format(self.vm.dir_path))
  133. if not os.path.exists(self.vm.dir_path):
  134. self.log.info('Creating directory: {0}'.format(self.vm.dir_path))
  135. os.makedirs(self.vm.dir_path)
  136. for name, target in self.vm.volumes.items():
  137. pool = self.get_pool(target)
  138. source = src_vm.volumes[name]
  139. volume = pool.clone(source, target)
  140. assert volume, "%s.clone() returned '%s'" % (pool.__class__,
  141. volume)
  142. self.vm.volumes[name] = volume
  143. def rename(self, old_name, new_name):
  144. ''' Notify the pools that the domain was renamed '''
  145. volumes = self.vm.volumes
  146. for name, volume in volumes.items():
  147. pool = self.get_pool(volume)
  148. volumes[name] = pool.rename(volume, old_name, new_name)
  149. def verify_files(self):
  150. '''Verify that the storage is sane.
  151. On success, returns normally. On failure, raises exception.
  152. '''
  153. if not os.path.exists(self.vm.dir_path):
  154. raise qubes.exc.QubesVMError(
  155. self.vm,
  156. 'VM directory does not exist: {}'.format(self.vm.dir_path))
  157. def remove(self):
  158. ''' Remove all the volumes.
  159. Errors on removal are catched and logged.
  160. '''
  161. for name, volume in self.vm.volumes.items():
  162. self.log.info('Removing volume %s: %s' % (name, volume.vid))
  163. try:
  164. self.get_pool(volume).remove(volume)
  165. except (IOError, OSError) as e:
  166. self.vm.log.exception("Failed to remove volume %s", name, e)
  167. def start(self):
  168. ''' Execute the start method on each pool '''
  169. for volume in self.vm.volumes.values():
  170. self.get_pool(volume).start(volume)
  171. def stop(self):
  172. ''' Execute the start method on each pool '''
  173. for volume in self.vm.volumes.values():
  174. self.get_pool(volume).stop(volume)
  175. def get_pool(self, volume):
  176. ''' Helper function '''
  177. assert isinstance(volume, Volume), "You need to pass a Volume"
  178. return self.pools[volume.name]
  179. def commit_template_changes(self):
  180. for volume in self.vm.volumes.values():
  181. if volume.volume_type == 'origin':
  182. self.get_pool(volume).commit_template_changes(volume)
  183. class Pool(object):
  184. ''' A Pool is used to manage different kind of volumes (File
  185. based/LVM/Btrfs/...).
  186. 3rd Parties providing own storage implementations will need to extend
  187. this class.
  188. '''
  189. private_img_size = qubes.config.defaults['private_img_size']
  190. root_img_size = qubes.config.defaults['root_img_size']
  191. def __init__(self, name, **kwargs):
  192. super(Pool, self).__init__(**kwargs)
  193. self.name = name
  194. kwargs['name'] = self.name
  195. def __xml__(self):
  196. return lxml.etree.Element('pool', **self.config)
  197. def create(self, volume, source_volume):
  198. ''' Create the given volume on disk or copy from provided
  199. `source_volume`.
  200. '''
  201. raise NotImplementedError("Pool %s has create() not implemented" %
  202. self.name)
  203. def commit_template_changes(self, volume):
  204. ''' Update origin device '''
  205. raise NotImplementedError(
  206. "Pool %s has commit_template_changes() not implemented" %
  207. self.name)
  208. @property
  209. def config(self):
  210. ''' Returns the pool config to be written to qubes.xml '''
  211. raise NotImplementedError("Pool %s has config() not implemented" %
  212. self.name)
  213. def clone(self, source, target):
  214. ''' Clone volume '''
  215. raise NotImplementedError("Pool %s has clone() not implemented" %
  216. self.name)
  217. def destroy(self):
  218. raise NotImplementedError("Pool %s has destroy() not implemented" %
  219. self.name)
  220. def remove(self, volume):
  221. ''' Remove volume'''
  222. raise NotImplementedError("Pool %s has remove() not implemented" %
  223. self.name)
  224. def rename(self, volume, old_name, new_name):
  225. ''' Called when the domain changes its name '''
  226. raise NotImplementedError("Pool %s has rename() not implemented" %
  227. self.name)
  228. def start(self, volume):
  229. ''' Do what ever is needed on start '''
  230. raise NotImplementedError("Pool %s has start() not implemented" %
  231. self.name)
  232. def setup(self):
  233. raise NotImplementedError("Pool %s has setup() not implemented" %
  234. self.name)
  235. def stop(self, volume):
  236. ''' Do what ever is needed on stop'''
  237. raise NotImplementedError("Pool %s has stop() not implemented" %
  238. self.name)
  239. def init_volume(self, vm, volume_config):
  240. ''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
  241. '''
  242. raise NotImplementedError("Pool %s has init_volume() not implemented" %
  243. self.name)
  244. def pool_drivers():
  245. """ Return a list of EntryPoints names """
  246. return [ep.name
  247. for ep in pkg_resources.iter_entry_points(STORAGE_ENTRY_POINT)]