Skip to content

Commit 0e5e34c

Browse files
committed
Merge remote-tracking branch 'origin/pr/170'
* origin/pr/170: qubes-vm-update: updates standalones too qubes-vm-update: add CLI options
2 parents 3e63050 + 4dd881e commit 0e5e34c

File tree

6 files changed

+229
-11
lines changed

6 files changed

+229
-11
lines changed

qubes_config/tests/conftest.py

+4
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ def add_feature_to_all(qapp, feature_name, enable_vm_names: List[str]):
217217

218218
@pytest.fixture
219219
def test_qapp():
220+
return test_qapp_impl()
221+
222+
223+
def test_qapp_impl():
220224
"""Test QubesApp"""
221225
qapp = QubesTest()
222226
qapp._local_name = 'dom0' # pylint: disable=protected-access

qui/updater/intro_page.py

+42
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,48 @@ def select_rows(self):
175175
row.selected = row.updates_available \
176176
in self.head_checkbox.allowed
177177

178+
def select_rows_ignoring_conditions(self, cliargs):
179+
cmd = ['qubes-vm-update', '--dry-run']
180+
181+
args = [a for a in dir(cliargs) if not a.startswith("_")]
182+
for arg in args:
183+
if arg in ("dom0", "no-restart", "restart", "max_concurrency",
184+
"log"):
185+
continue
186+
value = getattr(cliargs, arg)
187+
if value:
188+
if arg in ("skip", "targets"):
189+
vms = set(value.split(","))
190+
vms_without_dom0 = vms.difference({"dom0"})
191+
if not vms_without_dom0:
192+
continue
193+
value = ",".join(vms_without_dom0)
194+
cmd.extend((f"--{arg.replace('_', '-')}", str(value)))
195+
196+
if not cmd[2:]:
197+
to_update = set()
198+
else:
199+
self.log.debug("Run command %s", " ".join(cmd))
200+
output = subprocess.check_output(cmd)
201+
self.log.debug("Command returns: %s", output.decode())
202+
203+
to_update = {
204+
vm_name.strip()
205+
for line in output.decode().split("\n", maxsplit=1)
206+
for vm_name in line.split(":", maxsplit=1)[1].split(",")
207+
}
208+
209+
# handle dom0
210+
if cliargs.dom0 or cliargs.all:
211+
to_update.add("dom0")
212+
if cliargs.targets and "dom0" in cliargs.targets.split(","):
213+
to_update.add("dom0")
214+
if cliargs.skip and "dom0" in cliargs.skip.split(","):
215+
to_update = to_update.difference({"dom0"})
216+
217+
for row in self.list_store:
218+
row.selected = row.name in to_update
219+
178220

179221
class UpdateRowWrapper(RowWrapper):
180222
COLUMN_NUM = 9

qui/updater/tests/test_intro_page.py

+55
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
# along with this program; if not, write to the Free Software
1919
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
2020
# USA.
21+
import argparse
22+
2123
import pytest
2224
from unittest.mock import patch
2325
from unittest.mock import Mock
2426

27+
from qubes_config.tests.conftest import test_qapp_impl
2528
from qui.updater.intro_page import IntroPage, UpdateRowWrapper, UpdatesAvailable
29+
from qui.updater.updater import parse_args
2630
from qui.updater.utils import ListWrapper, HeaderCheckbox
2731

