Skip to content

Commit 29ca10f

Browse files
committed
Exclude preloads from removal output
Unless it is a global property. Fixes: QubesOS/qubes-issues#10227 For: QubesOS/qubes-issues#1512
1 parent f040673 commit 29ca10f

File tree

3 files changed

+133
-37
lines changed

3 files changed

+133
-37
lines changed

qubesadmin/tests/mock_app.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,24 @@
3232
Example: to run qui-domains widget in a qube using MockQubes,
3333
replace it's main with the following:
3434
35-
def main():
36-
''' main function '''
37-
# qapp = qubesadmin.Qubes()
38-
# dispatcher = qubesadmin.events.EventsDispatcher(qapp)
39-
# stats_dispatcher = qubesadmin.events.EventsDispatcher(
40-
qapp, api_method='admin.vm.Stats')
4135
42-
import qubesadmin.tests.mock_app as mock_app
43-
qapp = mock_app.MockQubesComplete()
44-
dispatcher = mock_app.MockDispatcher(qapp)
45-
stats_dispatcher = mock_app.MockDispatcher(
46-
qapp, api_method='admin.vm.Stats')
47-
48-
# continues as normal
49-
50-
To run a mocked program, remember to extend pythonpath appropriately, z.B.:
36+
>>> def main():
37+
... ''' main function '''
38+
... # qapp = qubesadmin.Qubes()
39+
... # dispatcher = qubesadmin.events.EventsDispatcher(qapp)
40+
... # stats_dispatcher = qubesadmin.events.EventsDispatcher(
41+
... # qapp, api_method='admin.vm.Stats'
42+
... # )
43+
...
44+
>>> import qubesadmin.tests.mock_app as mock_app
45+
... qapp = mock_app.MockQubesComplete()
46+
... dispatcher = mock_app.MockDispatcher(qapp)
47+
... stats_dispatcher = mock_app.MockDispatcher(
48+
... qapp, api_method='admin.vm.Stats')
49+
...
50+
>>> # continues as normal
51+
52+
To run a mocked program, remember to extend PYTHONPATH appropriately, z.B.:
5153
PYTHONPATH=../core-admin-client:. python3 qui/tray/domains.py
5254
5355
To collect information to modify this script, you can use the wrapper function

qubesadmin/tests/tools/qvm_remove.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,73 @@ def test_100_dependencies(self, mock_dependencies):
4545
self.app.expected_calls[
4646
('some-vm', 'admin.vm.Remove', None, None)] = \
4747
b'2\x00QubesVMInUseError\x00\x00An error occurred\x00'
48+
self.app.expected_calls[
49+
('some-vm', 'admin.vm.property.Get', 'is_preload', None)] = \
50+
b'2\x00QubesNoSuchPropertyError\x00\x00Invalid property\x00'
4851

4952
mock_dependencies.return_value = \
5053
[(None, 'default_template'), (self.app.domains['some-vm'], 'netvm')]
51-
5254
with self.assertRaises(SystemExit):
5355
qubesadmin.tools.qvm_remove.main(['-f', 'some-vm'], app=self.app)
56+
mock_dependencies.assert_called_once()
57+
58+
@unittest.mock.patch('qubesadmin.utils.vm_dependencies')
59+
def test_101_dvm_dependencies(self, mock_dependencies):
60+
self.app.expected_calls[
61+
('dom0', 'admin.vm.List', None, None)] = \
62+
b'0\x00some-template class=TemplateVM state=Halted\n' \
63+
b'some-vm class=AppVM state=Running\n' \
64+
b'some-dvm class=AppVM state=Running\n' \
65+
b'some-disp class=DispVM state=Running\n' \
66+
b'some-disp2 class=DispVM state=Running\n'
67+
self.app.expected_calls[
68+
('some-dvm', 'admin.vm.Remove', None, None)] = \
69+
b'2\x00QubesVMInUseError\x00\x00An error occurred\x00'
70+
self.app.expected_calls[
71+
('some-vm', 'admin.vm.property.Get', 'is_preload', None)] = \
72+
b'2\x00QubesNoSuchPropertyError\x00\x00Invalid property\x00'
73+
74+
# Cannot remove while it is default_dispvm of the system.
75+
mock_dependencies.return_value = \
76+
[(None, 'default_dispvm')]
77+
with self.assertRaises(SystemExit):
78+
qubesadmin.tools.qvm_remove.main(['-f', 'some-dvm'], app=self.app)
79+
mock_dependencies.assert_called_once()
80+
mock_dependencies.reset_mock()
5481

