Compare commits

..

32 Commits

Author SHA1 Message Date
2e059023f3 Release 2.4.0 2022-12-05 00:25:53 +02:00
da5d97788a do not update database via pacman, use ahriman's own databses 2022-12-04 23:45:10 +02:00
0e839fbbf2 Some minor documentation related fixes
* Improve some wording (again)
* Change default type for refresh option to False (does not affect
  behavior)
* Update docstrings to reflect last changes
* Configuration.__convert_path has been replaced by shlex
* aiosecurity functions support kwargs now
2022-12-04 02:10:46 +02:00
262462d3c3 improve wording in documentation 2022-12-02 15:45:01 +02:00
7aa91f9e2e do not trigger update on sign command 2022-12-02 01:41:23 +02:00
01eda513cf improve setup command by --makeflags-jobs argument and fix repository sign on creation 2022-12-02 01:41:23 +02:00
0161617e36 implement support of unix socket for server
This feature can be used for unauthorized access to apis - e.g. for
reporting service if it is run on the same machine. Since now it becomes
recommended way for the interprocess communication, thus some options
(e.g. creating user with as-service flag) are no longer available now
2022-12-02 01:41:23 +02:00
4811dec759 do not invoke configuration write in case if no salt or user was written 2022-12-02 01:41:23 +02:00
09623c20d5 add rebuild implementation to interface 2022-12-02 01:41:23 +02:00
eba247b759 make package actions as dropdown 2022-12-02 01:41:23 +02:00
5073c80af1 add key-import button to interface 2022-12-02 01:41:23 +02:00
766081d212 add demos links 2022-11-24 02:38:33 +02:00
896cd0bd71 add security notes 2022-11-24 02:38:33 +02:00
ce1bd2f2db add curl examples to web views 2022-11-24 02:38:33 +02:00
6ba96d838d build docs together with web views 2022-11-24 02:38:33 +02:00
df9e03f495 change respone for service requests 2022-11-24 02:38:33 +02:00
89944eb2b6 add fallback for copying to clipboard 2022-11-24 02:38:33 +02:00
336784519b add show/hide password button 2022-11-24 02:38:33 +02:00
f3341ec7cd update web preview picture 2022-11-24 02:38:33 +02:00
d36e851a29 render httpunauthorized as html in plain http requests 2022-11-22 23:40:27 +02:00
28bd5f2095 allow scrolling with fixed copy button position 2022-11-22 22:20:21 +02:00
011b4e2e31 change logging module imports 2022-11-22 22:20:21 +02:00
15609ba044 change wording for package actions 2022-11-22 15:46:49 +02:00
6d4f9981f7 fix login and logout buttons decorations 2022-11-22 11:17:59 +02:00
bbb97d1cdd add more notes about docker 2022-11-22 10:49:46 +02:00
3bca780bdd check log record in handler instead of client 2022-11-22 02:30:37 +02:00
78e6b48c24 ask users to repeat password
In case if password is asked via getpass, it is possible to make typo
and user will not see the mistake. In order to avoid it, additional
confirmation has been added
2022-11-22 02:19:37 +02:00
137d62e2f8 Extended package status page (#76)
* implement log storage at backend
* handle process id during removal. During one process we can write logs from different packages in different times (e.g. check and update later) and we would like to store all logs belong to the same process
* set package context in main functions
* implement logs support in interface
* filter out logs posting http logs
* add timestamp to log records
* hide getting logs under reporter permission

List of breaking changes:

* `ahriman.core.lazy_logging.LazyLogging` has been renamed to `ahriman.core.log.LazyLogging`
* `ahriman.core.configuration.Configuration.from_path` does not have `quiet` attribute now
* `ahriman.core.configuration.Configuration` class does not have `load_logging` method now
* `ahriman.core.status.client.Client.load` requires `report` argument now
2022-11-22 02:58:22 +03:00
8a6854c867 Release 2.3.0 2022-11-16 00:53:48 +02:00
299732181c remote threadname from logging
Since application is mostly singlethreaded it makes no sense to log it
2022-11-14 23:21:06 +02:00
84c1b4d82d Release 2.3.0rc4 2022-11-14 01:02:27 +02:00
cdd66ee780 fix case when no files were commited in remote push trigger
The issue appears together with --intent-to-add flag for adding new
files. Original testing has been performed by having already added new
files, thus it passed all checks.

This commit also adds `commit_author` option which will allow to
overwrite the author.
2022-11-14 00:59:43 +02:00
192 changed files with 7197 additions and 4157 deletions

View File

@ -0,0 +1,20 @@
---
name: Security report
about: Create a report related to security issues
title: ''
labels: security
assignees: ''
---
## Summary
A clear and concise description of what the issue is.
### Steps to reproduce
Steps to reproduce the behavior (commands, environment etc).
### Intended impact
Brief optional description of how this vulnerability can be used and which effects can be achieved.

View File

@ -1,6 +1,7 @@
version: 2
formats: all
formats:
- pdf
build:
os: ubuntu-20.04
@ -10,6 +11,7 @@ build:
sphinx:
builder: html
configuration: docs/conf.py
fail_on_warning: true
python:
install:
@ -17,4 +19,6 @@ python:
path: .
extra_requirements:
- docs
- s3
- web
system_packages: true

View File

@ -1,2 +1,2 @@
Current developers:
Evgenii Alekseev aka arcanis <esalexeev (at) gmail (dot) com>
Current maintainer:
Evgenii Alekseev <esalexeev (at) gmail (dot) com>

View File

@ -61,7 +61,7 @@ Again, the most checks can be performed by `make check` command, though some add
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools.
* No global variable is allowed outside of `ahriman.version` module.
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
* If your class writes anything to log, the `ahriman.core.lazy_logging.LazyLogging` trait must be used.
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
### Other checks

View File

@ -10,6 +10,7 @@ ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
ENV AHRIMAN_PORT=""
ENV AHRIMAN_REPOSITORY="aur-clone"
ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman"
ENV AHRIMAN_UNIX_SOCKET=""
ENV AHRIMAN_USER="ahriman"
# install environment
@ -26,7 +27,7 @@ COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
RUN pacman --noconfirm -Sy devtools git pyalpm python-inflection python-passlib python-requests python-setuptools python-srcinfo && \
pacman --noconfirm -Sy python-build python-installer python-wheel && \
pacman --noconfirm -Sy breezy mercurial python-aiohttp python-boto3 python-cryptography python-jinja rsync subversion && \
pacman --noconfirm -Sy breezy mercurial python-aiohttp python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-jinja2 python-aiohttp-debugtoolbar \
python-aiohttp-session python-aiohttp-security
@ -50,4 +51,4 @@ VOLUME ["/var/lib/ahriman"]
COPY "docker/entrypoint.sh" "/usr/local/bin/entrypoint"
ENTRYPOINT ["entrypoint"]
# default command
CMD ["repo-update"]
CMD ["repo-update", "--refresh"]

View File

@ -3,7 +3,7 @@
PROJECT := ahriman
FILES := AUTHORS CONTRIBUTING.md COPYING README.md docs package src setup.py tox.ini web.png
FILES := AUTHORS CONTRIBUTING.md COPYING Makefile README.md SECURITY.md docs package src setup.py tox.ini web.png
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
IGNORE_FILES := package/archlinux src/.mypy_cache

View File

@ -33,3 +33,9 @@ Every available option is described in the [documentation](https://ahriman.readt
The application provides reasonable defaults which allow to use it out-of-box; however additional steps (like configuring build toolchain and sudoers) are recommended and can be easily achieved by following install instructions.
## [FAQ](https://ahriman.readthedocs.io/en/latest/faq.html)
## Live demos
* [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks.
* [Repository index](http://repo.arcanis.me/x86_64/index.html).
* [Telegram feed](https://t.me/arcanisrepo).

9
SECURITY.md Normal file
View File

@ -0,0 +1,9 @@
# Security Policy
## Supported Versions
The project follows bleeding edge philosophy, thus only the latest version is supported with the exception for release candidates (i.e. tags which are marked with `rc` suffix).
## Reporting a Vulnerability
In the most cases you can report (suspected) security vulnerabilities directly on github by using ["Security report" template](https://github.com/arcan1s/ahriman/issues/new?assignees=&labels=security&template=02-security-report.md&title=). However, if your report could lead to data leak or break the system we kindly ask you to contact [current maintainer](AUTHORS) directly by email.

View File

@ -4,9 +4,17 @@ set -e
[ -n "$AHRIMAN_DEBUG" ] && set -x
# configuration tune
sed -i "s|root = /var/lib/ahriman|root = $AHRIMAN_REPOSITORY_ROOT|g" "/etc/ahriman.ini"
sed -i "s|database = /var/lib/ahriman/ahriman.db|database = $AHRIMAN_REPOSITORY_ROOT/ahriman.db|g" "/etc/ahriman.ini"
sed -i "s|host = 127.0.0.1|host = $AHRIMAN_HOST|g" "/etc/ahriman.ini"
cat <<EOF > "/etc/ahriman.ini.d/00-docker.ini"
[repository]
root = $AHRIMAN_REPOSITORY_ROOT
[settings]
database = $AHRIMAN_REPOSITORY_ROOT/ahriman.db
[web]
host = $AHRIMAN_HOST
EOF
sed -i "s|handlers = syslog_handler|handlers = ${AHRIMAN_OUTPUT}_handler|g" "/etc/ahriman.ini.d/logging.ini"
AHRIMAN_DEFAULT_ARGS=("--architecture" "$AHRIMAN_ARCHITECTURE")
@ -22,18 +30,23 @@ fi
[ -d "$AHRIMAN_REPOSITORY_ROOT" ] || mkdir "$AHRIMAN_REPOSITORY_ROOT"
chown "$AHRIMAN_USER":"$AHRIMAN_USER" "$AHRIMAN_REPOSITORY_ROOT"
# create .gnupg directory which is required for keys
AHRIMAN_GNUPG_HOME="$(getent passwd "$AHRIMAN_USER" | cut -d : -f 6)/.gnupg"
[ -d "$AHRIMAN_GNUPG_HOME" ] || mkdir -m700 "$AHRIMAN_GNUPG_HOME"
chown "$AHRIMAN_USER":"$AHRIMAN_USER" "$AHRIMAN_GNUPG_HOME"
# run built-in setup command
AHRIMAN_SETUP_ARGS=("--build-as-user" "$AHRIMAN_USER")
AHRIMAN_SETUP_ARGS+=("--packager" "$AHRIMAN_PACKAGER")
AHRIMAN_SETUP_ARGS+=("--repository" "$AHRIMAN_REPOSITORY")
if [ -n "$AHRIMAN_PORT" ]; then
# in addition it must be handled in docker run command
AHRIMAN_SETUP_ARGS+=("--web-port" "$AHRIMAN_PORT")
fi
if [ -n "$AHRIMAN_UNIX_SOCKET" ]; then
AHRIMAN_SETUP_ARGS+=("--web-unix-socket" "$AHRIMAN_UNIX_SOCKET")
fi
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" repo-setup "${AHRIMAN_SETUP_ARGS[@]}"
# refresh database
pacman -Syy &> /dev/null
# create machine-id which is required by build tools
systemd-machine-id-setup &> /dev/null

View File

@ -11,8 +11,8 @@ Depending on the goal the package can be used in different ways. Nevertheless, i
from ahriman.core.database import SQLite
architecture = "x86_64"
configuration = Configuration.from_path(Path("/etc/ahriman.ini"), architecture, quiet=False)
sqlite = SQLite.load(configuration)
configuration = Configuration.from_path(Path("/etc/ahriman.ini"), architecture)
database = SQLite.load(configuration)
At this point there are ``configuration`` and ``database`` instances which can be used later at any time anywhere, e.g.
@ -27,7 +27,7 @@ Almost all actions are wrapped by ``ahriman.core.repository.Repository`` class
from ahriman.core.repository import Repository
repository = Repository(architecture, configuration, database, no_report=False, unsafe=False)
repository = Repository(architecture, configuration, database, report=True, unsafe=False)
And the ``repository`` instance can be used to perform repository maintenance
@ -37,6 +37,6 @@ And the ``repository`` instance can be used to perform repository maintenance
built_packages = repository.packages_built()
update_result = repository.process_update(built_packages)
repository.process_triggers(update_result)
repository.triggers.on_result(update_result, repository.packages())
For the more info please refer to the classes documentation.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 613 KiB

After

Width:  |  Height:  |  Size: 621 KiB

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2022\-11\-11" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2022\-12\-05" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
@ -10,7 +10,7 @@ ArcH linux ReposItory MANager
.SH OPTIONS
.TP
\fB\-a\fR \fI\,ARCHITECTURE\/\fR, \fB\-\-architecture\fR \fI\,ARCHITECTURE\/\fR
target architectures (can be used multiple times)
target architectures. For several subcommands it can be used multiple times
.TP
\fB\-c\fR \fI\,CONFIGURATION\/\fR, \fB\-\-configuration\fR \fI\,CONFIGURATION\/\fR
@ -128,7 +128,7 @@ run triggers
update packages
.TP
\fBahriman\fR \fI\,shell\/\fR
envoke python shell
invoke python shell
.TP
\fBahriman\fR \fI\,user\-add\/\fR
create or update user
@ -507,9 +507,10 @@ root path of the extracted files
.SH COMMAND \fI\,'ahriman repo\-setup'\/\fR
usage: ahriman repo\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-build\-command BUILD_COMMAND]
[\-\-from\-configuration FROM_CONFIGURATION] [\-\-multilib | \-\-no\-multilib] \-\-packager PACKAGER
\-\-repository REPOSITORY [\-\-sign\-key SIGN_KEY] [\-\-sign\-target {disabled,pacakges,repository}]
[\-\-web\-port WEB_PORT]
[\-\-from\-configuration FROM_CONFIGURATION] [\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs]
[\-\-multilib | \-\-no\-multilib] \-\-packager PACKAGER \-\-repository REPOSITORY [\-\-sign\-key SIGN_KEY]
[\-\-sign\-target {disabled,packages,repository}] [\-\-web\-port WEB_PORT]
[\-\-web\-unix\-socket WEB_UNIX_SOCKET]
create initial service configuration, requires root
@ -526,6 +527,10 @@ build command prefix
\fB\-\-from\-configuration\fR \fI\,FROM_CONFIGURATION\/\fR
path to default devtools pacman configuration
.TP
\fB\-\-makeflags\-jobs\fR, \fB\-\-no\-makeflags\-jobs\fR
append MAKEFLAGS variable with parallelism set to number of cores (default: True)
.TP
\fB\-\-multilib\fR, \fB\-\-no\-multilib\fR
add or do not multilib repository (default: True)
@ -543,13 +548,17 @@ repository name
sign key id
.TP
\fB\-\-sign\-target\fR \fI\,{disabled,pacakges,repository}\/\fR
\fB\-\-sign\-target\fR \fI\,{disabled,packages,repository}\/\fR
sign options
.TP
\fB\-\-web\-port\fR \fI\,WEB_PORT\/\fR
port of the web service
.TP
\fB\-\-web\-unix\-socket\fR \fI\,WEB_UNIX_SOCKET\/\fR
path to unix socket used for interprocess communications
.SH COMMAND \fI\,'ahriman repo\-sign'\/\fR
usage: ahriman repo\-sign [\-h] [package ...]
@ -581,7 +590,7 @@ run triggers on empty build result as configured by settings
.TP
\fBtrigger\fR
instead of running all triggers as set by configuration, just process specified ones oin order of metion
instead of running all triggers as set by configuration, just process specified ones in order of mention
.SH COMMAND \fI\,'ahriman repo\-update'\/\fR
usage: ahriman repo\-update [\-h] [\-\-dry\-run] [\-e] [\-\-aur | \-\-no\-aur] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual]
@ -633,7 +642,7 @@ drop into python shell while having created application
instead of dropping into shell, just execute the specified code
.SH COMMAND \fI\,'ahriman user\-add'\/\fR
usage: ahriman user\-add [\-h] [\-\-as\-service] [\-p PASSWORD] [\-r {unauthorized,read,reporter,full}] [\-s] username
usage: ahriman user\-add [\-h] [\-p PASSWORD] [\-r {unauthorized,read,reporter,full}] [\-s] username
update user for web services with the given password and role. In case if password was not entered it will be asked interactively
@ -642,10 +651,6 @@ update user for web services with the given password and role. In case if passwo
username for web service
.SH OPTIONS \fI\,'ahriman user\-add'\/\fR
.TP
\fB\-\-as\-service\fR
add user as service user
.TP
\fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR
user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2
@ -678,7 +683,7 @@ return non\-zero exit status if result is empty
filter users by role
.SH COMMAND \fI\,'ahriman user\-remove'\/\fR
usage: ahriman user\-remove [\-h] [\-s] username
usage: ahriman user\-remove [\-h] username
remove user from the user mapping and update the configuration
@ -686,11 +691,6 @@ remove user from the user mapping and update the configuration
\fBusername\fR
username for web service
.SH OPTIONS \fI\,'ahriman user\-remove'\/\fR
.TP
\fB\-s\fR, \fB\-\-secure\fR
set file permissions to user\-only
.SH COMMAND \fI\,'ahriman version'\/\fR
usage: ahriman version [\-h]

View File

@ -36,6 +36,14 @@ ahriman.core.database.migrations.m003\_patch\_variables module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m004\_logs module
--------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m004_logs
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -20,6 +20,14 @@ ahriman.core.database.operations.build\_operations module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.operations.logs\_operations module
--------------------------------------------------------
.. automodule:: ahriman.core.database.operations.logs_operations
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.database.operations.operations module
--------------------------------------------------

45
docs/ahriman.core.log.rst Normal file
View File

@ -0,0 +1,45 @@
ahriman.core.log package
========================
Submodules
----------
ahriman.core.log.filtered\_access\_logger module
------------------------------------------------
.. automodule:: ahriman.core.log.filtered_access_logger
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.log.http\_log\_handler module
------------------------------------------
.. automodule:: ahriman.core.log.http_log_handler
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.log.lazy\_logging module
-------------------------------------
.. automodule:: ahriman.core.log.lazy_logging
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.log.log module
---------------------------
.. automodule:: ahriman.core.log.log
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.log
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -13,6 +13,7 @@ Subpackages
ahriman.core.database
ahriman.core.formatters
ahriman.core.gitremote
ahriman.core.log
ahriman.core.report
ahriman.core.repository
ahriman.core.sign
@ -39,14 +40,6 @@ ahriman.core.exceptions module
:no-undoc-members:
:show-inheritance:
ahriman.core.lazy\_logging module
---------------------------------
.. automodule:: ahriman.core.lazy_logging
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.spawn module
-------------------------

View File

@ -52,6 +52,14 @@ ahriman.models.internal\_status module
:no-undoc-members:
:show-inheritance:
ahriman.models.log\_record\_id module
-------------------------------------
.. automodule:: ahriman.models.log_record_id
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.migration module
-------------------------------

View File

@ -12,6 +12,22 @@ ahriman.web.views.service.add module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.service.pgp module
------------------------------------
.. automodule:: ahriman.web.views.service.pgp
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.service.rebuild module
----------------------------------------
.. automodule:: ahriman.web.views.service.rebuild
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.service.remove module
---------------------------------------
@ -36,6 +52,14 @@ ahriman.web.views.service.search module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.service.update module
---------------------------------------
.. automodule:: ahriman.web.views.service.update
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -4,6 +4,14 @@ ahriman.web.views.status package
Submodules
----------
ahriman.web.views.status.logs module
------------------------------------
.. automodule:: ahriman.web.views.status.logs
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.status.package module
---------------------------------------

View File

@ -28,7 +28,7 @@ This package contains application (aka executable) related classes and everythin
``ahriman.core`` package
^^^^^^^^^^^^^^^^^^^^^^^^
This package contains everything which is required for any time of application run and separated into several packages:
This package contains everything required for the most of application actions and it is separated into several packages:
* ``ahriman.core.alpm`` package controls pacman related functions. It provides wrappers for ``pyalpm`` library and safe calls for repository tools (``repo-add`` and ``repo-remove``). Also this package contains ``ahriman.core.alpm.remote`` package which provides wrapper for remote sources (e.g. AUR RPC and official repositories RPC).
* ``ahriman.core.auth`` package provides classes for authorization methods used by web mostly. Base class is ``ahriman.core.auth.Auth`` which must be called by ``load`` method.
@ -36,6 +36,7 @@ This package contains everything which is required for any time of application r
* ``ahriman.core.database`` is everything including data and schema migrations for database.
* ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
* ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger and access logger for HTTP services with additional filters.
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.
* ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.Repository`` class) implementation.
* ``ahriman.core.sign`` package provides sign feature (only gpg calls are available).
@ -53,7 +54,7 @@ This package also provides some generic functions and classes which may be used
``ahriman.models`` package
^^^^^^^^^^^^^^^^^^^^^^^^^^
It provides models for any other part of application. Unlike ``ahriman.core`` package classes from here provides only conversion methods (e.g. create class from another or convert to). Mostly case classes and enumerations.
It provides models for any other part of application. Unlike ``ahriman.core`` package classes from here provide only conversion methods (e.g. create class from another or convert to). Mostly case classes and enumerations.
``ahriman.web`` package
^^^^^^^^^^^^^^^^^^^^^^^
@ -76,7 +77,7 @@ Application run
* Return result (success or failure) of each subprocess and exit from application.
* Some handlers may override their status and throw ``ExitCode`` exception. This exception is just silently suppressed and changes application exit code to ``1``.
In most cases handlers spawn god class ``ahriman.application.application.Application`` class and call required methods.
In the most cases handlers spawn god class ``ahriman.application.application.Application`` class and call required methods.
Application is designed to run from ``systemd`` services and provides parametrized by architecture timer and service file for that.
@ -196,14 +197,16 @@ means that there is user ``username`` with ``read`` access and password ``passwo
OAuth provider uses library definitions (``aioauth-client``) in order *authenticate* users. It still requires user permission to be set in database, thus it inherits mapping provider without any changes. Whereas we could override ``check_credentials`` (authentication method) by something custom, OAuth flow is a bit more complex than just forward request, thus we have to implement the flow in login form.
OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature is used by service reporting (aka robots).
OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature can be used by service reporting (aka robots).
In addition, web service checks the source socket used. In case if it belongs to ``socket.AF_UNIX`` family, it will skip any furher checks considering the request to be performed in safe environment (e.g. on the same physical machine). This feature, in particular is being used by the reporter instances in case if socket address is set in configuration.
In order to configure users there are special commands.
Triggers
^^^^^^^^
Triggers are extensions which can be used in order to perform any actions on application start, after the update process and, finally, before the application exit. The package provides two default extensions - one is report generation and another one is remote upload feature.
Triggers are extensions which can be used in order to perform any actions on application start, after the update process and, finally, before the application exit.
The main idea is to load classes by their full path (e.g. ``ahriman.core.upload.UploadTrigger``) by using ``importlib``: get the last part of the import and treat it as class name, join remain part by ``.`` and interpret as module path, import module and extract attribute from it.
@ -244,6 +247,7 @@ Web application requires the following python packages to be installed:
* In addition, ``aiohttp_debugtoolbar`` is required for debug panel. Please note that this option does not work together with authorization and basically must not be used in production.
* In addition, authorization feature requires ``aiohttp_security``, ``aiohttp_session`` and ``cryptography``.
* In addition to base authorization dependencies, OAuth2 also requires ``aioauth-client`` library.
* In addition if you would like to disable authorization for local access (recommended way in order to run the application itself with reporting support), the ``requests-unixsocket`` library is required.
Middlewares
^^^^^^^^^^^

View File

@ -25,20 +25,10 @@ sys.path.insert(0, str(basedir))
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
for module in (
"aioauth_client",
"aiohttp",
"aiohttp.web",
"aiohttp.web_exceptions",
"aiohttp.web_response",
"aiohttp.web_urldispatcher",
"aiohttp_jinja2",
"aiohttp_security",
"aiohttp_session",
"aiohttp_session.cookie_storage",
"boto3",
"cryptography",
"pyalpm",
):
if module in sys.modules:
continue
sys.modules[module] = mock.Mock()
@ -89,7 +79,7 @@ html_theme = "default" if on_rtd else "alabaster"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_static_path = []
add_module_names = False

View File

@ -44,7 +44,7 @@ Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` libr
* ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days.
* ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used.
* ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used.
* ``salt`` - password hash salt, string, required in case if authorization enabled (automatically generated by ``create-user`` subcommand).
* ``salt`` - password hash salt, string, required in case if authorization enabled (automatically generated by ``user-add`` subcommand).
Authorized users are stored inside internal database, if any of external provides are used the password field for non-service users must be empty.
@ -104,6 +104,7 @@ It supports authorization; to do so you'd need to prefix the url with authorizat
Remote push trigger
^^^^^^^^^^^^^^^^^^^
* ``commit_author`` - git commit author, string, optional. In case if not set, the git will generate author for you. Note, however, that in this case it will disclosure your hostname.
* ``push_url`` - url of the remote repository to which PKGBUILDs should be pushed after build process, string, required.
* ``push_branch`` - branch of the remote repository to which PKGBUILDs should be pushed after build process, string, optional, default is ``master``.
@ -114,7 +115,7 @@ Report generation settings.
* ``target`` - list of reports to be generated, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. ``somerandomname`` must point to existing section, ``email`` must point to either ``email`` or ``email:x86_64`` (the one with architecture has higher priority).
Type will be read from several ways:
Type will be read from several sources:
* In case if ``type`` option set inside the section, it will be used.
* Otherwise, it will look for type from section name removing architecture name.
@ -179,7 +180,7 @@ Remote synchronization settings.
* ``target`` - list of synchronizations to be used, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. ``somerandomname`` must point to existing section, ``github`` must point to one of ``github`` of ``github:x86_64`` (with architecture it has higher priority).
Type will be read from several ways:
Type will be read from several sources:
* In case if ``type`` option set inside the section, it will be used.
* Otherwise, it will look for type from section name removing architecture name.
@ -239,4 +240,5 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil
* ``port`` - port to bind, int, optional.
* ``static_path`` - path to directory with static files, string, required.
* ``templates`` - path to templates directory, string, required.
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.

View File

@ -169,13 +169,30 @@ Unlike ``RemotePullTrigger`` trigger, the ``RemotePushTrigger`` more likely will
How to change PKGBUILDs before build
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Well it is supported also. The recommended way is to patch specific function, e.g. by running ``sudo -u ahriman ahriman patch-add ahriman version``. This command will prompt for new value of the PKGBUILD variable ``version``. You can also write it to file and read from it ``sudo -u ahriman ahriman patch-add ahriman version version.patch``.
Well it is supported also. The recommended way is to patch specific function, e.g. by running
.. code-block:: shell
sudo -u ahriman ahriman patch-add ahriman version
This command will prompt for new value of the PKGBUILD variable ``version``. You can also write it to file and read from it:
.. code-block:: shell
sudo -u ahriman ahriman patch-add ahriman version version.patch
Alternatively you can create full-diff patches, which are calculated by using ``git diff`` from current PKGBUILD master branch:
#. Clone sources from AUR.
#. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches).
#. Run ``sudo -u ahriman ahriman patch-set-add /path/to/local/directory/with/PKGBUILD``.
#.
Clone sources from AUR.
#.
Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches).
#.
Run command
.. code-block:: shell
sudo -u ahriman ahriman patch-set-add /path/to/local/directory/with/PKGBUILD
The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).
@ -219,6 +236,62 @@ Also, there is command ``repo-remove-unknown`` which checks packages in AUR and
Remove commands also remove any package files (patches, caches etc).
How to sign repository
^^^^^^^^^^^^^^^^^^^^^^
Repository sign feature is available in several configurations. The recommended way is just to sign repository database file by single key instead of trying to sign each package. However, the steps are pretty same, just configuration is a bit differ. For more details about options kindly refer to :doc:`configuration reference <configuration>`.
#.
First you would need to create the key on your local machine:
.. code-block:: shell
gpg --full-generate-key
This command will prompt you for several questions. Most of them may be left default, but you will need to fill real name and email address with some data. Because at the moment the service doesn't support passphrases, it must be left blank.
#.
The command above will generate key and print its hash, something like ``8BE91E5A773FB48AC05CC1EDBED105AED6246B39``. Copy it.
#.
Export your private key by using the hash above:
.. code-block:: shell
gpg --export-secret-keys -a 8BE91E5A773FB48AC05CC1EDBED105AED6246B39 > repository-key.gpg
#.
Copy the specified key to the build machine (i.e. where the service is running).
#.
Import the specified key to the service user:
.. code-block:: shell
sudo -u ahriman gpg --import repository-key.gpg
Don't forget to remove the key from filesystem after import.
#.
Change trust level to ``ultimate``:
.. code-block:: shell
sudo -u ahriman gpg --edit-key 8BE91E5A773FB48AC05CC1EDBED105AED6246B39
The command above will drop you into gpg shell, in which you will need to type ``trust``, choose ``5 = I trust ultimately``, confirm and exit ``quit``.
#.
Proceed with service configuration according to the :doc:`configuration <configuration>`:
.. code-block:: ini
[sign]
target = repository
key = 8BE91E5A773FB48AC05CC1EDBED105AED6246B39
How to rebuild packages after library update
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -290,17 +363,17 @@ The default action (in case if no arguments provided) is ``repo-update``. Basica
docker run --privileged -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
``--privileged`` flag is required to make mount possible inside container. In addition, you can pass own configuration overrides by using the same ``-v`` flag, e.g.:
``--privileged`` flag is required to make mount possible inside container. In order to make data available outside of container, you would need to mount local (parent) directory inside container by using ``-v /path/to/local/repo:/var/lib/ahriman`` argument, where ``/path/to/local/repo`` is a path to repository on local machine. In addition, you can pass own configuration overrides by using the same ``-v`` flag, e.g.:
.. code-block:: shell
docker run -v /path/to/local/repo:/var/lib/ahriman -v /etc/ahriman.ini:/etc/ahriman.ini.d/10-overrides.ini arcan1s/ahriman:latest
docker run --privileged -v /path/to/local/repo:/var/lib/ahriman -v /path/to/overrides/overrides.ini:/etc/ahriman.ini.d/10-overrides.ini arcan1s/ahriman:latest
The action can be specified during run, e.g.:
.. code-block:: shell
docker run arcan1s/ahriman:latest package-add ahriman --now
docker run --privileged -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest package-add ahriman --now
For more details please refer to docker FAQ.
@ -318,13 +391,25 @@ The following environment variables are supported:
* ``AHRIMAN_PORT`` - HTTP server port if any, default is empty.
* ``AHRIMAN_REPOSITORY`` - repository name, default is ``aur-clone``.
* ``AHRIMAN_REPOSITORY_ROOT`` - repository root. Because of filesystem rights it is required to override default repository root. By default, it uses ``ahriman`` directory inside ahriman's home, which can be passed as mount volume.
* ``AHRIMAN_UNIX_SOCKET`` - full path to unix socket which is used by web server, default is empty. Note that more likely you would like to put it inside ``AHRIMAN_REPOSITORY_ROOT`` directory (e.g. ``/var/lib/ahriman/ahriman/ahriman-web.sock``) or to ``/tmp``.
* ``AHRIMAN_USER`` - ahriman user, usually must not be overwritten, default is ``ahriman``.
You can pass any of these variables by using ``-e`` argument, e.g.:
.. code-block:: shell
docker run -e AHRIMAN_PORT=8080 arcan1s/ahriman:latest
docker run --privileged -e AHRIMAN_PORT=8080 -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
Daemon service
^^^^^^^^^^^^^^
There is special ``daemon`` subcommand which emulates systemd timer and will perform repository update periodically:
.. code-block:: shell
docker run --privileged -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest daemon
This command uses same rules as ``repo-update``, thus, e.g. requires ``--privileged`` flag.
Web service setup
^^^^^^^^^^^^^^^^^
@ -333,26 +418,23 @@ Well for that you would need to have web container instance running forever; it
.. code-block:: shell
docker run -p 8080:8080 -e AHRIMAN_PORT=8080 -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
docker run --privileged -p 8080:8080 -e AHRIMAN_PORT=8080 -e AHRIMAN_UNIX_SOCKET=/var/lib/ahriman/ahriman/ahriman-web.sock -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
Note about ``AHRIMAN_PORT`` environment variable which is required in order to enable web service. An additional port bind by ``-p 8080:8080`` is required to pass docker port outside of container.
For every next container run use arguments ``-e AHRIMAN_PORT=8080 --net=host``, e.g.:
The ``AHRIMAN_UNIX_SOCKET`` variable is not required, however, highly recommended as it can be used for interprocess communications. If you set this variable you would like to be sure that this path is available outside of container if you are going to use multiple docker instances.
If you are using ``AHRIMAN_UNIX_SOCKET`` variable, for every next container run it has to be passed also, e.g.:
.. code-block:: shell
docker run --privileged -e AHRIMAN_PORT=8080 --net=host -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
docker run --privileged -e AHRIMAN_UNIX_SOCKET=/var/lib/ahriman/ahriman/ahriman-web.sock -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
Daemon service
^^^^^^^^^^^^^^
There is special subcommand which emulates systemd timer and will perform repository update periodically:
Otherwise, you would need to pass ``AHRIMAN_PORT`` and mount container network to the host system (``--net=host``), e.g.:
.. code-block:: shell
docker run arcan1s/ahriman:latest daemon
This command uses same rules as ``repo-update``, thus, e.g. requires ``--privileged`` flag.
docker run --privileged --net=host -e AHRIMAN_PORT=8080 -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
Remote synchronization
----------------------
@ -610,19 +692,41 @@ How to enable basic authorization
[auth]
target = configuration
#.
Create user for the service:
#.
In order to provide access for reporting from application instances you can (recommended way) use unix sockets by configuring the following (note, that it requires ``python-requests-unixsocket`` package to be installed):
.. code-block:: ini
[web]
unix_socket = /var/lib/ahriman/ahriman-web.sock
This socket path must be available for web service instance and must be available for application instances (e.g. in case if you are using docker container, see above, you need to be sure that the socket is passed to the root filesystem).
By the way, unix socket variable will be automatically set in case if ``--web-unix-socket`` argument is supplied to the ``setup`` subcommand.
Alternatively, you need to create user for the service:
.. code-block:: shell
sudo -u ahriman ahriman user-add --as-service -r write api
sudo -u ahriman ahriman user-add -r full api
This command will ask for the password, just type it in stdin; *do not* leave the field blank, user will not be able to authorize.
This command will ask for the password, just type it in stdin; *do not* leave the field blank, user will not be able to authorize, and finally configure the application:
#.
Create end-user ``sudo -u ahriman ahriman user-add -r write my-first-user`` with password.
.. code-block:: ini
#. Restart web service ``systemctl restart ahriman-web@x86_64``.
[web]
username = api
password = pa55w0rd
#.
Create end-user with password:
.. code-block:: shell
sudo -u ahriman ahriman user-add -r full my-first-user
#.
Restart web service ``systemctl restart ahriman-web@x86_64``.
How to enable OAuth authorization
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -656,12 +760,19 @@ How to enable OAuth authorization
.. code-block:: shell
sudo -u ahriman ahriman user-add --as-service -r write api
sudo -u ahriman ahriman user-add --as-service -r full api
#.
Create end-user ``sudo -u ahriman ahriman user-add -r write my-first-user``. When it will ask for the password leave it blank.
Create end-user:
#. Restart web service ``systemctl restart ahriman-web@x86_64``.
.. code-block:: shell
sudo -u ahriman ahriman user-add -r full my-first-user
When it will ask for the password leave it blank.
#.
Restart web service ``systemctl restart ahriman-web@x86_64``.
Backup and restore
------------------
@ -681,7 +792,7 @@ The service provides several commands aim to do easy repository backup and resto
Copy created archive from source server ``server1.example.com`` to target ``server2.example.com``.
#.
Install ahriman as usual on the target server ``server2.example.com`` if you didn't yet.
Install package as usual on the target server ``server2.example.com`` if you didn't yet.
#.
Extract archive e.g. by using subcommand:

View File

@ -16,6 +16,13 @@ Features
* Triggers for repository updates, e.g. synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Repository status interface with optional authorization and control options.
Live demos
----------
* `Build status page <https://ahriman-demo.arcanis.me>`_. You can login as ``demo`` user by using ``demo`` password. Note, however, you will not be able to run tasks.
* `Repository index <http://repo.arcanis.me/x86_64/index.html>`_.
* `Telegram feed <https://t.me/arcanisrepo>`_.
Contents
--------

View File

@ -19,7 +19,7 @@ For the configuration details and settings explanation kindly refer to the :doc:
This trigger will be called before any action (``on_start``) and pulls remote PKGBUILD repository locally; after that it copies found PKGBUILDs from the cloned repository to the local cache. It is useful in case if you have patched PGKBUILDs (or even missing in AUR) which you would like to use for package building and, technically, just simplifies the local package building.
In order to update those packages you would need to clone your repository separately, make changes in PKGBUILD (e.g. bump version and update checksums), commit them and push back. On the next ahriman's repository update, it will pull changes you commited and will perform package update.
In order to update those packages you would need to clone your repository separately, make changes in PKGBUILD (e.g. bump version and update checksums), commit them and push back. On the next ahriman's repository update, it will pull changes you committed and will perform package update.
``ahriman.core.gitremote.RemotePushTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=2.3.0rc3
pkgver=2.4.0
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')
@ -20,6 +20,7 @@ optdepends=('breezy: -bzr packages support'
'python-aiohttp-session: web server with authorization'
'python-boto3: sync to s3'
'python-cryptography: web server with authorization'
'python-requests-unixsocket: client report to web server by unix socket'
'python-jinja: html report generation'
'rsync: sync by using rsync'
'subversion: -svn packages support')

View File

@ -20,11 +20,11 @@ formatter = syslog_format
args = ("/dev/log",)
[formatter_generic_format]
format = [%(levelname)s %(asctime)s] [%(threadName)s] [%(name)s]: %(message)s
format = [%(levelname)s %(asctime)s] [%(name)s]: %(message)s
datefmt =
[formatter_syslog_format]
format = [%(levelname)s] [%(threadName)s] [%(name)s]: %(message)s
format = [%(levelname)s] [%(name)s]: %(message)s
datefmt =
[logger_root]

View File

@ -12,6 +12,8 @@
<body>
{% include "utils/bootstrap-scripts.jinja2" %}
<div class="container">
<h1>ahriman
<img id="badge-version" src="https://img.shields.io/badge/version-unknown-informational" alt="unknown">
@ -22,16 +24,35 @@
</div>
<div class="container">
<div id="toolbar">
<div id="toolbar" class="dropdown">
{% if not auth.enabled or auth.username is not none %}
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-form" hidden>
<i class="bi bi-plus"></i> add
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-box"></i> packages
</button>
<button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" hidden>
<i class="bi bi-play"></i> update
</button>
<button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
<i class="bi bi-trash"></i> remove
<ul class="dropdown-menu">
<li>
<button id="package-add-btn" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden>
<i class="bi bi-plus"></i> add
</button>
</li>
<li>
<button id="package-update-btn" class="btn dropdown-item" onclick="updatePackages()" hidden>
<i class="bi bi-play"></i> update
</button>
</li>
<li>
<button id="package-rebuild-btn" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal" hidden>
<i class="bi bi-arrow-clockwise"></i> rebuild
</button>
</li>
<li>
<button id="package-remove-btn" class="btn dropdown-item" onclick="removePackages()" disabled hidden>
<i class="bi bi-trash"></i> remove
</button>
</li>
</ul>
<button id="key-import-btn" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden>
<i class="bi bi-key"></i> import key
</button>
{% endif %}
<button class="btn btn-secondary" onclick="reload()">
@ -40,7 +61,6 @@
</div>
<table id="packages" class="table table-striped table-hover"
data-click-to-select="true"
data-export-options='{"fileName": "packages"}'
data-page-list="[10, 25, 50, 100, all]"
data-page-size="10"
@ -76,25 +96,29 @@
<div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li>
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman" title="sources"><i class="bi bi-github"></i> ahriman</a></li>
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li>
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
</ul>
{% if index_url is not none %}
<ul class="nav">
<li><a class="nav-link" href="{{ index_url }}" title="repo index">repo index</a></li>
<li><a class="nav-link" href="{{ index_url }}" title="repo index"><i class="bi bi-house"></i> repo index</a></li>
</ul>
{% endif %}
{% if auth.enabled %}
{% if auth.username is none %}
{{ auth.control|safe }}
{% else %}
<form action="/api/v1/logout" method="post">
<button class="btn btn-link" style="text-decoration: none">logout ({{ auth.username }})</button>
</form>
{% endif %}
<ul class="nav">
{% if auth.username is none %}
<li>{{ auth.control|safe }}</li>
{% else %}
<li>
<form action="/api/v1/logout" method="post">
<button class="btn btn-link" style="text-decoration: none"><i class="bi bi-box-arrow-right"></i> logout ({{ auth.username }})</button>
</form>
</li>
{% endif %}
</ul>
{% endif %}
</footer>
</div>
@ -103,12 +127,14 @@
{% include "build-status/login-modal.jinja2" %}
{% endif %}
{% include "utils/bootstrap-scripts.jinja2" %}
{% include "build-status/failed-modal.jinja2" %}
{% include "build-status/success-modal.jinja2" %}
{% include "build-status/package-add-modal.jinja2" %}
{% include "build-status/package-rebuild-modal.jinja2" %}
{% include "build-status/key-import-modal.jinja2" %}
{% include "build-status/package-info-modal.jinja2" %}
{% include "build-status/table.jinja2" %}

View File

@ -1,28 +1,34 @@
<div id="failed-form" tabindex="-1" role="dialog" class="modal fade">
<div id="failed-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-danger">
<h4 class="modal-title">failed</h4>
<div class="modal-header bg-danger text-white">
<h4 id="failed-title" class="modal-title"></h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<p>Packages update has failed.</p>
<p id="error-details"></p>
<p id="failed-description"></p>
<p id="failed-details"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
</div>
</div>
</div>
</div>
<script>
const failedForm = $("#failed-form");
const errorDetails = $("#error-details");
failedForm.on("hidden.bs.modal", () => { reload(); });
const failedModal = $("#failed-modal");
failedModal.on("hidden.bs.modal", () => { reload(); });
function showFailure(details) {
errorDetails.text(details);
failedForm.modal("show");
const failedDescription = $("#failed-description");
const failedDetails = $("#failed-details");
const failedTitle = $("#failed-title");
function showFailure(title, description, details) {
failedTitle.text(title);
failedDescription.text(description);
failedDetails.text(details);
failedModal.modal("show");
}
</script>

View File

@ -0,0 +1,92 @@
<div id="key-import-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<form id="key-import-form" onsubmit="return false">
<div class="modal-header">
<h4 class="modal-title">Import key from PGP server</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="key-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label>
<div class="col-sm-10">
<input id="key-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
</div>
</div>
<div class="form-group row">
<label for="key-server-input" class="col-sm-2 col-form-label">key server</label>
<div class="col-sm-10">
<input id="key-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<pre class="language-less"><code id="key-body-input" class="pre-scrollable language-less"></code><button id="key-copy-btn" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" onclick="importPgpKey()"><i class="bi bi-play"></i> import</button>
<button type="submit" class="btn btn-success" onclick="fetchPgpKey()"><i class="bi bi-arrow-clockwise"></i> fetch</button>
</div>
</form>
</div>
</div>
</div>
<script>
const keyImportModal = $("#key-import-modal");
const keyImportForm = $("#key-import-form");
keyImportModal.on("hidden.bs.modal", () => {
keyBodyInput.text("");
keyImportForm.trigger("reset");
});
const keyBodyInput = $("#key-body-input");
const keyCopyButton = $("#key-copy-btn");
const keyFingerprintInput = $("#key-fingerprint-input");
const keyServerInput = $("#key-server-input");
async function copyPgpKey() {
const logs = keyBodyInput.text();
await copyToClipboard(logs, keyCopyButton);
}
function fetchPgpKey() {
const key = keyFingerprintInput.val();
const server = keyServerInput.val();
if (key && server) {
$.ajax({
url: "/api/v1/service/pgp",
data: {"key": key, "server": server},
type: "GET",
dataType: "json",
success: response => { keyBodyInput.text(response.key); },
});
}
}
function importPgpKey() {
const key = keyFingerprintInput.val();
const server = keyServerInput.val();
if (key && server) {
$.ajax({
url: "/api/v1/service/pgp",
data: JSON.stringify({key: key, server: server}),
type: "POST",
contentType: "application/json",
success: _ => {
keyImportModal.modal("hide");
showSuccess("Success", `Key ${key} has been imported`, "");
},
error: (jqXHR, _, errorThrown) => {
showFailure("Action failed", `Could not import key ${key} from ${server}`, errorThrown);
},
});
}
}
</script>

View File

@ -1,9 +1,9 @@
<div id="loginForm" tabindex="-1" role="dialog" class="modal fade">
<div id="login-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="/api/v1/login" method="post">
<div class="modal-header">
<h4 class="modal-title">login</h4>
<h4 class="modal-title">Login</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
@ -16,14 +16,36 @@
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">password</label>
<div class="col-sm-10">
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
<div class="input-group">
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" onclick="showPassword()"><i id="show-hide-password-btn" class="bi bi-eye"></i></button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary">login</button>
<button class="btn btn-primary"><i class="bi bi-person"></i> login</button>
</div>
</form>
</div>
</div>
</div>
<script>
const passwordInput = $("#password");
const showHidePasswordButton = $("#show-hide-password-btn");
function showPassword() {
if (passwordInput.attr("type") === "password") {
passwordInput.attr("type", "text");
showHidePasswordButton.removeClass("bi-eye");
showHidePasswordButton.addClass("bi-eye-slash");
} else {
passwordInput.attr("type", "password");
showHidePasswordButton.removeClass("bi-eye-slash");
showHidePasswordButton.addClass("bi-eye");
}
}
</script>

View File

@ -1,61 +1,74 @@
<div id="add-form" tabindex="-1" role="dialog" class="modal fade">
<div id="package-add-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">add new packages</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="package" class="col-sm-2 col-form-label">package</label>
<div class="col-sm-10">
<input id="package-form" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
<datalist id="known-packages-dlist"></datalist>
<form id="package-add-form" onsubmit="return false">
<div class="modal-header">
<h4 class="modal-title">Add new packages</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="package-input" class="col-sm-2 col-form-label">package</label>
<div class="col-sm-10">
<input id="package-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
<datalist id="known-packages-dlist"></datalist>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">close</button>
<button type="button" class="btn btn-success" data-bs-dismiss="modal" onclick="requestPackages()">request</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPackages()">add</button>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" onclick="packagesAdd()"><i class="bi bi-play"></i> add</button>
<button type="submit" class="btn btn-success" onclick="packagesRequest()"><i class="bi bi-plus"></i> request</button>
</div>
</form>
</div>
</div>
</div>
<script>
const packageInput = $("#package-form");
const knownPackages = $("#known-packages-dlist");
const packageAddModal = $("#package-add-modal");
const packageAddForm = $("#package-add-form");
packageAddModal.on("hidden.bs.modal", () => { packageAddForm.trigger("reset"); });
const packageInput = $("#package-input");
const knownPackagesList = $("#known-packages-dlist");
packageInput.keyup(() => {
clearTimeout(packageInput.data("timeout"));
packageInput.data("timeout", setTimeout($.proxy(() => {
const value = packageInput.val();
$.ajax({
url: "/api/v1/service/search",
data: {"for": value},
type: "GET",
dataType: "json",
success: response => {
const options = response.map(pkg => {
const option = document.createElement("option");
option.value = pkg.package;
option.innerText = `${pkg.package} (${pkg.description})`;
return option;
});
knownPackages.empty().append(options);
},
})
if (value.length >= 3) {
$.ajax({
url: "/api/v1/service/search",
data: {"for": value},
type: "GET",
dataType: "json",
success: response => {
const options = response.map(pkg => {
const option = document.createElement("option");
option.value = pkg.package;
option.innerText = `${pkg.package} (${pkg.description})`;
return option;
});
knownPackagesList.empty().append(options);
},
});
}
}, this), 500));
});
function addPackages() {
const packages = [packageInput.val()];
doPackageAction("/api/v1/service/add", packages);
function packagesAdd() {
const packages = packageInput.val();
if (packages) {
packageAddModal.modal("hide");
doPackageAction("/api/v1/service/add", [packages], "The following package has been added:", "Package addition failed:");
}
}
function requestPackages() {
const packages = [packageInput.val()];
doPackageAction("/api/v1/service/request", packages);
function packagesRequest() {
const packages = packageInput.val();
if (packages) {
packageAddModal.modal("hide");
doPackageAction("/api/v1/service/request", [packages], "The following package has been requested:", "Package request failed:");
}
}
</script>

View File

@ -0,0 +1,67 @@
<div id="package-info-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div id="package-info-modal-header" class="modal-header">
<h4 id="package-info" class="modal-title"></h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<pre class="language-logs"><code id="package-info-logs-input" class="pre-scrollable language-logs"></code><button id="logs-copy-btn" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
</div>
</div>
</div>
</div>
<script>
const packageInfoModal = $("#package-info-modal");
const packageInfoModalHeader = $("#package-info-modal-header");
const packageInfo = $("#package-info");
const packageInfoLogsInput = $("#package-info-logs-input");
const packageInfoLogsCopyButton = $("#logs-copy-btn");
async function copyLogs() {
const logs = packageInfoLogsInput.text();
await copyToClipboard(logs, packageInfoLogsCopyButton);
}
function showLogs(packageBase) {
const isPackageBaseSet = packageBase !== undefined;
if (isPackageBaseSet)
packageInfoModal.data("package", packageBase); // set package base as currently used
else
packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
const headerClass = status => {
if (status === "pending") return ["bg-warning"];
if (status === "building") return ["bg-warning"];
if (status === "failed") return ["bg-danger", "text-white"];
if (status === "success") return ["bg-success", "text-white"];
return ["bg-secondary", "text-white"];
};
$.ajax({
url: `/api/v1/packages/${packageBase}/logs`,
type: "GET",
dataType: "json",
success: response => {
packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`);
packageInfoLogsInput.text(response.logs);
packageInfoModalHeader.removeClass();
packageInfoModalHeader.addClass("modal-header");
headerClass(response.status.status).forEach((clz) => packageInfoModalHeader.addClass(clz));
if (isPackageBaseSet) packageInfoModal.modal("show"); // we don't need to show window again
},
error: (jqXHR, _, errorThrown) => {
// show failed modal in case if first time loading
if (isPackageBaseSet) showFailure("Load failure", `Could not load package ${packageBase} logs:`, errorThrown);
},
});
}
</script>

View File

@ -0,0 +1,39 @@
<div id="package-rebuild-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form id="package-rebuild-form" onsubmit="return false">
<div class="modal-header">
<h4 class="modal-title">Rebuild depending packages</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="dependency-input" class="col-sm-4 col-form-label">dependency</label>
<div class="col-sm-8">
<input id="dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" onclick="packagesRebuild()"><i class="bi bi-play"></i> rebuild</button>
</div>
</form>
</div>
</div>
</div>
<script>
const packageRebuildModal = $("#package-rebuild-modal");
const packageRebuildForm = $("#package-rebuild-form");
packageRebuildModal.on("hidden.bs.modal", () => { packageRebuildForm.trigger("reset"); });
const dependencyInput = $("#dependency-input");
function packagesRebuild() {
const packages = dependencyInput.val();
if (packages) {
packageRebuildModal.modal("hide");
doPackageAction("/api/v1/service/rebuild", [packages], "Repository rebuild ran for the following dependencies:", "Repository rebuild failed:");
}
}
</script>

View File

@ -1,28 +1,34 @@
<div id="success-form" tabindex="-1" role="dialog" class="modal fade">
<div id="success-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-success">
<h4 class="modal-title">success</h4>
<div class="modal-header bg-success text-white">
<h4 id="success-title" class="modal-title"></h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<p>Packages update has been run.</p>
<p id="success-description"></p>
<ul id="success-details"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
</div>
</div>
</div>
</div>
<script>
const successForm = $("#success-form");
const successDetails = $("#success-details");
successForm.on("hidden.bs.modal", () => { reload(); });
const successModal = $("#success-modal");
successModal.on("hidden.bs.modal", () => { reload(); });
function showSuccess(details) {
const successDescription = $("#success-description");
const successDetails = $("#success-details");
const successTitle = $("#success-title");
function showSuccess(title, description, details) {
successTitle.text(title);
successDescription.text(description);
successDetails.empty().append(details);
successForm.modal("show");
successModal.modal("show");
}
</script>

View File

@ -1,20 +1,27 @@
<script>
const addButton = $("#add-btn");
const removeButton = $("#remove-btn");
const updateButton = $("#update-btn");
const keyImportButton = $("#key-import-btn");
const packageAddButton = $("#package-add-btn");
const packageRebuildButton = $("#package-rebuild-btn");
const packageRemoveButton = $("#package-remove-btn");
const packageUpdateButton = $("#package-update-btn");
const table = $("#packages");
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
() => {
removeButton.prop("disabled", !table.bootstrapTable("getSelections").length);
});
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", () => {
packageRemoveButton.prop("disabled", !table.bootstrapTable("getSelections").length);
});
table.on("click-row.bs.table", (self, data, row, cell) => {
if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showLogs(data.id);
});
const architectureBadge = $("#badge-architecture");
const repositoryBadge = $("#badge-repository");
const statusBadge = $("#badge-status");
const versionBadge = $("#badge-version");
function doPackageAction(uri, packages) {
function doPackageAction(uri, packages, successText, failureText) {
$.ajax({
url: uri,
data: JSON.stringify({packages: packages}),
@ -26,9 +33,11 @@
li.innerText = pkg;
return li;
});
showSuccess(details);
showSuccess("Success", successText, details);
},
error: (jqXHR, _, errorThrown) => {
showFailure("Action failed", failureText, errorThrown);
},
error: (jqXHR, _, errorThrown) => { showFailure(errorThrown); },
});
}
@ -36,14 +45,22 @@
return table.bootstrapTable("getSelections").map(row => { return row.id; });
}
function removePackages() { doPackageAction("/api/v1/service/remove", getSelection()); }
function removePackages() {
doPackageAction("/api/v1/service/remove", getSelection(), "The following packages have been removed:", "Packages removal failed:");
}
function updatePackages() { doPackageAction("/api/v1/service/add", getSelection()); }
function updatePackages() {
const currentSelection = getSelection();
const url = currentSelection.length === 0 ? "/api/v1/service/update" : "/api/v1/service/add";
doPackageAction(url, getSelection(), "Packages update has been run", "Packages update failed:");
}
function hideControls(hidden) {
addButton.attr("hidden", hidden);
removeButton.attr("hidden", hidden);
updateButton.attr("hidden", hidden);
keyImportButton.attr("hidden", hidden);
packageAddButton.attr("hidden", hidden);
packageRebuildButton.attr("hidden", hidden);
packageRemoveButton.attr("hidden", hidden);
packageUpdateButton.attr("hidden", hidden);
}
function reload() {
@ -95,7 +112,7 @@
table.bootstrapTable("hideLoading");
} else {
// other errors
showFailure(errorThrown);
showFailure("Load failure", "Could not load list of packages:", errorThrown);
}
hideControls(true);
},

View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<title>Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
{% include "utils/style.jinja2" %}
</head>
<body>
{% include "utils/bootstrap-scripts.jinja2" %}
<div class="d-flex flex-row align-items-center">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12 text-center">
<span class="display-1 d-block">{{ code }}</span>
<div class="mb-4 lead">{{ reason }}</div>
<a class="btn btn-link" style="text-decoration: none" href="/" title="home"><i class="bi bi-house"></i> home</a>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -10,19 +10,22 @@
<body>
{% include "utils/bootstrap-scripts.jinja2" %}
<div class="container">
<h1>Arch linux user repository</h1>
</div>
<div class="container">
{% if pgp_key is not none %}
<p>This repository is signed with <a href="https://pgp.mit.edu/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
<p>This repository is signed with <a href="https://keyserver.ubuntu.com/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
{% endif %}
<pre>$ cat /etc/pacman.conf
[{{ repository }}]
<p>In order to use this repository edit your <code>/etc/pacman.conf</code> as following:</p>
<pre class="language-ini"><code id="pacman-conf" class="language-ini">[{{ repository }}]
Server = {{ link_path }}
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly</pre>
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly</code><button id="copy-btn" type="button" class="btn language-ini" onclick="copyPacmanConf()"><i class="bi bi-clipboard"></i> copy</button></pre>
</div>
<div class="container">
@ -83,16 +86,24 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
{% if homepage is not none %}
<li><a class="nav-link" href="{{ homepage }}" title="homepage">homepage</a></li>
<li><a class="nav-link" href="{{ homepage }}" title="homepage"><i class="bi bi-house"></i> homepage</a></li>
{% endif %}
</ul>
<ul class="nav">
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li>
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman" title="sources"><i class="bi bi-github"></i> ahriman</a></li>
</ul>
</footer>
</div>
{% include "utils/bootstrap-scripts.jinja2" %}
<script>
const pacmanConf = $("#pacman-conf");
const pacmanConfCopyButton = $("#copy-btn");
async function copyPacmanConf() {
const conf = pacmanConf.text();
await copyToClipboard(conf, pacmanConfCopyButton);
}
</script>
</body>

View File

@ -4,9 +4,30 @@
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.min.js" integrity="sha384-IDwe1+LCz02ROU9k972gdyvl+AESN10+x7tBKgc9I5HFtuNz0wWnPclzo6p9vxnk" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.21.1/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.21.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.21.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script>
async function copyToClipboard(text, button) {
if (navigator.clipboard === undefined) {
const input = document.createElement("textarea");
input.innerHTML = text;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
} else {
await navigator.clipboard.writeText(text);
}
button.html("<i class=\"bi bi-clipboard-check\"></i> copied");
setTimeout(()=> {
button.html("<i class=\"bi bi-clipboard\"></i> copy");
}, 2000);
}
</script>

View File

@ -1,9 +1,24 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.21.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet">
<style>
.pre-scrollable {
display: block;
max-height: 680px;
overflow-y: scroll;
}
pre[class*="language-"] {
position: relative;
}
pre[class*="language-"] button{
position: absolute;
top: 0;
right: 5px;
}
</style>

View File

@ -66,14 +66,18 @@ setup(
("share/ahriman/templates", [
"package/share/ahriman/templates/build-status.jinja2",
"package/share/ahriman/templates/email-index.jinja2",
"package/share/ahriman/templates/error.jinja2",
"package/share/ahriman/templates/repo-index.jinja2",
"package/share/ahriman/templates/shell",
"package/share/ahriman/templates/telegram-index.jinja2",
]),
("share/ahriman/templates/build-status", [
"package/share/ahriman/templates/build-status/failed-modal.jinja2",
"package/share/ahriman/templates/build-status/key-import-modal.jinja2",
"package/share/ahriman/templates/build-status/login-modal.jinja2",
"package/share/ahriman/templates/build-status/package-add-modal.jinja2",
"package/share/ahriman/templates/build-status/package-info-modal.jinja2",
"package/share/ahriman/templates/build-status/package-rebuild-modal.jinja2",
"package/share/ahriman/templates/build-status/success-modal.jinja2",
"package/share/ahriman/templates/build-status/table.jinja2",
]),
@ -130,6 +134,7 @@ setup(
"aiohttp_session",
"aiohttp_security",
"cryptography",
"requests-unixsocket", # required by unix socket support
],
},
)

View File

@ -68,8 +68,8 @@ def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="ahriman", description="ArcH linux ReposItory MANager",
epilog="Argument list can also be read from file by using @ prefix.",
fromfile_prefix_chars="@", formatter_class=_formatter)
parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
action="append")
parser.add_argument("-a", "--architecture", help="target architectures. For several subcommands it can be used "
"multiple times", action="append")
parser.add_argument("-c", "--configuration", help="configuration path", type=Path, default=Path("/etc/ahriman.ini"))
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
parser.add_argument("-l", "--lock", help="lock file", type=Path,
@ -169,7 +169,7 @@ def _set_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
action="count", default=0)
action="count", default=False)
parser.set_defaults(handler=handlers.Daemon, dry_run=False, exit_code=False, package=[])
return parser
@ -229,7 +229,7 @@ def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
"fail in case if key is not known for build user. This subcommand can be used "
"in order to import the PGP key to user keychain.",
formatter_class=_formatter)
parser.add_argument("--key-server", help="key server for key import", default="pgp.mit.edu")
parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com")
parser.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False)
return parser
@ -263,7 +263,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
action="count", default=0)
action="count", default=False)
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
@ -483,7 +483,7 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
action="count", default=0)
action="count", default=False)
parser.set_defaults(handler=handlers.Update, dry_run=True, aur=True, local=True, manual=False)
return parser
@ -632,6 +632,8 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--build-command", help="build command prefix", default="ahriman")
parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
type=Path, default=Path("/usr/share/devtools/pacman-extra.conf"))
parser.add_argument("--makeflags-jobs", help="append MAKEFLAGS variable with parallelism set to number of cores",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--multilib", help="add or do not multilib repository",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--packager", help="packager name and email", required=True)
@ -640,6 +642,7 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--sign-target", help="sign options", action="append",
type=SignSettings.from_option, choices=enum_values(SignSettings))
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.add_argument("--web-unix-socket", help="path to unix socket used for interprocess communications", type=Path)
parser.set_defaults(handler=handlers.Setup, lock=None, report=False, quiet=True, unsafe=True)
return parser
@ -714,7 +717,7 @@ def _set_repo_triggers_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="run triggers on empty build result as configured by settings",
formatter_class=_formatter)
parser.add_argument("trigger", help="instead of running all triggers as set by configuration, just process "
"specified ones oin order of metion", nargs="*")
"specified ones in order of mention", nargs="*")
parser.set_defaults(handler=handlers.Triggers)
return parser
@ -745,7 +748,7 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
action="count", default=0)
action="count", default=False)
parser.set_defaults(handler=handlers.Update)
return parser
@ -760,7 +763,7 @@ def _set_shell_parser(root: SubParserAction) -> argparse.ArgumentParser:
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("shell", help="envoke python shell",
parser = root.add_parser("shell", help="invoke python shell",
description="drop into python shell while having created application",
formatter_class=_formatter)
parser.add_argument("code", help="instead of dropping into shell, just execute the specified code", nargs="?")
@ -782,9 +785,10 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("user-add", help="create or update user",
description="update user for web services with the given password and role. "
"In case if password was not entered it will be asked interactively",
epilog="In case of first run (i.e. if password salt is not set yet) this action requires "
"root privileges because it performs write to filesystem configuration.",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("--as-service", help="add user as service user", action="store_true")
parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, "
"which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-r", "--role", help="user access level",
@ -830,7 +834,6 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="remove user from the user mapping and update the configuration",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true")
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False, # nosec
password="", quiet=True, unsafe=True)
return parser

View File

@ -19,7 +19,7 @@
#
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.repository import Repository
@ -44,7 +44,8 @@ class ApplicationProperties(LazyLogging):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
unsafe(bool): if set no user check will be performed before path creation
refresh_pacman_database(int): pacman database syncronization level, ``0`` is disabled
refresh_pacman_database(int, optional): pacman database syncronization level, ``0`` is disabled
(Default value = 0)
"""
self.configuration = configuration
self.architecture = architecture

View File

@ -17,8 +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/>.
#
import shutil
from pathlib import Path
from typing import Callable, Iterable, List
@ -54,7 +52,7 @@ class ApplicationRepository(ApplicationProperties):
Args:
cache(bool): clear directory with package caches
chroot(bool): clear build chroot
manual(bool): clear directory with manually added packages
manual(bool): clear directory with manually added packages' bases
packages(bool): clear directory with built packages
pacman(bool): clear directory with pacman databases
"""
@ -85,13 +83,10 @@ class ApplicationRepository(ApplicationProperties):
if archive.filepath is None:
self.logger.warning("filepath is empty for %s", package.base)
continue # avoid mypy warning
src = self.repository.paths.repository / archive.filepath
dst = self.repository.paths.packages / archive.filepath
shutil.copy(src, dst)
# run generic update function
self.update([])
self.repository.sign.process_sign_package(archive.filepath, package.base)
# sign repository database if set
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
# process triggers
self.on_result(Result())
def unknown(self) -> List[str]:

View File

@ -57,7 +57,7 @@ class Backup(Handler):
@staticmethod
def get_paths(configuration: Configuration) -> Set[Path]:
"""
extract paths to backup
extract paths to back up
Args:
configuration(Configuration): configuration instance

View File

@ -28,6 +28,7 @@ from typing import List, Type
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError
from ahriman.core.log import Log
from ahriman.models.repository_paths import RepositoryPaths
@ -44,6 +45,7 @@ class Handler:
be called directly. The recommended way is to call ``execute`` class method, e.g.::
>>> from ahriman.application.handlers import Add
>>>
>>> Add.execute(args)
"""
@ -94,7 +96,8 @@ class Handler:
bool: True on success, False otherwise
"""
try:
configuration = Configuration.from_path(args.configuration, architecture, args.quiet)
configuration = Configuration.from_path(args.configuration, architecture)
Log.load(configuration, quiet=args.quiet, report=args.report)
with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration, report=args.report, unsafe=args.unsafe)
return True

View File

@ -125,7 +125,6 @@ class Patch(Handler):
package_base(Optional[str]): package base
variables(List[str]): extract patches only for specified PKGBUILD variables
exit_code(bool): exit with error on empty search result
:
"""
patches = application.database.patches_list(package_base, variables)
Patch.check_if_empty(exit_code, not patches)

View File

@ -76,5 +76,5 @@ class Rebuild(Handler):
List[Package]: list of packages which were stored in database
"""
if from_database:
return application.repository.packages()
return [package for (package, _) in application.database.packages_get()]
return [package for (package, _) in application.database.packages_get()]
return application.repository.packages()

View File

@ -64,7 +64,7 @@ class Setup(Handler):
application = Application(architecture, configuration, report=report, unsafe=unsafe)
Setup.configuration_create_makepkg(args.packager, application.repository.paths)
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
Setup.executable_create(application.repository.paths, args.build_command, architecture)
Setup.configuration_create_devtools(args.build_command, architecture, args.from_configuration,
args.multilib, args.repository, application.repository.paths)
@ -118,7 +118,11 @@ class Setup(Handler):
section = Configuration.section_name("web", architecture)
configuration.set_option(section, "port", str(args.web_port))
target = include_path / "setup-overrides.ini"
if args.web_unix_socket is not None:
section = Configuration.section_name("web", architecture)
configuration.set_option(section, "unix_socket", str(args.web_unix_socket))
target = include_path / "00-setup-overrides.ini"
with target.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)
@ -135,7 +139,7 @@ class Setup(Handler):
prefix(str): command prefix in {prefix}-{architecture}-build
architecture(str): repository architecture
source(Path): path to source configuration file
multilib(bool): add or do not multilib repository
multilib(bool): add or do not multilib repository to the configuration
repository(str): repository name
paths(RepositoryPaths): repository paths instance
"""
@ -166,17 +170,23 @@ class Setup(Handler):
configuration.write(devtools_configuration)
@staticmethod
def configuration_create_makepkg(packager: str, paths: RepositoryPaths) -> None:
def configuration_create_makepkg(packager: str, makeflags_jobs: bool, paths: RepositoryPaths) -> None:
"""
create configuration for makepkg
Args:
packager(str): packager identifier (e.g. name, email)
makeflags_jobs(bool): set MAKEFLAGS variable to number of cores
paths(RepositoryPaths): repository paths instance
"""
content = f"PACKAGER='{packager}'\n"
if makeflags_jobs:
content += """MAKEFLAGS="-j$(nproc)"\n"""
uid, _ = paths.root_owner
home_dir = Path(getpwuid(uid).pw_dir)
(home_dir / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n", encoding="utf8")
(home_dir / ".makepkg.conf").write_text(content, encoding="utf8")
@staticmethod
def configuration_create_sudo(paths: RepositoryPaths, prefix: str, architecture: str) -> None:

View File

@ -56,7 +56,8 @@ class Shell(Handler):
# licensed by https://creativecommons.org/licenses/by-sa/3.0
path = Path(sys.prefix) / "share" / "ahriman" / "templates" / "shell"
StringPrinter(path.read_text(encoding="utf8")).print(verbose=False)
# we only want to pass application isntance inside
if args.code is None:
code.interact(local=locals())
code.interact(local={"application": application})
else:
code.InteractiveConsole(locals=locals()).runcode(args.code)
code.InteractiveConsole(locals={"application": application}).runcode(args.code)

View File

@ -21,11 +21,12 @@ import argparse
import getpass
from pathlib import Path
from typing import Type
from typing import Optional, Tuple, Type
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import PasswordError
from ahriman.core.formatters import UserPrinter
from ahriman.models.action import Action
from ahriman.models.user import User
@ -54,12 +55,13 @@ class Users(Handler):
database = SQLite.load(configuration)
if args.action == Action.Update:
salt = Users.get_salt(configuration)
old_salt, salt = Users.get_salt(configuration)
user = Users.user_create(args)
auth_configuration = Users.configuration_get(configuration.include)
if old_salt is None:
auth_configuration = Users.configuration_get(configuration.include)
Users.configuration_create(auth_configuration, salt, args.secure)
Users.configuration_create(auth_configuration, user, salt, args.as_service, args.secure)
database.user_update(user.hash_password(salt))
elif args.action == Action.List:
users = database.user_list(args.username, args.role)
@ -70,22 +72,16 @@ class Users(Handler):
database.user_remove(args.username)
@staticmethod
def configuration_create(configuration: Configuration, user: User, salt: str,
as_service_user: bool, secure: bool) -> None:
def configuration_create(configuration: Configuration, salt: str, secure: bool) -> None:
"""
enable configuration if it has been disabled
Args:
configuration(Configuration): configuration instance
user(User): user descriptor
salt(str): password hash salt
as_service_user(bool): add user as service user, also set password and user to configuration
secure(bool): if true then set file permissions to 0o600
"""
configuration.set_option("auth", "salt", salt)
if as_service_user:
configuration.set_option("web", "username", user.username)
configuration.set_option("web", "password", user.password)
Users.configuration_write(configuration, secure)
@staticmethod
@ -99,7 +95,7 @@ class Users(Handler):
Returns:
Configuration: configuration instance. In case if there are local settings they will be loaded
"""
target = include_path / "auth.ini"
target = include_path / "00-auth.ini"
configuration = Configuration()
configuration.load(target)
@ -123,7 +119,7 @@ class Users(Handler):
path.chmod(0o600)
@staticmethod
def get_salt(configuration: Configuration, salt_length: int = 20) -> str:
def get_salt(configuration: Configuration, salt_length: int = 20) -> Tuple[Optional[str], str]:
"""
get salt from configuration or create new string
@ -132,11 +128,12 @@ class Users(Handler):
salt_length(int, optional): salt length (Default value = 20)
Returns:
str: current salt
Tuple[Optional[str], str]: tuple containing salt from configuration if any and actual salt which must be
used for password hash
"""
if salt := configuration.get("auth", "salt", fallback=None):
return salt
return User.generate_password(salt_length)
return salt, salt
return None, User.generate_password(salt_length)
@staticmethod
def user_create(args: argparse.Namespace) -> User:
@ -149,7 +146,15 @@ class Users(Handler):
Returns:
User: built user descriptor
"""
def read_password() -> str:
first_password = getpass.getpass()
second_password = getpass.getpass("Repeat password: ")
if first_password != second_password:
raise PasswordError("passwords don't match")
return first_password
password = args.password
if password is None:
password = getpass.getpass()
password = read_password()
return User(username=args.username, password=password, access=args.role)

View File

@ -28,7 +28,7 @@ from typing import Literal, Optional, Type
from ahriman import version
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRunError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.status.client import Client
from ahriman.core.util import check_user
from ahriman.models.build_status import BuildStatusEnum
@ -73,7 +73,7 @@ class Lock(LazyLogging):
self.unsafe = args.unsafe
self.paths = configuration.repository_paths
self.reporter = Client.load(configuration) if args.report else Client()
self.reporter = Client.load(configuration, report=args.report)
def __enter__(self) -> Lock:
"""

View File

@ -24,7 +24,7 @@ from pyalpm import DB, Handle, Package, SIG_PACKAGE, error as PyalpmError # typ
from typing import Generator, Set
from ahriman.core.configuration import Configuration
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.models.repository_paths import RepositoryPaths
@ -68,7 +68,7 @@ class Pacman(LazyLogging):
Args:
database(DB): pacman database instance to be copied
pacman_root(Path): operating system pacman's root
pacman_root(Path): operating system pacman root
paths(RepositoryPaths): repository paths instance
use_ahriman_cache(bool): use local ahriman cache instead of system one
"""

View File

@ -22,7 +22,7 @@ from __future__ import annotations
from typing import Dict, List, Type
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.models.aur_package import AURPackage

View File

@ -21,7 +21,7 @@ from pathlib import Path
from typing import List
from ahriman.core.exceptions import BuildError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output
from ahriman.models.repository_paths import RepositoryPaths

View File

@ -23,7 +23,7 @@ from typing import Optional, Type
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.models.auth_settings import AuthSettings
from ahriman.models.user_access import UserAccess
@ -55,14 +55,14 @@ class Auth(LazyLogging):
def auth_control(self) -> str:
"""
This workaround is required to make different behaviour for login interface.
In case of internal authentication it must provide an interface (modal form) to login with button sends POST
In case of internal authentication it must provide an interface (modal form) to log in with button sends POST
request. But for an external providers behaviour can be different: e.g. OAuth provider requires sending GET
request to external resource
Returns:
str: login control as html code to insert
"""
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>"""
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#login-modal" style="text-decoration: none"><i class="bi bi-box-arrow-in-right"></i> login</button>"""
@classmethod
def load(cls: Type[Auth], configuration: Configuration, database: SQLite) -> Auth:

View File

@ -29,61 +29,65 @@ except ImportError:
__all__ = ["authorized_userid", "check_authorized", "forget", "remember"]
async def authorized_userid(*args: Any) -> Any:
async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
"""
handle aiohttp security methods
Args:
*args(Any): argument list as provided by authorized_userid function
**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
"""
if _has_aiohttp_security:
return await aiohttp_security.authorized_userid(*args) # pylint: disable=no-value-for-parameter
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None
async def check_authorized(*args: Any) -> Any:
async def check_authorized(*args: Any, **kwargs: Any) -> Any:
"""
handle aiohttp security methods
Args:
*args(Any): argument list as provided by check_authorized function
**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
"""
if _has_aiohttp_security:
return await aiohttp_security.check_authorized(*args) # pylint: disable=no-value-for-parameter
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None
async def forget(*args: Any) -> Any:
async def forget(*args: Any, **kwargs: Any) -> Any:
"""
handle aiohttp security methods
Args:
*args(Any): argument list as provided by forget function
**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
"""
if _has_aiohttp_security:
return await aiohttp_security.forget(*args) # pylint: disable=no-value-for-parameter
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None
async def remember(*args: Any) -> Any:
async def remember(*args: Any, **kwargs: Any) -> Any:
"""
handle disabled auth
Args:
*args(Any): argument list as provided by remember function
**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
"""
if _has_aiohttp_security:
return await aiohttp_security.remember(*args) # pylint: disable=no-value-for-parameter
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None

View File

@ -32,7 +32,7 @@ class Mapping(Auth):
user authorization based on mapping from configuration file
Attributes:
salt(str): random generated string to salt passwords
salt(str): random generated string to salted password
database(SQLite): database instance
"""

View File

@ -30,7 +30,7 @@ from ahriman.models.auth_settings import AuthSettings
class OAuth(Mapping):
"""
OAuth user authorization.
OAuth's user authorization.
It is required to create application first and put application credentials.
Attributes:
@ -58,7 +58,7 @@ class OAuth(Mapping):
# thus we expect that address is set
self.redirect_uri = f"""{configuration.get("web", "address")}/api/v1/login"""
self.provider = self.get_provider(configuration.get("auth", "oauth_provider"))
# it is list but we will have to convert to string it anyway
# it is list, but we will have to convert to string it anyway
self.scopes = configuration.get("auth", "oauth_scopes")
@property
@ -69,7 +69,7 @@ class OAuth(Mapping):
Returns:
str: login control as html code to insert
"""
return """<a class="nav-link" href="/api/v1/login" title="login via OAuth2">login</a>"""
return """<a class="nav-link" href="/api/v1/login" title="login via OAuth2"><i class="bi bi-google"></i> login</a>"""
@staticmethod
def get_provider(name: str) -> Type[aioauth_client.OAuth2Client]:

View File

@ -23,7 +23,7 @@ import shutil
from pathlib import Path
from typing import List, Optional
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output, walk
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -33,7 +33,7 @@ from ahriman.models.repository_paths import RepositoryPaths
class Sources(LazyLogging):
"""
helper to download package sources (PKGBUILD etc)
helper to download package sources (PKGBUILD etc...)
Attributes:
DEFAULT_BRANCH(str): (class attribute) default branch to process git repositories.
@ -161,12 +161,12 @@ class Sources(LazyLogging):
str: patch as plain text
"""
instance = Sources()
instance.add(sources_dir, *pattern)
instance.add(sources_dir, *pattern, intent_to_add=True)
diff = instance.diff(sources_dir)
return f"{diff}\n" # otherwise, patch will be broken
@staticmethod
def push(sources_dir: Path, remote: RemoteSource, *pattern: str) -> None:
def push(sources_dir: Path, remote: RemoteSource, *pattern: str, commit_author: Optional[str] = None) -> None:
"""
commit selected changes and push files to the remote repository
@ -174,19 +174,23 @@ class Sources(LazyLogging):
sources_dir(Path): local path to git repository
remote(RemoteSource): remote target, branch and url
*pattern(str): glob patterns
commit_author(Optional[str], optional): commit author in form of git config (i.e. ``user <user@host>``)
(Default value = None)
"""
instance = Sources()
instance.add(sources_dir, *pattern)
instance.commit(sources_dir)
instance.commit(sources_dir, author=commit_author)
Sources._check_output("git", "push", remote.git_url, remote.branch, cwd=sources_dir, logger=instance.logger)
def add(self, sources_dir: Path, *pattern: str) -> None:
def add(self, sources_dir: Path, *pattern: str, intent_to_add: bool = False) -> None:
"""
track found files via git
Args:
sources_dir(Path): local path to git repository
*pattern(str): glob patterns
intent_to_add(bool, optional): record only the fact that it will be added later, acts as
--intent-to-add git flag (Default value = False)
"""
# glob directory to find files which match the specified patterns
found_files: List[Path] = []
@ -196,23 +200,26 @@ class Sources(LazyLogging):
return # no additional files found
self.logger.info("found matching files %s", found_files)
# add them to index
Sources._check_output("git", "add", "--intent-to-add",
*[str(fn.relative_to(sources_dir)) for fn in found_files],
args = ["--intent-to-add"] if intent_to_add else []
Sources._check_output("git", "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files],
cwd=sources_dir, logger=self.logger)
def commit(self, sources_dir: Path, commit_message: Optional[str] = None) -> None:
def commit(self, sources_dir: Path, message: Optional[str] = None, author: Optional[str] = None) -> None:
"""
commit changes
Args:
sources_dir(Path): local path to git repository
commit_message(Optional[str]): optional commit message if any. If none set, message will be generated
according to the current timestamp
message(Optional[str], optional): optional commit message if any. If none set, message will be generated
according to the current timestamp (Default value = None)
author(Optional[str], optional): optional commit author if any (Default value = None)
"""
if commit_message is None:
commit_message = f"Autogenerated commit at {datetime.datetime.utcnow()}"
Sources._check_output("git", "commit", "--allow-empty", "--message", commit_message,
cwd=sources_dir, logger=self.logger)
if message is None:
message = f"Autogenerated commit at {datetime.datetime.utcnow()}"
args = ["--allow-empty", "--message", message]
if author is not None:
args.extend(["--author", author])
Sources._check_output("git", "commit", *args, cwd=sources_dir, logger=self.logger)
def diff(self, sources_dir: Path) -> str:
"""

View File

@ -24,7 +24,7 @@ from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import BuildError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
@ -84,10 +84,12 @@ class Task(LazyLogging):
user=self.uid)
# well it is not actually correct, but we can deal with it
packages = Task._check_output("makepkg", "--packagelist",
exception=BuildError(self.package.base),
cwd=sources_dir,
logger=self.logger).splitlines()
packages = Task._check_output(
"makepkg", "--packagelist",
exception=BuildError(self.package.base),
cwd=sources_dir,
logger=self.logger
).splitlines()
return [Path(package) for package in packages]
def init(self, sources_dir: Path, database: SQLite) -> None:

View File

@ -20,12 +20,11 @@
from __future__ import annotations
import configparser
import logging
import shlex
import sys
from logging.config import fileConfig
from pathlib import Path
from typing import Any, Dict, Generator, List, Optional, Tuple, Type
from typing import Any, Dict, List, Optional, Tuple, Type
from ahriman.core.exceptions import InitializeError
from ahriman.models.repository_paths import RepositoryPaths
@ -38,8 +37,6 @@ class Configuration(configparser.RawConfigParser):
Attributes:
ARCHITECTURE_SPECIFIC_SECTIONS(List[str]): (class attribute) known sections which can be architecture specific.
Required by dump and merging functions
DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback)
DEFAULT_LOG_LEVEL(int): (class attribute) default log level (in case of fallback)
SYSTEM_CONFIGURATION_PATH(Path): (class attribute) default system configuration path distributed by package
architecture(Optional[str]): repository architecture
path(Optional[Path]): path to root configuration file
@ -64,9 +61,6 @@ class Configuration(configparser.RawConfigParser):
>>> path, architecture = configuration.check_loaded()
"""
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
@ -75,11 +69,11 @@ class Configuration(configparser.RawConfigParser):
default constructor. In the most cases must not be called directly
Args:
allow_no_value(bool): copies ``configparser.RawConfigParser`` behaviour. In case if it is set to ``True``,
the keys without values will be allowed
allow_no_value(bool, optional): copies ``configparser.RawConfigParser`` behaviour. In case if it is set
to ``True``, the keys without values will be allowed (Default value = False)
"""
configparser.RawConfigParser.__init__(self, allow_no_value=allow_no_value, converters={
"list": self.__convert_list,
"list": shlex.split,
"path": self.__convert_path,
})
self.architecture: Optional[str] = None
@ -117,14 +111,13 @@ class Configuration(configparser.RawConfigParser):
return RepositoryPaths(self.getpath("repository", "root"), architecture)
@classmethod
def from_path(cls: Type[Configuration], path: Path, architecture: str, quiet: bool) -> Configuration:
def from_path(cls: Type[Configuration], path: Path, architecture: str) -> Configuration:
"""
constructor with full object initialization
Args:
path(Path): path to root configuration file
architecture(str): repository architecture
quiet(bool): force disable any log messages
Returns:
Configuration: configuration instance
@ -132,42 +125,8 @@ class Configuration(configparser.RawConfigParser):
configuration = cls()
configuration.load(path)
configuration.merge_sections(architecture)
configuration.load_logging(quiet)
return configuration
@staticmethod
def __convert_list(value: str) -> List[str]:
"""
convert string value to list of strings
Args:
value(str): string configuration value
Returns:
List[str]: list of string from the parsed string
Raises:
ValueError: in case if option value contains unclosed quotes
"""
def generator() -> Generator[str, None, None]:
quote_mark = None
word = ""
for char in value:
if char in ("'", "\"") and quote_mark is None: # quoted part started, store quote and do nothing
quote_mark = char
elif char == quote_mark: # quoted part ended, reset quotation
quote_mark = None
elif char == " " and quote_mark is None: # found space outside the quotation, yield the word
yield word
word = ""
else: # append character to the buffer
word += char
if quote_mark: # there is unmatched quote
raise ValueError(f"unmatched quote in {value}")
yield word # sequence done, return whatever we found
return [word for word in generator() if word]
@staticmethod
def section_name(section: str, suffix: str) -> str:
"""
@ -281,23 +240,6 @@ class Configuration(configparser.RawConfigParser):
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
pass
def load_logging(self, quiet: bool) -> None:
"""
setup logging settings from configuration
Args:
quiet(bool): force disable any log messages
"""
try:
path = self.logging_path
fileConfig(path)
except Exception:
logging.basicConfig(filename=None, format=self.DEFAULT_LOG_FORMAT,
level=self.DEFAULT_LOG_LEVEL)
logging.exception("could not load logging from configuration, fallback to stderr")
if quiet:
logging.disable(logging.WARNING) # only print errors here
def merge_sections(self, architecture: str) -> None:
"""
merge architecture specific sections into main configuration
@ -310,8 +252,8 @@ class Configuration(configparser.RawConfigParser):
# get overrides
specific = self.section_name(section, architecture)
if self.has_section(specific):
# if there is no such section it means that there is no overrides for this arch
# but we anyway will have to delete sections for others archs
# if there is no such section it means that there is no overrides for this arch,
# but we anyway will have to delete sections for others architectures
for key, value in self[specific].items():
self.set_option(section, key, value)
# remove any arch specific section

View File

@ -27,7 +27,7 @@ from typing import List, Type
from ahriman.core.configuration import Configuration
from ahriman.core.database.data import migrate_data
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.models.migration import Migration
from ahriman.models.migration_result import MigrationResult

View File

@ -0,0 +1,35 @@
#
# Copyright (c) 2021-2022 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 logs (
package_base text not null,
process_id integer not null,
created real not null,
record text
)
""",
"""
create index logs_package_base_process_id on logs (package_base, process_id)
""",
]

View File

@ -21,5 +21,6 @@ from ahriman.core.database.operations.operations import Operations
from ahriman.core.database.operations.auth_operations import AuthOperations
from ahriman.core.database.operations.build_operations import BuildOperations
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

View File

@ -26,7 +26,7 @@ from ahriman.models.package import Package
class BuildOperations(Operations):
"""
operations for main functions
operations for build queue functions
"""
def build_queue_clear(self, package_base: Optional[str]) -> None:

View File

@ -0,0 +1,102 @@
#
# Copyright (c) 2021-2022 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 typing import List, Optional
from ahriman.core.database.operations import Operations
from ahriman.core.util import pretty_datetime
from ahriman.models.log_record_id import LogRecordId
class LogsOperations(Operations):
"""
logs operations
"""
def logs_get(self, package_base: str) -> str:
"""
extract logs for specified package base
Args:
package_base(str): package base to extract logs
Return:
str: full package log
"""
def run(connection: Connection) -> List[str]:
return [
f"""[{pretty_datetime(row["created"])}] {row["record"]}"""
for row in connection.execute(
"""
select created, record from logs where package_base = :package_base
order by created
""",
{"package_base": package_base})
]
records = self.with_connection(run)
return "\n".join(records)
def logs_insert(self, log_record_id: LogRecordId, created: float, record: str) -> None:
"""
write new log record to database
Args:
log_record_id(LogRecordId): current log record id
created(float): log created timestamp from log record attribute
record(str): log record
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into logs
(package_base, process_id, created, record)
values
(:package_base, :process_id, :created, :record)
""",
dict(
package_base=log_record_id.package_base,
process_id=log_record_id.process_id,
created=created,
record=record
)
)
return self.with_connection(run, commit=True)
def logs_remove(self, package_base: str, current_process_id: Optional[int]) -> None:
"""
remove log records for the specified package
Args:
package_base(str): package base to remove logs
current_process_id(Optional[int]): current process id. If set it will remove only logs belonging to another
process
"""
def run(connection: Connection) -> None:
connection.execute(
"""
delete from logs
where package_base = :package_base and (:process_id is null or process_id <> :process_id)
""",
{"package_base": package_base, "process_id": current_process_id}
)
return self.with_connection(run, commit=True)

View File

@ -22,7 +22,8 @@ import sqlite3
from pathlib import Path
from typing import Any, Dict, Tuple, TypeVar, Callable
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
T = TypeVar("T")

View File

@ -27,10 +27,11 @@ from typing import Type
from ahriman.core.configuration import Configuration
from ahriman.core.database.migrations import Migrations
from ahriman.core.database.operations import AuthOperations, BuildOperations, PackageOperations, PatchOperations
from ahriman.core.database.operations import AuthOperations, BuildOperations, LogsOperations, PackageOperations, \
PatchOperations
class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations):
class SQLite(AuthOperations, BuildOperations, LogsOperations, PackageOperations, PatchOperations):
"""
wrapper for sqlite3 database

View File

@ -179,6 +179,21 @@ class PathError(ValueError):
ValueError.__init__(self, f"Path `{path}` does not belong to repository root `{root}`")
class PasswordError(ValueError):
"""
exception which will be raised in case of password related errors
"""
def __init__(self, details: Any) -> None:
"""
default constructor
Args:
details(Any); error details
"""
ValueError.__init__(self, f"Password error: {details}")
class ReportError(RuntimeError):
"""
report generation exception

View File

@ -25,7 +25,7 @@ from tempfile import TemporaryDirectory
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import GitRemoteError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.util import walk
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource

View File

@ -26,7 +26,7 @@ from typing import Generator
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import GitRemoteError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
@ -38,6 +38,7 @@ class RemotePush(LazyLogging):
sync PKGBUILDs to remote repository after actions
Attributes:
commit_author(Optional[str]): optional commit author in form of git config (i.e. ``user <user@host>``)
remote_source(RemoteSource): repository remote source (remote pull url and branch)
"""
@ -49,6 +50,7 @@ class RemotePush(LazyLogging):
configuration(Configuration): configuration instance
remote_push_trigger.py
"""
self.commit_author = configuration.get(section, "commit_author", fallback=None)
self.remote_source = RemoteSource(
git_url=configuration.get(section, "push_url"),
web_url="",
@ -73,10 +75,8 @@ class RemotePush(LazyLogging):
# firstly, we need to remove old data to make sure that removed files are not tracked anymore...
package_target_dir = target_dir / package.base
shutil.rmtree(package_target_dir, ignore_errors=True)
# ...secondly, we copy whole tree...
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, package.remote)
shutil.copytree(clone_dir, package_target_dir)
# ...secondly, we clone whole tree...
Sources.fetch(package_target_dir, package.remote)
# ...and last, but not least, we remove the dot-git directory...
shutil.rmtree(package_target_dir / ".git", ignore_errors=True)
# ...and finally return path to the copied directory
@ -107,7 +107,8 @@ class RemotePush(LazyLogging):
try:
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, self.remote_source)
Sources.push(clone_dir, self.remote_source, *RemotePush.packages_update(result, clone_dir))
Sources.push(clone_dir, self.remote_source, *RemotePush.packages_update(result, clone_dir),
commit_author=self.commit_author)
except Exception:
self.logger.exception("git push failed")
raise GitRemoteError()

View File

@ -0,0 +1,21 @@
#
# Copyright (c) 2021-2022 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 ahriman.core.log.lazy_logging import LazyLogging
from ahriman.core.log.log import Log

View File

@ -0,0 +1,60 @@
#
# Copyright (c) 2021-2022 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 re
from aiohttp.web import AccessLogger, BaseRequest, StreamResponse
class FilteredAccessLogger(AccessLogger):
"""
access logger implementation with log filter enabled
Attributes:
LOG_PATH_REGEX(re.Pattern): (class attribute) regex for logs uri
"""
# official packages have only ``[A-Za-z0-9_.+-]`` regex
LOG_PATH_REGEX = re.compile(r"^/api/v1/packages/[A-Za-z0-9_.+%-]+/logs$")
@staticmethod
def is_logs_post(request: BaseRequest) -> bool:
"""
check if request looks lie logs posting
Args:
request(BaseRequest): http reqeust descriptor
Returns:
bool: True in case if request looks like logs positing and False otherwise
"""
return request.method == "POST" and FilteredAccessLogger.LOG_PATH_REGEX.match(request.path) is not None
def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
"""
access log with enabled filter by request path
Args:
request(BaseRequest): http reqeust descriptor
response(StreamResponse): streaming response object
time(float):
"""
if self.is_logs_post(request):
return
AccessLogger.log(self, request, response, time)

View File

@ -0,0 +1,84 @@
#
# Copyright (c) 2021-2022 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 __future__ import annotations
import logging
from ahriman.core.configuration import Configuration
class HttpLogHandler(logging.Handler):
"""
handler for the http logging. Because default ``logging.handlers.HTTPHandler`` does not support cookies
authorization, we have to implement own handler which overrides the ``logging.handlers.HTTPHandler.emit`` method
Attributes:
reporter(Client): build status reporter instance
"""
def __init__(self, configuration: Configuration, *, report: bool) -> None:
"""
default constructor
Args:
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
# we don't really care about those parameters because they will be handled by the reporter
logging.Handler.__init__(self)
# client has to be importer here because of circular imports
from ahriman.core.status.client import Client
self.reporter = Client.load(configuration, report=report)
@classmethod
def load(cls, configuration: Configuration, *, report: bool) -> HttpLogHandler:
"""
install logger. This function creates handler instance and adds it to the handler list in case if no other
http handler found
Args:
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
root = logging.getLogger()
if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None:
return handler # there is already registered instance
handler = cls(configuration, report=report)
root.addHandler(handler)
return handler
def emit(self, record: logging.LogRecord) -> None:
"""
emit log records using reporter client
Args:
record(logging.LogRecord): log record to log
"""
package_base = getattr(record, "package_base", None)
if package_base is None:
return # in case if no package base supplied we need just skip log message
try:
self.reporter.logs(package_base, record)
except Exception:
self.handleError(record)

View File

@ -17,9 +17,10 @@
# 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 contextlib
import logging
from typing import Any
from typing import Any, Generator
class LazyLogging:
@ -62,3 +63,47 @@ class LazyLogging:
clazz = self.__class__
prefix = "" if clazz.__module__ is None else f"{clazz.__module__}."
return f"{prefix}{clazz.__qualname__}"
@staticmethod
def _package_logger_reset() -> None:
"""
reset package logger to empty one
"""
logging.setLogRecordFactory(logging.LogRecord)
@staticmethod
def _package_logger_set(package_base: str) -> None:
"""
set package base as extra info to the logger
Args:
package_base(str): package base
"""
current_factory = logging.getLogRecordFactory()
def package_record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
record = current_factory(*args, **kwargs)
record.package_base = package_base
return record
logging.setLogRecordFactory(package_record_factory)
@contextlib.contextmanager
def in_package_context(self, package_base: str) -> Generator[None, None, None]:
"""
execute function while setting package context
Args:
package_base(str): package base to set context in
Examples:
This function is designed to be called as context manager with ``package_base`` argument, e.g.:
>>> with self.in_package_context(package.base):
>>> build_package(package)
"""
try:
self._package_logger_set(package_base)
yield
finally:
self._package_logger_reset()

View File

@ -0,0 +1,61 @@
#
# Copyright (c) 2021-2022 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 logging
from logging.config import fileConfig
from ahriman.core.configuration import Configuration
from ahriman.core.log.http_log_handler import HttpLogHandler
class Log:
"""
simple static method class which setups application loggers
Attributes:
DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback)
DEFAULT_LOG_LEVEL(int): (class attribute) default log level (in case of fallback)
"""
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG
@staticmethod
def load(configuration: Configuration, *, quiet: bool, report: bool) -> None:
"""
setup logging settings from configuration
Args:
configuration(Configuration): configuration instance
quiet(bool): force disable any log messages
report(bool): force enable or disable reporting
"""
try:
path = configuration.logging_path
fileConfig(path)
except Exception:
logging.basicConfig(filename=None, format=Log.DEFAULT_LOG_FORMAT,
level=Log.DEFAULT_LOG_LEVEL)
logging.exception("could not load logging from configuration, fallback to stderr")
HttpLogHandler.load(configuration, report=report)
if quiet:
logging.disable(logging.WARNING) # only print errors here

View File

@ -23,7 +23,7 @@ from typing import Iterable, Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.models.package import Package
from ahriman.models.report_settings import ReportSettings
from ahriman.models.result import Result
@ -54,7 +54,7 @@ class Report(LazyLogging):
>>> except Exception as exception:
>>> handle_exceptions(exception)
>>>
>>> report.run([], Result())
>>> report.run(Result(), [])
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import requests # technically we could use python-telegram-bot, but it is just a single request, cmon
import requests # technically we could use python-telegram-bot, but it is just a single request, c'mon
from typing import Iterable

View File

@ -84,7 +84,8 @@ class Executor(Cleaner):
result = Result()
for single in updates:
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (build_dir := Path(dir_name)):
with self.in_package_context(single.base), \
TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (build_dir := Path(dir_name)):
try:
build_single(single, build_dir)
result.add_success(single)
@ -110,6 +111,7 @@ class Executor(Cleaner):
self.paths.tree_clear(package_base) # remove all internal files
self.database.build_queue_clear(package_base)
self.database.patches_remove(package_base, [])
self.database.logs_remove(package_base, None)
self.reporter.remove(package_base) # we only update status page in case of base removal
except Exception:
self.logger.exception("could not remove base %s", package_base)
@ -153,21 +155,21 @@ class Executor(Cleaner):
Returns:
Result: path to repository database
"""
def rename(archive: PackageDescription, base: str) -> None:
def rename(archive: PackageDescription, package_base: str) -> None:
if archive.filename is None:
self.logger.warning("received empty package name for base %s", base)
self.logger.warning("received empty package name for base %s", package_base)
return # suppress type checking, it never can be none actually
if (safe := safe_filename(archive.filename)) != archive.filename:
shutil.move(self.paths.packages / archive.filename, self.paths.packages / safe)
archive.filename = safe
def update_single(name: Optional[str], base: str) -> None:
def update_single(name: Optional[str], package_base: str) -> None:
if name is None:
self.logger.warning("received empty package name for base %s", base)
self.logger.warning("received empty package name for base %s", package_base)
return # suppress type checking, it never can be none actually
# in theory, it might be NOT packages directory, but we suppose it is
full_path = self.paths.packages / name
files = self.sign.process_sign_package(full_path, base)
files = self.sign.process_sign_package(full_path, package_base)
for src in files:
dst = self.paths.repository / safe_filename(src.name)
shutil.move(src, dst)
@ -180,24 +182,25 @@ class Executor(Cleaner):
result = Result()
for local in updates:
try:
for description in local.packages.values():
rename(description, local.base)
update_single(description.filename, local.base)
self.reporter.set_success(local)
result.add_success(local)
with self.in_package_context(local.base):
try:
for description in local.packages.values():
rename(description, local.base)
update_single(description.filename, local.base)
self.reporter.set_success(local)
result.add_success(local)
current_package_archives = {
package
for current in current_packages
if current.base == local.base
for package in current.packages
}
removed_packages.extend(current_package_archives.difference(local.packages))
except Exception:
self.reporter.set_failed(local.base)
result.add_failed(local)
self.logger.exception("could not process %s", local.base)
current_package_archives = {
package
for current in current_packages
if current.base == local.base
for package in current.packages
}
removed_packages.extend(current_package_archives.difference(local.packages))
except Exception:
self.reporter.set_failed(local.base)
result.add_failed(local)
self.logger.exception("could not process %s", local.base)
self.clear_packages()
self.process_remove(removed_packages)

View File

@ -46,8 +46,7 @@ class Repository(Executor, UpdateHandler):
>>> built_packages = repository.packages_built()
>>> update_result = repository.process_update(built_packages)
>>>
>>> repository.process_report(["email"], update_result)
>>> repository.process_sync(["s3"], update_result.success)
>>> repository.triggers.on_result(update_result, repository.packages())
"""
def load_archives(self, packages: Iterable[Path]) -> List[Package]:

View File

@ -22,7 +22,7 @@ from ahriman.core.alpm.repo import Repo
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnsafeRunError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client
from ahriman.core.triggers import TriggerLoader
@ -58,7 +58,8 @@ class RepositoryProperties(LazyLogging):
database(SQLite): database instance
report(bool): force enable or disable reporting
unsafe(bool): if set no user check will be performed before path creation
refresh_pacman_database(int): pacman database syncronization level, ``0`` is disabled
refresh_pacman_database(int, optional): pacman database syncronization level, ``0`` is disabled
(Default value = 0)
"""
self.architecture = architecture
self.configuration = configuration
@ -77,5 +78,5 @@ class RepositoryProperties(LazyLogging):
self.pacman = Pacman(architecture, configuration, refresh_database=refresh_pacman_database)
self.sign = GPG(architecture, configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(configuration) if report else Client()
self.reporter = Client.load(configuration, report=report)
self.triggers = TriggerLoader(architecture, configuration)

View File

@ -56,26 +56,26 @@ class UpdateHandler(Cleaner):
result: List[Package] = []
for local in self.packages():
if local.base in self.ignore_list:
continue
if local.is_vcs and not vcs:
continue
if filter_packages and local.base not in filter_packages:
continue
source = local.remote.source if local.remote is not None else None
with self.in_package_context(local.base):
if local.base in self.ignore_list:
continue
if local.is_vcs and not vcs:
continue
if filter_packages and local.base not in filter_packages:
continue
source = local.remote.source if local.remote is not None else None
try:
if source == PackageSource.Repository:
remote = Package.from_official(local.base, self.pacman)
else:
remote = Package.from_aur(local.base, self.pacman)
if local.is_outdated(remote, self.paths):
self.reporter.set_pending(local.base)
result.append(remote)
except Exception:
self.reporter.set_failed(local.base)
self.logger.exception("could not load remote package %s", local.base)
continue
try:
if source == PackageSource.Repository:
remote = Package.from_official(local.base, self.pacman)
else:
remote = Package.from_aur(local.base, self.pacman)
if local.is_outdated(remote, self.paths):
self.reporter.set_pending(local.base)
result.append(remote)
except Exception:
self.reporter.set_failed(local.base)
self.logger.exception("could not load remote package %s", local.base)
return result
@ -89,20 +89,21 @@ class UpdateHandler(Cleaner):
result: List[Package] = []
packages = {local.base: local for local in self.packages()}
for dirname in self.paths.cache.iterdir():
try:
Sources.fetch(dirname, remote=None)
remote = Package.from_build(dirname)
for cache_dir in self.paths.cache.iterdir():
with self.in_package_context(cache_dir.name):
try:
Sources.fetch(cache_dir, remote=None)
remote = Package.from_build(cache_dir)
local = packages.get(remote.base)
if local is None:
self.reporter.set_unknown(remote)
result.append(remote)
elif local.is_outdated(remote, self.paths):
self.reporter.set_pending(local.base)
result.append(remote)
except Exception:
self.logger.exception("could not process package at %s", dirname)
local = packages.get(remote.base)
if local is None:
self.reporter.set_unknown(remote)
result.append(remote)
elif local.is_outdated(remote, self.paths):
self.reporter.set_pending(local.base)
result.append(remote)
except Exception:
self.logger.exception("could not process package at %s", cache_dir)
return result

View File

@ -24,7 +24,7 @@ from typing import List, Optional, Set, Tuple
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import BuildError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output, exception_response_text
from ahriman.models.sign_settings import SignSettings
@ -118,7 +118,7 @@ class GPG(LazyLogging):
"""
key = key if key.startswith("0x") else f"0x{key}"
try:
response = requests.get(f"http://{server}/pks/lookup", params={
response = requests.get(f"https://{server}/pks/lookup", params={
"op": "get",
"options": "mr",
"search": key
@ -157,20 +157,20 @@ class GPG(LazyLogging):
logger=self.logger)
return [path, path.parent / f"{path.name}.sig"]
def process_sign_package(self, path: Path, base: str) -> List[Path]:
def process_sign_package(self, path: Path, package_base: str) -> List[Path]:
"""
sign package if required by configuration
Args:
path(Path): path to file to sign
base(str): package base required to check for key overrides
package_base(str): package base required to check for key overrides
Returns:
List[Path]: list of generated files including original file
"""
if SignSettings.Packages not in self.targets:
return [path]
key = self.configuration.get("sign", f"key_{base}", fallback=self.default_key)
key = self.configuration.get("sign", f"key_{package_base}", fallback=self.default_key)
if key is None:
self.logger.error("no default key set, skip package %s sign", path)
return [path]

View File

@ -24,10 +24,10 @@ import uuid
from multiprocessing import Process, Queue
from threading import Lock, Thread
from typing import Callable, Dict, Iterable, Tuple
from typing import Callable, Dict, Iterable, Optional, Tuple
from ahriman.core.configuration import Configuration
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.models.package_source import PackageSource
@ -78,6 +78,17 @@ class Spawn(Thread, LazyLogging):
result = callback(args, architecture)
queue.put((process_id, result))
def key_import(self, key: str, server: Optional[str]) -> None:
"""
import key to service cache
Args:
key(str): key to import
server(str): PGP key server
"""
kwargs = {} if server is None else {"key-server": server}
self.spawn_process("key-import", key, **kwargs)
def packages_add(self, packages: Iterable[str], *, now: bool) -> None:
"""
add packages
@ -86,12 +97,19 @@ class Spawn(Thread, LazyLogging):
packages(Iterable[str]): packages list to add
now(bool): build packages now
"""
if not packages:
return self.spawn_process("repo-update")
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
if now:
kwargs["now"] = ""
return self.spawn_process("package-add", *packages, **kwargs)
self.spawn_process("package-add", *packages, **kwargs)
def packages_rebuild(self, depends_on: str) -> None:
"""
rebuild packages which depend on the specified package
Args:
depends_on(str): packages dependency
"""
self.spawn_process("repo-rebuild", **{"depends-on": depends_on})
def packages_remove(self, packages: Iterable[str]) -> None:
"""
@ -102,6 +120,12 @@ class Spawn(Thread, LazyLogging):
"""
self.spawn_process("package-remove", *packages)
def packages_update(self, ) -> None:
"""
run full repository update
"""
self.spawn_process("repo-update")
def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
"""
spawn external ahriman process with supplied arguments

View File

@ -19,6 +19,8 @@
#
from __future__ import annotations
import logging
from typing import List, Optional, Tuple, Type
from ahriman.core.configuration import Configuration
@ -33,20 +35,29 @@ class Client:
"""
@classmethod
def load(cls: Type[Client], configuration: Configuration) -> Client:
def load(cls: Type[Client], configuration: Configuration, *, report: bool) -> Client:
"""
load client from settings
Args:
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
Returns:
Client: client according to current settings
"""
if not report:
return cls()
address = configuration.get("web", "address", fallback=None)
host = configuration.get("web", "host", fallback=None)
port = configuration.getint("web", "port", fallback=None)
if address or (host and port):
socket = configuration.get("web", "unix_socket", fallback=None)
# basically we just check if there is something we can use for interaction with remote server
# at the moment (end of 2022) I think it would be much better idea to introduce flag like `enabled`,
# but it will totally break used experience
if address or (host and port) or socket:
from ahriman.core.status.web_client import WebClient
return WebClient(configuration)
return cls()
@ -60,17 +71,17 @@ class Client:
status(BuildStatusEnum): current package build status
"""
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
def get(self, package_base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
"""
get package status
Args:
base(Optional[str]): package base to get
package_base(Optional[str]): package base to get
Returns:
List[Tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
del base
del package_base
return []
def get_internal(self) -> InternalStatus:
@ -82,20 +93,29 @@ class Client:
"""
return InternalStatus(status=BuildStatus())
def remove(self, base: str) -> None:
def logs(self, package_base: str, record: logging.LogRecord) -> None:
"""
post log record
Args:
package_base(str) package base
record(logging.LogRecord): log record to post to api
"""
def remove(self, package_base: str) -> None:
"""
remove packages from watcher
Args:
base(str): package base to remove
package_base(str): package base to remove
"""
def update(self, base: str, status: BuildStatusEnum) -> None:
def update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike ``add`` it does not update package properties
Args:
base(str): package base to update
package_base(str): package base to update
status(BuildStatusEnum): current package build status
"""
@ -107,32 +127,32 @@ class Client:
status(BuildStatusEnum): current ahriman status
"""
def set_building(self, base: str) -> None:
def set_building(self, package_base: str) -> None:
"""
set package status to building
Args:
base(str): package base to update
package_base(str): package base to update
"""
return self.update(base, BuildStatusEnum.Building)
return self.update(package_base, BuildStatusEnum.Building)
def set_failed(self, base: str) -> None:
def set_failed(self, package_base: str) -> None:
"""
set package status to failed
Args:
base(str): package base to update
package_base(str): package base to update
"""
return self.update(base, BuildStatusEnum.Failed)
return self.update(package_base, BuildStatusEnum.Failed)
def set_pending(self, base: str) -> None:
def set_pending(self, package_base: str) -> None:
"""
set package status to pending
Args:
base(str): package base to update
package_base(str): package base to update
"""
return self.update(base, BuildStatusEnum.Pending)
return self.update(package_base, BuildStatusEnum.Pending)
def set_success(self, package: Package) -> None:
"""

View File

@ -17,14 +17,17 @@
# 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 os
from typing import Dict, List, Optional, Tuple
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
@ -57,6 +60,9 @@ class Watcher(LazyLogging):
self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
self.status = BuildStatus()
# special variables for updating logs
self._last_log_record_id = LogRecordId("", os.getpid())
@property
def packages(self) -> List[Tuple[Package, BuildStatus]]:
"""
@ -67,12 +73,12 @@ class Watcher(LazyLogging):
"""
return list(self.known.values())
def get(self, base: str) -> Tuple[Package, BuildStatus]:
def get(self, package_base: str) -> Tuple[Package, BuildStatus]:
"""
get current package base build status
Args:
base(str): package base
package_base(str): package base
Returns:
Tuple[Package, BuildStatus]: package and its status
@ -81,9 +87,21 @@ class Watcher(LazyLogging):
UnknownPackage: if no package found
"""
try:
return self.known[base]
return self.known[package_base]
except KeyError:
raise UnknownPackageError(base)
raise UnknownPackageError(package_base)
def get_logs(self, package_base: str) -> str:
"""
extract logs for the package base
Args:
package_base(str): package base
Returns:
str: package logs
"""
return self.database.logs_get(package_base)
def load(self) -> None:
"""
@ -110,6 +128,17 @@ class Watcher(LazyLogging):
"""
self.known.pop(package_base, None)
self.database.package_remove(package_base)
self.remove_logs(package_base, None)
def remove_logs(self, package_base: str, current_process_id: Optional[int]) -> None:
"""
remove package related logs
Args:
package_base(str): package base
current_process_id(int): current process id
"""
self.database.logs_remove(package_base, current_process_id)
def update(self, package_base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
"""
@ -132,6 +161,21 @@ class Watcher(LazyLogging):
self.known[package_base] = (package, full_status)
self.database.package_update(package, full_status)
def update_logs(self, log_record_id: LogRecordId, created: float, record: str) -> None:
"""
make new log record into database
Args:
log_record_id(LogRecordId): log record id
created(float): log created record
record(str): log record
"""
if self._last_log_record_id != log_record_id:
# there is new log record, so we remove old ones
self.remove_logs(log_record_id.package_base, log_record_id.process_id)
self._last_log_record_id = log_record_id
self.database.logs_insert(log_record_id, created, record)
def update_self(self, status: BuildStatusEnum) -> None:
"""
update service status

View File

@ -17,12 +17,14 @@
# 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 logging
import requests
from typing import List, Optional, Tuple
from urllib.parse import quote_plus as urlencode
from ahriman.core.configuration import Configuration
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.status.client import Client
from ahriman.core.util import exception_response_text
from ahriman.models.build_status import BuildStatusEnum, BuildStatus
@ -47,13 +49,12 @@ class WebClient(Client, LazyLogging):
Args:
configuration(Configuration): configuration instance
"""
self.address = self.parse_address(configuration)
self.address, use_unix_socket = self.parse_address(configuration)
self.user = User.from_option(
configuration.get("web", "username", fallback=None),
configuration.get("web", "password", fallback=None))
self.__session = requests.session()
self._login()
self.__session = self._create_session(use_unix_socket=use_unix_socket)
@property
def _login_url(self) -> str:
@ -61,7 +62,7 @@ class WebClient(Client, LazyLogging):
get url for the login api
Returns:
str: full url for web service to login
str: full url for web service to log in
"""
return f"{self.address}/api/v1/login"
@ -76,7 +77,7 @@ class WebClient(Client, LazyLogging):
return f"{self.address}/api/v1/status"
@staticmethod
def parse_address(configuration: Configuration) -> str:
def parse_address(configuration: Configuration) -> Tuple[str, bool]:
"""
parse address from configuration
@ -84,15 +85,38 @@ class WebClient(Client, LazyLogging):
configuration(Configuration): configuration instance
Returns:
str: valid http address
Tuple[str, bool]: tuple of server address and socket flag (True in case if unix socket must be used)
"""
if (unix_socket := configuration.get("web", "unix_socket", fallback=None)) is not None:
# special pseudo-protocol which is used for unix sockets
return f"http+unix://{urlencode(unix_socket)}", True
address = configuration.get("web", "address", fallback=None)
if not address:
# build address from host and port directly
host = configuration.get("web", "host")
port = configuration.getint("web", "port")
address = f"http://{host}:{port}"
return address
return address, False
def _create_session(self, *, use_unix_socket: bool) -> requests.Session:
"""
generate new request session
Args:
use_unix_socket(bool): if set to True then unix socket session will be generated instead of native requests
Returns:
requests.Session: generated session object
"""
if use_unix_socket:
import requests_unixsocket # type: ignore
session: requests.Session = requests_unixsocket.Session()
return session
session = requests.Session()
self._login()
return session
def _login(self) -> None:
"""
@ -114,17 +138,29 @@ class WebClient(Client, LazyLogging):
except Exception:
self.logger.exception("could not login as %s", self.user)
def _package_url(self, base: str = "") -> str:
def _logs_url(self, package_base: str) -> str:
"""
get url for the logs api
Args:
package_base(str): package base
Returns:
str: full url for web service for logs
"""
return f"{self.address}/api/v1/packages/{package_base}/logs"
def _package_url(self, package_base: str = "") -> str:
"""
url generator
Args:
base(str, optional): package base to generate url (Default value = "")
package_base(str, optional): package base to generate url (Default value = "")
Returns:
str: full url of web service for specific package base
"""
return f"{self.address}/api/v1/packages/{base}"
return f"{self.address}/api/v1/packages/{package_base}"
def add(self, package: Package, status: BuildStatusEnum) -> None:
"""
@ -147,18 +183,18 @@ class WebClient(Client, LazyLogging):
except Exception:
self.logger.exception("could not add %s", package.base)
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
def get(self, package_base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
"""
get package status
Args:
base(Optional[str]): package base to get
package_base(Optional[str]): package base to get
Returns:
List[Tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
try:
response = self.__session.get(self._package_url(base or ""))
response = self.__session.get(self._package_url(package_base or ""))
response.raise_for_status()
status_json = response.json()
@ -167,9 +203,9 @@ class WebClient(Client, LazyLogging):
for package in status_json
]
except requests.HTTPError as e:
self.logger.exception("could not get %s: %s", base, exception_response_text(e))
self.logger.exception("could not get %s: %s", package_base, exception_response_text(e))
except Exception:
self.logger.exception("could not get %s", base)
self.logger.exception("could not get %s", package_base)
return []
def get_internal(self) -> InternalStatus:
@ -191,38 +227,56 @@ class WebClient(Client, LazyLogging):
self.logger.exception("could not get web service status")
return InternalStatus(status=BuildStatus())
def remove(self, base: str) -> None:
def logs(self, package_base: str, record: logging.LogRecord) -> None:
"""
post log record
Args:
package_base(str) package base
record(logging.LogRecord): log record to post to api
"""
payload = {
"created": record.created,
"message": record.getMessage(),
"process_id": record.process,
}
# in this method exception has to be handled outside in logger handler
response = self.__session.post(self._logs_url(package_base), json=payload)
response.raise_for_status()
def remove(self, package_base: str) -> None:
"""
remove packages from watcher
Args:
base(str): basename to remove
package_base(str): basename to remove
"""
try:
response = self.__session.delete(self._package_url(base))
response = self.__session.delete(self._package_url(package_base))
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not delete %s: %s", base, exception_response_text(e))
self.logger.exception("could not delete %s: %s", package_base, exception_response_text(e))
except Exception:
self.logger.exception("could not delete %s", base)
self.logger.exception("could not delete %s", package_base)
def update(self, base: str, status: BuildStatusEnum) -> None:
def update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike ``add`` it does not update package properties
Args:
base(str): package base to update
package_base(str): package base to update
status(BuildStatusEnum): current package build status
"""
payload = {"status": status.value}
try:
response = self.__session.post(self._package_url(base), json=payload)
response = self.__session.post(self._package_url(package_base), json=payload)
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not update %s: %s", base, exception_response_text(e))
self.logger.exception("could not update %s: %s", package_base, exception_response_text(e))
except Exception:
self.logger.exception("could not update %s", base)
self.logger.exception("could not update %s", package_base)
def update_self(self, status: BuildStatusEnum) -> None:
"""

View File

@ -20,7 +20,7 @@
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.models.package import Package
from ahriman.models.result import Result

View File

@ -27,7 +27,7 @@ from typing import Generator, Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExtensionError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.triggers import Trigger
from ahriman.models.package import Package
from ahriman.models.result import Result

View File

@ -24,7 +24,7 @@ from typing import Iterable, Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import SynchronizationError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.models.package import Package
from ahriman.models.upload_settings import UploadSettings

View File

@ -19,14 +19,15 @@
#
import datetime
import io
import logging
import os
import re
import requests
import subprocess
from enum import Enum
from logging import Logger
from pathlib import Path
from pwd import getpwuid
from typing import Any, Dict, Generator, IO, Iterable, List, Optional, Type, Union
from ahriman.core.exceptions import OptionError, UnsafeRunError
@ -38,16 +39,18 @@ __all__ = ["check_output", "check_user", "exception_response_text", "filter_json
def check_output(*args: str, exception: Optional[Exception] = None, cwd: Optional[Path] = None,
input_data: Optional[str] = None, logger: Optional[Logger] = None, user: Optional[int] = None) -> str:
input_data: Optional[str] = None, logger: Optional[logging.Logger] = None,
user: Optional[int] = None) -> str:
"""
subprocess wrapper
Args:
*args(str): command line arguments
exception(Optional[Exception]): exception which has to be reraised instead of default subprocess exception
exception(Optional[Exception], optional): exception which has to be reraised instead of default subprocess
exception (Default value = None)
cwd(Optional[Path], optional): current working directory (Default value = None)
input_data(Optional[str], optional): data which will be written to command stdin (Default value = None)
logger(Optional[Logger], optional): logger to log command result if required (Default value = None)
logger(Optional[logging.Logger], optional): logger to log command result if required (Default value = None)
user(Optional[int], optional): run process as specified user (Default value = None)
Returns:
@ -82,10 +85,11 @@ def check_output(*args: str, exception: Optional[Exception] = None, cwd: Optiona
if logger is not None:
logger.debug(single)
environment = {"HOME": getpwuid(user).pw_dir} if user is not None else {}
# FIXME additional workaround for linter and type check which do not know that user arg is supported
# pylint: disable=unexpected-keyword-arg
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
user=user, text=True, encoding="utf8", bufsize=1) as process:
user=user, env=environment, text=True, encoding="utf8", bufsize=1) as process:
if input_data is not None:
input_channel = get_io(process, "stdin")
input_channel.write(input_data)
@ -150,7 +154,7 @@ def enum_values(enum: Type[Enum]) -> List[str]:
Returns:
List[str]: available enumeration values as string
"""
return [key.value for key in enum]
return [str(key.value) for key in enum] # explicit str conversion for typing
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
@ -284,7 +288,7 @@ def safe_filename(source: str) -> str:
# https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
# however we would like to allow some gen-delims characters in filename, because those characters are used
# as delimiter in other URI parts. The ones we allow are
# as delimiter in other URI parts. The ones we allow to are:
# ":" - used as separator in schema and userinfo
# "[" and "]" - used for host part
# "@" - used as separator between host and userinfo

View File

@ -42,7 +42,7 @@ class AURPackage:
description(str): package base description
url(Optional[str]): package upstream URL
num_votes(int): number of votes for the package
polularity(float): package popularity
popularity(float): package popularity
out_of_date(Optional[datetime.datetime]): package out of date timestamp if any
maintainer(Optional[str]): package maintainer
first_submitted(datetime.datetime): timestamp of the first package submission

View File

@ -0,0 +1,34 @@
#
# Copyright (c) 2021-2022 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
@dataclass(frozen=True)
class LogRecordId:
"""
log record process identifier
Attributes:
package_base(str): package base for which log record belongs
process_id(int): process id from which log record was emitted
"""
package_base: str
process_id: int

View File

@ -30,7 +30,7 @@ from typing import Any, Dict, Iterable, List, Optional, Set, Type
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb
from ahriman.core.exceptions import PackageInfoError
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output, full_version
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
@ -218,7 +218,7 @@ class Package(LazyLogging):
Args:
name(str): package name (either base or normal name)
pacman(Pacman): alpm wrapper instance
use_syncdb(bool): use pacman databases instead of official repositories RPC (Default value = True)
use_syncdb(bool, optional): use pacman databases instead of official repositories RPC (Default value = True)
Returns:
Package: package properties
@ -365,7 +365,8 @@ class Package(LazyLogging):
Args:
remote(Package): package properties from remote source
paths(RepositoryPaths): repository paths instance. Required for VCS packages cache
calculate_version(bool, optional): expand version to actual value (by calculating git versions) (Default value = True)
calculate_version(bool, optional): expand version to actual value (by calculating git versions)
(Default value = True)
Returns:
bool: True if the package is out-of-dated and False otherwise

View File

@ -36,7 +36,7 @@ class PackageSource(str, Enum):
AUR(PackageSource): (class attribute) source is an AUR package for which it should search
Directory(PackageSource): (class attribute) source is a directory which contains packages
Local(PackageSource): (class attribute) source is locally stored PKGBUILD
Remote(PackageSource): (class attribute) source is remote (http, ftp etc) link
Remote(PackageSource): (class attribute) source is remote (http, ftp etc...) link
Repository(PackageSource): (class attribute) source is official repository
Examples:

View File

@ -34,7 +34,7 @@ class SignSettings(str, Enum):
"""
Disabled = "disabled"
Packages = "pacakges"
Packages = "packages"
Repository = "repository"
@classmethod

Some files were not shown because too many files have changed in this diff Show More