Add qvm-pool and qvm-volume tool

This commit is contained in:
Marek Marczykowski-Górecki 2017-03-13 04:33:12 +01:00
parent 1fd09f4e58
commit 36d8ee9b32
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
5 changed files with 706 additions and 1 deletions

View File

@ -0,0 +1,94 @@
# -*- encoding: utf8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2017 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
import qubesmgmt.tests
import qubesmgmt.tests.tools
import qubesmgmt.tools.qvm_pool
class TC_00_qvm_pool(qubesmgmt.tests.QubesTestCase):
def test_000_list(self):
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
b'0\x00pool-file\npool-lvm\n'
self.app.expected_calls[
('dom0', 'mgmt.pool.Info', 'pool-file', None)] = \
b'0\x00driver=file\ndir_path=/var/lib/qubes\n'
self.app.expected_calls[
('dom0', 'mgmt.pool.Info', 'pool-lvm', None)] = \
b'0\x00driver=lvm\nvolume_group=qubes_dom0\nthin_pool=pool00\n'
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
self.assertEqual(0,
qubesmgmt.tools.qvm_pool.main(['-l'], app=self.app))
self.assertEqual(stdout.getvalue(),
'NAME DRIVER\n'
'pool-file file\n'
'pool-lvm lvm\n')
self.assertAllCalled()
def test_010_list_drivers(self):
self.app.expected_calls[
('dom0', 'mgmt.pool.ListDrivers', None, None)] = \
b'0\x00file dir_path revisions_to_keep\n' \
b'lvm volume_group thin_pool revisions_to_keep\n'
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
self.assertEqual(0,
qubesmgmt.tools.qvm_pool.main(['--help-drivers'], app=self.app))
self.assertEqual(stdout.getvalue(),
'DRIVER OPTIONS\n'
'file dir_path, revisions_to_keep\n'
'lvm volume_group, thin_pool, revisions_to_keep\n'
)
self.assertAllCalled()
def test_020_add(self):
self.app.expected_calls[
('dom0', 'mgmt.pool.Add', 'file',
b'name=test-pool\ndir_path=/some/path\n')] = b'0\x00'
self.assertEqual(0,
qubesmgmt.tools.qvm_pool.main(
['--add', 'test-pool', 'file', '-o', 'dir_path=/some/path'],
app=self.app))
self.assertAllCalled()
def test_030_remove(self):
self.app.expected_calls[
('dom0', 'mgmt.pool.Remove', 'test-pool', None)] = b'0\x00'
self.assertEqual(0,
qubesmgmt.tools.qvm_pool.main(['--remove', 'test-pool'],
app=self.app))
self.assertAllCalled()
def test_040_info(self):
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
b'0\x00pool-file\npool-lvm\n'
self.app.expected_calls[
('dom0', 'mgmt.pool.Info', 'pool-lvm', None)] = \
b'0\x00driver=lvm\nvolume_group=qubes_dom0\nthin_pool=pool00\n'
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
self.assertEqual(0,
qubesmgmt.tools.qvm_pool.main(['-i', 'pool-lvm'],
app=self.app))
self.assertEqual(stdout.getvalue(),
'name pool-lvm\n'
'driver lvm\n'
'thin_pool pool00\n'
'volume_group qubes_dom0\n'
)
self.assertAllCalled()

View File

