Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardization of return codes + man page #160

Merged
merged 4 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions doc/tools/qubes-vm-update.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
===============
qubes-vm-update
===============

NAME
====
qubes-vm-update - update software in virtual machines (qubes)

SYNOPSIS
========
| qubes-vm-update [options]

OPTIONS
=======

Package Manager
---------------
--no-refresh
Do not refresh available packages before upgrading vm
--force-upgrade, -f
Try upgrade even if errors are encountered (like a refresh error)
--leave-obsolete
Do not remove obsolete packages during upgrading

Targeting
---------
--skip SKIP
Comma separated list of VMs to be skipped, works with all other options.
--targets TARGETS
Comma separated list of VMs to target. Ignores conditions.
--templates, -T
Target all updatable TemplateVMs.
--standalones, -S
Target all updatable StandaloneVMs.
--apps, -A
Target running updatable AppVMs to update in place. Updates will be lost after vm restart.
--all
DEFAULT. Target all updatable VMs except AdminVM. Use explicitly with "--targets" to include both.

Selecting
---------
--update-if-available
Update targeted VMs with known updates available
--update-if-stale UPDATE_IF_STALE
DEFAULT. Attempt to update targeted VMs with known updates available or for which last update check was more than N days ago. (default: dom0 feature `qubes-vm-update-update-if-stale` if set or 7)
--force-update
Attempt to update all targeted VMs even if no updates are available

Propagation
-----------
--apply-to-sys, --restart, -r
Restart not updated ServiceVMs whose template has been updated.
--apply-to-all, -R
Restart not updated ServiceVMs and shutdown not updated AppVMs whose template has been updated.
--no-apply
DEFAULT. Do not restart/shutdown any AppVMs.

Auxiliary
---------
--max-concurrency MAX_CONCURRENCY, -x MAX_CONCURRENCY
Maximum number of VMs configured simultaneously (default: number of cpus)
--log LOG
Provide logging level. Values: DEBUG, INFO (default), WARNING, ERROR, CRITICAL
--signal-no-updates
Return exit code 100 instead of 0 if there is no updates available.

--no-progress
Do not show upgrading progress
--dry-run
Just print what happens
--no-cleanup
Do not remove updater files from target qube

--help, -h
Show this help message and exit
--quiet, -q
Do not print anything to stdout
--show-output, --verbose, -v
Show output of management commands


How to correctly use targeting and selection?

Targeting is used to choose the VMs that will be checked for available updates, and the three-level selection is used to check if the previously chosen VMs qualify for updates (i.e., there are, for example, updates available for them).

Additionally, not all VMs in the system can be updated directly (such as AppVMs), and to update them, you must use one of the "propagation" options. This means, after updating the template, restarting the VM and applying the installed updates to it. Using at least the `--apply-to-sys` flag is recommended, which restarts all service VMs. Keep in mind that during this process, unsaved data may be lost.

RETURN CODES
============

0: ok

100: ok, returned if `--signal-no-updates` and no updates available

1: general error

2: usage error, unrecognized argument

11: error of TemplateVM shutdown

12: error of AppVM shutdown

13: error of AppVM startup

21: general error inside updated vm

22: error inside updated vm during updating/installing prerequisites/patches

23: repo-refresh error inside updated vm, check if vm is connected to network

24: error inside updated vm during installing updates

25: unhandled error inside updated vm

40: qrexec error, communication across domains was interrupted

64: usage error, wrong parameter value

130: user interruption

AUTHORS
=======
| Piotr Bartman-Szwarc <prbartman at invisiblethingslab dot com>
5 changes: 4 additions & 1 deletion vmupdate/agent/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from source.args import AgentArgs
from source.utils import get_os_data
from source.log_congfig import init_logs
from source.common.exit_codes import EXIT


def main(args=None):
Expand All @@ -33,6 +34,8 @@ def main(args=None):
log.debug("Notify dom0 about upgrades.")
os.system("/usr/lib/qubes/upgrades-status-notify")

if return_code not in EXIT.VM_HANDLED:
return_code = EXIT.ERR_VM_UNHANDLED
return return_code


Expand Down Expand Up @@ -90,4 +93,4 @@ def get_package_manager(os_data, log, log_handler, log_level, no_progress):
sys.exit(main())
except RuntimeError as ex:
print(ex)
sys.exit(1)
sys.exit(EXIT.ERR_VM_UNHANDLED)
7 changes: 4 additions & 3 deletions vmupdate/agent/source/apt/apt_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import apt_pkg

