diff --git a/ci/pylintrc b/ci/pylintrc index cb32a499..2bf4f092 100644 --- a/ci/pylintrc +++ b/ci/pylintrc @@ -36,7 +36,7 @@ ignored-classes= ignore-mixin-members=yes generated-members= iter_entry_points, - Element,ElementTree,QName,parse,tostring + Element,ElementTree,QName,SubElement,fromstring,parse,tostring, [BASIC] diff --git a/doc/conf.py b/doc/conf.py index c8c79e6c..a526ff58 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -250,7 +250,7 @@ man_pages = [ ('manpages/qvm-backup', 'qvm-backup', u'Create backup of specified VMs', _man_pages_author, 1), ('manpages/qvm-block', 'qvm-block', - u'List/set VM block devices.', _man_pages_author, 1), + u'Qubes volume and block device managment', _man_pages_author, 1), ('manpages/qvm-clone-template', 'qvm-clone-template', u'Clones an existing template by copying all its disk files', _man_pages_author, 1), ('manpages/qvm-clone', 'qvm-clone', diff --git a/doc/manpages/qvm-block.rst b/doc/manpages/qvm-block.rst index 143c7846..60f4d22c 100644 --- a/doc/manpages/qvm-block.rst +++ b/doc/manpages/qvm-block.rst @@ -1,50 +1,97 @@ .. program:: qvm-block -=============================================== -:program:`qvm-block` -- List/set VM PCI devices -=============================================== +:program:`qvm-block` -- Qubes volume and block device managment +=============================================================== Synopsis -======== -| :command:`qvm-block` [*options*] -l -| :command:`qvm-block` [*options*] -a <*device*> <*vm-name*> -| :command:`qvm-block` [*options*] -d <*device*> -| :command:`qvm-block` [*options*] -d <*vm-name*> +-------- +| :command:`qvm-block` *COMMAND* [-h] [--verbose] [--quiet] [options] [arguments] + +Description +----------- + +.. TODO Add description Options -======= +------- .. option:: --help, -h - Show this help message and exit + Show help message and exit -.. option:: --list, -l +.. option:: --verbose, -v - List block devices + Increase verbosity. -.. option:: --attach, -a +.. option:: --quiet, -q - Attach block device to specified VM + Decrease verbosity. -.. option:: --detach, -d +Commands +-------- - Detach block device +list +^^^^ -.. option:: --frontend=FRONTEND, -f FRONTEND +| :command:`qvm-block list` [-h] [--verbose] [--quiet] [-p *POOL_NAME*] [-i] [*VMNAME* [*VMNAME* ...]] - Specify device name at destination VM [default: xvdi] +List block devices. By default the internal devices are hidden. When the +stdout is connected to a TTY `qvm-block list` will print a pretty table by +omitting redundant data. This behaviour is disabled when `--full` option is +passed or stdout is redirected to a pipe or file. + +.. option:: -p, --pool + + list volumes from specified pool + +.. option:: -i, --internal + + list internal devices + +.. option:: --full + + print domain names + +.. option:: --all + + List volumes from all qubes. You can use :option:`--exclude` to limit the + qubes set. Don't forget — internal devices are hidden by default! + +.. option:: --exclude + + Exclude the qube from :option:`--all`. + +aliases: ls, l + +attach +^^^^^^ + +| :command:`qvm-block attach` [-h] [--verbose] [--quiet] [--ro] *VMNAME* *POOL_NAME:VOLUME_ID* + +Attach the volume with *VOLUME_ID* from *POOL_NAME* to the domain *VMNAME* .. option:: --ro - Force read-only mode + attach device read-only -.. option:: --no-auto-detach +aliases: a, at - Fail when device already connected to other VM +detach +^^^^^^ + +| :command:`qvm-block detach` [-h] [--verbose] [--quiet] *VMNAME* *POOL_NAME:VOLUME_ID* + +Detach the volume with *POOL_NAME:VOLUME_ID* from domain *VMNAME* + +aliases: d, dt Authors -======= +------- + | Joanna Rutkowska | Rafal Wojtczuk | Marek Marczykowski +| Bahtiar `kalkin-` Gadimov + +.. vim: ts=3 sw=3 et tw=80 diff --git a/qubes/storage/__init__.py b/qubes/storage/__init__.py index 876e0998..5f5bbda1 100644 --- a/qubes/storage/__init__.py +++ b/qubes/storage/__init__.py @@ -29,14 +29,14 @@ from __future__ import absolute_import import os import os.path +import string -import pkg_resources import lxml.etree - +import pkg_resources import qubes +import qubes.devices import qubes.exc import qubes.utils -import qubes.devices STORAGE_ENTRY_POINT = 'qubes.storage' @@ -58,7 +58,7 @@ class Volume(object): usage = 0 def __init__(self, name, pool, volume_type, vid=None, size=0, - removable=False, **kwargs): + removable=False, internal=False, **kwargs): super(Volume, self).__init__(**kwargs) self.name = str(name) self.pool = str(pool) @@ -66,6 +66,7 @@ class Volume(object): self.size = size self.volume_type = volume_type self.removable = removable + self.internal = internal def __xml__(self): return lxml.etree.Element('volume', **self.config) @@ -78,9 +79,7 @@ class Volume(object): 'volume_type': self.volume_type} def __repr__(self): - return '{}(name={!s}, pool={!r}, vid={!r}, volume_type={!r})'.format( - self.__class__.__name__, self.name, self.pool, self.vid, - self.volume_type) + return '{!r}'.format(self.pool + ':' + self.vid) def block_device(self): ''' Return :py:class:`qubes.devices.BlockDevice` for serialization in @@ -89,6 +88,16 @@ class Volume(object): return qubes.devices.BlockDevice(self.path, self.name, self.script, self.rw, self.domain, self.devtype) + def __eq__(self, other): + return other.pool == self.pool and other.vid == self.vid \ + and other.volume_type == self.volume_type + + def __neq__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash('%s:%s %s' % (self.pool, self.vid, self.volume_type)) + class Storage(object): ''' Class for handling VM virtual disks. @@ -97,6 +106,8 @@ class Storage(object): in mind. ''' + AVAILABLE_FRONTENDS = set(['xvd' + c for c in string.ascii_lowercase]) + def __init__(self, vm): #: Domain for which we manage storage self.vm = vm @@ -112,6 +123,59 @@ class Storage(object): self.vm.volumes[name] = pool.init_volume(self.vm, conf) self.pools[name] = pool + def attach(self, volume, rw=False): + ''' Attach a volume to the domain ''' + assert self.vm.is_running() + + if self._is_already_attached(volume): + self.vm.log.info("{!r} already attached".format(volume)) + return + + try: + frontend = self.unused_frontend() + except IndexError: + raise StoragePoolException("No unused frontend found") + disk = lxml.etree.Element("disk") + disk.set('type', 'block') + disk.set('device', 'disk') + lxml.etree.SubElement(disk, 'driver').set('name', 'phy') + lxml.etree.SubElement(disk, 'source').set('dev', '/dev/%s' % volume.vid) + lxml.etree.SubElement(disk, 'target').set('dev', frontend) + if not rw: + lxml.etree.SubElement(disk, 'readonly') + + if self.vm.qid != 0: + lxml.etree.SubElement(disk, 'backenddomain').set( + 'name', volume.pool.split('p_')[1]) + + xml_string = lxml.etree.tostring(disk, encoding='utf-8') + self.vm.libvirt_domain.attachDevice(xml_string) + # trigger watches to update device status + # FIXME: this should be removed once libvirt will report such + # events itself + # self.vm.qdb.write('/qubes-block-devices', '') ← do we need this? + + def _is_already_attached(self, volume): + ''' Checks if the given volume is already attached ''' + parsed_xml = lxml.etree.fromstring(self.vm.libvirt_domain.XMLDesc()) + disk_sources = parsed_xml.xpath("//domain/devices/disk/source") + for source in disk_sources: + if source.get('dev') == '/dev/%s' % volume.vid: + return True + return False + + def detach(self, volume): + ''' Detach a volume from domain ''' + parsed_xml = lxml.etree.fromstring(self.vm.libvirt_domain.XMLDesc()) + disks = parsed_xml.xpath("//domain/devices/disk") + for disk in disks: + source = disk.xpath('source')[0] + if source.get('dev') == '/dev/%s' % volume.vid: + disk_xml = lxml.etree.tostring(disk, encoding='utf-8') + self.vm.libvirt_domain.detachDevice(disk_xml) + return + raise StoragePoolException('Volume {!r} is not attached'.format(volume)) + @property def kernels_dir(self): '''Directory where kernel resides. @@ -192,6 +256,8 @@ class Storage(object): raise qubes.exc.QubesVMError( self.vm, 'VM directory does not exist: {}'.format(self.vm.dir_path)) + for volume in self.vm.volumes.values(): + self.get_pool(volume).verify(volume) self.vm.fire_event('domain-verify-files') def remove(self): @@ -226,6 +292,21 @@ class Storage(object): if volume.volume_type == 'origin': self.get_pool(volume).commit_template_changes(volume) + def unused_frontend(self): + ''' Find an unused device name ''' + unused_frontends = self.AVAILABLE_FRONTENDS.difference( + self.used_frontends) + return sorted(unused_frontends)[0] + + @property + def used_frontends(self): + ''' Used device names ''' + xml = self.vm.libvirt_domain.XMLDesc() + parsed_xml = lxml.etree.fromstring(xml) + return set([target.get('dev', None) + for target in parsed_xml.xpath( + "//domain/devices/disk/target")]) + class Pool(object): ''' A Pool is used to manage different kind of volumes (File @@ -307,6 +388,17 @@ class Pool(object): raise NotImplementedError("Pool %s has init_volume() not implemented" % self.name) + def verify(self, volume): + ''' Verifies the volume. ''' + raise NotImplementedError("Pool %s has verify() not implemented" % + self.name) + + @property + def volumes(self): + ''' Return a list of volumes managed by this pool ''' + raise NotImplementedError("Pool %s has volumes() not implemented" % + self.name) + def pool_drivers(): """ Return a list of EntryPoints names """ diff --git a/qubes/storage/file.py b/qubes/storage/file.py index 6e1c9be0..a5c33777 100644 --- a/qubes/storage/file.py +++ b/qubes/storage/file.py @@ -46,6 +46,7 @@ class FilePool(Pool): super(FilePool, self).__init__(name=name) assert dir_path, "No pool dir_path specified" self.dir_path = os.path.normpath(dir_path) + self._volumes = [] def clone(self, source, target): ''' Clones the volume if the `source.pool` if the source is a @@ -266,7 +267,16 @@ class FilePool(Pool): else: volume_config['target_dir'] = self.target_dir(vm) - return known_types[volume_type](**volume_config) + volume = known_types[volume_type](**volume_config) + self._volumes += [volume] + return volume + + def verify(self, volume): + return volume.verify() + + @property + def volumes(self): + return self._volumes class FileVolume(Volume): @@ -313,6 +323,7 @@ class SizeMixIn(FileVolume): class ReadWriteFile(SizeMixIn): ''' Represents a readable & writable file image based volume ''' + def __init__(self, **kwargs): super(ReadWriteFile, self).__init__(**kwargs) self.path = os.path.join(self.target_dir, self.name + '.img') @@ -330,6 +341,11 @@ class ReadWriteFile(SizeMixIn): self.path = new_path self.vid = self.path + def verify(self): + ''' Verifies the volume. ''' + if not os.path.exists(self.path): + raise StoragePoolException('Missing image file: %s' % self.path) + class ReadOnlyFile(FileVolume): ''' Represents a readonly file image based volume ''' @@ -358,6 +374,11 @@ class ReadOnlyFile(FileVolume): self.path = new_path self.vid = self.path + def verify(self): + ''' Verifies the volume. ''' + if not os.path.exists(self.path): + raise StoragePoolException('Missing image file: %s' % self.path) + class OriginFile(SizeMixIn): ''' Represents a readable, writeable & snapshotable file image based volume. @@ -403,6 +424,12 @@ class OriginFile(SizeMixIn): result += get_disk_usage(self.path_cow) return result + def verify(self): + ''' Verifies the volume. ''' + if not os.path.exists(self.path_origin): + raise StoragePoolException('Missing image file: %s' % + self.path_origin) + class SnapshotFile(FileVolume): ''' Represents a readonly snapshot of an :py:class:`OriginFile` volume ''' @@ -418,6 +445,12 @@ class SnapshotFile(FileVolume): self.path = '%s:%s' % (self.path_origin, self.path_cow) self.vid = self.path_origin + def verify(self): + ''' Verifies the volume. ''' + if not os.path.exists(self.path_origin): + raise StoragePoolException('Missing image file: %s' % + self.path_origin) + class VolatileFile(SizeMixIn): ''' Represents a readable & writeable file based volume, which will be @@ -439,6 +472,10 @@ class VolatileFile(SizeMixIn): self.path = new_path self.vid = self.path + def verify(self): + ''' Verifies the volume. ''' + pass + def create_sparse_file(path, size): ''' Create an empty sparse file ''' diff --git a/qubes/storage/kernels.py b/qubes/storage/kernels.py index 895c18ae..afd323e8 100644 --- a/qubes/storage/kernels.py +++ b/qubes/storage/kernels.py @@ -58,10 +58,6 @@ class LinuxKernel(Pool): volume = LinuxModules(self.dir_path, vm.kernel, **volume_config) - _check_path(volume.path) - _check_path(volume.vmlinuz) - _check_path(volume.initramfs) - return volume def clone(self, source, target): @@ -106,6 +102,11 @@ class LinuxKernel(Pool): def stop(self, volume): pass + def verify(self, volume): + _check_path(volume.path) + _check_path(volume.vmlinuz) + _check_path(volume.initramfs) + @property def volumes(self): ''' Return all known kernel volumes ''' @@ -113,6 +114,7 @@ class LinuxKernel(Pool): kernel_version, pool=self.name, name=kernel_version, + internal=True, volume_type='read-only') for kernel_version in os.listdir(self.dir_path)] diff --git a/qubes/tools/__init__.py b/qubes/tools/__init__.py index 71a1517f..1cff3fd4 100644 --- a/qubes/tools/__init__.py +++ b/qubes/tools/__init__.py @@ -199,25 +199,103 @@ class VmNameAction(QubesAction): parser.error('no such domain: {!r}'.format(vm_name)) -class PoolsAction(QubesAction): - ''' Action for argument parser to gather multiple pools ''' +class RunningVmNameAction(VmNameAction): + ''' Action for argument parser that gets a running domain from VMNAME ''' # pylint: disable=too-few-public-methods + def __init__(self, option_strings, nargs=1, dest='vmnames', help=None, + **kwargs): + # pylint: disable=redefined-builtin + if help is None: + if nargs == argparse.OPTIONAL: + help = 'at most one running domain' + elif nargs == 1: + help = 'running domain name' + elif nargs == argparse.ZERO_OR_MORE: + help = 'zero or more running domains' + elif nargs == argparse.ONE_OR_MORE: + help = 'one or more running domains' + elif nargs > 1: + help = '%s running domains' % nargs + else: + raise argparse.ArgumentError( + nargs, "Passed unexpected value {!s} as {!s} nargs ".format( + nargs, dest)) + super(RunningVmNameAction, self).__init__( + option_strings, dest=dest, help=help, nargs=nargs, **kwargs) + + def parse_qubes_app(self, parser, namespace): + super(RunningVmNameAction, self).parse_qubes_app(parser, namespace) + for vm in namespace.domains: + if not vm.is_running(): + parser.error_runtime("domain {!r} is not running".format( + vm.name)) + + +class VolumeAction(QubesAction): + ''' Action for argument parser that gets the + :py:class:``qubes.storage.Volume`` from a POOL_NAME:VOLUME_ID string. + ''' + # pylint: disable=too-few-public-methods + + def __init__(self, help='A pool & volume id combination', + required=True, **kwargs): + # pylint: disable=redefined-builtin + super(VolumeAction, self).__init__(help=help, required=required, + **kwargs) + def __call__(self, parser, namespace, values, option_string=None): ''' Set ``namespace.vmname`` to ``values`` ''' setattr(namespace, self.dest, values) def parse_qubes_app(self, parser, namespace): + ''' Acquire the :py:class:``qubes.storage.Volume`` object from + ``namespace.app``. + ''' + assert hasattr(namespace, 'app') app = namespace.app - name = getattr(namespace, self.dest) - if not name: - return - try: - setattr(namespace, self.dest, app.get_pool(name)) - except qubes.exc.QubesException as e: - parser.error(e.message) - sys.exit(2) + try: + pool_name, vid = getattr(namespace, self.dest).split(':') + try: + pool = app.pools[pool_name] + volume = [v for v in pool.volumes if v.vid == vid] + assert volume > 1, 'Duplicate vids in pool %s' % pool_name + if len(volume) == 0: + parser.error_runtime( + 'no volume with id {!r} pool: {!r}'.format(vid, + pool_name)) + else: + setattr(namespace, self.dest, volume[0]) + except KeyError: + parser.error_runtime('no pool {!r}'.format(pool_name)) + except ValueError: + parser.error('expected a pool & volume id combination like foo:bar') + + +class PoolsAction(QubesAction): + ''' Action for argument parser to gather multiple pools ''' + # pylint: disable=too-few-public-methods + + def __call__(self, parser, namespace, values, option_string=None): + ''' Set ``namespace.vmname`` to ``values`` ''' + if hasattr(namespace, self.dest) and getattr(namespace, self.dest): + names = getattr(namespace, self.dest) + else: + names = [] + names += [values] + setattr(namespace, self.dest, names) + + def parse_qubes_app(self, parser, namespace): + app = namespace.app + pool_names = getattr(namespace, self.dest) + if pool_names: + try: + pools = [app.get_pool(name) for name in pool_names] + setattr(namespace, self.dest, pools) + except qubes.exc.QubesException as e: + parser.error(e.message) + sys.exit(2) class QubesArgumentParser(argparse.ArgumentParser): @@ -285,8 +363,17 @@ class QubesArgumentParser(argparse.ArgumentParser): self.dont_run_as_root(namespace) for action in self._actions: + # pylint: disable=protected-access if issubclass(action.__class__, QubesAction): action.parse_qubes_app(self, namespace) + elif issubclass(action.__class__, + argparse._SubParsersAction): # pylint: disable=no-member + assert hasattr(namespace, 'command') + command = namespace.command + subparser = action._name_parser_map[command] + for subaction in subparser._actions: + if issubclass(subaction.__class__, QubesAction): + subaction.parse_qubes_app(self, namespace) return namespace @@ -409,17 +496,20 @@ class VmNameGroup(argparse._MutuallyExclusiveGroup): :py:class:``argparse.ArgumentParser```. ''' - def __init__(self, container, required, vm_action=VmNameAction): + def __init__(self, container, required, vm_action=VmNameAction, help=None): + # pylint: disable=redefined-builtin super(VmNameGroup, self).__init__(container, required=required) - self.add_argument('--all', action='store_true', - dest='all_domains', - help='perform the action on all qubes') + if not help: + help = 'perform the action on all qubes' + self.add_argument('--all', action='store_true', dest='all_domains', + help=help) container.add_argument('--exclude', action='append', default=[], help='exclude the qube from --all') - self.add_argument('VMNAME', action=vm_action, nargs='*', - default=[]) # the default parameter is important! see - # https://stackoverflow.com/questions/35044288 - # and `argparse.ArgumentParser.parse_args()` + + # ⚠ the default parameter below is important! ⚠ + # See https://stackoverflow.com/questions/35044288 and + # `argparse.ArgumentParser.parse_args()` implementation + self.add_argument('VMNAME', action=vm_action, nargs='*', default=[]) def print_table(table): diff --git a/qubes/tools/qvm_block.py b/qubes/tools/qvm_block.py new file mode 100644 index 00000000..4f1d708a --- /dev/null +++ b/qubes/tools/qvm_block.py @@ -0,0 +1,213 @@ +#!/usr/bin/python2 +# pylint: disable=C,R +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov +# +# 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. + +'''Qubes volume and block device managment''' + +from __future__ import print_function + +import sys + +import qubes +import qubes.exc +import qubes.tools + + +def prepare_table(vd_list, full=False): + ''' Converts a list of :py:class:`VolumeData` objects to a list of tupples + for the :py:func:`qubes.tools.print_table`. + + If :program:`qvm-block` is running in a TTY, it will ommit duplicate + data. + + :param list vd_list: List of :py:class:`VolumeData` objects. + :param bool full: If set to true duplicate data is printed even when + running from TTY. + :returns: list of tupples + ''' + output = [] + if sys.stdout.isatty(): + output += [('POOL_NAME:VOLUME_ID', 'VOLUME_TYPE', 'VMNAME')] + + for volume in vd_list: + if volume.domains: + vmname = volume.domains.pop() + output += [(str(volume), volume.volume_type, vmname)] + for vmname in volume.domains: + if full or not sys.stdout.isatty(): + output += [(str(volume), volume.volume_type, vmname)] + else: + output += [('', '', vmname)] + else: + output += [(str(volume), volume.volume_type)] + + return output + + +class VolumeData(object): + ''' Wrapper object around :py:class:`qubes.storage.Volume`, mainly to track + the domains a volume is attached to. + ''' + # pylint: disable=too-few-public-methods + def __init__(self, volume): + self.name = volume.name + self.pool = volume.pool + self.volume_type = volume.volume_type + self.vid = volume.vid + self.domains = [] + + def __str__(self): + return "{!s}:{!s}".format(self.pool, self.vid) + + +def list_volumes(args): + ''' Called by the parser to execute the qubes-block list subcommand. ''' + app = args.app + + if args.pools: + pools = args.pools # only specified pools + else: + pools = app.pools.values() # all pools + + volumes = [v for p in pools for v in p.volumes] + + if not args.internal: # hide internal volumes + volumes = [v for v in volumes if not v.internal] + + vd_dict = {} + + for volume in volumes: + volume_data = VolumeData(volume) + try: + vd_dict[volume.pool][volume.vid] = volume_data + except KeyError: + vd_dict[volume.pool] = {volume.vid: volume_data} + + if hasattr(args, 'domains') and args.domains: + domains = args.domains + else: + domains = args.app.domains + for domain in domains: # gather the domain names + try: + for volume in domain.attached_volumes: + try: + volume_data = vd_dict[volume.pool][volume.vid] + volume_data.domains += [domain.name] + except KeyError: + # Skipping volume + continue + except AttributeError: + # Skipping domain without volumes + continue + + if hasattr(args, 'domains') and args.domains: + result = [x # reduce to only VolumeData with assigned domains + for p in vd_dict.itervalues() for x in p.itervalues() + if x.domains] + else: + result = [x for p in vd_dict.itervalues() for x in p.itervalues()] + qubes.tools.print_table(prepare_table(result, full=args.full)) + + +def attach_volumes(args): + ''' Called by the parser to execute the :program:`qvm-block attach` + subcommand. + ''' + volume = args.volume + vm = args.domains[0] + try: + rw = not args.ro + vm.storage.attach(volume, rw=rw) + except qubes.storage.StoragePoolException as e: + print(e.message, file=sys.stderr) + sys.exit(1) + + +def detach_volumes(args): + ''' Called by the parser to execute the :program:`qvm-block detach` + subcommand. + ''' + volume = args.volume + vm = args.domains[0] + try: + vm.storage.detach(volume) + except qubes.storage.StoragePoolException as e: + print(e.message, file=sys.stderr) + sys.exit(1) + + +def init_list_parser(sub_parsers): + ''' Configures the parser for the :program:`qvm-block list` subcommand ''' + # pylint: disable=protected-access + list_parser = sub_parsers.add_parser('list', aliases=('ls', 'l'), + help='list block devices') + list_parser.add_argument('-p', '--pool', dest='pools', + action=qubes.tools.PoolsAction) + list_parser.add_argument('-i', '--internal', action='store_true', + help='Show internal volumes') + list_parser.add_argument( + '--full', action='store_true', + help='print full line for each POOL_NAME:VOLUME_ID & vm combination') + + vm_name_group = qubes.tools.VmNameGroup( + list_parser, required=False, vm_action=qubes.tools.VmNameAction, + help='list volumes from specified domain(s)') + list_parser._mutually_exclusive_groups.append(vm_name_group) + list_parser.set_defaults(func=list_volumes) + + +def get_parser(): + '''Create :py:class:`argparse.ArgumentParser` suitable for + :program:`qvm-block`. + ''' + parser = qubes.tools.QubesArgumentParser(description=__doc__, want_app=True) + parser.register('action', 'parsers', qubes.tools.AliasedSubParsersAction) + sub_parsers = parser.add_subparsers( + title='commands', + description="For more information see qvm-block command -h", + dest='command') + init_list_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 + + +def main(args=None): + '''Main routine of :program:`qvm-block`.''' + args = get_parser().parse_args(args) + args.func(args) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/qubes/vm/adminvm.py b/qubes/vm/adminvm.py index aa06a3ac..965a4434 100644 --- a/qubes/vm/adminvm.py +++ b/qubes/vm/adminvm.py @@ -42,6 +42,10 @@ class AdminVM(qubes.vm.qubesvm.QubesVM): default=None, doc='There are other ways to set kernel for Dom0.') + @property + def attached_volumes(self): + return [] + @property def xid(self): '''Always ``0``. diff --git a/qubes/vm/appvm.py b/qubes/vm/appvm.py index 6924d3b7..f567213a 100644 --- a/qubes/vm/appvm.py +++ b/qubes/vm/appvm.py @@ -45,23 +45,27 @@ class AppVM(qubes.vm.qubesvm.QubesVM): 'name': 'root', 'pool': 'default', 'volume_type': 'snapshot', + 'internal': True }, 'private': { 'name': 'private', 'pool': 'default', 'volume_type': 'origin', 'size': defaults['private_img_size'], + 'internal': True }, 'volatile': { 'name': 'volatile', 'pool': 'default', 'volume_type': 'volatile', 'size': defaults['root_img_size'], + 'internal': True }, 'kernel': { 'name': 'kernel', 'pool': 'linux-kernel', 'volume_type': 'read-only', + 'internal': True } } super(AppVM, self).__init__(*args, **kwargs) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 2d99e054..afe1a21e 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -325,6 +325,23 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): e.get_error_code())) raise + @property + def attached_volumes(self): + result = [] + xml_desc = self.libvirt_domain.XMLDesc() + xml = lxml.etree.fromstring(xml_desc) + for disk in xml.xpath("//domain/devices/disk"): + if disk.find('backenddomain') is not None: + pool_name = 'p_%s' % disk.find('backenddomain').get('name') + pool = self.app.pools[pool_name] + vid = disk.find('source').get('dev').split('/dev/')[1] + for volume in pool.volumes: + if volume.vid == vid: + result += [volume] + break + + return result + self.volumes.values() + @property def libvirt_domain(self): '''Libvirt domain object from libvirt. diff --git a/qubes/vm/templatevm.py b/qubes/vm/templatevm.py index ca97dec4..e681f5a0 100644 --- a/qubes/vm/templatevm.py +++ b/qubes/vm/templatevm.py @@ -62,23 +62,27 @@ class TemplateVM(QubesVM): 'pool': 'default', 'volume_type': 'origin', 'size': defaults['root_img_size'], + 'internal': True }, 'private': { 'name': 'private', 'pool': 'default', 'volume_type': 'read-write', 'size': defaults['private_img_size'], + 'internal': True }, 'volatile': { 'name': 'volatile', 'pool': 'default', 'size': defaults['root_img_size'], 'volume_type': 'volatile', + 'internal': True }, 'kernel': { 'name': 'kernel', 'pool': 'linux-kernel', 'volume_type': 'read-only', + 'internal': True } } super(TemplateVM, self).__init__(*args, **kwargs) diff --git a/rpm_spec/core-dom0.spec b/rpm_spec/core-dom0.spec index bc4a6ef8..b6a5927f 100644 --- a/rpm_spec/core-dom0.spec +++ b/rpm_spec/core-dom0.spec @@ -244,6 +244,7 @@ fi %{python_sitelib}/qubes/tools/qubes_create.py* %{python_sitelib}/qubes/tools/qubes_monitor_layout_notify.py* %{python_sitelib}/qubes/tools/qubes_prefs.py* +%{python_sitelib}/qubes/tools/qvm_block.py* %{python_sitelib}/qubes/tools/qvm_create.py* %{python_sitelib}/qubes/tools/qvm_features.py* %{python_sitelib}/qubes/tools/qvm_kill.py*