diff --git a/vmupdate/agent/entrypoint.py b/vmupdate/agent/entrypoint.py index 1a82070..ebae54c 100755 --- a/vmupdate/agent/entrypoint.py +++ b/vmupdate/agent/entrypoint.py @@ -32,7 +32,11 @@ def main(args=None): ) log.debug("Notify dom0 about upgrades.") - os.system("/usr/lib/qubes/upgrades-status-notify") + if os_data["os_family"] == "NixOS": + # use non-absolute path since NixOS will configure PATH correctly for us + os.system("upgrades-status-notify") + else: + os.system("/usr/lib/qubes/upgrades-status-notify") if not args.no_cleanup: return_code = max(pkg_mng.clean(), return_code) @@ -78,9 +82,11 @@ def get_package_manager(os_data, log, log_handler, log_level, no_progress): from source.dnf.dnf_cli import DNFCLI as PackageManager elif os_data["os_family"] == "ArchLinux": from source.pacman.pacman_cli import PACMANCLI as PackageManager + elif os_data["os_family"] == "NixOS": + from source.nixos.nixos_cli import NIXOSCLI as PackageManager else: raise NotImplementedError( - "Only Debian, RedHat and ArchLinux based OS is supported.") + "Only Debian, RedHat, ArchLinux, NixOS based OS is supported.") requirements = {} for plugin in plugins.entrypoints: diff --git a/vmupdate/agent/source/common/package_manager.py b/vmupdate/agent/source/common/package_manager.py index 7844d8b..34ae53a 100644 --- a/vmupdate/agent/source/common/package_manager.py +++ b/vmupdate/agent/source/common/package_manager.py @@ -218,7 +218,7 @@ def _print_changes(self, changes): if changes["installed"]: for pkg in changes["installed"]: result.out += self._print_to_string( - pkg, changes["installed"][pkg]) + pkg, ", ".join(changes["installed"][pkg])) else: result.out += self._print_to_string("None") @@ -227,9 +227,9 @@ def _print_changes(self, changes): for pkg in changes["updated"]: result.out += self._print_to_string( pkg, - str(changes["updated"][pkg]["old"])[2:-2] + ", ".join((changes["updated"][pkg]["old"])) + " -> " + - str(changes["updated"][pkg]["new"])[2:-2]) + ", ".join((changes["updated"][pkg]["new"])) else: result.out += self._print_to_string("None") @@ -237,7 +237,7 @@ def _print_changes(self, changes): if changes["removed"]: for pkg in changes["removed"]: result.out += self._print_to_string( - pkg, changes["removed"][pkg]) + pkg, ", ".join(changes["removed"][pkg])) else: result.out += self._print_to_string("None") return result @@ -276,7 +276,10 @@ def upgrade_internal(self, remove_obsolete: bool) -> ProcessResult: Just run upgrade via CLI. """ cmd = [self.package_manager, - "--noconfirm" if self.package_manager == "pacman" else "-y", + *( + ["--noconfirm"] if self.package_manager == "pacman" + else [] if self.package_manager == "qubes-nixos-rebuild" else "-y" + ), *self.get_action(remove_obsolete)] return self.run_cmd(cmd) diff --git a/vmupdate/agent/source/nixos/__init__.py b/vmupdate/agent/source/nixos/__init__.py new file mode 100644 index 0000000..1ece793 --- /dev/null +++ b/vmupdate/agent/source/nixos/__init__.py @@ -0,0 +1,18 @@ +# coding=utf-8 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# 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. diff --git a/vmupdate/agent/source/nixos/nixos_cli.py b/vmupdate/agent/source/nixos/nixos_cli.py new file mode 100644 index 0000000..aaa1ddb --- /dev/null +++ b/vmupdate/agent/source/nixos/nixos_cli.py @@ -0,0 +1,70 @@ +# coding=utf-8 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# 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 typing import List, Dict + +from source.common.package_manager import PackageManager +from source.common.process_result import ProcessResult + + +class NIXOSCLI(PackageManager): + def __init__(self, log_handler, log_level): + super().__init__(log_handler, log_level) + self.package_manager = "qubes-nixos-rebuild" + + def refresh(self, hard_fail: bool) -> ProcessResult: + """ + Use package manager to refresh available packages. + + Note: Is a no-op in NixOS because the qubes-nixos-rebuild + wrapper takes care of it, and having just sync could cause problems. + + :return: (exit_code, stdout, stderr) + """ + cmd = ["true"] + return self.run_cmd(cmd) + + def get_packages(self) -> Dict[str, List[str]]: + """ + Use nix to return the installed packages and their versions. + """ + + cmd = ["qubes-nixos-get-packages"] + # EXAMPLE OUTPUT: + # qubes-core-agent-linux: ∅ → 4.3.5, +1413.6 KiB + # python3: ∅ → 3.11.9, 3.12.4, +229814.3 KiB + # dns-root-data: ∅ → 2024-06-20 + + result = self.run_cmd(cmd, realtime=False) + + packages: Dict[str, List[str]] = {} + for line in result.out.splitlines(): + package, info = line.split(":", 1) + versions = info.lstrip("∅ → ").split(", ") + for version in versions: + if not version.startswith("+"): + packages.setdefault(package, []).append(version) + + return packages + + def get_action(self, remove_obsolete) -> List[str]: + """ + qubes-nixos-rebuild will handle obsoletions itself + """ + return [] diff --git a/vmupdate/agent/source/utils.py b/vmupdate/agent/source/utils.py index d486954..20d09d8 100644 --- a/vmupdate/agent/source/utils.py +++ b/vmupdate/agent/source/utils.py @@ -65,6 +65,9 @@ def get_os_data(logger: Optional = None) -> Dict[str, Any]: if 'arch' in family: data["os_family"] = 'ArchLinux' + if "nixos" in family: + data["os_family"] = "NixOS" + return data diff --git a/vmupdate/qube_connection.py b/vmupdate/qube_connection.py index 01279cc..8675d4d 100644 --- a/vmupdate/qube_connection.py +++ b/vmupdate/qube_connection.py @@ -45,7 +45,7 @@ class QubeConnection: stop the qube if it was started by this connection. """ - PYTHON_PATH = "/usr/bin/python3" + PYTHON_PATH = "/usr/bin" def __init__( self, @@ -162,8 +162,11 @@ def run_entrypoint( result = self._run_shell_command_in_qube(self.qube, command) # run entrypoint - command = [QubeConnection.PYTHON_PATH, entrypoint_path, - *AgentArgs.to_cli_args(agent_args)] + command = ["sh", "-c", " ".join( + ["env", f"PATH=${QubeConnection.PYTHON_PATH}:$PATH", + "python3", entrypoint_path, + *AgentArgs.to_cli_args(agent_args)])] + result += self._run_shell_command_in_qube( self.qube, command, show=self.show_progress)