Skip to content

Commit 7b958e7

Browse files
committed
qvm-volume: add 'info' and 'config' actions
This allows to get and set volumes properties. Fixes QubesOS/qubes-issues#3256
1 parent b57b101 commit 7b958e7

File tree

3 files changed

+268
-4
lines changed

3 files changed

+268
-4
lines changed

doc/manpages/qvm-volume.rst

+24
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,30 @@ passed or stdout is redirected to a pipe or file.
6060

6161
aliases: ls, l
6262

63+
info
64+
^^^^
65+
| :command:`qvm-volume info` [-h] [--verbose] [--quiet] *VMNAME:VOLUME* [*PROPERTY*]
66+
67+
Show information about given volume - all properties and available revisions
68+
(for `revert` action). If specific property is given, only its value is printed.
69+
For list of revisions use `revisions` value.
70+
71+
aliases: i
72+
73+
config
74+
^^^^^^
75+
| :command:`qvm-volume config` [-h] [--verbose] [--quiet] *VMNAME:VOLUME* *PROPERTY* *VALUE*
76+
77+
Set property of given volume. Properties currently possible to change:
78+
79+
- `rw` - `True` if volume should be writeable by the qube, `False` otherwise
80+
- `revisions_to_keep` - how many revisions (previous versions of volume)
81+
should be keep. At each qube shutdown its previous state is saved in new
82+
revision, and the oldest revisions are remove so that only
83+
`revisions_to_keep` are left. Set to `0` to not leave any previous versions.
84+
85+
aliases: c, set, s
86+
6387
extend
6488
^^^^^^
6589
| :command:`qvm-volume extend` [-h] [--verbose] [--quiet] *POOL_NAME:VOLUME_ID* *NEW_SIZE*

qubesadmin/tests/tools/qvm_volume.py

