Skip to content

Commit 44604f7

Browse files
committed
Add free-form text to qube for notes, comments, ...
qubesadmin part of adding free-form text to each qube for comments, notes, descriptions, remarks, reminders, etc. fixes: QubesOS/qubes-issues#899 Additional options to skip legacy backup tests or super slow tests
1 parent 02a9b4f commit 44604f7

File tree

13 files changed

+422
-2
lines changed

13 files changed

+422
-2
lines changed

doc/conf.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
#
8585
# This is also used if you do content translation via gettext catalogs.
8686
# Usually you set "language" from the command line for these cases.
87-
language = None
87+
language = 'en'
8888

8989
# There are two options for replacing |today|: either, you set today to some
9090
# non-false value, then it is used:
@@ -344,6 +344,8 @@
344344
u'Kill the specified qube', _man_pages_author, 1),
345345
('manpages/qvm-ls', 'qvm-ls',
346346
u'List VMs and various information about them', _man_pages_author, 1),
347+
('manpages/qvm-notes', 'qvm-notes',
348+
u'Manipulate qube notes', _man_pages_author, 1),
347349
('manpages/qvm-pause', 'qvm-pause',
348350
u'Pause a specified qube(s)', _man_pages_author, 1),
349351
('manpages/qvm-pool', 'qvm-pool',
@@ -370,7 +372,6 @@
370372
u'Pause a qube', _man_pages_author, 1),
371373
('manpages/qvm-volume', 'qvm-volume',
372374
u'Manage storage volumes of a qube', _man_pages_author, 1),
373-
374375
('manpages/qubes-prefs', 'qubes-prefs',
375376
u'Display system-wide Qubes settings', _man_pages_author, 1),
376377
]

doc/manpages/qvm-notes.rst

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
.. program:: qvm-notes
2+
3+
:program:`qvm-notes` -- Manipulate qube notes
4+
=============================================
5+
6+
Synopsis
7+
--------
8+
9+
:command:`qvm-notes` [options] *VMNAME* [--edit | --print | --import *FILENAME* | --set '*NOTES*' | --append '*NOTES*' | --delete]
10+
11+
Description
12+
-----------
13+
14+
This command is used to manipulate individual qube notes. Each qube notes is
15+
limited to 256KB of clear text which could contain most UTF-8 characters.
16+
However, some UTF-8 characters will be replaced with underline (`_`) due to
17+
security limitations. Qube notes will be included in backup/restore.
18+
19+
If this command is run outside dom0, it will require `admin.vm.notes.Get` and/or
20+
`admin.vm.notes.Set` access privileges for the target qube in the RPC policies.
21+
22+
General options
23+
---------------
24+
25+
.. option:: --verbose, -v
26+
27+
increase verbosity
28+
29+
.. option:: --quiet, -q
30+
31+
decrease verbosity
32+
33+
.. option:: --help, -h
34+
35+
show this help message and exit
36+
37+
.. option:: --version
38+
39+
show program's version number and exit
40+
41+
.. option:: --force, -f
42+
43+
Do not prompt for confirmation; assume `yes`
44+
45+
Action options
46+
--------------
47+
48+
.. option:: --edit, -e
49+
50+
Edit qube notes in $EDITOR (default text editor). This is the default action.
51+
52+
.. option:: --print, -p
53+
54+
Print qube notes
55+
56+
.. option:: --import=FILENAME, -i FILENAME
57+
58+
Import qube notes from file
59+
60+
.. option:: --set='NOTES', -s 'NOTES'
61+
62+
Set qube notes from the provided string
63+
64+
.. option:: --append='NOTES'
65+
66+
Append the provided string to qube notes
67+
68+
.. option:: --delete, -d
69+
70+
Delete qube notes
71+
72+
Authors
73+
-------
74+
75+
| Marek Marczykowski <marmarek at invisiblethingslab dot com>
76+
| Ali Mirjamali <ali at mirjamali dot com>
77+
78+
| For complete author list see: https://github.com/QubesOS/qubes-core-admin-client.git
79+
80+
.. vim: ts=3 sw=3 et tw=80

