From 49f71dbb11b2518c988f92cc6ae5a962f9239b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Wed, 29 Sep 2021 02:43:25 +0200 Subject: [PATCH 1/4] storage: fix formatting - use double quotes for docstrings - fix line breaks and empty lines No functional change --- qubesadmin/storage.py | 102 +++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/qubesadmin/storage.py b/qubesadmin/storage.py index d3ca1d67..91632667 100644 --- a/qubesadmin/storage.py +++ b/qubesadmin/storage.py @@ -18,13 +18,14 @@ # You should have received a copy of the GNU Lesser General Public License along # with this program; if not, see . -'''Storage subsystem.''' +"""Storage subsystem.""" import qubesadmin.exc + class Volume(object): - '''Storage volume.''' + """Storage volume.""" def __init__(self, app, pool=None, vid=None, vm=None, vm_name=None): - '''Construct a Volume object. + """Construct a Volume object. Volume may be identified using pool+vid, or vm+vm_name. Either of those argument pairs must be given. @@ -34,7 +35,7 @@ def __init__(self, app, pool=None, vid=None, vm=None, vm_name=None): :param str vid: volume id (within pool) :param str vm: owner VM name :param str vm_name: name within owning VM (like 'private', 'root' etc) - ''' + """ self.app = app if pool is None and vm is None: raise ValueError('Either pool or vm must be given') @@ -49,13 +50,13 @@ def __init__(self, app, pool=None, vid=None, vm=None, vm_name=None): self._info = None def _qubesd_call(self, func_name, payload=None, payload_stream=None): - '''Make a call to qubesd regarding this volume + """Make a call to qubesd regarding this volume :param str func_name: API function name, like `Info` or `Resize` :param bytes payload: Payload to send. :param file payload_stream: Stream to pipe payload from. Only one of `payload` and `payload_stream` can be used. - ''' + """ if self._vm is not None: method = 'admin.vm.volume.' + func_name dest = self._vm @@ -72,16 +73,17 @@ def _qubesd_call(self, func_name, payload=None, payload_stream=None): payload = self._vid.encode('ascii') + b' ' + payload else: payload = self._vid.encode('ascii') - return self.app.qubesd_call(dest, method, arg, payload=payload, + return self.app.qubesd_call( + dest, method, arg, payload=payload, payload_stream=payload_stream) def _fetch_info(self, force=True): - '''Fetch volume properties + """Fetch volume properties Populate self._info dict :param bool force: refresh self._info, even if already populated. - ''' + """ if not force and self._info is not None: return info = self._qubesd_call('Info') @@ -102,15 +104,14 @@ def __lt__(self, other): return (self._pool, self._vid) < (other._pool, other._vid) return NotImplemented - @property def name(self): - '''per-VM volume name, if available''' + """per-VM volume name, if available""" return self._vm_name @property def pool(self): - '''Storage volume pool name.''' + """Storage volume pool name.""" if self._pool is not None: return self._pool try: @@ -121,7 +122,7 @@ def pool(self): @property def vid(self): - '''Storage volume id, unique within given pool.''' + """Storage volume id, unique within given pool.""" if self._vid is not None: return self._vid try: @@ -132,7 +133,7 @@ def vid(self): @property def size(self): - '''Size of volume, in bytes.''' + """Size of volume, in bytes.""" try: self._fetch_info() except qubesadmin.exc.QubesDaemonAccessError: @@ -141,7 +142,7 @@ def size(self): @property def usage(self): - '''Used volume space, in bytes.''' + """Used volume space, in bytes.""" try: self._fetch_info() except qubesadmin.exc.QubesDaemonAccessError: @@ -150,7 +151,7 @@ def usage(self): @property def rw(self): - '''True if volume is read-write.''' + """True if volume is read-write.""" try: self._fetch_info() except qubesadmin.exc.QubesDaemonAccessError: @@ -159,13 +160,13 @@ def rw(self): @rw.setter def rw(self, value): - '''Set rw property''' + """Set rw property""" self._qubesd_call('Set.rw', str(value).encode('ascii')) self._info = None @property def snap_on_start(self): - '''Create a snapshot from source on VM start.''' + """Create a snapshot from source on VM start.""" try: self._fetch_info() except qubesadmin.exc.QubesDaemonAccessError: @@ -174,7 +175,7 @@ def snap_on_start(self): @property def save_on_stop(self): - '''Commit changes to original volume on VM stop.''' + """Commit changes to original volume on VM stop.""" try: self._fetch_info() except qubesadmin.exc.QubesDaemonAccessError: @@ -183,10 +184,10 @@ def save_on_stop(self): @property def source(self): - '''Volume ID of source volume (for :py:attr:`snap_on_start`). + """Volume ID of source volume (for :py:attr:`snap_on_start`). If None, this volume itself will be used. - ''' + """ try: self._fetch_info() except qubesadmin.exc.QubesDaemonAccessError: @@ -197,7 +198,7 @@ def source(self): @property def revisions_to_keep(self): - '''Number of revisions to keep around''' + """Number of revisions to keep around""" try: self._fetch_info() except qubesadmin.exc.QubesDaemonAccessError: @@ -206,14 +207,14 @@ def revisions_to_keep(self): @revisions_to_keep.setter def revisions_to_keep(self, value): - '''Set revisions_to_keep property''' + """Set revisions_to_keep property""" self._qubesd_call('Set.revisions_to_keep', str(value).encode('ascii')) self._info = None def is_outdated(self): - '''Returns `True` if this snapshot of a source volume (for + """Returns `True` if this snapshot of a source volume (for `snap_on_start`=True) is outdated. - ''' + """ try: self._fetch_info() except qubesadmin.exc.QubesDaemonAccessError: @@ -221,65 +222,65 @@ def is_outdated(self): return self._info.get('is_outdated', False) == 'True' def resize(self, size): - '''Resize volume. + """Resize volume. Currently only extending is supported. :param int size: new size in bytes. - ''' + """ self._qubesd_call('Resize', str(size).encode('ascii')) @property def revisions(self): - ''' Returns iterable containing revision identifiers''' + """ Returns iterable containing revision identifiers""" revisions = self._qubesd_call('ListSnapshots') return revisions.decode('ascii').splitlines() def revert(self, revision): - ''' Revert volume to previous revision + """ Revert volume to previous revision :param str revision: Revision identifier to revert to - ''' + """ if not isinstance(revision, str): raise TypeError('revision must be a str') self._qubesd_call('Revert', revision.encode('ascii')) def import_data(self, stream): - ''' Import volume data from a given file-like object. + """ Import volume data from a given file-like object. This function overrides existing volume content. :param stream: file-like object, must support fileno() - ''' + """ self._qubesd_call('Import', payload_stream=stream) def import_data_with_size(self, stream, size): - ''' Import volume data from a given file-like object, informing qubesd + """ Import volume data from a given file-like object, informing qubesd that data has a specific size. This function overrides existing volume content. :param stream: file-like object, must support fileno() :param size: size of data in bytes - ''' + """ size_line = str(size) + '\n' self._qubesd_call( 'ImportWithSize', payload=size_line.encode(), payload_stream=stream) def clear_data(self): - ''' Clear existing volume content. ''' + """ Clear existing volume content. """ self._qubesd_call('Clear') def clone(self, source): - ''' Clone data from sane volume of another VM. + """ Clone data from sane volume of another VM. This function override existing volume content. This operation is implemented for VM volumes - those in vm.volumes collection (not pool.volumes). :param source: source volume object - ''' + """ # pylint: disable=protected-access @@ -290,15 +291,15 @@ def clone(self, source): class Pool(object): - ''' A Pool is used to manage different kind of volumes (File + """ A Pool is used to manage different kind of volumes (File based/LVM/Btrfs/...). - ''' + """ def __init__(self, app, name=None): - ''' Initialize storage pool wrapper + """ Initialize storage pool wrapper :param app: Qubes() object :param name: name of the pool - ''' + """ self.app = app self.name = name self._config = None @@ -320,7 +321,7 @@ def __lt__(self, other): @property def usage_details(self): - ''' Storage pool usage details (current - not cached) ''' + """ Storage pool usage details (current - not cached) """ try: pool_usage_data = self.app.qubesd_call( 'dom0', 'admin.pool.UsageDetails', self.name, None) @@ -338,7 +339,7 @@ def _int_split(text): # pylint: disable=missing-docstring @property def config(self): - ''' Storage pool config ''' + """ Storage pool config """ if self._config is None: try: pool_info_data = self.app.qubesd_call( @@ -354,7 +355,7 @@ def config(self): @property def size(self): - ''' Storage pool size, in bytes''' + """ Storage pool size, in bytes""" try: return int(self.usage_details['data_size']) except KeyError: @@ -363,7 +364,7 @@ def size(self): @property def usage(self): - ''' Space used in the pool, in bytes ''' + """ Space used in the pool, in bytes """ try: return int(self.usage_details['data_usage']) except KeyError: @@ -372,18 +373,19 @@ def usage(self): @property def driver(self): - ''' Storage pool driver ''' + """ Storage pool driver """ return self.config['driver'] @property def revisions_to_keep(self): - '''Number of revisions to keep around''' + """Number of revisions to keep around""" return int(self.config['revisions_to_keep']) @revisions_to_keep.setter def revisions_to_keep(self, value): - '''Set revisions_to_keep property''' - self.app.qubesd_call('dom0', + """Set revisions_to_keep property""" + self.app.qubesd_call( + 'dom0', 'admin.pool.Set.revisions_to_keep', self.name, str(value).encode('ascii')) @@ -391,7 +393,7 @@ def revisions_to_keep(self, value): @property def volumes(self): - ''' Volumes managed by this pool ''' + """ Volumes managed by this pool """ try: volumes_data = self.app.qubesd_call( 'dom0', 'admin.pool.volume.List', self.name, None) From dfc909bef499339a8ab79594794a993be465bc95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Wed, 29 Sep 2021 02:48:37 +0200 Subject: [PATCH 2/4] tools/qvm-volume: improve formatting - use double quotes for docstrings - adjust empty lines line breaks --- qubesadmin/tools/qvm_volume.py | 68 ++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/qubesadmin/tools/qvm_volume.py b/qubesadmin/tools/qvm_volume.py index 5cfd6c96..e7efff02 100644 --- a/qubesadmin/tools/qvm_volume.py +++ b/qubesadmin/tools/qvm_volume.py @@ -20,7 +20,7 @@ # with this program; if not, see . -'''Qubes volume management''' +"""Qubes volume management""" from __future__ import print_function @@ -37,7 +37,7 @@ def prepare_table(vd_list, full=False): - ''' Converts a list of :py:class:`VolumeData` objects to a list of tupples + """ 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 @@ -47,7 +47,7 @@ def prepare_table(vd_list, full=False): :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')] @@ -69,9 +69,9 @@ def prepare_table(vd_list, full=False): class VolumeData(object): - ''' Wrapper object around :py:class:`qubes.storage.Volume`, mainly to track + """ 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 @@ -88,10 +88,12 @@ def __lt__(self, other): def __str__(self): return "{!s}:{!s}".format(self.pool, self.vid) + def info_volume(args): - ''' Show info about selected volume ''' + """ Show info about selected volume """ volume = args.volume - info_items = ('pool', 'vid', 'rw', 'source', 'save_on_stop', + info_items = ( + 'pool', 'vid', 'rw', 'source', 'save_on_stop', 'snap_on_start', 'size', 'usage', 'revisions_to_keep') if args.property: if args.property == 'revisions': @@ -125,16 +127,18 @@ def info_volume(args): else: print('Available revisions (for revert): none') + def config_volume(args): - ''' Change property of selected volume ''' + """ Change property of selected volume """ volume = args.volume if not args.property in ('rw', 'revisions_to_keep'): raise qubesadmin.exc.QubesNoSuchPropertyError( 'Invalid property: {}'.format(args.property)) setattr(volume, args.property, args.value) + def import_volume(args): - ''' Import a file into volume ''' + """ Import a file into volume """ volume = args.volume input_path = args.input_path @@ -161,8 +165,9 @@ def import_volume(args): if input_path != '-': input_file.close() + def list_volumes(args): - ''' Called by the parser to execute the qvm-volume list subcommand. ''' + """ Called by the parser to execute the qvm-volume list subcommand. """ app = args.app if hasattr(args, 'domains') and args.domains: @@ -208,7 +213,7 @@ def list_volumes(args): def revert_volume(args): - ''' Revert volume to previous state ''' + """ Revert volume to previous state """ volume = args.volume if args.revision: revision = args.revision @@ -223,9 +228,9 @@ def revert_volume(args): def extend_volumes(args): - ''' Called by the parser to execute the :program:`qvm-block extend` + """ Called by the parser to execute the :program:`qvm-block extend` subcommand - ''' + """ volume = args.volume size = qubesadmin.utils.parse_size(args.size) if not args.force and size < volume.size: @@ -239,7 +244,7 @@ def extend_volumes(args): def init_list_parser(sub_parsers): - ''' Configures the parser for the :program:`qvm-block list` subcommand ''' + """ 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') @@ -257,13 +262,14 @@ def init_list_parser(sub_parsers): def init_revert_parser(sub_parsers): - ''' Add 'revert' action related options ''' + """ 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=qubesadmin.tools.VMVolumeAction) - revert_parser.add_argument(metavar='REVISION', dest='revision', + revert_parser.add_argument( + metavar='REVISION', dest='revision', help='Optional revision to revert to; ' 'if not specified, latest one is assumed', action='store', nargs='?') @@ -271,31 +277,35 @@ def init_revert_parser(sub_parsers): def init_extend_parser(sub_parsers): - ''' Add 'extend' action related options ''' + """ Add 'extend' action related options """ extend_parser = sub_parsers.add_parser( "resize", aliases=('extend', ), help="resize volume for domain") extend_parser.add_argument(metavar='VM:VOLUME', dest='volume', action=qubesadmin.tools.VMVolumeAction) extend_parser.add_argument('size', help='New size in bytes') - extend_parser.add_argument('--force', '-f', action='store_true', + extend_parser.add_argument( + '--force', '-f', action='store_true', help='Force operation, even if new size is smaller than the current ' 'one') extend_parser.set_defaults(func=extend_volumes) + def init_info_parser(sub_parsers): - ''' Add 'info' action related options ''' + """ Add 'info' action related options """ info_parser = sub_parsers.add_parser( 'info', aliases=('i',), help='info about volume') info_parser.add_argument(metavar='VM:VOLUME', dest='volume', action=qubesadmin.tools.VMVolumeAction) - info_parser.add_argument(dest='property', action='store', + info_parser.add_argument( + dest='property', action='store', nargs=argparse.OPTIONAL, help='Show only this property instead of all of them; use ' '\'revisions\' to list available revisions') info_parser.set_defaults(func=info_volume) + def init_config_parser(sub_parsers): - ''' Add 'info' action related options ''' + """ Add 'info' action related options """ info_parser = sub_parsers.add_parser( 'config', aliases=('c', 'set', 's'), help='set config option for a volume') @@ -305,12 +315,13 @@ def init_config_parser(sub_parsers): info_parser.add_argument(dest='value', action='store') info_parser.set_defaults(func=config_volume) + def init_import_parser(sub_parsers): - ''' Add 'import' action related options ''' + """ Add 'import' action related options """ import_parser = sub_parsers.add_parser( 'import', help='import volume data') import_parser.add_argument(metavar='VM:VOLUME', dest='volume', - action=qubesadmin.tools.VMVolumeAction) + action=qubesadmin.tools.VMVolumeAction) import_parser.add_argument('input_path', metavar='PATH', help='File path to import, use \'-\' for standard input') import_parser.add_argument('--size', action='store', type=int, @@ -319,12 +330,14 @@ def init_import_parser(sub_parsers): help='Do not resize volume before importing data') import_parser.set_defaults(func=import_volume) + def get_parser(): - '''Create :py:class:`argparse.ArgumentParser` suitable for + """Create :py:class:`argparse.ArgumentParser` suitable for :program:`qvm-volume`. - ''' + """ parser = qubesadmin.tools.QubesArgumentParser(description=__doc__) - parser.register('action', 'parsers', + parser.register( + 'action', 'parsers', qubesadmin.tools.AliasedSubParsersAction) sub_parsers = parser.add_subparsers( title='commands', @@ -343,7 +356,7 @@ def get_parser(): def main(args=None, app=None): - '''Main routine of :program:`qvm-volume`.''' + """Main routine of :program:`qvm-volume`.""" parser = get_parser() try: args = parser.parse_args(args, app=app) @@ -354,5 +367,6 @@ def main(args=None, app=None): return 0 + if __name__ == '__main__': sys.exit(main()) From 4b9fe24d0a75ff591e68a7e82384d0456a31815d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Wed, 29 Sep 2021 02:52:44 +0200 Subject: [PATCH 3/4] storage: add Volume.ephemeral property QubesOS/qubes-issues#904 --- qubesadmin/storage.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/qubesadmin/storage.py b/qubesadmin/storage.py index 91632667..4b94d069 100644 --- a/qubesadmin/storage.py +++ b/qubesadmin/storage.py @@ -164,6 +164,21 @@ def rw(self, value): self._qubesd_call('Set.rw', str(value).encode('ascii')) self._info = None + @property + def ephemeral(self): + """True if volume is read-write.""" + try: + self._fetch_info() + except qubesadmin.exc.QubesDaemonAccessError: + raise qubesadmin.exc.QubesPropertyAccessError('ephemeral') + return self._info.get('ephemeral', 'False') == 'True' + + @ephemeral.setter + def ephemeral(self, value): + """Set rw property""" + self._qubesd_call('Set.ephemeral', str(value).encode('ascii')) + self._info = None + @property def snap_on_start(self): """Create a snapshot from source on VM start.""" From 39cff4ba66e1c642e36e4461ee662abc80c65af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Wed, 29 Sep 2021 02:54:25 +0200 Subject: [PATCH 4/4] tools/qvm-volume: add showing and setting 'ephemeral' property QubesOS/qubes-issues#904 --- doc/manpages/qvm-volume.rst | 6 +++ qubesadmin/tests/tools/qvm_volume.py | 65 ++++++++++++++++++++++++++++ qubesadmin/tools/qvm_volume.py | 4 +- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/doc/manpages/qvm-volume.rst b/doc/manpages/qvm-volume.rst index ad95b9af..e3dafcf2 100644 --- a/doc/manpages/qvm-volume.rst +++ b/doc/manpages/qvm-volume.rst @@ -81,6 +81,12 @@ Set property of given volume. Properties currently possible to change: should be keep. At each qube shutdown its previous state is saved in new revision, and the oldest revisions are remove so that only `revisions_to_keep` are left. Set to `0` to not leave any previous versions. + - `ephemeral` - should the volume be encrypted with en ephemeral key? This can + be enabled only on a volume with `save_on_stop=False` and `snap_on_start=True` + - which is only `volatile` volume. When set, it provides a bit more + anti-forensics protection against attacker with access to the LUKS disk key. + In majority of use cases, it only degrades performance due to additional + encryption level. aliases: c, set, s diff --git a/qubesadmin/tests/tools/qvm_volume.py b/qubesadmin/tests/tools/qvm_volume.py index feefe5d8..16afb9f2 100644 --- a/qubesadmin/tests/tools/qvm_volume.py +++ b/qubesadmin/tests/tools/qvm_volume.py @@ -360,6 +360,21 @@ def test_032_set_invalid(self): app=self.app)) self.assertAllCalled() + def test_033_set_ephemeral(self): + self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00testvm class=AppVM state=Running\n' + self.app.expected_calls[ + ('testvm', 'admin.vm.volume.List', None, None)] = \ + b'0\x00root\nprivate\nvolatile\n' + self.app.expected_calls[ + ('testvm', 'admin.vm.volume.Set.ephemeral', 'volatile', + b'True')] = b'0\x00' + self.assertEqual(0, + qubesadmin.tools.qvm_volume.main( + ['set', 'testvm:volatile', 'ephemeral', 'True'], + app=self.app)) + self.assertAllCalled() + def test_040_info(self): self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \ b'0\x00testvm class=AppVM state=Running\n' @@ -377,6 +392,7 @@ def test_040_info(self): b'save_on_stop=True\n' \ b'snap_on_start=False\n' \ b'revisions_to_keep=3\n' \ + b'ephemeral=False\n' \ b'is_outdated=False\n' self.app.expected_calls[ ('testvm', 'admin.vm.volume.ListSnapshots', 'private', None)] = \ @@ -398,6 +414,7 @@ def test_040_info(self): 'size 2147483648\n' 'usage 10000000\n' 'revisions_to_keep 3\n' + 'ephemeral False\n' 'is_outdated False\n' 'Available revisions (for revert):\n' ' 200101010000\n' @@ -422,6 +439,7 @@ def test_041_info_no_revisions(self): b'save_on_stop=False\n' \ b'snap_on_start=True\n' \ b'revisions_to_keep=0\n' \ + b'ephemeral=True\n' \ b'is_outdated=False\n' self.app.expected_calls[ ('testvm', 'admin.vm.volume.ListSnapshots', 'root', None)] = \ @@ -440,6 +458,7 @@ def test_041_info_no_revisions(self): 'size 2147483648\n' 'usage 10000000\n' 'revisions_to_keep 0\n' + 'ephemeral True\n' 'is_outdated False\n' 'Available revisions (for revert): none\n') self.assertAllCalled() @@ -490,6 +509,52 @@ def test_043_info_revisions_only(self): '200301010000\n') self.assertAllCalled() + def test_044_info_no_ephemeral(self): + self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00testvm class=AppVM state=Running\n' + self.app.expected_calls[ + ('testvm', 'admin.vm.volume.List', None, None)] = \ + b'0\x00root\nprivate\n' + self.app.expected_calls[ + ('testvm', 'admin.vm.volume.Info', 'private', None)] = \ + b'0\x00pool=lvm\n' \ + b'vid=qubes_dom0/vm-testvm-private\n' \ + b'size=2147483648\n' \ + b'usage=10000000\n' \ + b'rw=True\n' \ + b'source=\n' \ + b'save_on_stop=True\n' \ + b'snap_on_start=False\n' \ + b'revisions_to_keep=3\n' \ + b'is_outdated=False\n' + self.app.expected_calls[ + ('testvm', 'admin.vm.volume.ListSnapshots', 'private', None)] = \ + b'0\x00200101010000\n200201010000\n200301010000\n' + with qubesadmin.tests.tools.StdoutBuffer() as stdout: + self.assertEqual(0, + qubesadmin.tools.qvm_volume.main(['info', 'testvm:private'], + app=self.app)) + output = stdout.getvalue() + # travis... + output = output.replace('\nsource\n', '\nsource \n') + self.assertEqual(output, + 'pool lvm\n' + 'vid qubes_dom0/vm-testvm-private\n' + 'rw True\n' + 'source \n' + 'save_on_stop True\n' + 'snap_on_start False\n' + 'size 2147483648\n' + 'usage 10000000\n' + 'revisions_to_keep 3\n' + 'ephemeral False\n' + 'is_outdated False\n' + 'Available revisions (for revert):\n' + ' 200101010000\n' + ' 200201010000\n' + ' 200301010000\n') + self.assertAllCalled() + def test_050_import_file(self): self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \ b'0\x00testvm class=AppVM state=Running\n' diff --git a/qubesadmin/tools/qvm_volume.py b/qubesadmin/tools/qvm_volume.py index e7efff02..862866bc 100644 --- a/qubesadmin/tools/qvm_volume.py +++ b/qubesadmin/tools/qvm_volume.py @@ -94,7 +94,7 @@ def info_volume(args): volume = args.volume info_items = ( 'pool', 'vid', 'rw', 'source', 'save_on_stop', - 'snap_on_start', 'size', 'usage', 'revisions_to_keep') + 'snap_on_start', 'size', 'usage', 'revisions_to_keep', 'ephemeral') if args.property: if args.property == 'revisions': for rev in volume.revisions: @@ -131,7 +131,7 @@ def info_volume(args): def config_volume(args): """ Change property of selected volume """ volume = args.volume - if not args.property in ('rw', 'revisions_to_keep'): + if args.property not in ('rw', 'revisions_to_keep', 'ephemeral'): raise qubesadmin.exc.QubesNoSuchPropertyError( 'Invalid property: {}'.format(args.property)) setattr(volume, args.property, args.value)