__init__.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  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 string
  30. import lxml.etree
  31. import pkg_resources
  32. import qubes
  33. import qubes.devices
  34. import qubes.exc
  35. import qubes.utils
  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,
  50. removable=False, internal=False, **kwargs):
  51. super(Volume, self).__init__(**kwargs)
  52. self.name = str(name)
  53. self.pool = str(pool)
  54. self.vid = vid
  55. self.size = size
  56. self.volume_type = volume_type
  57. self.removable = removable
  58. self.internal = internal
  59. def __xml__(self):
  60. return lxml.etree.Element('volume', **self.config)
  61. @property
  62. def config(self):
  63. ''' return config data for serialization to qubes.xml '''
  64. return {'name': self.name,
  65. 'pool': self.pool,
  66. 'volume_type': self.volume_type}
  67. def __repr__(self):
  68. return '{!r}'.format(self.pool + ':' + self.vid)
  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. def __eq__(self, other):
  76. return other.pool == self.pool and other.vid == self.vid \
  77. and other.volume_type == self.volume_type
  78. def __neq__(self, other):
  79. return not self.__eq__(other)
  80. def __hash__(self):
  81. return hash('%s:%s %s' % (self.pool, self.vid, self.volume_type))
  82. class Storage(object):
  83. ''' Class for handling VM virtual disks.
  84. This is base class for all other implementations, mostly with Xen on Linux
  85. in mind.
  86. '''
  87. AVAILABLE_FRONTENDS = set(['xvd' + c for c in string.ascii_lowercase])
  88. def __init__(self, vm):
  89. #: Domain for which we manage storage
  90. self.vm = vm
  91. self.log = self.vm.log
  92. #: Additional drive (currently used only by HVM)
  93. self.drive = None
  94. self.pools = {}
  95. if hasattr(vm, 'volume_config'):
  96. for name, conf in self.vm.volume_config.items():
  97. assert 'pool' in conf, "Pool missing in volume_config" % str(
  98. conf)
  99. pool = self.vm.app.get_pool(conf['pool'])
  100. self.vm.volumes[name] = pool.init_volume(self.vm, conf)
  101. self.pools[name] = pool
  102. def attach(self, volume, rw=False):
  103. ''' Attach a volume to the domain '''
  104. assert self.vm.is_running()
  105. if self._is_already_attached(volume):
  106. self.vm.log.info("{!r} already attached".format(volume))
  107. return
  108. try:
  109. frontend = self.unused_frontend()
  110. except IndexError:
  111. raise StoragePoolException("No unused frontend found")
  112. disk = lxml.etree.Element("disk")
  113. disk.set('type', 'block')
  114. disk.set('device', 'disk')
  115. lxml.etree.SubElement(disk, 'driver').set('name', 'phy')
  116. lxml.etree.SubElement(disk, 'source').set('dev', '/dev/%s' % volume.vid)
  117. lxml.etree.SubElement(disk, 'target').set('dev', frontend)
  118. if not rw:
  119. lxml.etree.SubElement(disk, 'readonly')
  120. if self.vm.qid != 0:
  121. lxml.etree.SubElement(disk, 'backenddomain').set(
  122. 'name', volume.pool.split('p_')[1])
  123. xml_string = lxml.etree.tostring(disk, encoding='utf-8')
  124. self.vm.libvirt_domain.attachDevice(xml_string)
  125. # trigger watches to update device status
  126. # FIXME: this should be removed once libvirt will report such
  127. # events itself
  128. # self.vm.qdb.write('/qubes-block-devices', '') ← do we need this?
  129. def _is_already_attached(self, volume):
  130. ''' Checks if the given volume is already attached '''
  131. parsed_xml = lxml.etree.fromstring(self.vm.libvirt_domain.XMLDesc())
  132. disk_sources = parsed_xml.xpath("//domain/devices/disk/source")
  133. for source in disk_sources:
  134. if source.get('dev') == '/dev/%s' % volume.vid:
  135. return True
  136. return False
  137. def detach(self, volume):
  138. ''' Detach a volume from domain '''
  139. parsed_xml = lxml.etree.fromstring(self.vm.libvirt_domain.XMLDesc())
  140. disks = parsed_xml.xpath("//domain/devices/disk")
  141. for disk in disks:
  142. source = disk.xpath('source')[0]
  143. if source.get('dev') == '/dev/%s' % volume.vid:
  144. disk_xml = lxml.etree.tostring(disk, encoding='utf-8')
  145. self.vm.libvirt_domain.detachDevice(disk_xml)
  146. return
  147. raise StoragePoolException('Volume {!r} is not attached'.format(volume))
  148. @property
  149. def kernels_dir(self):
  150. '''Directory where kernel resides.
  151. If :py:attr:`self.vm.kernel` is :py:obj:`None`, the this points inside
  152. :py:attr:`self.vm.dir_path`
  153. '''
  154. assert 'kernel' in self.vm.volumes, "VM has no kernel pool"
  155. return self.vm.volumes['kernel'].kernels_dir
  156. def get_disk_utilization(self):
  157. ''' Returns summed up disk utilization for all domain volumes '''
  158. result = 0
  159. for volume in self.vm.volumes.values():
  160. result += volume.usage
  161. return result
  162. def resize(self, volume, size):
  163. ''' Resize volume '''
  164. self.get_pool(volume).resize(volume, size)
  165. def create(self, source_template=None):
  166. if source_template is None and hasattr(self.vm, 'template'):
  167. source_template = self.vm.template
  168. old_umask = os.umask(002)
  169. for name, volume in self.vm.volumes.items():
  170. source_volume = None
  171. if source_template and hasattr(source_template, 'volumes'):
  172. source_volume = source_template.volumes[name]
  173. self.get_pool(volume).create(volume, source_volume=source_volume)
  174. os.umask(old_umask)
  175. def clone(self, src_vm):
  176. self.vm.log.info('Creating directory: {0}'.format(self.vm.dir_path))
  177. if not os.path.exists(self.vm.dir_path):
  178. self.log.info('Creating directory: {0}'.format(self.vm.dir_path))
  179. os.makedirs(self.vm.dir_path)
  180. for name, target in self.vm.volumes.items():
  181. pool = self.get_pool(target)
  182. source = src_vm.volumes[name]
  183. volume = pool.clone(source, target)
  184. assert volume, "%s.clone() returned '%s'" % (pool.__class__,
  185. volume)
  186. self.vm.volumes[name] = volume
  187. @property
  188. def outdated_volumes(self):
  189. ''' Returns a list of outdated volumes '''
  190. result = []
  191. if self.vm.is_halted():
  192. return result
  193. volumes = self.vm.volumes
  194. for volume in volumes.values():
  195. pool = self.get_pool(volume)
  196. if pool.is_outdated(volume):
  197. result += [volume]
  198. return result
  199. def rename(self, old_name, new_name):
  200. ''' Notify the pools that the domain was renamed '''
  201. volumes = self.vm.volumes
  202. for name, volume in volumes.items():
  203. pool = self.get_pool(volume)
  204. volumes[name] = pool.rename(volume, old_name, new_name)
  205. def verify_files(self):
  206. '''Verify that the storage is sane.
  207. On success, returns normally. On failure, raises exception.
  208. '''
  209. if not os.path.exists(self.vm.dir_path):
  210. raise qubes.exc.QubesVMError(
  211. self.vm,
  212. 'VM directory does not exist: {}'.format(self.vm.dir_path))
  213. for volume in self.vm.volumes.values():
  214. self.get_pool(volume).verify(volume)
  215. self.vm.fire_event('domain-verify-files')
  216. def remove(self):
  217. ''' Remove all the volumes.
  218. Errors on removal are catched and logged.
  219. '''
  220. for name, volume in self.vm.volumes.items():
  221. self.log.info('Removing volume %s: %s' % (name, volume.vid))
  222. try:
  223. self.get_pool(volume).remove(volume)
  224. except (IOError, OSError) as e:
  225. self.vm.log.exception("Failed to remove volume %s", name, e)
  226. def start(self):
  227. ''' Execute the start method on each pool '''
  228. for volume in self.vm.volumes.values():
  229. self.get_pool(volume).start(volume)
  230. def stop(self):
  231. ''' Execute the start method on each pool '''
  232. for volume in self.vm.volumes.values():
  233. self.get_pool(volume).stop(volume)
  234. def get_pool(self, volume):
  235. ''' Helper function '''
  236. assert isinstance(volume, Volume), "You need to pass a Volume"
  237. return self.pools[volume.name]
  238. def commit_template_changes(self):
  239. for volume in self.vm.volumes.values():
  240. if volume.volume_type == 'origin':
  241. self.get_pool(volume).commit_template_changes(volume)
  242. def unused_frontend(self):
  243. ''' Find an unused device name '''
  244. unused_frontends = self.AVAILABLE_FRONTENDS.difference(
  245. self.used_frontends)
  246. return sorted(unused_frontends)[0]
  247. @property
  248. def used_frontends(self):
  249. ''' Used device names '''
  250. xml = self.vm.libvirt_domain.XMLDesc()
  251. parsed_xml = lxml.etree.fromstring(xml)
  252. return set([target.get('dev', None)
  253. for target in parsed_xml.xpath(
  254. "//domain/devices/disk/target")])
  255. class Pool(object):
  256. ''' A Pool is used to manage different kind of volumes (File
  257. based/LVM/Btrfs/...).
  258. 3rd Parties providing own storage implementations will need to extend
  259. this class.
  260. '''
  261. private_img_size = qubes.config.defaults['private_img_size']
  262. root_img_size = qubes.config.defaults['root_img_size']
  263. def __init__(self, name, **kwargs):
  264. super(Pool, self).__init__(**kwargs)
  265. self.name = name
  266. kwargs['name'] = self.name
  267. def __xml__(self):
  268. return lxml.etree.Element('pool', **self.config)
  269. def create(self, volume, source_volume):
  270. ''' Create the given volume on disk or copy from provided
  271. `source_volume`.
  272. '''
  273. raise NotImplementedError("Pool %s has create() not implemented" %
  274. self.name)
  275. def commit_template_changes(self, volume):
  276. ''' Update origin device '''
  277. raise NotImplementedError(
  278. "Pool %s has commit_template_changes() not implemented" %
  279. self.name)
  280. @property
  281. def config(self):
  282. ''' Returns the pool config to be written to qubes.xml '''
  283. raise NotImplementedError("Pool %s has config() not implemented" %
  284. self.name)
  285. def clone(self, source, target):
  286. ''' Clone volume '''
  287. raise NotImplementedError("Pool %s has clone() not implemented" %
  288. self.name)
  289. def destroy(self):
  290. raise NotImplementedError("Pool %s has destroy() not implemented" %
  291. self.name)
  292. def is_outdated(self, volume):
  293. raise NotImplementedError("Pool %s has is_outdated() not implemented" %
  294. self.name)
  295. def remove(self, volume):
  296. ''' Remove volume'''
  297. raise NotImplementedError("Pool %s has remove() not implemented" %
  298. self.name)
  299. def rename(self, volume, old_name, new_name):
  300. ''' Called when the domain changes its name '''
  301. raise NotImplementedError("Pool %s has rename() not implemented" %
  302. self.name)
  303. def start(self, volume):
  304. ''' Do what ever is needed on start '''
  305. raise NotImplementedError("Pool %s has start() not implemented" %
  306. self.name)
  307. def setup(self):
  308. raise NotImplementedError("Pool %s has setup() not implemented" %
  309. self.name)
  310. def stop(self, volume):
  311. ''' Do what ever is needed on stop'''
  312. raise NotImplementedError("Pool %s has stop() not implemented" %
  313. self.name)
  314. def init_volume(self, vm, volume_config):
  315. ''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
  316. '''
  317. raise NotImplementedError("Pool %s has init_volume() not implemented" %
  318. self.name)
  319. def verify(self, volume):
  320. ''' Verifies the volume. '''
  321. raise NotImplementedError("Pool %s has verify() not implemented" %
  322. self.name)
  323. @property
  324. def volumes(self):
  325. ''' Return a list of volumes managed by this pool '''
  326. raise NotImplementedError("Pool %s has volumes() not implemented" %
  327. self.name)
  328. def pool_drivers():
  329. """ Return a list of EntryPoints names """
  330. return [ep.name
  331. for ep in pkg_resources.iter_entry_points(STORAGE_ENTRY_POINT)]