deprecate init/repo-init command

In current workflow you need to run setup to run init (because of
repository name), but you need to run init before setup (because of
repository tree rights).

New solution just add `Repo.init()` method call to setup subcommand
after config reload to make sure that repository name has been applied.
In addition chown method as well as setuid method for check_output have
been added.
This commit is contained in:
2022-03-21 00:44:44 +03:00
parent 041b3824c1
commit 04174a3e6d
24 changed files with 225 additions and 147 deletions

View File

@ -85,7 +85,6 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_check_parser(subparsers)
_set_repo_clean_parser(subparsers)
_set_repo_config_parser(subparsers)
_set_repo_init_parser(subparsers)
_set_repo_rebuild_parser(subparsers)
_set_repo_remove_unknown_parser(subparsers)
_set_repo_report_parser(subparsers)
@ -342,19 +341,6 @@ def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_repo_init_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository init subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("repo-init", aliases=["init"], help="create repository tree",
description="create empty repository tree. Optional command for auto architecture support",
formatter_class=_formatter)
parser.set_defaults(handler=handlers.Init, no_report=True)
return parser
def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository rebuild subcommand
@ -406,7 +392,7 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("repo-setup", aliases=["setup"], help="initial service configuration",
parser = root.add_parser("repo-setup", aliases=["init", "repo-init", "setup"], help="initial service configuration",
description="create initial service configuration, requires root",
epilog="Create _minimal_ configuration for the service according to provided options.",
formatter_class=_formatter)

View File

@ -22,7 +22,6 @@ from ahriman.application.handlers.handler import Handler
from ahriman.application.handlers.add import Add
from ahriman.application.handlers.clean import Clean
from ahriman.application.handlers.dump import Dump
from ahriman.application.handlers.init import Init
from ahriman.application.handlers.key_import import KeyImport
from ahriman.application.handlers.patch import Patch
from ahriman.application.handlers.rebuild import Rebuild

View File

@ -27,7 +27,7 @@ from typing import List, Type
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitecture, MultipleArchitecture
from ahriman.core.exceptions import MissingArchitecture, MultipleArchitectures
from ahriman.models.repository_paths import RepositoryPaths
@ -95,7 +95,7 @@ class Handler:
# actually we do not have to spawn another process if it is single-process application, do we?
if len(architectures) > 1:
if not cls.ALLOW_MULTI_ARCHITECTURE_RUN:
raise MultipleArchitecture(args.command)
raise MultipleArchitectures(args.command)
with Pool(len(architectures)) as pool:
result = pool.starmap(

View File

@ -1,47 +0,0 @@
#
# 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
from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
class Init(Handler):
"""
repository init handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).repository.repo.init()

View File

@ -55,14 +55,19 @@ class Setup(Handler):
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
"""
Setup.configuration_create_ahriman(args, architecture, args.repository, configuration.include)
configuration.reload()
application = Application(architecture, configuration, no_report, unsafe)
Setup.configuration_create_makepkg(args.packager, application.repository.paths)
Setup.executable_create(args.build_command, architecture)
Setup.configuration_create_devtools(args.build_command, architecture, args.from_configuration,
args.no_multilib, args.repository, application.repository.paths)
Setup.configuration_create_ahriman(args, architecture, args.repository, configuration.include)
Setup.configuration_create_sudo(args.build_command, architecture)
application.repository.repo.init()
@staticmethod
def build_command(prefix: str, architecture: str) -> Path:
"""

View File

@ -32,6 +32,7 @@ from ahriman.core.exceptions import DuplicateRun
from ahriman.core.status.client import Client
from ahriman.core.util import check_user
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.repository_paths import RepositoryPaths
class Lock:
@ -40,7 +41,7 @@ class Lock:
:ivar force: remove lock file on start if any
:ivar path: path to lock file if any
:ivar reporter: build status reporter instance
:ivar root: repository root (i.e. ahriman home)
:ivar paths: repository paths instance
:ivar unsafe: skip user check
"""
@ -55,7 +56,7 @@ class Lock:
self.force = args.force
self.unsafe = args.unsafe
self.root = Path(configuration.get("repository", "root"))
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
self.reporter = Client() if args.no_report else Client.load(configuration)
def __enter__(self) -> Lock:
@ -103,7 +104,7 @@ class Lock:
"""
check if current user is actually owner of ahriman root
"""
check_user(self.root, self.unsafe)
check_user(self.paths, self.unsafe)
def clear(self) -> None:
"""

View File

@ -34,6 +34,7 @@ class Repo:
:ivar name: repository name
:ivar paths: repository paths instance
:ivar sign_args: additional args which have to be used to sign repository archive
:ivar uid: uid of the repository owner user
"""
_check_output = check_output
@ -48,6 +49,7 @@ class Repo:
self.logger = logging.getLogger("build_details")
self.name = name
self.paths = paths
self.uid, _ = paths.root_owner
self.sign_args = sign_args
@property
@ -66,7 +68,8 @@ class Repo:
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
exception=BuildFailed(path.name),
cwd=self.paths.repository,
logger=self.logger)
logger=self.logger,
user=self.uid)
def init(self) -> None:
"""
@ -76,7 +79,8 @@ class Repo:
"repo-add", *self.sign_args, str(self.repo_path),
exception=None,
cwd=self.paths.repository,
logger=self.logger)
logger=self.logger,
user=self.uid)
def remove(self, package: str, filename: Path) -> None:
"""
@ -93,4 +97,5 @@ class Repo:
"repo-remove", *self.sign_args, str(self.repo_path), package,
exception=BuildFailed(package),
cwd=self.paths.repository,
logger=self.logger)
logger=self.logger,
user=self.uid)

