diff --git a/launcher/sdw_updater_gui/Updater.py b/launcher/sdw_updater_gui/Updater.py index 5d940ff0b..829166903 100644 --- a/launcher/sdw_updater_gui/Updater.py +++ b/launcher/sdw_updater_gui/Updater.py @@ -45,37 +45,9 @@ def get_dom0_path(folder): return os.path.join(os.path.expanduser("~"), folder) -def check_all_updates(): +def apply_updates(vms=current_templates.keys()): """ - Check for updates for all vms listed in current_templates above - """ - - sdlog.info("Checking for all updates") - - for progress_current, vm in enumerate(current_templates.keys()): - # yield the progress percentage for UI updates - progress_percentage = int( - ((progress_current + 1) / len(current_templates.keys())) * 100 - ) - update_results = check_updates(vm) - yield vm, progress_percentage, update_results - - -def check_updates(vm): - """ - Check for updates for a single VM - """ - if vm == "dom0": - return _check_updates_dom0() - elif vm == "fedora": - return _check_updates_fedora() - else: - return _check_updates_debian(vm) - - -def apply_updates(vms): - """ - Apply updates to the TemplateVMs of VM list specified in parameter + Apply updates to all TemplateVMs """ sdlog.info("Applying all updates") @@ -91,70 +63,6 @@ def apply_updates(vms): yield vm, progress_percentage, upgrade_results -def _check_updates_dom0(): - """ - Check for dom0 updates - """ - try: - subprocess.check_call(["sudo", "qubes-dom0-update", "--check-only"]) - except subprocess.CalledProcessError as e: - sdlog.error("dom0 updates required or cannot check for updates") - sdlog.error(str(e)) - return UpdateStatus.UPDATES_REQUIRED - - sdlog.info("dom0 is up to date") - return UpdateStatus.UPDATES_OK - - -def _check_updates_fedora(): - """ - Check for updates to the default Fedora TemplateVM. Fedora has a very rapid - release cycle and there are almost always updates to fedora VMs. Let's just - return UPDATES_REQUIRED and always upgrade those VMs, since they no longer - trigger a full workstation reboot on upgrade. - """ - return UpdateStatus.UPDATES_REQUIRED - - -def _check_updates_debian(vm): - """ - Check for updates for a given Debian-based TemplateVM - """ - updates_required = False - try: - # There is no apt command that uses exit codes in such a way that we can discover if - # updates are required by relying on exit codes. - # Since we don't want to use --pass-io and parse the output, we have to count - # the lines on the vm output - sdlog.info("Checking for updates {}:{}".format(vm, current_templates[vm])) - subprocess.check_call(["qvm-run", current_templates[vm], "sudo apt update"]) - subprocess.check_call( - [ - "qvm-run", - current_templates[vm], - "[[ $(apt list --upgradable | wc -l) -eq 1 ]]", - ] - ) - except subprocess.CalledProcessError as e: - sdlog.error( - "Updates required for {} or cannot check for updates".format( - current_templates[vm] - ) - ) - sdlog.error(str(e)) - updates_required = True - finally: - reboot_status = _safely_shutdown_vm(current_templates[vm]) - if reboot_status == UpdateStatus.UPDATES_FAILED: - return reboot_status - - if not updates_required: - sdlog.info("{} is up to date".format(current_templates[vm])) - return UpdateStatus.UPDATES_OK - else: - return UpdateStatus.UPDATES_REQUIRED - - def _apply_updates_dom0(): """ Apply updates to dom0. Any update to dom0 will require a reboot after @@ -162,15 +70,22 @@ def _apply_updates_dom0(): """ sdlog.info("Updating dom0") try: - subprocess.check_call(["sudo", "qubes-dom0-update", "-y"]) + output = subprocess.check_output(["sudo", "qubes-dom0-update", "-y"]).decode( + "utf-8" + ) except subprocess.CalledProcessError as e: sdlog.error( "An error has occurred updating dom0. Please contact your administrator." ) sdlog.error(str(e)) return UpdateStatus.UPDATES_FAILED - sdlog.info("dom0 update successful") - return UpdateStatus.REBOOT_REQUIRED + + if output.find("No packages downloaded") != -1: + sdlog.info("No dom0 updates available, no reboot needed.") + return UpdateStatus.UPDATES_OK + else: + sdlog.info("dom0 updates have been applied and a reboot is required.") + return UpdateStatus.REBOOT_REQUIRED def _apply_updates_vm(vm): diff --git a/launcher/sdw_updater_gui/UpdaterApp.py b/launcher/sdw_updater_gui/UpdaterApp.py index 88cabbc0c..6bbf74f8f 100644 --- a/launcher/sdw_updater_gui/UpdaterApp.py +++ b/launcher/sdw_updater_gui/UpdaterApp.py @@ -30,96 +30,34 @@ def __init__(self, parent=None): self.progress = 0 self.setupUi(self) + + # We use a single dialog with button visibility toggled at different + # stages. In the first stage, we only show the "Start Updates" and + # "Cancel" buttons. + + self.applyUpdatesButton.setEnabled(True) + self.applyUpdatesButton.show() + self.applyUpdatesButton.clicked.connect(self.apply_all_updates) + + self.cancelButton.setEnabled(True) + self.cancelButton.show() + self.cancelButton.clicked.connect(self.exit_launcher) + self.clientOpenButton.setEnabled(False) self.clientOpenButton.hide() self.clientOpenButton.clicked.connect(launch_securedrop_client) + self.rebootButton.setEnabled(False) self.rebootButton.hide() self.rebootButton.clicked.connect(self.reboot_workstation) - self.applyUpdatesButton.setEnabled(False) - self.applyUpdatesButton.hide() - self.applyUpdatesButton.clicked.connect(self.apply_all_updates) - - self.cancelButton.setEnabled(False) - self.cancelButton.hide() - self.cancelButton.clicked.connect(self.exit_launcher) self.show() - self.proposedActionDescription.setText( - strings.description_status_checking_updates - ) + self.proposedActionDescription.setText(strings.description_introduction) self.progress += 1 self.progressBar.setProperty("value", self.progress) - - self.vms_to_update = [] - - logger.info("Starting UpdateThread") - self.update_thread = UpdateThread() - self.update_thread.update_signal.connect(self.update_status) - self.update_thread.progress_signal.connect(self.update_progress_bar) - self.update_thread.start() - - @pyqtSlot(dict) - def update_status(self, result): - """ - This slot will receive update signals from UpdateThread, thread which - is used to check for TemplateVM updates - """ - logger.info("Signal: update_status {}".format(str(result))) - self.progress = 100 - self.progressBar.setProperty("value", self.progress) - - if result["recommended_action"] == UpdateStatus.UPDATES_REQUIRED: - logger.info("Updates required") - self.vms_to_update = self.get_vms_that_need_upgrades(result) - self.applyUpdatesButton.setEnabled(True) - self.applyUpdatesButton.show() - self.cancelButton.setEnabled(True) - self.cancelButton.show() - self.proposedActionDescription.setText( - strings.description_status_updates_available - ) - elif result["recommended_action"] == UpdateStatus.UPDATES_OK: - logger.info("VMs up-to-date, OK to start client") - self.clientOpenButton.setEnabled(True) - self.clientOpenButton.show() - self.cancelButton.setEnabled(True) - self.cancelButton.show() - self.proposedActionDescription.setText( - strings.description_status_up_to_date - ) - elif result["recommended_action"] == UpdateStatus.REBOOT_REQUIRED: - logger.info("Reboot will be required") - # We also have further updates to do, let's apply updates and reboot - # once those are done - if len(self.get_vms_that_need_upgrades(result)) > 0: - logger.info("Reboot will be after applying upgrades") - self.vms_to_update = self.get_vms_that_need_upgrades(result) - self.applyUpdatesButton.setEnabled(True) - self.applyUpdatesButton.show() - self.cancelButton.setEnabled(True) - self.cancelButton.show() - self.proposedActionDescription.setText( - strings.description_status_updates_available - ) - # No updates required, show reboot button. - else: - logger.info("Reboot required") - self.rebootButton.setEnabled(True) - self.rebootButton.show() - self.cancelButton.setEnabled(True) - self.cancelButton.show() - self.proposedActionDescription.setText( - strings.description_status_reboot_required - ) - else: - logger.error("Error checking for updates") - logger.error(str(result)) - self.proposedActionDescription.setText( - strings.description_error_check_updates_failed - ) + self.progressBar.hide() @pyqtSlot(dict) def upgrade_status(self, result): @@ -160,9 +98,9 @@ def upgrade_status(self, result): @pyqtSlot(int) def update_progress_bar(self, value): """ - This slot will receive updates from UpdateThread and UpgradeThread which - will provide a int representing the percentage of the progressBar. This - slot will update the progressBar value once it receives the signal. + This slot will receive updates from UpgradeThread which will provide a + int representing the percentage of the progressBar. This slot will + update the progressBar value once it receives the signal. """ current_progress = int(value) if current_progress <= 0: @@ -174,18 +112,6 @@ def update_progress_bar(self, value): self.progress = current_progress self.progressBar.setProperty("value", self.progress) - def get_vms_that_need_upgrades(self, results): - """ - Helper method that returns a list of VMs that need upgrades based - on the results returned by the UpdateThread - """ - vms_to_upgrade = [] - for vm in results.keys(): - if vm != "recommended_action": # ignore this higher_level key - if results[vm] == UpdateStatus.UPDATES_REQUIRED: - vms_to_upgrade.append(vm) - return vms_to_upgrade - def apply_all_updates(self): """ Method used by the applyUpdatesButton that will create and start an @@ -194,15 +120,14 @@ def apply_all_updates(self): logger.info("Starting UpgradeThread") self.progress = 5 self.progressBar.setProperty("value", self.progress) + self.progressBar.show() self.proposedActionDescription.setText( strings.description_status_applying_updates ) self.applyUpdatesButton.setEnabled(False) self.applyUpdatesButton.hide() self.cancelButton.setEnabled(False) - self.cancelButton.hide() - # Create thread with list of VMs to update - self.upgrade_thread = UpgradeThread(self.vms_to_update) + self.upgrade_thread = UpgradeThread() self.upgrade_thread.start() self.upgrade_thread.upgrade_signal.connect(self.upgrade_status) self.upgrade_thread.progress_signal.connect(self.update_progress_bar) @@ -227,39 +152,6 @@ def exit_launcher(self): sys.exit() -class UpdateThread(QThread): - """ - This thread will check for TemplateVM updates - """ - - update_signal = pyqtSignal("PyQt_PyObject") - progress_signal = pyqtSignal("int") - - def __init__(self): - QThread.__init__(self) - - def run(self): - update_generator = Updater.check_all_updates() - results = {} - - for vm, progress, result in update_generator: - results[vm] = result - self.progress_signal.emit(progress) - - run_results = Updater.overall_update_status(results) - Updater._write_updates_status_flag_to_disk(run_results) - # Write the "last updated" date to disk if the system is up-to-date. - # Even though no updates have been newly applied at this stage, for the - # purposes of security checks, all we need to know is that the system is - # up-to-date as of this run. - if run_results == UpdateStatus.UPDATES_OK: - Updater._write_last_updated_flags_to_disk() - # populate signal contents - message = results # copy all the information from results - message["recommended_action"] = run_results - self.update_signal.emit(message) - - class UpgradeThread(QThread): """ This thread will apply updates for TemplateVMs based on the VM list @@ -268,14 +160,12 @@ class UpgradeThread(QThread): upgrade_signal = pyqtSignal("PyQt_PyObject") progress_signal = pyqtSignal("int") - vms_to_upgrade = [] - def __init__(self, vms): + def __init__(self): QThread.__init__(self) - self.vms_to_upgrade = vms def run(self): - upgrade_generator = Updater.apply_updates(self.vms_to_upgrade) + upgrade_generator = Updater.apply_updates() results = {} for vm, progress, result in upgrade_generator: diff --git a/launcher/sdw_updater_gui/UpdaterAppUi.py b/launcher/sdw_updater_gui/UpdaterAppUi.py index 1efe27bf9..4ed651759 100644 --- a/launcher/sdw_updater_gui/UpdaterAppUi.py +++ b/launcher/sdw_updater_gui/UpdaterAppUi.py @@ -52,27 +52,17 @@ def setupUi(self, UpdaterDialog): self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) self.gridLayout = QtGui.QGridLayout(self.layoutWidget) self.gridLayout.setContentsMargins(0, 35, 0, 15) + self.gridLayout.setHorizontalSpacing(3) self.gridLayout.setObjectName(_fromUtf8("gridLayout")) - spacerItem = QtGui.QSpacerItem( - 40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum - ) - self.gridLayout.addItem(spacerItem, 5, 0, 1, 1) - self.rebootButton = QtGui.QPushButton(self.layoutWidget) - self.rebootButton.setObjectName(_fromUtf8("rebootButton")) - self.gridLayout.addWidget(self.rebootButton, 5, 2, 1, 1) - self.applyUpdatesButton = QtGui.QPushButton(self.layoutWidget) - self.applyUpdatesButton.setObjectName(_fromUtf8("applyUpdatesButton")) - self.gridLayout.addWidget(self.applyUpdatesButton, 5, 1, 1, 1) - self.cancelButton = QtGui.QPushButton(self.layoutWidget) - self.cancelButton.setObjectName(_fromUtf8("cancelButton")) - self.gridLayout.addWidget(self.cancelButton, 5, 4, 1, 1) self.clientOpenButton = QtGui.QPushButton(self.layoutWidget) + self.clientOpenButton.setStyleSheet(_fromUtf8("")) + self.clientOpenButton.setAutoDefault(True) self.clientOpenButton.setObjectName(_fromUtf8("clientOpenButton")) - self.gridLayout.addWidget(self.clientOpenButton, 5, 3, 1, 1) + self.gridLayout.addWidget(self.clientOpenButton, 6, 4, 1, 1) self.progressBar = QtGui.QProgressBar(self.layoutWidget) self.progressBar.setProperty("value", 0) self.progressBar.setObjectName(_fromUtf8("progressBar")) - self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 5) + self.gridLayout.addWidget(self.progressBar, 1, 1, 1, 5) self.proposedActionDescription = QtGui.QLabel(self.layoutWidget) self.proposedActionDescription.setAlignment( QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop @@ -81,13 +71,33 @@ def setupUi(self, UpdaterDialog): self.proposedActionDescription.setObjectName( _fromUtf8("proposedActionDescription") ) - self.gridLayout.addWidget(self.proposedActionDescription, 3, 0, 1, 5) + self.gridLayout.addWidget(self.proposedActionDescription, 3, 1, 1, 5) self.label = QtGui.QLabel(self.layoutWidget) self.label.setMinimumSize(QtCore.QSize(0, 20)) self.label.setMaximumSize(QtCore.QSize(16777215, 20)) self.label.setText(_fromUtf8("")) self.label.setObjectName(_fromUtf8("label")) - self.gridLayout.addWidget(self.label, 2, 0, 1, 5) + self.gridLayout.addWidget(self.label, 2, 1, 1, 5) + spacerItem = QtGui.QSpacerItem( + 40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum + ) + self.gridLayout.addItem(spacerItem, 6, 1, 1, 1) + self.rebootButton = QtGui.QPushButton(self.layoutWidget) + self.rebootButton.setStyleSheet(_fromUtf8("")) + self.rebootButton.setAutoDefault(True) + self.rebootButton.setObjectName(_fromUtf8("rebootButton")) + self.gridLayout.addWidget(self.rebootButton, 6, 3, 1, 1) + self.applyUpdatesButton = QtGui.QPushButton(self.layoutWidget) + self.applyUpdatesButton.setStyleSheet(_fromUtf8("")) + self.applyUpdatesButton.setAutoDefault(True) + self.applyUpdatesButton.setDefault(False) + self.applyUpdatesButton.setObjectName(_fromUtf8("applyUpdatesButton")) + self.gridLayout.addWidget(self.applyUpdatesButton, 6, 2, 1, 1) + self.cancelButton = QtGui.QPushButton(self.layoutWidget) + self.cancelButton.setStyleSheet(_fromUtf8("")) + self.cancelButton.setAutoDefault(True) + self.cancelButton.setObjectName(_fromUtf8("cancelButton")) + self.gridLayout.addWidget(self.cancelButton, 6, 5, 1, 1) self.retranslateUi(UpdaterDialog) QtCore.QMetaObject.connectSlotsByName(UpdaterDialog) @@ -98,12 +108,12 @@ def retranslateUi(self, UpdaterDialog): "UpdaterDialog", "SecureDrop Workstation preflight updater", None ) ) + self.clientOpenButton.setText(_translate("UpdaterDialog", "Continue", None)) + self.proposedActionDescription.setText( + _translate("UpdaterDialog", "Description goes here", None) + ) self.rebootButton.setText(_translate("UpdaterDialog", "Reboot", None)) self.applyUpdatesButton.setText( _translate("UpdaterDialog", "Start Updates", None) ) self.cancelButton.setText(_translate("UpdaterDialog", "Cancel", None)) - self.clientOpenButton.setText(_translate("UpdaterDialog", "Continue", None)) - self.proposedActionDescription.setText( - _translate("UpdaterDialog", "Description goes here", None) - ) diff --git a/launcher/sdw_updater_gui/sdw_updater.ui b/launcher/sdw_updater_gui/sdw_updater.ui index 27df05ccc..c696551ff 100644 --- a/launcher/sdw_updater_gui/sdw_updater.ui +++ b/launcher/sdw_updater_gui/sdw_updater.ui @@ -65,55 +65,30 @@ 15 - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Reboot - - - - - - - Start Updates - - - - - - - Cancel - - - - + + 3 + + + + + Continue + + true + - + 0 - + Description goes here @@ -126,7 +101,7 @@ - + @@ -145,6 +120,61 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Reboot + + + true + + + + + + + + + + Start Updates + + + true + + + false + + + + + + + + + + Cancel + + + true + + + diff --git a/launcher/sdw_updater_gui/strings.py b/launcher/sdw_updater_gui/strings.py index a73e54f7e..937f10a4f 100644 --- a/launcher/sdw_updater_gui/strings.py +++ b/launcher/sdw_updater_gui/strings.py @@ -1,28 +1,14 @@ -# checking for updates -description_status_checking_updates = ( - "

