mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-15 23:09:56 +00:00
Compare commits
4 Commits
2.14.2
...
54a331cc96
Author | SHA1 | Date | |
---|---|---|---|
54a331cc96 | |||
5f79cbc34b | |||
ea4193eef4 | |||
40fa94afbb |
@ -92,7 +92,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
```
|
||||
|
||||
* Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated.
|
||||
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typinng.Optional` (e.g. `str | None` instead of `Optional[str]`).
|
||||
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typing.Optional` (e.g. `str | None` instead of `Optional[str]`).
|
||||
* `classmethod` should (almost) always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
|
||||
* Recommended order of function definitions in class:
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/tests.yml)
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml)
|
||||
[](https://hub.docker.com/r/arcan1s/ahriman)
|
||||
[](https://hub.docker.com/r/arcan1s/ahriman)
|
||||
[](https://www.codefactor.io/repository/github/arcan1s/ahriman)
|
||||
[](https://ahriman.readthedocs.io)
|
||||
|
||||
|
@ -4,6 +4,14 @@ ahriman.core.build\_tools package
|
||||
Submodules
|
||||
----------
|
||||
|
||||
ahriman.core.build\_tools.package\_archive module
|
||||
-------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.build_tools.package_archive
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.build\_tools.sources module
|
||||
----------------------------------------
|
||||
|
||||
|
@ -124,14 +124,6 @@ ahriman.models.package module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.package\_archive module
|
||||
--------------------------------------
|
||||
|
||||
.. automodule:: ahriman.models.package_archive
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.package\_description module
|
||||
------------------------------------------
|
||||
|
||||
|
@ -81,14 +81,13 @@ Authorized users are stored inside internal database, if any of external provide
|
||||
|
||||
Build related configuration. Group name can refer to architecture, e.g. ``build:x86_64`` can be used for x86_64 architecture specific settings.
|
||||
|
||||
* ``allowed_scan_paths`` - paths to be used for implicit dependencies scan, scape separated list of paths, optional.
|
||||
* ``archbuild_flags`` - additional flags passed to ``archbuild`` command, space separated list of strings, optional.
|
||||
* ``blacklisted_scan_paths`` - paths to be excluded for implicit dependencies scan, scape separated list of paths, optional. Normally all elements of this option must be child paths of any of ``allowed_scan_paths`` element.
|
||||
* ``build_command`` - default build command, string, required.
|
||||
* ``ignore_packages`` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional.
|
||||
* ``include_debug_packages`` - distribute debug packages, boolean, optional, default ``yes``.
|
||||
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional.
|
||||
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
|
||||
* ``scan_paths`` - paths to be used for implicit dependencies scan, space separated list of strings, optional. If any of those paths is matched against the path, it will be added to the allowed list.
|
||||
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of definition.
|
||||
* ``triggers_known`` - optional list of ``ahriman.core.triggers.Trigger`` class implementations which are not run automatically and used only for trigger discovery and configuration validation.
|
||||
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, integer, optional, default is 7 days.
|
||||
|
@ -379,7 +379,7 @@ After the success build the application extracts all linked libraries and used d
|
||||
|
||||
In order to disable this check completely, the ``--no-check-files`` flag can be used.
|
||||
|
||||
In addition, there is possibility to control paths which will be used for checking, by using options ``build.allowed_scan_paths`` and ``build.blacklisted_scan_paths``. Leaving ``build.allowed_scan_paths`` blank will effectively disable any check too.
|
||||
In addition, there is possibility to control paths which will be used for checking, by using option ``build.scan_paths``, which supports regular expressions. Leaving ``build.scan_paths`` blank will effectively disable any check too.
|
||||
|
||||
How to install built packages
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -50,12 +50,8 @@ allow_read_only = yes
|
||||
;salt =
|
||||
|
||||
[build]
|
||||
; List of paths to be used for implicit dependency scan
|
||||
allowed_scan_paths = /usr/lib
|
||||
; List of additional flags passed to archbuild command.
|
||||
;archbuild_flags =
|
||||
; List of paths to be excluded for implicit dependency scan. Usually they should be subpaths of allowed_scan_paths
|
||||
blacklisted_scan_paths = /usr/lib/cmake
|
||||
; Path to build command
|
||||
;build_command =
|
||||
; List of packages to be ignored during automatic updates.
|
||||
@ -66,6 +62,8 @@ blacklisted_scan_paths = /usr/lib/cmake
|
||||
;makechrootpkg_flags =
|
||||
; List of additional flags passed to makepkg command.
|
||||
makepkg_flags = --nocolor --ignorearch
|
||||
; List of paths to be used for implicit dependency scan. Regular expressions are supported
|
||||
scan_paths = ^usr/lib(?!/cmake).*$
|
||||
; List of enabled triggers in the order of calls.
|
||||
triggers = ahriman.core.gitremote.RemotePullTrigger ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger ahriman.core.gitremote.RemotePushTrigger
|
||||
; List of well-known triggers. Used only for configuration purposes.
|
||||
|
@ -59,7 +59,7 @@ class Handler:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
|
||||
Returns:
|
||||
bool: True on success, False otherwise
|
||||
bool: ``True`` on success, ``False`` otherwise
|
||||
"""
|
||||
try:
|
||||
configuration = Configuration.from_path(args.configuration, repository_id)
|
||||
@ -129,7 +129,7 @@ class Handler:
|
||||
check condition and flag and raise ExitCode exception in case if it is enabled and condition match
|
||||
|
||||
Args:
|
||||
enabled(bool): if False no check will be performed
|
||||
enabled(bool): if ``False`` no check will be performed
|
||||
predicate(bool): indicates condition on which exception should be thrown
|
||||
|
||||
Raises:
|
||||
|
@ -97,7 +97,7 @@ class Lock(LazyLogging):
|
||||
fd(int): file descriptor:
|
||||
|
||||
Returns:
|
||||
bool: True in case if file is locked and False otherwise
|
||||
bool: ``True`` in case if file is locked and ``False`` otherwise
|
||||
"""
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
@ -119,7 +119,7 @@ class Lock(LazyLogging):
|
||||
watch until lock disappear
|
||||
|
||||
Returns:
|
||||
bool: True in case if file is locked and False otherwise
|
||||
bool: ``True`` in case if file is locked and ``False`` otherwise
|
||||
"""
|
||||
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
|
||||
# race conditions because multiple processes will be notified at the same time. Secondly, it is good library,
|
||||
@ -223,7 +223,7 @@ class Lock(LazyLogging):
|
||||
exc_tb(TracebackType): exception traceback if any
|
||||
|
||||
Returns:
|
||||
Literal[False]: always False (do not suppress any exception)
|
||||
Literal[False]: always ``False`` (do not suppress any exception)
|
||||
"""
|
||||
self.clear()
|
||||
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
||||
|
@ -101,7 +101,7 @@ class PacmanDatabase(SyncHttpClient):
|
||||
local_path(Path): path to locally stored file
|
||||
|
||||
Returns:
|
||||
bool: True in case if remote file is newer than local file
|
||||
bool: ``True`` in case if remote file is newer than local file
|
||||
|
||||
Raises:
|
||||
PacmanError: in case if no last-modified header was found
|
||||
|
@ -96,7 +96,7 @@ class Auth(LazyLogging):
|
||||
password(str | None): entered password
|
||||
|
||||
Returns:
|
||||
bool: True in case if password matches, False otherwise
|
||||
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||
"""
|
||||
del username, password
|
||||
return True
|
||||
@ -109,7 +109,7 @@ class Auth(LazyLogging):
|
||||
username(str): username
|
||||
|
||||
Returns:
|
||||
bool: True in case if user is known and can be authorized and False otherwise
|
||||
bool: ``True`` in case if user is known and can be authorized and ``False`` otherwise
|
||||
"""
|
||||
del username
|
||||
return True
|
||||
@ -124,7 +124,7 @@ class Auth(LazyLogging):
|
||||
context(str | None): URI request path
|
||||
|
||||
Returns:
|
||||
bool: True in case if user is allowed to do this request and False otherwise
|
||||
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||
"""
|
||||
del username, required, context
|
||||
return True
|
||||
|
@ -38,7 +38,7 @@ async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
|
||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||
|
||||
Returns:
|
||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||
"""
|
||||
if _has_aiohttp_security:
|
||||
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||
@ -54,7 +54,7 @@ async def check_authorized(*args: Any, **kwargs: Any) -> Any:
|
||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||
|
||||
Returns:
|
||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||
"""
|
||||
if _has_aiohttp_security:
|
||||
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||
@ -70,7 +70,7 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
|
||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||
|
||||
Returns:
|
||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||
"""
|
||||
if _has_aiohttp_security:
|
||||
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||
@ -86,7 +86,7 @@ async def remember(*args: Any, **kwargs: Any) -> Any:
|
||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||
|
||||
Returns:
|
||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||
"""
|
||||
if _has_aiohttp_security:
|
||||
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||
|
@ -57,7 +57,7 @@ class Mapping(Auth):
|
||||
password(str | None): entered password
|
||||
|
||||
Returns:
|
||||
bool: True in case if password matches, False otherwise
|
||||
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||
"""
|
||||
if password is None:
|
||||
return False # invalid data supplied
|
||||
@ -72,7 +72,7 @@ class Mapping(Auth):
|
||||
username(str): username
|
||||
|
||||
Returns:
|
||||
User | None: user descriptor if username is known and None otherwise
|
||||
User | None: user descriptor if username is known and ``None`` otherwise
|
||||
"""
|
||||
return self.database.user_get(username)
|
||||
|
||||
@ -84,7 +84,7 @@ class Mapping(Auth):
|
||||
username(str): username
|
||||
|
||||
Returns:
|
||||
bool: True in case if user is known and can be authorized and False otherwise
|
||||
bool: ``True`` in case if user is known and can be authorized and ``False`` otherwise
|
||||
"""
|
||||
return username is not None and self.get_user(username) is not None
|
||||
|
||||
@ -98,7 +98,7 @@ class Mapping(Auth):
|
||||
context(str | None): URI request path
|
||||
|
||||
Returns:
|
||||
bool: True in case if user is allowed to do this request and False otherwise
|
||||
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||
"""
|
||||
user = self.get_user(username)
|
||||
return user is not None and user.verify_access(required)
|
||||
|
@ -79,7 +79,7 @@ class PAM(Mapping):
|
||||
password(str | None): entered password
|
||||
|
||||
Returns:
|
||||
bool: True in case if password matches, False otherwise
|
||||
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||
"""
|
||||
if password is None:
|
||||
return False # invalid data supplied
|
||||
@ -101,7 +101,7 @@ class PAM(Mapping):
|
||||
username(str): username
|
||||
|
||||
Returns:
|
||||
bool: True in case if user is known and can be authorized and False otherwise
|
||||
bool: ``True`` in case if user is known and can be authorized and ``False`` otherwise
|
||||
"""
|
||||
try:
|
||||
_ = getpwnam(username)
|
||||
@ -119,7 +119,7 @@ class PAM(Mapping):
|
||||
context(str | None): URI request path
|
||||
|
||||
Returns:
|
||||
bool: True in case if user is allowed to do this request and False otherwise
|
||||
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||
"""
|
||||
# this method is basically inverted, first we check overrides in database and then fallback to the PAM logic
|
||||
if (user := self.get_user(username)) is not None:
|
||||
|
@ -17,7 +17,6 @@
|
||||
# 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 dataclasses import dataclass
|
||||
from elftools.elf.dynamic import DynamicSection
|
||||
from elftools.elf.elffile import ELFFile
|
||||
from pathlib import Path
|
||||
@ -33,7 +32,6 @@ from ahriman.models.package import Package
|
||||
from ahriman.models.scan_paths import ScanPaths
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageArchive:
|
||||
"""
|
||||
helper for package archives
|
||||
@ -45,10 +43,20 @@ class PackageArchive:
|
||||
scan_paths(ScanPaths): scan paths holder
|
||||
"""
|
||||
|
||||
root: Path
|
||||
package: Package
|
||||
pacman: Pacman
|
||||
scan_paths: ScanPaths
|
||||
def __init__(self, root: Path, package: Package, pacman: Pacman, scan_paths: ScanPaths) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
root(Path): path to root filesystem
|
||||
package(Package): package descriptor
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
scan_paths(ScanPaths): scan paths holder
|
||||
"""
|
||||
self.root = root
|
||||
self.package = package
|
||||
self.pacman = pacman
|
||||
self.scan_paths = scan_paths
|
||||
|
||||
@staticmethod
|
||||
def dynamic_needed(binary_path: Path) -> list[str]:
|
@ -138,7 +138,7 @@ class Sources(LazyLogging):
|
||||
sources_dir(Path): local path to git repository
|
||||
|
||||
Returns:
|
||||
bool: True in case if there is any remote and false otherwise
|
||||
bool: ``True`` in case if there is any remote and false otherwise
|
||||
"""
|
||||
instance = Sources()
|
||||
remotes = check_output(*instance.git(), "remote", cwd=sources_dir, logger=instance.logger)
|
||||
@ -261,7 +261,7 @@ class Sources(LazyLogging):
|
||||
commit_author(tuple[str, str] | None, optional): optional commit author if any (Default value = None)
|
||||
|
||||
Returns:
|
||||
bool: True in case if changes have been committed and False otherwise
|
||||
bool: ``True`` in case if changes have been committed and ``False`` otherwise
|
||||
"""
|
||||
if not self.has_changes(sources_dir):
|
||||
return False # nothing to commit
|
||||
@ -351,7 +351,7 @@ class Sources(LazyLogging):
|
||||
sources_dir(Path): local path to git repository
|
||||
|
||||
Returns:
|
||||
bool: True if there are uncommitted changes and False otherwise
|
||||
bool: ``True`` if there are uncommitted changes and ``False`` otherwise
|
||||
"""
|
||||
# there is --exit-code argument to diff, however, there might be other process errors
|
||||
changes = check_output(*self.git(), "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger)
|
||||
|
@ -169,14 +169,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"build": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"allowed_scan_paths": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
},
|
||||
},
|
||||
"archbuild_flags": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
@ -185,14 +177,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
"blacklisted_scan_paths": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
},
|
||||
},
|
||||
"build_command": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
@ -226,6 +210,14 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
"scan_paths": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
"triggers": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
|
@ -149,7 +149,7 @@ class Validator(RootValidator):
|
||||
check if paths exists
|
||||
|
||||
Args:
|
||||
constraint(bool): True in case if path must exist and False otherwise
|
||||
constraint(bool): ``True`` in case if path must exist and ``False`` otherwise
|
||||
field(str): field name to be checked
|
||||
value(Path): value to be checked
|
||||
|
||||
|
38
src/ahriman/core/database/migrations/m014_auditlog.py
Normal file
38
src/ahriman/core/database/migrations/m014_auditlog.py
Normal file
@ -0,0 +1,38 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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/>.
|
||||
#
|
||||
__all__ = ["steps"]
|
||||
|
||||
|
||||
steps = [
|
||||
"""
|
||||
create table auditlog (
|
||||
created integer not null,
|
||||
repository text not null,
|
||||
event text not null,
|
||||
object_id text not null,
|
||||
message text,
|
||||
data json
|
||||
)
|
||||
""",
|
||||
"""
|
||||
create index auditlog_created_repository_event_object_id
|
||||
on auditlog (created, repository, event, object_id)
|
||||
""",
|
||||
]
|
@ -21,6 +21,7 @@ from ahriman.core.database.operations.auth_operations import AuthOperations
|
||||
from ahriman.core.database.operations.build_operations import BuildOperations
|
||||
from ahriman.core.database.operations.changes_operations import ChangesOperations
|
||||
from ahriman.core.database.operations.dependencies_operations import DependenciesOperations
|
||||
from ahriman.core.database.operations.event_operations import EventOperations
|
||||
from ahriman.core.database.operations.logs_operations import LogsOperations
|
||||
from ahriman.core.database.operations.package_operations import PackageOperations
|
||||
from ahriman.core.database.operations.patch_operations import PatchOperations
|
||||
|
@ -39,7 +39,7 @@ class DependenciesOperations(Operations):
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
|
||||
Returns:
|
||||
Dependencies: changes for the package base if available
|
||||
Dependencies: dependencies for the package base if available
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
|
104
src/ahriman/core/database/operations/event_operations.py
Normal file
104
src/ahriman/core/database/operations/event_operations.py
Normal file
@ -0,0 +1,104 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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/>.
|
||||
#
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.database.operations.operations import Operations
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class EventOperations(Operations):
|
||||
"""
|
||||
operations for audit log table
|
||||
"""
|
||||
|
||||
def event_get(self, event: str | EventType | None = None, object_id: str | None = None,
|
||||
limit: int = -1, offset: int = 0, repository_id: RepositoryId | None = None) -> list[Event]:
|
||||
"""
|
||||
get list of events with filters applied
|
||||
|
||||
Args:
|
||||
event(str | EventType | None, optional): filter by event type (Default value = None)
|
||||
object_id(str | None, optional): filter by event object (Default value = None)
|
||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
|
||||
Returns:
|
||||
list[Event]: list of audit log events
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> list[Event]:
|
||||
return [
|
||||
Event(
|
||||
event=row["event"],
|
||||
object_id=row["object_id"],
|
||||
message=row["message"],
|
||||
data=row["data"],
|
||||
created=row["created"],
|
||||
) for row in connection.execute(
|
||||
"""
|
||||
select created, event, object_id, message, data from auditlog
|
||||
where (:event is null or event = :event)
|
||||
and (:object_id is null or object_id = :object_id)
|
||||
and repository = :repository
|
||||
order by created limit :limit offset :offset
|
||||
""",
|
||||
{
|
||||
"event": event,
|
||||
"object_id": object_id,
|
||||
"repository": repository_id.id,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
return self.with_connection(run)
|
||||
|
||||
def event_insert(self, event: Event, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
insert audit log event
|
||||
|
||||
Args:
|
||||
event(Event): event to insert
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
insert into auditlog
|
||||
(created, repository, event, object_id, message, data)
|
||||
values
|
||||
(:created, :repository, :event, :object_id, :message, :data)
|
||||
""",
|
||||
{
|
||||
"created": event.created,
|
||||
"repository": repository_id.id,
|
||||
"event": event.event,
|
||||
"object_id": event.object_id,
|
||||
"message": event.message,
|
||||
"data": event.data,
|
||||
})
|
||||
|
||||
return self.with_connection(run, commit=True)
|
@ -74,7 +74,7 @@ class Operations(LazyLogging):
|
||||
|
||||
Args:
|
||||
query(Callable[[Connection], T]): function to be called with connection
|
||||
commit(bool, optional): if True commit() will be called on success (Default value = False)
|
||||
commit(bool, optional): if ``True`` commit() will be called on success (Default value = False)
|
||||
|
||||
Returns:
|
||||
T: result of the ``query`` call
|
||||
|
@ -26,7 +26,7 @@ from typing import Self
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.migrations import Migrations
|
||||
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
|
||||
DependenciesOperations, LogsOperations, PackageOperations, PatchOperations
|
||||
DependenciesOperations, EventOperations, LogsOperations, PackageOperations, PatchOperations
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
@ -35,6 +35,7 @@ class SQLite(
|
||||
BuildOperations,
|
||||
ChangesOperations,
|
||||
DependenciesOperations,
|
||||
EventOperations,
|
||||
LogsOperations,
|
||||
PackageOperations,
|
||||
PatchOperations):
|
||||
|
@ -32,7 +32,7 @@ class BuildPrinter(StringPrinter):
|
||||
|
||||
Args:
|
||||
package(Package): built package
|
||||
is_success(bool): True in case if build has success status and False otherwise
|
||||
is_success(bool): ``True`` in case if build has success status and ``False`` otherwise
|
||||
use_utf(bool): use utf instead of normal symbols
|
||||
"""
|
||||
StringPrinter.__init__(self, f"{self.sign(is_success, use_utf)} {package.base}")
|
||||
@ -43,7 +43,7 @@ class BuildPrinter(StringPrinter):
|
||||
generate sign according to settings
|
||||
|
||||
Args:
|
||||
is_success(bool): True in case if build has success status and False otherwise
|
||||
is_success(bool): ``True`` in case if build has success status and ``False`` otherwise
|
||||
use_utf(bool): use utf instead of normal symbols
|
||||
|
||||
Returns:
|
||||
|
@ -57,7 +57,7 @@ class ChangesPrinter(Printer):
|
||||
generate entry title from content
|
||||
|
||||
Returns:
|
||||
str | None: content title if it can be generated and None otherwise
|
||||
str | None: content title if it can be generated and ``None`` otherwise
|
||||
"""
|
||||
if self.changes.is_empty:
|
||||
return None
|
||||
|
@ -63,7 +63,7 @@ class Printer:
|
||||
generate entry title from content
|
||||
|
||||
Returns:
|
||||
str | None: content title if it can be generated and None otherwise
|
||||
str | None: content title if it can be generated and ``None`` otherwise
|
||||
"""
|
||||
return None
|
||||
|
||||
|
@ -42,6 +42,6 @@ class StringPrinter(Printer):
|
||||
generate entry title from content
|
||||
|
||||
Returns:
|
||||
str | None: content title if it can be generated and None otherwise
|
||||
str | None: content title if it can be generated and ``None`` otherwise
|
||||
"""
|
||||
return self.content
|
||||
|
@ -85,7 +85,7 @@ class SyncHttpClient(LazyLogging):
|
||||
exception(requests.RequestException): exception raised
|
||||
|
||||
Returns:
|
||||
str: text of the response if it is not None and empty string otherwise
|
||||
str: text of the response if it is not ``None`` and empty string otherwise
|
||||
"""
|
||||
result: str = exception.response.text if exception.response is not None else ""
|
||||
return result
|
||||
|
@ -38,8 +38,8 @@ class JinjaTemplate:
|
||||
|
||||
* homepage - link to homepage, string, optional
|
||||
* link_path - prefix fo packages to download, string, required
|
||||
* has_package_signed - True in case if package sign enabled, False otherwise, required
|
||||
* has_repo_signed - True in case if repository database sign enabled, False otherwise, required
|
||||
* has_package_signed - ``True`` in case if package sign enabled, ``False`` otherwise, required
|
||||
* has_repo_signed - ``True`` in case if repository database sign enabled, ``False`` otherwise, required
|
||||
* packages - sorted list of packages properties, required
|
||||
* architecture, string
|
||||
* archive_size, pretty printed size, string
|
||||
|
@ -78,7 +78,7 @@ class RemoteCall(Report):
|
||||
process_id(str): remote process id
|
||||
|
||||
Returns:
|
||||
bool: True in case if remote process is alive and False otherwise
|
||||
bool: ``True`` in case if remote process is alive and ``False`` otherwise
|
||||
"""
|
||||
try:
|
||||
response = self.client.make_request("GET", f"{self.client.address}/api/v1/service/process/{process_id}")
|
||||
|
@ -23,13 +23,13 @@ from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from ahriman.core.build_tools.package_archive import PackageArchive
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.core.utils import safe_filename
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_archive import PackageArchive
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.packagers import Packagers
|
||||
from ahriman.models.result import Result
|
||||
|
@ -80,10 +80,7 @@ class RepositoryProperties(LazyLogging):
|
||||
self.reporter = Client.load(repository_id, configuration, database, report=report)
|
||||
self.triggers = TriggerLoader.load(repository_id, configuration)
|
||||
|
||||
self.scan_paths = ScanPaths(
|
||||
allowed_paths=configuration.getpathlist("build", "allowed_scan_paths", fallback=[]),
|
||||
blacklisted_paths=configuration.getpathlist("build", "blacklisted_scan_paths", fallback=[]),
|
||||
)
|
||||
self.scan_paths = ScanPaths(configuration.getlist("build", "scan_paths", fallback=[]))
|
||||
|
||||
@property
|
||||
def architecture(self) -> str:
|
||||
|
@ -72,7 +72,7 @@ class Spawn(Thread, LazyLogging):
|
||||
value(bool): command line argument value
|
||||
|
||||
Returns:
|
||||
str: if ``value`` is True, then returns positive flag and negative otherwise
|
||||
str: if ``value`` is ``True``, then returns positive flag and negative otherwise
|
||||
"""
|
||||
return name if value else f"no-{name}"
|
||||
|
||||
@ -153,7 +153,7 @@ class Spawn(Thread, LazyLogging):
|
||||
process_id(str): process id to be checked as returned by :func:`_spawn_process()`
|
||||
|
||||
Returns:
|
||||
bool: True in case if process still counts as active and False otherwise
|
||||
bool: ``True`` in case if process still counts as active and ``False`` otherwise
|
||||
"""
|
||||
with self._lock:
|
||||
return process_id in self.active
|
||||
|
@ -25,6 +25,7 @@ from ahriman.core.database import SQLite
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
@ -79,6 +80,37 @@ class Client:
|
||||
|
||||
return make_local_client()
|
||||
|
||||
def event_add(self, event: Event) -> None:
|
||||
"""
|
||||
create new event
|
||||
|
||||
Args:
|
||||
event(Event): audit log event
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def event_get(self, event: str | EventType | None, object_id: str | None,
|
||||
limit: int = -1, offset: int = 0) -> list[Event]:
|
||||
"""
|
||||
retrieve list of events
|
||||
|
||||
Args:
|
||||
event(str | EventType | None): filter by event type
|
||||
object_id(str | None): filter by event object
|
||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
|
||||
Returns:
|
||||
list[Event]: list of audit log events
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
get package changes
|
||||
@ -184,7 +216,7 @@ class Client:
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
version(str | None): package version to remove logs. If None set, all logs will be removed
|
||||
version(str | None): package version to remove logs. If ``None`` is set, all logs will be removed
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
@ -213,7 +245,7 @@ class Client:
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
variable(str | None): patch name. If None set, all patches will be removed
|
||||
variable(str | None): patch name. If ``None`` is set, all patches will be removed
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
|
@ -22,6 +22,7 @@ from ahriman.core.status import Client
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
@ -48,6 +49,31 @@ class LocalClient(Client):
|
||||
self.database = database
|
||||
self.repository_id = repository_id
|
||||
|
||||
def event_add(self, event: Event) -> None:
|
||||
"""
|
||||
create new event
|
||||
|
||||
Args:
|
||||
event(Event): audit log event
|
||||
"""
|
||||
self.database.event_insert(event, self.repository_id)
|
||||
|
||||
def event_get(self, event: str | EventType | None, object_id: str | None,
|
||||
limit: int = -1, offset: int = 0) -> list[Event]:
|
||||
"""
|
||||
retrieve list of events
|
||||
|
||||
Args:
|
||||
event(str | EventType | None): filter by event type
|
||||
object_id(str | None): filter by event object
|
||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
|
||||
Returns:
|
||||
list[Event]: list of audit log events
|
||||
"""
|
||||
return self.database.event_get(event, object_id, limit, offset, self.repository_id)
|
||||
|
||||
def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
get package changes
|
||||
@ -138,7 +164,7 @@ class LocalClient(Client):
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
version(str | None): package version to remove logs. If None set, all logs will be removed
|
||||
version(str | None): package version to remove logs. If ``None`` is set, all logs will be removed
|
||||
"""
|
||||
self.database.logs_remove(package_base, version, self.repository_id)
|
||||
|
||||
@ -162,7 +188,7 @@ class LocalClient(Client):
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
variable(str | None): patch name. If None set, all patches will be removed
|
||||
variable(str | None): patch name. If ``None`` is set, all patches will be removed
|
||||
"""
|
||||
variables = [variable] if variable is not None else None
|
||||
self.database.patches_remove(package_base, variables)
|
||||
|
@ -27,6 +27,7 @@ from ahriman.core.status import Client
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
@ -68,6 +69,10 @@ class Watcher(LazyLogging):
|
||||
with self._lock:
|
||||
return list(self._known.values())
|
||||
|
||||
event_add: Callable[[Event], None]
|
||||
|
||||
event_get: Callable[[str | EventType | None, str | None, int, int], list[Event]]
|
||||
|
||||
def load(self) -> None:
|
||||
"""
|
||||
load packages from local database
|
||||
|
@ -27,6 +27,7 @@ from ahriman.core.status import Client
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
@ -109,6 +110,15 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
"""
|
||||
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/dependencies"
|
||||
|
||||
def _events_url(self) -> str:
|
||||
"""
|
||||
get url for the events api
|
||||
|
||||
Returns:
|
||||
str: full url for web service for events
|
||||
"""
|
||||
return f"{self.address}/api/v1/events"
|
||||
|
||||
def _logs_url(self, package_base: str) -> str:
|
||||
"""
|
||||
get url for the logs api
|
||||
@ -157,6 +167,44 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
"""
|
||||
return f"{self.address}/api/v1/status"
|
||||
|
||||
def event_add(self, event: Event) -> None:
|
||||
"""
|
||||
create new event
|
||||
|
||||
Args:
|
||||
event(Event): audit log event
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._events_url(), params=self.repository_id.query(), json=event.view())
|
||||
|
||||
def event_get(self, event: str | EventType | None, object_id: str | None,
|
||||
limit: int = -1, offset: int = 0) -> list[Event]:
|
||||
"""
|
||||
retrieve list of events
|
||||
|
||||
Args:
|
||||
event(str | EventType | None): filter by event type
|
||||
object_id(str | None): filter by event object
|
||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
|
||||
Returns:
|
||||
list[Event]: list of audit log events
|
||||
"""
|
||||
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
||||
if event is not None:
|
||||
query.append(("event", str(event)))
|
||||
if object_id is not None:
|
||||
query.append(("object_id", object_id))
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
response = self.make_request("GET", self._events_url(), params=query)
|
||||
response_json = response.json()
|
||||
|
||||
return [Event.from_json(event) for event in response_json]
|
||||
|
||||
return []
|
||||
|
||||
def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
get package changes
|
||||
@ -274,8 +322,9 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
Returns:
|
||||
list[tuple[float, str]]: package logs
|
||||
"""
|
||||
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
||||
response = self.make_request("GET", self._logs_url(package_base), params=query)
|
||||
response_json = response.json()
|
||||
|
||||
@ -289,12 +338,13 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
version(str | None): package version to remove logs. If None set, all logs will be removed
|
||||
version(str | None): package version to remove logs. If ``None`` is set, all logs will be removed
|
||||
"""
|
||||
query = self.repository_id.query()
|
||||
if version is not None:
|
||||
query += [("version", version)]
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
query = self.repository_id.query()
|
||||
if version is not None:
|
||||
query += [("version", version)]
|
||||
self.make_request("DELETE", self._logs_url(package_base), params=query)
|
||||
|
||||
def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
||||
@ -323,7 +373,7 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
variable(str | None): patch name. If None set, all patches will be removed
|
||||
variable(str | None): patch name. If ``None`` is set, all patches will be removed
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("DELETE", self._patches_url(package_base, variable or ""))
|
||||
@ -361,6 +411,7 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
payload = {"status": status.value}
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._package_url(package_base),
|
||||
params=self.repository_id.query(), json=payload)
|
||||
@ -380,6 +431,7 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
"status": status.value,
|
||||
"package": package.view(),
|
||||
}
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._package_url(package.base),
|
||||
params=self.repository_id.query(), json=payload)
|
||||
@ -407,5 +459,6 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
status(BuildStatusEnum): current ahriman status
|
||||
"""
|
||||
payload = {"status": status.value}
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._status_url(), params=self.repository_id.query(), json=payload)
|
||||
|
@ -64,7 +64,7 @@ class Leaf:
|
||||
packages(Iterable[Leaf]): list of known leaves
|
||||
|
||||
Returns:
|
||||
bool: True in case if package is dependency of others and False otherwise
|
||||
bool: ``True`` in case if package is dependency of others and ``False`` otherwise
|
||||
"""
|
||||
for leaf in packages:
|
||||
if leaf.dependencies.intersection(self.items):
|
||||
@ -79,7 +79,7 @@ class Leaf:
|
||||
packages(Iterable[Leaf]): list of known leaves
|
||||
|
||||
Returns:
|
||||
bool: True if any of packages is dependency of the leaf, False otherwise
|
||||
bool: ``True`` if any of packages is dependency of the leaf, ``False`` otherwise
|
||||
"""
|
||||
for leaf in packages:
|
||||
if self.dependencies.intersection(leaf.items):
|
||||
|
@ -160,7 +160,7 @@ class GitHub(Upload, HttpUpload):
|
||||
get release object if any
|
||||
|
||||
Returns:
|
||||
dict[str, Any] | None: GitHub API release object if release found and None otherwise
|
||||
dict[str, Any] | None: GitHub API release object if release found and ``None`` otherwise
|
||||
"""
|
||||
url = f"https://api.github.com/repos/{self.github_owner}/{
|
||||
self.github_repository}/releases/tags/{self.github_release_tag}"
|
||||
|
@ -225,8 +225,8 @@ def extract_user() -> str | None:
|
||||
extract user from system environment
|
||||
|
||||
Returns:
|
||||
str | None: SUDO_USER in case if set and USER otherwise. It can return None in case if environment has been
|
||||
cleared before application start
|
||||
str | None: SUDO_USER in case if set and USER otherwise. It can return ``None`` in case if environment has been
|
||||
cleared before application start
|
||||
"""
|
||||
return os.getenv("SUDO_USER") or os.getenv("DOAS_USER") or os.getenv("USER")
|
||||
|
||||
@ -295,7 +295,7 @@ def package_like(filename: Path) -> bool:
|
||||
filename(Path): name of file to check
|
||||
|
||||
Returns:
|
||||
bool: True in case if name contains ``.pkg.`` and not signature, False otherwise
|
||||
bool: ``True`` in case if name contains ``.pkg.`` and not signature, ``False`` otherwise
|
||||
"""
|
||||
name = filename.name
|
||||
return not name.startswith(".") and ".pkg." in name and not name.endswith(".sig")
|
||||
|
@ -44,7 +44,7 @@ class AuthSettings(StrEnum):
|
||||
get enabled flag
|
||||
|
||||
Returns:
|
||||
bool: False in case if authorization is disabled and True otherwise
|
||||
bool: ``False`` in case if authorization is disabled and ``True`` otherwise
|
||||
"""
|
||||
return self != AuthSettings.Disabled
|
||||
|
||||
|
92
src/ahriman/models/event.py
Normal file
92
src/ahriman/models/event.py
Normal file
@ -0,0 +1,92 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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/>.
|
||||
#
|
||||
from dataclasses import dataclass, field, fields
|
||||
from enum import StrEnum
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.core.utils import dataclass_view, filter_json, utcnow
|
||||
|
||||
|
||||
class EventType(StrEnum):
|
||||
"""
|
||||
predefined event types
|
||||
|
||||
Attributes:
|
||||
PackageOutdated(EventType): (class attribute) package has been marked as out-of-date
|
||||
PackageRemoved(EventType): (class attribute) package has been removed
|
||||
PackageUpdateFailed(EventType): (class attribute) package update has been failed
|
||||
PackageUpdated(EventType): (class attribute) package has been updated
|
||||
"""
|
||||
|
||||
PackageOutdated = "package-outdated"
|
||||
PackageRemoved = "package-removed"
|
||||
PackageUpdateFailed = "package-update-failed"
|
||||
PackageUpdated = "package-updated"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Event:
|
||||
"""
|
||||
audit log event
|
||||
|
||||
Attributes:
|
||||
created(int): event timestamp
|
||||
data(dict[str, Any]): event metadata
|
||||
event(str | EventType): event type
|
||||
message(str | None): event message if available
|
||||
object_id(str): object identifier
|
||||
"""
|
||||
|
||||
event: str | EventType
|
||||
object_id: str
|
||||
message: str | None = None
|
||||
data: dict[str, Any] = field(default_factory=dict)
|
||||
created: int = field(default_factory=lambda: int(utcnow().timestamp()))
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""
|
||||
convert event type to enum if it is a well-known event type
|
||||
"""
|
||||
if self.event in EventType:
|
||||
object.__setattr__(self, "event", EventType(self.event))
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||
"""
|
||||
construct event from the json dump
|
||||
|
||||
Args:
|
||||
dump(dict[str, Any]): json dump body
|
||||
|
||||
Returns:
|
||||
Self: dependencies object
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
return cls(**filter_json(dump, known_fields))
|
||||
|
||||
def view(self) -> dict[str, Any]:
|
||||
"""
|
||||
generate json event view
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: json-friendly dictionary
|
||||
"""
|
||||
return dataclass_view(self)
|
@ -41,7 +41,7 @@ class MigrationResult:
|
||||
check migration and check if there are pending migrations
|
||||
|
||||
Returns:
|
||||
bool: True in case if it requires migrations and False otherwise
|
||||
bool: ``True`` in case if it requires migrations and ``False`` otherwise
|
||||
"""
|
||||
self.validate()
|
||||
return self.new_version > self.old_version
|
||||
|
@ -158,7 +158,7 @@ class Package(LazyLogging):
|
||||
get VCS flag based on the package base
|
||||
|
||||
Returns:
|
||||
bool: True in case if package base looks like VCS package and False otherwise
|
||||
bool: ``True`` in case if package base looks like VCS package and ``False`` otherwise
|
||||
"""
|
||||
return self.base.endswith("-bzr") \
|
||||
or self.base.endswith("-csv")\
|
||||
@ -504,7 +504,7 @@ class Package(LazyLogging):
|
||||
timestamp(float | int): timestamp to check build date against
|
||||
|
||||
Returns:
|
||||
bool: True in case if package was built after the specified date and False otherwise. In case if build date
|
||||
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise. In case if build date
|
||||
is not set by any of packages, it returns False
|
||||
"""
|
||||
return any(
|
||||
@ -528,7 +528,7 @@ class Package(LazyLogging):
|
||||
(Default value = True)
|
||||
|
||||
Returns:
|
||||
bool: True if the package is out-of-dated and False otherwise
|
||||
bool: ``True`` if the package is out-of-dated and ``False`` otherwise
|
||||
"""
|
||||
min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age
|
||||
if calculate_version and not self.is_newer_than(min_vcs_build_date):
|
||||
|
@ -55,7 +55,7 @@ class PkgbuildPatch:
|
||||
parse key and define whether it function or not
|
||||
|
||||
Returns:
|
||||
bool: True in case if key ends with parentheses and False otherwise
|
||||
bool: ``True`` in case if key ends with parentheses and ``False`` otherwise
|
||||
"""
|
||||
return self.key is not None and self.key.endswith("()")
|
||||
|
||||
@ -65,7 +65,7 @@ class PkgbuildPatch:
|
||||
check if patch is full diff one or just single-variable patch
|
||||
|
||||
Returns:
|
||||
bool: True in case key set and False otherwise
|
||||
bool: ``True`` in case key set and ``False`` otherwise
|
||||
"""
|
||||
return self.key is None
|
||||
|
||||
|
@ -29,7 +29,7 @@ class Property:
|
||||
Attributes:
|
||||
name(str): name of the property
|
||||
value(Any): property value
|
||||
is_required(bool): if set to True then this property is required
|
||||
is_required(bool): if set to ``True`` then this property is required
|
||||
indent(int): property indentation level
|
||||
"""
|
||||
|
||||
|
@ -57,7 +57,7 @@ class RemoteSource:
|
||||
check if source is remote
|
||||
|
||||
Returns:
|
||||
bool: True in case if package is well-known remote source (e.g. AUR) and False otherwise
|
||||
bool: ``True`` in case if package is well-known remote source (e.g. AUR) and ``False`` otherwise
|
||||
"""
|
||||
return self.source in (PackageSource.AUR, PackageSource.Repository)
|
||||
|
||||
|
@ -52,7 +52,7 @@ class RepositoryId:
|
||||
check if all data is supplied for the loading
|
||||
|
||||
Returns:
|
||||
bool: True in case if architecture or name are not set and False otherwise
|
||||
bool: ``True`` in case if architecture or name are not set and ``False`` otherwise
|
||||
"""
|
||||
return not self.architecture or not self.name
|
||||
|
||||
@ -85,7 +85,7 @@ class RepositoryId:
|
||||
other(Any): other object to compare
|
||||
|
||||
Returns:
|
||||
bool: True in case if this is less than other and False otherwise
|
||||
bool: ``True`` in case if this is less than other and ``False`` otherwise
|
||||
|
||||
Raises:
|
||||
TypeError: if other is different from RepositoryId type
|
||||
|
@ -82,7 +82,7 @@ class Result:
|
||||
get if build result is empty or not
|
||||
|
||||
Returns:
|
||||
bool: True in case if success list is empty and False otherwise
|
||||
bool: ``True`` in case if success list is empty and ``False`` otherwise
|
||||
"""
|
||||
return not self._added and not self._updated
|
||||
|
||||
@ -191,7 +191,7 @@ class Result:
|
||||
other(Any): other object instance
|
||||
|
||||
Returns:
|
||||
bool: True if the other object is the same and False otherwise
|
||||
bool: ``True`` if the other object is the same and ``False`` otherwise
|
||||
"""
|
||||
if not isinstance(other, Result):
|
||||
return False
|
||||
|
@ -17,29 +17,33 @@
|
||||
# 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 re
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@dataclass(frozen=True)
|
||||
class ScanPaths:
|
||||
"""
|
||||
paths used for scan filesystem
|
||||
|
||||
Attributes:
|
||||
allowed_paths(list[Path]): list of whitelisted paths
|
||||
blacklisted_paths(list[Path]): list of paths to be skipped from scan
|
||||
paths(list[str]): list of regular expressions to be used to match paths
|
||||
"""
|
||||
|
||||
allowed_paths: list[Path]
|
||||
blacklisted_paths: list[Path]
|
||||
paths: list[str]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@cached_property
|
||||
def patterns(self) -> list[re.Pattern[str]]:
|
||||
"""
|
||||
compute relative to / paths
|
||||
compiled regular expressions
|
||||
|
||||
Returns:
|
||||
list[re.Pattern]: a list of compiled regular expressions
|
||||
"""
|
||||
object.__setattr__(self, "allowed_paths", [path.relative_to("/") for path in self.allowed_paths])
|
||||
object.__setattr__(self, "blacklisted_paths", [path.relative_to("/") for path in self.blacklisted_paths])
|
||||
return [re.compile(path) for path in self.paths]
|
||||
|
||||
def is_allowed(self, path: Path) -> bool:
|
||||
"""
|
||||
@ -49,10 +53,7 @@ class ScanPaths:
|
||||
path(Path): path to be checked
|
||||
|
||||
Returns:
|
||||
bool: ``True`` in case if :attr:`allowed_paths` contains element which is parent for the path and
|
||||
:attr:`blacklisted_paths` doesn't and ``False`` otherwise
|
||||
bool: ``True`` in case if :attr:`paths` contains at least one element to which the path is matched
|
||||
and ``False`` otherwise
|
||||
"""
|
||||
if any(path.is_relative_to(blacklisted) for blacklisted in self.blacklisted_paths):
|
||||
return False # path is blacklisted
|
||||
# check if we actually have to check this path
|
||||
return any(path.is_relative_to(allowed) for allowed in self.allowed_paths)
|
||||
return any(pattern.match(str(path)) for pattern in self.patterns)
|
||||
|
@ -98,7 +98,7 @@ class User:
|
||||
salt(str): salt for hashed password
|
||||
|
||||
Returns:
|
||||
bool: True in case if password matches, False otherwise
|
||||
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||
"""
|
||||
try:
|
||||
verified: bool = self._HASHER.verify(password + salt, self.password)
|
||||
@ -131,7 +131,7 @@ class User:
|
||||
required(UserAccess): required access level
|
||||
|
||||
Returns:
|
||||
bool: True in case if user is allowed to do this request and False otherwise
|
||||
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||
"""
|
||||
return self.access.permits(required)
|
||||
|
||||
|
@ -47,7 +47,7 @@ class UserAccess(StrEnum):
|
||||
other(UserAccess): other permission to compare
|
||||
|
||||
Returns:
|
||||
bool: True in case if current permission allows the operation and False otherwise
|
||||
bool: ``True`` in case if current permission allows the operation and ``False`` otherwise
|
||||
"""
|
||||
for member in UserAccess:
|
||||
if member == other:
|
||||
|
@ -63,7 +63,7 @@ class _AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy):
|
||||
identity(str): username
|
||||
|
||||
Returns:
|
||||
str | None: user identity (username) in case if user exists and None otherwise
|
||||
str | None: user identity (username) in case if user exists and ``None`` otherwise
|
||||
"""
|
||||
return identity if await self.validator.known_username(identity) else None
|
||||
|
||||
@ -77,7 +77,7 @@ class _AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy):
|
||||
context(str | None, optional): URI request path (Default value = None)
|
||||
|
||||
Returns:
|
||||
bool: True in case if user is allowed to perform this request and False otherwise
|
||||
bool: ``True`` in case if user is allowed to perform this request and ``False`` otherwise
|
||||
"""
|
||||
# some methods for type checking and parent class compatibility
|
||||
if identity is None or not isinstance(permission, UserAccess):
|
||||
|
@ -38,7 +38,7 @@ def _is_templated_unauthorized(request: Request) -> bool:
|
||||
request(Request): source request to check
|
||||
|
||||
Returns:
|
||||
bool: True in case if response should be rendered as html and False otherwise
|
||||
bool: ``True`` in case if response should be rendered as html and ``False`` otherwise
|
||||
"""
|
||||
return request.path in ("/api/v1/login", "/api/v1/logout") \
|
||||
and "application/json" not in request.headers.getall("accept", [])
|
||||
|
@ -24,6 +24,8 @@ from ahriman.web.schemas.changes_schema import ChangesSchema
|
||||
from ahriman.web.schemas.counters_schema import CountersSchema
|
||||
from ahriman.web.schemas.dependencies_schema import DependenciesSchema
|
||||
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||
from ahriman.web.schemas.event_schema import EventSchema
|
||||
from ahriman.web.schemas.event_search_schema import EventSearchSchema
|
||||
from ahriman.web.schemas.file_schema import FileSchema
|
||||
from ahriman.web.schemas.info_schema import InfoSchema
|
||||
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
||||
|
47
src/ahriman/web/schemas/event_schema.py
Normal file
47
src/ahriman/web/schemas/event_schema.py
Normal file
@ -0,0 +1,47 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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/>.
|
||||
#
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
from ahriman.models.event import EventType
|
||||
|
||||
|
||||
class EventSchema(Schema):
|
||||
"""
|
||||
request/response event schema
|
||||
"""
|
||||
|
||||
created = fields.Integer(required=True, metadata={
|
||||
"description": "Event creation timestamp",
|
||||
"example": 1680537091,
|
||||
})
|
||||
event = fields.String(required=True, metadata={
|
||||
"description": "Event type",
|
||||
"example": EventType.PackageUpdated,
|
||||
})
|
||||
object_id = fields.String(required=True, metadata={
|
||||
"description": "Event object identifier",
|
||||
"example": "ahriman",
|
||||
})
|
||||
message = fields.String(metadata={
|
||||
"description": "Event message if available",
|
||||
})
|
||||
data = fields.Dict(keys=fields.String(), metadata={
|
||||
"description": "Event metadata if available",
|
||||
})
|
38
src/ahriman/web/schemas/event_search_schema.py
Normal file
38
src/ahriman/web/schemas/event_search_schema.py
Normal file
@ -0,0 +1,38 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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/>.
|
||||
#
|
||||
from marshmallow import fields
|
||||
|
||||
from ahriman.models.event import EventType
|
||||
from ahriman.web.schemas.pagination_schema import PaginationSchema
|
||||
|
||||
|
||||
class EventSearchSchema(PaginationSchema):
|
||||
"""
|
||||
request event search schema
|
||||
"""
|
||||
|
||||
event = fields.String(metadata={
|
||||
"description": "Event type",
|
||||
"example": EventType.PackageUpdated,
|
||||
})
|
||||
object_id = fields.String(metadata={
|
||||
"description": "Event object identifier",
|
||||
"example": "ahriman",
|
||||
})
|
@ -247,7 +247,7 @@ class BaseView(View, CorsViewMixin):
|
||||
extract username from request if any
|
||||
|
||||
Returns:
|
||||
str | None: authorized username if any and None otherwise (e.g. if authorization is disabled)
|
||||
str | None: authorized username if any and ``None`` otherwise (e.g. if authorization is disabled)
|
||||
"""
|
||||
try: # try to read from payload
|
||||
data: dict[str, str] = await self.request.json() # technically it is not, but we only need str here
|
||||
|
19
src/ahriman/web/views/v1/auditlog/__init__.py
Normal file
19
src/ahriman/web/views/v1/auditlog/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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/>.
|
||||
#
|
104
src/ahriman/web/views/v1/auditlog/events.py
Normal file
104
src/ahriman/web/views/v1/auditlog/events.py
Normal file
@ -0,0 +1,104 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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 aiohttp_apispec # type: ignore[import-untyped]
|
||||
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||
|
||||
from ahriman.models.event import Event
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, EventSchema, EventSearchSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
class EventsView(BaseView):
|
||||
"""
|
||||
audit log view
|
||||
|
||||
Attributes:
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/events"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Audit log"],
|
||||
summary="Get events",
|
||||
description="Retrieve events from audit log",
|
||||
responses={
|
||||
200: {"description": "Success response", "schema": EventSchema(many=True)},
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(EventSearchSchema)
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get events list
|
||||
|
||||
Returns:
|
||||
Response: 200 with workers list on success
|
||||
"""
|
||||
limit, offset = self.page()
|
||||
event = self.request.query.get("event") or None
|
||||
object_id = self.request.query.get("object_id") or None
|
||||
|
||||
events = self.service().event_get(event, object_id, limit, offset)
|
||||
response = [event.view() for event in events]
|
||||
|
||||
return json_response(response)
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Audit log"],
|
||||
summary="Create event",
|
||||
description="Add new event to the audit log",
|
||||
responses={
|
||||
204: {"description": "Success response"},
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.json_schema(EventSchema)
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
add new audit log event
|
||||
|
||||
Raises:
|
||||
HTTPBadRequest: if bad data is supplied
|
||||
HTTPNoContent: in case of success response
|
||||
"""
|
||||
try:
|
||||
data = await self.request.json()
|
||||
event = Event.from_json(data)
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
self.service().event_add(event)
|
||||
|
||||
raise HTTPNoContent
|
@ -25,6 +25,7 @@ from ahriman.models.remote_source import RemoteSource
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
from ahriman.models.result import Result
|
||||
from ahriman.models.scan_paths import ScanPaths
|
||||
from ahriman.models.user import User
|
||||
from ahriman.models.user_access import UserAccess
|
||||
|
||||
@ -587,6 +588,20 @@ def result(package_ahriman: Package) -> Result:
|
||||
return result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scan_paths(configuration: Configuration) -> ScanPaths:
|
||||
"""
|
||||
scan paths fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration test instance
|
||||
|
||||
Returns:
|
||||
ScanPaths: scan paths test instance
|
||||
"""
|
||||
return ScanPaths(configuration.getlist("build", "scan_paths", fallback=[]))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spawner(configuration: Configuration) -> Spawn:
|
||||
"""
|
||||
|
@ -1,6 +1,35 @@
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.build_tools.package_archive import PackageArchive
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
from ahriman.models.scan_paths import ScanPaths
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_archive_ahriman(package_ahriman: Package, repository_paths: RepositoryPaths, pacman: Pacman,
|
||||
scan_paths: ScanPaths, passwd: Any, mocker: MockerFixture) -> PackageArchive:
|
||||
"""
|
||||
package archive fixture
|
||||
|
||||
Args:
|
||||
package_ahriman(Package): package test instance
|
||||
repository_paths(RepositoryPaths): repository paths test instance
|
||||
pacman(Pacman): pacman test instance
|
||||
scan_paths(ScanPaths): scan paths test instance
|
||||
passwd(Any): passwd structure test instance
|
||||
mocker(MockerFixture): mocker object
|
||||
|
||||
Returns:
|
||||
PackageArchive: package archive test instance
|
||||
"""
|
||||
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
|
||||
return PackageArchive(repository_paths.build_directory, package_ahriman, pacman, scan_paths)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -3,16 +3,16 @@ from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
from ahriman.core.build_tools.package_archive import PackageArchive
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.models.filesystem_package import FilesystemPackage
|
||||
from ahriman.models.package_archive import PackageArchive
|
||||
|
||||
|
||||
def test_dynamic_needed(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly define list of dynamically linked libraries
|
||||
"""
|
||||
mocker.patch("ahriman.models.package_archive.PackageArchive.is_elf", return_value=True)
|
||||
mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.is_elf", return_value=True)
|
||||
|
||||
linked = PackageArchive.dynamic_needed(Path(".tox") / "tests" / "bin" / "python")
|
||||
assert linked
|
||||
@ -24,7 +24,7 @@ def test_dynamic_needed_not_elf(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip checking if not an elf file
|
||||
"""
|
||||
mocker.patch("ahriman.models.package_archive.PackageArchive.is_elf", return_value=False)
|
||||
mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.is_elf", return_value=False)
|
||||
assert not PackageArchive.dynamic_needed(Path(".tox") / "tests" / "bin" / "python")
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ def test_dynamic_needed_no_section(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip checking if there was no dynamic section found
|
||||
"""
|
||||
mocker.patch("ahriman.models.package_archive.PackageArchive.is_elf", return_value=True)
|
||||
mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.is_elf", return_value=True)
|
||||
mocker.patch("elftools.elf.elffile.ELFFile.iter_sections", return_value=[])
|
||||
assert not PackageArchive.dynamic_needed(Path(".tox") / "tests" / "bin" / "python")
|
||||
|
||||
@ -109,8 +109,8 @@ def test_raw_dependencies_packages(package_archive_ahriman: PackageArchive, mock
|
||||
files=[Path("package2") / "file4", Path("package2") / "file3"],
|
||||
),
|
||||
}
|
||||
mocker.patch("ahriman.models.package_archive.PackageArchive.installed_packages", return_value=packages)
|
||||
mocker.patch("ahriman.models.package_archive.PackageArchive.depends_on_paths", return_value=(
|
||||
mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.installed_packages", return_value=packages)
|
||||
mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.depends_on_paths", return_value=(
|
||||
{"file1", "file3"},
|
||||
{Path("usr") / "dir2", Path("dir3"), Path("package2") / "dir4"},
|
||||
))
|
||||
@ -165,17 +165,19 @@ def test_depends_on(package_archive_ahriman: PackageArchive, mocker: MockerFixtu
|
||||
"""
|
||||
must extract packages and files which are dependencies for the package
|
||||
"""
|
||||
raw_mock = mocker.patch("ahriman.models.package_archive.PackageArchive._raw_dependencies_packages",
|
||||
raw_mock = mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive._raw_dependencies_packages",
|
||||
return_value="1")
|
||||
refined_mock = mocker.patch("ahriman.models.package_archive.PackageArchive._refine_dependencies", return_value={
|
||||
Path("package1") / "file1": [FilesystemPackage(package_name="package1", depends=set(), opt_depends=set())],
|
||||
Path("package2") / "file3": [FilesystemPackage(package_name="package2", depends=set(), opt_depends=set())],
|
||||
Path("package2") / "dir4": [FilesystemPackage(package_name="package2", depends=set(), opt_depends=set())],
|
||||
Path("usr") / "dir2": [
|
||||
FilesystemPackage(package_name="package1", depends=set(), opt_depends=set()),
|
||||
FilesystemPackage(package_name="package2", depends=set(), opt_depends=set()),
|
||||
],
|
||||
})
|
||||
refined_mock = mocker.patch(
|
||||
"ahriman.core.build_tools.package_archive.PackageArchive._refine_dependencies", return_value={
|
||||
Path("package1") / "file1": [FilesystemPackage(package_name="package1", depends=set(), opt_depends=set())],
|
||||
Path("package2") / "file3": [FilesystemPackage(package_name="package2", depends=set(), opt_depends=set())],
|
||||
Path("package2") / "dir4": [FilesystemPackage(package_name="package2", depends=set(), opt_depends=set())],
|
||||
Path("usr") / "dir2": [
|
||||
FilesystemPackage(package_name="package1", depends=set(), opt_depends=set()),
|
||||
FilesystemPackage(package_name="package2", depends=set(), opt_depends=set()),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
result = package_archive_ahriman.depends_on()
|
||||
raw_mock.assert_called_once_with()
|
||||
@ -194,8 +196,9 @@ def test_depends_on_paths(package_archive_ahriman: PackageArchive, mocker: Mocke
|
||||
"""
|
||||
package_dir = package_archive_ahriman.root / "build" / \
|
||||
package_archive_ahriman.package.base / "pkg" / package_archive_ahriman.package.base
|
||||
dynamic_mock = mocker.patch("ahriman.models.package_archive.PackageArchive.dynamic_needed", return_value=["lib"])
|
||||
walk_mock = mocker.patch("ahriman.models.package_archive.walk", return_value=[
|
||||
dynamic_mock = mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.dynamic_needed",
|
||||
return_value=["lib"])
|
||||
walk_mock = mocker.patch("ahriman.core.build_tools.package_archive.walk", return_value=[
|
||||
package_dir / "root" / "file",
|
||||
Path("directory"),
|
||||
])
|
||||
@ -213,7 +216,7 @@ def test_installed_packages(package_archive_ahriman: PackageArchive, mocker: Moc
|
||||
"""
|
||||
must load list of installed packages and their files
|
||||
"""
|
||||
walk_mock = mocker.patch("ahriman.models.package_archive.walk", return_value=[
|
||||
walk_mock = mocker.patch("ahriman.core.build_tools.package_archive.walk", return_value=[
|
||||
Path("ahriman-2.13.3-1") / "desc",
|
||||
Path("ahriman-2.13.3-1") / "files",
|
||||
])
|
@ -0,0 +1,8 @@
|
||||
from ahriman.core.database.migrations.m014_auditlog import steps
|
||||
|
||||
|
||||
def test_migration_auditlog() -> None:
|
||||
"""
|
||||
migration must not be empty
|
||||
"""
|
||||
assert steps
|
@ -0,0 +1,40 @@
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
def test_event_insert_get(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must insert and get event
|
||||
"""
|
||||
event = Event(EventType.PackageUpdated, package_ahriman.base, "Updated", {"key": "value"})
|
||||
database.event_insert(event)
|
||||
assert database.event_get() == [event]
|
||||
|
||||
event2 = Event("event", "object")
|
||||
database.event_insert(event2, RepositoryId("i686", database._repository_id.name))
|
||||
assert database.event_get() == [event]
|
||||
assert database.event_get(repository_id=RepositoryId("i686", database._repository_id.name)) == [event2]
|
||||
|
||||
|
||||
def test_event_insert_get_filter(database: SQLite) -> None:
|
||||
"""
|
||||
must insert and get events with filter
|
||||
"""
|
||||
database.event_insert(Event("event 1", "object 1", created=1))
|
||||
database.event_insert(Event("event 2", "object 2"))
|
||||
database.event_insert(Event(EventType.PackageUpdated, "package"))
|
||||
|
||||
assert database.event_get(event="event 1") == [Event("event 1", "object 1", created=1)]
|
||||
assert database.event_get(object_id="object 1") == [Event("event 1", "object 1", created=1)]
|
||||
assert all(event.event == EventType.PackageUpdated for event in database.event_get(event=EventType.PackageUpdated))
|
||||
|
||||
|
||||
def test_event_insert_get_pagination(database: SQLite) -> None:
|
||||
"""
|
||||
must insert and get events with pagination
|
||||
"""
|
||||
database.event_insert(Event("1", "1"))
|
||||
database.event_insert(Event("2", "2"))
|
||||
assert all(event.event == "2" for event in database.event_get(limit=1, offset=1))
|
@ -24,7 +24,7 @@ def test_process_build(executor: Executor, package_ahriman: Package, passwd: Any
|
||||
move_mock = mocker.patch("shutil.move")
|
||||
status_client_mock = mocker.patch("ahriman.core.status.Client.set_building")
|
||||
commit_sha_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||
depends_on_mock = mocker.patch("ahriman.models.package_archive.PackageArchive.depends_on",
|
||||
depends_on_mock = mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.depends_on",
|
||||
return_value=Dependencies())
|
||||
dependencies_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_dependencies_update")
|
||||
|
||||
|
@ -11,6 +11,7 @@ from ahriman.core.status.web_client import WebClient
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
@ -94,6 +95,22 @@ def test_load_web_client_from_legacy_unix_socket(configuration: Configuration, d
|
||||
assert isinstance(Client.load(repository_id, configuration, database, report=True), WebClient)
|
||||
|
||||
|
||||
def test_event_add(client: Client) -> None:
|
||||
"""
|
||||
must raise not implemented on event insertion
|
||||
"""
|
||||
with pytest.raises(NotImplementedError):
|
||||
client.event_add(Event("", ""))
|
||||
|
||||
|
||||
def test_event_get(client: Client) -> None:
|
||||
"""
|
||||
must raise not implemented on events request
|
||||
"""
|
||||
with pytest.raises(NotImplementedError):
|
||||
client.event_get(None, None)
|
||||
|
||||
|
||||
def test_package_changes_get(client: Client, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must raise not implemented on package changes request
|
||||
|
@ -7,11 +7,32 @@ from ahriman.core.status.local_client import LocalClient
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
def test_event_add(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must add new event
|
||||
"""
|
||||
event_mock = mocker.patch("ahriman.core.database.SQLite.event_insert")
|
||||
event = Event(EventType.PackageUpdated, package_ahriman.base)
|
||||
|
||||
local_client.event_add(event)
|
||||
event_mock.assert_called_once_with(event, local_client.repository_id)
|
||||
|
||||
|
||||
def test_event_get(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must retrieve events
|
||||
"""
|
||||
event_mock = mocker.patch("ahriman.core.database.SQLite.event_get")
|
||||
local_client.event_get(EventType.PackageUpdated, package_ahriman.base, 1, 2)
|
||||
event_mock.assert_called_once_with(EventType.PackageUpdated, package_ahriman.base, 1, 2, local_client.repository_id)
|
||||
|
||||
|
||||
def test_package_changes_get(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must retrieve package changes
|
||||
|
@ -10,6 +10,7 @@ from ahriman.core.status.web_client import WebClient
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
@ -54,18 +55,12 @@ def test_dependencies_url(web_client: WebClient, package_ahriman: Package) -> No
|
||||
"/api/v1/packages/some%2Fpackage%25name/dependencies")
|
||||
|
||||
|
||||
def test__patches_url(web_client: WebClient, package_ahriman: Package) -> None:
|
||||
def test_event_url(web_client: WebClient) -> None:
|
||||
"""
|
||||
must generate changes url correctly
|
||||
must generate audit log url correctly
|
||||
"""
|
||||
assert web_client._patches_url(package_ahriman.base).startswith(web_client.address)
|
||||
assert web_client._patches_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/patches")
|
||||
assert web_client._patches_url("some/package%name").endswith("/api/v1/packages/some%2Fpackage%25name/patches")
|
||||
|
||||
assert web_client._patches_url(package_ahriman.base, "var").endswith(
|
||||
f"/api/v1/packages/{package_ahriman.base}/patches/var")
|
||||
assert web_client._patches_url(package_ahriman.base, "some/variable%name").endswith(
|
||||
f"/api/v1/packages/{package_ahriman.base}/patches/some%2Fvariable%25name")
|
||||
assert web_client._events_url().startswith(web_client.address)
|
||||
assert web_client._events_url().endswith("/api/v1/events")
|
||||
|
||||
|
||||
def test_logs_url(web_client: WebClient, package_ahriman: Package) -> None:
|
||||
@ -89,6 +84,20 @@ def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
|
||||
assert web_client._package_url("some/package%name").endswith("/api/v1/packages/some%2Fpackage%25name")
|
||||
|
||||
|
||||
def test_patches_url(web_client: WebClient, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must generate changes url correctly
|
||||
"""
|
||||
assert web_client._patches_url(package_ahriman.base).startswith(web_client.address)
|
||||
assert web_client._patches_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/patches")
|
||||
assert web_client._patches_url("some/package%name").endswith("/api/v1/packages/some%2Fpackage%25name/patches")
|
||||
|
||||
assert web_client._patches_url(package_ahriman.base, "var").endswith(
|
||||
f"/api/v1/packages/{package_ahriman.base}/patches/var")
|
||||
assert web_client._patches_url(package_ahriman.base, "some/variable%name").endswith(
|
||||
f"/api/v1/packages/{package_ahriman.base}/patches/some%2Fvariable%25name")
|
||||
|
||||
|
||||
def test_status_url(web_client: WebClient) -> None:
|
||||
"""
|
||||
must generate package status url correctly
|
||||
@ -97,6 +106,135 @@ def test_status_url(web_client: WebClient) -> None:
|
||||
assert web_client._status_url().endswith("/api/v1/status")
|
||||
|
||||
|
||||
def test_event_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create event
|
||||
"""
|
||||
event = Event(EventType.PackageUpdated, package_ahriman.base)
|
||||
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
|
||||
|
||||
web_client.event_add(event)
|
||||
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True),
|
||||
params=web_client.repository_id.query(), json=event.view())
|
||||
|
||||
|
||||
def test_event_add_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during events creation
|
||||
"""
|
||||
mocker.patch("requests.Session.request", side_effect=Exception())
|
||||
web_client.event_add(Event("", ""))
|
||||
|
||||
|
||||
def test_event_add_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress HTTP exception happened during events creation
|
||||
"""
|
||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
||||
web_client.event_add(Event("", ""))
|
||||
|
||||
|
||||
def test_event_add_failed_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during events creaton and don't log
|
||||
"""
|
||||
web_client.suppress_errors = True
|
||||
mocker.patch("requests.Session.request", side_effect=Exception())
|
||||
logging_mock = mocker.patch("logging.exception")
|
||||
|
||||
web_client.event_add(Event("", ""))
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_event_add_failed_http_error_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress HTTP exception happened during events creation and don't log
|
||||
"""
|
||||
web_client.suppress_errors = True
|
||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
||||
logging_mock = mocker.patch("logging.exception")
|
||||
|
||||
web_client.event_add(Event("", ""))
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_event_get(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must get events
|
||||
"""
|
||||
event = Event(EventType.PackageUpdated, package_ahriman.base)
|
||||
response_obj = requests.Response()
|
||||
response_obj._content = json.dumps([event.view()]).encode("utf8")
|
||||
response_obj.status_code = 200
|
||||
|
||||
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj)
|
||||
|
||||
result = web_client.event_get(None, None)
|
||||
requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True),
|
||||
params=web_client.repository_id.query() + [("limit", "-1"), ("offset", "0")])
|
||||
assert result == [event]
|
||||
|
||||
|
||||
def test_event_get_filter(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must get events with filter
|
||||
"""
|
||||
response_obj = requests.Response()
|
||||
response_obj._content = json.dumps(Event("", "").view()).encode("utf8")
|
||||
response_obj.status_code = 200
|
||||
|
||||
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj)
|
||||
|
||||
web_client.event_get("event", "object", 1, 2)
|
||||
requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True),
|
||||
params=web_client.repository_id.query() + [
|
||||
("limit", "1"),
|
||||
("offset", "2"),
|
||||
("event", "event"),
|
||||
("object_id", "object"),
|
||||
])
|
||||
|
||||
|
||||
def test_event_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during events fetch
|
||||
"""
|
||||
mocker.patch("requests.Session.request", side_effect=Exception())
|
||||
web_client.event_get(None, None)
|
||||
|
||||
|
||||
def test_event_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress HTTP exception happened during events fetch
|
||||
"""
|
||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
||||
web_client.event_get(None, None)
|
||||
|
||||
|
||||
def test_event_get_failed_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during events fetch and don't log
|
||||
"""
|
||||
web_client.suppress_errors = True
|
||||
mocker.patch("requests.Session.request", side_effect=Exception())
|
||||
logging_mock = mocker.patch("logging.exception")
|
||||
|
||||
web_client.event_get(None, None)
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_event_get_failed_http_error_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress HTTP exception happened during events fetch and don't log
|
||||
"""
|
||||
web_client.suppress_errors = True
|
||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
||||
logging_mock = mocker.patch("logging.exception")
|
||||
|
||||
web_client.event_get(None, None)
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_package_changes_get(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must get changes
|
||||
|
@ -1,24 +1,17 @@
|
||||
import pytest
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman import __version__
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.remote import AUR
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.filesystem_package import FilesystemPackage
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_archive import PackageArchive
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
from ahriman.models.scan_paths import ScanPaths
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -77,27 +70,6 @@ def internal_status(counters: Counters) -> InternalStatus:
|
||||
repository="aur-clone")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_archive_ahriman(package_ahriman: Package, repository_paths: RepositoryPaths, pacman: Pacman,
|
||||
scan_paths: ScanPaths, passwd: Any, mocker: MockerFixture) -> PackageArchive:
|
||||
"""
|
||||
package archive fixture
|
||||
|
||||
Args:
|
||||
package_ahriman(Package): package test instance
|
||||
repository_paths(RepositoryPaths): repository paths test instance
|
||||
pacman(Pacman): pacman test instance
|
||||
scan_paths(ScanPaths): scan paths test instance
|
||||
passwd(Any): passwd structure test instance
|
||||
mocker(MockerFixture): mocker object
|
||||
|
||||
Returns:
|
||||
PackageArchive: package archive test instance
|
||||
"""
|
||||
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
|
||||
return PackageArchive(repository_paths.build_directory, package_ahriman, pacman, scan_paths)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_tpacpi_bat_git() -> Package:
|
||||
"""
|
||||
@ -161,20 +133,3 @@ def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescr
|
||||
type(mock).provides = PropertyMock(return_value=package_description_ahriman.provides)
|
||||
type(mock).url = PropertyMock(return_value=package_description_ahriman.url)
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scan_paths(configuration: Configuration) -> ScanPaths:
|
||||
"""
|
||||
scan paths fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration test instance
|
||||
|
||||
Returns:
|
||||
ScanPaths: scan paths test instance
|
||||
"""
|
||||
return ScanPaths(
|
||||
allowed_paths=configuration.getpathlist("build", "allowed_scan_paths"),
|
||||
blacklisted_paths=configuration.getpathlist("build", "blacklisted_scan_paths"),
|
||||
)
|
||||
|
17
tests/ahriman/models/test_event.py
Normal file
17
tests/ahriman/models/test_event.py
Normal file
@ -0,0 +1,17 @@
|
||||
from ahriman.models.event import Event, EventType
|
||||
|
||||
|
||||
def test_post_init() -> None:
|
||||
"""
|
||||
must replace event type for known types
|
||||
"""
|
||||
assert Event("random", "")
|
||||
assert isinstance(Event(str(EventType.PackageUpdated), "").event, EventType)
|
||||
|
||||
|
||||
def test_from_json_view() -> None:
|
||||
"""
|
||||
must construct and serialize event to json
|
||||
"""
|
||||
event = Event("event", "object", "message", {"key": "value"})
|
||||
assert Event.from_json(event.view()) == event
|
@ -3,40 +3,30 @@ from pathlib import Path
|
||||
from ahriman.models.scan_paths import ScanPaths
|
||||
|
||||
|
||||
def test_post_init(scan_paths: ScanPaths) -> None:
|
||||
"""
|
||||
must convert paths to / relative
|
||||
"""
|
||||
assert all(not path.is_absolute() for path in scan_paths.allowed_paths)
|
||||
assert all(not path.is_absolute() for path in scan_paths.blacklisted_paths)
|
||||
|
||||
|
||||
def test_is_allowed() -> None:
|
||||
"""
|
||||
must check if path is subpath of one in allowed list
|
||||
"""
|
||||
assert ScanPaths(allowed_paths=[Path("/") / "usr"], blacklisted_paths=[]).is_allowed(Path("usr"))
|
||||
assert ScanPaths(allowed_paths=[Path("/") / "usr"], blacklisted_paths=[]).is_allowed(Path("usr") / "lib")
|
||||
assert not ScanPaths(allowed_paths=[Path("/") / "usr"], blacklisted_paths=[]).is_allowed(Path("var"))
|
||||
assert ScanPaths(["usr"]).is_allowed(Path("usr"))
|
||||
assert ScanPaths(["usr"]).is_allowed(Path("usr") / "lib")
|
||||
assert not ScanPaths(["usr"]).is_allowed(Path("var"))
|
||||
|
||||
assert ScanPaths(["usr(?!/lib)"]).is_allowed(Path("usr"))
|
||||
assert ScanPaths(["usr(?!/lib)", "var"]).is_allowed(Path("var"))
|
||||
assert not ScanPaths(["usr(?!/lib)"]).is_allowed(Path("usr") / "lib")
|
||||
|
||||
|
||||
def test_is_blacklisted() -> None:
|
||||
def test_is_allowed_default(scan_paths: ScanPaths) -> None:
|
||||
"""
|
||||
must check if path is not subpath of one in blacklist
|
||||
must provide expected default configuration
|
||||
"""
|
||||
assert ScanPaths(
|
||||
allowed_paths=[Path("/") / "usr"],
|
||||
blacklisted_paths=[Path("/") / "usr" / "lib"],
|
||||
).is_allowed(Path("usr"))
|
||||
assert ScanPaths(
|
||||
allowed_paths=[Path("/") / "usr", Path("/") / "var"],
|
||||
blacklisted_paths=[Path("/") / "usr" / "lib"],
|
||||
).is_allowed(Path("var"))
|
||||
assert not ScanPaths(
|
||||
allowed_paths=[Path("/") / "usr"],
|
||||
blacklisted_paths=[Path("/") / "usr" / "lib"],
|
||||
).is_allowed(Path(" usr") / "lib")
|
||||
assert not ScanPaths(
|
||||
allowed_paths=[Path("/") / "usr"],
|
||||
blacklisted_paths=[Path("/") / "usr" / "lib"],
|
||||
).is_allowed(Path("usr") / "lib" / "qt")
|
||||
assert not scan_paths.is_allowed(Path("usr"))
|
||||
assert not scan_paths.is_allowed(Path("var"))
|
||||
|
||||
assert scan_paths.is_allowed(Path("usr") / "lib")
|
||||
assert scan_paths.is_allowed(Path("usr") / "lib" / "libm.so")
|
||||
|
||||
# cmake case
|
||||
assert scan_paths.is_allowed(Path("usr") / "lib" / "libcmake.so")
|
||||
assert not scan_paths.is_allowed(Path("usr") / "lib" / "cmake")
|
||||
assert not scan_paths.is_allowed(Path("usr") / "lib" / "cmake" / "file.cmake")
|
||||
|
1
tests/ahriman/web/schemas/test_event_schema.py
Normal file
1
tests/ahriman/web/schemas/test_event_schema.py
Normal file
@ -0,0 +1 @@
|
||||
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_event_search_schema.py
Normal file
1
tests/ahriman/web/schemas/test_event_search_schema.py
Normal file
@ -0,0 +1 @@
|
||||
# schema testing goes in view class tests
|
@ -0,0 +1,104 @@
|
||||
import pytest
|
||||
|
||||
from aiohttp.test_utils import TestClient
|
||||
|
||||
from ahriman.models.event import Event
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.views.v1.auditlog.events import EventsView
|
||||
|
||||
|
||||
async def test_get_permission() -> None:
|
||||
"""
|
||||
must return correct permission for the request
|
||||
"""
|
||||
for method in ("GET", "POST"):
|
||||
request = pytest.helpers.request("", "", method)
|
||||
assert await EventsView.get_permission(request) == UserAccess.Full
|
||||
|
||||
|
||||
def test_routes() -> None:
|
||||
"""
|
||||
must return correct routes
|
||||
"""
|
||||
assert EventsView.ROUTES == ["/api/v1/events"]
|
||||
|
||||
|
||||
async def test_get(client: TestClient) -> None:
|
||||
"""
|
||||
must return all events
|
||||
"""
|
||||
event1 = Event("event1", "object1", "message", {"key": "value"})
|
||||
event2 = Event("event2", "object2")
|
||||
await client.post("/api/v1/events", json=event1.view())
|
||||
await client.post("/api/v1/events", json=event2.view())
|
||||
response_schema = pytest.helpers.schema_response(EventsView.get)
|
||||
|
||||
response = await client.get("/api/v1/events")
|
||||
assert response.ok
|
||||
json = await response.json()
|
||||
assert not response_schema.validate(json, many=True)
|
||||
|
||||
events = [Event.from_json(event) for event in json]
|
||||
assert events == [event1, event2]
|
||||
|
||||
|
||||
async def test_get_with_pagination(client: TestClient) -> None:
|
||||
"""
|
||||
must get events with pagination
|
||||
"""
|
||||
event1 = Event("event1", "object1", "message", {"key": "value"})
|
||||
event2 = Event("event2", "object2")
|
||||
await client.post("/api/v1/events", json=event1.view())
|
||||
await client.post("/api/v1/events", json=event2.view())
|
||||
request_schema = pytest.helpers.schema_request(EventsView.get, location="querystring")
|
||||
response_schema = pytest.helpers.schema_response(EventsView.get)
|
||||
|
||||
payload = {"limit": 1, "offset": 1}
|
||||
assert not request_schema.validate(payload)
|
||||
response = await client.get("/api/v1/events", params=payload)
|
||||
assert response.status == 200
|
||||
|
||||
json = await response.json()
|
||||
assert not response_schema.validate(json, many=True)
|
||||
|
||||
assert [Event.from_json(event) for event in json] == [event2]
|
||||
|
||||
|
||||
async def test_get_bad_request(client: TestClient) -> None:
|
||||
"""
|
||||
must return bad request for invalid query parameters
|
||||
"""
|
||||
response_schema = pytest.helpers.schema_response(EventsView.get, code=400)
|
||||
|
||||
response = await client.get("/api/v1/events", params={"limit": "limit"})
|
||||
assert response.status == 400
|
||||
assert not response_schema.validate(await response.json())
|
||||
|
||||
response = await client.get("/api/v1/events", params={"offset": "offset"})
|
||||
assert response.status == 400
|
||||
assert not response_schema.validate(await response.json())
|
||||
|
||||
|
||||
async def test_post(client: TestClient) -> None:
|
||||
"""
|
||||
must create event
|
||||
"""
|
||||
event = Event("event1", "object1", "message", {"key": "value"})
|
||||
request_schema = pytest.helpers.schema_request(EventsView.post)
|
||||
|
||||
payload = event.view()
|
||||
assert not request_schema.validate(payload)
|
||||
|
||||
response = await client.post("/api/v1/events", json=payload)
|
||||
assert response.status == 204
|
||||
|
||||
|
||||
async def test_post_exception(client: TestClient) -> None:
|
||||
"""
|
||||
must raise exception on invalid payload
|
||||
"""
|
||||
response_schema = pytest.helpers.schema_response(EventsView.post, code=400)
|
||||
|
||||
response = await client.post("/api/v1/events", json={})
|
||||
assert response.status == 400
|
||||
assert not response_schema.validate(await response.json())
|
@ -20,13 +20,12 @@ salt = salt
|
||||
allow_read_only = no
|
||||
|
||||
[build]
|
||||
allowed_scan_paths = /usr/lib
|
||||
archbuild_flags =
|
||||
blacklisted_scan_paths = /usr/lib/cmake
|
||||
build_command = extra-x86_64-build
|
||||
ignore_packages =
|
||||
makechrootpkg_flags =
|
||||
makepkg_flags = --skippgpcheck
|
||||
scan_paths = ^usr/lib(?!/cmake).*$
|
||||
triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger
|
||||
triggers_known = ahriman.core.distributed.WorkerLoaderTrigger ahriman.core.distributed.WorkerRegisterTrigger ahriman.core.distributed.WorkerTrigger ahriman.core.distributed.WorkerUnregisterTrigger ahriman.core.gitremote.RemotePullTrigger ahriman.core.gitremote.RemotePushTrigger ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger ahriman.core.support.KeyringTrigger ahriman.core.support.MirrorlistTrigger
|
||||
|
||||
|
1
tox.ini
1
tox.ini
@ -11,6 +11,7 @@ flags = --implicit-reexport --strict --allow-untyped-decorators --allow-subclass
|
||||
|
||||
[pytest]
|
||||
addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
asyncio_mode = auto
|
||||
spec_test_format = {result} {docstring_summary}
|
||||
|
||||
|
Reference in New Issue
Block a user