@ -0,0 +1,167 @@
# -*- encoding: utf8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2017 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
import qubesmgmt.tests
import qubesmgmt.tests.tools
import qubesmgmt.tools.qvm_volume
class TC_00_qvm_volume(qubesmgmt.tests.QubesTestCase):
def setup_expected_calls_for_list(self, vms=('vm1', 'sys-net')):
self.app.expected_calls[
('dom0', 'mgmt.vm.List', None, None)] = \
b'0\x00vm1 class=AppVM state=Running\n' \
b'sys-net class=AppVM state=Running\n'
for vm in vms:
for vol in ('root', 'private'):
self.app.expected_calls[
(vm, 'mgmt.vm.volume.Info', vol, None)] = \
b'0\x00' + \
(b'pool=pool-file\n' if vol == 'root' else
b'pool=other-pool\n') + \
b'vid=' + vm.encode() + b'-' + vol.encode() + b'\n' \
b'internal=True\n' \
b'size=10737418240\n'
self.app.expected_calls[
(vm, 'mgmt.vm.volume.ListSnapshots', vol, None)] = \
b'0\x00snap1\n' if vol == 'private' else b'0\x00'
self.app.expected_calls[
(vm, 'mgmt.vm.volume.List', None, None)] = \
b'0\x00root\nprivate\n'
def test_000_list(self):
self.setup_expected_calls_for_list()
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
self.assertEqual(0,
qubesmgmt.tools.qvm_volume.main(['ls', '-i'], app=self.app))
self.assertEqual(stdout.getvalue(),
'POOL:VOLUME VMNAME VOLUME_NAME '
'REVERT_POSSIBLE\n'
'other-pool:sys-net-private sys-net private Yes\n'
'other-pool:vm1-private vm1 private Yes\n'
'pool-file:sys-net-root sys-net root No\n'
'pool-file:vm1-root vm1 root No\n'
)
self.assertAllCalled()
def test_001_list_domain(self):
self.setup_expected_calls_for_list(vms=('vm1',))
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
self.assertEqual(0,
qubesmgmt.tools.qvm_volume.main(['ls', '-i', 'vm1'],
app=self.app))
self.assertEqual(stdout.getvalue(),
'POOL:VOLUME VMNAME VOLUME_NAME REVERT_POSSIBLE\n'
'other-pool:vm1-private vm1 private Yes\n'
'pool-file:vm1-root vm1 root No\n'
)
self.assertAllCalled()
def test_002_list_domain_pool(self):
self.setup_expected_calls_for_list(vms=('vm1',))
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
b'0\x00pool-file\nother-pool\n'
del self.app.expected_calls[
('vm1', 'mgmt.vm.volume.ListSnapshots', 'private', None)]
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
self.assertEqual(0,
qubesmgmt.tools.qvm_volume.main(
['ls', '-i', '-p', 'pool-file', 'vm1'],
app=self.app))
self.assertEqual(stdout.getvalue(),
'POOL:VOLUME VMNAME VOLUME_NAME REVERT_POSSIBLE\n'
'pool-file:vm1-root vm1 root No\n'
)
self.assertAllCalled()
def test_003_list_pool(self):
self.setup_expected_calls_for_list()
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
b'0\x00pool-file\nother-pool\n'
del self.app.expected_calls[
('vm1', 'mgmt.vm.volume.ListSnapshots', 'private', None)]
del self.app.expected_calls[
('sys-net', 'mgmt.vm.volume.ListSnapshots', 'private', None)]
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
self.assertEqual(0,
qubesmgmt.tools.qvm_volume.main(
['ls', '-i', '-p', 'pool-file'],
app=self.app))
self.assertEqual(stdout.getvalue(),
'POOL:VOLUME VMNAME VOLUME_NAME REVERT_POSSIBLE\n'
'pool-file:sys-net-root sys-net root No\n'
'pool-file:vm1-root vm1 root No\n'
)
self.assertAllCalled()
def test_004_list_multiple_domains(self):
self.setup_expected_calls_for_list(vms=('vm1', 'vm2'))
self.app.expected_calls[
('dom0', 'mgmt.vm.List', None, None)] = \
b'0\x00vm1 class=AppVM state=Running\n' \
b'vm2 class=AppVM state=Running\n' \
b'vm3 class=AppVM state=Running\n'
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
self.assertEqual(0,
qubesmgmt.tools.qvm_volume.main(
['ls', '-i', 'vm1', 'vm2'], app=self.app))
self.assertEqual(stdout.getvalue(),
'POOL:VOLUME VMNAME VOLUME_NAME REVERT_POSSIBLE\n'
'other-pool:vm1-private vm1 private Yes\n'
'other-pool:vm2-private vm2 private Yes\n'
'pool-file:vm1-root vm1 root No\n'
'pool-file:vm2-root vm2 root No\n'
)
self.assertAllCalled()
def test_010_extend(self):
self.app.expected_calls[('dom0', 'mgmt.vm.List', None, None)] = \
b'0\x00testvm class=AppVM state=Running\n'
self.app.expected_calls[
('testvm', 'mgmt.vm.volume.List', None, None)] = \
b'0\x00root\nprivate\n'
self.app.expected_calls[
('testvm', 'mgmt.vm.volume.Resize', 'private', b'10737418240')] = \
b'0\x00'
self.assertEqual(0,
qubesmgmt.tools.qvm_volume.main(
['extend', 'testvm:private', '10GiB'],
app=self.app))
self.assertAllCalled()
def test_011_extend_error(self):
self.app.expected_calls[('dom0', 'mgmt.vm.List', None, None)] = \
b'0\x00testvm class=AppVM state=Running\n'
self.app.expected_calls[
('testvm', 'mgmt.vm.volume.List', None, None)] = \
b'0\x00root\nprivate\n'
self.app.expected_calls[
('testvm', 'mgmt.vm.volume.Resize', 'private', b'1073741824')] = \
b'2\x00StoragePoolException\x00\x00Failed to resize volume: ' \
b'shrink not allowed\x00'
with qubesmgmt.tests.tools.StderrBuffer() as stderr:
self.assertEqual(1,
qubesmgmt.tools.qvm_volume.main(
['extend', 'testvm:private', '1GiB'],
app=self.app))
self.assertIn('shrink not allowed', stderr.getvalue())
self.assertAllCalled()

