lvm.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  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 operator
  22. import os
  23. import subprocess
  24. import time
  25. import asyncio
  26. import qubes
  27. import qubes.storage
  28. import qubes.utils
  29. def check_lvm_version():
  30. #Check if lvm is very very old, like in Travis-CI
  31. try:
  32. lvm_help = subprocess.check_output(['lvm', 'lvcreate', '--help'],
  33. stderr=subprocess.DEVNULL).decode()
  34. return '--setactivationskip' not in lvm_help
  35. except (subprocess.CalledProcessError, FileNotFoundError):
  36. pass
  37. lvm_is_very_old = check_lvm_version()
  38. class ThinPool(qubes.storage.Pool):
  39. ''' LVM Thin based pool implementation
  40. ''' # pylint: disable=protected-access
  41. size_cache = None
  42. driver = 'lvm_thin'
  43. def __init__(self, volume_group, thin_pool, revisions_to_keep=1, **kwargs):
  44. super(ThinPool, self).__init__(revisions_to_keep=revisions_to_keep,
  45. **kwargs)
  46. self.volume_group = volume_group
  47. self.thin_pool = thin_pool
  48. self._pool_id = "{!s}/{!s}".format(volume_group, thin_pool)
  49. self.log = logging.getLogger('qubes.storage.lvm.%s' % self._pool_id)
  50. self._volume_objects_cache = {}
  51. @property
  52. def config(self):
  53. return {
  54. 'name': self.name,
  55. 'volume_group': self.volume_group,
  56. 'thin_pool': self.thin_pool,
  57. 'driver': ThinPool.driver
  58. }
  59. def destroy(self):
  60. pass # TODO Should we remove an existing pool?
  61. def init_volume(self, vm, volume_config):
  62. ''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
  63. '''
  64. if 'revisions_to_keep' not in volume_config.keys():
  65. volume_config['revisions_to_keep'] = self.revisions_to_keep
  66. if 'vid' not in volume_config.keys():
  67. if vm and hasattr(vm, 'name'):
  68. vm_name = vm.name
  69. else:
  70. # for the future if we have volumes not belonging to a vm
  71. vm_name = qubes.utils.random_string()
  72. assert self.name
  73. volume_config['vid'] = "{!s}/vm-{!s}-{!s}".format(
  74. self.volume_group, vm_name, volume_config['name'])
  75. volume_config['volume_group'] = self.volume_group
  76. volume_config['pool'] = self
  77. volume = ThinVolume(**volume_config)
  78. self._volume_objects_cache[volume_config['vid']] = volume
  79. return volume
  80. def setup(self):
  81. reset_cache()
  82. cache_key = self.volume_group + '/' + self.thin_pool
  83. if cache_key not in size_cache:
  84. raise qubes.storage.StoragePoolException(
  85. 'Thin pool {} does not exist'.format(cache_key))
  86. if size_cache[cache_key]['attr'][0] != 't':
  87. raise qubes.storage.StoragePoolException(
  88. 'Volume {} is not a thin pool'.format(cache_key))
  89. # TODO Should we create a non existing pool?
  90. def get_volume(self, vid):
  91. ''' Return a volume with given vid'''
  92. if vid in self._volume_objects_cache:
  93. return self._volume_objects_cache[vid]
  94. config = {
  95. 'pool': self,
  96. 'vid': vid,
  97. 'name': vid,
  98. 'volume_group': self.volume_group,
  99. }
  100. # don't cache this object, as it doesn't carry full configuration
  101. return ThinVolume(**config)
  102. def list_volumes(self):
  103. ''' Return a list of volumes managed by this pool '''
  104. volumes = []
  105. for vid, vol_info in size_cache.items():
  106. if not vid.startswith(self.volume_group + '/'):
  107. continue
  108. if vol_info['pool_lv'] != self.thin_pool:
  109. continue
  110. if vid.endswith('-snap'):
  111. # implementation detail volume
  112. continue
  113. if vid.endswith('-back'):
  114. # old revisions
  115. continue
  116. config = {
  117. 'pool': self,
  118. 'vid': vid,
  119. 'name': vid,
  120. 'volume_group': self.volume_group,
  121. 'rw': vol_info['attr'][1] == 'w',
  122. }
  123. volumes += [ThinVolume(**config)]
  124. return volumes
  125. @property
  126. def size(self):
  127. try:
  128. return qubes.storage.lvm.size_cache[
  129. self.volume_group + '/' + self.thin_pool]['size']
  130. except KeyError:
  131. return 0
  132. @property
  133. def usage(self):
  134. try:
  135. return qubes.storage.lvm.size_cache[
  136. self.volume_group + '/' + self.thin_pool]['usage']
  137. except KeyError:
  138. return 0
  139. def init_cache(log=logging.getLogger('qubes.storage.lvm')):
  140. cmd = ['lvs', '--noheadings', '-o',
  141. 'vg_name,pool_lv,name,lv_size,data_percent,lv_attr,origin',
  142. '--units', 'b', '--separator', ';']
  143. if os.getuid() != 0:
  144. cmd.insert(0, 'sudo')
  145. environ = os.environ.copy()
  146. environ['LC_ALL'] = 'C.utf8'
  147. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  148. close_fds=True, env=environ)
  149. out, err = p.communicate()
  150. return_code = p.returncode
  151. if return_code == 0 and err:
  152. log.warning(err)
  153. elif return_code != 0:
  154. raise qubes.storage.StoragePoolException(err)
  155. result = {}
  156. for line in out.splitlines():
  157. line = line.decode().strip()
  158. pool_name, pool_lv, name, size, usage_percent, attr, \
  159. origin = line.split(';', 6)
  160. if '' in [pool_name, name, size, usage_percent]:
  161. continue
  162. name = pool_name + "/" + name
  163. size = int(size[:-1]) # Remove 'B' suffix
  164. usage = int(size / 100 * float(usage_percent))
  165. result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv,
  166. 'attr': attr, 'origin': origin}
  167. return result
  168. size_cache = init_cache()
  169. class ThinVolume(qubes.storage.Volume):
  170. ''' Default LVM thin volume implementation
  171. ''' # pylint: disable=too-few-public-methods
  172. def __init__(self, volume_group, size=0, **kwargs):
  173. self.volume_group = volume_group
  174. super(ThinVolume, self).__init__(size=size, **kwargs)
  175. self.log = logging.getLogger('qubes.storage.lvm.%s' % str(self.pool))
  176. if self.snap_on_start or self.save_on_stop:
  177. self._vid_snap = self.vid + '-snap'
  178. self._size = size
  179. @property
  180. def path(self):
  181. return '/dev/' + self.vid
  182. @property
  183. def revisions(self):
  184. name_prefix = self.vid + '-'
  185. revisions = {}
  186. for revision_vid in size_cache:
  187. if not revision_vid.startswith(name_prefix):
  188. continue
  189. if not revision_vid.endswith('-back'):
  190. continue
  191. revision_vid = revision_vid[len(name_prefix):]
  192. seconds = int(revision_vid[:-len('-back')])
  193. iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
  194. revisions[revision_vid] = iso_date
  195. return revisions
  196. @property
  197. def size(self):
  198. try:
  199. if self.is_dirty():
  200. return qubes.storage.lvm.size_cache[self._vid_snap]['size']
  201. return qubes.storage.lvm.size_cache[self.vid]['size']
  202. except KeyError:
  203. return self._size
  204. @size.setter
  205. def size(self, _):
  206. raise qubes.storage.StoragePoolException(
  207. "You shouldn't use lvm size setter")
  208. def _reset(self):
  209. ''' Resets a volatile volume '''
  210. assert not self.snap_on_start and not self.save_on_stop, \
  211. "Not a volatile volume"
  212. self.log.debug('Resetting volatile %s', self.vid)
  213. try:
  214. cmd = ['remove', self.vid]
  215. qubes_lvm(cmd, self.log)
  216. except qubes.storage.StoragePoolException:
  217. pass
  218. # pylint: disable=protected-access
  219. cmd = ['create', self.pool._pool_id, self.vid.split('/')[1],
  220. str(self.size)]
  221. qubes_lvm(cmd, self.log)
  222. def _remove_revisions(self, revisions=None):
  223. '''Remove old volume revisions.
  224. If no revisions list is given, it removes old revisions according to
  225. :py:attr:`revisions_to_keep`
  226. :param revisions: list of revisions to remove
  227. '''
  228. if revisions is None:
  229. revisions = sorted(self.revisions.items(),
  230. key=operator.itemgetter(1))
  231. # pylint: disable=invalid-unary-operand-type
  232. revisions = revisions[:(-self.revisions_to_keep) or None]
  233. revisions = [rev_id for rev_id, _ in revisions]
  234. for rev_id in revisions:
  235. try:
  236. cmd = ['remove', self.vid + '-' + rev_id]
  237. qubes_lvm(cmd, self.log)
  238. except qubes.storage.StoragePoolException:
  239. pass
  240. def _commit(self):
  241. msg = "Trying to commit {!s}, but it has save_on_stop == False"
  242. msg = msg.format(self)
  243. assert self.save_on_stop, msg
  244. msg = "Trying to commit {!s}, but it has rw == False"
  245. msg = msg.format(self)
  246. assert self.rw, msg
  247. assert hasattr(self, '_vid_snap')
  248. if self.revisions_to_keep > 0:
  249. cmd = ['clone', self.vid,
  250. '{}-{}-back'.format(self.vid, int(time.time()))]
  251. qubes_lvm(cmd, self.log)
  252. reset_cache()
  253. self._remove_revisions()
  254. # TODO: when converting this function to coroutine, this _must_ be
  255. # under a lock
  256. # remove old volume only after _successful_ clone of the new one
  257. cmd = ['rename', self.vid, self.vid + '-tmp']
  258. qubes_lvm(cmd, self.log)
  259. try:
  260. cmd = ['clone', self._vid_snap, self.vid]
  261. qubes_lvm(cmd, self.log)
  262. except:
  263. # restore original volume
  264. cmd = ['rename', self.vid + '-tmp', self.vid]
  265. qubes_lvm(cmd, self.log)
  266. raise
  267. else:
  268. cmd = ['remove', self.vid + '-tmp']
  269. qubes_lvm(cmd, self.log)
  270. def create(self):
  271. assert self.vid
  272. assert self.size
  273. if self.save_on_stop:
  274. if self.source:
  275. cmd = ['clone', str(self.source), self.vid]
  276. else:
  277. cmd = [
  278. 'create',
  279. self.pool._pool_id, # pylint: disable=protected-access
  280. self.vid.split('/', 1)[1],
  281. str(self.size)
  282. ]
  283. qubes_lvm(cmd, self.log)
  284. reset_cache()
  285. return self
  286. def remove(self):
  287. assert self.vid
  288. try:
  289. if os.path.exists('/dev/' + self._vid_snap):
  290. cmd = ['remove', self._vid_snap]
  291. qubes_lvm(cmd, self.log)
  292. except AttributeError:
  293. pass
  294. self._remove_revisions(self.revisions.keys())
  295. if not os.path.exists(self.path):
  296. return
  297. cmd = ['remove', self.vid]
  298. qubes_lvm(cmd, self.log)
  299. reset_cache()
  300. # pylint: disable=protected-access
  301. self.pool._volume_objects_cache.pop(self.vid, None)
  302. def export(self):
  303. ''' Returns an object that can be `open()`. '''
  304. # make sure the device node is available
  305. qubes_lvm(['activate', self.vid], self.log)
  306. devpath = '/dev/' + self.vid
  307. return devpath
  308. @asyncio.coroutine
  309. def import_volume(self, src_volume):
  310. if not src_volume.save_on_stop:
  311. return self
  312. # HACK: neat trick to speed up testing if you have same physical thin
  313. # pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
  314. # pylint: disable=line-too-long
  315. if isinstance(src_volume.pool, ThinPool) and \
  316. src_volume.pool.thin_pool == self.pool.thin_pool: # NOQA
  317. cmd = ['remove', self.vid]
  318. qubes_lvm(cmd, self.log)
  319. cmd = ['clone', str(src_volume), str(self)]
  320. qubes_lvm(cmd, self.log)
  321. else:
  322. if src_volume.size != self.size:
  323. self.resize(src_volume.size)
  324. src_path = src_volume.export()
  325. cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self.vid,
  326. 'conv=sparse']
  327. p = yield from asyncio.create_subprocess_exec(*cmd)
  328. yield from p.wait()
  329. if p.returncode != 0:
  330. raise qubes.storage.StoragePoolException(
  331. 'Failed to import volume {!r}, dd exit code: {}'.format(
  332. src_volume, p.returncode))
  333. reset_cache()
  334. return self
  335. def import_data(self):
  336. ''' Returns an object that can be `open()`. '''
  337. devpath = '/dev/' + self.vid
  338. return devpath
  339. def is_dirty(self):
  340. if self.save_on_stop:
  341. return os.path.exists('/dev/' + self._vid_snap)
  342. return False
  343. def is_outdated(self):
  344. if not self.snap_on_start:
  345. return False
  346. if self._vid_snap not in size_cache:
  347. return False
  348. return (size_cache[self._vid_snap]['origin'] !=
  349. self.source.vid.split('/')[1])
  350. def revert(self, revision=None):
  351. if revision is None:
  352. revision = \
  353. max(self.revisions.items(), key=operator.itemgetter(1))[0]
  354. old_path = self.path + '-' + revision
  355. if not os.path.exists(old_path):
  356. msg = "Volume {!s} has no {!s}".format(self, old_path)
  357. raise qubes.storage.StoragePoolException(msg)
  358. cmd = ['remove', self.vid]
  359. qubes_lvm(cmd, self.log)
  360. cmd = ['clone', self.vid + '-' + revision, self.vid]
  361. qubes_lvm(cmd, self.log)
  362. reset_cache()
  363. return self
  364. def resize(self, size):
  365. ''' Expands volume, throws
  366. :py:class:`qubst.storage.qubes.storage.StoragePoolException` if
  367. given size is less than current_size
  368. '''
  369. if not self.rw:
  370. msg = 'Can not resize reađonly volume {!s}'.format(self)
  371. raise qubes.storage.StoragePoolException(msg)
  372. if size < self.size:
  373. raise qubes.storage.StoragePoolException(
  374. 'For your own safety, shrinking of %s is'
  375. ' disabled (%d < %d). If you really know what you'
  376. ' are doing, use `lvresize` on %s manually.' %
  377. (self.name, size, self.size, self.vid))
  378. if size == self.size:
  379. return
  380. if self.is_dirty():
  381. cmd = ['extend', self._vid_snap, str(size)]
  382. qubes_lvm(cmd, self.log)
  383. elif self.save_on_stop or not self.snap_on_start:
  384. cmd = ['extend', self.vid, str(size)]
  385. qubes_lvm(cmd, self.log)
  386. reset_cache()
  387. def _snapshot(self):
  388. try:
  389. cmd = ['remove', self._vid_snap]
  390. qubes_lvm(cmd, self.log)
  391. except: # pylint: disable=bare-except
  392. pass
  393. if self.source is None:
  394. cmd = ['clone', self.vid, self._vid_snap]
  395. else:
  396. cmd = ['clone', str(self.source), self._vid_snap]
  397. qubes_lvm(cmd, self.log)
  398. def start(self):
  399. try:
  400. if self.snap_on_start or self.save_on_stop:
  401. if not self.save_on_stop or not self.is_dirty():
  402. self._snapshot()
  403. else:
  404. self._reset()
  405. finally:
  406. reset_cache()
  407. return self
  408. def stop(self):
  409. try:
  410. if self.save_on_stop:
  411. self._commit()
  412. if self.snap_on_start or self.save_on_stop:
  413. cmd = ['remove', self._vid_snap]
  414. qubes_lvm(cmd, self.log)
  415. else:
  416. cmd = ['remove', self.vid]
  417. qubes_lvm(cmd, self.log)
  418. finally:
  419. reset_cache()
  420. return self
  421. def verify(self):
  422. ''' Verifies the volume. '''
  423. if not self.save_on_stop and not self.snap_on_start:
  424. # volatile volumes don't need any files
  425. return True
  426. if self.source is not None:
  427. vid = str(self.source)
  428. else:
  429. vid = self.vid
  430. try:
  431. vol_info = size_cache[vid]
  432. if vol_info['attr'][4] != 'a':
  433. raise qubes.storage.StoragePoolException(
  434. 'volume {} not active'.format(vid))
  435. except KeyError:
  436. raise qubes.storage.StoragePoolException(
  437. 'volume {} missing'.format(vid))
  438. return True
  439. def block_device(self):
  440. ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
  441. the libvirt XML template as <disk>.
  442. '''
  443. if self.snap_on_start or self.save_on_stop:
  444. return qubes.storage.BlockDevice(
  445. '/dev/' + self._vid_snap, self.name, self.script,
  446. self.rw, self.domain, self.devtype)
  447. return super(ThinVolume, self).block_device()
  448. @property
  449. def usage(self): # lvm thin usage always returns at least the same usage as
  450. # the parent
  451. try:
  452. return qubes.storage.lvm.size_cache[self.vid]['usage']
  453. except KeyError:
  454. return 0
  455. def pool_exists(pool_id):
  456. ''' Return true if pool exists '''
  457. try:
  458. vol_info = size_cache[pool_id]
  459. return vol_info['attr'][0] == 't'
  460. except KeyError:
  461. return False
  462. def qubes_lvm(cmd, log=logging.getLogger('qubes.storage.lvm')):
  463. ''' Call :program:`lvm` to execute an LVM operation '''
  464. action = cmd[0]
  465. if action == 'remove':
  466. lvm_cmd = ['lvremove', '-f', cmd[1]]
  467. elif action == 'clone':
  468. lvm_cmd = ['lvcreate', '-kn', '-ay', '-s', cmd[1], '-n', cmd[2]]
  469. elif action == 'create':
  470. lvm_cmd = ['lvcreate', '-T', cmd[1], '-kn', '-ay', '-n', cmd[2], '-V',
  471. str(cmd[3]) + 'B']
  472. elif action == 'extend':
  473. size = int(cmd[2]) / (1024 * 1024)
  474. lvm_cmd = ["lvextend", "-L%s" % size, cmd[1]]
  475. elif action == 'activate':
  476. lvm_cmd = ['lvchange', '-ay', cmd[1]]
  477. elif action == 'rename':
  478. lvm_cmd = ['lvrename', cmd[1], cmd[2]]
  479. else:
  480. raise NotImplementedError('unsupported action: ' + action)
  481. if lvm_is_very_old:
  482. # old lvm in trusty image used there does not support -k option
  483. lvm_cmd = [x for x in lvm_cmd if x != '-kn']
  484. if os.getuid() != 0:
  485. cmd = ['sudo', 'lvm'] + lvm_cmd
  486. else:
  487. cmd = ['lvm'] + lvm_cmd
  488. environ = os.environ.copy()
  489. environ['LC_ALL'] = 'C.utf8'
  490. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  491. close_fds=True, env=environ)
  492. out, err = p.communicate()
  493. return_code = p.returncode
  494. if out:
  495. log.debug(out)
  496. if return_code == 0 and err:
  497. log.warning(err)
  498. elif return_code != 0:
  499. assert err, "Command exited unsuccessful, but printed nothing to stderr"
  500. raise qubes.storage.StoragePoolException(err)
  501. return True
  502. def reset_cache():
  503. qubes.storage.lvm.size_cache = init_cache()