View File

@ -38,6 +38,7 @@ class Task:
:ivar logger: class logger
:ivar package: package definitions
:ivar paths: repository paths instance
:ivar uid: uid of the repository owner user
"""
_check_output = check_output
@ -53,6 +54,7 @@ class Task:
self.build_logger = logging.getLogger("build_details")
self.package = package
self.paths = paths
self.uid, _ = paths.root_owner
self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[])
self.build_command = configuration.get("build", "build_command")
@ -74,7 +76,8 @@ class Task:
*command,
exception=BuildFailed(self.package.base),
cwd=self.paths.sources_for(self.package.base),
logger=self.build_logger)
logger=self.build_logger,
user=self.uid)
# well it is not actually correct, but we can deal with it
packages = Task._check_output("makepkg", "--packagelist",

View File

@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from pathlib import Path
from typing import Any
@ -85,6 +86,20 @@ class InvalidOption(ValueError):
ValueError.__init__(self, f"Invalid or unknown option value `{value}`")
class InvalidPath(ValueError):
"""
exception which will be raised on path which is not belong to root directory
"""
def __init__(self, path: Path, root: Path) -> None:
"""
default constructor
:param path: path which raised an exception
:param root: repository root (i.e. ahriman home)
"""
ValueError.__init__(self, f"Path `{path}` does not belong to repository root `{root}`")
class InvalidPackageInfo(RuntimeError):
"""
exception which will be raised on package load errors
@ -111,7 +126,7 @@ class MissingArchitecture(ValueError):
ValueError.__init__(self, f"Architecture required for subcommand {command}, but missing")
class MultipleArchitecture(ValueError):
class MultipleArchitectures(ValueError):
"""
exception which will be raised if multiple architectures are not supported by the handler
"""

View File

@ -62,7 +62,7 @@ class Properties:
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
try:
check_user(self.paths.root, unsafe)
check_user(self.paths, unsafe)
self.paths.tree_create()
except UnsafeRun:
self.logger.warning("root owner differs from the current user, skipping tree creation")

View File

@ -27,10 +27,11 @@ from pathlib import Path
from typing import Any, Dict, Generator, Iterable, Optional, Union
from ahriman.core.exceptions import InvalidOption, UnsafeRun
from ahriman.models.repository_paths import RepositoryPaths
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
input_data: Optional[str] = None, logger: Optional[Logger] = None) -> str:
input_data: Optional[str] = None, logger: Optional[Logger] = None, user: Optional[int] = None) -> str:
"""
subprocess wrapper
:param args: command line arguments
@ -38,12 +39,15 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
:param cwd: current working directory
:param input_data: data which will be written to command stdin
:param logger: logger to log command result if required
:param user: run process as specified user
:return: command output
"""
try:
# universal_newlines is required to read input from string
# FIXME additional workaround for linter and type check which do not know that user arg is supported
# pylint: disable=unexpected-keyword-arg
result: str = subprocess.check_output(args, cwd=cwd, input=input_data, stderr=subprocess.STDOUT,
universal_newlines=True).strip()
universal_newlines=True, user=user).strip() # type: ignore
if logger is not None:
for line in result.splitlines():
logger.debug(line)
@ -55,18 +59,18 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
raise exception or e
def check_user(root: Path, unsafe: bool) -> None:
def check_user(paths: RepositoryPaths, unsafe: bool) -> None:
"""
check if current user is the owner of the root
:param root: root directory (i.e. ahriman home)
:param paths: repository paths object
:param unsafe: if set no user check will be performed before path creation
"""
if not root.exists():
if not paths.root.exists():
return # no directory found, skip check
if unsafe:
return # unsafe flag is enabled, no check performed
current_uid = os.getuid()
root_uid = root.stat().st_uid
root_uid, _ = paths.root_owner
if current_uid != root_uid:
raise UnsafeRun(current_uid, root_uid)

View File

@ -19,11 +19,14 @@
#
from __future__ import annotations
import os
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Set, Type
from typing import Set, Tuple, Type
from ahriman.core.exceptions import InvalidPath
@dataclass
@ -80,6 +83,13 @@ class RepositoryPaths:
"""
return self.root / "repository" / self.architecture
@property
def root_owner(self) -> Tuple[int, int]:
"""
:return: owner user and group of the root directory
"""
return self.owner(self.root)
@property
def sources(self) -> Path:
"""
@ -101,6 +111,16 @@ class RepositoryPaths:
if path.is_dir()
}
@staticmethod
def owner(path: Path) -> Tuple[int, int]:
"""
retrieve owner information by path
:param path: path for which extract ids
:return: owner user and group ids of the directory
"""
stat = path.stat()
return stat.st_uid, stat.st_gid
def cache_for(self, package_base: str) -> Path:
"""
get path to cached PKGBUILD and package sources for the package base
@ -109,6 +129,28 @@ class RepositoryPaths:
"""
return self.cache / package_base
def chown(self, path: Path) -> None:
"""
set owner of path recursively (from root) to root owner
:param path: path to be chown
"""
def set_owner(current: Path) -> None:
"""
set owner to the specified path
:param current: path to set
"""
uid, gid = self.owner(current)
if uid == root_uid and gid == root_gid:
return
os.chown(current, root_uid, root_gid, follow_symlinks=False)
if self.root not in path.parents:
raise InvalidPath(path, self.root)
root_uid, root_gid = self.root_owner
while path != self.root:
set_owner(path)
path = path.parent
def manual_for(self, package_base: str) -> Path:
"""
get manual path for specific package base
@ -158,3 +200,4 @@ class RepositoryPaths:
self.repository,
self.sources):
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
self.chown(directory)