__init__.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2013-2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  5. # Copyright (C) 2013-2015 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  8. #
  9. # This program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation; either version 2 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License along
  20. # with this program; if not, write to the Free Software Foundation, Inc.,
  21. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  22. #
  23. """ Qubes storage system"""
  24. from __future__ import absolute_import
  25. import inspect
  26. import os
  27. import os.path
  28. import string # pylint: disable=deprecated-module
  29. import time
  30. from datetime import datetime
  31. import asyncio
  32. import lxml.etree
  33. import pkg_resources
  34. import qubes
  35. import qubes.exc
  36. import qubes.utils
  37. STORAGE_ENTRY_POINT = 'qubes.storage'
  38. class StoragePoolException(qubes.exc.QubesException):
  39. ''' A general storage exception '''
  40. pass
  41. class BlockDevice(object):
  42. ''' Represents a storage block device. '''
  43. # pylint: disable=too-few-public-methods
  44. def __init__(self, path, name, script=None, rw=True, domain=None,
  45. devtype='disk'):
  46. assert name, 'Missing device name'
  47. assert path, 'Missing device path'
  48. self.path = path
  49. self.name = name
  50. self.rw = rw
  51. self.script = script
  52. self.domain = domain
  53. self.devtype = devtype
  54. class Volume(object):
  55. ''' Encapsulates all data about a volume for serialization to qubes.xml and
  56. libvirt config.
  57. Keep in mind!
  58. volatile = not snap_on_start and not save_on_stop
  59. snapshot = snap_on_start and not save_on_stop
  60. origin = not snap_on_start and save_on_stop
  61. origin_snapshot = snap_on_start and save_on_stop
  62. '''
  63. devtype = 'disk'
  64. domain = None
  65. path = None
  66. script = None
  67. #: disk space used by this volume, can be smaller than :py:attr:`size`
  68. #: for sparse volumes
  69. usage = 0
  70. def __init__(self, name, pool, vid, internal=False, removable=False,
  71. revisions_to_keep=0, rw=False, save_on_stop=False, size=0,
  72. snap_on_start=False, source=None, **kwargs):
  73. ''' Initialize a volume.
  74. :param str name: The name of the volume inside owning domain
  75. :param Pool pool: The pool object
  76. :param str vid: Volume identifier needs to be unique in pool
  77. :param bool internal: If `True` volume is hidden when qvm-block ls
  78. is used
  79. :param bool removable: If `True` volume can be detached from vm at
  80. run time
  81. :param int revisions_to_keep: Amount of revisions to keep around
  82. :param bool rw: If true volume will be mounted read-write
  83. :param bool snap_on_start: Create a snapshot from source on
  84. start, instead of using volume own data
  85. :param bool save_on_stop: Write changes to the volume in
  86. vm.stop(), otherwise - discard
  87. :param Volume source: other volume in same pool to make snapshot
  88. from, required if *snap_on_start*=`True`
  89. :param str/int size: Size of the volume
  90. '''
  91. super(Volume, self).__init__(**kwargs)
  92. assert isinstance(pool, Pool)
  93. assert source is None or (isinstance(source, Volume)
  94. and source.pool == pool)
  95. #: Name of the volume in a domain it's attached to (like `root` or
  96. #: `private`).
  97. self.name = str(name)
  98. #: :py:class:`Pool` instance owning this volume
  99. self.pool = pool
  100. self.internal = internal
  101. self.removable = removable
  102. #: How many revisions of the volume to keep. Each revision is created
  103. # at :py:meth:`stop`, if :py:attr:`save_on_stop` is True
  104. self.revisions_to_keep = int(revisions_to_keep)
  105. #: Should this volume be writable by domain.
  106. self.rw = rw
  107. #: Should volume state be saved or discarded at :py:meth:`stop`
  108. self.save_on_stop = save_on_stop
  109. self._size = int(size)
  110. #: Should the volume state be initialized with a snapshot of
  111. #: same-named volume of domain's template.
  112. self.snap_on_start = snap_on_start
  113. #: source volume for :py:attr:`snap_on_start` volumes
  114. self.source = source
  115. #: Volume unique (inside given pool) identifier
  116. self.vid = vid
  117. def __eq__(self, other):
  118. if isinstance(other, Volume):
  119. return other.pool == self.pool and other.vid == self.vid
  120. return NotImplemented
  121. def __hash__(self):
  122. return hash('%s:%s' % (self.pool, self.vid))
  123. def __neq__(self, other):
  124. return not self.__eq__(other)
  125. def __repr__(self):
  126. return '{!r}'.format(str(self.pool) + ':' + self.vid)
  127. def __str__(self):
  128. return str(self.vid)
  129. def __xml__(self):
  130. config = _sanitize_config(self.config)
  131. return lxml.etree.Element('volume', **config)
  132. def create(self):
  133. ''' Create the given volume on disk.
  134. This method is called only once in the volume lifetime. Before
  135. calling this method, no data on disk should be touched (in
  136. context of this volume).
  137. This can be implemented as a coroutine.
  138. '''
  139. raise self._not_implemented("create")
  140. def remove(self):
  141. ''' Remove volume.
  142. This can be implemented as a coroutine.'''
  143. raise self._not_implemented("remove")
  144. def export(self):
  145. ''' Returns a path to read the volume data from.
  146. Reading from this path when domain owning this volume is
  147. running (i.e. when :py:meth:`is_dirty` is True) should return the
  148. data from before domain startup.
  149. Reading from the path returned by this method should return the
  150. volume data. If extracting volume data require something more
  151. than just reading from file (for example connecting to some other
  152. domain, or decompressing the data), the returned path may be a pipe.
  153. '''
  154. raise self._not_implemented("export")
  155. def import_data(self):
  156. ''' Returns a path to overwrite volume data.
  157. This method is called after volume was already :py:meth:`create`-ed.
  158. Writing to this path should overwrite volume data. If importing
  159. volume data require something more than just writing to a file (
  160. for example connecting to some other domain, or converting data
  161. on the fly), the returned path may be a pipe.
  162. '''
  163. raise self._not_implemented("import")
  164. def import_data_end(self, success):
  165. ''' End the data import operation. This may be used by pool
  166. implementation to commit changes, cleanup temporary files etc.
  167. This method is called regardless the operation was successful or not.
  168. :param success: True if data import was successful, otherwise False
  169. '''
  170. # by default do nothing
  171. pass
  172. def import_volume(self, src_volume):
  173. ''' Imports data from a different volume (possibly in a different
  174. pool.
  175. The volume needs to be create()d first.
  176. This can be implemented as a coroutine. '''
  177. # pylint: disable=unused-argument
  178. raise self._not_implemented("import_volume")
  179. def is_dirty(self):
  180. ''' Return `True` if volume was not properly shutdown and committed.
  181. This include the situation when domain owning the volume is still
  182. running.
  183. '''
  184. raise self._not_implemented("is_dirty")
  185. def is_outdated(self):
  186. ''' Returns `True` if this snapshot of a source volume (for
  187. `snap_on_start`=True) is outdated.
  188. '''
  189. raise self._not_implemented("is_outdated")
  190. def resize(self, size):
  191. ''' Expands volume, throws
  192. :py:class:`qubes.storage.StoragePoolException` if
  193. given size is less than current_size
  194. This can be implemented as a coroutine.
  195. :param int size: new size in bytes
  196. '''
  197. # pylint: disable=unused-argument
  198. raise self._not_implemented("resize")
  199. def revert(self, revision=None):
  200. ''' Revert volume to previous revision
  201. :param revision: revision to revert volume to, see :py:attr:`revisions`
  202. '''
  203. # pylint: disable=unused-argument
  204. raise self._not_implemented("revert")
  205. def start(self):
  206. ''' Do what ever is needed on start.
  207. This include making a snapshot of template's volume if
  208. :py:attr:`snap_on_start` is set.
  209. This can be implemented as a coroutine.'''
  210. raise self._not_implemented("start")
  211. def stop(self):
  212. ''' Do what ever is needed on stop.
  213. This include committing data if :py:attr:`save_on_stop` is set.
  214. This can be implemented as a coroutine.'''
  215. def verify(self):
  216. ''' Verifies the volume.
  217. This can be implemented as a coroutine.'''
  218. raise self._not_implemented("verify")
  219. def block_device(self):
  220. ''' Return :py:class:`BlockDevice` for serialization in
  221. the libvirt XML template as <disk>.
  222. '''
  223. return BlockDevice(self.path, self.name, self.script,
  224. self.rw, self.domain, self.devtype)
  225. @property
  226. def revisions(self):
  227. ''' Returns a dict containing revision identifiers and time of their
  228. creation '''
  229. msg = "{!s} has revisions not implemented".format(self.__class__)
  230. raise NotImplementedError(msg)
  231. @property
  232. def size(self):
  233. ''' Volume size in bytes '''
  234. return self._size
  235. @size.setter
  236. def size(self, size):
  237. # pylint: disable=attribute-defined-outside-init
  238. self._size = int(size)
  239. @property
  240. def config(self):
  241. ''' return config data for serialization to qubes.xml '''
  242. result = {
  243. 'name': self.name,
  244. 'pool': str(self.pool),
  245. 'vid': self.vid,
  246. 'internal': self.internal,
  247. 'removable': self.removable,
  248. 'revisions_to_keep': self.revisions_to_keep,
  249. 'rw': self.rw,
  250. 'save_on_stop': self.save_on_stop,
  251. 'snap_on_start': self.snap_on_start,
  252. }
  253. if self.size:
  254. result['size'] = self.size
  255. if self.source:
  256. result['source'] = str(self.source)
  257. return result
  258. def _not_implemented(self, method_name):
  259. ''' Helper for emitting helpful `NotImplementedError` exceptions '''
  260. msg = "Volume {!s} has {!s}() not implemented"
  261. msg = msg.format(str(self.__class__.__name__), method_name)
  262. return NotImplementedError(msg)
  263. class Storage(object):
  264. ''' Class for handling VM virtual disks.
  265. This is base class for all other implementations, mostly with Xen on Linux
  266. in mind.
  267. '''
  268. AVAILABLE_FRONTENDS = set(['xvd' + c for c in string.ascii_lowercase])
  269. def __init__(self, vm):
  270. #: Domain for which we manage storage
  271. self.vm = vm
  272. self.log = self.vm.log
  273. #: Additional drive (currently used only by HVM)
  274. self.drive = None
  275. if hasattr(vm, 'volume_config'):
  276. for name, conf in self.vm.volume_config.items():
  277. if 'source' in conf:
  278. template = getattr(vm, 'template', None)
  279. if template:
  280. # we have no control over VM load order,
  281. # so initialize storage recursively if needed
  282. if template.storage is None:
  283. template.storage = Storage(template)
  284. # FIXME: this effectively ignore 'source' value;
  285. # maybe we don't need it at all if it's always from
  286. # VM's template?
  287. conf['source'] = template.volumes[name]
  288. self.init_volume(name, conf)
  289. def init_volume(self, name, volume_config):
  290. ''' Initialize Volume instance attached to this domain '''
  291. assert 'pool' in volume_config, "Pool missing in volume_config " + str(
  292. volume_config)
  293. if 'name' not in volume_config:
  294. volume_config['name'] = name
  295. pool = self.vm.app.get_pool(volume_config['pool'])
  296. volume = pool.init_volume(self.vm, volume_config)
  297. self.vm.volumes[name] = volume
  298. return volume
  299. def attach(self, volume, rw=False):
  300. ''' Attach a volume to the domain '''
  301. assert self.vm.is_running()
  302. if self._is_already_attached(volume):
  303. self.vm.log.info("{!r} already attached".format(volume))
  304. return
  305. try:
  306. frontend = self.unused_frontend()
  307. except IndexError:
  308. raise StoragePoolException("No unused frontend found")
  309. disk = lxml.etree.Element("disk")
  310. disk.set('type', 'block')
  311. disk.set('device', 'disk')
  312. lxml.etree.SubElement(disk, 'driver').set('name', 'phy')
  313. lxml.etree.SubElement(disk, 'source').set('dev', '/dev/%s' % volume.vid)
  314. lxml.etree.SubElement(disk, 'target').set('dev', frontend)
  315. if not rw:
  316. lxml.etree.SubElement(disk, 'readonly')
  317. if volume.domain is not None:
  318. lxml.etree.SubElement(disk, 'backenddomain').set(
  319. 'name', volume.domain.name)
  320. xml_string = lxml.etree.tostring(disk, encoding='utf-8')
  321. self.vm.libvirt_domain.attachDevice(xml_string)
  322. # trigger watches to update device status
  323. # FIXME: this should be removed once libvirt will report such
  324. # events itself
  325. # self.vm.qdb.write('/qubes-block-devices', '') ← do we need this?
  326. def _is_already_attached(self, volume):
  327. ''' Checks if the given volume is already attached '''
  328. parsed_xml = lxml.etree.fromstring(self.vm.libvirt_domain.XMLDesc())
  329. disk_sources = parsed_xml.xpath("//domain/devices/disk/source")
  330. for source in disk_sources:
  331. if source.get('dev') == '/dev/%s' % volume.vid:
  332. return True
  333. return False
  334. def detach(self, volume):
  335. ''' Detach a volume from domain '''
  336. parsed_xml = lxml.etree.fromstring(self.vm.libvirt_domain.XMLDesc())
  337. disks = parsed_xml.xpath("//domain/devices/disk")
  338. for disk in disks:
  339. source = disk.xpath('source')[0]
  340. if source.get('dev') == '/dev/%s' % volume.vid:
  341. disk_xml = lxml.etree.tostring(disk, encoding='utf-8')
  342. self.vm.libvirt_domain.detachDevice(disk_xml)
  343. return
  344. raise StoragePoolException('Volume {!r} is not attached'.format(volume))
  345. @property
  346. def kernels_dir(self):
  347. '''Directory where kernel resides.
  348. If :py:attr:`self.vm.kernel` is :py:obj:`None`, the this points inside
  349. :py:attr:`self.vm.dir_path`
  350. '''
  351. assert 'kernel' in self.vm.volumes, "VM has no kernel volume"
  352. return self.vm.volumes['kernel'].kernels_dir
  353. def get_disk_utilization(self):
  354. ''' Returns summed up disk utilization for all domain volumes '''
  355. result = 0
  356. for volume in self.vm.volumes.values():
  357. result += volume.usage
  358. return result
  359. @asyncio.coroutine
  360. def resize(self, volume, size):
  361. ''' Resizes volume a read-writable volume '''
  362. if isinstance(volume, str):
  363. volume = self.vm.volumes[volume]
  364. ret = volume.resize(size)
  365. if asyncio.iscoroutine(ret):
  366. yield from ret
  367. if self.vm.is_running():
  368. yield from self.vm.run_service_for_stdio('qubes.ResizeDisk',
  369. input=volume.name.encode(),
  370. user='root')
  371. @asyncio.coroutine
  372. def create(self):
  373. ''' Creates volumes on disk '''
  374. old_umask = os.umask(0o002)
  375. coros = []
  376. for volume in self.vm.volumes.values():
  377. # launch the operation, if it's asynchronous, then append to wait
  378. # for them at the end
  379. ret = volume.create()
  380. if asyncio.iscoroutine(ret):
  381. coros.append(ret)
  382. if coros:
  383. yield from asyncio.wait(coros)
  384. os.umask(old_umask)
  385. @asyncio.coroutine
  386. def clone_volume(self, src_vm, name):
  387. ''' Clone single volume from the specified vm
  388. :param QubesVM src_vm: source VM
  389. :param str name: name of volume to clone ('root', 'private' etc)
  390. :return cloned volume object
  391. '''
  392. config = self.vm.volume_config[name]
  393. dst_pool = self.vm.app.get_pool(config['pool'])
  394. dst = dst_pool.init_volume(self.vm, config)
  395. src_volume = src_vm.volumes[name]
  396. msg = "Importing volume {!s} from vm {!s}"
  397. self.vm.log.info(msg.format(src_volume.name, src_vm.name))
  398. clone_op_ret = dst.import_volume(src_volume)
  399. # clone/import functions may be either synchronous or asynchronous
  400. # in the later case, we need to wait for them to finish
  401. if asyncio.iscoroutine(clone_op_ret):
  402. clone_op_ret = yield from clone_op_ret
  403. self.vm.volumes[name] = clone_op_ret
  404. return self.vm.volumes[name]
  405. @asyncio.coroutine
  406. def clone(self, src_vm):
  407. ''' Clone volumes from the specified vm '''
  408. self.vm.volumes = {}
  409. with VmCreationManager(self.vm):
  410. yield from asyncio.wait(self.clone_volume(src_vm, vol_name)
  411. for vol_name in self.vm.volume_config.keys())
  412. @property
  413. def outdated_volumes(self):
  414. ''' Returns a list of outdated volumes '''
  415. result = []
  416. if self.vm.is_halted():
  417. return result
  418. volumes = self.vm.volumes
  419. for volume in volumes.values():
  420. if volume.is_outdated():
  421. result += [volume]
  422. return result
  423. @asyncio.coroutine
  424. def verify(self):
  425. '''Verify that the storage is sane.
  426. On success, returns normally. On failure, raises exception.
  427. '''
  428. if not os.path.exists(self.vm.dir_path):
  429. raise qubes.exc.QubesVMError(
  430. self.vm,
  431. 'VM directory does not exist: {}'.format(self.vm.dir_path))
  432. futures = []
  433. for volume in self.vm.volumes.values():
  434. ret = volume.verify()
  435. if asyncio.iscoroutine(ret):
  436. futures.append(ret)
  437. if futures:
  438. yield from asyncio.wait(futures)
  439. self.vm.fire_event('domain-verify-files')
  440. return True
  441. @asyncio.coroutine
  442. def remove(self):
  443. ''' Remove all the volumes.
  444. Errors on removal are catched and logged.
  445. '''
  446. futures = []
  447. for name, volume in self.vm.volumes.items():
  448. self.log.info('Removing volume %s: %s' % (name, volume.vid))
  449. try:
  450. ret = volume.remove()
  451. if asyncio.iscoroutine(ret):
  452. futures.append(ret)
  453. except (IOError, OSError) as e:
  454. self.vm.log.exception("Failed to remove volume %s", name, e)
  455. if futures:
  456. try:
  457. yield from asyncio.wait(futures)
  458. except (IOError, OSError) as e:
  459. self.vm.log.exception("Failed to remove some volume", e)
  460. @asyncio.coroutine
  461. def start(self):
  462. ''' Execute the start method on each pool '''
  463. futures = []
  464. for volume in self.vm.volumes.values():
  465. ret = volume.start()
  466. if asyncio.iscoroutine(ret):
  467. futures.append(ret)
  468. if futures:
  469. yield from asyncio.wait(futures)
  470. @asyncio.coroutine
  471. def stop(self):
  472. ''' Execute the start method on each pool '''
  473. futures = []
  474. for volume in self.vm.volumes.values():
  475. ret = volume.stop()
  476. if asyncio.iscoroutine(ret):
  477. futures.append(ret)
  478. if futures:
  479. yield from asyncio.wait(futures)
  480. def unused_frontend(self):
  481. ''' Find an unused device name '''
  482. unused_frontends = self.AVAILABLE_FRONTENDS.difference(
  483. self.used_frontends)
  484. return sorted(unused_frontends)[0]
  485. @property
  486. def used_frontends(self):
  487. ''' Used device names '''
  488. xml = self.vm.libvirt_domain.XMLDesc()
  489. parsed_xml = lxml.etree.fromstring(xml)
  490. return set([target.get('dev', None)
  491. for target in parsed_xml.xpath(
  492. "//domain/devices/disk/target")])
  493. def export(self, volume):
  494. ''' Helper function to export volume (pool.export(volume))'''
  495. assert isinstance(volume, (Volume, str)), \
  496. "You need to pass a Volume or pool name as str"
  497. if isinstance(volume, Volume):
  498. return volume.export()
  499. return self.vm.volumes[volume].export()
  500. def import_data(self, volume):
  501. ''' Helper function to import volume data (pool.import_data(volume))'''
  502. assert isinstance(volume, (Volume, str)), \
  503. "You need to pass a Volume or pool name as str"
  504. if isinstance(volume, Volume):
  505. return volume.import_data()
  506. return self.vm.volumes[volume].import_data()
  507. def import_data_end(self, volume, success):
  508. ''' Helper function to finish/cleanup data import
  509. (pool.import_data_end( volume))'''
  510. assert isinstance(volume, (Volume, str)), \
  511. "You need to pass a Volume or pool name as str"
  512. if isinstance(volume, Volume):
  513. return volume.import_data_end(volume,
  514. success=success)
  515. return self.vm.volumes[volume].import_data_end(success=success)
  516. class VolumesCollection(object):
  517. '''Convenient collection wrapper for pool.get_volume and
  518. pool.list_volumes
  519. '''
  520. def __init__(self, pool):
  521. self._pool = pool
  522. def __getitem__(self, item):
  523. ''' Get a single volume with given Volume ID.
  524. You can also a Volume instance to get the same Volume or KeyError if
  525. Volume no longer exists.
  526. :param item: a Volume ID (str) or a Volume instance
  527. '''
  528. if isinstance(item, Volume):
  529. if item.pool == self._pool:
  530. return self[item.vid]
  531. else:
  532. raise KeyError(item)
  533. try:
  534. return self._pool.get_volume(item)
  535. except NotImplementedError:
  536. for vol in self:
  537. if vol.vid == item:
  538. return vol
  539. # if list_volumes is not implemented too, it will raise
  540. # NotImplementedError again earlier
  541. raise KeyError(item)
  542. def __iter__(self):
  543. ''' Get iterator over pool's volumes '''
  544. return iter(self._pool.list_volumes())
  545. def __contains__(self, item):
  546. ''' Check if given volume (either Volume ID or Volume instance) is
  547. present in the pool
  548. '''
  549. try:
  550. return self[item] is not None
  551. except KeyError:
  552. return False
  553. def keys(self):
  554. ''' Return list of volume IDs '''
  555. return [vol.vid for vol in self]
  556. def values(self):
  557. ''' Return list of Volumes'''
  558. return [vol for vol in self]
  559. class Pool(object):
  560. ''' A Pool is used to manage different kind of volumes (File
  561. based/LVM/Btrfs/...).
  562. 3rd Parties providing own storage implementations will need to extend
  563. this class.
  564. ''' # pylint: disable=unused-argument
  565. private_img_size = qubes.config.defaults['private_img_size']
  566. root_img_size = qubes.config.defaults['root_img_size']
  567. def __init__(self, name, revisions_to_keep=1, **kwargs):
  568. super(Pool, self).__init__(**kwargs)
  569. self._volumes_collection = VolumesCollection(self)
  570. self.name = name
  571. self.revisions_to_keep = revisions_to_keep
  572. kwargs['name'] = self.name
  573. def __eq__(self, other):
  574. if isinstance(other, Pool):
  575. return self.name == other.name
  576. elif isinstance(other, str):
  577. return self.name == other
  578. return NotImplemented
  579. def __neq__(self, other):
  580. return not self.__eq__(other)
  581. def __str__(self):
  582. return self.name
  583. def __hash__(self):
  584. return hash(self.name)
  585. def __xml__(self):
  586. config = _sanitize_config(self.config)
  587. return lxml.etree.Element('pool', **config)
  588. @property
  589. def config(self):
  590. ''' Returns the pool config to be written to qubes.xml '''
  591. raise self._not_implemented("config")
  592. def destroy(self):
  593. ''' Called when removing the pool. Use this for implementation specific
  594. clean up.
  595. '''
  596. raise self._not_implemented("destroy")
  597. def init_volume(self, vm, volume_config):
  598. ''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
  599. '''
  600. raise self._not_implemented("init_volume")
  601. def setup(self):
  602. ''' Called when adding a pool to the system. Use this for implementation
  603. specific set up.
  604. '''
  605. raise self._not_implemented("setup")
  606. @property
  607. def volumes(self):
  608. ''' Return a collection of volumes managed by this pool '''
  609. return self._volumes_collection
  610. def list_volumes(self):
  611. ''' Return a list of volumes managed by this pool '''
  612. raise self._not_implemented("list_volumes")
  613. def get_volume(self, vid):
  614. ''' Return a volume with *vid* from this pool
  615. :raise KeyError: if no volume is found
  616. '''
  617. raise self._not_implemented("get_volume")
  618. def _not_implemented(self, method_name):
  619. ''' Helper for emitting helpful `NotImplementedError` exceptions '''
  620. msg = "Pool driver {!s} has {!s}() not implemented"
  621. msg = msg.format(str(self.__class__.__name__), method_name)
  622. return NotImplementedError(msg)
  623. def _sanitize_config(config):
  624. ''' Helper function to convert types to appropriate strings
  625. ''' # FIXME: find another solution for serializing basic types
  626. result = {}
  627. for key, value in config.items():
  628. if isinstance(value, bool):
  629. if value:
  630. result[key] = 'True'
  631. else:
  632. result[key] = str(value)
  633. return result
  634. def pool_drivers():
  635. """ Return a list of EntryPoints names """
  636. return [ep.name
  637. for ep in pkg_resources.iter_entry_points(STORAGE_ENTRY_POINT)]
  638. def driver_parameters(name):
  639. ''' Get __init__ parameters from a driver with out `self` & `name`. '''
  640. init_function = qubes.utils.get_entry_point_one(
  641. qubes.storage.STORAGE_ENTRY_POINT, name).__init__
  642. params = init_function.func_code.co_varnames
  643. ignored_params = ['self', 'name']
  644. return [p for p in params if p not in ignored_params]
  645. def isodate(seconds=time.time()):
  646. ''' Helper method which returns an iso date '''
  647. return datetime.utcfromtimestamp(seconds).isoformat("T")
  648. class VmCreationManager(object):
  649. ''' A `ContextManager` which cleans up if volume creation fails.
  650. ''' # pylint: disable=too-few-public-methods
  651. def __init__(self, vm):
  652. self.vm = vm
  653. def __enter__(self):
  654. pass
  655. def __exit__(self, type, value, tb): # pylint: disable=redefined-builtin
  656. if type is not None and value is not None and tb is not None:
  657. for volume in self.vm.volumes.values():
  658. try:
  659. volume.remove()
  660. except Exception: # pylint: disable=broad-except
  661. pass
  662. os.rmdir(self.vm.dir_path)