2832
@patch('subprocess.check_output')
@@ -153,3 +157,54 @@ def test_on_checkbox_toggled(
153157
# no selected row
154158
assert not sut.checkbox_column_button.get_inconsistent()
155159
assert not sut.checkbox_column_button.get_active()
160+
161+
all_domains = {vm.name for vm in test_qapp_impl().domains}
162+
all_templates = {vm.name for vm in test_qapp_impl().domains if vm.klass == "TemplateVM"}
163+
all_standalones = {vm.name for vm in test_qapp_impl().domains if vm.klass == "StandaloneVM"}
164+
165+
@patch('subprocess.check_output')
166+
@pytest.mark.parametrize(
167+
"args, templates, rest, selected",
168+
(
169+
pytest.param(('--all',), ",".join(all_templates).encode(), ",".join(all_domains.difference({"dom0"}).difference(all_templates)).encode(), all_domains),
170+
pytest.param(('--update-if-stale', '10'), b'fedora-36', b'', {'fedora-36'}),
171+
pytest.param(('--targets', 'dom0,fedora-36'), b'fedora-36', b'', {'dom0', 'fedora-36'}),
172+
pytest.param(('--dom0', '--skip', 'dom0'), b'', b'', set()),
173+
pytest.param(('--skip', 'dom0'), b'', b'', set()),
174+
pytest.param(('--targets', 'dom0', '--skip', 'dom0'), b'', b'', set()),
175+
pytest.param(('--dom0',), b'', b'', {'dom0'}),
176+
pytest.param(('--standalones',), b'', ",".join(all_standalones).encode(), all_standalones),
177+
pytest.param(('--templates', '--skip', 'fedora-36,garbage-name'),
178+
",".join(all_templates.difference({"fedora-36"})).encode(),
179+
b'',
180+
all_templates.difference({"fedora-36"})),
181+
),
182+
)
183+
def test_select_rows_ignoring_conditions(
184+
mock_subprocess, args, templates, rest, selected, real_builder, test_qapp,
185+
mock_next_button, mock_settings, mock_list_store
186+
):
187+
mock_log = Mock()
188+
sut = IntroPage(real_builder, mock_log, mock_next_button)
189+
190+
# populate_vm_list
191+
sut.list_store = ListWrapper(UpdateRowWrapper, mock_list_store)
192+
for vm in test_qapp.domains:
193+
sut.list_store.append_vm(vm)
194+
195+
assert len(sut.list_store) == 12
196+
197+
mock_subprocess.return_value = (
198+
b'Following templates will be updated: ' + templates + b'\n'
199+
b'Following qubes will be updated: ' + rest
200+
)
201+
202+
cliargs = parse_args(args)
203+
sut.select_rows_ignoring_conditions(cliargs)
204+
to_update = {row.name for row in sut.list_store if row.selected}
205+
206+
assert to_update == selected
207+
if not templates + rest:
208+
mock_subprocess.assert_not_called()
209+
210+

qui/updater/tests/test_updater.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@
2020
# USA.
2121
from unittest.mock import patch, call
2222

23-
from qui.updater.updater import QubesUpdater
23+
from qui.updater.updater import QubesUpdater, parse_args
2424

2525

2626
@patch('logging.FileHandler')
2727
@patch('logging.getLogger')
2828
@patch('qui.updater.intro_page.IntroPage.populate_vm_list')
2929
def test_setup(populate_vm_list, _mock_logging, __mock_logging, test_qapp):
30-
sut = QubesUpdater(test_qapp)
30+
sut = QubesUpdater(test_qapp, parse_args(()))
3131
sut.perform_setup()
3232
calls = [call(sut.qapp, sut.settings)]
3333
populate_vm_list.assert_has_calls(calls)

qui/updater/updater.py

+95-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
33
# pylint: disable=wrong-import-position,import-error
4+
import argparse
45
import logging
56
import time
67

@@ -10,7 +11,7 @@
1011
from qubes_config.widgets.gtk_utils import load_icon_at_gtk_size, load_theme, \
1112
show_dialog_with_icon, RESPONSES_OK
1213
from qui.updater.progress_page import ProgressPage
13-
from qui.updater.updater_settings import Settings
14+
from qui.updater.updater_settings import Settings, OverridenSettings
1415
from qui.updater.summary_page import SummaryPage
1516
from qui.updater.intro_page import IntroPage
1617

@@ -33,14 +34,15 @@ class QubesUpdater(Gtk.Application):
3334
LOGPATH = '/var/log/qubes/qui.updater.log'
3435
LOG_FORMAT = '%(asctime)s %(message)s'
3536

36-
def __init__(self, qapp):
37+
def __init__(self, qapp, cliargs):
3738
super().__init__(
3839
application_id="org.gnome.example",
3940
flags=Gio.ApplicationFlags.FLAGS_NONE
4041
)
4142
self.qapp = qapp
4243
self.primary = False
4344
self.connect("activate", self.do_activate)
45+
self.cliargs = cliargs
4446

4547
log_handler = logging.FileHandler(
4648
QubesUpdater.LOGPATH, encoding='utf-8')
@@ -49,13 +51,15 @@ def __init__(self, qapp):
4951

5052
self.log = logging.getLogger('vm-update.agent.PackageManager')
5153
self.log.addHandler(log_handler)
52-
self.log.setLevel("DEBUG")
54+
self.log.setLevel(self.cliargs.log)
5355

5456
def do_activate(self, *_args, **_kwargs):
5557
if not self.primary:
5658
self.perform_setup()
5759
self.primary = True
5860
self.hold()
61+
elif len(self.intro_page.get_vms_to_update()) == 0:
62+
self.exit_updater()
5963
else:
6064
self.main_window.present()
6165

@@ -105,11 +109,25 @@ def perform_setup(self, *_args, **_kwargs):
105109
'qubes-customize', Gtk.IconSize.LARGE_TOOLBAR)
106110
settings_image = Gtk.Image.new_from_pixbuf(settings_pixbuf)
107111
self.button_settings.set_image(settings_image)
112+
113+
overriden_restart = None
114+
if self.cliargs.restart:
115+
overriden_restart = True
116+
elif self.cliargs.no_restart:
117+
overriden_restart = False
118+
119+
overrides = OverridenSettings(
120+
restart=overriden_restart,
121+
max_concurrency=self.cliargs.max_concurrency,
122+
update_if_stale=self.cliargs.update_if_stale,
123+
)
124+
108125
self.settings = Settings(
109126
self.main_window,
110127
self.qapp,
111128
self.log,
112-
refresh_callback=self.intro_page.refresh_update_list
129+
refresh_callback=self.intro_page.refresh_update_list,
130+
overrides=overrides,
113131
)
114132