from source.common.process_result import ProcessResult
from source.common.exit_codes import EXIT
from source.common.progress_reporter import ProgressReporter, Progress

from .apt_cli import APTCLI
Expand Down Expand Up @@ -64,11 +65,11 @@ def refresh(self, hard_fail: bool) -> ProcessResult:
self.log.debug("Cache refresh successful.")
else:
self.log.warning("Cache refresh failed.")
result += ProcessResult(1)
result += ProcessResult(EXIT.ERR_VM_REFRESH)
except Exception as exc:
self.log.error(
"An error occurred while refreshing packages: %s", str(exc))
result += ProcessResult(2, out="", err=str(exc))
result += ProcessResult(EXIT.ERR_VM_REFRESH, out="", err=str(exc))

return result

Expand All @@ -92,7 +93,7 @@ def upgrade_internal(self, remove_obsolete: bool) -> ProcessResult:
except Exception as exc:
self.log.error(
"An error occurred while upgrading packages: %s", str(exc))
result += ProcessResult(3, out="", err=str(exc))
result += ProcessResult(EXIT.ERR_VM_UPDATE, out="", err=str(exc))

return result

Expand Down
43 changes: 43 additions & 0 deletions vmupdate/agent/source/common/exit_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# coding=utf-8
#
# The Qubes OS Project, https://www.qubes-os.org
#
# Copyright (C) 2024 Piotr Bartman-Szwarc <prbartman@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
from dataclasses import dataclass


@dataclass(frozen=True)
class EXIT:
OK = 0
OK_NO_UPDATES = 100

ERR = 1
ERR_SHUTDOWN_TMPL = 11 # unable to shut down some TemplateVMs
ERR_SHUTDOWN_APP = 12 # unable to shut down some AppVMs
ERR_START_APP = 13 # unable to start some AppVMs

VM_HANDLED = (0, 100, 21, 22, 23, 24)
ERR_VM = 21
ERR_VM_PRE = 22
ERR_VM_REFRESH = 23
ERR_VM_UPDATE = 24
ERR_VM_UNHANDLED = 25

ERR_QREXEX = 40
ERR_USAGE = 64
SIGINT = 130
38 changes: 24 additions & 14 deletions vmupdate/agent/source/common/package_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import sys
from typing import Optional, Dict, List
from .process_result import ProcessResult
from .exit_codes import EXIT


