lvm.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. #
  2. # The Qubes OS Project, http://www.qubes-os.org
  3. #
  4. # Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.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. ''' Driver for storing vm images in a LVM thin pool '''
  20. import logging
  21. import os
  22. import subprocess
  23. import time
  24. import asyncio
  25. import qubes
  26. import qubes.storage
  27. import qubes.utils
  28. def check_lvm_version():
  29. #Check if lvm is very very old, like in Travis-CI
  30. try:
  31. lvm_help = subprocess.check_output(['lvm', 'lvcreate', '--help'],
  32. stderr=subprocess.DEVNULL).decode()
  33. return '--setactivationskip' not in lvm_help
  34. except (subprocess.CalledProcessError, FileNotFoundError):
  35. pass
  36. lvm_is_very_old = check_lvm_version()
  37. class ThinPool(qubes.storage.Pool):
  38. ''' LVM Thin based pool implementation
  39. Volumes are stored as LVM thin volumes, in thin pool specified by
  40. *volume_group*/*thin_pool* arguments. LVM volume naming scheme:
  41. vm-{vm_name}-{volume_name}[-suffix]
  42. Where suffix can be one of:
  43. "-snap" - snapshot for currently running VM, at VM shutdown will be
  44. either discarded (if save_on_stop=False), or committed
  45. (if save_on_stop=True)
  46. "-{revision_id}" - volume revision - new revision is automatically
  47. created at each VM shutdown, *revisions_to_keep* control how many
  48. old revisions (in addition to the current one) should be stored
  49. "" (no suffix) - the most recent committed volume state; also volatile
  50. volume (snap_on_start=False, save_on_stop=False)
  51. On VM startup, new volume is created, depending on volume type,
  52. according to the table below:
  53. snap_on_start, save_on_stop
  54. False, False, - no suffix, fresh empty volume
  55. False, True, - "-snap", snapshot of last committed revision
  56. True , False, - "-snap", snapshot of last committed revision
  57. of source volume (from VM's template)
  58. True, True, - unsupported configuration
  59. Volume's revision_id format is "{timestamp}-back", where timestamp is in
  60. '%s' format (seconds since unix epoch)
  61. ''' # pylint: disable=protected-access
  62. size_cache = None
  63. driver = 'lvm_thin'
  64. def __init__(self, volume_group, thin_pool, revisions_to_keep=1, **kwargs):
  65. super(ThinPool, self).__init__(revisions_to_keep=revisions_to_keep,
  66. **kwargs)
  67. self.volume_group = volume_group
  68. self.thin_pool = thin_pool
  69. self._pool_id = "{!s}/{!s}".format(volume_group, thin_pool)
  70. self.log = logging.getLogger('qubes.storage.lvm.%s' % self._pool_id)
  71. self._volume_objects_cache = {}
  72. def __repr__(self):
  73. return '<{} at {:#x} name={!r} volume_group={!r} thin_pool={!r}>'.\
  74. format(
  75. type(self).__name__, id(self),
  76. self.name, self.volume_group, self.thin_pool)
  77. @property
  78. def config(self):
  79. return {
  80. 'name': self.name,
  81. 'volume_group': self.volume_group,
  82. 'thin_pool': self.thin_pool,
  83. 'driver': ThinPool.driver,
  84. 'revisions_to_keep': self.revisions_to_keep,
  85. }
  86. def destroy(self):
  87. pass # TODO Should we remove an existing pool?
  88. def init_volume(self, vm, volume_config):
  89. ''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
  90. '''
  91. if 'revisions_to_keep' not in volume_config.keys():
  92. volume_config['revisions_to_keep'] = self.revisions_to_keep
  93. if 'vid' not in volume_config.keys():
  94. if vm and hasattr(vm, 'name'):
  95. vm_name = vm.name
  96. else:
  97. # for the future if we have volumes not belonging to a vm
  98. vm_name = qubes.utils.random_string()
  99. assert self.name
  100. volume_config['vid'] = "{!s}/vm-{!s}-{!s}".format(
  101. self.volume_group, vm_name, volume_config['name'])
  102. volume_config['volume_group'] = self.volume_group
  103. volume_config['pool'] = self
  104. volume = ThinVolume(**volume_config)
  105. self._volume_objects_cache[volume_config['vid']] = volume
  106. return volume
  107. def setup(self):
  108. reset_cache()
  109. cache_key = self.volume_group + '/' + self.thin_pool
  110. if cache_key not in size_cache:
  111. raise qubes.storage.StoragePoolException(
  112. 'Thin pool {} does not exist'.format(cache_key))
  113. if size_cache[cache_key]['attr'][0] != 't':
  114. raise qubes.storage.StoragePoolException(
  115. 'Volume {} is not a thin pool'.format(cache_key))
  116. # TODO Should we create a non existing pool?
  117. def get_volume(self, vid):
  118. ''' Return a volume with given vid'''
  119. if vid in self._volume_objects_cache:
  120. return self._volume_objects_cache[vid]
  121. config = {
  122. 'pool': self,
  123. 'vid': vid,
  124. 'name': vid,
  125. 'volume_group': self.volume_group,
  126. }
  127. # don't cache this object, as it doesn't carry full configuration
  128. return ThinVolume(**config)
  129. def list_volumes(self):
  130. ''' Return a list of volumes managed by this pool '''
  131. volumes = []
  132. for vid, vol_info in size_cache.items():
  133. if not vid.startswith(self.volume_group + '/'):
  134. continue
  135. if vol_info['pool_lv'] != self.thin_pool:
  136. continue
  137. if vid.endswith('-snap') or vid.endswith('-import'):
  138. # implementation detail volume
  139. continue
  140. if vid.endswith('-back'):
  141. # old revisions
  142. continue
  143. volume = self.get_volume(vid)
  144. if volume in volumes:
  145. continue
  146. volumes.append(volume)
  147. return volumes
  148. @property
  149. def size(self):
  150. try:
  151. return qubes.storage.lvm.size_cache[
  152. self.volume_group + '/' + self.thin_pool]['size']
  153. except KeyError:
  154. return 0
  155. @property
  156. def usage(self):
  157. try:
  158. return qubes.storage.lvm.size_cache[
  159. self.volume_group + '/' + self.thin_pool]['usage']
  160. except KeyError:
  161. return 0
  162. def init_cache(log=logging.getLogger('qubes.storage.lvm')):
  163. cmd = ['lvs', '--noheadings', '-o',
  164. 'vg_name,pool_lv,name,lv_size,data_percent,lv_attr,origin',
  165. '--units', 'b', '--separator', ';']
  166. if os.getuid() != 0:
  167. cmd.insert(0, 'sudo')
  168. environ = os.environ.copy()
  169. environ['LC_ALL'] = 'C.utf8'
  170. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  171. close_fds=True, env=environ)
  172. out, err = p.communicate()
  173. return_code = p.returncode
  174. if return_code == 0 and err:
  175. log.warning(err)
  176. elif return_code != 0:
  177. raise qubes.storage.StoragePoolException(err)
  178. result = {}
  179. for line in out.splitlines():
  180. line = line.decode().strip()
  181. pool_name, pool_lv, name, size, usage_percent, attr, \
  182. origin = line.split(';', 6)
  183. if '' in [pool_name, name, size, usage_percent]:
  184. continue
  185. name = pool_name + "/" + name
  186. size = int(size[:-1]) # Remove 'B' suffix
  187. usage = int(size / 100 * float(usage_percent))
  188. result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv,
  189. 'attr': attr, 'origin': origin}
  190. return result
  191. size_cache = init_cache()
  192. def _revision_sort_key(revision):
  193. '''Sort key for revisions. Sort them by time
  194. :returns timestamp
  195. '''
  196. if isinstance(revision, tuple):
  197. revision = revision[0]
  198. if '-' in revision:
  199. revision = revision.split('-')[0]
  200. return int(revision)
  201. class ThinVolume(qubes.storage.Volume):
  202. ''' Default LVM thin volume implementation
  203. ''' # pylint: disable=too-few-public-methods
  204. def __init__(self, volume_group, size=0, **kwargs):
  205. self.volume_group = volume_group
  206. super(ThinVolume, self).__init__(size=size, **kwargs)
  207. self.log = logging.getLogger('qubes.storage.lvm.%s' % str(self.pool))
  208. if self.snap_on_start or self.save_on_stop:
  209. self._vid_snap = self.vid + '-snap'
  210. if self.save_on_stop:
  211. self._vid_import = self.vid + '-import'
  212. self._size = size
  213. @property
  214. def path(self):
  215. return '/dev/' + self._vid_current
  216. @property
  217. def _vid_current(self):
  218. if self.vid in size_cache:
  219. return self.vid
  220. vol_revisions = self.revisions
  221. if vol_revisions:
  222. last_revision = \
  223. max(vol_revisions.items(), key=_revision_sort_key)[0]
  224. return self.vid + '-' + last_revision
  225. # detached pool? return expected path
  226. return self.vid
  227. @property
  228. def revisions(self):
  229. name_prefix = self.vid + '-'
  230. revisions = {}
  231. for revision_vid in size_cache:
  232. if not revision_vid.startswith(name_prefix):
  233. continue
  234. if not revision_vid.endswith('-back'):
  235. continue
  236. revision_vid = revision_vid[len(name_prefix):]
  237. # get revision without suffix
  238. seconds = int(revision_vid.split('-')[0])
  239. iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
  240. revisions[revision_vid] = iso_date
  241. return revisions
  242. @property
  243. def size(self):
  244. try:
  245. if self.is_dirty():
  246. return qubes.storage.lvm.size_cache[self._vid_snap]['size']
  247. return qubes.storage.lvm.size_cache[self._vid_current]['size']
  248. except KeyError:
  249. return self._size
  250. @size.setter
  251. def size(self, _):
  252. raise qubes.storage.StoragePoolException(
  253. "You shouldn't use lvm size setter")
  254. def _reset(self):
  255. ''' Resets a volatile volume '''
  256. assert not self.snap_on_start and not self.save_on_stop, \
  257. "Not a volatile volume"
  258. self.log.debug('Resetting volatile %s', self.vid)
  259. try:
  260. cmd = ['remove', self.vid]
  261. qubes_lvm(cmd, self.log)
  262. except qubes.storage.StoragePoolException:
  263. pass
  264. # pylint: disable=protected-access
  265. cmd = ['create', self.pool._pool_id, self.vid.split('/')[1],
  266. str(self.size)]
  267. qubes_lvm(cmd, self.log)
  268. def _remove_revisions(self, revisions=None):
  269. '''Remove old volume revisions.
  270. If no revisions list is given, it removes old revisions according to
  271. :py:attr:`revisions_to_keep`
  272. :param revisions: list of revisions to remove
  273. '''
  274. if revisions is None:
  275. revisions = sorted(self.revisions.items(),
  276. key=_revision_sort_key)
  277. # pylint: disable=invalid-unary-operand-type
  278. revisions = revisions[:(-self.revisions_to_keep) or None]
  279. revisions = [rev_id for rev_id, _ in revisions]
  280. for rev_id in revisions:
  281. # safety check
  282. assert rev_id != self._vid_current
  283. try:
  284. cmd = ['remove', self.vid + '-' + rev_id]
  285. qubes_lvm(cmd, self.log)
  286. except qubes.storage.StoragePoolException:
  287. pass
  288. def _commit(self, vid_to_commit=None, keep=False):
  289. '''
  290. Commit temporary volume into current one. By default
  291. :py:attr:`_vid_snap` is used (which is created by :py:meth:`start()`),
  292. but can be overriden by *vid_to_commit* argument.
  293. :param vid_to_commit: LVM volume ID to commit into this one
  294. :param keep: whether to keep or not *vid_to_commit*.
  295. IOW use 'clone' or 'rename' methods.
  296. :return: None
  297. '''
  298. msg = "Trying to commit {!s}, but it has save_on_stop == False"
  299. msg = msg.format(self)
  300. assert self.save_on_stop, msg
  301. msg = "Trying to commit {!s}, but it has rw == False"
  302. msg = msg.format(self)
  303. assert self.rw, msg
  304. if vid_to_commit is None:
  305. assert hasattr(self, '_vid_snap')
  306. vid_to_commit = self._vid_snap
  307. # TODO: when converting this function to coroutine, this _must_ be
  308. # under a lock
  309. if not os.path.exists('/dev/' + vid_to_commit):
  310. # nothing to commit
  311. return
  312. if self._vid_current == self.vid:
  313. cmd = ['rename', self.vid,
  314. '{}-{}-back'.format(self.vid, int(time.time()))]
  315. qubes_lvm(cmd, self.log)
  316. reset_cache()
  317. cmd = ['clone' if keep else 'rename',
  318. vid_to_commit,
  319. self.vid]
  320. qubes_lvm(cmd, self.log)
  321. reset_cache()
  322. # make sure the one we've committed right now is properly
  323. # detected as the current one - before removing anything
  324. assert self._vid_current == self.vid
  325. # and remove old snapshots, if needed
  326. self._remove_revisions()
  327. def create(self):
  328. assert self.vid
  329. assert self.size
  330. if self.save_on_stop:
  331. if self.source:
  332. cmd = ['clone', self.source.path, self.vid]
  333. else:
  334. cmd = [
  335. 'create',
  336. self.pool._pool_id, # pylint: disable=protected-access
  337. self.vid.split('/', 1)[1],
  338. str(self.size)
  339. ]
  340. qubes_lvm(cmd, self.log)
  341. reset_cache()
  342. return self
  343. def remove(self):
  344. assert self.vid
  345. try:
  346. if os.path.exists('/dev/' + self._vid_snap):
  347. cmd = ['remove', self._vid_snap]
  348. qubes_lvm(cmd, self.log)
  349. except AttributeError:
  350. pass
  351. try:
  352. if os.path.exists('/dev/' + self._vid_import):
  353. cmd = ['remove', self._vid_import]
  354. qubes_lvm(cmd, self.log)
  355. except AttributeError:
  356. pass
  357. self._remove_revisions(self.revisions.keys())
  358. if not os.path.exists(self.path):
  359. return
  360. cmd = ['remove', self.path]
  361. qubes_lvm(cmd, self.log)
  362. reset_cache()
  363. # pylint: disable=protected-access
  364. self.pool._volume_objects_cache.pop(self.vid, None)
  365. def export(self):
  366. ''' Returns an object that can be `open()`. '''
  367. # make sure the device node is available
  368. qubes_lvm(['activate', self.path], self.log)
  369. devpath = self.path
  370. return devpath
  371. @asyncio.coroutine
  372. def import_volume(self, src_volume):
  373. if not src_volume.save_on_stop:
  374. return self
  375. if self.is_dirty():
  376. raise qubes.storage.StoragePoolException(
  377. 'Cannot import to dirty volume {} -'
  378. ' start and stop a qube to cleanup'.format(self.vid))
  379. self.abort_if_import_in_progress()
  380. # HACK: neat trick to speed up testing if you have same physical thin
  381. # pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
  382. # pylint: disable=line-too-long
  383. if isinstance(src_volume.pool, ThinPool) and \
  384. src_volume.pool.thin_pool == self.pool.thin_pool: # NOQA
  385. self._commit(src_volume.path[len('/dev/'):], keep=True)
  386. else:
  387. cmd = ['create',
  388. self.pool._pool_id, # pylint: disable=protected-access
  389. self._vid_import.split('/')[1],
  390. str(src_volume.size)]
  391. qubes_lvm(cmd, self.log)
  392. src_path = src_volume.export()
  393. cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self._vid_import,
  394. 'conv=sparse', 'status=none']
  395. if not os.access('/dev/' + self._vid_import, os.W_OK) or \
  396. not os.access(src_path, os.R_OK):
  397. cmd.insert(0, 'sudo')
  398. p = yield from asyncio.create_subprocess_exec(*cmd)
  399. yield from p.wait()
  400. if p.returncode != 0:
  401. cmd = ['remove', self._vid_import]
  402. qubes_lvm(cmd, self.log)
  403. raise qubes.storage.StoragePoolException(
  404. 'Failed to import volume {!r}, dd exit code: {}'.format(
  405. src_volume, p.returncode))
  406. self._commit(self._vid_import)
  407. return self
  408. def import_data(self):
  409. ''' Returns an object that can be `open()`. '''
  410. if self.is_dirty():
  411. raise qubes.storage.StoragePoolException(
  412. 'Cannot import data to dirty volume {}, stop the qube first'.
  413. format(self.vid))
  414. self.abort_if_import_in_progress()
  415. # pylint: disable=protected-access
  416. cmd = ['create', self.pool._pool_id, self._vid_import.split('/')[1],
  417. str(self.size)]
  418. qubes_lvm(cmd, self.log)
  419. reset_cache()
  420. devpath = '/dev/' + self._vid_import
  421. return devpath
  422. def import_data_end(self, success):
  423. '''Either commit imported data, or discard temporary volume'''
  424. if not os.path.exists('/dev/' + self._vid_import):
  425. raise qubes.storage.StoragePoolException(
  426. 'No import operation in progress on {}'.format(self.vid))
  427. if success:
  428. self._commit(self._vid_import)
  429. else:
  430. cmd = ['remove', self._vid_import]
  431. qubes_lvm(cmd, self.log)
  432. def abort_if_import_in_progress(self):
  433. try:
  434. devpath = '/dev/' + self._vid_import
  435. if os.path.exists(devpath):
  436. raise qubes.storage.StoragePoolException(
  437. 'Import operation in progress on {}'.format(self.vid))
  438. except AttributeError: # self._vid_import
  439. # no vid_import - import definitely not in progress
  440. pass
  441. def is_dirty(self):
  442. if self.save_on_stop:
  443. return os.path.exists('/dev/' + self._vid_snap)
  444. return False
  445. def is_outdated(self):
  446. if not self.snap_on_start:
  447. return False
  448. if self._vid_snap not in size_cache:
  449. return False
  450. return (size_cache[self._vid_snap]['origin'] !=
  451. self.source.path.split('/')[-1])
  452. def revert(self, revision=None):
  453. if self.is_dirty():
  454. raise qubes.storage.StoragePoolException(
  455. 'Cannot revert dirty volume {}, stop the qube first'.format(
  456. self.vid))
  457. self.abort_if_import_in_progress()
  458. if revision is None:
  459. revision = \
  460. max(self.revisions.items(), key=_revision_sort_key)[0]
  461. old_path = '/dev/' + self.vid + '-' + revision
  462. if not os.path.exists(old_path):
  463. msg = "Volume {!s} has no {!s}".format(self, old_path)
  464. raise qubes.storage.StoragePoolException(msg)
  465. if self.vid in size_cache:
  466. cmd = ['remove', self.vid]
  467. qubes_lvm(cmd, self.log)
  468. cmd = ['clone', self.vid + '-' + revision, self.vid]
  469. qubes_lvm(cmd, self.log)
  470. reset_cache()
  471. return self
  472. def resize(self, size):
  473. ''' Expands volume, throws
  474. :py:class:`qubst.storage.qubes.storage.StoragePoolException` if
  475. given size is less than current_size
  476. '''
  477. if not self.rw:
  478. msg = 'Can not resize reađonly volume {!s}'.format(self)
  479. raise qubes.storage.StoragePoolException(msg)
  480. if size < self.size:
  481. raise qubes.storage.StoragePoolException(
  482. 'For your own safety, shrinking of %s is'
  483. ' disabled (%d < %d). If you really know what you'
  484. ' are doing, use `lvresize` on %s manually.' %
  485. (self.name, size, self.size, self.vid))
  486. if size == self.size:
  487. return
  488. if self.is_dirty():
  489. cmd = ['extend', self._vid_snap, str(size)]
  490. qubes_lvm(cmd, self.log)
  491. elif hasattr(self, '_vid_import') and \
  492. os.path.exists('/dev/' + self._vid_import):
  493. cmd = ['extend', self._vid_import, str(size)]
  494. qubes_lvm(cmd, self.log)
  495. elif self.save_on_stop or not self.snap_on_start:
  496. cmd = ['extend', self._vid_current, str(size)]
  497. qubes_lvm(cmd, self.log)
  498. reset_cache()
  499. def _snapshot(self):
  500. try:
  501. cmd = ['remove', self._vid_snap]
  502. qubes_lvm(cmd, self.log)
  503. except: # pylint: disable=bare-except
  504. pass
  505. if self.source is None:
  506. cmd = ['clone', self._vid_current, self._vid_snap]
  507. else:
  508. cmd = ['clone', self.source.path, self._vid_snap]
  509. qubes_lvm(cmd, self.log)
  510. def start(self):
  511. self.abort_if_import_in_progress()
  512. try:
  513. if self.snap_on_start or self.save_on_stop:
  514. if not self.save_on_stop or not self.is_dirty():
  515. self._snapshot()
  516. else:
  517. self._reset()
  518. finally:
  519. reset_cache()
  520. return self
  521. def stop(self):
  522. try:
  523. if self.save_on_stop:
  524. self._commit()
  525. if self.snap_on_start and not self.save_on_stop:
  526. cmd = ['remove', self._vid_snap]
  527. qubes_lvm(cmd, self.log)
  528. elif not self.snap_on_start and not self.save_on_stop:
  529. cmd = ['remove', self.vid]
  530. qubes_lvm(cmd, self.log)
  531. finally:
  532. reset_cache()
  533. return self
  534. def verify(self):
  535. ''' Verifies the volume. '''
  536. if not self.save_on_stop and not self.snap_on_start:
  537. # volatile volumes don't need any files
  538. return True
  539. if self.source is not None:
  540. vid = self.source.path[len('/dev/'):]
  541. else:
  542. vid = self._vid_current
  543. try:
  544. vol_info = size_cache[vid]
  545. if vol_info['attr'][4] != 'a':
  546. raise qubes.storage.StoragePoolException(
  547. 'volume {} not active'.format(vid))
  548. except KeyError:
  549. raise qubes.storage.StoragePoolException(
  550. 'volume {} missing'.format(vid))
  551. return True
  552. def block_device(self):
  553. ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
  554. the libvirt XML template as <disk>.
  555. '''
  556. if self.snap_on_start or self.save_on_stop:
  557. return qubes.storage.BlockDevice(
  558. '/dev/' + self._vid_snap, self.name, self.script,
  559. self.rw, self.domain, self.devtype)
  560. return super(ThinVolume, self).block_device()
  561. @property
  562. def usage(self): # lvm thin usage always returns at least the same usage as
  563. # the parent
  564. try:
  565. return qubes.storage.lvm.size_cache[self._vid_current]['usage']
  566. except KeyError:
  567. return 0
  568. def pool_exists(pool_id):
  569. ''' Return true if pool exists '''
  570. try:
  571. vol_info = size_cache[pool_id]
  572. return vol_info['attr'][0] == 't'
  573. except KeyError:
  574. return False
  575. def qubes_lvm(cmd, log=logging.getLogger('qubes.storage.lvm')):
  576. ''' Call :program:`lvm` to execute an LVM operation '''
  577. action = cmd[0]
  578. if action == 'remove':
  579. lvm_cmd = ['lvremove', '-f', cmd[1]]
  580. elif action == 'clone':
  581. lvm_cmd = ['lvcreate', '-kn', '-ay', '-s', cmd[1], '-n', cmd[2]]
  582. elif action == 'create':
  583. lvm_cmd = ['lvcreate', '-T', cmd[1], '-kn', '-ay', '-n', cmd[2], '-V',
  584. str(cmd[3]) + 'B']
  585. elif action == 'extend':
  586. size = int(cmd[2]) / (1024 * 1024)
  587. lvm_cmd = ["lvextend", "-L%s" % size, cmd[1]]
  588. elif action == 'activate':
  589. lvm_cmd = ['lvchange', '-ay', cmd[1]]
  590. elif action == 'rename':
  591. lvm_cmd = ['lvrename', cmd[1], cmd[2]]
  592. else:
  593. raise NotImplementedError('unsupported action: ' + action)
  594. if lvm_is_very_old:
  595. # old lvm in trusty image used there does not support -k option
  596. lvm_cmd = [x for x in lvm_cmd if x != '-kn']
  597. if os.getuid() != 0:
  598. cmd = ['sudo', 'lvm'] + lvm_cmd
  599. else:
  600. cmd = ['lvm'] + lvm_cmd
  601. environ = os.environ.copy()
  602. environ['LC_ALL'] = 'C.utf8'
  603. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  604. close_fds=True, env=environ)
  605. out, err = p.communicate()
  606. err = err.decode()
  607. # Filter out warning about intended over-provisioning.
  608. # Upstream discussion about missing option to silence it:
  609. # https://bugzilla.redhat.com/1347008
  610. err = '\n'.join(line for line in err.splitlines()
  611. if 'exceeds the size of thin pool' not in line)
  612. return_code = p.returncode
  613. if out:
  614. log.debug(out)
  615. if return_code == 0 and err:
  616. log.warning(err)
  617. elif return_code != 0:
  618. assert err, "Command exited unsuccessful, but printed nothing to stderr"
  619. err = err.replace('%', '%%')
  620. raise qubes.storage.StoragePoolException(err)
  621. return True
  622. def reset_cache():
  623. qubes.storage.lvm.size_cache = init_cache()