__init__.py 10 KB

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