Skip to content

Commit 2fdb146

Browse files
committed
Add free-form text to qube for notes, comments, ...
Core and API part of adding free-form text to each qube for comments, notes, descriptions, remarks, reminders, etc. fixes: QubesOS/qubes-issues#899
1 parent 6828311 commit 2fdb146

7 files changed

Lines changed: 127 additions & 0 deletions

File tree

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ ADMIN_API_METHODS_SIMPLE = \
109109
admin.vm.firewall.GetPolicy \
110110
admin.vm.firewall.SetPolicy \
111111
admin.vm.firewall.Reload \
112+
admin.vm.notes.Get \
113+
admin.vm.notes.Set \
112114
admin.vm.property.Get \
113115
admin.vm.property.GetAll \
114116
admin.vm.property.GetDefault \

qubes/api/admin.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,3 +2038,43 @@ async def vm_current_state(self):
20382038
"power_state": self.dest.get_power_state(),
20392039
}
20402040
return " ".join("{}={}".format(k, v) for k, v in state.items())
2041+
2042+
@qubes.api.method(
2043+
"admin.vm.notes.Get", no_payload=True, scope="local", read=True
2044+
)
2045+
async def vm_notes_get(self):
2046+
"""Get qube notes"""
2047+
self.enforce(self.dest.name != "dom0")
2048+
self.fire_event_for_permission()
2049+
try:
2050+
notes = self.dest.get_notes()
2051+
except Exception as e:
2052+
raise qubes.exc.QubesException(
2053+
"Could not read qube notes: " + str(e)
2054+
)
2055+
return notes
2056+
2057+
@qubes.api.method("admin.vm.notes.Set", scope="local", write=True)
2058+
async def vm_notes_set(self, untrusted_payload):
2059+
"""Set qube notes"""
2060+
self.enforce(self.dest.name != "dom0")
2061+
self.fire_event_for_permission()
2062+
if len(untrusted_payload) > 256000:
2063+
raise qubes.exc.ProtocolError(
2064+
"Maximum note size is 256000 bytes ({} bytes received)".format(
2065+
len(untrusted_payload)
2066+
)
2067+
)
2068+
allowed_chars = string.printable
2069+
notes = "".join(
2070+
[
2071+
c if c in allowed_chars else "_"
2072+
for c in untrusted_payload.decode("ascii")
2073+
]
2074+
)
2075+
try:
2076+
self.dest.set_notes(notes)
2077+
except Exception as e:
2078+
raise qubes.exc.QubesException(
2079+
"Could not write qube notes: " + str(e)
2080+
)

qubes/backup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,10 @@ def get_files_to_backup(self):
433433
if os.path.exists(firewall_conf):
434434
vm_files.append(self.FileToBackup(firewall_conf, subdir))
435435

436+
notes_file_path = os.path.join(vm.dir_path, vm.notes_file)
437+
if os.path.exists(notes_file_path):
438+
vm_files.append(self.FileToBackup(notes_file_path, subdir))
439+
436440
if not vm_files:
437441
# subdir/ is needed in the tar file, otherwise restore
438442
# of a (Disp)VM without any backed up files is going

qubes/tests/api_admin.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2130,6 +2130,35 @@ def test_450_property_reset(self):
21302130
self.assertIsNone(value)
21312131
self.app.save.assert_called_once_with()
21322132

