Merge remote-tracking branch 'origin/pr/99'

* origin/pr/99:
  devices: add missing docstring for _get_device_classes
  devices: make iteration device classes compatible with Python2
  tools/qvm-device: make PEP8 happy
  tests/devices: add test for handling listing device classes
  tests/devices: make PEP8 happy
  devices: handle listing of available device classes
  devices: make PEP8 happy
This commit is contained in:
Marek Marczykowski-Górecki 2019-08-08 14:13:38 +02:00
commit 489efce9cb
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
3 changed files with 170 additions and 124 deletions

View File

@ -20,7 +20,7 @@
# 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/>.
'''API for various types of devices.
"""API for various types of devices.
Main concept is that some domain main
expose (potentially multiple) devices, which can be attached to other domains.
@ -29,13 +29,14 @@ class is implemented by an extension.
Devices are identified by pair of (backend domain, `ident`), where `ident` is
:py:class:`str`.
'''
"""
class DeviceAssignment(object): # pylint: disable=too-few-public-methods
''' Maps a device to a frontend_domain. '''
""" Maps a device to a frontend_domain. """
def __init__(self, backend_domain, ident, options=None,
persistent=False, frontend_domain=None, devclass=None):
def __init__(self, backend_domain, ident, options=None, persistent=False,
frontend_domain=None, devclass=None):
self.backend_domain = backend_domain
self.ident = ident
self.devclass = devclass
@ -54,10 +55,10 @@ class DeviceAssignment(object): # pylint: disable=too-few-public-methods
return NotImplemented
return self.backend_domain == other.backend_domain \
and self.ident == other.ident
and self.ident == other.ident
def clone(self):
'''Clone object instance'''
"""Clone object instance"""
return self.__class__(
self.backend_domain,
self.ident,
@ -69,12 +70,13 @@ class DeviceAssignment(object): # pylint: disable=too-few-public-methods
@property
def device(self):
'''Get DeviceInfo object corresponding to this DeviceAssignment'''
"""Get DeviceInfo object corresponding to this DeviceAssignment"""
return self.backend_domain.devices[self.devclass][self.ident]
class DeviceInfo(object):
''' Holds all information about a device '''
""" Holds all information about a device """
# pylint: disable=too-few-public-methods
def __init__(self, backend_domain, devclass, ident, description=None,
**kwargs):
@ -94,9 +96,9 @@ class DeviceInfo(object):
def __eq__(self, other):
try:
return (
self.devclass == other.devclass and
self.backend_domain == other.backend_domain and
self.ident == other.ident
self.devclass == other.devclass and
self.backend_domain == other.backend_domain and
self.ident == other.ident
)
except AttributeError:
return False
@ -107,35 +109,36 @@ class DeviceInfo(object):
class UnknownDevice(DeviceInfo):
# pylint: disable=too-few-public-methods
'''Unknown device - for example exposed by domain not running currently'''
"""Unknown device - for example exposed by domain not running currently"""
def __init__(self, backend_domain, devclass, ident, description=None,
**kwargs):
**kwargs):
if description is None:
description = "Unknown device"
super(UnknownDevice, self).__init__(backend_domain, devclass, ident,
description, **kwargs)
description, **kwargs)
class DeviceCollection(object):
'''Bag for devices.
"""Bag for devices.
Used as default value for :py:meth:`DeviceManager.__missing__` factory.
:param vm: VM for which we manage devices
:param class_: device class
'''
"""
def __init__(self, vm, class_):
self._vm = vm
self._class = class_
self._dev_cache = {}
def attach(self, device_assignment):
'''Attach (add) device to domain.
"""Attach (add) device to domain.
:param DeviceAssignment device_assignment: device object
'''
"""
if not device_assignment.frontend_domain:
device_assignment.frontend_domain = self._vm
@ -150,20 +153,21 @@ class DeviceCollection(object):
options = device_assignment.options.copy()
if device_assignment.persistent:
options['persistent'] = 'True'
options_str = ' '.join('{}={}'.format(opt,
val) for opt, val in sorted(options.items()))
options_str = ' '.join('{}={}'.format(opt, val)
for opt, val in sorted(options.items()))
self._vm.qubesd_call(None,
'admin.vm.device.{}.Attach'.format(self._class),
'{!s}+{!s}'.format(device_assignment.backend_domain,
device_assignment.ident),
options_str.encode('utf-8'))
'admin.vm.device.{}.Attach'.format(self._class),
'{!s}+{!s}'.format(
device_assignment.backend_domain,
device_assignment.ident),
options_str.encode('utf-8'))
def detach(self, device_assignment):
'''Detach (remove) device from domain.
"""Detach (remove) device from domain.
:param DeviceAssignment device_assignment: device to detach
(obtained from :py:meth:`assignments`)
'''
"""
if not device_assignment.frontend_domain:
device_assignment.frontend_domain = self._vm
else:
@ -175,12 +179,13 @@ class DeviceCollection(object):
assert device_assignment.devclass == self._class
self._vm.qubesd_call(None,
'admin.vm.device.{}.Detach'.format(self._class),
'{!s}+{!s}'.format(device_assignment.backend_domain,
device_assignment.ident))
'admin.vm.device.{}.Detach'.format(self._class),
'{!s}+{!s}'.format(
device_assignment.backend_domain,
device_assignment.ident))
def assignments(self, persistent=None):
'''List assignments for devices which are (or may be) attached to the
"""List assignments for devices which are (or may be) attached to the
vm.
Devices may be attached persistently (so they are included in
@ -189,73 +194,79 @@ class DeviceCollection(object):
:param bool persistent: only include devices which are or are not
attached persistently.
'''
"""
assignments_str = self._vm.qubesd_call(None,
'admin.vm.device.{}.List'.format(self._class)).decode()
'admin.vm.device.{}.List'.format(
self._class)).decode()
for assignment_str in assignments_str.splitlines():
device, _, options_all = assignment_str.partition(' ')
backend_domain, ident = device.split('+', 1)
options = dict(opt_single.split('=', 1)
for opt_single in options_all.split(' ') if opt_single)
for opt_single in options_all.split(' ') if
opt_single)
dev_persistent = (options.pop('persistent', False) in
['True', 'yes', True])
['True', 'yes', True])
if persistent is not None and dev_persistent != persistent:
continue
backend_domain = self._vm.app.domains[backend_domain]
yield DeviceAssignment(backend_domain, ident, options,
persistent=dev_persistent, frontend_domain=self._vm,
devclass=self._class)
persistent=dev_persistent,
frontend_domain=self._vm,
devclass=self._class)
def attached(self):
'''List devices which are (or may be) attached to this vm '''
"""List devices which are (or may be) attached to this vm """
for assignment in self.assignments():
yield assignment.device
def persistent(self):
''' Devices persistently attached and safe to access before libvirt
""" Devices persistently attached and safe to access before libvirt
bootstrap.
'''
"""
for assignment in self.assignments(True):
yield assignment.device
def available(self):
'''List devices exposed by this vm'''
devices_str = self._vm.qubesd_call(None,
'admin.vm.device.{}.Available'.format(self._class)).decode()
"""List devices exposed by this vm"""
devices_str = \
self._vm.qubesd_call(None,
'admin.vm.device.{}.Available'.format(
self._class)).decode()
for dev_str in devices_str.splitlines():
ident, _, info = dev_str.partition(' ')
# description is special that it can contain spaces
info, _, description = info.partition('description=')
info_dict = dict(info_single.split('=', 1)
for info_single in info.split(' ') if info_single)
for info_single in info.split(' ') if info_single)
yield DeviceInfo(self._vm, self._class, ident,
description=description,
**info_dict)
description=description,
**info_dict)
def update_persistent(self, device, persistent):
'''Update `persistent` flag of already attached device.
"""Update `persistent` flag of already attached device.
:param DeviceInfo device: device for which change persistent flag
:param bool persistent: new persistent flag
'''
"""
self._vm.qubesd_call(None,
'admin.vm.device.{}.Set.persistent'.format(self._class),
'{!s}+{!s}'.format(device.backend_domain,
device.ident),
str(persistent).encode('utf-8'))
'admin.vm.device.{}.Set.persistent'.format(
self._class),
'{!s}+{!s}'.format(device.backend_domain,
device.ident),
str(persistent).encode('utf-8'))
__iter__ = available
def clear_cache(self):
'''Clear cache of available devices'''
"""Clear cache of available devices"""
self._dev_cache.clear()
def __getitem__(self, item):
'''Get device object with given ident.
"""Get device object with given ident.
:returns: py:class:`DeviceInfo`
@ -263,7 +274,7 @@ class DeviceCollection(object):
so return UnknownDevice object. Also do the same for non-existing
devices - otherwise it will be impossible to detach already
disconnected device.
'''
"""
# fist, check if we have cached device info
if item in self._dev_cache:
return self._dev_cache[item]
@ -277,12 +288,11 @@ class DeviceCollection(object):
return UnknownDevice(self._vm, self._class, item)
class DeviceManager(dict):
'''Device manager that hold all devices by their classess.
"""Device manager that hold all devices by their classes.
:param vm: VM for which we manage devices
'''
"""
def __init__(self, vm):
super(DeviceManager, self).__init__()
@ -291,3 +301,19 @@ class DeviceManager(dict):
def __missing__(self, key):
self[key] = DeviceCollection(self._vm, key)
return self[key]
def __iter__(self):
return iter(self._get_device_classes())
def keys(self):
return self._get_device_classes()
def _get_device_classes(self):
"""Function used to call Qubesd in order to obtain
the device classes list
"""
device_classes = \
self._vm.app.qubesd_call('dom0', 'admin.deviceclass.List').decode()
device_classes = sorted(device_classes.splitlines())
return device_classes