115133
headers = [(3, "intro_name"), (3, "progress_name"), (3, "summary_name"),
@@ -137,6 +155,23 @@ def cell_data_func(_column, cell, model, it, data):
137155
self.main_window.connect("key-press-event", self.check_escape)
138156

139157
self.intro_page.populate_vm_list(self.qapp, self.settings)
158+
159+
if skip_intro_if_args(self.cliargs):
160+
self.log.info("Skipping intro page.")
161+
self.intro_page.select_rows_ignoring_conditions(
162+
cliargs=self.cliargs)
163+
if len(self.intro_page.get_vms_to_update()) == 0:
164+
show_dialog_with_icon(
165+
None, l("Quit"),
166+
l("Nothing to do."),
167+
buttons=RESPONSES_OK,
168+
icon_name="qubes-info"
169+
)
170+
self.main_window.close()
171+
return
172+
self.next_clicked(None, skip_intro=True)
173+
else:
174+
self.log.info("Show intro page.")
140175
self.main_window.show_all()
141176
width = self.intro_page.vm_list.get_preferred_width().natural_width
142177
self.main_window.resize(width + 50, int(width * 1.2))
@@ -145,9 +180,9 @@ def cell_data_func(_column, cell, model, it, data):
145180
def open_settings_window(self, _emitter):
146181
self.settings.show()
147182

148-
def next_clicked(self, _emitter):
183+
def next_clicked(self, _emitter, skip_intro=False):
149184
self.log.debug("Next clicked")
150-
if self.intro_page.is_visible:
185+
if self.intro_page.is_visible or skip_intro:
151186
vms_to_update = self.intro_page.get_vms_to_update()
152187
self.intro_page.active = False
153188
self.progress_page.show()
@@ -213,9 +248,61 @@ def exit_updater(self, _emitter=None):
213248
self.release()
214249

215250

216-
def main():
251+
def parse_args(args):
252+
parser = argparse.ArgumentParser()
253+
254+
parser.add_argument('--log', action='store', default='WARNING',
255+
help='Provide logging level. Values: DEBUG, INFO, '
256+
'WARNING (default), ERROR, CRITICAL')
257+
parser.add_argument('--max-concurrency', action='store',
258+
help='Maximum number of VMs configured simultaneously '
259+
'(default: number of cpus)',
260+
type=int)
261+
restart_gr = parser.add_mutually_exclusive_group()
262+
restart_gr.add_argument('--restart', action='store_true',
263+
help='Restart AppVMs whose template '
264+
'has been updated.')
265+
restart_gr.add_argument('--no-restart', action='store_true',
266+
help='Do not restart AppVMs whose template '
267+
'has been updated.')
268+
269+
group = parser.add_mutually_exclusive_group()
270+
group.add_argument('--targets', action='store',
271+
help='Comma separated list of VMs to target')
272+
group.add_argument('--all', action='store_true',
273+
help='Target all non-disposable VMs (TemplateVMs and '
274+
'AppVMs)')
275+
group.add_argument('--update-if-stale', action='store',
276+
help='Target all TemplateVMs with known updates or for '
277+
'which last update check was more than N days '
278+
'ago.',
279+
type=int)
280+
281+
parser.add_argument('--skip', action='store',
282+
help='Comma separated list of VMs to be skipped, '
283+
'works with all other options.', default="")
284+
parser.add_argument('--templates', action='store_true',
285+
help='Target all TemplatesVMs')
286+
parser.add_argument('--standalones', action='store_true',
287+
help='Target all StandaloneVMs')
288+
parser.add_argument('--dom0', action='store_true',
289+
help='Target dom0')
290+
291+
args = parser.parse_args(args)
292+
293+
return args
294+
295+
296+
def skip_intro_if_args(args):
297+
return args is not None and (args.templates or args.standalones or args.skip
298+
or args.update_if_stale or args.all
299+
or args.targets or args.dom0)
300+
301+
302+
def main(args=None):
303+
cliargs = parse_args(args)
217304
qapp = Qubes()
218-
app = QubesUpdater(qapp)
305+
app = QubesUpdater(qapp, cliargs)
219306
app.run()
220307

221308

0 commit comments

Comments
 (0)