+169
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,172 @@ def test_023_revert_specific(self):
250250
['revert', 'testvm:private', '20050101'],
251251
app=self.app))
252252
self.assertAllCalled()
253+
254+
def test_030_set_revisions_to_keep(self):
255+
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
256+
b'0\x00testvm class=AppVM state=Running\n'
257+
self.app.expected_calls[
258+
('testvm', 'admin.vm.volume.List', None, None)] = \
259+
b'0\x00root\nprivate\n'
260+
self.app.expected_calls[
261+
('testvm', 'admin.vm.volume.Set.revisions_to_keep', 'private',
262+
b'3')] = b'0\x00'
263+
self.assertEqual(0,
264+
qubesadmin.tools.qvm_volume.main(
265+
['set', 'testvm:private', 'revisions_to_keep', '3'],
266+
app=self.app))
267+
self.assertAllCalled()
268+
269+
def test_031_set_rw(self):
270+
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
271+
b'0\x00testvm class=AppVM state=Running\n'
272+
self.app.expected_calls[
273+
('testvm', 'admin.vm.volume.List', None, None)] = \
274+
b'0\x00root\nprivate\n'
275+
self.app.expected_calls[
276+
('testvm', 'admin.vm.volume.Set.rw', 'private',
277+
b'True')] = b'0\x00'
278+
self.assertEqual(0,
279+
qubesadmin.tools.qvm_volume.main(
280+
['set', 'testvm:private', 'rw', 'True'],
281+
app=self.app))
282+
self.assertAllCalled()
283+
284+
def test_032_set_invalid(self):
285+
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
286+
b'0\x00testvm class=AppVM state=Running\n'
287+
self.app.expected_calls[
288+
('testvm', 'admin.vm.volume.List', None, None)] = \
289+
b'0\x00root\nprivate\n'
290+
self.assertNotEqual(0,
291+
qubesadmin.tools.qvm_volume.main(
292+
['set', 'testvm:private', 'invalid', 'True'],
293+
app=self.app))
294+
self.assertAllCalled()
295+
296+
def test_040_info(self):
297+
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
298+
b'0\x00testvm class=AppVM state=Running\n'
299+
self.app.expected_calls[
300+
('testvm', 'admin.vm.volume.List', None, None)] = \
301+
b'0\x00root\nprivate\n'
302+
self.app.expected_calls[
303+
('testvm', 'admin.vm.volume.Info', 'private', None)] = \
304+
b'0\x00pool=lvm\n' \
305+
b'vid=qubes_dom0/vm-testvm-private\n' \
306+
b'size=2147483648\n' \
307+
b'usage=10000000\n' \
308+
b'rw=True\n' \
309+
b'source=\n' \
310+
b'save_on_stop=True\n' \
311+
b'snap_on_start=False\n' \
312+
b'revisions_to_keep=3\n' \
313+
b'is_outdated=False\n'
314+
self.app.expected_calls[
315+
('testvm', 'admin.vm.volume.ListSnapshots', 'private', None)] = \
316+
b'0\x00200101010000\n200201010000\n200301010000\n'
317+
with qubesadmin.tests.tools.StdoutBuffer() as stdout:
318+
self.assertEqual(0,
319+
qubesadmin.tools.qvm_volume.main(['info', 'testvm:private'],
320+
app=self.app))
321+
self.assertEqual(stdout.getvalue(),
322+
'pool lvm\n'
323+
'vid qubes_dom0/vm-testvm-private\n'
324+
'rw True\n'
325+
'source \n'
326+
'save_on_stop True\n'
327+
'snap_on_start False\n'
328+
'size 2147483648\n'
329+
'usage 10000000\n'
330+
'revisions_to_keep 3\n'
331+
'is_outdated False\n'
332+
'Available revisions (for revert):\n'
333+
' 200101010000\n'
334+
' 200201010000\n'
335+
' 200301010000\n')
336+
self.assertAllCalled()
337+
338+
def test_041_info_no_revisions(self):
339+
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
340+
b'0\x00testvm class=AppVM state=Running\n'
341+
self.app.expected_calls[
342+
('testvm', 'admin.vm.volume.List', None, None)] = \
343+
b'0\x00root\nprivate\n'
344+
self.app.expected_calls[
345+
('testvm', 'admin.vm.volume.Info', 'root', None)] = \
346+
b'0\x00pool=lvm\n' \
347+
b'vid=qubes_dom0/vm-testvm-root\n' \
348+
b'size=2147483648\n' \
349+
b'usage=10000000\n' \
350+
b'rw=True\n' \
351+
b'source=qubes_dom0/vm-fedora-26-root\n' \
352+
b'save_on_stop=False\n' \
353+
b'snap_on_start=True\n' \
354+
b'revisions_to_keep=0\n' \
355+
b'is_outdated=False\n'
356+
self.app.expected_calls[
357+
('testvm', 'admin.vm.volume.ListSnapshots', 'root', None)] = \
358+
b'0\x00'
359+
with qubesadmin.tests.tools.StdoutBuffer() as stdout:
360+
self.assertEqual(0,
361+
qubesadmin.tools.qvm_volume.main(['info', 'testvm:root'],
362+
app=self.app))
363+
self.assertEqual(stdout.getvalue(),
364+
'pool lvm\n'
365+
'vid qubes_dom0/vm-testvm-root\n'
366+
'rw True\n'
367+
'source qubes_dom0/vm-fedora-26-root\n'
368+
'save_on_stop False\n'
369+
'snap_on_start True\n'
370+
'size 2147483648\n'
371+
'usage 10000000\n'
372+
'revisions_to_keep 0\n'
373+
'is_outdated False\n'
374+
'Available revisions (for revert): none\n')
375+
self.assertAllCalled()
376+
377+
def test_042_info_single_prop(self):
378+
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
379+
b'0\x00testvm class=AppVM state=Running\n'
380+
self.app.expected_calls[
381+
('testvm', 'admin.vm.volume.List', None, None)] = \
382+
b'0\x00root\nprivate\n'
383+
self.app.expected_calls[
384+
('testvm', 'admin.vm.volume.Info', 'root', None)] = \
385+
b'0\x00pool=lvm\n' \
386+
b'vid=qubes_dom0/vm-testvm-root\n' \
387+
b'size=2147483648\n' \
388+
b'usage=10000000\n' \
389+
b'rw=True\n' \
390+
b'source=qubes_dom0/vm-fedora-26-root\n' \
391+
b'save_on_stop=False\n' \
392+
b'snap_on_start=True\n' \
393+
b'revisions_to_keep=0\n' \
394+
b'is_outdated=False\n'
395+
with qubesadmin.tests.tools.StdoutBuffer() as stdout:
396+
self.assertEqual(0,
397+
qubesadmin.tools.qvm_volume.main(
398+
['info', 'testvm:root', 'usage'],
399+
app=self.app))
400+
self.assertEqual(stdout.getvalue(), '10000000\n')
401+
self.assertAllCalled()
402+
403+
def test_043_info_revisions_only(self):
404+
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
405+
b'0\x00testvm class=AppVM state=Running\n'
406+
self.app.expected_calls[
407+
('testvm', 'admin.vm.volume.List', None, None)] = \
408+
b'0\x00root\nprivate\n'
409+
self.app.expected_calls[
410+
('testvm', 'admin.vm.volume.ListSnapshots', 'private', None)] = \
411+
b'0\x00200101010000\n200201010000\n200301010000\n'
412+
with qubesadmin.tests.tools.StdoutBuffer() as stdout:
413+
self.assertEqual(0,
414+
qubesadmin.tools.qvm_volume.main(
415+
['info', 'testvm:private', 'revisions'],
416+
app=self.app))
417+
self.assertEqual(stdout.getvalue(),
418+
'200101010000\n'
419+
'200201010000\n'
420+
'200301010000\n')
421+
self.assertAllCalled()

qubesadmin/tools/qvm_volume.py

+75-4
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424

2525
from __future__ import print_function
2626

27+
import argparse
2728
import sys
2829

30+
import collections
31+
2932
import qubesadmin
3033
import qubesadmin.exc
3134
import qubesadmin.tools
@@ -84,6 +87,50 @@ def __lt__(self, other):
8487
def __str__(self):
8588
return "{!s}:{!s}".format(self.pool, self.vid)
8689