Checking for updates...

This should take around 3 to 5 minutes.

" -) - -# Results of update search -description_status_updates_available = ( - "

Security updates required.

Before you can safely use the SecureDrop " - "app, Qubes needs to download and install critical security updates.

" - "

Updates should take around 5 to 10 minutes. The computer may need to be restarted " - "after updates are complete.

" - "

Network access will be briefly interrupted as VMs are rebooting.

" - "

Any interruption in this process may break Workstation components.

" - "

Please close this window if now is a bad time, or click Start Updates " +description_introduction = ( + "

Security updates required

Before you can safely use the SecureDrop " + "app, we need to download and install any available security updates. " + "This typically takes between 5 and 30 minutes.

" + "

Any interruption of the update process may break Workstation components.

" + "

As part of the update process, system VMs will be restarted, and you will briefly " + "lose network access." + "

Please click Cancel if now is a bad time, or click Start Updates " "to continue.

" ) -description_status_up_to_date = ( - "

No updates today!

" - "

Click Continue to launch the SecureDrop app.

" -) -description_error_check_updates_failed = ( - "

Cannot check for updates

There was an error retrieving updates. " - "Please check your Internet connection. If this problem persists, " - "please contact your administrator." -) + # Applying updates description_status_applying_updates = ( "

Downloading and installing updates...