55-
self.assertTrue(mock_dependencies.called,
56-
"Dependencies check not called.")
82+
# Cannot remove while it is default_dispvm of a qube.
83+
mock_dependencies.return_value = \
84+
[(self.app.domains['some-vm'], 'default_dispvm')]
85+
with self.assertRaises(SystemExit):
86+
qubesadmin.tools.qvm_remove.main(['-f', 'some-dvm'], app=self.app)
87+
mock_dependencies.assert_called_once()
88+
mock_dependencies.reset_mock()
89+
90+
# Cannot remove while it is default_dispvm of its own disposable.
91+
self.app.expected_calls[
92+
('some-disp', 'admin.vm.property.Get', 'is_preload', None)] = \
93+
b'0\x00default=False type=bool False'
94+
mock_dependencies.return_value = \
95+
[(self.app.domains['some-disp'], 'default_dispvm')]
96+
with self.assertRaises(SystemExit):
97+
qubesadmin.tools.qvm_remove.main(['-f', 'some-dvm'], app=self.app)
98+
mock_dependencies.assert_called_once()
99+
mock_dependencies.reset_mock()
100+
101+
# Cannot remove while it is template of non preload.
102+
self.app.expected_calls[
103+
('some-disp', 'admin.vm.property.Get', 'is_preload', None)] = \
104+
b'0\x00default=False type=bool True'
105+
self.app.expected_calls[
106+
('some-disp2', 'admin.vm.property.Get', 'is_preload', None)] = \
107+
b'0\x00default=False type=bool False'
108+
mock_dependencies.return_value = [
109+
(self.app.domains['some-disp'], 'template'),
110+
(self.app.domains['some-disp2'], 'template')
111+
]
112+
with self.assertRaises(SystemExit):
113+
qubesadmin.tools.qvm_remove.main(['-f', 'some-dvm'], app=self.app)
114+
mock_dependencies.assert_called_once()
115+
mock_dependencies.reset_mock()
116+
117+
self.assertAllCalled()

qubesadmin/tools/qvm_remove.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,39 +19,48 @@
1919
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
2020
#
2121

22-
''' Remove domains from the system '''
22+
"""Remove domains from the system"""
2323

2424
import sys
2525

2626
import qubesadmin.exc
2727
from qubesadmin.tools import QubesArgumentParser
2828
import qubesadmin.utils
2929

30-
parser = QubesArgumentParser(description=__doc__,
31-
vmname_nargs='+')
32-
parser.add_argument("--force", "-f", action="store_true", dest="no_confirm",
33-
default=False, help="Do not prompt for confirmation")
34-
30+
parser = QubesArgumentParser(description=__doc__, vmname_nargs="+")
31+
parser.add_argument(
32+
"--force",
33+
"-f",
34+
action="store_true",
35+
dest="no_confirm",
36+
default=False,
37+
help="Do not prompt for confirmation",
38+
)
3539

3640

3741
def main(args=None, app=None): # pylint: disable=missing-docstring
3842
args = parser.parse_args(args, app=app)
3943
go_ahead = ""
4044

4145
if "dom0" in args.domains:
42-
print("Domain 'dom0' cannot be removed.")
46+
print("Domain 'dom0' cannot be removed.", file=sys.stderr)
4347
return 1
4448

4549
if args.all_domains and not (args.no_confirm or args.exclude):
46-
print("WARNING!!! Removing all domains may leave your system in an "
47-
"unrecoverable state!")
50+
print(
51+
"WARNING!!! Removing all domains may leave your system in an "
52+
"unrecoverable state!",
53+
file=sys.stderr,
54+
)
4855
go_ahead_remove_all = input("Are you certain? [N/IKNOWWHATIAMDOING]")
4956
if not go_ahead_remove_all == "IKNOWWHATIAMDOING":
50-
print("Remove cancelled.")
57+
print("Remove cancelled.", file=sys.stderr)
5158
return 1
5259

5360
if not args.no_confirm:
54-
print("This will completely remove the selected VM(s)...")
61+
print(
62+
"This will completely remove the selected VM(s)...", file=sys.stderr
63+
)
5564
for vm in args.domains:
5665
print(" ", vm.name)
5766
go_ahead = input("Are you sure? [y/N] ").upper()
@@ -62,25 +71,49 @@ def main(args=None, app=None): # pylint: disable=missing-docstring
6271
del args.app.domains[vm.name]
6372
except qubesadmin.exc.QubesVMInUseError as e:
6473
dependencies = qubesadmin.utils.vm_dependencies(vm.app, vm)
74+
# Check qubes.app::domain-pre-delete for logic.
75+
preloads = [
76+
(holder, prop)
77+
for holder, prop in dependencies
78+
if holder
79+
and getattr(holder, "is_preload", False)
80+
and (
81+
prop == "template"
82+
or (
83+
prop == "default_dispvm"
84+
and getattr(holder, "template", None) == vm
85+
)
86+
)
87+
]
88+
dependencies = list(set(dependencies) - set(preloads))
6589
if dependencies:
66-
print("VM {} cannot be removed. It is in use as:".format(
67-
vm.name))
68-
for (holder, prop) in dependencies:
90+
print(
91+
"VM {} cannot be removed. It is in use as:".format(
92+
vm.name
93+
),
94+
file=sys.stderr,
95+
)
96+
for holder, prop in dependencies:
6997
if holder:
70-
print(" - {} for {}".format(prop, holder.name))
98+
print(
99+
" - {} for {}".format(prop, holder.name),
100+
file=sys.stderr,
101+
)
71102
else:
72-
print(" - global property {}".format(prop))
103+
print(
104+
" - global property {}".format(prop),
105+
file=sys.stderr,
106+
)
73107
# Display the original message as well
74108
parser.error_runtime(e)
75109
except qubesadmin.exc.QubesException as e:
76110
parser.error_runtime(e)
77111
retcode = 0
78112
else:
79-
print("Remove cancelled.")
113+
print("Remove cancelled.", file=sys.stderr)
80114
retcode = 1
81115
return retcode
82116

83117

84-
85-
if __name__ == '__main__':
118+
if __name__ == "__main__":
86119
sys.exit(main())

0 commit comments

Comments
 (0)