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:
parent
c7ca4a445e
commit
12adf8bede
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user