90+
def info_volume(args):
91+
''' Show info about selected volume '''
92+
volume = args.volume
93+
info_items = ('pool', 'vid', 'rw', 'source', 'save_on_stop',
94+
'snap_on_start', 'size', 'usage', 'revisions_to_keep')
95+
if args.property:
96+
if args.property == 'revisions':
97+
for rev in volume.revisions:
98+
print(rev)
99+
elif args.property == 'is_outdated':
100+
print(volume.is_outdated())
101+
elif args.property in info_items:
102+
value = getattr(volume, args.property)
103+
if value is None:
104+
value = ''
105+
print(value)
106+
else:
107+
raise qubesadmin.exc.StoragePoolException(
108+
'No such property: {}'.format(args.property))
109+
else:
110+
info = collections.OrderedDict()
111+
for item in info_items:
112+
value = getattr(volume, item)
113+
if value is None:
114+
value = ''
115+
info[item] = str(value)
116+
info['is_outdated'] = str(volume.is_outdated())
117+
118+
qubesadmin.tools.print_table(info.items())
119+
revisions = volume.revisions
120+
if revisions:
121+
print('Available revisions (for revert):')
122+
for rev in revisions:
123+
print(' ' + rev)
124+
else:
125+
print('Available revisions (for revert): none')
126+
127+
def config_volume(args):
128+
''' Change property of selected volume '''
129+
volume = args.volume
130+
if not args.property in ('rw', 'revisions_to_keep'):
131+
raise qubesadmin.exc.QubesNoSuchPropertyError(
132+
'Invalid property: {}'.format(args.property))
133+
setattr(volume, args.property, args.value)
87134

88135
def list_volumes(args):
89136
''' Called by the parser to execute the qvm-volume list subcommand. '''
@@ -181,7 +228,7 @@ def init_revert_parser(sub_parsers):
181228
revert_parser.add_argument(metavar='VM:VOLUME', dest='volume',
182229
action=qubesadmin.tools.VMVolumeAction)
183230
revert_parser.add_argument(metavar='REVISION', dest='revision',
184-
help='Optional revision to revert to;'
231+
help='Optional revision to revert to; '
185232
'if not specified, latest one is assumed',
186233
action='store', nargs='?')
187234
revert_parser.set_defaults(func=revert_volume)
@@ -196,19 +243,43 @@ def init_extend_parser(sub_parsers):
196243
extend_parser.add_argument('size', help='New size in bytes')
197244
extend_parser.set_defaults(func=extend_volumes)
198245

246+
def init_info_parser(sub_parsers):
247+
''' Add 'info' action related options '''
248+
info_parser = sub_parsers.add_parser(
249+
'info', aliases=('i',), help='info about volume')
250+
info_parser.add_argument(metavar='VM:VOLUME', dest='volume',
251+
action=qubesadmin.tools.VMVolumeAction)
252+
info_parser.add_argument(dest='property', action='store',
253+
nargs=argparse.OPTIONAL,
254+
help='Show only this property instead of all of them; use '
255+
'\'revisions\' to list available revisions')
256+
info_parser.set_defaults(func=info_volume)
257+
258+
def init_config_parser(sub_parsers):
259+
''' Add 'info' action related options '''
260+
info_parser = sub_parsers.add_parser(
261+
'config', aliases=('c', 'set', 's'),
262+
help='set config option for a volume')
263+
info_parser.add_argument(metavar='VM:VOLUME', dest='volume',
264+
action=qubesadmin.tools.VMVolumeAction)
265+
info_parser.add_argument(dest='property', action='store')
266+
info_parser.add_argument(dest='value', action='store')
267+
info_parser.set_defaults(func=config_volume)
199268

200269
def get_parser():
201270
'''Create :py:class:`argparse.ArgumentParser` suitable for
202-
:program:`qvm-block`.
271+
:program:`qvm-volume`.
203272
'''
204273
parser = qubesadmin.tools.QubesArgumentParser(description=__doc__,
205274
want_app=True)
206275
parser.register('action', 'parsers',
207276
qubesadmin.tools.AliasedSubParsersAction)
208277
sub_parsers = parser.add_subparsers(
209278
title='commands',
210-
description="For more information see qvm-block command -h",
279+
description="For more information see qvm-volume command -h",
211280
dest='command')
281+
init_info_parser(sub_parsers)
282+
init_config_parser(sub_parsers)
212283
init_extend_parser(sub_parsers)
213284
init_list_parser(sub_parsers)
214285
init_revert_parser(sub_parsers)
@@ -219,7 +290,7 @@ def get_parser():
219290

220291

221292
def main(args=None, app=None):
222-
'''Main routine of :program:`qvm-block`.'''
293+
'''Main routine of :program:`qvm-volume`.'''
223294
parser = get_parser()
224295
try:
225296
args = parser.parse_args(args, app=app)

0 commit comments

Comments
 (0)