diff --git a/doc/manpages/qvm-block.rst b/doc/manpages/qvm-block.rst index be8fa65f..fb340d24 100644 --- a/doc/manpages/qvm-block.rst +++ b/doc/manpages/qvm-block.rst @@ -86,6 +86,12 @@ Detach the volume with *POOL_NAME:VOLUME_ID* from domain *VMNAME* aliases: d, dt +extend +^^^^^^ +| :command:`qvm-block extend` [-h] [--verbose] [--quiet] *POOL_NAME:VOLUME_ID* *NEW_SIZE* + +Extend the volume with *POOL_NAME:VOLUME_ID* TO *NEW_SIZE* + revert ^^^^^^ diff --git a/qubes/storage/file.py b/qubes/storage/file.py index fc24f7ae..a98ec7e4 100644 --- a/qubes/storage/file.py +++ b/qubes/storage/file.py @@ -136,6 +136,7 @@ class FilePool(qubes.storage.Pool): # resize loop device subprocess.check_call(['sudo', 'losetup', '--set-capacity', loop_dev]) + volume.size = size def remove(self, volume): if not volume.internal: diff --git a/qubes/storage/lvm.py b/qubes/storage/lvm.py index 794ded7e..6dc6063e 100644 --- a/qubes/storage/lvm.py +++ b/qubes/storage/lvm.py @@ -32,6 +32,8 @@ 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): @@ -90,6 +92,7 @@ class ThinPool(qubes.storage.Pool): str(volume.size) ] qubes_lvm(cmd, self.log) + reset_cache() return volume def destroy(self): @@ -146,6 +149,7 @@ class ThinPool(qubes.storage.Pool): dst.write(tmp) p.stdin.close() p.wait() + reset_cache() return dst_volume def is_dirty(self, volume): @@ -161,6 +165,7 @@ class ThinPool(qubes.storage.Pool): 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 ''' @@ -178,6 +183,7 @@ class ThinPool(qubes.storage.Pool): if not volume._is_volatile: volume._vid_snap = volume.vid + '-snap' + reset_cache() return volume def revert(self, volume, revision=None): @@ -190,8 +196,29 @@ class ThinPool(qubes.storage.Pool): 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) @@ -212,6 +239,7 @@ class ThinPool(qubes.storage.Pool): if not self.is_dirty(volume): self._snapshot(volume) + reset_cache() return volume def stop(self, volume): @@ -226,6 +254,7 @@ class ThinPool(qubes.storage.Pool): else: cmd = ['remove', volume._vid_snap] qubes_lvm(cmd, self.log) + reset_cache() return volume def _snapshot(self, volume): @@ -289,13 +318,44 @@ class ThinPool(qubes.storage.Pool): str(volume.size)] qubes_lvm(cmd, self.log) + +def init_cache(log=logging.getLogger('qube.storage.lvm')): + cmd = ['sudo', 'lvs', '--noheadings', '-o', + 'vg_name,name,lv_size,data_percent', '--units', 'b', '--separator', + ','] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + 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, name, size, usage_percent = line.split(',', 3) + if '' in [pool_name, 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} + + 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, **kwargs): + + def __init__(self, volume_group, size=0, **kwargs): self.volume_group = volume_group - super(ThinVolume, self).__init__(**kwargs) + 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" @@ -310,6 +370,8 @@ class ThinVolume(qubes.storage.Volume): if not self._is_volatile: self._vid_snap = self.vid + '-snap' + self._size = size + @property def revisions(self): path = self.path + '-back' @@ -335,6 +397,22 @@ class ThinVolume(qubes.storage.Volume): 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 + + @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 ''' cmd = ['pool', pool_id] @@ -357,3 +435,7 @@ def qubes_lvm(cmd, log=logging.getLogger('qube.storage.lvm')): 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 diff --git a/qubes/tools/qubes_lvm.py b/qubes/tools/qubes_lvm.py index 33b4ef4e..8ad9627c 100644 --- a/qubes/tools/qubes_lvm.py +++ b/qubes/tools/qubes_lvm.py @@ -127,6 +127,21 @@ def rename_volume(old_name, new_name): return new_name +def extend_volume(args): + ''' Extends an existing lvm volume. Note this works on any lvm volume not + only on thin volumes. + ''' + vid = args.name + size = int(args.size) / (1000 * 1000) + log.debug("Extending LVM %s to %s", vid, size) + cmd = ["lvextend", "-L%s" % size, vid] + log.debug(cmd) + retcode = subprocess.call(cmd) + if retcode != 0: + raise IOError("Error extending LVM %s to %s " % (vid, size)) + return 0 + + def init_pool_parser(sub_parsers): ''' Initialize pool subparser ''' pool_parser = sub_parsers.add_parser( @@ -219,6 +234,17 @@ def init_remove_parser(sub_parsers): remove_parser.set_defaults(func=remove_volume) +def init_extend_parser(sub_parsers): + ''' Initialize extend subparser ''' + extend_parser = sub_parsers.add_parser('extend', + help='extends a LogicalVolume') + extend_parser.add_argument('name', metavar='VG/VID', + help='volume_group/volume_name') + extend_parser.set_defaults(func=extend_volume) + extend_parser.add_argument( + 'size', help='size in bytes of the new ThinPoolLogicalVolume') + + def get_parser(): '''Create :py:class:`argparse.ArgumentParser` suitable for :program:`qubes-lvm`. @@ -230,12 +256,13 @@ def get_parser(): title='commands', description="For more information see qubes-lvm command -h", dest='command') - init_pool_parser(sub_parsers) + init_clone_parser(sub_parsers) + init_extend_parser(sub_parsers) init_import_parser(sub_parsers) init_new_parser(sub_parsers) - init_volumes_parser(sub_parsers) + init_pool_parser(sub_parsers) init_remove_parser(sub_parsers) - init_clone_parser(sub_parsers) + init_volumes_parser(sub_parsers) return parser diff --git a/qubes/tools/qvm_block.py b/qubes/tools/qvm_block.py index a702d591..9385a009 100644 --- a/qubes/tools/qvm_block.py +++ b/qubes/tools/qvm_block.py @@ -29,6 +29,7 @@ import sys import qubes import qubes.exc import qubes.tools +import qubes.utils def prepare_table(vd_list, full=False): @@ -171,6 +172,17 @@ def detach_volumes(args): sys.exit(1) +def extend_volumes(args): + ''' Called by the parser to execute the :program:`qvm-block extend` + subcommand + ''' + volume = args.volume + app = args.app + size = qubes.utils.parse_size(args.size) + pool = app.get_pool(volume.pool) + pool.resize(volume, volume.size+size) + app.save() + def init_list_parser(sub_parsers): ''' Configures the parser for the :program:`qvm-block list` subcommand ''' # pylint: disable=protected-access @@ -198,6 +210,32 @@ def init_revert_parser(sub_parsers): action=qubes.tools.VolumeAction) revert_parser.set_defaults(func=revert_volume) +def init_attach_parser(sub_parsers): + attach_parser = sub_parsers.add_parser( + 'attach', help="Attach volume to domain", aliases=('at', 'a')) + attach_parser.add_argument('--ro', help='attach device read-only', + action='store_true') + attach_parser.add_argument('VMNAME', action=qubes.tools.RunningVmNameAction) + attach_parser.add_argument(metavar='POOL_NAME:VOLUME_ID', dest='volume', + action=qubes.tools.VolumeAction) + attach_parser.set_defaults(func=attach_volumes) + + +def init_dettach_parser(sub_parsers): + detach_parser = sub_parsers.add_parser( + "detach", help="Detach volume from domain", aliases=('d', 'dt')) + detach_parser.add_argument('VMNAME', action=qubes.tools.RunningVmNameAction) + detach_parser.add_argument(metavar='POOL_NAME:VOLUME_ID', dest='volume', + action=qubes.tools.VolumeAction) + detach_parser.set_defaults(func=detach_volumes) + +def init_extend_parser(sub_parsers): + extend_parser = sub_parsers.add_parser( + "extend", help="extend volume from domain", aliases=('d', 'dt')) + extend_parser.add_argument(metavar='POOL_NAME:VOLUME_ID', dest='volume', + action=qubes.tools.VolumeAction) + extend_parser.add_argument('size', help='New size in bytes') + extend_parser.set_defaults(func=extend_volumes) def get_parser(): '''Create :py:class:`argparse.ArgumentParser` suitable for @@ -209,22 +247,11 @@ def get_parser(): title='commands', description="For more information see qvm-block command -h", dest='command') + init_attach_parser(sub_parsers) + init_dettach_parser(sub_parsers) + init_extend_parser(sub_parsers) init_list_parser(sub_parsers) init_revert_parser(sub_parsers) - attach_parser = sub_parsers.add_parser( - 'attach', help="Attach volume to domain", aliases=('at', 'a')) - attach_parser.add_argument('--ro', help='attach device read-only', - action='store_true') - attach_parser.add_argument('VMNAME', action=qubes.tools.RunningVmNameAction) - attach_parser.add_argument(metavar='POOL_NAME:VOLUME_ID', dest='volume', - action=qubes.tools.VolumeAction) - attach_parser.set_defaults(func=attach_volumes) - detach_parser = sub_parsers.add_parser( - "detach", help="Detach volume from domain", aliases=('d', 'dt')) - detach_parser.add_argument('VMNAME', action=qubes.tools.RunningVmNameAction) - detach_parser.add_argument(metavar='POOL_NAME:VOLUME_ID', dest='volume', - action=qubes.tools.VolumeAction) - detach_parser.set_defaults(func=detach_volumes) return parser