core-admin/qubes/storage/lvm.py
Marek Marczykowski-Górecki 0471453773
storage/lvm: call lvm directly, don't use qubes-lvm wrapper
The wrapper doesn't do anything else than translating command
parameters, but it's load time is significant (because of python imports
mostly). Since we can't use python lvm API from non-root user anyway,
lets drop the wrapper and call `lvm` directly (or through sudo when
necessary).

This makes VM startup much faster - storage preparation is down from
over 10s to about 3s.

QubesOS/qubes-issues#2256
2016-11-04 14:18:55 +01:00

455 lines
14 KiB
Python

# vim: fileencoding=utf-8
# pylint: disable=abstract-method
#
# 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
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')
cmd = ['remove', volume.vid + "-back"]
qubes_lvm(cmd, self.log)
cmd = ['clone', volume._vid_snap, 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)
cmd = ['remove', volume._vid_snap]
@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.source:
return self.clone(volume.source, volume)
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
if not os.access(devpath, os.R_OK):
# FIXME: convert to udev rules, and drop after introducing qubesd
subprocess.check_call(['sudo', 'chgrp', 'qubes', devpath])
subprocess.check_call(['sudo', 'chmod', 'g+rw', devpath])
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)
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)
if volume.save_on_stop or volume._is_volatile:
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
volume.vid = new_vid
if not volume._is_volatile:
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 _reset(self, volume):
try:
self.remove(volume)
except qubes.storage.StoragePoolException:
pass
self.create(volume)
def setup(self):
pass # TODO Should we create a non existing pool?
def start(self, volume):
if volume._is_snapshot:
self._snapshot(volume)
elif volume._is_volatile:
self._reset(volume)
else:
if not self.is_dirty(volume):
self._snapshot(volume)
reset_cache()
return volume
def stop(self, volume):
if volume.save_on_stop:
self._commit(volume)
if volume._is_snapshot:
cmd = ['remove', volume._vid_snap]
qubes_lvm(cmd, self.log)
elif volume._is_volatile:
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
else:
cmd = ['remove', volume._vid_snap]
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
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.volume_type == 'volatile', \
'Expected a volatile volume, but got {!r}'.format(volume)
self.log.debug('Resetting volatile ' + volume.vid)
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
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.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 not self._is_volatile:
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")
@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 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()