View File

@ -103,7 +103,8 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
def test_020_attach(self):
self.app.expected_calls[
('test-vm', 'admin.vm.device.test.Attach', 'test-vm2+dev1', b'')] = \
('test-vm', 'admin.vm.device.test.Attach', 'test-vm2+dev1',
b'')] = \
b'0\0'
assign = qubesadmin.devices.DeviceAssignment(
self.app.domains['test-vm2'], 'dev1')
@ -113,7 +114,7 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
def test_021_attach_options(self):
self.app.expected_calls[
('test-vm', 'admin.vm.device.test.Attach', 'test-vm2+dev1',
b'ro=True something=value')] = b'0\0'
b'ro=True something=value')] = b'0\0'
assign = qubesadmin.devices.DeviceAssignment(
self.app.domains['test-vm2'], 'dev1')
assign.options['ro'] = True
@ -124,7 +125,7 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
def test_022_attach_persistent(self):
self.app.expected_calls[
('test-vm', 'admin.vm.device.test.Attach', 'test-vm2+dev1',
b'persistent=True')] = b'0\0'
b'persistent=True')] = b'0\0'
assign = qubesadmin.devices.DeviceAssignment(
self.app.domains['test-vm2'], 'dev1')
assign.persistent = True
@ -134,7 +135,7 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
def test_023_attach_persistent_options(self):
self.app.expected_calls[
('test-vm', 'admin.vm.device.test.Attach', 'test-vm2+dev1',
b'persistent=True ro=True')] = b'0\0'
b'persistent=True ro=True')] = b'0\0'
assign = qubesadmin.devices.DeviceAssignment(
self.app.domains['test-vm2'], 'dev1')
assign.persistent = True
@ -145,7 +146,7 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
def test_030_detach(self):
self.app.expected_calls[
('test-vm', 'admin.vm.device.test.Detach', 'test-vm2+dev1',
None)] = b'0\0'
None)] = b'0\0'
assign = qubesadmin.devices.DeviceAssignment(
self.app.domains['test-vm2'], 'dev1')
self.vm.devices['test'].detach(assign)
@ -166,25 +167,25 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
self.assertEqual(len(assigns), 2)
self.assertIsInstance(assigns[0], qubesadmin.devices.DeviceAssignment)
self.assertEqual(assigns[0].backend_domain,
self.app.domains['test-vm2'])
self.app.domains['test-vm2'])
self.assertEqual(assigns[0].ident, 'dev1')
self.assertEqual(assigns[0].frontend_domain,
self.app.domains['test-vm'])
self.app.domains['test-vm'])
self.assertEqual(assigns[0].options, {})
self.assertEqual(assigns[0].devclass, 'test')
self.assertEqual(assigns[0].device,
self.app.domains['test-vm2'].devices['test']['dev1'])
self.app.domains['test-vm2'].devices['test']['dev1'])
self.assertIsInstance(assigns[1], qubesadmin.devices.DeviceAssignment)
self.assertEqual(assigns[1].backend_domain,
self.app.domains['test-vm3'])
self.app.domains['test-vm3'])
self.assertEqual(assigns[1].ident, 'dev2')
self.assertEqual(assigns[1].frontend_domain,
self.app.domains['test-vm'])
self.app.domains['test-vm'])
self.assertEqual(assigns[1].options, {})
self.assertEqual(assigns[1].devclass, 'test')
self.assertEqual(assigns[1].device,
self.app.domains['test-vm3'].devices['test']['dev2'])
self.app.domains['test-vm3'].devices['test']['dev2'])
self.assertAllCalled()
@ -197,20 +198,20 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
self.assertEqual(len(assigns), 2)
self.assertIsInstance(assigns[0], qubesadmin.devices.DeviceAssignment)
self.assertEqual(assigns[0].backend_domain,
self.app.domains['test-vm2'])
self.app.domains['test-vm2'])
self.assertEqual(assigns[0].ident, 'dev1')
self.assertEqual(assigns[0].frontend_domain,
self.app.domains['test-vm'])
self.app.domains['test-vm'])
self.assertEqual(assigns[0].options, {'ro': 'True'})
self.assertEqual(assigns[0].persistent, False)
self.assertEqual(assigns[0].devclass, 'test')
self.assertIsInstance(assigns[1], qubesadmin.devices.DeviceAssignment)
self.assertEqual(assigns[1].backend_domain,
self.app.domains['test-vm3'])
self.app.domains['test-vm3'])
self.assertEqual(assigns[1].ident, 'dev2')
self.assertEqual(assigns[1].frontend_domain,
self.app.domains['test-vm'])
self.app.domains['test-vm'])
self.assertEqual(assigns[1].options, {'ro': 'False'})
self.assertEqual(assigns[1].persistent, True)
self.assertEqual(assigns[1].devclass, 'test')
@ -226,10 +227,10 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
self.assertEqual(len(assigns), 1)
self.assertIsInstance(assigns[0], qubesadmin.devices.DeviceAssignment)
self.assertEqual(assigns[0].backend_domain,
self.app.domains['test-vm3'])
self.app.domains['test-vm3'])
self.assertEqual(assigns[0].ident, 'dev2')
self.assertEqual(assigns[0].frontend_domain,
self.app.domains['test-vm'])
self.app.domains['test-vm'])
self.assertEqual(assigns[0].options, {})
self.assertEqual(assigns[0].persistent, True)
self.assertEqual(assigns[0].devclass, 'test')
@ -244,10 +245,10 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
self.assertEqual(len(assigns), 1)
self.assertIsInstance(assigns[0], qubesadmin.devices.DeviceAssignment)
self.assertEqual(assigns[0].backend_domain,
self.app.domains['test-vm2'])
self.app.domains['test-vm2'])
self.assertEqual(assigns[0].ident, 'dev1')
self.assertEqual(assigns[0].frontend_domain,
self.app.domains['test-vm'])
self.app.domains['test-vm'])
self.assertEqual(assigns[0].options, {})
self.assertEqual(assigns[0].persistent, False)
self.assertAllCalled()
@ -291,7 +292,7 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
def test_070_update_persistent(self):
self.app.expected_calls[
('test-vm', 'admin.vm.device.test.Set.persistent', 'test-vm2+dev1',
b'True')] = b'0\0'
b'True')] = b'0\0'
dev = qubesadmin.devices.DeviceInfo(
self.app.domains['test-vm2'], 'test', 'dev1')
self.vm.devices['test'].update_persistent(dev, True)
@ -300,8 +301,18 @@ class TC_00_DeviceCollection(qubesadmin.tests.QubesTestCase):
def test_071_update_persistent_false(self):
self.app.expected_calls[
('test-vm', 'admin.vm.device.test.Set.persistent', 'test-vm2+dev1',
b'False')] = b'0\0'
b'False')] = b'0\0'
dev = qubesadmin.devices.DeviceInfo(
self.app.domains['test-vm2'], 'test', 'dev1')
self.vm.devices['test'].update_persistent(dev, False)
self.assertAllCalled()
def test_072_list(self):
self.app.expected_calls[
('dom0', 'admin.deviceclass.List', None, None)] = \
b'0\x00block\nmic\nusb\n'
seen = set()
for devclass in self.app.domains['test-vm'].devices:
self.assertNotIn(devclass, seen)
seen.add(devclass)
self.assertEqual(seen, {'block', 'mic', 'usb'})

