callback.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2020 David Hobach <david@hobach.de>
  5. #
  6. # This library is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU Lesser General Public
  8. # License as published by the Free Software Foundation; either
  9. # version 2.1 of the License, or (at your option) any later version.
  10. #
  11. # This library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. # Lesser General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public
  17. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  18. #
  19. # pylint: disable=line-too-long
  20. import logging
  21. import subprocess
  22. import json
  23. import asyncio
  24. import locale
  25. from shlex import quote
  26. from qubes.utils import coro_maybe
  27. import qubes.storage
  28. class UnhandledSignalException(qubes.storage.StoragePoolException):
  29. def __init__(self, pool, signal):
  30. super().__init__('The pool %s failed to handle the signal %s, likely because it was run from synchronous code.' % (pool.name, signal))
  31. class CallbackPool(qubes.storage.Pool):
  32. ''' Proxy storage pool driver adding callback functionality to other pool drivers.
  33. This way, users can extend storage pool drivers with custom functionality using the programming language of their choice.
  34. All configuration for this pool driver must be done in `/etc/qubes_callback.json`. Each configuration ID `conf_id` can be used
  35. to create a callback pool with e.g. `qvm-pool -o conf_id=your_conf_id -a pool_name callback`.
  36. Check `/usr/share/doc/qubes/qubes_callback.json.example` for an overview of the available options.
  37. Example applications of this driver:
  38. - custom pool mounts
  39. - encryption
  40. - debugging
  41. - run synchronous pool drivers asynchronously
  42. A word of caution:
  43. This implementation runs all methods that `qubes.storage.Pool` allows to be asynchronous asynchronously. So if a backend pool driver does
  44. not support a particular method to be run asynchronously, there may be issues. In short, it is always preferable to use the original backend
  45. driver over this one unless the functionality of this driver is required for a particular use case.
  46. **Integration tests**:
  47. (all of these tests assume the `qubes_callback.json.example` configuration)
  48. Tests that should **fail**:
  49. ```
  50. qvm-pool -a test callback
  51. qvm-pool -o conf_id=non-existing -a test callback
  52. qvm-pool -o conf_id=conf_id -a test callback
  53. qvm-pool -o conf_id=testing-fail-missing-all -a test callback
  54. qvm-pool -o conf_id=testing-fail-missing-bdriver-args -a test callback
  55. ```
  56. Tests that should **work**:
  57. ```
  58. qvm-pool -o conf_id=testing-succ-file-01 -a test callback
  59. qvm-pool
  60. ls /mnt/test01
  61. qvm-pool -r test && sudo rm -rf /mnt/test01
  62. echo '#!/bin/bash'$'\n''i=1 ; for arg in "$@" ; do echo "$i: $arg" >> /tmp/callback.log ; (( i++)) ; done ; exit 0' > /usr/bin/testCbLogArgs && chmod +x /usr/bin/testCbLogArgs
  63. rm -f /tmp/callback.log
  64. qvm-pool -o conf_id=testing-succ-file-02 -a test callback
  65. qvm-pool
  66. ls /mnt/test02
  67. less /tmp/callback.log (pre_setup should be there and in that order)
  68. qvm-create -l red -P test test-vm
  69. cat /tmp/callback.log (2x pre_volume_create + 2x post_volume_create should be added)
  70. qvm-start test-vm
  71. qvm-volume | grep test-vm
  72. grep test-vm /var/lib/qubes/qubes.xml
  73. ls /mnt/test02/appvms/
  74. cat /tmp/callback.log (2x pre_volume_start & 2x post_volume_start should be added)
  75. qvm-shutdown test-vm
  76. cat /tmp/callback.log (2x post_volume_stop should be added)
  77. #reboot
  78. cat /tmp/callback.log (nothing (!) should be there)
  79. qvm-start test-vm
  80. cat /tmp/callback.log (pre_sinit & 2x pre_volume_start & 2x post_volume_start should be added)
  81. qvm-shutdown --wait test-vm && qvm-remove test-vm
  82. qvm-pool -r test && sudo rm -rf /mnt/test02
  83. less /tmp/callback.log (2x post_volume_stop, 2x post_volume_remove, post_destroy should be added)
  84. qvm-pool -o conf_id=testing-succ-file-02 -a test callback
  85. qvm-create -l red -P test test-dvm
  86. qvm-prefs test-dvm template_for_dispvms True
  87. qvm-run --dispvm test-dvm xterm
  88. grep -E 'test-dvm|disp' /var/lib/qubes/qubes.xml
  89. qvm-volume | grep -E 'test-dvm|disp' (unexpected by most users: Qubes OS places only the private volume on the pool, cf. #5933)
  90. ls /mnt/test02/appvms/
  91. cat /tmp/callback.log
  92. #close the disposable VM
  93. qvm-remove test-dvm
  94. qvm-pool -r test
  95. qvm-pool -o conf_id=testing-succ-file-03 -a test callback
  96. qvm-pool
  97. ls /mnt/test03
  98. less /tmp/callback.log (pre_setup should be there, no more arguments)
  99. qvm-pool -r test && sudo rm -rf /mnt/test03
  100. less /tmp/callback.log (nothing should have been added)
  101. #luks pool test:
  102. #(make sure /mnt/test.key & /mnt/test.luks don't exist)
  103. qvm-pool -o conf_id=testing-succ-file-luks -a tluks callback
  104. ls /mnt/
  105. qvm-pool
  106. sudo cryptsetup status test-luks
  107. sudo mount | grep test_luks
  108. ls /mnt/test_luks/
  109. qvm-create -l red -P tluks test-luks (journalctl -b0 should show two pre_volume_create callbacks)
  110. ls /mnt/test_luks/appvms/test-luks/
  111. qvm-volume | grep test-luks
  112. qvm-start test-luks
  113. #reboot
  114. grep luks /var/lib/qubes/qubes.xml
  115. sudo cryptsetup status test-luks (should be inactive due to late pre_sinit!)
  116. qvm-start test-luks
  117. sudo mount | grep test_luks
  118. qvm-shutdown --wait test-luks
  119. qvm-remove test-luks
  120. qvm-pool -r tluks
  121. sudo cryptsetup status test-luks
  122. ls -l /mnt/
  123. #ephemeral luks pool test (key in RAM / lost on reboot):
  124. qvm-pool -o conf_id=testing-succ-file-luks-eph -a teph callback (executes setup() twice due to signal_back)
  125. ls /mnt/
  126. ls /mnt/ram
  127. md5sum /mnt/ram/teph.key (1)
  128. sudo mount|grep -E 'ram|test'
  129. sudo cryptsetup status test-eph
  130. qvm-create -l red -P teph test-eph (should execute two pre_volume_create callbacks)
  131. qvm-volume | grep test-eph
  132. ls /mnt/test_eph/appvms (should have private.img and volatile.img)
  133. ls /var/lib/qubes/appvms/test-eph (should only have the icon)
  134. qvm-start test-eph
  135. #reboot
  136. ls /mnt/ram (should be empty)
  137. ls /mnt/
  138. sudo mount|grep -E 'ram|test' (should be empty)
  139. qvm-ls | grep eph (should still have test-eph)
  140. grep eph /var/lib/qubes/qubes.xml (should still have test-eph)
  141. qvm-remove test-eph (should create a new encrypted pool backend)
  142. sudo cryptsetup status test-eph
  143. grep eph /var/lib/qubes/qubes.xml (only the pool should be left)
  144. ls /mnt/test_eph/ (should have the appvms directory etc.)
  145. qvm-create -l red -P teph test-eph2
  146. ls /mnt/test_eph/appvms/
  147. ls /mnt/ram
  148. qvm-start test-eph2
  149. md5sum /mnt/ram/teph.key ((2), different than in (1))
  150. qvm-shutdown --wait test-eph2
  151. systemctl restart qubesd
  152. qvm-start test-eph2 (trigger storage re-init)
  153. md5sum /mnt/ram/teph.key (same as in (2))
  154. qvm-shutdown --wait test-eph2
  155. sudo umount /mnt/test_eph
  156. qvm-create -l red -P teph test-eph-fail (must fail with error in journalctl)
  157. ls /mnt/test_eph/ (should be empty)
  158. systemctl restart qubesd
  159. qvm-remove test-eph2
  160. qvm-create -l red -P teph test-eph3
  161. md5sum /mnt/ram/teph.key (same as in (2))
  162. sudo mount|grep -E 'ram|test'
  163. ls /mnt/test_eph/appvms/test-eph3
  164. qvm-remove test-eph3
  165. qvm-ls | grep test-eph
  166. qvm-pool -r teph
  167. grep eph /var/lib/qubes/qubes.xml (nothing should be left)
  168. qvm-pool
  169. ls /mnt/
  170. ls /mnt/ram/ (should be empty)
  171. ```
  172. '''
  173. def __init__(self, *, name, conf_id):
  174. '''Constructor.
  175. :param conf_id: Identifier as found inside the user-controlled configuration at `/etc/qubes_callback.json`.
  176. Non-ASCII, non-alphanumeric characters may be disallowed.
  177. **Security Note**: Depending on your RPC policy (admin.pool.Add) this constructor and its parameters
  178. may be called from an untrusted VM (not by default though). In those cases it may be security-relevant
  179. not to pick easily guessable `conf_id` values for your configuration as untrusted VMs may otherwise
  180. execute callbacks meant for other pools.
  181. :raise StoragePoolException: For user configuration issues.
  182. '''
  183. #NOTE: attribute names **must** start with `_cb_` unless they are meant to be stored as self._cb_impl attributes
  184. self._cb_ctor_done = False #: Boolean to indicate whether or not `__init__` successfully ran through.
  185. self._cb_log = logging.getLogger('qubes.storage.callback') #: Logger instance.
  186. if not isinstance(conf_id, str):
  187. raise qubes.storage.StoragePoolException('conf_id is no String. VM attack?!')
  188. self._cb_conf_id = conf_id #: Configuration ID as passed to `__init__()`.
  189. config_path = '/etc/qubes_callback.json'
  190. with open(config_path) as json_file:
  191. conf_all = json.load(json_file)
  192. if not isinstance(conf_all, dict):
  193. raise qubes.storage.StoragePoolException('The file %s is supposed to define a dict.' % config_path)
  194. try:
  195. self._cb_conf = conf_all[self._cb_conf_id] #: Dictionary holding all configuration for the given _cb_conf_id.
  196. except KeyError:
  197. #we cannot throw KeyErrors as we'll otherwise generate incorrect error messages @qubes.app._get_pool()
  198. raise qubes.storage.StoragePoolException('The specified conf_id %s could not be found inside %s.' % (self._cb_conf_id, config_path))
  199. try:
  200. bdriver = self._cb_conf['bdriver']
  201. except KeyError:
  202. raise qubes.storage.StoragePoolException('Missing bdriver for the conf_id %s inside %s.' % (self._cb_conf_id, config_path))
  203. self._cb_cmd_arg = json.dumps(self._cb_conf, sort_keys=True, indent=2) #: Full configuration as string in the format required by _callback().
  204. try:
  205. cls = qubes.utils.get_entry_point_one(qubes.storage.STORAGE_ENTRY_POINT, bdriver)
  206. except KeyError:
  207. raise qubes.storage.StoragePoolException('The driver %s was not found on your system.' % bdriver)
  208. if not issubclass(cls, qubes.storage.Pool):
  209. raise qubes.storage.StoragePoolException('The class %s must be a subclass of qubes.storage.Pool.' % cls)
  210. self._cb_requires_init = self._check_init() #: Boolean indicating whether late storage initialization yet has to be done or not.
  211. self._cb_init_lock = asyncio.Lock() #: Lock ensuring that late storage initialization is only run exactly once.
  212. bdriver_args = self._cb_conf.get('bdriver_args', {})
  213. self._cb_impl = cls(name=name, **bdriver_args) #: Instance of the backend pool driver.
  214. super().__init__(name=name, revisions_to_keep=int(bdriver_args.get('revisions_to_keep', 1)))
  215. self._cb_ctor_done = True
  216. def _check_init(self):
  217. ''' Whether or not this object requires late storage initialization via callback. '''
  218. cmd = self._cb_conf.get('pre_sinit')
  219. if not cmd:
  220. cmd = self._cb_conf.get('cmd')
  221. return bool(cmd and cmd != '-')
  222. @asyncio.coroutine
  223. def _init(self, callback=True):
  224. ''' Late storage initialization on first use for e.g. decryption on first usage request.
  225. :param callback: Whether to trigger the `pre_sinit` callback or not.
  226. '''
  227. with (yield from self._cb_init_lock):
  228. if self._cb_requires_init:
  229. if callback:
  230. yield from self._callback('pre_sinit')
  231. self._cb_requires_init = False
  232. @asyncio.coroutine
  233. def _assert_initialized(self, **kwargs):
  234. if self._cb_requires_init:
  235. yield from self._init(**kwargs)
  236. @asyncio.coroutine
  237. def _callback(self, cb, cb_args=None):
  238. '''Run a callback.
  239. :param cb: Callback identifier string.
  240. :param cb_args: Optional list of arguments to pass to the command as last arguments.
  241. Only passed on for the generic command specified as `cmd`, not for `on_xyz` callbacks.
  242. :return: Nothing.
  243. '''
  244. if self._cb_ctor_done:
  245. cmd = self._cb_conf.get(cb)
  246. args = [] #on_xyz callbacks should never receive arguments
  247. if not cmd:
  248. if cb_args is None:
  249. cb_args = []
  250. cmd = self._cb_conf.get('cmd')
  251. args = [self.name, self._cb_conf['bdriver'], cb, self._cb_cmd_arg, *cb_args]
  252. if cmd and cmd != '-':
  253. args = ' '.join(quote(str(a)) for a in args)
  254. cmd = ' '.join(filter(None, [cmd, args]))
  255. self._cb_log.info('callback driver executing (%s, %s %s): %s', self._cb_conf_id, cb, cb_args, cmd)
  256. cmd_arr = ['/bin/bash', '-c', cmd]
  257. proc = yield from asyncio.create_subprocess_exec(*cmd_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  258. stdout, stderr = yield from proc.communicate()
  259. encoding = locale.getpreferredencoding()
  260. stdout = stdout.decode(encoding)
  261. stderr = stderr.decode(encoding)
  262. if proc.returncode != 0:
  263. raise subprocess.CalledProcessError(returncode=proc.returncode, cmd=cmd, output=stdout, stderr=stderr)
  264. self._cb_log.debug('callback driver stdout (%s, %s %s): %s', self._cb_conf_id, cb, cb_args, stdout)
  265. self._cb_log.debug('callback driver stderr (%s, %s %s): %s', self._cb_conf_id, cb, cb_args, stderr)
  266. if self._cb_conf.get('signal_back', False) is True:
  267. yield from self._process_signals(stdout)
  268. @asyncio.coroutine
  269. def _process_signals(self, out):
  270. '''Process any signals found inside a string.
  271. :param out: String to check for signals. Each signal must be on a dedicated line.
  272. They are executed in the order they are found. Callbacks are not triggered.
  273. '''
  274. for line in out.splitlines():
  275. if line == 'SIGNAL_setup':
  276. self._cb_log.info('callback driver processing SIGNAL_setup for %s', self._cb_conf_id)
  277. #NOTE: calling our own methods may lead to a deadlock / qubesd freeze due to `self._assert_initialized()` / `self._cb_init_lock`
  278. yield from coro_maybe(self._cb_impl.setup())
  279. @property
  280. def backend_class(self):
  281. '''Class of the first non-CallbackPool backend Pool.'''
  282. if isinstance(self._cb_impl, CallbackPool):
  283. return self._cb_impl.backend_class
  284. return self._cb_impl.__class__
  285. @property
  286. def config(self):
  287. return {
  288. 'name': self.name,
  289. 'driver': 'callback',
  290. 'conf_id': self._cb_conf_id,
  291. }
  292. @asyncio.coroutine
  293. def destroy(self):
  294. yield from self._assert_initialized()
  295. ret = yield from coro_maybe(self._cb_impl.destroy())
  296. yield from self._callback('post_destroy')
  297. return ret
  298. def init_volume(self, vm, volume_config):
  299. ret = CallbackVolume(self, self._cb_impl.init_volume(vm, volume_config))
  300. volume_config['pool'] = self
  301. return ret
  302. @asyncio.coroutine
  303. def setup(self):
  304. yield from self._assert_initialized(callback=False) #setup is assumed to include storage initialization
  305. yield from self._callback('pre_setup')
  306. return (yield from coro_maybe(self._cb_impl.setup()))
  307. @property
  308. def volumes(self):
  309. for vol in self._cb_impl.volumes:
  310. yield CallbackVolume(self, vol)
  311. def list_volumes(self):
  312. for vol in self._cb_impl.list_volumes():
  313. yield CallbackVolume(self, vol)
  314. def get_volume(self, vid):
  315. return CallbackVolume(self, self._cb_impl.get_volume(vid))
  316. def included_in(self, app):
  317. if self._cb_requires_init:
  318. return None
  319. return self._cb_impl.included_in(app)
  320. @property
  321. def size(self):
  322. if self._cb_requires_init:
  323. return None
  324. return self._cb_impl.size
  325. @property
  326. def usage(self):
  327. if self._cb_requires_init:
  328. return None
  329. return self._cb_impl.usage
  330. @property
  331. def usage_details(self):
  332. if self._cb_requires_init:
  333. return {}
  334. return self._cb_impl.usage_details
  335. #shadow all qubes.storage.Pool class attributes as instance properties
  336. #NOTE: this will cause a subtle difference to using an actual _cb_impl instance: CallbackPool.private_img_size will return a property object, Pool.private_img_size the actual value
  337. @property
  338. def private_img_size(self):
  339. return self._cb_impl.private_img_size
  340. @private_img_size.setter
  341. def private_img_size(self, private_img_size):
  342. self._cb_impl.private_img_size = private_img_size
  343. @property
  344. def root_img_size(self):
  345. return self._cb_impl.root_img_size
  346. @root_img_size.setter
  347. def root_img_size(self, root_img_size):
  348. self._cb_impl.root_img_size = root_img_size
  349. #remaining method & attribute delegation ("delegation pattern")
  350. #Convention: The methods of this object have priority over the delegated object's methods. All attributes are
  351. # passed to the delegated object unless their name starts with '_cb_'.
  352. def __getattr__(self, name):
  353. #NOTE: This method is only called when an attribute cannot be resolved locally (not part of the instance,
  354. # not part of the class tree). It is also called for methods that cannot be resolved.
  355. return getattr(self._cb_impl, name)
  356. def __setattr__(self, name, value):
  357. #NOTE: This method is called on every attribute assignment.
  358. if name.startswith('_cb_'):
  359. super().__setattr__(name, value)
  360. else:
  361. setattr(self._cb_impl, name, value)
  362. def __delattr__(self, name):
  363. if name.startswith('_cb_'):
  364. super().__delattr__(name)
  365. else:
  366. delattr(self._cb_impl, name)
  367. class CallbackVolume(qubes.storage.Volume):
  368. ''' Proxy volume adding callback functionality to other volumes.
  369. Required to support the `pre_sinit` and other callbacks.
  370. '''
  371. def __init__(self, pool, impl):
  372. '''Constructor.
  373. :param pool: `CallbackPool` of this volume
  374. :param impl: `qubes.storage.Volume` object to wrap
  375. '''
  376. # pylint: disable=super-init-not-called
  377. #NOTE: we must *not* call super().__init__() as it would prevent attribute delegation
  378. assert isinstance(impl, qubes.storage.Volume), 'impl must be a qubes.storage.Volume instance. Found a %s instance.' % impl.__class__
  379. assert isinstance(pool, CallbackPool), 'pool must use a qubes.storage.CallbackPool instance. Found a %s instance.' % pool.__class__
  380. impl.pool = pool #enforce the CallbackPool instance as the parent pool of the volume
  381. self._cb_pool = pool #: CallbackPool instance the Volume belongs to.
  382. self._cb_impl = impl #: Backend volume implementation instance.
  383. @asyncio.coroutine
  384. def _assert_initialized(self, **kwargs):
  385. yield from self._cb_pool._assert_initialized(**kwargs) # pylint: disable=protected-access
  386. @asyncio.coroutine
  387. def _callback(self, cb, cb_args=None, **kwargs):
  388. if cb_args is None:
  389. cb_args = []
  390. vol_args = [self.name, self.vid, self.source, *cb_args]
  391. yield from self._cb_pool._callback(cb, cb_args=vol_args, **kwargs) # pylint: disable=protected-access
  392. @property
  393. def backend_class(self):
  394. '''Class of the first non-CallbackVolume backend Volume.'''
  395. if isinstance(self._cb_impl, CallbackVolume):
  396. return self._cb_impl.backend_class
  397. return self._cb_impl.__class__
  398. @asyncio.coroutine
  399. def create(self):
  400. yield from self._assert_initialized()
  401. yield from self._callback('pre_volume_create')
  402. ret = yield from coro_maybe(self._cb_impl.create())
  403. yield from self._callback('post_volume_create')
  404. return ret
  405. @asyncio.coroutine
  406. def remove(self):
  407. yield from self._assert_initialized()
  408. ret = yield from coro_maybe(self._cb_impl.remove())
  409. yield from self._callback('post_volume_remove')
  410. return ret
  411. @asyncio.coroutine
  412. def resize(self, size):
  413. yield from self._assert_initialized()
  414. yield from self._callback('pre_volume_resize', cb_args=[size])
  415. return (yield from coro_maybe(self._cb_impl.resize(size)))
  416. @asyncio.coroutine
  417. def start(self):
  418. yield from self._assert_initialized()
  419. yield from self._callback('pre_volume_start')
  420. ret = yield from coro_maybe(self._cb_impl.start())
  421. yield from self._callback('post_volume_start')
  422. return ret
  423. @asyncio.coroutine
  424. def stop(self):
  425. yield from self._assert_initialized()
  426. ret = yield from coro_maybe(self._cb_impl.stop())
  427. yield from self._callback('post_volume_stop')
  428. return ret
  429. @asyncio.coroutine
  430. def import_data(self, size):
  431. yield from self._assert_initialized()
  432. yield from self._callback('pre_volume_import_data', cb_args=[size])
  433. return (yield from coro_maybe(self._cb_impl.import_data(size)))
  434. @asyncio.coroutine
  435. def import_data_end(self, success):
  436. yield from self._assert_initialized()
  437. ret = yield from coro_maybe(self._cb_impl.import_data_end(success))
  438. yield from self._callback('post_volume_import_data_end', cb_args=[success])
  439. return ret
  440. @asyncio.coroutine
  441. def import_volume(self, src_volume):
  442. yield from self._assert_initialized()
  443. yield from self._callback('pre_volume_import', cb_args=[src_volume.vid])
  444. ret = yield from coro_maybe(self._cb_impl.import_volume(src_volume))
  445. yield from self._callback('post_volume_import', cb_args=[src_volume.vid])
  446. return ret
  447. def is_dirty(self):
  448. # pylint: disable=protected-access
  449. if self._cb_pool._cb_requires_init:
  450. return False
  451. return self._cb_impl.is_dirty()
  452. def is_outdated(self):
  453. # pylint: disable=protected-access
  454. if self._cb_pool._cb_requires_init:
  455. return False
  456. return self._cb_impl.is_outdated()
  457. @property
  458. def revisions(self):
  459. return self._cb_impl.revisions
  460. @property
  461. def size(self):
  462. return self._cb_impl.size
  463. @size.setter
  464. def size(self, size):
  465. self._cb_impl.size = size
  466. @property
  467. def config(self):
  468. return self._cb_impl.config
  469. def block_device(self):
  470. # pylint: disable=protected-access
  471. if self._cb_pool._cb_requires_init:
  472. # usually Volume.start() is called beforehand
  473. # --> we should be initialized in 99% of cases
  474. return None
  475. return self._cb_impl.block_device()
  476. @asyncio.coroutine
  477. def export(self):
  478. yield from self._assert_initialized()
  479. yield from self._callback('pre_volume_export')
  480. return (yield from coro_maybe(self._cb_impl.export()))
  481. @asyncio.coroutine
  482. def export_end(self, path):
  483. yield from self._assert_initialized()
  484. ret = yield from coro_maybe(self._cb_impl.export_end(path))
  485. yield from self._callback('post_volume_export_end', cb_args=[path])
  486. return ret
  487. @asyncio.coroutine
  488. def verify(self):
  489. yield from self._assert_initialized()
  490. return (yield from coro_maybe(self._cb_impl.verify()))
  491. @asyncio.coroutine
  492. def revert(self, revision=None):
  493. yield from self._assert_initialized()
  494. return (yield from coro_maybe(self._cb_impl.revert(revision=revision)))
  495. #shadow all qubes.storage.Volume class attributes as instance properties
  496. #NOTE: this will cause a subtle difference to using an actual _cb_impl instance: CallbackVolume.devtype will return a property object, Volume.devtype the actual value
  497. @property
  498. def devtype(self):
  499. return self._cb_impl.devtype
  500. @devtype.setter
  501. def devtype(self, devtype):
  502. self._cb_impl.devtype = devtype
  503. @property
  504. def domain(self):
  505. return self._cb_impl.domain
  506. @domain.setter
  507. def domain(self, domain):
  508. self._cb_impl.domain = domain
  509. @property
  510. def path(self):
  511. return self._cb_impl.path
  512. @path.setter
  513. def path(self, path):
  514. self._cb_impl.path = path
  515. @property
  516. def script(self):
  517. return self._cb_impl.script
  518. @script.setter
  519. def script(self, script):
  520. self._cb_impl.script = script
  521. @property
  522. def usage(self):
  523. return self._cb_impl.usage
  524. @usage.setter
  525. def usage(self, usage):
  526. self._cb_impl.usage = usage
  527. #remaining method & attribute delegation
  528. def __getattr__(self, name):
  529. return getattr(self._cb_impl, name)
  530. def __setattr__(self, name, value):
  531. if name.startswith('_cb_'):
  532. super().__setattr__(name, value)
  533. else:
  534. setattr(self._cb_impl, name, value)
  535. def __delattr__(self, name):
  536. if name.startswith('_cb_'):
  537. super().__delattr__(name)
  538. else:
  539. delattr(self._cb_impl, name)