class PackageManager:
Expand Down Expand Up @@ -77,36 +78,45 @@ def _upgrade(
if requirements:
print("Install requirements", flush=True)
result_install = self.install_requirements(requirements, curr_pkg)
if result_install:
self.log.warning(
"Installing requirements failed with exit code: %d",
result_install.code)
result_install.code = EXIT.ERR_VM_PRE
result += result_install
if result.code != 0:
self.log.warning("Installing requirements failed.")
if hard_fail:
self.log.error("Exiting due to a packages install error. "
"Use --force-upgrade to upgrade anyway.")
return result
if result and hard_fail:
self.log.error("Exiting due to a packages install error. "
"Use --force-upgrade to upgrade anyway.")
return result

if refresh:
print("Refreshing package info", flush=True)
result_refresh = self.refresh(hard_fail)
if result_refresh:
self.log.warning("Refreshing failed with code: %d",
result_refresh.code)
result_refresh.code = EXIT.ERR_VM_REFRESH
result += result_refresh
if result.code != 0:
self.log.warning("Refreshing failed.")
if hard_fail:
self.log.error("Exiting due to a refresh error. "
"Use --force-upgrade to upgrade anyway.")
return result
if result and hard_fail:
self.log.error("Exiting due to a refresh error. "
"Use --force-upgrade to upgrade anyway.")
return result

result_upgrade = self.upgrade_internal(remove_obsolete)
if result_upgrade:
result_upgrade.code = EXIT.ERR_VM_UPDATE
result += result_upgrade

new_pkg = self.get_packages()

changes = PackageManager.compare_packages(old=curr_pkg, new=new_pkg)
summary = self._print_changes(changes)
if summary:
summary.code = EXIT.ERR_VM
result += summary

if not result.code and not (changes["installed"] or changes["updated"]):
result.code = 100 # Nothing to upgrade
if not result and not (changes["installed"] or changes["updated"]):
result.code = EXIT.OK_NO_UPDATES

return result

Expand Down
7 changes: 4 additions & 3 deletions vmupdate/agent/source/common/process_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import sys
from copy import deepcopy
from typing import Union, Optional
from .exit_codes import EXIT


class ProcessResult:
Expand All @@ -32,7 +33,7 @@ class ProcessResult:
"""
def __init__(
self,
code: int = 0, out: str = "", err: str = "",
code: int = EXIT.OK, out: str = "", err: str = "",
realtime: bool = False
):
self.code: int = code
Expand Down Expand Up @@ -75,7 +76,7 @@ def from_untrusted_out_err(
untrusted_err_bytes = untrusted_err
err = ProcessResult.sanitize_output(untrusted_err_bytes)

return cls(0, out, err)
return cls(EXIT.OK, out, err)

@staticmethod
def sanitize_output(untrusted_bytes: bytes, single: bool = False) -> str:
Expand Down Expand Up @@ -114,4 +115,4 @@ def __repr__(self):
def error_from_messages(self):
out_lines = (self.out + '\n' + self.err).splitlines()
if any(line.lower().startswith("err") for line in out_lines):
self.code = 1
self.code = EXIT.ERR
19 changes: 10 additions & 9 deletions vmupdate/agent/source/dnf/dnf_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import dnf.transaction

from source.common.process_result import ProcessResult
from source.common.exit_codes import EXIT
from source.common.progress_reporter import ProgressReporter, Progress

from .dnf_cli import DNFCLI
Expand Down Expand Up @@ -62,11 +63,11 @@ def refresh(self, hard_fail: bool) -> ProcessResult:
self.log.debug("Cache refresh successful.")
else:
self.log.warning("Cache refresh failed.")
result += ProcessResult(1)
result += ProcessResult(EXIT.ERR_VM_REFRESH)
except Exception as exc:
self.log.error(
"An error occurred while refreshing packages: %s", str(exc))
result += ProcessResult(2, out="", err=str(exc))
result += ProcessResult(EXIT.ERR_VM_REFRESH, out="", err=str(exc))

return result

Expand All @@ -88,15 +89,15 @@ def upgrade_internal(self, remove_obsolete: bool) -> ProcessResult:
trans = self.base.transaction
if not trans:
self.log.info("No packages to upgrade, quitting.")
return ProcessResult(0, out="", err="")
return ProcessResult(EXIT.OK, out="", err="")

self.base.download_packages(
trans.install_set,
progress=self.progress.fetch_progress
)
result += sign_check(self.base, trans.install_set, self.log)

if result.code == 0:
if result.code == EXIT.OK:
print("Updating packages.", flush=True)
self.log.debug("Committing upgrade...")
self.base.do_transaction(self.progress.upgrade_progress)
Expand All @@ -107,7 +108,7 @@ def upgrade_internal(self, remove_obsolete: bool) -> ProcessResult:
except Exception as exc:
self.log.error(
"An error occurred while upgrading packages: %s", str(exc))
result += ProcessResult(3, out="", err=str(exc))
result += ProcessResult(EXIT.ERR_VM_UPDATE, out="", err=str(exc))
finally:
self.base.close()

Expand All @@ -116,13 +117,13 @@ def upgrade_internal(self, remove_obsolete: bool) -> ProcessResult:

def sign_check(base, packages, log) -> ProcessResult:
"""
Check signature of packages.
Check a signature of packages.
"""
log.debug("Check signature of packages.")
result = ProcessResult()
for package in packages:
ret_code, message = base.package_signature_check(package)
if ret_code != 0:
if ret_code != EXIT.OK:
# Import key and re-try the check
try:
base.package_import_key(package, askcb=(lambda a, b, c: True))
Expand All @@ -133,10 +134,10 @@ def sign_check(base, packages, log) -> ProcessResult:
# do that explicitly anyway, in case the behavior would change
# (intentionally or not)
ret_code, message = base.package_signature_check(package)
if ret_code != 0:
if ret_code != EXIT.OK:
result += ProcessResult(ret_code, out="", err=message)
else:
result += ProcessResult(0, out=message, err="")
result += ProcessResult(EXIT.OK, out=message, err="")

return result

Expand Down
Loading