Setup command (#9)

* block issues without templates

* add setup subcommand

* handle devtools config correctly
This commit is contained in:
Evgenii Alekseev 2021-03-29 03:24:58 +03:00 committed by GitHub
parent 80a1f37c85
commit 10e4f3b629
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 406 additions and 38 deletions

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -19,14 +19,14 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
```shell
echo 'PACKAGES="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
echo 'PACKAGER="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
```
* Configure build tools (it is required for correct dependency management system):
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build` (you can choose any name for command, basically it should be `{name}-{arch}-build`);
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`);
* change configuration file, add your own repository, add multilib repository etc. Hint: you can use `Include` option as well;
* change configuration file, add your own repository, add multilib repository etc;
* set `build_command` option to point to your command;
* configure `/etc/sudoers.d/ahriman` to allow running command without a password.
@ -66,3 +66,5 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
```shell
sudo -u ahriman ahriman -a x86_64 add yay
```
Note that initial service configuration can be done by running `ahriman setup` with specific arguments.

View File

@ -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")
@ -44,6 +40,7 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
parser.add_argument("-v", "--version", action="version", version=version.__version__)
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
add_parser = subparsers.add_parser("add", description="add package")
@ -60,12 +57,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 +76,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 +100,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")
@ -110,8 +114,8 @@ def _parser() -> argparse.ArgumentParser:
if __name__ == "__main__":
arg_parser = _parser()
args = arg_parser.parse_args()
args_parser = _parser()
args = args_parser.parse_args()
handler: handlers.Handler = args.handler
status = handler.execute(args)

View File

@ -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

View File

@ -0,0 +1,160 @@
#
# 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,
args.repository, application.repository.paths)
Setup.create_ahriman_configuration(args.build_command, architecture, args.repository, 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, repository: str, include_path: Path) -> None:
"""
create service specific configuration
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
:param repository: repository name
:param include_path: path to directory with configuration includes
"""
config = configparser.ConfigParser()
config.add_section("build")
config.set("build", "build_command", str(Setup.build_command(prefix, architecture)))
config.add_section("repository")
config.set("repository", "name", repository)
target = include_path / "build-overrides.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.ConfigParser()
# preserve case
# stupid mypy thinks that it is impossible
config.optionxform = lambda key: key # type: ignore
# load default configuration first
# we cannot use Include here because it will be copied to new chroot, thus no includes there
config.read(source)
# set our architecture now
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
"""
command = Setup.build_command(prefix, architecture)
command.unlink(missing_ok=True)
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -0,0 +1,145 @@
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", args.repository, configuration.include)
add_section_mock.assert_has_calls([
mock.call("build"),
mock.call("repository"),
])
set_mock.assert_has_calls([
mock.call("build", "build_command", str(command)),
mock.call("repository", "name", args.repository),
])
write_mock.assert_called_once()
def test_create_devtools_configuration(args: argparse.Namespace, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must create configuration for the devtools
"""
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("multilib"),
mock.call(args.repository)
])
write_mock.assert_called_once()
def test_create_devtools_configuration_no_multilib(args: argparse.Namespace, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must create configuration for the devtools without multilib
"""
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), True,
args.repository, repository_paths)
add_section_mock.assert_called_once()
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 executable
"""
args = _default_args(args)
symlink_text_mock = mocker.patch("pathlib.Path.symlink_to")
unlink_text_mock = mocker.patch("pathlib.Path.unlink")
Setup.create_executable(args.build_command, "x86_64")
symlink_text_mock.assert_called_once()
unlink_text_mock.assert_called_once()

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

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

View File

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