diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 218b04f9..0d1e48d6 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -31,12 +31,8 @@ def _parser() -> argparse.ArgumentParser: :return: command line parser for the application """ parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager") - parser.add_argument( - "-a", - "--architecture", - help="target architectures (can be used multiple times)", - action="append", - required=True) + parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)", + action="append", required=True) parser.add_argument("-c", "--config", help="configuration path", default="/etc/ahriman.ini") parser.add_argument("--force", help="force run, remove file lock", action="store_true") parser.add_argument("--lock", help="lock file", default="/tmp/ahriman.lock") @@ -60,12 +56,10 @@ def _parser() -> argparse.ArgumentParser: clean_parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true") clean_parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true") clean_parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true") - clean_parser.add_argument( - "--no-manual", - help="do not clear directory with manually added packages", - action="store_true") + clean_parser.add_argument("--no-manual", help="do not clear directory with manually added packages", + action="store_true") clean_parser.add_argument("--no-packages", help="do not clear directory with built packages", action="store_true") - clean_parser.set_defaults(handler=handlers.Clean) + clean_parser.set_defaults(handler=handlers.Clean, unsafe=True) config_parser = subparsers.add_parser("config", description="dump configuration for specified architecture") config_parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True) @@ -81,6 +75,15 @@ def _parser() -> argparse.ArgumentParser: report_parser.add_argument("target", help="target to generate report", nargs="*") report_parser.set_defaults(handler=handlers.Report) + setup_parser = subparsers.add_parser("setup", description="create initial service configuration, requires root") + setup_parser.add_argument("--build-command", help="build command prefix", default="ahriman") + setup_parser.add_argument("--from-config", help="path to default devtools pacman configuration", + default="/usr/share/devtools/pacman-extra.conf") + setup_parser.add_argument("--no-multilib", help="do not add multilib repository", action="store_true") + setup_parser.add_argument("--packager", help="packager name and email", required=True) + setup_parser.add_argument("--repository", help="repository name", default="aur-clone") + setup_parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, unsafe=True) + sign_parser = subparsers.add_parser("sign", description="(re-)sign packages and repository database") sign_parser.add_argument("package", help="sign only specified packages", nargs="*") sign_parser.set_defaults(handler=handlers.Sign) @@ -96,8 +99,8 @@ def _parser() -> argparse.ArgumentParser: update_parser = subparsers.add_parser("update", description="run updates") update_parser.add_argument("package", help="filter check by package base", nargs="*") - update_parser.add_argument( - "--dry-run", help="just perform check for updates, same as check command", action="store_true") + update_parser.add_argument("--dry-run", help="just perform check for updates, same as check command", + action="store_true") update_parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true") update_parser.add_argument("--no-manual", help="do not include manual updates", action="store_true") update_parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true") diff --git a/src/ahriman/application/handlers/__init__.py b/src/ahriman/application/handlers/__init__.py index 7691c927..d64f241b 100644 --- a/src/ahriman/application/handlers/__init__.py +++ b/src/ahriman/application/handlers/__init__.py @@ -25,6 +25,7 @@ from ahriman.application.handlers.dump import Dump from ahriman.application.handlers.rebuild import Rebuild from ahriman.application.handlers.remove import Remove from ahriman.application.handlers.report import Report +from ahriman.application.handlers.setup import Setup from ahriman.application.handlers.sign import Sign from ahriman.application.handlers.status import Status from ahriman.application.handlers.sync import Sync diff --git a/src/ahriman/application/handlers/setup.py b/src/ahriman/application/handlers/setup.py new file mode 100644 index 00000000..4cfcfb7a --- /dev/null +++ b/src/ahriman/application/handlers/setup.py @@ -0,0 +1,148 @@ +# +# Copyright (c) 2021 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# 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 3 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, see . +# +import argparse +import configparser + +from pathlib import Path +from typing import Type + +from ahriman.application.application import Application +from ahriman.application.handlers.handler import Handler +from ahriman.core.configuration import Configuration +from ahriman.models.repository_paths import RepositoryPaths + + +class Setup(Handler): + """ + setup handler + :cvar ARCHBUILD_COMMAND_PATH: default devtools command + :cvar BIN_DIR_PATH: directory for custom binaries + :cvar MIRRORLIST_PATH: path to pacman default mirrorlist (used by multilib repository) + :cvar SUDOERS_PATH: path to sudoers.d include configuration + """ + + ARCHBUILD_COMMAND_PATH = Path("/usr/bin/archbuild") + BIN_DIR_PATH = Path("/usr/local/bin") + MIRRORLIST_PATH = Path("/etc/pacman.d/mirrorlist") + SUDOERS_PATH = Path("/etc/sudoers.d/ahriman") + + @classmethod + def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: + """ + callback for command line + :param args: command line args + :param architecture: repository architecture + :param config: configuration instance + """ + application = Application(architecture, config) + Setup.create_makepkg_configuration(args.packager, application.repository.paths) + Setup.create_executable(args.build_command, architecture) + Setup.create_devtools_configuration(args.build_command, architecture, Path(args.from_config), args.no_multilib, + application.repository.name, application.repository.paths) + Setup.create_ahriman_configuration(args.build_command, architecture, config.include) + Setup.create_sudo_configuration(args.build_command, architecture) + + @staticmethod + def build_command(prefix: str, architecture: str) -> Path: + """ + generate build command name + :param prefix: command prefix in {prefix}-{architecture}-build + :param architecture: repository architecture + :return: valid devtools command name + """ + return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build" + + @staticmethod + def create_ahriman_configuration(prefix: str, architecture: str, include_path: Path) -> None: + """ + create service specific configuration + :param prefix: command prefix in {prefix}-{architecture}-build + :param architecture: repository architecture + :param include_path: path to directory with configuration includes + """ + config = configparser.RawConfigParser() + config.add_section("build") + config.set("build", "build_command", str(Setup.build_command(prefix, architecture))) + + target = include_path / "build.ini" + with target.open("w") as ahriman_config: + config.write(ahriman_config) + + @staticmethod + def create_devtools_configuration(prefix: str, architecture: str, source: Path, + no_multilib: bool, repository: str, paths: RepositoryPaths) -> None: + """ + create configuration for devtools based on `source` configuration + :param prefix: command prefix in {prefix}-{architecture}-build + :param architecture: repository architecture + :param source: path to source configuration file + :param no_multilib: do not add multilib repository + :param repository: repository name + :param paths: repository paths instance + """ + config = configparser.RawConfigParser() + + # include base configuration + config.add_section("options") + config.set("options", "Include", str(source)) + config.set("options", "Architecture", architecture) + + # add multilib + if not no_multilib: + config.add_section("multilib") + config.set("multilib", "Include", str(Setup.MIRRORLIST_PATH)) + + # add repository itself + config.add_section(repository) + config.set(repository, "SigLevel", "Optional TrustAll") # we don't care + config.set(repository, "Server", f"file://{paths.repository}") + + target = source.parent / f"pacman-{prefix}.conf" + with target.open("w") as devtools_config: + config.write(devtools_config) + + @staticmethod + def create_makepkg_configuration(packager: str, paths: RepositoryPaths) -> None: + """ + create configuration for makepkg + :param packager: packager identifier (e.g. name, email) + :param paths: repository paths instance + """ + (paths.root / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n") + + @staticmethod + def create_sudo_configuration(prefix: str, architecture: str) -> None: + """ + create configuration to run build command with sudo without password + :param prefix: command prefix in {prefix}-{architecture}-build + :param architecture: repository architecture + """ + command = Setup.build_command(prefix, architecture) + Setup.SUDOERS_PATH.write_text(f"ahriman ALL=(ALL) NOPASSWD: {command} *\n") + Setup.SUDOERS_PATH.chmod(0o400) # security! + + @staticmethod + def create_executable(prefix: str, architecture: str) -> None: + """ + create executable for the service + :param prefix: command prefix in {prefix}-{architecture}-build + :param architecture: repository architecture + """ + Setup.build_command(prefix, architecture).symlink_to(Setup.BIN_DIR_PATH) diff --git a/tests/ahriman/application/handlers/test_handler_add.py b/tests/ahriman/application/handlers/test_handler_add.py index 671ff6ee..4a6f0a4e 100644 --- a/tests/ahriman/application/handlers/test_handler_add.py +++ b/tests/ahriman/application/handlers/test_handler_add.py @@ -6,12 +6,17 @@ from ahriman.application.handlers import Add from ahriman.core.configuration import Configuration +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + args.package = [] + args.without_dependencies = False + return args + + def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ must run command """ - args.package = [] - args.without_dependencies = False + args = _default_args(args) mocker.patch("pathlib.Path.mkdir") application_mock = mocker.patch("ahriman.application.application.Application.add") diff --git a/tests/ahriman/application/handlers/test_handler_clean.py b/tests/ahriman/application/handlers/test_handler_clean.py index 50555798..db2c0e06 100644 --- a/tests/ahriman/application/handlers/test_handler_clean.py +++ b/tests/ahriman/application/handlers/test_handler_clean.py @@ -6,15 +6,20 @@ from ahriman.application.handlers import Clean from ahriman.core.configuration import Configuration -def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: - """ - must run command - """ +def _default_args(args: argparse.Namespace) -> argparse.Namespace: args.no_build = False args.no_cache = False args.no_chroot = False args.no_manual = False args.no_packages = False + return args + + +def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command + """ + args = _default_args(args) mocker.patch("pathlib.Path.mkdir") application_mock = mocker.patch("ahriman.application.application.Application.clean") diff --git a/tests/ahriman/application/handlers/test_handler_remove.py b/tests/ahriman/application/handlers/test_handler_remove.py index 5f9a22b2..df990cbc 100644 --- a/tests/ahriman/application/handlers/test_handler_remove.py +++ b/tests/ahriman/application/handlers/test_handler_remove.py @@ -6,11 +6,16 @@ from ahriman.application.handlers import Remove from ahriman.core.configuration import Configuration +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + args.package = [] + return args + + def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ must run command """ - args.package = [] + args = _default_args(args) mocker.patch("pathlib.Path.mkdir") application_mock = mocker.patch("ahriman.application.application.Application.remove") diff --git a/tests/ahriman/application/handlers/test_handler_report.py b/tests/ahriman/application/handlers/test_handler_report.py index 9dbba316..7deaa60d 100644 --- a/tests/ahriman/application/handlers/test_handler_report.py +++ b/tests/ahriman/application/handlers/test_handler_report.py @@ -6,11 +6,16 @@ from ahriman.application.handlers import Report from ahriman.core.configuration import Configuration +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + args.target = [] + return args + + def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ must run command """ - args.target = [] + args = _default_args(args) mocker.patch("pathlib.Path.mkdir") application_mock = mocker.patch("ahriman.application.application.Application.report") diff --git a/tests/ahriman/application/handlers/test_handler_setup.py b/tests/ahriman/application/handlers/test_handler_setup.py new file mode 100644 index 00000000..0203fc79 --- /dev/null +++ b/tests/ahriman/application/handlers/test_handler_setup.py @@ -0,0 +1,121 @@ +import argparse + +from pathlib import Path +from pytest_mock import MockerFixture +from unittest import mock + +from ahriman.application.handlers import Setup +from ahriman.core.configuration import Configuration +from ahriman.models.repository_paths import RepositoryPaths + + +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + args.build_command = "ahriman" + args.from_config = "/usr/share/devtools/pacman-extra.conf" + args.no_multilib = False + args.packager = "John Doe " + args.repository = "aur-clone" + return args + + +def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command + """ + args = _default_args(args) + mocker.patch("pathlib.Path.mkdir") + ahriman_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_ahriman_configuration") + devtools_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_devtools_configuration") + makepkg_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_makepkg_configuration") + sudo_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_sudo_configuration") + executable_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_executable") + + Setup.run(args, "x86_64", configuration) + ahriman_configuration_mock.assert_called_once() + devtools_configuration_mock.assert_called_once() + makepkg_configuration_mock.assert_called_once() + sudo_configuration_mock.assert_called_once() + executable_mock.assert_called_once() + + +def test_build_command(args: argparse.Namespace) -> None: + """ + must generate correct build command name + """ + args = _default_args(args) + assert Setup.build_command(args.build_command, "x86_64").name == f"{args.build_command}-x86_64-build" + + +def test_create_ahriman_configuration(args: argparse.Namespace, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must create configuration for the service + """ + args = _default_args(args) + mocker.patch("pathlib.Path.open") + add_section_mock = mocker.patch("configparser.RawConfigParser.add_section") + set_mock = mocker.patch("configparser.RawConfigParser.set") + write_mock = mocker.patch("configparser.RawConfigParser.write") + + command = Setup.build_command(args.build_command, "x86_64") + Setup.create_ahriman_configuration(args.build_command, "x86_64", configuration.include) + add_section_mock.assert_called_once() + set_mock.assert_called_with("build", "build_command", str(command)) + write_mock.assert_called_once() + + +def test_create_devtools_configuration(args: argparse.Namespace, repository_paths: RepositoryPaths, + mocker: MockerFixture) -> None: + """ + must create configuration for the service + """ + args = _default_args(args) + mocker.patch("pathlib.Path.open") + mocker.patch("configparser.RawConfigParser.set") + add_section_mock = mocker.patch("configparser.RawConfigParser.add_section") + write_mock = mocker.patch("configparser.RawConfigParser.write") + + Setup.create_devtools_configuration(args.build_command, "x86_64", Path(args.from_config), args.no_multilib, + args.repository, repository_paths) + add_section_mock.assert_has_calls([ + mock.call("options"), + mock.call("multilib"), + mock.call(args.repository) + ]) + write_mock.assert_called_once() + + +def test_create_makepkg_configuration(args: argparse.Namespace, repository_paths: RepositoryPaths, + mocker: MockerFixture) -> None: + """ + must create makepkg configuration + """ + args = _default_args(args) + write_text_mock = mocker.patch("pathlib.Path.write_text") + + Setup.create_makepkg_configuration(args.packager, repository_paths) + write_text_mock.assert_called_once() + + +def test_create_sudo_configuration(args: argparse.Namespace, mocker: MockerFixture) -> None: + """ + must create sudo configuration + """ + args = _default_args(args) + chmod_text_mock = mocker.patch("pathlib.Path.chmod") + write_text_mock = mocker.patch("pathlib.Path.write_text") + + Setup.create_sudo_configuration(args.build_command, "x86_64") + chmod_text_mock.assert_called_with(0o400) + write_text_mock.assert_called_once() + + +def test_create_executable(args: argparse.Namespace, mocker: MockerFixture) -> None: + """ + must create sudo configuration + """ + args = _default_args(args) + symlink_text_mock = mocker.patch("pathlib.Path.symlink_to") + + Setup.create_executable(args.build_command, "x86_64") + symlink_text_mock.assert_called_once() diff --git a/tests/ahriman/application/handlers/test_handler_sign.py b/tests/ahriman/application/handlers/test_handler_sign.py index 38f906f4..0b16f01a 100644 --- a/tests/ahriman/application/handlers/test_handler_sign.py +++ b/tests/ahriman/application/handlers/test_handler_sign.py @@ -6,11 +6,16 @@ from ahriman.application.handlers import Sign from ahriman.core.configuration import Configuration +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + args.package = [] + return args + + def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ must run command """ - args.package = [] + args = _default_args(args) mocker.patch("pathlib.Path.mkdir") application_mock = mocker.patch("ahriman.application.application.Application.sign") diff --git a/tests/ahriman/application/handlers/test_handler_status.py b/tests/ahriman/application/handlers/test_handler_status.py index 8ded654e..a1cac924 100644 --- a/tests/ahriman/application/handlers/test_handler_status.py +++ b/tests/ahriman/application/handlers/test_handler_status.py @@ -6,13 +6,17 @@ from ahriman.application.handlers import Status from ahriman.core.configuration import Configuration +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + args.ahriman = True + args.package = [] + return args + + def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ must run command """ - args.ahriman = True - args.package = [] - args.without_dependencies = False + args = _default_args(args) mocker.patch("pathlib.Path.mkdir") application_mock = mocker.patch("ahriman.core.status.client.Client.get_self") packages_mock = mocker.patch("ahriman.core.status.client.Client.get") diff --git a/tests/ahriman/application/handlers/test_handler_sync.py b/tests/ahriman/application/handlers/test_handler_sync.py index 4926976e..28dac940 100644 --- a/tests/ahriman/application/handlers/test_handler_sync.py +++ b/tests/ahriman/application/handlers/test_handler_sync.py @@ -6,11 +6,16 @@ from ahriman.application.handlers import Sync from ahriman.core.configuration import Configuration +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + args.target = [] + return args + + def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: """ must run command """ - args.target = [] + args = _default_args(args) mocker.patch("pathlib.Path.mkdir") application_mock = mocker.patch("ahriman.application.application.Application.sync") diff --git a/tests/ahriman/application/handlers/test_handler_update.py b/tests/ahriman/application/handlers/test_handler_update.py index 3de20521..a4bbafd2 100644 --- a/tests/ahriman/application/handlers/test_handler_update.py +++ b/tests/ahriman/application/handlers/test_handler_update.py @@ -6,15 +6,20 @@ from ahriman.application.handlers import Update from ahriman.core.configuration import Configuration -def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: - """ - must run command - """ +def _default_args(args: argparse.Namespace) -> argparse.Namespace: args.package = [] args.dry_run = False args.no_aur = False args.no_manual = False args.no_vcs = False + return args + + +def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command + """ + args = _default_args(args) mocker.patch("pathlib.Path.mkdir") application_mock = mocker.patch("ahriman.application.application.Application.update") updates_mock = mocker.patch("ahriman.application.application.Application.get_updates") @@ -28,11 +33,8 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, moc """ must run simplified command """ - args.package = [] + args = _default_args(args) args.dry_run = True - args.no_aur = False - args.no_manual = False - args.no_vcs = False mocker.patch("pathlib.Path.mkdir") updates_mock = mocker.patch("ahriman.application.application.Application.get_updates") diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index ea87dd59..0b4153fd 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -26,6 +26,14 @@ def test_subparsers_check(parser: argparse.ArgumentParser) -> None: assert args.dry_run +def test_subparsers_clean(parser: argparse.ArgumentParser) -> None: + """ + clean command must imply unsafe + """ + args = parser.parse_args(["-a", "x86_64", "clean"]) + assert args.unsafe + + def test_subparsers_config(parser: argparse.ArgumentParser) -> None: """ config command must imply lock, no_report and unsafe @@ -36,6 +44,16 @@ def test_subparsers_config(parser: argparse.ArgumentParser) -> None: assert args.unsafe +def test_subparsers_setup(parser: argparse.ArgumentParser) -> None: + """ + setup command must imply lock, no_report and unsafe + """ + args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe "]) + assert args.lock is None + assert args.no_report + assert args.unsafe + + def test_subparsers_status(parser: argparse.ArgumentParser) -> None: """ status command must imply lock, no_report and unsafe diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index b918f520..e7faa097 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -1,4 +1,5 @@ [settings] +include = . logging = logging.ini [alpm]