lvm.py 29 KB

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