core-admin/qubes/storage/lvm.py
Marek Marczykowski-Górecki 46b60dbf42
storage: add Pool.import_data to the API
Allow importing not only from another volume, but also raw data. In
practice, for all currently implemented storage pools, this is the same
as Pool.export, because path returned there is read-write. But lets not
abuse this fact, some future implementation may need different methods.

QubesOS/qubes-issues#2622
QubesOS/qubes-issues#2256
2017-05-23 15:35:55 +02:00

475 lines
15 KiB
Python

#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
''' Driver for storing vm images in a LVM thin pool '''
import logging
import os
import subprocess
import qubes
def check_lvm_version():
#Check if lvm is very very old, like in Travis-CI
try:
lvm_help = subprocess.check_output(['lvm', 'lvcreate', '--help'],
stderr=subprocess.DEVNULL).decode()
return '--setactivationskip' not in lvm_help
except subprocess.CalledProcessError:
pass
lvm_is_very_old = check_lvm_version()
class ThinPool(qubes.storage.Pool):
''' LVM Thin based pool implementation
''' # pylint: disable=protected-access
size_cache = None
driver = 'lvm_thin'
def __init__(self, volume_group, thin_pool, revisions_to_keep=1, **kwargs):
super(ThinPool, self).__init__(revisions_to_keep=revisions_to_keep,
**kwargs)
self.volume_group = volume_group
self.thin_pool = thin_pool
self._pool_id = "{!s}/{!s}".format(volume_group, thin_pool)
self.log = logging.getLogger('qube.storage.lvm.%s' % self._pool_id)
def clone(self, source, target):
cmd = ['clone', str(source), str(target)]
qubes_lvm(cmd, self.log)
return target
def _commit(self, volume):
msg = "Trying to commit {!s}, but it has save_on_stop == False"
msg = msg.format(volume)
assert volume.save_on_stop, msg
msg = "Trying to commit {!s}, but it has rw == False"
msg = msg.format(volume)
assert volume.rw, msg
assert hasattr(volume, '_vid_snap')
try:
cmd = ['remove', volume.vid + "-back"]
qubes_lvm(cmd, self.log)
except qubes.storage.StoragePoolException:
pass
cmd = ['clone', volume.vid, volume.vid + "-back"]
qubes_lvm(cmd, self.log)
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
cmd = ['clone', volume._vid_snap, volume.vid]
qubes_lvm(cmd, self.log)
@property
def config(self):
return {
'name': self.name,
'volume_group': self.volume_group,
'thin_pool': self.thin_pool,
'driver': ThinPool.driver
}
def create(self, volume):
assert volume.vid
assert volume.size
if volume.save_on_stop:
if volume.source:
cmd = ['clone', str(volume.source), volume.vid]
else:
cmd = [
'create',
self._pool_id,
volume.vid.split('/', 1)[1],
str(volume.size)
]
qubes_lvm(cmd, self.log)
reset_cache()
return volume
def destroy(self):
pass # TODO Should we remove an existing pool?
def export(self, volume):
''' Returns an object that can be `open()`. '''
devpath = '/dev/' + volume.vid
return devpath
def import_data(self, volume):
''' Returns an object that can be `open()`. '''
devpath = '/dev/' + volume.vid
return devpath
def init_volume(self, vm, volume_config):
''' Initialize a :py:class:`qubes.storage.Volume` from `volume_config`.
'''
if 'vid' not in volume_config.keys():
if vm and hasattr(vm, 'name'):
vm_name = vm.name
else:
# for the future if we have volumes not belonging to a vm
vm_name = qubes.utils.random_string()
assert self.name
volume_config['vid'] = "{!s}/{!s}-{!s}".format(
self.volume_group, vm_name, volume_config['name'])
volume_config['volume_group'] = self.volume_group
return ThinVolume(**volume_config)
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
if not src_volume.save_on_stop:
return dst_volume
src_path = src_pool.export(src_volume)
# HACK: neat trick to speed up testing if you have same physical thin
# pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
# pylint: disable=line-too-long
if isinstance(src_pool, ThinPool) and src_pool.thin_pool == dst_pool.thin_pool: # NOQA
return self.clone(src_volume, dst_volume)
else:
dst_volume = self.create(dst_volume)
cmd = ['sudo', 'dd', 'if=' + src_path, 'of=/dev/' + dst_volume.vid,
'conv=sparse']
subprocess.check_call(cmd)
reset_cache()
return dst_volume
def is_dirty(self, volume):
if volume.save_on_stop:
return os.path.exists(volume.path + '-snap')
return False
def remove(self, volume):
assert volume.vid
if self.is_dirty(volume):
cmd = ['remove', volume._vid_snap]
qubes_lvm(cmd, self.log)
if not os.path.exists(volume.path):
return
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
reset_cache()
def rename(self, volume, old_name, new_name):
''' Called when the domain changes its name '''
new_vid = "{!s}/{!s}-{!s}".format(self.volume_group, new_name,
volume.name)
if volume.save_on_stop:
cmd = ['clone', volume.vid, new_vid]
qubes_lvm(cmd, self.log)
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
volume.vid = new_vid
if volume.snap_on_start:
volume._vid_snap = volume.vid + '-snap'
reset_cache()
return volume
def revert(self, volume, revision=None):
old_path = volume.path + '-back'
if not os.path.exists(old_path):
msg = "Volume {!s} has no {!s}".format(volume, old_path)
raise qubes.storage.StoragePoolException(msg)
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
cmd = ['clone', volume.vid + '-back', volume.vid]
qubes_lvm(cmd, self.log)
reset_cache()
return volume
def resize(self, volume, size):
''' Expands volume, throws
:py:class:`qubst.storage.qubes.storage.StoragePoolException` if
given size is less than current_size
'''
if not volume.rw:
msg = 'Can not resize reađonly volume {!s}'.format(volume)
raise qubes.storage.StoragePoolException(msg)
if size <= volume.size:
raise qubes.storage.StoragePoolException(
'For your own safety, shrinking of %s is'
' disabled. If you really know what you'
' are doing, use `lvresize` on %s manually.' %
(volume.name, volume.vid))
cmd = ['extend', volume.vid, str(size)]
qubes_lvm(cmd, self.log)
reset_cache()
def setup(self):
pass # TODO Should we create a non existing pool?
def start(self, volume):
if volume.snap_on_start:
if not volume.save_on_stop or not self.is_dirty(volume):
self._snapshot(volume)
elif not volume.save_on_stop:
self._reset_volume(volume)
reset_cache()
return volume
def stop(self, volume):
if volume.save_on_stop and volume.snap_on_start:
self._commit(volume)
if volume.snap_on_start:
cmd = ['remove', volume._vid_snap]
qubes_lvm(cmd, self.log)
elif not volume.save_on_stop:
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
reset_cache()
return volume
def _snapshot(self, volume):
try:
cmd = ['remove', volume._vid_snap]
qubes_lvm(cmd, self.log)
except: # pylint: disable=bare-except
pass
if volume.source is None:
cmd = ['clone', volume.vid, volume._vid_snap]
else:
cmd = ['clone', str(volume.source), volume._vid_snap]
qubes_lvm(cmd, self.log)
def verify(self, volume):
''' Verifies the volume. '''
try:
vol_info = size_cache[volume.vid]
return vol_info['attr'][4] == 'a'
except KeyError:
return False
@property
def volumes(self):
''' Return a list of volumes managed by this pool '''
volumes = []
for vid, vol_info in size_cache.items():
if not vid.startswith(self.volume_group + '/'):
continue
if vol_info['pool_lv'] != self.thin_pool:
continue
if vid.endswith('-snap'):
# implementation detail volume
continue
config = {
'pool': self.name,
'vid': vid,
'name': vid,
'volume_group': self.volume_group,
'rw': vol_info['attr'][1] == 'w',
}
volumes += [ThinVolume(**config)]
return volumes
def _reset_volume(self, volume):
''' Resets a volatile volume '''
assert volume._is_volatile, \
'Expected a volatile volume, but got {!r}'.format(volume)
self.log.debug('Resetting volatile ' + volume.vid)
try:
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
except qubes.storage.StoragePoolException:
pass
cmd = ['create', self._pool_id, volume.vid.split('/')[1],
str(volume.size)]
qubes_lvm(cmd, self.log)
def init_cache(log=logging.getLogger('qube.storage.lvm')):
cmd = ['lvs', '--noheadings', '-o',
'vg_name,pool_lv,name,lv_size,data_percent,lv_attr',
'--units', 'b', '--separator', ',']
if os.getuid() != 0:
cmd.insert(0, 'sudo')
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
close_fds=True)
out, err = p.communicate()
return_code = p.returncode
if return_code == 0 and err:
log.warning(err)
elif return_code != 0:
raise qubes.storage.StoragePoolException(err)
result = {}
for line in out.splitlines():
line = line.decode().strip()
pool_name, pool_lv, name, size, usage_percent, attr = line.split(',', 5)
if '' in [pool_name, pool_lv, name, size, usage_percent]:
continue
name = pool_name + "/" + name
size = int(size[:-1])
usage = int(size / 100 * float(usage_percent))
result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv,
'attr': attr}
return result
size_cache = init_cache()
class ThinVolume(qubes.storage.Volume):
''' Default LVM thin volume implementation
''' # pylint: disable=too-few-public-methods
def __init__(self, volume_group, size=0, **kwargs):
self.volume_group = volume_group
super(ThinVolume, self).__init__(size=size, **kwargs)
if self.snap_on_start and self.source is None:
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)
self.path = '/dev/' + self.vid
if self.snap_on_start:
self._vid_snap = self.vid + '-snap'
self._size = size
@property
def revisions(self):
path = self.path + '-back'
if os.path.exists(path):
seconds = os.path.getctime(path)
iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
return {iso_date: path}
return {}
@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
def size(self):
try:
return qubes.storage.lvm.size_cache[self.vid]['size']
except KeyError:
return self._size
@size.setter
def size(self, _):
raise qubes.storage.StoragePoolException(
"You shouldn't use lvm size setter")
def block_device(self):
''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
the libvirt XML template as <disk>.
'''
if self.snap_on_start:
return qubes.storage.BlockDevice(
'/dev/' + self._vid_snap, self.name, self.script,
self.rw, self.domain, self.devtype)
return super(ThinVolume, self).block_device()
@property
def usage(self): # lvm thin usage always returns at least the same usage as
# the parent
try:
return qubes.storage.lvm.size_cache[self.vid]['usage']
except KeyError:
return 0
def pool_exists(pool_id):
''' Return true if pool exists '''
try:
vol_info = size_cache[pool_id]
return vol_info['attr'][0] == 't'
except KeyError:
return False
def qubes_lvm(cmd, log=logging.getLogger('qube.storage.lvm')):
''' Call :program:`lvm` to execute an LVM operation '''
action = cmd[0]
if action == 'remove':
lvm_cmd = ['lvremove', '-f', cmd[1]]
elif action == 'clone':
lvm_cmd = ['lvcreate', '-kn', '-ay', '-s', cmd[1], '-n', cmd[2]]
elif action == 'create':
lvm_cmd = ['lvcreate', '-T', cmd[1], '-kn', '-ay', '-n', cmd[2], '-V',
str(cmd[3]) + 'B']
elif action == 'extend':
size = int(cmd[2]) / (1000 * 1000)
lvm_cmd = ["lvextend", "-L%s" % size, cmd[1]]
else:
raise NotImplementedError('unsupported action: ' + action)
if lvm_is_very_old:
# old lvm in trusty image used there does not support -k option
lvm_cmd = [x for x in lvm_cmd if x != '-kn']
if os.getuid() != 0:
cmd = ['sudo', 'lvm'] + lvm_cmd
else:
cmd = ['lvm'] + lvm_cmd
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
close_fds=True)
out, err = p.communicate()
return_code = p.returncode
if out:
log.debug(out)
if return_code == 0 and err:
log.warning(err)
elif return_code != 0:
assert err, "Command exited unsuccessful, but printed nothing to stderr"
raise qubes.storage.StoragePoolException(err)
return True
def reset_cache():
qubes.storage.lvm.size_cache = init_cache()