" diff --git a/launcher/tests/test_updater.py b/launcher/tests/test_updater.py index ee0b5c7d3..93048086f 100644 --- a/launcher/tests/test_updater.py +++ b/launcher/tests/test_updater.py @@ -61,224 +61,6 @@ def test_updater_vms_present(): assert len(updater.current_templates) == 9 -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_updates_fedora_always_needs_updates(mocked_info, mocked_error): - status = updater._check_updates_fedora() - assert status == UpdateStatus.UPDATES_REQUIRED - assert not mocked_info.called - assert not mocked_error.called - - -@mock.patch("subprocess.check_call", return_value=0) -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_updates_dom0_up_to_date(mocked_info, mocked_error, mocked_call, capsys): - status = updater._check_updates_dom0() - assert status == UpdateStatus.UPDATES_OK - mocked_info.assert_called_once_with("dom0 is up to date") - assert not mocked_error.called - - -@mock.patch( - "subprocess.check_call", side_effect=subprocess.CalledProcessError(1, "check_call") -) -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_updates_dom0_needs_updates( - mocked_info, mocked_error, mocked_call, capsys -): - status = updater._check_updates_dom0() - assert status == UpdateStatus.UPDATES_REQUIRED - error_log = [ - call("dom0 updates required or cannot check for updates"), - call("Command 'check_call' returned non-zero exit status 1."), - ] - mocked_error.assert_has_calls(error_log) - assert not mocked_info.called - - -@mock.patch("subprocess.check_output", return_value="0") -@mock.patch("subprocess.check_call", return_value=0) -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_debian_updates_up_to_date( - mocked_info, mocked_error, mocked_call, mocked_output, capsys -): - status = updater._check_updates_debian("sd-app") - assert status == UpdateStatus.UPDATES_OK - info_log = [ - call("Checking for updates {}:{}".format("sd-app", "sd-app-buster-template")), - call("{} is up to date".format("sd-app-buster-template")), - ] - mocked_info.assert_has_calls(info_log) - assert not mocked_error.called - - -@mock.patch( - "subprocess.check_output", side_effect=["0", "0"], -) -@mock.patch( - "subprocess.check_call", - side_effect=[subprocess.CalledProcessError(1, "check_call"), "0"], -) -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_updates_debian_updates_required( - mocked_info, mocked_error, mocked_call, mocked_output, capsys -): - status = updater._check_updates_debian("sd-app") - assert status == UpdateStatus.UPDATES_REQUIRED - error_log = [ - call( - "Updates required for {} or cannot check for updates".format( - "sd-app-buster-template" - ) - ), - call("Command 'check_call' returned non-zero exit status 1."), - ] - info_log = [ - call("Checking for updates {}:{}".format("sd-app", "sd-app-buster-template")) - ] - mocked_error.assert_has_calls(error_log) - mocked_info.assert_has_calls(info_log) - - -@mock.patch( - "subprocess.check_output", - side_effect=subprocess.CalledProcessError(1, "check_output",), -) -@mock.patch( - "subprocess.check_call", side_effect=subprocess.CalledProcessError(1, "check_call") -) -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_debian_updates_failed( - mocked_info, mocked_error, mocked_call, mocked_output, capsys -): - status = updater._check_updates_debian("sd-app") - assert status == UpdateStatus.UPDATES_FAILED - error_log = [ - call( - "Updates required for {} or cannot check for updates".format( - "sd-app-buster-template" - ) - ), - call("Command 'check_call' returned non-zero exit status 1."), - call("Failed to shut down {}".format("sd-app-buster-template")), - call("Command 'check_output' returned non-zero exit status 1."), - ] - info_log = [ - call("Checking for updates {}:{}".format("sd-app", "sd-app-buster-template")) - ] - mocked_error.assert_has_calls(error_log) - mocked_info.assert_has_calls(info_log) - - -@mock.patch( - "subprocess.check_output", side_effect="0", -) -@mock.patch( - "subprocess.check_call", - side_effect=[subprocess.CalledProcessError(1, "check_call"), "0", "0"], -) -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_debian_has_updates( - mocked_info, mocked_error, mocked_call, mocked_output, capsys -): - error_log = [ - call( - "Updates required for {} or cannot check for updates".format( - "sd-log-buster-template" - ) - ), - call("Command 'check_call' returned non-zero exit status 1."), - ] - info_log = [ - call("Checking for updates {}:{}".format("sd-log", "sd-log-buster-template")) - ] - - status = updater._check_updates_debian("sd-log") - assert status == UpdateStatus.UPDATES_REQUIRED - - mocked_error.assert_has_calls(error_log) - mocked_info.assert_has_calls(info_log) - - -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_updates_fedora_calls_fedora(mocked_info, mocked_error): - status = updater.check_updates("fedora") - assert status == UpdateStatus.UPDATES_REQUIRED - - -@pytest.mark.parametrize("vm", current_templates.keys()) -@mock.patch("subprocess.check_output") -@mock.patch("subprocess.check_call") -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_updates_calls_correct_commands( - mocked_info, mocked_error, mocked_call, mocked_output, vm -): - status = updater.check_updates(vm) - if vm == "fedora": - assert status == UpdateStatus.UPDATES_REQUIRED - else: - assert status == UpdateStatus.UPDATES_OK - - if vm in debian_based_vms: - subprocess_call_list = [ - call(["qvm-run", current_templates[vm], "sudo apt update"]), - call( - [ - "qvm-run", - current_templates[vm], - "[[ $(apt list --upgradable | wc -l) -eq 1 ]]", - ] - ), - ] - check_output_call_list = [ - call(["qvm-shutdown", "--wait", current_templates[vm]], stderr=-1), - ] - elif vm == "dom0": - subprocess_call_list = [call(["sudo", "qubes-dom0-update", "--check-only"])] - check_output_call_list = [] - elif vm == "fedora": - subprocess_call_list = [] - check_output_call_list = [] - else: - pytest.fail("Unupported VM: {}".format(vm)) - - mocked_call.assert_has_calls(subprocess_call_list) - mocked_output.assert_has_calls(check_output_call_list) - assert not mocked_error.called - - -@mock.patch("Updater.check_updates", return_value={"test": UpdateStatus.UPDATES_OK}) -@mock.patch("subprocess.check_call") -@mock.patch("Updater.sdlog.error") -@mock.patch("Updater.sdlog.info") -def test_check_all_updates( - mocked_info, mocked_error, mocked_call, mocked_check_updates -): - - update_generator = updater.check_all_updates() - results = {} - - for vm, progress, result in update_generator: - results[vm] = result - assert progress is not None - results[vm] = result - - check_updates_call_list = [call(x) for x in current_templates.keys()] - mocked_check_updates.assert_has_calls(check_updates_call_list) - - assert not mocked_call.called - assert not mocked_error.called - assert updater.overall_update_status(results) == UpdateStatus.UPDATES_OK - - @mock.patch("Updater._write_updates_status_flag_to_disk") @mock.patch("Updater._write_last_updated_flags_to_disk") @mock.patch("Updater._apply_updates_vm") @@ -464,33 +246,55 @@ def test_write_last_updated_flags_dom0_folder_creation_fail( mocked_error.assert_has_calls(error_log) -@mock.patch("subprocess.check_output") -@mock.patch("subprocess.check_call") +@mock.patch("subprocess.check_output", return_value=b"") @mock.patch("Updater._write_updates_status_flag_to_disk") @mock.patch("Updater._write_last_updated_flags_to_disk") @mock.patch("Updater.shutdown_and_start_vms") @mock.patch("Updater._apply_updates_vm") @mock.patch("Updater.sdlog.error") @mock.patch("Updater.sdlog.info") -def test_apply_updates_dom0_success( +def test_apply_updates_dom0_updates_applied( mocked_info, mocked_error, apply_vm, shutdown, write_updated, write_status, - mocked_call, mocked_output, ): result = updater._apply_updates_dom0() assert result == UpdateStatus.REBOOT_REQUIRED + mocked_output.assert_called_once_with(["sudo", "qubes-dom0-update", "-y"]) + assert not mocked_error.called + assert not apply_vm.called + + +@mock.patch("subprocess.check_output", return_value=b"No packages downloaded") +@mock.patch("Updater._write_updates_status_flag_to_disk") +@mock.patch("Updater._write_last_updated_flags_to_disk") +@mock.patch("Updater.shutdown_and_start_vms") +@mock.patch("Updater._apply_updates_vm") +@mock.patch("Updater.sdlog.error") +@mock.patch("Updater.sdlog.info") +def test_apply_updates_dom0_no_updates( + mocked_info, + mocked_error, + apply_vm, + shutdown, + write_updated, + write_status, + mocked_call, +): + result = updater._apply_updates_dom0() + assert result == UpdateStatus.UPDATES_OK mocked_call.assert_called_once_with(["sudo", "qubes-dom0-update", "-y"]) assert not mocked_error.called assert not apply_vm.called @mock.patch( - "subprocess.check_call", side_effect=subprocess.CalledProcessError(1, "check_call") + "subprocess.check_output", + side_effect=subprocess.CalledProcessError(1, "check_output"), ) @mock.patch("Updater.sdlog.error") @mock.patch("Updater.sdlog.info") @@ -498,7 +302,7 @@ def test_apply_updates_dom0_failure(mocked_info, mocked_error, mocked_call): result = updater._apply_updates_dom0() error_log = [ call("An error has occurred updating dom0. Please contact your administrator."), - call("Command 'check_call' returned non-zero exit status 1."), + call("Command 'check_output' returned non-zero exit status 1."), ] assert result == UpdateStatus.UPDATES_FAILED mocked_error.assert_has_calls(error_log)