add setup subcommand

This commit is contained in:
Evgenii Alekseev 2021-03-29 02:16:48 +03:00
parent 89e86079b8
commit 9b9c045f79
14 changed files with 362 additions and 34 deletions

View File

@ -31,12 +31,8 @@ def _parser() -> argparse.ArgumentParser:
:return: command line parser for the application :return: command line parser for the application
""" """
parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager") parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager")
parser.add_argument( parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
"-a", action="append", required=True)
"--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("-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("--force", help="force run, remove file lock", action="store_true")
parser.add_argument("--lock", help="lock file", default="/tmp/ahriman.lock") 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-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-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-chroot", help="do not clear build chroot", action="store_true")
clean_parser.add_argument( clean_parser.add_argument("--no-manual", help="do not clear directory with manually added packages",
"--no-manual",
help="do not clear directory with manually added packages",
action="store_true") action="store_true")
clean_parser.add_argument("--no-packages", help="do not clear directory with built 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 = subparsers.add_parser("config", description="dump configuration for specified architecture")
config_parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True) 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.add_argument("target", help="target to generate report", nargs="*")
report_parser.set_defaults(handler=handlers.Report) 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 = subparsers.add_parser("sign", description="(re-)sign packages and repository database")
sign_parser.add_argument("package", help="sign only specified packages", nargs="*") sign_parser.add_argument("package", help="sign only specified packages", nargs="*")
sign_parser.set_defaults(handler=handlers.Sign) 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 = subparsers.add_parser("update", description="run updates")
update_parser.add_argument("package", help="filter check by package base", nargs="*") update_parser.add_argument("package", help="filter check by package base", nargs="*")
update_parser.add_argument( update_parser.add_argument("--dry-run", help="just perform check for updates, same as check command",
"--dry-run", help="just perform check for updates, same as check command", action="store_true") 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-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-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") update_parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")

View File

@ -25,6 +25,7 @@ from ahriman.application.handlers.dump import Dump
from ahriman.application.handlers.rebuild import Rebuild from ahriman.application.handlers.rebuild import Rebuild
from ahriman.application.handlers.remove import Remove from ahriman.application.handlers.remove import Remove
from ahriman.application.handlers.report import Report 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.sign import Sign
from ahriman.application.handlers.status import Status from ahriman.application.handlers.status import Status
from ahriman.application.handlers.sync import Sync from ahriman.application.handlers.sync import Sync

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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)

View File

@ -6,12 +6,17 @@ from ahriman.application.handlers import Add
from ahriman.core.configuration import Configuration 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: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.package = [] args = _default_args(args)
args.without_dependencies = False
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.add") application_mock = mocker.patch("ahriman.application.application.Application.add")

View File

@ -6,15 +6,20 @@ from ahriman.application.handlers import Clean
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
must run command
"""
args.no_build = False args.no_build = False
args.no_cache = False args.no_cache = False
args.no_chroot = False args.no_chroot = False
args.no_manual = False args.no_manual = False
args.no_packages = 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") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.clean") application_mock = mocker.patch("ahriman.application.application.Application.clean")

View File

@ -6,11 +6,16 @@ from ahriman.application.handlers import Remove
from ahriman.core.configuration import Configuration 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: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.package = [] args = _default_args(args)
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.remove") application_mock = mocker.patch("ahriman.application.application.Application.remove")

View File

@ -6,11 +6,16 @@ from ahriman.application.handlers import Report
from ahriman.core.configuration import Configuration 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: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.target = [] args = _default_args(args)
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.report") application_mock = mocker.patch("ahriman.application.application.Application.report")

View File

@ -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 <john@doe.com>"
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()

View File

@ -6,11 +6,16 @@ from ahriman.application.handlers import Sign
from ahriman.core.configuration import Configuration 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: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.package = [] args = _default_args(args)
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.sign") application_mock = mocker.patch("ahriman.application.application.Application.sign")

View File

@ -6,13 +6,17 @@ from ahriman.application.handlers import Status
from ahriman.core.configuration import Configuration 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: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.ahriman = True args = _default_args(args)
args.package = []
args.without_dependencies = False
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.core.status.client.Client.get_self") application_mock = mocker.patch("ahriman.core.status.client.Client.get_self")
packages_mock = mocker.patch("ahriman.core.status.client.Client.get") packages_mock = mocker.patch("ahriman.core.status.client.Client.get")

View File

@ -6,11 +6,16 @@ from ahriman.application.handlers import Sync
from ahriman.core.configuration import Configuration 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: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.target = [] args = _default_args(args)
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.sync") application_mock = mocker.patch("ahriman.application.application.Application.sync")

View File

@ -6,15 +6,20 @@ from ahriman.application.handlers import Update
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
must run command
"""
args.package = [] args.package = []
args.dry_run = False args.dry_run = False
args.no_aur = False args.no_aur = False
args.no_manual = False args.no_manual = False
args.no_vcs = 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") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.update") application_mock = mocker.patch("ahriman.application.application.Application.update")
updates_mock = mocker.patch("ahriman.application.application.Application.get_updates") 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 must run simplified command
""" """
args.package = [] args = _default_args(args)
args.dry_run = True args.dry_run = True
args.no_aur = False
args.no_manual = False
args.no_vcs = False
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
updates_mock = mocker.patch("ahriman.application.application.Application.get_updates") updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")

View File

@ -26,6 +26,14 @@ def test_subparsers_check(parser: argparse.ArgumentParser) -> None:
assert args.dry_run 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: def test_subparsers_config(parser: argparse.ArgumentParser) -> None:
""" """
config command must imply lock, no_report and unsafe config command must imply lock, no_report and unsafe
@ -36,6 +44,16 @@ def test_subparsers_config(parser: argparse.ArgumentParser) -> None:
assert args.unsafe 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 <john@doe.com>"])
assert args.lock is None
assert args.no_report
assert args.unsafe
def test_subparsers_status(parser: argparse.ArgumentParser) -> None: def test_subparsers_status(parser: argparse.ArgumentParser) -> None:
""" """
status command must imply lock, no_report and unsafe status command must imply lock, no_report and unsafe

View File

@ -1,4 +1,5 @@
[settings] [settings]
include = .
logging = logging.ini logging = logging.ini
[alpm] [alpm]