storage/lvm: major cleanup, update

- remove obsolete volume types, use snap_on_start/save_on_stop directly
- handle multiple revisions
- implement is_outdated()

QubesOS/qubes-issues#2256
This commit is contained in:
Marek Marczykowski-Górecki 2017-07-04 03:29:54 +02:00
parent c7ca4a445e
commit 12adf8bede
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724

View File

@ -21,9 +21,14 @@
''' Driver for storing vm images in a LVM thin pool ''' ''' Driver for storing vm images in a LVM thin pool '''
import logging import logging
import operator
import os import os
import subprocess import subprocess
import time
import asyncio
import qubes import qubes
import qubes.storage import qubes.storage
import qubes.utils import qubes.utils
@ -102,6 +107,9 @@ class ThinPool(qubes.storage.Pool):
if vid.endswith('-snap'): if vid.endswith('-snap'):
# implementation detail volume # implementation detail volume
continue continue
if vid.endswith('-back'):
# old revisions
continue
config = { config = {
'pool': self, 'pool': self,
'vid': vid, 'vid': vid,
@ -115,7 +123,7 @@ class ThinPool(qubes.storage.Pool):
def init_cache(log=logging.getLogger('qube.storage.lvm')): def init_cache(log=logging.getLogger('qube.storage.lvm')):
cmd = ['lvs', '--noheadings', '-o', cmd = ['lvs', '--noheadings', '-o',
'vg_name,pool_lv,name,lv_size,data_percent,lv_attr', 'vg_name,pool_lv,name,lv_size,data_percent,lv_attr,origin',
'--units', 'b', '--separator', ','] '--units', 'b', '--separator', ',']
if os.getuid() != 0: if os.getuid() != 0:
cmd.insert(0, 'sudo') cmd.insert(0, 'sudo')
@ -132,14 +140,15 @@ def init_cache(log=logging.getLogger('qube.storage.lvm')):
for line in out.splitlines(): for line in out.splitlines():
line = line.decode().strip() line = line.decode().strip()
pool_name, pool_lv, name, size, usage_percent, attr = line.split(',', 5) pool_name, pool_lv, name, size, usage_percent, attr, \
origin = line.split(',', 6)
if '' in [pool_name, pool_lv, name, size, usage_percent]: if '' in [pool_name, pool_lv, name, size, usage_percent]:
continue continue
name = pool_name + "/" + name name = pool_name + "/" + name
size = int(size[:-1]) size = int(size[:-1])
usage = int(size / 100 * float(usage_percent)) usage = int(size / 100 * float(usage_percent))
result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv, result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv,
'attr': attr} 'attr': attr, 'origin': origin}
return result return result
@ -156,16 +165,7 @@ class ThinVolume(qubes.storage.Volume):
super(ThinVolume, self).__init__(size=size, **kwargs) super(ThinVolume, self).__init__(size=size, **kwargs)
self.log = logging.getLogger('qube.storage.lvm.%s' % str(self.pool)) self.log = logging.getLogger('qube.storage.lvm.%s' % str(self.pool))
if self.snap_on_start and self.source is None: if self.snap_on_start or self.save_on_stop:
msg = "snap_on_start specified on {!r} but no volume source set"
msg = msg.format(self.name)
raise qubes.storage.StoragePoolException(msg)
elif not self.snap_on_start and self.source is not None:
msg = "source specified on {!r} but no snap_on_start set"
msg = msg.format(self.name)
raise qubes.storage.StoragePoolException(msg)
if self.snap_on_start:
self._vid_snap = self.vid + '-snap' self._vid_snap = self.vid + '-snap'
self._size = size self._size = size
@ -176,28 +176,18 @@ class ThinVolume(qubes.storage.Volume):
@property @property
def revisions(self): def revisions(self):
path = self.path + '-back' name_prefix = self.vid + '-'
if os.path.exists(path): revisions = {}
seconds = os.path.getctime(path) for revision_vid in size_cache:
if not revision_vid.startswith(name_prefix):
continue
if not revision_vid.endswith('-back'):
continue
revision_vid = revision_vid[len(name_prefix):]
seconds = int(revision_vid[:-len('-back')])
iso_date = qubes.storage.isodate(seconds).split('.', 1)[0] iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
return {iso_date: path} revisions[revision_vid] = iso_date
return {} return revisions
@property
def _is_origin(self):
return not self.snap_on_start and self.save_on_stop
@property
def _is_origin_snapshot(self):
return self.snap_on_start and self.save_on_stop
@property
def _is_snapshot(self):
return self.snap_on_start and not self.save_on_stop
@property
def _is_volatile(self):
return not self.snap_on_start and not self.save_on_stop
@property @property
def size(self): def size(self):
@ -213,8 +203,8 @@ class ThinVolume(qubes.storage.Volume):
def _reset(self): def _reset(self):
''' Resets a volatile volume ''' ''' Resets a volatile volume '''
assert self._is_volatile, \ assert not self.snap_on_start and not self.save_on_stop, \
'Expected a volatile volume, but got {!r}'.format(self) "Not a volatile volume"
self.log.debug('Resetting volatile ' + self.vid) self.log.debug('Resetting volatile ' + self.vid)
try: try:
cmd = ['remove', self.vid] cmd = ['remove', self.vid]
@ -226,6 +216,27 @@ class ThinVolume(qubes.storage.Volume):
str(self.size)] str(self.size)]
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
def _remove_revisions(self, revisions=None):
'''Remove old volume revisions.
If no revisions list is given, it removes old revisions according to
:py:attr:`revisions_to_keep`
:param revisions: list of revisions to remove
'''
if revisions is None:
revisions = sorted(self.revisions.items(),
key=operator.itemgetter(1))
revisions = revisions[:-self.revisions_to_keep]
revisions = [rev_id for rev_id, _ in revisions]
for rev_id in revisions:
try:
cmd = ['remove', self.vid + rev_id]
qubes_lvm(cmd, self.log)
except qubes.storage.StoragePoolException:
pass
def _commit(self): def _commit(self):
msg = "Trying to commit {!s}, but it has save_on_stop == False" msg = "Trying to commit {!s}, but it has save_on_stop == False"
msg = msg.format(self) msg = msg.format(self)
@ -236,13 +247,11 @@ class ThinVolume(qubes.storage.Volume):
assert self.rw, msg assert self.rw, msg
assert hasattr(self, '_vid_snap') assert hasattr(self, '_vid_snap')
try: if self.revisions_to_keep > 0:
cmd = ['remove', self.vid + "-back"] cmd = ['clone', self.vid,
'{}-{}-back'.format(self.vid, int(time.time()))]
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
except qubes.storage.StoragePoolException: self._remove_revisions()
pass
cmd = ['clone', self.vid, self.vid + "-back"]
qubes_lvm(cmd, self.log)
cmd = ['remove', self.vid] cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
@ -273,6 +282,7 @@ class ThinVolume(qubes.storage.Volume):
cmd = ['remove', self._vid_snap] cmd = ['remove', self._vid_snap]
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
self._remove_revisions(self.revisions.keys())
if not os.path.exists(self.path): if not os.path.exists(self.path):
return return
cmd = ['remove', self.vid] cmd = ['remove', self.vid]
@ -284,6 +294,7 @@ class ThinVolume(qubes.storage.Volume):
devpath = '/dev/' + self.vid devpath = '/dev/' + self.vid
return devpath return devpath
@asyncio.coroutine
def import_volume(self, src_volume): def import_volume(self, src_volume):
if not src_volume.save_on_stop: if not src_volume.save_on_stop:
return self return self
@ -299,9 +310,14 @@ class ThinVolume(qubes.storage.Volume):
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
else: else:
src_path = src_volume.export() src_path = src_volume.export()
cmd = ['sudo', 'dd', 'if=' + src_path, 'of=/dev/' + self.vid, cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self.vid,
'conv=sparse'] 'conv=sparse']
subprocess.check_call(cmd) p = yield from asyncio.create_subprocess_exec(*cmd)
yield from p.wait()
if p.returncode != 0:
raise qubes.storage.StoragePoolException(
'Failed to import volume {!r}, dd exit code: {}'.format(
src_volume, p.returncode))
reset_cache() reset_cache()
return self return self
@ -313,18 +329,30 @@ class ThinVolume(qubes.storage.Volume):
def is_dirty(self): def is_dirty(self):
if self.save_on_stop: if self.save_on_stop:
return os.path.exists(self.path + '-snap') return os.path.exists('/dev/' + self._vid_snap)
return False return False
def is_outdated(self):
if not self.snap_on_start:
return False
if self._vid_snap not in size_cache:
return False
return (size_cache[self._vid_snap]['origin'] !=
self.source.vid.split('/')[1])
def revert(self, revision=None): def revert(self, revision=None):
old_path = self.path + '-back' if revision is None:
revision = \
max(self.revisions.items(), key=operator.itemgetter(1))[0]
old_path = self.path + '-' + revision
if not os.path.exists(old_path): if not os.path.exists(old_path):
msg = "Volume {!s} has no {!s}".format(self, old_path) msg = "Volume {!s} has no {!s}".format(self, old_path)
raise qubes.storage.StoragePoolException(msg) raise qubes.storage.StoragePoolException(msg)
cmd = ['remove', self.vid] cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
cmd = ['clone', self.vid + '-back', self.vid] cmd = ['clone', self.vid + '-' + revision, self.vid]
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
reset_cache() reset_cache()
return self return self
@ -364,22 +392,22 @@ class ThinVolume(qubes.storage.Volume):
def start(self): def start(self):
if self.snap_on_start: if self.snap_on_start or self.save_on_stop:
if not self.save_on_stop or not self.is_dirty(): if not self.save_on_stop or not self.is_dirty():
self._snapshot() self._snapshot()
elif not self.save_on_stop: else:
self._reset() self._reset()
reset_cache() reset_cache()
return self return self
def stop(self): def stop(self):
if self.save_on_stop and self.snap_on_start: if self.save_on_stop:
self._commit() self._commit()
if self.snap_on_start: if self.snap_on_start or self.save_on_stop:
cmd = ['remove', self._vid_snap] cmd = ['remove', self._vid_snap]
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
elif not self.save_on_stop: else:
cmd = ['remove', self.vid] cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
reset_cache() reset_cache()
@ -398,7 +426,7 @@ class ThinVolume(qubes.storage.Volume):
''' Return :py:class:`qubes.storage.BlockDevice` for serialization in ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
the libvirt XML template as <disk>. the libvirt XML template as <disk>.
''' '''
if self.snap_on_start: if self.snap_on_start or self.save_on_stop:
return qubes.storage.BlockDevice( return qubes.storage.BlockDevice(
'/dev/' + self._vid_snap, self.name, self.script, '/dev/' + self._vid_snap, self.name, self.script,
self.rw, self.domain, self.devtype) self.rw, self.domain, self.devtype)