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..4de7923b 100644 --- a/doc/manpages/qvm-block.rst +++ b/doc/manpages/qvm-block.rst @@ -1,11 +1,10 @@ .. 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*> @@ -13,38 +12,43 @@ Synopsis Options -======= +------- .. option:: --help, -h - Show this help message and exit + Show this help message and exit .. option:: --list, -l - List block devices + List block devices .. option:: --attach, -a - Attach block device to specified VM + Attach block device to specified domain .. option:: --detach, -d - Detach block device + Detach block device -.. option:: --frontend=FRONTEND, -f FRONTEND - Specify device name at destination VM [default: xvdi] +.. option:: -D, --devtype + + Device type. Default is disk .. option:: --ro - Force read-only mode + Force read-only mode .. option:: --no-auto-detach - Fail when device already connected to other VM + Fail when device already connected to other domain 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 c1a66c74..70e030ec 100644 --- a/qubes/storage/__init__.py +++ b/qubes/storage/__init__.py @@ -164,6 +164,18 @@ class Storage(object): 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. diff --git a/qubes/tools/qvm_block.py b/qubes/tools/qvm_block.py new file mode 100644 index 00000000..a76fe521 --- /dev/null +++ b/qubes/tools/qvm_block.py @@ -0,0 +1,228 @@ +#!/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 +from lxml import etree + +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.volumes.values(): + 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 + + for domain in domains: + if domain.qid == 0: + continue + lvirt = domain.libvirt_domain + xml = etree.fromstring(lvirt.XMLDesc()) + disks = xml.xpath("//domain/devices/disk") + for disk in disks: + if disk.find('backenddomain') is not None: + pool = 'p_%s' % disk.find('backenddomain').get('name') + vid = disk.find('source').get('dev').split('/dev/')[1] + volume_data = vd_dict[pool][vid] + volume_data.domains += [domain.name] + + 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/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*