From f984ea75d04b70705cf7464a25b374c83483b939 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Sat, 31 Dec 2022 03:22:54 +0200 Subject: [PATCH] fully lazy handle load In case of immediate handle load it would try to sync databases (or at least to create database files), which is not possible in case if command is run as non-ahriman user. This commit makes handle load lazy and allows to run some commands as non-ahriman user --- src/ahriman/application/handlers/setup.py | 4 +- src/ahriman/core/alpm/pacman.py | 67 +++++++++++++++++++---- tests/ahriman/core/alpm/test_pacman.py | 30 +++++----- 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/src/ahriman/application/handlers/setup.py b/src/ahriman/application/handlers/setup.py index f53473f9..f3ddad01 100644 --- a/src/ahriman/application/handlers/setup.py +++ b/src/ahriman/application/handlers/setup.py @@ -71,6 +71,8 @@ class Setup(Handler): Setup.configuration_create_sudo(application.repository.paths, args.build_command, architecture) application.repository.repo.init() + # lazy database sync + application.repository.pacman.handle # pylint: disable=pointless-statement @staticmethod def build_command(root: Path, prefix: str, architecture: str) -> Path: @@ -78,7 +80,7 @@ class Setup(Handler): generate build command name Args: - root(Path): root directory for the build command (must be root of the reporitory) + root(Path): root directory for the build command (must be root of the repository) prefix(str): command prefix in {prefix}-{architecture}-build architecture(str): repository architecture diff --git a/src/ahriman/core/alpm/pacman.py b/src/ahriman/core/alpm/pacman.py index 658c384c..fb3c6311 100644 --- a/src/ahriman/core/alpm/pacman.py +++ b/src/ahriman/core/alpm/pacman.py @@ -21,7 +21,7 @@ import shutil from pathlib import Path from pyalpm import DB, Handle, Package, SIG_PACKAGE, error as PyalpmError # type: ignore -from typing import Generator, Set +from typing import Any, Callable, Generator, Set from ahriman.core.configuration import Configuration from ahriman.core.log import LazyLogging @@ -36,6 +36,8 @@ class Pacman(LazyLogging): handle(Handle): pyalpm root ``Handle`` """ + handle: Handle + def __init__(self, architecture: str, configuration: Configuration, *, refresh_database: int) -> None: """ default constructor @@ -46,6 +48,22 @@ class Pacman(LazyLogging): refresh_database(int): synchronize local cache to remote. If set to ``0``, no syncronization will be enabled, if set to ``1`` - normal syncronization, if set to ``2`` - force syncronization """ + self.__create_handle_fn: Callable[[], Handle] = lambda: self.__create_handle( + architecture, configuration, refresh_database=refresh_database) + + def __create_handle(self, architecture: str, configuration: Configuration, *, refresh_database: int) -> Handle: + """ + create lazy handle function + + Args: + architecture(str): repository architecture + configuration(Configuration): configuration instance + refresh_database(int): synchronize local cache to remote. If set to ``0``, no syncronization will be + enabled, if set to ``1`` - normal syncronization, if set to ``2`` - force syncronization + + Returns: + Handle: fully initialized pacman handle + """ root = configuration.getpath("alpm", "root") pacman_root = configuration.getpath("alpm", "database") use_ahriman_cache = configuration.getboolean("alpm", "use_ahriman_cache") @@ -53,20 +71,42 @@ class Pacman(LazyLogging): paths = configuration.repository_paths database_path = paths.pacman if use_ahriman_cache else pacman_root - self.handle = Handle(str(root), str(database_path)) + handle = Handle(str(root), str(database_path)) for repository in configuration.getlist("alpm", "repositories"): - database = self.database_init(repository, mirror, architecture) - self.database_copy(database, pacman_root, paths, use_ahriman_cache=use_ahriman_cache) + database = self.database_init(handle, repository, mirror, architecture) + self.database_copy(handle, database, pacman_root, paths, use_ahriman_cache=use_ahriman_cache) if use_ahriman_cache and refresh_database: - self.database_sync(refresh_database > 1) + self.database_sync(handle, force=refresh_database > 1) - def database_copy(self, database: DB, pacman_root: Path, paths: RepositoryPaths, *, + return handle + + def __getattr__(self, item: str) -> Any: + """ + pacman handle extractor + + Args: + item(str): property name + + Returns: + Any: attribute by its name + + Raises: + AttributeError: in case if no such attribute found + """ + if item == "handle": + handle = self.__create_handle_fn() + setattr(self, item, handle) + return handle + return super().__getattr__(item) # required for logging attribute + + def database_copy(self, handle: Handle, database: DB, pacman_root: Path, paths: RepositoryPaths, *, use_ahriman_cache: bool) -> None: """ copy database from the operating system root to the ahriman local home Args: + handle(Handle): pacman handle which will be used for database copying database(DB): pacman database instance to be copied pacman_root(Path): operating system pacman root paths(RepositoryPaths): repository paths instance @@ -78,7 +118,7 @@ class Pacman(LazyLogging): if not use_ahriman_cache: return # copy root database if no local copy found - pacman_db_path = Path(self.handle.dbpath) + pacman_db_path = Path(handle.dbpath) if not pacman_db_path.is_dir(): return # root directory does not exist yet dst = repository_database(pacman_db_path) @@ -92,11 +132,12 @@ class Pacman(LazyLogging): shutil.copy(src, dst) paths.chown(dst) - def database_init(self, repository: str, mirror: str, architecture: str) -> DB: + def database_init(self, handle: Handle, repository: str, mirror: str, architecture: str) -> DB: """ create database instance from pacman handler and set its properties Args: + handle(Handle): pacman handle which will be used for database initializing repository(str): pacman repository name (e.g. core) mirror(str): arch linux mirror url architecture(str): repository architecture @@ -104,21 +145,23 @@ class Pacman(LazyLogging): Returns: DB: loaded pacman database instance """ - database: DB = self.handle.register_syncdb(repository, SIG_PACKAGE) + self.logger.info("loading pacman databases") + database: DB = handle.register_syncdb(repository, SIG_PACKAGE) # replace variables in mirror address database.servers = [mirror.replace("$repo", repository).replace("$arch", architecture)] return database - def database_sync(self, force: bool) -> None: + def database_sync(self, handle: Handle, *, force: bool) -> None: """ sync local database Args: + handle(Handle): pacman handle which will be used for database sync force(bool): force database syncronization (same as ``pacman -Syy``) """ self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force) - transaction = self.handle.init_transaction() - for database in self.handle.get_syncdbs(): + transaction = handle.init_transaction() + for database in handle.get_syncdbs(): try: database.update(force) except PyalpmError: diff --git a/tests/ahriman/core/alpm/test_pacman.py b/tests/ahriman/core/alpm/test_pacman.py index 9ed5373a..52501040 100644 --- a/tests/ahriman/core/alpm/test_pacman.py +++ b/tests/ahriman/core/alpm/test_pacman.py @@ -1,3 +1,5 @@ +import pytest + from pathlib import Path from pyalpm import error as PyalpmError from pytest_mock import MockerFixture @@ -21,8 +23,9 @@ def test_init_with_local_cache(configuration: Configuration, mocker: MockerFixtu with TemporaryDirectory(ignore_cleanup_errors=True) as pacman_root: mocker.patch.object(RepositoryPaths, "pacman", Path(pacman_root)) # during the creation pyalpm.Handle will create also version file which we would like to remove later - Pacman("x86_64", configuration, refresh_database=1) - sync_mock.assert_called_once_with(False) + pacman = Pacman("x86_64", configuration, refresh_database=1) + assert pacman.handle + sync_mock.assert_called_once_with(pytest.helpers.anyvar(int), force=False) def test_init_with_local_cache_forced(configuration: Configuration, mocker: MockerFixture) -> None: @@ -37,8 +40,9 @@ def test_init_with_local_cache_forced(configuration: Configuration, mocker: Mock with TemporaryDirectory(ignore_cleanup_errors=True) as pacman_root: mocker.patch.object(RepositoryPaths, "pacman", Path(pacman_root)) # during the creation pyalpm.Handle will create also version file which we would like to remove later - Pacman("x86_64", configuration, refresh_database=2) - sync_mock.assert_called_once_with(True) + pacman = Pacman("x86_64", configuration, refresh_database=2) + assert pacman.handle + sync_mock.assert_called_once_with(pytest.helpers.anyvar(int), force=True) def test_database_copy(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: @@ -54,7 +58,7 @@ def test_database_copy(pacman: Pacman, repository_paths: RepositoryPaths, mocker copy_mock = mocker.patch("shutil.copy") chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown") - pacman.database_copy(database, path, repository_paths, use_ahriman_cache=True) + pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True) copy_mock.assert_called_once_with(path / "sync" / "core.db", dst_path) chown_mock.assert_called_once_with(dst_path) @@ -70,7 +74,7 @@ def test_database_copy_skip(pacman: Pacman, repository_paths: RepositoryPaths, m mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: True if p.is_relative_to(path) else False) copy_mock = mocker.patch("shutil.copy") - pacman.database_copy(database, path, repository_paths, use_ahriman_cache=False) + pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=False) copy_mock.assert_not_called() @@ -85,7 +89,7 @@ def test_database_copy_no_directory(pacman: Pacman, repository_paths: Repository mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: True if p.is_relative_to(path) else False) copy_mock = mocker.patch("shutil.copy") - pacman.database_copy(database, path, repository_paths, use_ahriman_cache=True) + pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True) copy_mock.assert_not_called() @@ -100,7 +104,7 @@ def test_database_copy_no_root_file(pacman: Pacman, repository_paths: Repository mocker.patch("pathlib.Path.is_file", return_value=False) copy_mock = mocker.patch("shutil.copy") - pacman.database_copy(database, path, repository_paths, use_ahriman_cache=True) + pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True) copy_mock.assert_not_called() @@ -114,7 +118,7 @@ def test_database_copy_database_exist(pacman: Pacman, repository_paths: Reposito mocker.patch("pathlib.Path.is_file", return_value=True) copy_mock = mocker.patch("shutil.copy") - pacman.database_copy(database, Path("root"), repository_paths, use_ahriman_cache=True) + pacman.database_copy(pacman.handle, database, Path("root"), repository_paths, use_ahriman_cache=True) copy_mock.assert_not_called() @@ -123,7 +127,7 @@ def test_database_init(pacman: Pacman, configuration: Configuration) -> None: must init database with settings """ mirror = configuration.get("alpm", "mirror") - database = pacman.database_init("test", mirror, "x86_64") + database = pacman.database_init(pacman.handle, "test", mirror, "x86_64") assert len(database.servers) == 1 @@ -139,7 +143,7 @@ def test_database_sync(pacman: Pacman) -> None: handle_mock.init_transaction.return_value = transaction_mock pacman.handle = handle_mock - pacman.database_sync(False) + pacman.database_sync(pacman.handle, force=False) handle_mock.init_transaction.assert_called_once_with() core_mock.update.assert_called_once_with(False) extra_mock.update.assert_called_once_with(False) @@ -157,7 +161,7 @@ def test_database_sync_failed(pacman: Pacman) -> None: handle_mock.get_syncdbs.return_value = [core_mock, extra_mock] pacman.handle = handle_mock - pacman.database_sync(False) + pacman.database_sync(pacman.handle, force=False) extra_mock.update.assert_called_once_with(False) @@ -170,7 +174,7 @@ def test_database_sync_forced(pacman: Pacman) -> None: handle_mock.get_syncdbs.return_value = [core_mock] pacman.handle = handle_mock - pacman.database_sync(True) + pacman.database_sync(pacman.handle, force=True) handle_mock.init_transaction.assert_called_once_with() core_mock.update.assert_called_once_with(True)