View File

@ -229,6 +229,45 @@ class VolumeAction(QubesAction):
parser.error('expected a pool & volume id combination like foo:bar')
class VMVolumeAction(QubesAction):
''' Action for argument parser that gets the
:py:class:``qubes.storage.Volume`` from a VM:VOLUME string.
'''
# pylint: disable=too-few-public-methods
def __init__(self, help='A pool & volume id combination',
required=True, **kwargs):
# pylint: disable=redefined-builtin
super(VMVolumeAction, 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
try:
vm_name, vol_name = getattr(namespace, self.dest).split(':')
try:
vm = app.domains[vm_name]
try:
volume = vm.volumes[vol_name]
setattr(namespace, self.dest, volume)
except KeyError:
parser.error_runtime('vm {!r} has no volume {!r}'.format(
vm_name, vol_name))
except KeyError:
parser.error_runtime('no vm {!r}'.format(vm_name))
except ValueError:
parser.error('expected a vm & volume combination like foo:bar')
class PoolsAction(QubesAction):
''' Action for argument parser to gather multiple pools '''
# pylint: disable=too-few-public-methods
@ -247,7 +286,7 @@ class PoolsAction(QubesAction):
pool_names = getattr(namespace, self.dest)
if pool_names:
try:
pools = [app.get_pool(name) for name in pool_names]
pools = [app.pools[name] for name in pool_names]
setattr(namespace, self.dest, pools)
except qubesmgmt.exc.QubesException as e:
parser.error(str(e))

178
qubesmgmt/tools/qvm_pool.py Normal file
View File

@ -0,0 +1,178 @@
# pylint: disable=too-few-public-methods
#
# 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 Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
'''Manages Qubes pools and their options'''
from __future__ import print_function
import argparse
import sys
import qubesmgmt
import qubesmgmt.exc
import qubesmgmt.storage
import qubesmgmt.tools
class _Info(qubesmgmt.tools.PoolsAction):
''' Action for argument parser that displays pool info and exits. '''
def __init__(self, option_strings, help='print pool info and exit',
**kwargs):
# pylint: disable=redefined-builtin
super(_Info, self).__init__(option_strings, help=help, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, 'command', 'info')
super(_Info, self).__call__(parser, namespace, values, option_string)
def pool_info(pool):
''' Prints out pool name and config '''
data = [("name", pool.name)]
data += [i for i in sorted(pool.config.items()) if i[0] != 'name']
qubesmgmt.tools.print_table(data)
def list_pools(app):
''' Prints out all known pools and their drivers '''
result = [('NAME', 'DRIVER')]
for pool in app.pools:
result += [(pool.name, pool.driver)]
qubesmgmt.tools.print_table(result)
class _Remove(argparse.Action):
''' Action for argument parser that removes a pool '''
def __init__(self, option_strings, dest=None, default=None, metavar=None):
super(_Remove, self).__init__(option_strings=option_strings,
dest=dest,
metavar=metavar,
default=default,
help='remove pool')
def __call__(self, parser, namespace, name, option_string=None):
setattr(namespace, 'command', 'remove')
setattr(namespace, 'name', name)
class _Add(argparse.Action):
''' Action for argument parser that adds a pool. '''
def __init__(self, option_strings, dest=None, default=None, metavar=None):
super(_Add, self).__init__(option_strings=option_strings,
dest=dest,
metavar=metavar,
default=default,
nargs=2,
help='add pool')
def __call__(self, parser, namespace, values, option_string=None):
name, driver = values
setattr(namespace, 'command', 'add')
setattr(namespace, 'name', name)
setattr(namespace, 'driver', driver)
class _Options(argparse.Action):
''' Action for argument parser that parsers options. '''
def __init__(self, option_strings, dest, default, metavar='options'):
super(_Options, self).__init__(
option_strings=option_strings,
dest=dest,
metavar=metavar,
default=default,
help='comma-separated list of driver options')
def __call__(self, parser, namespace, options, option_string=None):
setattr(namespace, 'options',
dict([option.split('=', 1) for option in options.split(',')]))
def get_parser():
''' Parses the provided args '''
parser = qubesmgmt.tools.QubesArgumentParser(description=__doc__)
parser.add_argument('-o', action=_Options, dest='options', default={})
group = parser.add_mutually_exclusive_group()
group.add_argument('-l',
'--list',
dest='command',
const='list',
action='store_const',
help='list all pools and exit (default action)')
group.add_argument('-i', '--info', metavar='POOLNAME', dest='pools',
action=_Info, default=[])
group.add_argument('-a',
'--add',
action=_Add,
dest='command',
metavar=('NAME', 'DRIVER'))
group.add_argument('-r', '--remove', metavar='NAME', action=_Remove)
group.add_argument('--help-drivers',
dest='command',
const='list-drivers',
action='store_const',
help='list all drivers with their options and exit')
return parser
def main(args=None, app=None):
'''Main routine of :program:`qvm-pools`.
:param list args: Optional arguments to override those delivered from \
command line.
'''
parser = get_parser()
try:
args = parser.parse_args(args, app=app)
except qubesmgmt.exc.QubesException as e:
parser.print_error(str(e))
return 1
if args.command is None or args.command == 'list':
list_pools(args.app)
elif args.command == 'list-drivers':
result = [('DRIVER', 'OPTIONS')]
for driver in sorted(args.app.pool_drivers):
params = args.app.pool_driver_parameters(driver)
driver_options = ', '.join(params)
result += [(driver, driver_options)]
qubesmgmt.tools.print_table(result)
elif args.command == 'add':
try:
args.app.add_pool(name=args.name, driver=args.driver,
**args.options)
except qubesmgmt.exc.QubesException as e:
parser.error('failed to add pool %s: %s\n' % (args.name, str(e)))
elif args.command == 'remove':
try:
args.app.remove_pool(args.name)
except KeyError:
parser.print_error('no such pool %s\n' % args.name)
elif args.command == 'info':
for pool in args.pools:
pool_info(pool)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,227 @@
# encoding=utf-8
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
# Copyright (C) 2017 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
'''Qubes volume management'''
from __future__ import print_function
import sys
import qubesmgmt
import qubesmgmt.exc
import qubesmgmt.tools
import qubesmgmt.utils
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-volume` 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 = []
output += [('POOL:VOLUME', 'VMNAME', 'VOLUME_NAME', 'REVERT_POSSIBLE')]
for volume in sorted(vd_list):
if volume.domains:
vmname, volume_name = volume.domains.pop()
output += [(str(volume), vmname, volume_name, volume.revisions)]
for tupple in volume.domains:
vmname, volume_name = tupple
if full or not sys.stdout.isatty():
output += [(str(volume), vmname, volume_name,
volume.revisions)]
else:
output += [('', vmname, volume_name, volume.revisions)]
else:
output += [(str(volume), "")]
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.pool = volume.pool
self.vid = volume.vid
if len(volume.revisions) > 0:
self.revisions = 'Yes'
else:
self.revisions = 'No'
self.domains = []
def __lt__(self, other):
return (self.pool, self.vid) < (other.pool, other.vid)
def __str__(self):
return "{!s}:{!s}".format(self.pool, self.vid)
def list_volumes(args):
''' Called by the parser to execute the qvm-volume list subcommand. '''
app = args.app
if hasattr(args, 'domains') and args.domains:
domains = args.domains
else:
domains = app.domains
volumes = [v for vm in domains for v in vm.volumes.values()]
if args.pools:
# only specified pools
volumes = [v for v in volumes if v.pool in args.pools]
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}
for domain in domains: # gather the domain names
try:
for name, volume in domain.volumes.items():
try:
volume_data = vd_dict[volume.pool][volume.vid]
volume_data.domains += [(domain.name, 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.values() for x in p.values()
if x.domains]
else:
result = [x for p in vd_dict.values() for x in p.values()]
qubesmgmt.tools.print_table(prepare_table(result, full=args.full))
def revert_volume(args):
''' Revert volume to previous state '''
volume = args.volume
app = args.app
try:
pool = app.pools[volume.pool]
pool.revert(volume)
except qubesmgmt.exc.StoragePoolException as e:
print(str(e), file=sys.stderr)
sys.exit(1)
def extend_volumes(args):
''' Called by the parser to execute the :program:`qvm-block extend`
subcommand
'''
volume = args.volume
size = qubesmgmt.utils.parse_size(args.size)
volume.resize(size)
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 storage volumes')
list_parser.add_argument('-p', '--pool', dest='pools',
action=qubesmgmt.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 = qubesmgmt.tools.VmNameGroup(
list_parser, required=False, vm_action=qubesmgmt.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 init_revert_parser(sub_parsers):
''' Add 'revert' action related options '''
revert_parser = sub_parsers.add_parser(
'revert', aliases=('rv', 'r'),
help='revert volume to previous revision')
revert_parser.add_argument(metavar='VM:VOLUME', dest='volume',
action=qubesmgmt.tools.VMVolumeAction)
revert_parser.set_defaults(func=revert_volume)
def init_extend_parser(sub_parsers):
''' Add 'extend' action related options '''
extend_parser = sub_parsers.add_parser(
"extend", help="extend volume from domain", aliases=('d', 'dt'))
extend_parser.add_argument(metavar='VM:VOLUME', dest='volume',
action=qubesmgmt.tools.VMVolumeAction)
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
:program:`qvm-block`.
'''
parser = qubesmgmt.tools.QubesArgumentParser(description=__doc__,
want_app=True)
parser.register('action', 'parsers',
qubesmgmt.tools.AliasedSubParsersAction)
sub_parsers = parser.add_subparsers(
title='commands',
description="For more information see qvm-block command -h",
dest='command')
init_extend_parser(sub_parsers)
init_list_parser(sub_parsers)
init_revert_parser(sub_parsers)
return parser
def main(args=None, app=None):
'''Main routine of :program:`qvm-block`.'''
parser = get_parser()
try:
args = parser.parse_args(args, app=app)
args.func(args)
except qubesmgmt.exc.QubesException as e:
parser.print_error(str(e))
return 1
return 0
if __name__ == '__main__':
sys.exit(main())