2133+
def test_notes_get(self):
2134+
notes = "For Your Eyes Only"
2135+
self.app.domains["test-vm1"].get_notes = unittest.mock.Mock()
2136+
self.app.domains["test-vm1"].get_notes.configure_mock(
2137+
**{"return_value": notes}
2138+
)
2139+
value = self.call_mgmt_func(b"admin.vm.notes.Get", b"test-vm1")
2140+
self.assertEqual(value, notes)
2141+
self.app.domains["test-vm1"].get_notes.configure_mock(
2142+
**{"side_effect": Exception()}
2143+
)
2144+
with self.assertRaises(qubes.exc.QubesException):
2145+
self.call_mgmt_func(b"admin.vm.notes.Get", b"test-vm1")
2146+
self.assertEqual(
2147+
self.app.domains["test-vm1"].get_notes.mock_calls,
2148+
[unittest.mock.call(), unittest.mock.call()],
2149+
)
2150+
self.assertFalse(self.app.save.called)
2151+
2152+
def test_notes_set(self):
2153+
self.app.domains["test-vm1"].set_notes = unittest.mock.Mock()
2154+
with self.assertRaises(qubes.exc.ProtocolError):
2155+
payload = ("x" * 256001).encode()
2156+
self.call_mgmt_func(
2157+
b"admin.vm.notes.Set",
2158+
b"test-vm1",
2159+
payload=payload,
2160+
)
2161+
21332162
def device_list_testclass(self, vm, event):
21342163
if vm is not self.vm:
21352164
return

qubes/tests/vm/qubesvm.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2947,3 +2947,18 @@ def test_801_ordering(self):
29472947
assert qubes.vm.qubesvm.QubesVM(
29482948
self.app, None, qid=1, name="bogus"
29492949
) > qubes.vm.adminvm.AdminVM(self.app, None)
2950+
2951+
def test_802_notes(self):
2952+
vm = self.get_vm()
2953+
notes = "For Your Eyes Only"
2954+
with unittest.mock.patch(
2955+
"builtins.open", unittest.mock.mock_open(read_data=notes)
2956+
) as mock_open:
2957+
with self.assertNotRaises(Exception):
2958+
vm.set_notes(notes)
2959+
self.assertEqual(vm.get_notes(), notes)
2960+
mock_open.side_effect = FileNotFoundError()
2961+
self.assertEqual(vm.get_notes(), "")
2962+
with self.assertRaises(Exception):
2963+
mock_open.side_effect = PermissionError()
2964+
vm.get_notes()

qubes/vm/qubesvm.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2585,6 +2585,41 @@ def kernelopts_common(self):
25852585
else:
25862586
return base_kernelopts + qubes.config.defaults["kernelopts_common"]
25872587

2588+
#
2589+
# free-form text for descriptions, notes, comments, remarks, etc.
2590+
#
2591+
2592+
@property
2593+
def notes_file(self) -> str:
2594+
"""Notes file name within /var/lib/qubes (per each qube sub-dir)"""
2595+
return "notes.txt"
2596+
2597+
def get_notes(self) -> str:
2598+
"""Read the notes file and return its content"""
2599+
try:
2600+
with open(
2601+
os.path.join(self.dir_path, self.notes_file), encoding="ascii"
2602+
) as fd:
2603+
return fd.read()
2604+
except FileNotFoundError:
2605+
return ""
2606+
except Exception as exc:
2607+
self.log.error("Failed to read notes file: %s", str(exc))
2608+
raise
2609+
2610+
def set_notes(self, notes: str):
2611+
"""Write to notes file. Return True on success, False on error"""
2612+
try:
2613+
with open(
2614+
os.path.join(self.dir_path, self.notes_file),
2615+
"w",
2616+
encoding="ascii",
2617+
) as fd:
2618+
fd.write(notes)
2619+
except Exception as exc:
2620+
self.log.error("Failed to write notes file: %s", str(exc))
2621+
raise
2622+
25882623
#
25892624
# helper methods
25902625
#

rpm_spec/core-dom0.spec.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ admin.vm.feature.Set
286286
admin.vm.firewall.Get
287287
admin.vm.firewall.Reload
288288
admin.vm.firewall.Set
289+
admin.vm.notes.Get
290+
admin.vm.notes.Set
289291
admin.vm.property.Get
290292
admin.vm.property.GetAll
291293
admin.vm.property.GetDefault

0 commit comments

Comments
 (0)