qubesadmin/app.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ def clone_vm(self, src_vm, new_name, new_cls=None, *, pool=None, pools=None,
365365
ignore_errors=False, ignore_volumes=None,
366366
ignore_devices=False):
367367
# pylint: disable=too-many-statements
368+
# pylint: disable=too-many-branches
368369
"""Clone Virtual Machine
369370
370371
Example usage with custom storage pools:
@@ -474,6 +475,16 @@ def clone_vm(self, src_vm, new_name, new_cls=None, *, pool=None, pools=None,
474475
if not ignore_errors:
475476
raise
476477

478+
try:
479+
vm_notes = src_vm.get_notes()
480+
if vm_notes:
481+
dst_vm.set_notes(vm_notes)
482+
except qubesadmin.exc.QubesException as e:
483+
dst_vm.log.error(
484+
'Failed to clone qube notes: {!s}'.format(e))
485+
if not ignore_errors:
486+
raise
487+
477488
try:
478489
dst_vm.firewall.save_rules(src_vm.firewall.rules)
479490
except qubesadmin.exc.QubesException as e:

qubesadmin/backup/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,7 @@ def included_in_backup(self):
7171
def handle_firewall_xml(self, vm, stream):
7272
'''Import appropriate format of firewall.xml'''
7373
raise NotImplementedError
74+
75+
def handle_notes_txt(self, vm, stream):
76+
'''Import qubes notes.txt'''
77+
raise NotImplementedError

qubesadmin/backup/core2.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ def _translate_action(key):
140140
except: # pylint: disable=bare-except
141141
vm.log.exception('Failed to set firewall')
142142

143+
def handle_notes_txt(self, vm, stream):
144+
'''Qube notes did not exist at this time'''
145+
raise NotImplementedError
146+
143147

144148
class Core2Qubes(qubesadmin.backup.BackupApp):
145149
'''Parsed qubes.xml'''

qubesadmin/backup/core3.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ def handle_firewall_xml(self, vm, stream):
5252
except: # pylint: disable=bare-except
5353
vm.log.exception('Failed to set firewall')
5454

55+
def handle_notes_txt(self, vm, stream):
56+
'''Load new (Qubes >= 4.2) notes'''
57+
try:
58+
vm.set_notes(stream.read().decode())
59+
except: # pylint: disable=bare-except
60+
vm.log.exception('Failed to set notes')
61+
5562
class Core3Qubes(qubesadmin.backup.BackupApp):
5663
'''Parsed qubes.xml'''
5764
def __init__(self, store=None):

qubesadmin/backup/restore.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1963,6 +1963,8 @@ def restore_do(self, restore_info):
19631963
handlers[img_path] = data_func
19641964
handlers[os.path.join(vm_info.subdir, 'firewall.xml')] = \
19651965
functools.partial(vm_info.vm.handle_firewall_xml, vm)
1966+
handlers[os.path.join(vm_info.subdir, 'notes.txt')] = \
1967+
functools.partial(vm_info.vm.handle_notes_txt, vm)
19661968
handlers[os.path.join(vm_info.subdir,
19671969
'whitelisted-appmenus.list')] = \
19681970
functools.partial(self._handle_appmenus_list, vm)

qubesadmin/exc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,5 +232,9 @@ def __init__(self, prop):
232232
super().__init__("Failed to access '%s' property" % prop)
233233

234234

235+
236+
class QubesNotesError(QubesException):
237+
"""Some problem with qube notes."""
238+
235239
# legacy name
236240
QubesDaemonNoResponseError = QubesDaemonAccessError

qubesadmin/tests/app.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,11 @@ def clone_setup_common_calls(self, src, dst):
358358
(dst, 'admin.vm.firewall.Set', None, rules)] = \
359359
b'0\x00'
360360

361+
# notes
362+
self.app.expected_calls[
363+
(src, 'admin.vm.notes.Get', None, None)] = \
364+
b'0\0'
365+
361366
# storage
362367
for vm in (src, dst):
363368
self.app.expected_calls[
@@ -813,6 +818,50 @@ def test_044_clone_devices_fail(self):
813818

814819
self.assertAllCalled()
815820

821+
def test_045_clone_notes_fail(self):
822+
self.app.expected_calls[
823+
('test-vm', 'admin.vm.notes.Get', None, None)] = \
824+
b'0\0Secret Note\0'
825+
self.app.expected_calls[
826+
('new-name', 'admin.vm.notes.Set', None, b'Secret Note\0')] = \
827+
b'2\0QubesNotesException\0\0It was For Your Eyes Only!\0'
828+
self.app.expected_calls[
829+
('test-vm', 'admin.vm.property.List', None, None)] = \
830+
b'0\0qid\nname\ntemplate\nlabel\nmemory\n'
831+
self.app.expected_calls[
832+
('test-vm', 'admin.vm.volume.List', None, None)] = \
833+
b'0\x00'
834+
self.app.expected_calls[
835+
('test-vm', 'admin.vm.property.Get', 'label', None)] = \
836+
b'0\0default=False type=label red'
837+
self.app.expected_calls[
838+
('test-vm', 'admin.vm.property.Get', 'template', None)] = \
839+
b'0\0default=False type=vm test-template'
840+
self.app.expected_calls[
841+
('test-vm', 'admin.vm.property.Get', 'memory', None)] = \
842+
b'0\0default=False type=int 400'
843+
self.app.expected_calls[
844+
('new-name', 'admin.vm.property.Set', 'memory', b'400')] = \
845+
b'0\0'
846+
self.app.expected_calls[
847+
('test-vm', 'admin.vm.tag.List', None, None)] = \
848+
b'0\0'
849+
self.app.expected_calls[
850+
('test-vm', 'admin.vm.feature.List', None, None)] = \
851+
b'0\0'
852+
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
853+
b'0\x00new-name class=AppVM state=Halted\n' \
854+
b'test-vm class=AppVM state=Halted\n' \
855+
b'test-template class=TemplateVM state=Halted\n' \
856+
b'test-net class=AppVM state=Halted\n'
857+
self.app.expected_calls[('dom0', 'admin.vm.Create.AppVM',
858+
'test-template', b'name=new-name label=red')] = b'0\x00'
859+
self.app.expected_calls[('new-name', 'admin.vm.Remove', None, None)] = \
860+
b'0\x00'
861+
with self.assertRaises(qubesadmin.exc.QubesException):
862+
self.app.clone_vm('test-vm', 'new-name')
863+
self.assertAllCalled()
864+
816865
def test_050_automatic_reset_cache(self):
817866
self.app.cache_enabled = True
818867
dispatcher = qubesadmin.events.EventsDispatcher(self.app)

qubesadmin/tests/backup/backupcompatibility.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,14 @@ def create_v4_files(self):
10501050
"tests/backup/v4-firewall.xml"
10511051
f_firewall.write(xml_path.read_bytes())
10521052

1053+
# setup notes only on one VM
1054+
with open(
1055+
self.fullpath("appvms/test-work/notes.txt"),
1056+
"w+",
1057+
encoding="utf-8",
1058+
) as notes:
1059+
notes.write("For Your Eyes Only")
1060+
10531061
# StandaloneVMs
10541062
for vm in ('test-standalonevm', 'test-hvm'):
10551063
os.mkdir(self.fullpath('appvms/{}'.format(vm)))
@@ -1528,6 +1536,8 @@ def create_limited_tmpdir(self, size):
15281536
self.addCleanup(self.cleanup_tmpdir, tmpdir)
15291537
return tmpdir.name
15301538

1539+
@unittest.skipIf(os.environ.get('DISABLE_LEGACY_TESTS', False),
1540+
'Set DISABLE_LEGACY_TESTS=1 environment variable to skip this test')
15311541
def test_210_r2(self):
15321542
self.create_v3_backup(False)
15331543
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = (
@@ -1601,6 +1611,8 @@ def test_210_r2(self):
16011611

16021612
self.assertDom0Restored(dummy_timestamp)
16031613

1614+
@unittest.skipIf(os.environ.get('DISABLE_LEGACY_TESTS', False),
1615+
'Set DISABLE_LEGACY_TESTS=1 environment variable to skip this test')
16041616
def test_220_r2_encrypted(self):
16051617
self.create_v3_backup(True)
16061618

@@ -1676,6 +1688,8 @@ def test_220_r2_encrypted(self):
16761688

16771689
self.assertDom0Restored(dummy_timestamp)
16781690

1691+
@unittest.skipIf(os.environ.get('DISABLE_LEGACY_TESTS', False),
1692+
'Set DISABLE_LEGACY_TESTS=1 environment variable to skip this test')
16791693
def test_230_r2_uncompressed(self):
16801694
self.create_v3_backup(False, False)
16811695
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = (
@@ -1781,6 +1795,9 @@ def test_230_r4(self):
17811795
self.app.expected_calls[
17821796
('test-work', 'admin.vm.firewall.Set', None,
17831797
firewall_data.encode())] = b'0\0'
1798+
self.app.expected_calls[
1799+
('test-work', 'admin.vm.notes.Set', None,
1800+
b'For Your Eyes Only')] = b'0\0'
17841801

17851802
qubesd_calls_queue = multiprocessing.Queue()
17861803

@@ -1858,6 +1875,9 @@ def test_230_r4_compressed(self):
18581875
self.app.expected_calls[
18591876
('test-work', 'admin.vm.firewall.Set', None,
18601877
firewall_data.encode())] = b'0\0'
1878+
self.app.expected_calls[
1879+
('test-work', 'admin.vm.notes.Set', None,
1880+
b'For Your Eyes Only')] = b'0\0'
18611881

18621882
qubesd_calls_queue = multiprocessing.Queue()
18631883

@@ -1935,6 +1955,9 @@ def test_230_r4_custom_cmpression(self):
19351955
self.app.expected_calls[
19361956
('test-work', 'admin.vm.firewall.Set', None,
19371957
firewall_data.encode())] = b'0\0'
1958+
self.app.expected_calls[
1959+
('test-work', 'admin.vm.notes.Set', None,
1960+
b'For Your Eyes Only')] = b'0\0'
19381961

19391962
qubesd_calls_queue = multiprocessing.Queue()
19401963

@@ -2049,6 +2072,9 @@ def test_230_r4_uncommon_compression_forced(self):
20492072
self.app.expected_calls[
20502073
('test-work', 'admin.vm.firewall.Set', None,
20512074
firewall_data.encode())] = b'0\0'
2075+
self.app.expected_calls[
2076+
('test-work', 'admin.vm.notes.Set', None,
2077+
b'For Your Eyes Only')] = b'0\0'
20522078

20532079
qubesd_calls_queue = multiprocessing.Queue()
20542080

@@ -2126,6 +2152,9 @@ def test_230_r4_optional_compression(self):
21262152
self.app.expected_calls[
21272153
('test-work', 'admin.vm.firewall.Set', None,
21282154
firewall_data.encode())] = b'0\0'
2155+
self.app.expected_calls[
2156+
('test-work', 'admin.vm.notes.Set', None,
2157+
b'For Your Eyes Only')] = b'0\0'
21292158

21302159
qubesd_calls_queue = multiprocessing.Queue()
21312160

@@ -2171,6 +2200,8 @@ def test_230_r4_optional_compression(self):
21712200

21722201
self.assertDom0Restored(dummy_timestamp)
21732202

2203+
@unittest.skipIf(os.environ.get('DISABLE_SUPER_SLOW_TESTS', False),
2204+
'Set DISABLE_SUPER_SLOW_TESTS=1 environment variable to skip this test')
21742205
@unittest.skipUnless(shutil.which('scrypt'),
21752206
"scrypt not installed")
21762207
def test_300_r4_no_space(self):
@@ -2202,6 +2233,9 @@ def test_300_r4_no_space(self):
22022233
self.app.expected_calls[
22032234
('test-work', 'admin.vm.firewall.Set', None,
22042235
firewall_data.encode())] = b'0\0'
2236+
self.app.expected_calls[
2237+
('test-work', 'admin.vm.notes.Set', None,
2238+
b'For Your Eyes Only')] = b'0\0'
22052239

22062240
qubesd_calls_queue = multiprocessing.Queue()
22072241

0 commit comments

Comments
 (0)