View File

@ -21,7 +21,7 @@
# 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'''
"""Qubes volume and block device managment"""
import argparse
import os
@ -34,7 +34,7 @@ import qubesadmin.devices
def prepare_table(dev_list):
''' Converts a list of :py:class:`qubes.devices.DeviceInfo` objects to a
""" Converts a list of :py:class:`qubes.devices.DeviceInfo` objects to a
list of tupples for the :py:func:`qubes.tools.print_table`.
If :program:`qvm-devices` is running in a TTY, it will ommit duplicate
@ -43,7 +43,7 @@ def prepare_table(dev_list):
:param iterable dev_list: List of :py:class:`qubes.devices.DeviceInfo`
objects.
:returns: list of tupples
'''
"""
output = []
header = []
if sys.stdout.isatty():
@ -60,7 +60,8 @@ def prepare_table(dev_list):
class Line(object):
'''Helper class to hold single device info for listing'''
"""Helper class to hold single device info for listing"""
# pylint: disable=too-few-public-methods
def __init__(self, device: qubesadmin.devices.DeviceInfo, attached_to=None):
self.ident = "{!s}:{!s}".format(device.backend_domain, device.ident)
@ -70,13 +71,13 @@ class Line(object):
@property
def assignments(self):
'''list of frontends the device is assigned to'''
"""list of frontends the device is assigned to"""
return ', '.join(self.frontends)
def list_devices(args):
''' Called by the parser to execute the qubes-devices list
subcommand. '''
""" Called by the parser to execute the qubes-devices list
subcommand. """
app = args.app
devices = set()
@ -105,7 +106,8 @@ def list_devices(args):
if assignment.options:
result[dev].frontends.append('{!s} ({})'.format(
domain, ', '.join('{}={}'.format(key, value)
for key, value in assignment.options.items())))
for key, value in
assignment.options.items())))
else:
result[dev].frontends.append(str(domain))
@ -113,9 +115,9 @@ def list_devices(args):
def attach_device(args):
''' Called by the parser to execute the :program:`qvm-devices attach`
""" Called by the parser to execute the :program:`qvm-devices attach`
subcommand.
'''
"""
device_assignment = args.device_assignment
vm = args.domains[0]
options = dict(opt.split('=', 1) for opt in args.option or [])
@ -127,9 +129,9 @@ def attach_device(args):
def detach_device(args):
''' Called by the parser to execute the :program:`qvm-devices detach`
""" Called by the parser to execute the :program:`qvm-devices detach`
subcommand.
'''
"""
vm = args.domains[0]
if args.device_assignment:
vm.devices[args.devclass].detach(args.device_assignment)
@ -139,7 +141,7 @@ def detach_device(args):
def init_list_parser(sub_parsers):
''' Configures the parser for the :program:`qvm-devices list` subcommand '''
""" Configures the parser for the :program:`qvm-devices list` subcommand """
# pylint: disable=protected-access
list_parser = sub_parsers.add_parser('list', aliases=('ls', 'l'),
help='list devices')
@ -152,10 +154,10 @@ def init_list_parser(sub_parsers):
class DeviceAction(qubesadmin.tools.QubesAction):
''' Action for argument parser that gets the
""" Action for argument parser that gets the
:py:class:``qubesadmin.device.DeviceAssignment`` from a
BACKEND:DEVICE_ID string.
''' # pylint: disable=too-few-public-methods
""" # pylint: disable=too-few-public-methods
def __init__(self, help='A backend & device id combination',
required=True, allow_unknown=False, **kwargs):
@ -165,7 +167,7 @@ class DeviceAction(qubesadmin.tools.QubesAction):
**kwargs)
def __call__(self, parser, namespace, values, option_string=None):
''' Set ``namespace.device_assignment`` to ``values`` '''
""" Set ``namespace.device_assignment`` to ``values`` """
setattr(namespace, self.dest, values)
def parse_qubes_app(self, parser, namespace):
@ -177,6 +179,7 @@ class DeviceAction(qubesadmin.tools.QubesAction):
try:
vmname, device_id = backend_device_id.split(':', 1)
vm = None
try:
vm = app.domains[vmname]
except KeyError:
@ -184,36 +187,37 @@ class DeviceAction(qubesadmin.tools.QubesAction):
try:
dev = vm.devices[devclass][device_id]
if not self.allow_unknown and isinstance(dev,
qubesadmin.devices.UnknownDevice):
if not self.allow_unknown and \
isinstance(dev, qubesadmin.devices.UnknownDevice):
raise KeyError(device_id)
except KeyError:
parser.error_runtime(
"backend vm {!r} doesn't expose device {!r}"
.format(vmname, device_id))
device_assignment = qubesadmin.devices.DeviceAssignment(vm,
device_id)
"backend vm {!r} doesn't expose device {!r}".format(
vmname, device_id))
device_assignment = qubesadmin.devices.DeviceAssignment(
vm, device_id)
setattr(namespace, self.dest, device_assignment)
except ValueError:
parser.error('expected a backend vm & device id combination ' \
'like foo:bar got %s' % backend_device_id)
parser.error(
'expected a backend vm & device id combination like foo:bar '
'got %s' % backend_device_id)
def get_parser(device_class=None):
'''Create :py:class:`argparse.ArgumentParser` suitable for
"""Create :py:class:`argparse.ArgumentParser` suitable for
:program:`qvm-block`.
'''
"""
parser = qubesadmin.tools.QubesArgumentParser(description=__doc__,
want_app=True)
want_app=True)
parser.register('action', 'parsers',
qubesadmin.tools.AliasedSubParsersAction)
qubesadmin.tools.AliasedSubParsersAction)
if device_class:
parser.add_argument('devclass', const=device_class,
action='store_const',
help=argparse.SUPPRESS)
action='store_const',
help=argparse.SUPPRESS)
else:
parser.add_argument('devclass', metavar='DEVICE_CLASS', action='store',
help="Device class to manage ('pci', 'usb', etc)")
help="Device class to manage ('pci', 'usb', etc)")
# default action
parser.set_defaults(func=list_devices)
@ -229,27 +233,32 @@ def get_parser(device_class=None):
"detach", help="Detach device from domain", aliases=('d', 'dt'))
attach_parser.add_argument('VMNAME', nargs=1,
action=qubesadmin.tools.VmNameAction)
action=qubesadmin.tools.VmNameAction)
detach_parser.add_argument('VMNAME', nargs=1,
action=qubesadmin.tools.VmNameAction)
action=qubesadmin.tools.VmNameAction)
attach_parser.add_argument(metavar='BACKEND:DEVICE_ID',
dest='device_assignment',
action=DeviceAction)
dest='device_assignment',
action=DeviceAction)
detach_parser.add_argument(metavar='BACKEND:DEVICE_ID',
dest='device_assignment', nargs=argparse.OPTIONAL,
action=DeviceAction, allow_unknown=True)
dest='device_assignment',
nargs=argparse.OPTIONAL,
action=DeviceAction, allow_unknown=True)
attach_parser.add_argument('--option', '-o', action='append',
help="Set option for the device in opt=value form (can be specified "
"multiple times), see man qvm-device for details")
help="Set option for the device in opt=value "
"form (can be specified "
"multiple times), see man qvm-device for "
"details")
attach_parser.add_argument('--ro', action='store_true', default=False,
help="Attach device read-only (alias for read-only=yes option, "
"takes precedence)")
help="Attach device read-only (alias for "
"read-only=yes option, "
"takes precedence)")
attach_parser.add_argument('--persistent', '-p', action='store_true',
default=False,
help="Attach device persistently (so it will be automatically "
"attached at qube startup)")
default=False,
help="Attach device persistently (so it will "
"be automatically "
"attached at qube startup)")
attach_parser.set_defaults(func=attach_device)
detach_parser.set_defaults(func=detach_device)
@ -258,7 +267,7 @@ def get_parser(device_class=None):
def main(args=None, app=None):
'''Main routine of :program:`qvm-block`.'''
"""Main routine of :program:`qvm-block`."""
basename = os.path.basename(sys.argv[0])
devclass = None
if basename.startswith('qvm-') and basename != 'qvm-device':