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