Compare commits

...

33 Commits

Author SHA1 Message Date
4f6bd29ff4 Release 2.2.1 2022-09-14 04:49:08 +03:00
f6d9ea480a docs update 2022-09-14 04:48:11 +03:00
08f62842ba Release 2.2.0 2022-09-14 03:28:28 +03:00
1912790ccc Make optional dependencies trully optional (#67)
The issue appears when there is no boto, jinja and some other libraries
are not installed because the classes which use these libraries are
still being imported inside the package file. The fix removes those
imports from package root, because they should not be here, in fact,
content of report and upload packages must be imported only inside the
trigger class and only if they are actually required

This commit also adds setuptools as required dependency since it is used
for some parsers (previously it was provided dependency)
2022-09-11 01:44:06 +03:00
cf3c48ffeb patch architecture list in runtime (#66) 2022-08-09 15:18:20 +03:00
6633766cc3 frozen dataclasses 2022-07-26 14:40:28 +03:00
f73d1eb424 reduce docker image size a bit 2022-07-18 11:42:26 +03:00
87ab3683ad Release 2.1.0 2022-07-02 15:42:39 +03:00
9c2f73af8c simplify tmpdir method 2022-06-28 19:11:38 +03:00
31f551bdf2 review loggers once more
This commit makes loggers like java.util.logging with fully qualified
logger name which is created by LazyLogging trait
2022-06-28 11:00:45 +03:00
cbbe2b01e4 fix case with package name which cannot be downloaded
(without special settings)

The issue appears if file or its version contains one of special URI
characters, e.g. +. Theu will be interpreted as query parameters by
(some) servers (e.g. S3 works in this way). In this commit we rename
archive to the one with safe name.
2022-06-27 18:53:48 +03:00
cd361a483d review loggers once more
This commit makes loggers like java.util.logging with fully qualified
logger name which is created by LazyLogging trait
2022-06-27 01:41:49 +03:00
cee4fd4cce bump web libraries
also encode strings for the views
2022-06-24 12:03:38 +03:00
18daecaac7 review loggers 2022-06-21 11:48:42 +03:00
03c298c762 replace if with while for telelgram reporting 2022-06-10 12:02:00 +03:00
79d4a488a0 Release 2.0.0 2022-06-05 20:46:32 +03:00
22847aac68 small docs update 2022-06-04 15:24:14 +03:00
75b9a96798 do not pass timestamp to pretty_datetime functioon 2022-05-31 21:26:39 +03:00
a0cfb7561d rewrite api under single /api endpoint 2022-05-29 17:15:49 +03:00
f4131b8cd7 add shell and version parser 2022-05-27 18:34:07 +03:00
9f134e37b6 update readme 2022-05-22 16:00:24 +03:00
07b7689701 do not bump timestamp on empty updates 2022-05-21 04:19:04 +03:00
47de715d7d dynamic html load (#63)
* dynamic html load
* split by classes
2022-05-20 22:29:36 +03:00
5674b7b388 add ability to run only speicifed triggers from command line
This commit also restores repo-report and repo-sync subcommands
2022-05-12 18:43:36 +03:00
f6e57a411c fix case when provided trigger path is directory
If trigger is directory and we have permissions to read it, the loaded
will try to load this from it and will fail with IsADirectoryError
2022-05-12 07:22:50 +03:00
38e48b1571 add not about pacman database update 2022-05-11 04:30:24 +03:00
d492c968ea add contribution guide 2022-05-11 03:17:37 +03:00
a11fd188a2 Release 2.0.0rc11 2022-05-10 06:03:33 +03:00
2431d5de0e fix bug with checking file
The bug appear when the file exists or doesn't, but we don't have
permissions to read it. This one must be treated as missed permission
2022-05-10 06:01:41 +03:00
88f71b240d Release 2.0.0rc10 2022-05-09 21:51:35 +03:00
99874845b5 triggers implementation (#62) 2022-05-09 20:00:20 +03:00
d98cfa3732 Release 2.0.0rc9 2022-05-08 03:58:53 +03:00
b6db2a8035 fix error with missing sources
In case if package has local cache it will fail to load because no
remote source set. Particially this case can be observed during tree
load
2022-05-08 03:56:54 +03:00
229 changed files with 6641 additions and 4943 deletions

View File

@ -1 +1 @@
skips: ['B101', 'B105', 'B404']
skips: ['B101', 'B105', 'B106', 'B404']

View File

@ -11,9 +11,9 @@ assignees: ''
A clear and concise description of what the bug is.
### Steps to Reproduce
### Steps to reproduce
Steps to reproduce the behavior (commands, environment etc)
Steps to reproduce the behavior (commands, environment etc).
### Expected behavior
@ -21,4 +21,8 @@ A clear and concise description of what you expected to happen.
### Logs
Add logs to help explain your problem. Logs to stderr can be generated by using `--no-log` command line option.
Add logs to help explain your problem. By default, the application writes logs into `/dev/log` which is usually default systemd journal and can be accessed by `journalctl` command.
You can also attach any additional information which can be helpful, e.g. configuration used by the application (be aware of passwords and other secrets if any); it can be generated by using `ahriman config` command.
It is also sometimes useful to have information about installed packages which can be accessed by `ahriman version` command.

View File

@ -13,7 +13,7 @@ Brief description of the feature required
### Cause of the feature request
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
### Proposed changes and/or features

View File

@ -8,7 +8,7 @@ echo -e '[arcanisrepo]\nServer = http://repo.arcanis.me/$arch\nSigLevel = Never'
# refresh the image
pacman --noconfirm -Syu
# main dependencies
pacman --noconfirm -Sy base-devel devtools git pyalpm python-aur python-passlib python-srcinfo sudo
pacman --noconfirm -Sy base-devel devtools git pyalpm python-aur python-passlib python-setuptools python-srcinfo sudo
# make dependencies
pacman --noconfirm -Sy python-build python-installer python-wheel
# optional dependencies

View File

@ -4,7 +4,7 @@
set -ex
# install dependencies
pacman --noconfirm -Syu base-devel python-pip python-tox
pacman --noconfirm -Syu base-devel python-pip python-setuptools python-tox
# run test and check targets
make check tests

View File

@ -60,17 +60,7 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
raw-checker-failed,
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
@ -78,67 +68,6 @@ disable=print-statement,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
missing-module-docstring,
line-too-long,
no-name-in-module,

67
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,67 @@
# Contributing to ahriman
Welcome to ahriman! The goal of the project is to provide the best user experience to manage Archlinux repositories. In order to follow this we set some limitations for the issue creations and heavily restricted code contribution.
## Create an issue
Basically just follow the suggested templates:
* Bug report requires at least the way to reproduce the issue and behaviour description (expected and actual ones). In order to resolve the bug, the additional questions may be asked, please consider them as lesser evil.
* Feature requests basically just require feature description and the purpose why do you want this feature to be implemented. It is required to make sure that the feature you want is going to be implemented in the way you really want it (and to make sure that this feature is not already implemented).
* Questions and discussions have free templates, and you are free to ask your question in the way you want.
## Code contribution
There are some strict limitation for suggested pull requests:
* `autopep8`, `bandit`, `pylint`, `mypy` must pass.
* Test coverage must remain 100%.
### Code formatting
In order to resolve all difficult cases the `autopep8` is used. You can perform formatting at any time by running `make check` or running `autopep8` command directly.
### Code style
Again, the most checks can be performed by `make check` command, though some additional guidelines must be applied:
* Every class, every function (including private and protected), every attribute must be documented. The project follows [Google style documentation](https://google.github.io/styleguide/pyguide.html). The only exception is local functions.
* For any path interactions `pathlib.Path` must be used.
* Configuration interactions must go through `ahriman.core.configuration.Configuration` class instance.
* In case if class load requires some actions, it is recommended to create class method which can be used for class instantiating.
* The code must follow the exception safety, unless it is explicitly asked by end user. It means that most exceptions must be handled and printed to log, no other actions must be done (e.g. raising another exception).
* For the external command `ahriman.core.util.check_output` function must be used.
* Every temporary file/directory must be removed at the end of processing, no matter what. The ``tempfile`` module provides good ways to do it.
* Import order must be the following:
```python
# optional imports from future module
from __future__ import annotations
# Module import for those which are installed into environment (no matter standard library or not)...
import os
# ...one per line...
import pyalpm
# ...in alphabetical order
import sys
# Blank line between
# ``from module import submodule`` import
from pathlib import Path
# ...again in alphabet order. It is possible to do several imports, but make sure that they are also in alphabetical order.
from pyalpm import Handle, Package
# Blank line again and package imports
from ahriman.core.configuration import Configuration
```
* One file should define only one class, exception is class satellites in case if file length remain less than 200 lines.
* It is possible to create file which contains some functions (e.g. `ahriman.core.util`), but in this case you would need to define `__all__` attribute.
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits.
* No global variable 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.
### Other checks
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.

View File

@ -1,4 +1,4 @@
FROM archlinux:base-devel
FROM archlinux:base
# image configuration
ENV AHRIMAN_ARCHITECTURE="x86_64"
@ -13,24 +13,22 @@ ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman"
ENV AHRIMAN_USER="ahriman"
# install environment
## install git which is required for AUR interaction and go for yay
RUN pacman --noconfirm -Syu git go
## install minimal required packages
RUN pacman --noconfirm -Syu binutils fakeroot git make sudo
## create build user
RUN useradd -m -d /home/build -s /usr/bin/nologin build && \
echo "build ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/build
## install AUR helper
RUN YAY_DIR="$(runuser -u build -- mktemp -d)" && \
git clone https://aur.archlinux.org/yay.git "$YAY_DIR" && \
cd "$YAY_DIR" && \
runuser -u build -- makepkg --noconfirm --install && \
cd - && rm -r "$YAY_DIR"
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
## install package dependencies
RUN runuser -u build -- yay --noconfirm -Sy devtools git pyalpm python-inflection python-passlib python-requests python-srcinfo && \
runuser -u build -- yay --noconfirm -Sy python-build python-installer python-wheel && \
runuser -u build -- yay --noconfirm -Sy breezy darcs mercurial python-aioauth-client python-aiohttp \
python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security \
python-aiohttp-session python-boto3 python-cryptography python-jinja \
rsync subversion
## 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 && \
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-jinja2 python-aiohttp-debugtoolbar \
python-aiohttp-session python-aiohttp-security
# cleanup unused
RUN find "/var/cache/pacman/pkg" -type f -delete
# install ahriman
## copy tree
@ -41,7 +39,7 @@ RUN cd "/home/build/ahriman" && \
cp ./*-src.tar.xz "package/archlinux" && \
cd "package/archlinux" && \
runuser -u build -- makepkg --noconfirm --install --skipchecksums && \
cd - && rm -r "/home/build/ahriman"
cd / && rm -r "/home/build/ahriman"
VOLUME ["/var/lib/ahriman"]

View File

@ -1,9 +1,9 @@
.PHONY: archive archive_directory archlinux check clean directory push spec spec-html tests version
.PHONY: archive archlinux check clean directory html push specification tests version
.DEFAULT_GOAL := archlinux
PROJECT := ahriman
FILES := AUTHORS COPYING README.md docs package src setup.py tox.ini web.png
FILES := AUTHORS CONTRIBUTING.md COPYING README.md docs package src setup.py tox.ini web.png
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
IGNORE_FILES := package/archlinux src/.mypy_cache
@ -33,22 +33,22 @@ clean:
directory: clean
mkdir "$(PROJECT)"
push: spec archlinux
html: specification
rm -rf docs/html
tox -e docs-html
push: specification archlinux
git add package/archlinux/PKGBUILD src/ahriman/version.py docs/ahriman-architecture.svg docs/ahriman.1
git commit -m "Release $(VERSION)"
git tag "$(VERSION)"
git push
git push --tags
spec:
specification:
# make sure that old files are removed
find docs -type f -name "$(PROJECT)*.rst" -delete
tox -e docs
spec-html: spec
rm -rf docs/html
tox -e docs-html
tests: clean
tox -e tests

View File

@ -2,7 +2,7 @@
[![tests status](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
[![setup status](https://github.com/arcan1s/ahriman/actions/workflows/run-setup.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/run-setup.yml)
[![docker image](https://github.com/arcan1s/ahriman/actions/workflows/docker-image.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/docker-image.yml)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/arcan1s/ahriman?label=docker%20image)](https://hub.docker.com/r/arcan1s/ahriman)
[![CodeFactor](https://www.codefactor.io/repository/github/arcan1s/ahriman/badge)](https://www.codefactor.io/repository/github/arcan1s/ahriman)
[![Documentation Status](https://readthedocs.org/projects/ahriman/badge/?version=latest)](https://ahriman.readthedocs.io/?badge=latest)
@ -10,23 +10,26 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
## Features
* Install-configure-forget manager for own repository.
* Install-configure-forget manager for the very own repository.
* Multi-architecture support.
* VCS packages support.
* Sign support with gpg (repository, package, per package settings).
* Synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Dependency manager.
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Sign support with gpg (repository, package, per package settings).
* 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:
![web interface](web.png)
## Installation and run
For installation details please refer to the [documentation](docs/setup.rst). For command help, `--help` subcommand must be used. Subcommands have own help message as well. The package also provides a [man page](docs/ahriman.1).
For installation details kindly refer to the [documentation](https://ahriman.readthedocs.io/en/latest/setup.html). For application commands it is possible to get information by using `--help`/`help` command or by using man page ([web version](https://ahriman.readthedocs.io/en/latest/command-line.html)).
## Configuration
Every available option is described in the [documentation](docs/configuration.rst).
Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/latest/configuration.html).
## [FAQ](docs/faq.rst)
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)

View File

@ -33,7 +33,7 @@ fi
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" repo-setup "${AHRIMAN_SETUP_ARGS[@]}"
# refresh database
runuser -u build -- yay --noconfirm -Syy &> /dev/null
pacman -Syy &> /dev/null
# create machine-id which is required by build tools
systemd-machine-id-setup &> /dev/null

12
docker/install-aur-package.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
set -e
for PACKAGE in "$@"; do
BUILD_DIR="$(mktemp -d)"
git clone https://aur.archlinux.org/"$PACKAGE".git "$BUILD_DIR"
cd "$BUILD_DIR"
makepkg --noconfirm --install --rmdeps --syncdeps
cd /
rm -r "$BUILD_DIR"
done

View File

@ -37,7 +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_report(None, update_result)
repository.process_sync(None, update_result.success)
repository.process_triggers(update_result)
For the more info please refer to the classes documentation.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 537 KiB

View File

@ -3,9 +3,9 @@
ahriman
.SH SYNOPSIS
.B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-update,update,user-add,user-list,user-remove,web} ...
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ...
.SH DESCRIPTION
ArcH Linux ReposItory MANager
ArcH linux ReposItory MANager
.SH OPTIONS
.TP
@ -37,7 +37,7 @@ force disable any logging
allow to run ahriman as non\-ahriman user. Some actions might be unavailable
.TP
\fB\-v\fR, \fB\-\-version\fR
\fB\-V\fR, \fB\-\-version\fR
show program's version number and exit
.SH
@ -115,9 +115,15 @@ update repository status
\fBahriman\fR \fI\,repo-sync\/\fR
sync repository
.TP
\fBahriman\fR \fI\,repo-triggers\/\fR
run triggers
.TP
\fBahriman\fR \fI\,repo-update\/\fR
update packages
.TP
\fBahriman\fR \fI\,shell\/\fR
envoke python shell
.TP
\fBahriman\fR \fI\,user-add\/\fR
create or update user
.TP
@ -127,6 +133,9 @@ user known users and their access
\fBahriman\fR \fI\,user-remove\/\fR
remove user
.TP
\fBahriman\fR \fI\,version\/\fR
application version
.TP
\fBahriman\fR \fI\,web\/\fR
web server
.SH COMMAND \fI\,'ahriman aur-search'\/\fR
@ -189,8 +198,7 @@ PGP key to import from public server
key server for key import
.SH COMMAND \fI\,'ahriman package-add'\/\fR
usage: ahriman package-add [-h] [-e] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}]
usage: ahriman package-add [-h] [-e] [-n] [-s {auto,archive,aur,directory,local,remote,repository}]
[--without-dependencies]
package [package ...]
@ -210,7 +218,7 @@ return non\-zero exit status if result is empty
run update function after
.TP
\fB\-s\fR \fI\,{PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}\/\fR, \fB\-\-source\fR \fI\,{PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}\/\fR
\fB\-s\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR, \fB\-\-source\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR
explicitly specify the package source for this command
.TP
@ -227,9 +235,7 @@ remove package from the repository
package name or base
.SH COMMAND \fI\,'ahriman package-status'\/\fR
usage: ahriman package-status [-h] [--ahriman] [-e] [-i]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
usage: ahriman package-status [-h] [--ahriman] [-e] [-i] [-s {unknown,pending,building,failed,success}] [package ...]
request status of the package
@ -251,7 +257,7 @@ return non\-zero exit status if result is empty
show additional package information
.TP
\fB\-s\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR, \fB\-\-status\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR
\fB\-s\fR \fI\,{unknown,pending,building,failed,success}\/\fR, \fB\-\-status\fR \fI\,{unknown,pending,building,failed,success}\/\fR
filter packages by status
.SH COMMAND \fI\,'ahriman package-status-remove'\/\fR
@ -264,9 +270,7 @@ remove the package from the status page
remove specified packages
.SH COMMAND \fI\,'ahriman package-status-update'\/\fR
usage: ahriman package-status-update [-h]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
usage: ahriman package-status-update [-h] [-s {unknown,pending,building,failed,success}] [package ...]
update package status on the status page
@ -276,7 +280,7 @@ set status for specified packages. If no packages supplied, service status will
.SH OPTIONS \fI\,'ahriman package-status-update'\/\fR
.TP
\fB\-s\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR, \fB\-\-status\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR
\fB\-s\fR \fI\,{unknown,pending,building,failed,success}\/\fR, \fB\-\-status\fR \fI\,{unknown,pending,building,failed,success}\/\fR
new status
.SH COMMAND \fI\,'ahriman patch-add'\/\fR
@ -409,14 +413,10 @@ just perform check for packages without removal
show additional package information
.SH COMMAND \fI\,'ahriman repo-report'\/\fR
usage: ahriman repo-report [-h] [target ...]
usage: ahriman repo-report [-h]
generate repository report according to current settings
.TP
\fBtarget\fR
target to generate report
.SH COMMAND \fI\,'ahriman repo-restore'\/\fR
usage: ahriman repo-restore [-h] [-o OUTPUT] path
@ -434,8 +434,7 @@ 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] [--no-multilib] --packager PACKAGER --repository
REPOSITORY [--sign-key SIGN_KEY]
[--sign-target {SignSettings.Disabled,SignSettings.Packages,SignSettings.Repository}]
REPOSITORY [--sign-key SIGN_KEY] [--sign-target {disabled,pacakges,repository}]
[--web-port WEB_PORT]
create initial service configuration, requires root
@ -470,7 +469,7 @@ repository name
sign key id
.TP
\fB\-\-sign\-target\fR \fI\,{SignSettings.Disabled,SignSettings.Packages,SignSettings.Repository}\/\fR
\fB\-\-sign\-target\fR \fI\,{disabled,pacakges,repository}\/\fR
sign options
.TP
@ -487,24 +486,28 @@ usage: ahriman repo-sign [-h] [package ...]
sign only specified packages
.SH COMMAND \fI\,'ahriman repo-status-update'\/\fR
usage: ahriman repo-status-update [-h]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
usage: ahriman repo-status-update [-h] [-s {unknown,pending,building,failed,success}]
update repository status on the status page
.SH OPTIONS \fI\,'ahriman repo-status-update'\/\fR
.TP
\fB\-s\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR, \fB\-\-status\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR
\fB\-s\fR \fI\,{unknown,pending,building,failed,success}\/\fR, \fB\-\-status\fR \fI\,{unknown,pending,building,failed,success}\/\fR
new status
.SH COMMAND \fI\,'ahriman repo-sync'\/\fR
usage: ahriman repo-sync [-h] [target ...]
usage: ahriman repo-sync [-h]
sync repository files to remote server according to current settings
.SH COMMAND \fI\,'ahriman repo-triggers'\/\fR
usage: ahriman repo-triggers [-h] [trigger ...]
run triggers on empty build result as configured by settings
.TP
\fBtarget\fR
target to sync
\fBtrigger\fR
instead of running all triggers as set by configuration, just process specified ones oin order of metion
.SH COMMAND \fI\,'ahriman repo-update'\/\fR
usage: ahriman repo-update [-h] [--dry-run] [-e] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
@ -540,9 +543,13 @@ do not include manual updates
\fB\-\-no\-vcs\fR
do not check VCS packages
.SH COMMAND \fI\,'ahriman shell'\/\fR
usage: ahriman shell [-h]
drop into python shell while having created application
.SH COMMAND \fI\,'ahriman user-add'\/\fR
usage: ahriman user-add [-h] [--as-service] [-p PASSWORD] [-r {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [-s]
username
usage: ahriman user-add [-h] [--as-service] [-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
@ -561,7 +568,7 @@ user password. Blank password will be treated as empty password, which is in par
authorization type.
.TP
\fB\-r\fR \fI\,{UserAccess.Safe,UserAccess.Read,UserAccess.Write}\/\fR, \fB\-\-role\fR \fI\,{UserAccess.Safe,UserAccess.Read,UserAccess.Write}\/\fR
\fB\-r\fR \fI\,{unauthorized,read,reporter,full}\/\fR, \fB\-\-role\fR \fI\,{unauthorized,read,reporter,full}\/\fR
user access level
.TP
@ -569,7 +576,7 @@ user access level
set file permissions to user\-only
.SH COMMAND \fI\,'ahriman user-list'\/\fR
usage: ahriman user-list [-h] [-e] [-r {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [username]
usage: ahriman user-list [-h] [-e] [-r {unauthorized,read,reporter,full}] [username]
list users from the user mapping and their roles
@ -583,7 +590,7 @@ filter users by username
return non\-zero exit status if result is empty
.TP
\fB\-r\fR \fI\,{UserAccess.Safe,UserAccess.Read,UserAccess.Write}\/\fR, \fB\-\-role\fR \fI\,{UserAccess.Safe,UserAccess.Read,UserAccess.Write}\/\fR
\fB\-r\fR \fI\,{unauthorized,read,reporter,full}\/\fR, \fB\-\-role\fR \fI\,{unauthorized,read,reporter,full}\/\fR
filter users by role
.SH COMMAND \fI\,'ahriman user-remove'\/\fR
@ -600,6 +607,11 @@ username for web service
\fB\-s\fR, \fB\-\-secure\fR
set file permissions to user\-only
.SH COMMAND \fI\,'ahriman version'\/\fR
usage: ahriman version [-h]
print application and its dependencies versions
.SH COMMAND \fI\,'ahriman web'\/\fR
usage: ahriman web [-h]

View File

@ -92,14 +92,6 @@ ahriman.application.handlers.remove\_unknown module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.report module
------------------------------------------
.. automodule:: ahriman.application.handlers.report
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.restore module
-------------------------------------------
@ -124,6 +116,14 @@ ahriman.application.handlers.setup module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.shell module
-----------------------------------------
.. automodule:: ahriman.application.handlers.shell
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.sign module
----------------------------------------
@ -148,10 +148,10 @@ ahriman.application.handlers.status\_update module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.sync module
----------------------------------------
ahriman.application.handlers.triggers module
--------------------------------------------
.. automodule:: ahriman.application.handlers.sync
.. automodule:: ahriman.application.handlers.triggers
:members:
:no-undoc-members:
:show-inheritance:
@ -180,6 +180,14 @@ ahriman.application.handlers.users module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.versions module
--------------------------------------------
.. automodule:: ahriman.application.handlers.versions
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.web module
---------------------------------------

View File

@ -20,6 +20,14 @@ ahriman.core.database.migrations.m001\_package\_source module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m002\_user\_access module
----------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m002_user_access
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -76,6 +76,14 @@ ahriman.core.formatters.user\_printer module
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.version\_printer module
-----------------------------------------------
.. automodule:: ahriman.core.formatters.version_printer
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -44,6 +44,14 @@ ahriman.core.report.report module
:no-undoc-members:
:show-inheritance:
ahriman.core.report.report\_trigger module
------------------------------------------
.. automodule:: ahriman.core.report.report_trigger
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.report.telegram module
-----------------------------------

View File

@ -16,6 +16,7 @@ Subpackages
ahriman.core.repository
ahriman.core.sign
ahriman.core.status
ahriman.core.triggers
ahriman.core.upload
Submodules
@ -37,6 +38,14 @@ 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

@ -0,0 +1,29 @@
ahriman.core.triggers package
=============================
Submodules
----------
ahriman.core.triggers.trigger module
------------------------------------
.. automodule:: ahriman.core.triggers.trigger
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.triggers.trigger\_loader module
--------------------------------------------
.. automodule:: ahriman.core.triggers.trigger_loader
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.triggers
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -44,6 +44,14 @@ ahriman.core.upload.upload module
:no-undoc-members:
:show-inheritance:
ahriman.core.upload.upload\_trigger module
------------------------------------------
.. automodule:: ahriman.core.upload.upload_trigger
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -92,6 +92,14 @@ ahriman.models.package\_source module
:no-undoc-members:
:show-inheritance:
ahriman.models.pkgbuild\_patch module
-------------------------------------
.. automodule:: ahriman.models.pkgbuild_patch
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.property module
------------------------------

View File

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

View File

@ -7,7 +7,7 @@ Package structure
Packages have strict rules of importing:
* ``ahriman.application`` package must not be used anywhere except for itself.
* ``ahriman.core`` and ``ahriman.models`` packages don't have any import restriction. Actually we would like to totally restrict importing of ``core`` package from ``models``\ , but it is impossible at the moment.
* ``ahriman.core`` and ``ahriman.models`` packages don't have any import restriction. Actually we would like to totally restrict importing of ``core`` package from ``models``, but it is impossible at the moment.
* ``ahriman.web`` package is allowed to be imported from ``ahriman.application`` (web handler only, only ``ahriman.web.web`` methods). It also must not be imported globally, only local import is allowed.
Full dependency diagram:
@ -19,27 +19,28 @@ Full dependency diagram:
``ahriman.application`` package
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This package contains application (aka executable) related classes and everything for that. It also contains package called ``ahriman.application.handlers`` in which all available subcommands are described as separated classes derived from base ``ahriman.application.handlers.handler.Handler`` class.
This package contains application (aka executable) related classes and everything for that. It also contains package called ``ahriman.application.handlers`` in which all available subcommands are described as separated classes derived from base ``ahriman.application.handlers.Handler`` class.
``ahriman.application.application.application.Application`` (god class) is used for any interaction from parsers with repository, web etc. It is divided into multiple traits by functions (package related and repository related) in the same package.
``ahriman.application.application.Application`` (god class) is used for any interaction from parsers with repository. It is divided into multiple traits by functions (package related and repository related) in the same package.
``ahriman.application.ahriman`` contains only command line parses and executes specified ``Handler`` on success, ``ahriman.application.lock.Lock`` is additional class which provides file-based lock and also performs some common checks.
``ahriman.core`` package
^^^^^^^^^^^^^^^^^^^^^^^^
This package contains everything which is required for any time of application run and separated to several packages:
This package contains everything which is required for any time of application run and 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.Auth`` which must be called by ``load`` method.
* ``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.
* ``ahriman.core.build_tools`` is a package which provides wrapper for ``devtools`` commands.
* ``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.report`` is a package with reporting classes. Usually it must be called by ``ahriman.core.report.report.Report.load`` method.
* ``ahriman.core.repository`` contains several traits and base repository (\ ``ahriman.core.repository.repository.Repository`` class) implementation.
* ``ahriman.core.report`` is a package with reporting classes. Usually it must be called by ``ahriman.core.report.Report.load`` method.
* ``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).
* ``ahriman.core.status`` contains helpers and watcher class which are required for web application. Reporter must be initialized by using ``ahriman.core.status.client.Client.load`` method.
* ``ahriman.core.upload`` package provides sync feature, must be called by ``ahriman.core.upload.upload.Upload.load`` method.
* ``ahriman.core.triggers`` package contains base trigger classes. Classes from this package must be imported in order to implement user extensions. In fact, ``ahriman.core.report`` and ``ahriman.core.upload`` use this package.
* ``ahriman.core.upload`` package provides sync feature, must be called by ``ahriman.core.upload.Upload.load`` method.
This package also provides some generic functions and classes which may be used by other packages:
@ -68,7 +69,7 @@ Application run
* Parse command line arguments, find command and related handler which is set by parser.
* Call ``Handler.execute`` method.
* Define list of architectures to run. In case if there is more than one architecture specified run several subprocesses or process in current process otherwise. Class attribute ``ALLOW_MULTI_ARCHITECTURE_RUN`` controls whether application can be run in multiple processes or not - this feature is required for some handlers (e.g. ``Web``\ ) which should be able to spawn child process in daemon mode (it is impossible to do for daemonic processes).
* Define list of architectures to run. In case if there is more than one architecture specified run several subprocesses or process in current process otherwise. Class attribute ``ALLOW_MULTI_ARCHITECTURE_RUN`` controls whether application can be run in multiple processes or not - this feature is required for some handlers (e.g. ``Web``) which should be able to spawn child process in daemon mode (it is impossible to do from daemonic processes).
* In each child process call lock functions.
* After success checks pass control to ``Handler.run`` method defined by specific handler class.
* Return result (success or failure) of each subprocess and exit from application.
@ -86,14 +87,14 @@ The service uses SQLite database in order to store some internal info.
Database instance
^^^^^^^^^^^^^^^^^
All methods related to specific part of database (basically operations per table) are split into different traits located inside ``ahriman.core.database.operations`` package. The base trait ``ahriman.core.database.operations.operations.Operations`` also provides generic methods for database access (e.g. row converters and transactional support).
All methods related to specific part of database (basically operations per table) are split into different traits located inside ``ahriman.core.database.operations`` package. The base trait ``ahriman.core.database.operations.Operations`` also provides generic methods for database access (e.g. row converters and transactional support).
The ``ahriman.core.database.sqlite.SQLite`` class itself derives from all of these traits and implements methods for initialization, including migrations.
The ``ahriman.core.database.SQLite`` class itself derives from all of these traits and implements methods for initialization, including migrations.
Schema and data migrations
^^^^^^^^^^^^^^^^^^^^^^^^^^
The schema migration are applied according to current ``pragma user_info`` values, located at ``ahriman.core.database.migrations`` package and named as ``m000_migration_name.py`` (the preceding ``m`` is required in order to import migration content for tests). Additional class ``ahriman.core.database.migrations.Migrations`` reads all migrations autmatically and applies them in alphabetical order.
The schema migration are applied according to current ``pragma user_info`` values, located at ``ahriman.core.database.migrations`` package and named as ``m000_migration_name.py`` (the preceding ``m`` is required in order to import migration content for tests). Additional class ``ahriman.core.database.migrations.Migrations`` reads all migrations automatically and applies them in alphabetical order.
There are also data migrations which are located at ``ahriman.core.database.data`` package and move data from old-style (e.g. json files in filesystem, directory trees, etc) to the database. They are also part of migration and (unlike schema migrations) are applied only at specific version breakpoints (e.g. if ``user_version`` is more than 0 no initial migration will be applied).
@ -102,7 +103,7 @@ Type conversions
By default, it parses rows into python dictionary. In addition, the following pseudo-types are supported:
* ``Dict[str, Any]``\ , ``List[Any]`` - for storing JSON data structures in database (technically there is no restriction on types for dictionary keys and values, but it is recommended to use only string keys). The type is stored as ``json`` datatype and ``json.loads`` and ``json.dumps`` methods are used in order to read and write from/to database respectively.
* ``Dict[str, Any]``, ``List[Any]`` - for storing JSON data structures in database (technically there is no restriction on types for dictionary keys and values, but it is recommended to use only string keys). The type is stored as ``json`` datatype and ``json.loads`` and ``json.dumps`` methods are used in order to read and write from/to database respectively.
Basic flows
-----------
@ -113,8 +114,8 @@ Add new packages or rebuild existing
Idea is to copy package to the directory from which it will be handled at the next update run. Different variants are supported:
* If supplied argument is file then application moves the file to the directory with built packages. Same rule applies for directory, but in this case it copies every package-like file from the specified directory.
* If supplied argument is directory and there is ``PKGBUILD`` file there it will be treated as local package. In this case it will queue this package to build and copy source files (\ ``PKGBUILD`` and ``.SRCINFO``\ ) to caches.
* If supplied argument iis not file then application tries to lookup for the specified name in AUR and clones it into the directory with manual updates. This scenario can also handle package dependencies which are missing in repositories.
* If supplied argument is directory and there is ``PKGBUILD`` file there it will be treated as local package. In this case it will queue this package to build and copy source files (``PKGBUILD`` and ``.SRCINFO``) to caches.
* If supplied argument is not file then application tries to lookup for the specified name in AUR and clones it into the directory with manual updates. This scenario can also handle package dependencies which are missing in repositories.
This logic can be overwritten by specifying the ``source`` parameter, which is partially useful if you would like to add package from AUR, but there is local directory cloned from AUR.
@ -142,7 +143,7 @@ This feature is divided into to stages: check AUR for updates and run rebuild fo
#. Build every package in clean chroot.
#. Sign packages if required.
#. Add packages to database and sign database if required.
#. Process sync and report methods.
#. Process triggers.
After any step any package data is being removed.
@ -152,7 +153,14 @@ Core functions reference
Configuration
^^^^^^^^^^^^^
``ahriman.core.configuration.Configuration`` class provides some additional methods (e.g. ``getpath`` and ``getlist``\ ) and also combines multiple files into single configuration dictionary using architecture overrides. It is the recommended way to deal with settings.
``ahriman.core.configuration.Configuration`` class provides some additional methods (e.g. ``getpath`` and ``getlist``) and also combines multiple files into single configuration dictionary using architecture overrides. It is the recommended way to deal with settings.
Enumerations
^^^^^^^^^^^^
All enumerations are derived from ``str`` and ``enum.Enum``. Integer enumerations are not allowed, because most of operations require conversions from string variable. Derivation from string class is required to make json conversions implicitly (e.g. during calling ``json.dumps`` methods).
In addition, some enumerations provide ``from_option`` class methods in order to allow some flexibility while reading configuration options.
Utils
^^^^^
@ -176,7 +184,7 @@ Mapping (aka configuration) provider uses hashed passwords with salt from the da
* ``check_credentials`` - user password validation (authentication).
* ``verify_access`` - user permission validation (authorization).
Passwords must be stored in database as ``hash(password + salt)``\ , where ``password`` is user defined password (taken from user input), ``salt`` is random string (any length) defined globally in configuration and ``hash`` is secure hash function. Thus, the following configuration
Passwords must be stored in database as ``hash(password + salt)``, where ``password`` is user defined password (taken from user input), ``salt`` is random string (any length) defined globally in configuration and ``hash`` is secure hash function. Thus, the following configuration
.. code-block::
@ -185,20 +193,31 @@ Passwords must be stored in database as ``hash(password + salt)``\ , where ``pas
means that there is user ``username`` with ``read`` access and password ``password`` hashed by ``sha512`` with salt ``salt``.
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 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).
In order to configure users there are special commands.
Triggers
^^^^^^^^
Triggers are extensions which can be used in order to perform any actions after the update process. The package provides two default extensions - one is report generation and another one is remote upload feature.
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.
The loaded triggers will be called with ``ahriman.models.result.Result`` and ``List[Packages]`` arguments, which describes the process result and current repository packages respectively. Any exception raised will be suppressed and will generate an exception message in logs.
For more details how to deal with the triggers, refer to :doc:`documentation <triggers>` and modules descriptions.
Remote synchronization
^^^^^^^^^^^^^^^^^^^^^^
There are several supported synchronization providers, currently they are ``rsync``\ , ``s3``\ , ``github``.
There are several supported synchronization providers, currently they are ``rsync``, ``s3``, ``github``.
``rsync`` provider does not have any specific logic except for running external rsync application with configured arguments. The service does not handle SSH configuration, thus it has to be configured before running application manually.
``s3`` provider uses ``boto3`` package and implements sync feature. The files are stored in architecture directory (e.g. if bucket is ``repository``\ , packages will be stored in ``repository/x86_64`` for the ``x86_64`` architecture), bucket must be created before any action and API key must have permissions to write to the bucket. No external configuration required. In order to upload only changed files the service compares calculated hashes with the Amazon ETags, used realization is described `here <https://teppen.io/2018/10/23/aws_s3_verify_etags/>`_.
``s3`` provider uses ``boto3`` package and implements sync feature. The files are stored in architecture directory (e.g. if bucket is ``repository``, packages will be stored in ``repository/x86_64`` for the ``x86_64`` architecture), bucket must be created before any action and API key must have permissions to write to the bucket. No external configuration required. In order to upload only changed files the service compares calculated hashes with the Amazon ETags, used realization is described `here <https://teppen.io/2018/10/23/aws_s3_verify_etags/>`_.
``github`` provider authenticates through basic auth, API key with repository write permissions is required. There will be created a release with the name of the architecture in case if it does not exist; files will be uploaded to the release assets. It also stores array of files and their MD5 checksums in release body in order to upload only changed ones. According to the Github API in case if there is already uploaded asset with the same name (e.g. database files), asset will be removed first.
@ -207,7 +226,7 @@ Additional features
Some features require optional dependencies to be installed:
* Version control executables (e.g. ``git``\ , ``svn``\ ) for VCS packages.
* Version control executables (e.g. ``git``, ``svn``) for VCS packages.
* ``gnupg`` application for package and repository sign feature.
* ``rsync`` application for rsync based repository sync.
* ``boto3`` python package for ``S3`` sync.
@ -220,7 +239,7 @@ Web application requires the following python packages to be installed:
* Core part requires ``aiohttp`` (application itself), ``aiohttp_jinja2`` and ``Jinja2`` (HTML generation from templates).
* 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, authorization feature requires ``aiohttp_security``, ``aiohttp_session`` and ``cryptography``.
* In addition to base authorization dependencies, OAuth2 also requires ``aioauth-client`` library.
Middlewares
@ -251,7 +270,7 @@ Requests and scopes
Service provides optional authorization which can be turned on in settings. In order to control user access there are two levels of authorization - read-only (only GET-like requests) and write (anything) which are provided by each web view directly.
If this feature is configured any request will be prohibited without authentication. In addition, configuration flag ``auth.safe_build_status`` can be used in order to allow seeing main page without authorization.
If this feature is configured any request will be prohibited without authentication. In addition, configuration flag ``auth.allow_read_only`` can be used in order to allow read-only operations - reading index page and packages - without authorization.
For authenticated users it uses encrypted session cookies to store tokens; encryption key is generated each time at the start of the application. It also stores expiration time of the session inside.

View File

@ -1,5 +1,5 @@
Commands help
=============
Commands reference
==================
ahriman
-------

View File

@ -104,5 +104,4 @@ autodoc_member_order = "groupwise"
autodoc_default_options = {
"no-undoc-members": True,
"special-members": "__init__",
}

View File

@ -1,16 +1,16 @@
Configuration
=============
Some groups can be specified for each architecture separately. E.g. if there are ``build`` and ``build:x86_64`` groups it will use the option from ``build:x86_64`` for the ``x86_64`` architecture and ``build`` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
Some groups can be specified for each architecture separately. E.g. if there are ``build`` and ``build:x86_64`` groups it will use an option from ``build:x86_64`` for the ``x86_64`` architecture and ``build`` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
There are two variable types which have been added to default ones, they are paths and lists. List values will be read in the same way as shell does:
* By default, it splits value by spaces excluding empty elements.
* In case if quotation mark (\ ``"`` or ``'``\ ) will be found, any spaces inside will be ignored.
* In case if quotation mark (``"`` or ``'``) will be found, any spaces inside will be ignored.
* In order to use quotation mark inside value it is required to put it to another quotation mark, e.g. ``wor"'"d "with quote"`` will be parsed as ``["wor'd", "with quote"]`` and vice versa.
* Unclosed quotation mark is not allowed and will rise an exception.
Path values, except for casting to ``pathlib.Path`` type, will be also expanded to absolute paths relative to the configuration path. E.g. if path is set to ``ahriman.ini.d/logging.ini`` and root configuration path is ``/etc/ahriman.ini``\ , the value will be expanded to ``/etc/ahriman.ini.d/logging.ini``. In order to disable path expand, use the full path, e.g. ``/etc/ahriman.ini.d/logging.ini``.
Path values, except for casting to ``pathlib.Path`` type, will be also expanded to absolute paths relative to the configuration path. E.g. if path is set to ``ahriman.ini.d/logging.ini`` and root configuration path is ``/etc/ahriman.ini``, the value will be expanded to ``/etc/ahriman.ini.d/logging.ini``. In order to disable path expand, use the full path, e.g. ``/etc/ahriman.ini.d/logging.ini``.
``settings`` group
------------------
@ -35,13 +35,13 @@ libalpm and AUR related configuration.
Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` library to be installed.
* ``target`` - specifies authorization provider, string, optional, default ``disabled``. Allowed values are ``disabled``\ , ``configuration``\ , ``oauth``.
* ``target`` - specifies authorization provider, string, optional, default ``disabled``. Allowed values are ``disabled``, ``configuration``, ``oauth``.
* ``allow_read_only`` - allow requesting status APIs without authorization, boolean, required.
* ``client_id`` - OAuth2 application client ID, string, required in case if ``oauth`` is used.
* ``client_secret`` - OAuth2 application client secret key, string, required in case if ``oauth`` is used.
* ``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.
* ``safe_build_status`` - allow requesting status page without authorization, boolean, required.
* ``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).
Authorized users are stored inside internal database, if any of external provides are used the password field for non-service users must be empty.
@ -56,6 +56,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
* ``ignore_packages`` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional.
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional.
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of mention.
``repository`` group
--------------------
@ -79,7 +80,7 @@ Settings for signing packages or repository. Group name can refer to architectur
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 one of ``email`` of ``email:x86_64`` (the one with architecture has higher priority).
* ``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:
@ -90,14 +91,15 @@ Type will be read from several ways:
``console`` type
^^^^^^^^^^^^^^^^
Section name must be either ``console`` (plus optional architecture name, e.g. ``console:x86_64``\ ) or random name with ``type`` set.
Section name must be either ``console`` (plus optional architecture name, e.g. ``console:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``console`` if exists.
* ``use_utf`` - use utf8 symbols in output if set and ascii otherwise, boolean, optional, default ``yes``.
``email`` type
^^^^^^^^^^^^^^
Section name must be either ``email`` (plus optional architecture name, e.g. ``email:x86_64``\ ) or random name with ``type`` set.
Section name must be either ``email`` (plus optional architecture name, e.g. ``email:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``email`` if exists.
* ``full_template_path`` - path to Jinja2 template for full package description index, string, optional.
@ -109,25 +111,25 @@ Section name must be either ``email`` (plus optional architecture name, e.g. ``e
* ``port`` - SMTP port for sending emails, int, required.
* ``receivers`` - SMTP receiver addresses, space separated list of strings, required.
* ``sender`` - SMTP sender address, string, required.
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``\ , ``starttls``\ , ``disabled``\ , optional, default ``disabled``.
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``, ``starttls``, ``disabled``, optional, default ``disabled``.
* ``template_path`` - path to Jinja2 template, string, required.
* ``user`` - SMTP user to authenticate, string, optional.
``html`` type
^^^^^^^^^^^^^
Section name must be either ``html`` (plus optional architecture name, e.g. ``html:x86_64``\ ) or random name with ``type`` set.
Section name must be either ``html`` (plus optional architecture name, e.g. ``html:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``html`` if exists.
* ``path`` - path to html report file, string, required.
* ``homepage`` - link to homepage, string, optional.
* ``link_path`` - prefix for HTML links, string, required.
* ``path`` - path to html report file, string, required.
* ``template_path`` - path to Jinja2 template, string, required.
``telegram`` type
^^^^^^^^^^^^^^^^^
Section name must be either ``telegram`` (plus optional architecture name, e.g. ``telegram:x86_64``\ ) or random name with ``type`` set.
Section name must be either ``telegram`` (plus optional architecture name, e.g. ``telegram:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``telegram`` if exists.
* ``api_key`` - telegram bot API key, string, required. Please refer FAQ about how to create chat and bot
@ -135,7 +137,8 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
* ``homepage`` - link to homepage, string, optional.
* ``link_path`` - prefix for HTML links, string, required.
* ``template_path`` - path to Jinja2 template, string, required.
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``\ , ``HTML``\ , ``Markdown``\ , string, optional, default ``HTML``.
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
``upload`` group
----------------
@ -153,7 +156,7 @@ Type will be read from several ways:
``github`` type
^^^^^^^^^^^^^^^
This feature requires Github key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``\ ) or random name with ``type`` set.
This feature requires Github key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``) or random name with ``type`` set.
* ``type`` - type of the upload, string, optional, must be set to ``github`` if exists.
* ``owner`` - Github repository owner, string, required.
@ -165,35 +168,36 @@ This feature requires Github key creation (see below). Section name must be eith
#. Generate new token. Required scope is ``public_repo`` (or ``repo`` for private repository support).
* ``repository`` - Github repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme).
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``username`` - Github authorization user, string, required. Basically the same as ``owner``.
``rsync`` type
^^^^^^^^^^^^^^
Requires ``rsync`` package to be installed. Do not forget to configure ssh for user ``ahriman``. Section name must be either ``rsync`` (plus optional architecture name, e.g. ``rsync:x86_64``\ ) or random name with ``type`` set.
Requires ``rsync`` package to be installed. Do not forget to configure ssh for user ``ahriman``. Section name must be either ``rsync`` (plus optional architecture name, e.g. ``rsync:x86_64``) or random name with ``type`` set.
* ``type`` - type of the upload, string, optional, must be set to ``rsync`` if exists.
* ``command`` - rsync command to run, space separated list of string, required.
* ``remote`` - remote server to rsync (e.g. ``1.2.3.4:path/to/sync``\ ), string, required.
* ``remote`` - remote server to rsync (e.g. ``1.2.3.4:path/to/sync``), string, required.
``s3`` type
^^^^^^^^^^^
Requires ``boto3`` library to be installed. Section name must be either ``s3`` (plus optional architecture name, e.g. ``s3:x86_64``\ ) or random name with ``type`` set.
Requires ``boto3`` library to be installed. Section name must be either ``s3`` (plus optional architecture name, e.g. ``s3:x86_64``) or random name with ``type`` set.
* ``type`` - type of the upload, string, optional, must be set to ``github`` if exists.
* ``access_key`` - AWS access key ID, string, required.
* ``bucket`` - bucket name (e.g. ``bucket``\ ), string, required.
* ``bucket`` - bucket name (e.g. ``bucket``), string, required.
* ``chunk_size`` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024.
* ``region`` - bucket region (e.g. ``eu-central-1``\ ), string, required.
* ``region`` - bucket region (e.g. ``eu-central-1``), string, required.
* ``secret_key`` - AWS secret access key, string, required.
``web:*`` groups
----------------
Web server settings. If any of ``host``\ /\ ``port`` is not set, web integration will be disabled. Group name can refer to architecture, e.g. ``web:x86_64`` can be used for x86_64 architecture specific settings. This feature requires ``aiohttp`` libraries to be installed.
Web server settings. If any of ``host``/``port`` is not set, web integration will be disabled. Group name can refer to architecture, e.g. ``web:x86_64`` can be used for x86_64 architecture specific settings. This feature requires ``aiohttp`` libraries to be installed.
* ``address`` - optional address in form ``proto://host:port`` (\ ``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
* ``address`` - optional address in form ``proto://host:port`` (``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
* ``debug`` - enable debug toolbar, boolean, optional, default ``no``.
* ``debug_check_host`` - check hosts to access debug toolbar, boolean, optional, default ``no``.
* ``debug_allowed_hosts`` - allowed hosts to get access to debug toolbar, space separated list of string, optional.

View File

@ -118,7 +118,7 @@ But I just wanted to change PKGBUILD from AUR a bit!
Well it is supported also.
#. Clone sources from AUR.
#. Make changes you would like to (e.g. edit ``PKGBUILD``\ , add external patches).
#. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches).
#. Run ``sudo -u ahriman ahriman patch-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).
@ -130,7 +130,9 @@ So it is the same as adding any other package, but due to restrictions you must
.. code-block:: shell
sudo -u ahriman ahriman package-add pacmann -s repository
sudo -u ahriman ahriman package-add pacman -s repository
This feature is heavily depends on local pacman cache. In order to use this feature it is recommended to either run ``pacman -Sy`` before the interaction or configure timer for this.
Package build fails because it cannot validate PGP signature of source files
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -177,7 +179,7 @@ However, note that you do not need to rebuild repository in case if you just cha
Hmm, I have packages built, but how can I use it?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Add the following lines to your ``pacman.conf``\ :
Add the following lines to your ``pacman.conf``:
.. code-block:: ini
@ -238,7 +240,7 @@ The default action (in case if no arguments provided) is ``repo-update``. Basica
docker run -v /path/to/local/repo:/var/lib/ahriman -v /etc/ahriman.ini:/etc/ahriman.ini.d/10-overrides.ini arcan1s/ahriman:latest
By default, it runs ``repo-update``\ , but it can be overwritten to any other command you would like to, e.g.:
The action can be specified during run, e.g.:
.. code-block:: shell
@ -255,7 +257,7 @@ The following environment variables are supported:
* ``AHRIMAN_DEBUG`` - if set all commands will be logged to console.
* ``AHRIMAN_FORCE_ROOT`` - force run ahriman as root instead of guessing by subcommand.
* ``AHRIMAN_HOST`` - host for the web interface, default is ``0.0.0.0``.
* ``AHRIMAN_OUTPUT`` - controls logging handler, e.g. ``syslog``\ , ``console``. The name must be found in logging configuration. Note that if ``syslog`` (the default) handler is used you will need to mount ``/dev/log`` inside container because it is not available there.
* ``AHRIMAN_OUTPUT`` - controls logging handler, e.g. ``syslog``, ``console``. The name must be found in logging configuration. Note that if ``syslog`` (the default) handler is used you will need to mount ``/dev/log`` inside container because it is not available there.
* ``AHRIMAN_PACKAGER`` - packager name from which packages will be built, default is ``ahriman bot <ahriman@example.com>``.
* ``AHRIMAN_PORT`` - HTTP server port if any, default is empty.
* ``AHRIMAN_REPOSITORY`` - repository name, default is ``aur-clone``.
@ -279,7 +281,7 @@ Well for that you would need to have web container instance running forever; it
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.:
For every next container run use arguments ``-e AHRIMAN_PORT=8080 --net=host``, e.g.:
.. code-block:: shell
@ -294,7 +296,7 @@ Wait I would like to use the repository from another server
There are several choices:
#.
Easy and cheap, just share your local files through the internet, e.g. for ``nginx``\ :
Easy and cheap, just share your local files through the internet, e.g. for ``nginx``:
.. code-block::
@ -316,7 +318,7 @@ There are several choices:
[rsync]
remote = 192.168.0.1:/srv/repo
After that just add ``/srv/repo`` to the ``pacman.conf`` as usual. You can also upload to S3 (e.g. ``Server = https://s3.eu-central-1.amazonaws.com/repository/x86_64``\ ) or to Github (e.g. ``Server = https://github.com/ahriman/repository/releases/download/x86_64``\ ).
After that just add ``/srv/repo`` to the ``pacman.conf`` as usual. You can also upload to S3 (e.g. ``Server = https://s3.eu-central-1.amazonaws.com/repository/x86_64``) or to Github (e.g. ``Server = https://github.com/ahriman/repository/releases/download/x86_64``).
How do I configure S3?
^^^^^^^^^^^^^^^^^^^^^^
@ -474,7 +476,7 @@ I would like to get messages to my telegram account/channel
#. Make your channel public
#.
Get chat id if you want to use by numerical id or just use id prefixed with ``@`` (e.g. ``@ahriman``\ ). If you are not using chat the chat id is your user id. If you don't want to make channel public you can use `this guide <https://stackoverflow.com/a/33862907>`_.
Get chat id if you want to use by numerical id or just use id prefixed with ``@`` (e.g. ``@ahriman``). If you are not using chat the chat id is your user id. If you don't want to make channel public you can use `this guide <https://stackoverflow.com/a/33862907>`_.
#.
Configure the service:
@ -489,7 +491,7 @@ I would like to get messages to my telegram account/channel
chat_id = @ahriman
link_path = http://example.com/x86_64
``api_key`` is the one sent by `@BotFather <https://t.me/botfather>`_\ , ``chat_id`` is the value retrieved from previous step.
``api_key`` is the one sent by `@BotFather <https://t.me/botfather>`_, ``chat_id`` is the value retrieved from previous step.
If you did everything fine you should receive the message with the next update. Quick credentials check can be done by using the following command:
@ -606,7 +608,7 @@ The service provides several commands aim to do easy repository backup and resto
sudo ahriman repo-backup /tmp/repo.tar.gz
This command will pack all configuration files together with database file into the archive specified as command line argument (i.e. ``/tmp/repo.tar.gz``\ ). In addition it will also archive ``cache`` directory (the one which contains local clones used by e.g. local packages) and ``.gnupg`` of the ``ahriman`` user.
This command will pack all configuration files together with database file into the archive specified as command line argument (i.e. ``/tmp/repo.tar.gz``). In addition it will also archive ``cache`` directory (the one which contains local clones used by e.g. local packages) and ``.gnupg`` of the ``ahriman`` user.
#.
Copy created archive from source server ``server1.example.com`` to target ``server2.example.com``.
@ -621,7 +623,7 @@ The service provides several commands aim to do easy repository backup and resto
sudo ahriman repo-restore /tmp/repo.tar.gz
An additional argument ``-o``\ /\ ``--output`` can be used to specify extraction root (\ ``/`` by default).
An additional argument ``-o``/``--output`` can be used to specify extraction root (``/`` by default).
#.
Rebuild repository:
@ -684,13 +686,13 @@ I would like to check service logs
By default, the service writes logs to ``/dev/log`` which can be accessed by using ``journalctl`` command (logs are written to the journal of the user under which command is run).
You can also edit configuration and forward logs to ``stderr``\ , just change ``handlers`` value, e.g.:
You can also edit configuration and forward logs to ``stderr``, just change ``handlers`` value, e.g.:
.. code-block:: shell
sed -i 's/handlers = syslog_handler/handlers = console_handler/g' /etc/ahriman.ini.d/logging.ini
You can even configure logging as you wish, but kindly refer to python ``logging`` module `configuration <https://docs.python.org/3/library/logging.config.html>`_.
You can even configure logging as you wish, but kindly refer to python ``logging`` module `configuration <https://docs.python.org/3/library/logging.config.html>`_. The application uses java concept to log messages, e.g. class ``Application`` imported from ``ahriman.application.application`` package will have logger called ``ahriman.application.application.Application``. In order to e.g. change logger name for whole application package it is possible to change values for ``ahriman.application`` package; thus editing ``ahriman`` logger configuration will change logging for whole application (unless there are overrides for another logger).
Html customization
^^^^^^^^^^^^^^^^^^

View File

@ -6,13 +6,14 @@ Wrapper for managing custom repository inspired by `repo-scripts <https://github
Features
--------
* Install-configure-forget manager for own repository.
* Install-configure-forget manager for the very own repository.
* Multi-architecture support.
* VCS packages support.
* Sign support with gpg (repository, package, per package settings).
* Synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Dependency manager.
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Sign support with gpg (repository, package, per package settings).
* 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.
Contents
@ -27,11 +28,5 @@ Contents
faq
architecture
advanced-usage
triggers
modules
Indices and tables
------------------
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -6,7 +6,7 @@ Initial setup
#.
Change settings if required, see :doc:`configuration reference <configuration>` for more details.
#.
TL;DR
TL;DR:
.. code-block:: shell
@ -15,7 +15,7 @@ Initial setup
``repo-setup`` literally does the following steps:
#.
Create ``/var/lib/ahriman/.makepkg.conf`` with ``makepkg.conf`` overrides if required (at least you might want to set ``PACKAGER``\ ):
Create ``/var/lib/ahriman/.makepkg.conf`` with ``makepkg.conf`` overrides if required (at least you might want to set ``PACKAGER``):
.. code-block:: shell
@ -25,21 +25,24 @@ Initial setup
Configure build tools (it is required for correct dependency management system):
#.
Create build command, e.g. ``ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build`` (you can choose any name for command, basically it should be ``{name}-{arch}-build``\ ).
#.
Create configuration file, e.g. ``cp /usr/share/devtools/pacman-{extra,ahriman}.conf`` (same as previous ``pacman-{name}.conf``\ ).
#.
Change configuration file, add your own repository, add multilib repository etc;
#.
Set ``build_command`` option to point to your command.
#.
Configure ``/etc/sudoers.d/ahriman`` to allow running command without a password.
Create build command (you can choose any name for command, basically it should be ``{name}-{arch}-build``):
.. code-block:: shell
ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build
#.
Create configuration file (same as previous ``pacman-{name}.conf``):
.. code-block:: shell
cp /usr/share/devtools/pacman-{extra,ahriman}.conf
#.
Change configuration file, add your own repository, add multilib repository etc:
.. code-block:: shell
echo '[multilib]' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman-ahriman.conf
@ -47,15 +50,25 @@ Initial setup
echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo 'Server = file:///var/lib/ahriman/repository/$arch' | tee -a /usr/share/devtools/pacman-ahriman.conf
#.
Set ``build_command`` option to point to your command:
.. code-block:: shell
echo '[build]' | tee -a /etc/ahriman.ini.d/build.ini
echo 'build_command = ahriman-x86_64-build' | tee -a /etc/ahriman.ini.d/build.ini
#.
Configure ``/etc/sudoers.d/ahriman`` to allow running command without a password:
.. code-block:: shell
echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman
#.
Start and enable ``ahriman@.timer`` via ``systemctl``\ :
Start and enable ``ahriman@.timer`` via ``systemctl``:
.. code-block:: shell

60
docs/triggers.rst Normal file
View File

@ -0,0 +1,60 @@
Triggers
========
The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides two types of extensions - reporting and files uploading. Each extension must derive from the ``ahriman.core.triggers.Trigger`` class and implement ``run`` method
Trigger example
---------------
Lets consider example of reporting trigger (e.g. `slack <https://slack.com/>`_, which provides easy HTTP API for integration triggers).
In order to post message to slack we will need a specific trigger url (something like ``https://hooks.slack.com/services/company_id/trigger_id``), channel (e.g. ``#archrepo``) and username (``repo-bot``).
As it has been mentioned, our trigger must derive from specific class:
.. code-block:: python
from ahriman.core.triggers import Trigger
class SlackReporter(Trigger):
def __init__(self, architecture, configuration):
Trigger.__init__(self, architecture, configuration)
self.slack_url = configuration.get("slack", "url")
self.channel = configuration.get("slack", "channel")
self.username = configuration.get("slack", "username")
By now we have class with all required variables. Lets implement run method. Slack API requires positing data with specific payload by HTTP, thus:
.. code-block:: python
import json
import requests
def notify(result, slack_url, channel, username):
text = f"""Build has been completed with packages: {", ".join([package.name for package in result.success])}"""
payload = {"channel": channel, "username": username, "text": text}
response = requests.post(slack_url, data={"payload": json.dumps(payload)})
response.raise_for_status()
Obviously you can implement the specified method in class, but for guide purpose it has been done as separated method. Now we can merge this method into the class:
.. code-block:: python
class SlackReporter(Trigger):
def __init__(self, architecture, configuration):
Trigger.__init__(self, architecture, configuration)
self.slack_url = configuration.get("slack", "url")
self.channel = configuration.get("slack", "channel")
self.username = configuration.get("slack", "username")
def run(self, result, packages):
notify(result, self.slack_url, self.channel, self.username)
Setup the trigger
-----------------
First, put the trigger in any path it can be exported, e.g. by packing the resource into python package (which will lead to import path as ``package.slack_reporter.SlackReporter``) or just put file somewhere it can be accessed by application (e.g. ``/usr/local/lib/slack_reporter.py.SlackReporter``.
After that run application as usual and receive notification in your slack channel.

View File

@ -1,13 +1,13 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=2.0.0rc8
pkgver=2.2.1
pkgrel=1
pkgdesc="ArcH Linux ReposItory MANager"
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')
url="https://github.com/arcan1s/ahriman"
license=('GPL3')
depends=('devtools' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo')
depends=('devtools' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-requests' 'python-setuptools' 'python-srcinfo')
makedepends=('python-build' 'python-installer' 'python-wheel')
optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support'

View File

@ -1 +1 @@
u ahriman 643 "ArcH Linux ReposItory MANager" /var/lib/ahriman
u ahriman 643 "ArcH linux ReposItory MANager" /var/lib/ahriman

View File

@ -1,5 +1,5 @@
[Unit]
Description=ArcH Linux ReposItory MANager web server (%I architecture)
Description=ArcH linux ReposItory MANager web server (%I architecture)
After=network.target
[Service]

View File

@ -1,5 +1,5 @@
[Unit]
Description=ArcH Linux ReposItory MANager (%I architecture)
Description=ArcH linux ReposItory MANager (%I architecture)
[Service]
ExecStart=/usr/bin/ahriman --architecture %i update

View File

@ -1,5 +1,5 @@
[Unit]
Description=ArcH Linux ReposItory MANager timer (%I architecture)
Description=ArcH linux ReposItory MANager timer (%I architecture)
[Timer]
OnCalendar=daily

View File

@ -13,14 +13,15 @@ target = disabled
max_age = 604800
oauth_provider = GoogleClient
oauth_scopes = https://www.googleapis.com/auth/userinfo.email
safe_build_status = yes
allow_read_only = yes
[build]
archbuild_flags =
build_command = extra-x86_64-build
ignore_packages =
makechrootpkg_flags =
makepkg_flags = --nocolor
makepkg_flags = --nocolor --ignorearch
triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger
[repository]
name = aur-clone
@ -36,7 +37,6 @@ target = console
use_utf = yes
[email]
full_template_path = /usr/share/ahriman/templates/repo-index.jinja2
no_empty_report = yes
template_path = /usr/share/ahriman/templates/email-index.jinja2
ssl = disabled

View File

@ -1,5 +1,5 @@
[loggers]
keys = root,build_details,database,http,stderr,boto3,botocore,nose,s3transfer
keys = root,http,stderr,boto3,botocore,nose,s3transfer
[handlers]
keys = console_handler,syslog_handler
@ -20,11 +20,11 @@ formatter = syslog_format
args = ("/dev/log",)
[formatter_generic_format]
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s
format = [%(levelname)s %(asctime)s] [%(threadName)s] [%(name)s]: %(message)s
datefmt =
[formatter_syslog_format]
format = [%(levelname)s] [%(name)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
format = [%(levelname)s] [%(threadName)s] [%(name)s]: %(message)s
datefmt =
[logger_root]
@ -32,18 +32,6 @@ level = DEBUG
handlers = syslog_handler
qualname = root
[logger_build_details]
level = DEBUG
handlers = syslog_handler
qualname = build_details
propagate = 0
[logger_database]
level = DEBUG
handlers = syslog_handler
qualname = database
propagate = 0
[logger_http]
level = DEBUG
handlers = syslog_handler

View File

@ -14,28 +14,29 @@
<div class="container">
<h1>ahriman
{% if auth.authenticated %}
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
<img src="https://img.shields.io/badge/repository-{{ repository | replace("-", "--") }}-informational" alt="{{ repository }}">
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
{% endif %}
<img id="badge-version" src="https://img.shields.io/badge/version-unknown-informational" alt="unknown">
<img id="badge-repository" src="https://img.shields.io/badge/repository-unknown-informational" alt="unknown">
<img id="badge-architecture" src="https://img.shields.io/badge/architecture-unknown-informational" alt="unknown">
<img id="badge-status" src="https://img.shields.io/badge/service%20status-unknown-inactive" alt="unknown">
</h1>
</div>
<div class="container">
<div id="toolbar">
{% if not auth.enabled or auth.username is not none %}
<button id="add" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addForm">
<i class="fa fa-plus"></i> add
<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>
<button id="update" class="btn btn-secondary" onclick="updatePackages()" disabled>
<i class="fa fa-play"></i> update
<button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" disabled hidden>
<i class="bi bi-play"></i> update
</button>
<button id="remove" class="btn btn-danger" onclick="removePackages()" disabled>
<i class="fa fa-trash"></i> remove
<button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
<i class="bi bi-trash"></i> remove
</button>
{% endif %}
<button class="btn btn-secondary" onclick="reload()">
<i class="bi bi-arrow-clockwise"></i> reload
</button>
</div>
<table id="packages" class="table table-striped table-hover"
@ -53,42 +54,22 @@
data-show-fullscreen="true"
data-show-search-clear-button="true"
data-sortable="true"
data-sort-reset="true"
data-sort-name="base"
data-sort-order="asc"
data-toggle="table"
data-toolbar="#toolbar">
<thead class="table-primary">
<tr>
<th data-checkbox="true"></th>
<th data-sortable="true" data-switchable="false">package base</th>
<th data-sortable="true">version</th>
<th data-sortable="true">packages</th>
<th data-sortable="true" data-visible="false">groups</th>
<th data-sortable="true" data-visible="false">licenses</th>
<th data-sortable="true">last update</th>
<th data-sortable="true">status</th>
<th data-sortable="true" data-switchable="false" data-field="base">package base</th>
<th data-sortable="true" data-field="version">version</th>
<th data-sortable="true" data-field="packages">packages</th>
<th data-sortable="true" data-visible="false" data-field="groups">groups</th>
<th data-sortable="true" data-visible="false" data-field="licenses">licenses</th>
<th data-sortable="true" data-field="timestamp">last update</th>
<th data-sortable="true" data-cell-style="statusFormat" data-field="status">status</th>
</tr>
</thead>
<tbody>
{% if auth.authenticated %}
{% for package in packages %}
<tr data-package-base="{{ package.base }}">
<td data-checkbox="true"></td>
<td>{% if package.web_url is not none %}<a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a>{% else %}{{ package.base }}{% endif %}</td>
<td>{{ package.version }}</td>
<td>{{ package.packages|join("<br>"|safe) }}</td>
<td>{{ package.groups|join("<br>"|safe) }}</td>
<td>{{ package.licenses|join("<br>"|safe) }}</td>
<td>{{ package.timestamp }}</td>
<td class="table-{{ package.status_color }}">{{ package.status }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="100%">In order to see statuses you must login first.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
@ -110,7 +91,7 @@
{% if auth.username is none %}
{{ auth.control|safe }}
{% else %}
<form action="/user-api/v1/logout" method="post">
<form action="/api/v1/logout" method="post">
<button class="btn btn-link" style="text-decoration: none">logout ({{ auth.username }})</button>
</form>
{% endif %}
@ -122,11 +103,14 @@
{% include "build-status/login-modal.jinja2" %}
{% endif %}
{% include "build-status/package-actions-modals.jinja2" %}
{% include "utils/bootstrap-scripts.jinja2" %}
{% include "build-status/package-actions-script.jinja2" %}
{% include "build-status/failed-modal.jinja2" %}
{% include "build-status/success-modal.jinja2" %}
{% include "build-status/package-add-modal.jinja2" %}
{% include "build-status/table.jinja2" %}
</body>

View File

@ -0,0 +1,28 @@
<div id="failed-form" 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>
<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>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
</div>
</div>
</div>
</div>
<script>
const failedForm = $("#failed-form");
const errorDetails = $("#error-details");
failedForm.on("hidden.bs.modal", () => { reload(); });
function showFailure(details) {
errorDetails.text(details);
failedForm.modal("show");
}
</script>

View File

@ -1,7 +1,7 @@
<div id="loginForm" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="/user-api/v1/login" method="post">
<form action="/api/v1/login" method="post">
<div class="modal-header">
<h4 class="modal-title">login</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
@ -26,4 +26,4 @@
</form>
</div>
</div>
</div>
</div>

View File

@ -1,60 +0,0 @@
<div id="addForm" 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" type="text" list="knownPackages" class="form-control" placeholder="AUR package" name="package" required>
<datalist id="knownPackages"></datalist>
</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>
</div>
</div>
<div id="failedForm" 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>
<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="errorDetails"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
</div>
</div>
</div>
</div>
<div id="successForm" 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>
<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>
<ul id="successDetails"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
</div>
</div>
</div>
</div>

View File

@ -1,95 +0,0 @@
<script>
const $remove = $("#remove");
const $update = $("#update");
const $table = $("#packages");
$table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
function () {
$remove.prop("disabled", !$table.bootstrapTable("getSelections").length);
$update.prop("disabled", !$table.bootstrapTable("getSelections").length);
})
const $successForm = $("#successForm");
const $successDetails = $("#successDetails");
$successForm.on("hidden.bs.modal", function() { window.location.reload(); });
const $failedForm = $("#failedForm");
const $errorDetails = $("#errorDetails");
$failedForm.on("hidden.bs.modal", function() { window.location.reload(); });
const $package = $("#package");
const $knownPackages = $("#knownPackages");
$package.keyup(function () {
const $this = $(this);
clearTimeout($this.data("timeout"));
$this.data("timeout", setTimeout($.proxy(function () {
const $value = $package.val();
$.ajax({
url: "/service-api/v1/search",
data: {"for": $value},
type: "GET",
dataType: "json",
success: function (resp) {
const $options = resp.map(function (pkg) {
const $option = document.createElement("option");
$option.value = pkg.package;
$option.innerText = `${pkg.package} (${pkg.description})`;
return $option;
});
$knownPackages.empty().append($options);
$this.focus();
},
})
}, this), 500));
})
function doPackageAction($uri, $packages) {
if ($packages.length === 0)
return;
$.ajax({
url: $uri,
data: JSON.stringify({packages: $packages}),
type: "POST",
contentType: "application/json",
success: function (_) {
const $details = $packages.map(function (pkg) {
const $li = document.createElement("li");
$li.innerText = pkg;
return $li;
});
$successDetails.empty().append($details);
$successForm.modal("show");
},
error: function (jqXHR, textStatus, errorThrown) {
$errorDetails.text(errorThrown);
$failedForm.modal("show");
},
})
}
function getSelection() {
return $.map($table.bootstrapTable("getSelections"), function(row) {
return row._data["package-base"];
})
}
function addPackages() {
const $packages = [$package.val()]
doPackageAction("/service-api/v1/add", $packages);
}
function requestPackages() {
const $packages = [$package.val()]
doPackageAction("/service-api/v1/request", $packages);
}
function removePackages() { doPackageAction("/service-api/v1/remove", getSelection()); }
function updatePackages() { doPackageAction("/service-api/v1/add", getSelection()); }
$(function () {
$table.bootstrapTable("uncheckAll");
})
</script>

View File

@ -0,0 +1,61 @@
<div id="add-form" 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>
</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>
</div>
</div>
<script>
const packageInput = $("#package-form");
const knownPackages = $("#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);
},
})
}, this), 500));
});
function addPackages() {
const packages = [packageInput.val()];
doPackageAction("/api/v1/service/add", packages);
}
function requestPackages() {
const packages = [packageInput.val()];
doPackageAction("/api/v1/service/request", packages);
}
</script>

View File

@ -0,0 +1,28 @@
<div id="success-form" 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>
<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>
<ul id="success-details"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
</div>
</div>
</div>
</div>
<script>
const successForm = $("#success-form");
const successDetails = $("#success-details");
successForm.on("hidden.bs.modal", () => { reload(); });
function showSuccess(details) {
successDetails.empty().append(details);
successForm.modal("show");
}
</script>

View File

@ -0,0 +1,160 @@
<script>
const addButton = $("#add-btn");
const removeButton = $("#remove-btn");
const updateButton = $("#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);
updateButton.prop("disabled", !table.bootstrapTable("getSelections").length);
});
const architectureBadge = $("#badge-architecture");
const repositoryBadge = $("#badge-repository");
const statusBadge = $("#badge-status");
const versionBadge = $("#badge-version");
function doPackageAction(uri, packages) {
if (packages.length === 0)
return;
$.ajax({
url: uri,
data: JSON.stringify({packages: packages}),
type: "POST",
contentType: "application/json",
success: _ => {
const details = packages.map(pkg => {
const li = document.createElement("li");
li.innerText = pkg;
return li;
});
showSuccess(details);
},
error: (jqXHR, _, errorThrown) => { showFailure(errorThrown); },
});
}
function getSelection() {
return table.bootstrapTable("getSelections").map(row => { return row.id; });
}
function removePackages() { doPackageAction("/api/v1/service/remove", getSelection()); }
function updatePackages() { doPackageAction("/api/v1/service/add", getSelection()); }
function hideControls(hidden) {
addButton.attr("hidden", hidden);
removeButton.attr("hidden", hidden);
updateButton.attr("hidden", hidden);
}
function reload() {
table.bootstrapTable("showLoading");
$.ajax({
url: "/api/v1/packages",
type: "GET",
dataType: "json",
success: response => {
const extractListProperties = (description, property) => {
return Object.values(description.packages)
.map(pkg => { return pkg[property]; })
.reduce((left, right) => { return left.concat(right); }, []);
};
const listToTable = data => {
return Array.from(new Set(data))
.sort()
.map(entry => { return safe(entry); })
.join("<br>");
};
const payload = response.map(description => {
const package_base = description.package.base;
const web_url = description.package.remote?.web_url;
return {
id: package_base,
base: web_url ? `<a href="${safe(web_url)}" title="${safe(package_base)}">${safe(package_base)}</a>` : safe(package_base),
version: safe(description.package.version),
packages: listToTable(Object.keys(description.package.packages)),
groups: listToTable(extractListProperties(description.package, "groups")),
licenses: listToTable(extractListProperties(description.package, "licenses")),
timestamp: new Date(1000 * description.status.timestamp).toISOString(),
status: description.status.status,
};
});
table.bootstrapTable("load", payload);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
hideControls(false);
},
error: (jqXHR, _, errorThrown) => {
if ((jqXHR.status === 401) || (jqXHR.status === 403)) {
// authorization error
const text = "In order to see statuses you must login first.";
table.find("tr.unauthorized").remove();
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
table.bootstrapTable("hideLoading");
} else {
// other errors
showFailure(errorThrown);
}
hideControls(true);
},
});
$.ajax({
url: "/api/v1/status",
type: "GET",
dataType: "json",
success: response => {
const badgeColor = status => {
if (status === "pending") return "yellow";
if (status === "building") return "yellow";
if (status === "failed") return "critical";
if (status === "success") return "success";
return "inactive";
};
architectureBadge
.attr("src", `https://img.shields.io/badge/architecture-${response.architecture}-informational`)
.attr("alt", response.architecture);
repositoryBadge
.attr("src", `https://img.shields.io/badge/repository-${response.repository.replace(/-/g, "--")}-informational`)
.attr("alt", response.repository);
statusBadge
.attr("src", `https://img.shields.io/badge/service%20status-${response.status.status}-${badgeColor(response.status.status)}`)
.attr("alt", response.status.status)
.attr("title", `at ${new Date(1000 * response.status.timestamp).toISOString()}`);
versionBadge
.attr("src", `https://img.shields.io/badge/version-${response.version}-informational`)
.attr("alt", response.version);
},
});
}
function safe(string) {
return String(string)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function statusFormat(value) {
const cellClass = status => {
if (status === "pending") return "table-warning";
if (status === "building") return "table-warning";
if (status === "failed") return "table-danger";
if (status === "success") return "table-success";
return "table-secondary";
};
return {classes: cellClass(value)};
}
$(() => {
table.bootstrapTable({});
reload();
});
</script>

View File

@ -11,7 +11,7 @@
<body>
<div class="container">
<h1>Arch Linux user repository</h1>
<h1>Arch linux user repository</h1>
</div>
<div class="container">
@ -40,7 +40,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
data-show-fullscreen="true"
data-show-search-clear-button="true"
data-sortable="true"
data-sort-reset="true"
data-sort-name="base"
data-sort-order="asc"
data-toggle="table">
<thead class="table-primary">
<tr>

View File

@ -0,0 +1,18 @@

▄▄▄ ▄▄▄▄▄▄█▀ 
▄▄▄▄▄▄▄▄▄██▄▄▄█▄▄ 
██▄▄███▄▄▄▄▄██▄▄█▄▄ 
█▄██████▄▄▄████▄▄█▄▄ ▄
█▄▄▄█████████▄▄▄▄▀▄█▄█▀
█▄▀▄████▄█▄▄▄▄███▄▄ ▀▀ 
█▄▄▄████▄██████████ 
▄▄▄▄▄▄▄▄▄ ▀█▄█████▄▄▄▄█▄███▄ 
▄▄███▄▄▄▄▄▄▄▄▄ ▀ ▀▄█████▄▄█▄███ 
███▄▄████▄▄█▄██▄▄▄ ███▄▄▄▄▄▄█▀▀ 
███████▄▀ ▄▄▄██▄▄▄█████ 
██▄▄████ █▄█▄▄█████████ 
▄▄█▄▄██▄▀ ▀▄████▄██▄██▄▄ 
████████ ▄███▄▄▄█▄████ 
▄▄▄▄███▄▀ ▄▄█████ ███▄▄▄▄ 
▄▄▄██▄▄█▄▄▄▀ █████▄█ █████▄█ 
▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ 

View File

@ -4,20 +4,9 @@
<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.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.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://unpkg.com/bootstrap-table@1.18.3/dist/extensions/export/bootstrap-table-export.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.18.3/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script>
$("#packages").bootstrapTable({
formatClearSearch: function () {
return "Clear search";
},
formatSearch: function () {
return "search";
}
})
</script>
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>

View File

@ -1,7 +1,7 @@
<script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
<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 href="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/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">

View File

@ -1,5 +1,5 @@
from pathlib import Path
from setuptools import setup, find_packages
from setuptools import find_packages, setup
from typing import Any, Dict
@ -15,7 +15,7 @@ setup(
version=metadata["__version__"],
zip_safe=False,
description="ArcH Linux ReposItory MANager",
description="ArcH linux ReposItory MANager",
author="ahriman team",
author_email="",
@ -32,6 +32,7 @@ setup(
"inflection",
"passlib",
"requests",
"setuptools",
"srcinfo",
],
setup_requires=[
@ -66,12 +67,15 @@ setup(
"package/share/ahriman/templates/build-status.jinja2",
"package/share/ahriman/templates/email-index.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/login-modal.jinja2",
"package/share/ahriman/templates/build-status/package-actions-modals.jinja2",
"package/share/ahriman/templates/build-status/package-actions-script.jinja2",
"package/share/ahriman/templates/build-status/package-add-modal.jinja2",
"package/share/ahriman/templates/build-status/success-modal.jinja2",
"package/share/ahriman/templates/build-status/table.jinja2",
]),
("share/ahriman/templates/static", [
"package/share/ahriman/templates/static/favicon.ico",

View File

@ -26,6 +26,7 @@ from typing import List, TypeVar
from ahriman import version
from ahriman.application import handlers
from ahriman.core.util import enum_values
from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package_source import PackageSource
@ -63,7 +64,7 @@ def _parser() -> argparse.ArgumentParser:
Returns:
argparse.ArgumentParser: command line parser for the application
"""
parser = argparse.ArgumentParser(prog="ahriman", description="ArcH Linux ReposItory MANager",
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)",
@ -76,7 +77,7 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
action="store_true")
parser.add_argument("-v", "--version", action="version", version=version.__version__)
parser.add_argument("-V", "--version", action="version", version=version.__version__)
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
@ -104,10 +105,13 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_sign_parser(subparsers)
_set_repo_status_update_parser(subparsers)
_set_repo_sync_parser(subparsers)
_set_repo_triggers_parser(subparsers)
_set_repo_update_parser(subparsers)
_set_shell_parser(subparsers)
_set_user_add_parser(subparsers)
_set_user_list_parser(subparsers)
_set_user_remove_parser(subparsers)
_set_version_parser(subparsers)
_set_web_parser(subparsers)
return parser
@ -224,7 +228,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=PackageSource, default=PackageSource.Auto)
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add)
return parser
@ -266,7 +270,7 @@ def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status",
type=BuildStatusEnum, choices=BuildStatusEnum)
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, quiet=True, unsafe=True)
return parser
@ -308,7 +312,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen
"If no packages supplied, service status will be updated",
nargs="*")
parser.add_argument("-s", "--status", help="new status",
type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success)
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, quiet=True,
unsafe=True)
return parser
@ -510,8 +514,7 @@ def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="generate repository report according to current settings",
epilog="Create and/or update repository report as configured.",
formatter_class=_formatter)
parser.add_argument("target", help="target to generate report", nargs="*")
parser.set_defaults(handler=handlers.Report)
parser.set_defaults(handler=handlers.Triggers, trigger=["ahriman.core.report.ReportTrigger"])
return parser
@ -556,7 +559,7 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--repository", help="repository name", required=True)
parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", action="append",
type=SignSettings.from_option, choices=SignSettings)
type=SignSettings.from_option, choices=enum_values(SignSettings))
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, quiet=True, unsafe=True)
return parser
@ -594,7 +597,7 @@ def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentPa
parser = root.add_parser("repo-status-update", help="update repository status",
description="update repository status on the status page", formatter_class=_formatter)
parser.add_argument("-s", "--status", help="new status",
type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success)
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, package=[],
quiet=True, unsafe=True)
return parser
@ -614,8 +617,26 @@ def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="sync repository files to remote server according to current settings",
epilog="Synchronize the repository to remote services as configured.",
formatter_class=_formatter)
parser.add_argument("target", help="target to sync", nargs="*")
parser.set_defaults(handler=handlers.Sync)
parser.set_defaults(handler=handlers.Triggers, trigger=["ahriman.core.upload.UploadTrigger"])
return parser
def _set_repo_triggers_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository triggers subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-triggers", help="run triggers",
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="*")
parser.set_defaults(handler=handlers.Triggers)
return parser
@ -643,6 +664,24 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_shell_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for shell subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("shell", help="envoke python shell",
description="drop into python shell while having created application",
formatter_class=_formatter)
parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true")
parser.set_defaults(handler=handlers.Shell, lock=None, no_report=True)
return parser
def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for create user subcommand
@ -662,7 +701,7 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
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",
type=UserAccess, choices=UserAccess, default=UserAccess.Read)
type=UserAccess, choices=enum_values(UserAccess), default=UserAccess.Read)
parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true")
parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture=[""], lock=None, no_report=True,
quiet=True, unsafe=True)
@ -684,7 +723,7 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=_formatter)
parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=UserAccess)
parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess))
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, no_report=True, # nosec
password="", quiet=True, unsafe=True)
return parser
@ -710,6 +749,23 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_version_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for version subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("version", help="application version",
description="print application and its dependencies versions", formatter_class=_formatter)
parser.set_defaults(handler=handlers.Versions, architecture=[""], lock=None, no_report=True, quiet=True,
unsafe=True)
return parser
def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for web subcommand

View File

@ -56,8 +56,7 @@ class Application(ApplicationPackages, ApplicationRepository):
Args:
result(Result): build result
"""
self.report([], result)
self.sync([], result.success)
self.repository.process_triggers(result)
def _known_packages(self) -> Set[str]:
"""

View File

@ -21,11 +21,12 @@ import requests
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any, Iterable, Set
from ahriman.application.application.application_properties import ApplicationProperties
from ahriman.core.build_tools.sources import Sources
from ahriman.core.util import package_like, tmpdir
from ahriman.core.util import package_like
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result
@ -85,9 +86,10 @@ class ApplicationPackages(ApplicationProperties):
self.database.build_queue_insert(package)
self.database.remote_update(package)
with tmpdir() as local_path:
Sources.load(local_path, package.remote, self.database.patches_get(package.base))
self._process_dependencies(local_path, known_packages, without_dependencies)
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, \
(local_dir := Path(dir_name)): # pylint: disable=confusing-with-statement
Sources.load(local_dir, package, self.database.patches_get(package.base), self.repository.paths)
self._process_dependencies(local_dir, known_packages, without_dependencies)
def _add_directory(self, source: str, *_: Any) -> None:
"""
@ -96,8 +98,8 @@ class ApplicationPackages(ApplicationProperties):
Args:
source(str): path to local directory
"""
local_path = Path(source)
for full_path in filter(package_like, local_path.iterdir()):
local_dir = Path(source)
for full_path in filter(package_like, local_dir.iterdir()):
self._add_archive(str(full_path))
def _add_local(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None:
@ -127,7 +129,7 @@ class ApplicationPackages(ApplicationProperties):
source(str): remote URL of the package archive
"""
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
response = requests.get(source, stream=True)
response = requests.get(source, stream=True, timeout=None) # timeout=None to suppress pylint warns
response.raise_for_status()
with dst.open("wb") as local_file:
@ -146,19 +148,19 @@ class ApplicationPackages(ApplicationProperties):
self.database.remote_update(package)
# repository packages must not depend on unknown packages, thus we are not going to process dependencies
def _process_dependencies(self, local_path: Path, known_packages: Set[str], without_dependencies: bool) -> None:
def _process_dependencies(self, local_dir: Path, known_packages: Set[str], without_dependencies: bool) -> None:
"""
process package dependencies
Args:
local_path(Path): path to local package sources (i.e. cloned AUR repository)
local_dir(Path): path to local package sources (i.e. cloned AUR repository)
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
"""
if without_dependencies:
return
dependencies = Package.dependencies(local_path)
dependencies = Package.dependencies(local_dir)
self.add(dependencies.difference(known_packages), PackageSource.AUR, without_dependencies)
def add(self, names: Iterable[str], source: PackageSource, without_dependencies: bool) -> None:

View File

@ -17,14 +17,13 @@
# 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 ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.repository import Repository
class ApplicationProperties:
class ApplicationProperties(LazyLogging):
"""
application base properties class
@ -32,7 +31,6 @@ class ApplicationProperties:
architecture(str): repository architecture
configuration(Configuration): configuration instance
database(SQLite): database instance
logger(logging.Logger): application logger
repository(Repository): repository instance
"""
@ -44,9 +42,8 @@ class ApplicationProperties:
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
unsafe(bool): if set no user check will be performed before path creation
"""
self.logger = logging.getLogger("root")
self.configuration = configuration
self.architecture = architecture
self.database = SQLite.load(configuration)

View File

@ -66,17 +66,6 @@ class ApplicationRepository(ApplicationProperties):
if packages:
self.repository.clear_packages()
def report(self, target: Iterable[str], result: Result) -> None:
"""
generate report
Args:
target(Iterable[str]): list of targets to run (e.g. html)
result(Result): build result
"""
targets = target or None
self.repository.process_report(targets, result)
def sign(self, packages: Iterable[str]) -> None:
"""
sign packages and repository
@ -102,17 +91,6 @@ class ApplicationRepository(ApplicationProperties):
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
self._finalize(Result())
def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
"""
sync to remote server
Args:
target(Iterable[str]): list of targets to run (e.g. s3)
built_packages(Iterable[Package]): list of packages which has just been built
"""
targets = target or None
self.repository.process_sync(targets, built_packages)
def unknown(self) -> List[str]:
"""
get packages which were not found in AUR
@ -169,7 +147,7 @@ class ApplicationRepository(ApplicationProperties):
process_update(packages, build_result)
# process manual packages
tree = Tree.load(updates, self.database)
tree = Tree.load(updates, self.repository.paths, self.database)
for num, level in enumerate(tree.levels()):
self.logger.info("processing level #%i %s", num, [package.base for package in level])
build_result = self.repository.process_build(level)

View File

@ -29,15 +29,16 @@ from ahriman.application.handlers.patch import Patch
from ahriman.application.handlers.rebuild import Rebuild
from ahriman.application.handlers.remove import Remove
from ahriman.application.handlers.remove_unknown import RemoveUnknown
from ahriman.application.handlers.report import Report
from ahriman.application.handlers.restore import Restore
from ahriman.application.handlers.search import Search
from ahriman.application.handlers.setup import Setup
from ahriman.application.handlers.shell import Shell
from ahriman.application.handlers.sign import Sign
from ahriman.application.handlers.status import Status
from ahriman.application.handlers.status_update import StatusUpdate
from ahriman.application.handlers.sync import Sync
from ahriman.application.handlers.triggers import Triggers
from ahriman.application.handlers.unsafe_commands import UnsafeCommands
from ahriman.application.handlers.update import Update
from ahriman.application.handlers.users import Users
from ahriman.application.handlers.versions import Versions
from ahriman.application.handlers.web import Web

View File

@ -71,10 +71,10 @@ class Handler:
if args.architecture: # architecture is specified explicitly
return sorted(set(args.architecture))
config = Configuration()
config.load(args.configuration)
configuration = Configuration()
configuration.load(args.configuration)
# wtf???
root = config.getpath("repository", "root") # pylint: disable=assignment-from-no-return
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
architectures = RepositoryPaths.known_architectures(root)
if not architectures: # well we did not find anything

View File

@ -52,7 +52,7 @@ class Rebuild(Handler):
if args.from_database:
updates = Rebuild.extract_packages(application)
else:
updates = application.repository.packages_depends_on(depends_on)
updates = application.repository.packages_depend_on(depends_on)
Rebuild.check_if_empty(args.exit_code, not updates)
if args.dry_run:

View File

@ -18,19 +18,25 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import code
import sys
from pathlib import Path
from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import StringPrinter
class Sync(Handler):
class Shell(Handler):
"""
remote sync handler
python shell handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
@ -44,4 +50,10 @@ class Sync(Handler):
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).sync(args.target, [])
# pylint: disable=possibly-unused-variable
application = Application(architecture, configuration, no_report, unsafe)
if args.verbose:
# 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)
code.interact(local=locals())

View File

@ -52,11 +52,11 @@ class Status(Handler):
# we are using reporter here
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter
if args.ahriman:
ahriman = client.get_self()
StatusPrinter(ahriman).print(args.info)
service_status = client.get_internal()
StatusPrinter(service_status.status).print(args.info)
if args.package:
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
[client.get(base) for base in args.package],
(client.get(base) for base in args.package),
start=[])
else:
packages = client.get(None)

View File

@ -27,9 +27,9 @@ from ahriman.core.configuration import Configuration
from ahriman.models.result import Result
class Report(Handler):
class Triggers(Handler):
"""
generate report handler
triggers handlers
"""
@classmethod
@ -45,4 +45,8 @@ class Report(Handler):
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).report(args.target, Result())
application = Application(architecture, configuration, no_report, unsafe)
if args.trigger:
loader = application.repository.triggers
loader.triggers = [loader.load_trigger(trigger) for trigger in args.trigger]
application.repository.process_triggers(Result())

View File

@ -149,7 +149,7 @@ class Users(Handler):
Returns:
User: built user descriptor
"""
user = User(args.username, args.password, args.role)
if user.password is None:
user.password = getpass.getpass()
return user
password = args.password
if password is None:
password = getpass.getpass()
return User(username=args.username, password=password, access=args.role)

View File

@ -0,0 +1,87 @@
#
# 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 argparse
import pkg_resources
import sys
from typing import Dict, List, Tuple, Type
from ahriman import version
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import VersionPrinter
class Versions(Handler):
"""
version handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
VersionPrinter(f"Module version {version.__version__}",
{"Python": sys.version}).print(verbose=False, separator=" ")
packages = Versions.package_dependencies("ahriman", ("pacman", "s3", "web"))
VersionPrinter("Installed packages", packages).print(verbose=False, separator=" ")
@staticmethod
def package_dependencies(root: str, root_extras: Tuple[str, ...] = ()) -> Dict[str, str]:
"""
extract list of ahriman package dependencies installed into system with their versions
Args:
root(str): root package name
root_extras(Tuple[str, ...]): extras for the root package (Default value = ())
Returns:
Dict[str, str]: map of installed dependency to its version
"""
resources: Dict[str, pkg_resources.Distribution] = pkg_resources.working_set.by_key # type: ignore
def dependencies_by_key(key: str, extras: Tuple[str, ...] = ()) -> List[str]:
return [entry.key for entry in resources[key].requires(extras)]
keys: List[str] = []
portion = {key for key in dependencies_by_key(root, root_extras) if key in resources}
while portion:
keys.extend(portion)
portion = {
key
for key in sum((dependencies_by_key(key) for key in portion), start=[])
if key not in keys and key in resources
}
return {
resource.project_name: resource.version
for resource in map(lambda key: resources[key], keys)
}

View File

@ -20,7 +20,6 @@
from __future__ import annotations
import argparse
import logging
from pathlib import Path
from types import TracebackType
@ -29,12 +28,13 @@ from typing import Literal, Optional, Type
from ahriman import version
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRun
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.status.client import Client
from ahriman.core.util import check_user
from ahriman.models.build_status import BuildStatusEnum
class Lock:
class Lock(LazyLogging):
"""
wrapper for application lock file
@ -115,10 +115,8 @@ class Lock:
"""
status = self.reporter.get_internal()
if status.version is not None and status.version != version.__version__:
logging.getLogger("root").warning(
"status watcher version mismatch, our %s, their %s",
version.__version__,
status.version)
self.logger.warning("status watcher version mismatch, our %s, their %s",
version.__version__, status.version)
def check_user(self) -> None:
"""

View File

@ -36,11 +36,13 @@ class AUR(Remote):
DEFAULT_AUR_URL(str): (class attribute) default AUR url
DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url
DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
"""
DEFAULT_AUR_URL = "https://aur.archlinux.org"
DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc"
DEFAULT_RPC_VERSION = "5"
DEFAULT_TIMEOUT = 30
@staticmethod
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
@ -113,7 +115,7 @@ class AUR(Remote):
query[key] = value
try:
response = requests.get(self.DEFAULT_RPC_URL, params=query)
response = requests.get(self.DEFAULT_RPC_URL, params=query, timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status()
return self.parse_response(response.json())
except requests.HTTPError as e:

View File

@ -36,11 +36,13 @@ class Official(Remote):
DEFAULT_ARCHLINUX_URL(str): (class attribute) default archlinux url
DEFAULT_SEARCH_REPOSITORIES(List[str]): (class attribute) default list of repositories to search
DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
"""
DEFAULT_ARCHLINUX_URL = "https://archlinux.org"
DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib", "Community"]
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
DEFAULT_TIMEOUT = 30
@staticmethod
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
@ -101,7 +103,10 @@ class Official(Remote):
List[AURPackage]: response parsed to package list
"""
try:
response = requests.get(self.DEFAULT_RPC_URL, params={by: args, "repo": self.DEFAULT_SEARCH_REPOSITORIES})
response = requests.get(
self.DEFAULT_RPC_URL,
params={by: args, "repo": self.DEFAULT_SEARCH_REPOSITORIES},
timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status()
return self.parse_response(response.json())
except requests.HTTPError as e:

View File

@ -19,21 +19,17 @@
#
from __future__ import annotations
import logging
from typing import Dict, List, Type
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.lazy_logging import LazyLogging
from ahriman.models.aur_package import AURPackage
class Remote:
class Remote(LazyLogging):
"""
base class for remote package search
Attributes:
logger(logging.Logger): class logger
Examples:
These classes are designed to be used without instancing. In order to achieve it several class methods are
provided: ``info``, ``multisearch`` and ``search``. Thus, the basic flow is the following::
@ -47,12 +43,6 @@ class Remote:
directly, whereas ``multisearch`` splits search one by one and finds intersection between search results.
"""
def __init__(self) -> None:
"""
default constructor
"""
self.logger = logging.getLogger("build_details")
@classmethod
def info(cls: Type[Remote], package_name: str, *, pacman: Pacman) -> AURPackage:
"""

View File

@ -17,22 +17,20 @@
# 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 pathlib import Path
from typing import List
from ahriman.core.exceptions import BuildFailed
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.util import check_output
from ahriman.models.repository_paths import RepositoryPaths
class Repo:
class Repo(LazyLogging):
"""
repo-add and repo-remove wrapper
Attributes:
logger(logging.Logger): class logger
name(str): repository name
paths(RepositoryPaths): repository paths instance
sign_args(List[str]): additional args which have to be used to sign repository archive
@ -50,7 +48,6 @@ class Repo:
paths(RepositoryPaths): repository paths instance
sign_args(List[str]): additional args which have to be used to sign repository archive
"""
self.logger = logging.getLogger("build_details")
self.name = name
self.paths = paths
self.uid, _ = paths.root_owner

View File

@ -19,25 +19,23 @@
#
from __future__ import annotations
import logging
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.models.auth_settings import AuthSettings
from ahriman.models.user_access import UserAccess
class Auth:
class Auth(LazyLogging):
"""
helper to deal with user authorization
Attributes:
enabled(bool): indicates if authorization is enabled
logger(logging.Logger): class logger
max_age(int): session age in seconds. It will be used for both client side and server side checks
safe_build_status(bool): allow read only access to the index page
allow_read_only(bool): allow read only access to APIs
"""
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
@ -48,9 +46,7 @@ class Auth:
configuration(Configuration): configuration instance
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.Disabled)
"""
self.logger = logging.getLogger("http")
self.safe_build_status = configuration.getboolean("auth", "safe_build_status")
self.allow_read_only = configuration.getboolean("auth", "allow_read_only")
self.enabled = provider.is_enabled
self.max_age = configuration.getint("auth", "max_age", fallback=7 * 24 * 3600)
@ -89,7 +85,7 @@ class Auth:
return OAuth(configuration, database)
return cls(configuration)
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool:
"""
validate user password
@ -103,7 +99,7 @@ class Auth:
del username, password
return True
async def known_username(self, username: Optional[str]) -> bool: # pylint: disable=no-self-use
async def known_username(self, username: Optional[str]) -> bool:
"""
check if user is known
@ -116,7 +112,7 @@ class Auth:
del username
return True
async def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool: # pylint: disable=no-self-use
async def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool:
"""
validate if user has access to requested resource

View File

@ -56,7 +56,7 @@ class OAuth(Mapping):
self.client_secret = configuration.get("auth", "client_secret")
# in order to use OAuth feature the service must be publicity available
# thus we expect that address is set
self.redirect_uri = f"""{configuration.get("web", "address")}/user-api/v1/login"""
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
self.scopes = configuration.get("auth", "oauth_scopes")
@ -69,7 +69,7 @@ class OAuth(Mapping):
Returns:
str: login control as html code to insert
"""
return """<a class="nav-link" href="/user-api/v1/login" title="login via OAuth2">login</a>"""
return """<a class="nav-link" href="/api/v1/login" title="login via OAuth2">login</a>"""
@staticmethod
def get_provider(name: str) -> Type[aioauth_client.OAuth2Client]:

View File

@ -17,64 +17,49 @@
# 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 shutil
from pathlib import Path
from typing import List, Optional
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.util import check_output, walk
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths
class Sources:
class Sources(LazyLogging):
"""
helper to download package sources (PKGBUILD etc)
Attributes:
DEFAULT_BRANCH(str): (class attribute) default branch to process git repositories.
Must be used only for local stored repositories, use RemoteSource descriptor instead for real packages
logger(logging.Logger): (class attribute) class logger
"""
DEFAULT_BRANCH = "master" # default fallback branch
logger = logging.getLogger("build_details")
_check_output = check_output
@staticmethod
def add(sources_dir: Path, *pattern: str) -> None:
def extend_architectures(sources_dir: Path, architecture: str) -> None:
"""
track found files via git
extend existing PKGBUILD with repository architecture
Args:
sources_dir(Path): local path to git repository
*pattern(str): glob patterns
sources_dir(Path): local path to directory with source files
architecture(str): repository architecture
"""
# glob directory to find files which match the specified patterns
found_files: List[Path] = []
for glob in pattern:
found_files.extend(sources_dir.glob(glob))
if not found_files:
return # no additional files found
Sources.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],
exception=None, cwd=sources_dir, logger=Sources.logger)
pkgbuild_path = sources_dir / "PKGBUILD"
if not pkgbuild_path.is_file():
return
@staticmethod
def diff(sources_dir: Path) -> str:
"""
generate diff from the current version and write it to the output file
Args:
sources_dir(Path): local path to git repository
Returns:
str: patch as plain string
"""
return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
architectures = Package.supported_architectures(sources_dir)
architectures.add(architecture)
patch = PkgbuildPatch("arch", list(architectures))
patch.write(pkgbuild_path)
@staticmethod
def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None:
@ -85,36 +70,38 @@ class Sources:
sources_dir(Path): local path to fetch
remote(Optional[RemoteSource]): remote target (from where to fetch)
"""
instance = Sources()
# local directory exists and there is .git directory
is_initialized_git = (sources_dir / ".git").is_dir()
if is_initialized_git and not Sources.has_remotes(sources_dir):
if is_initialized_git and not instance.has_remotes(sources_dir):
# there is git repository, but no remote configured so far
Sources.logger.info("skip update at %s because there are no branches configured", sources_dir)
instance.logger.info("skip update at %s because there are no branches configured", sources_dir)
return
branch = remote.branch if remote is not None else Sources.DEFAULT_BRANCH
branch = remote.branch if remote is not None else instance.DEFAULT_BRANCH
if is_initialized_git:
Sources.logger.info("update HEAD to remote at %s using branch %s", sources_dir, branch)
instance.logger.info("update HEAD to remote at %s using branch %s", sources_dir, branch)
Sources._check_output("git", "fetch", "origin", branch,
exception=None, cwd=sources_dir, logger=Sources.logger)
exception=None, cwd=sources_dir, logger=instance.logger)
elif remote is not None:
Sources.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch)
instance.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch)
Sources._check_output("git", "clone", "--branch", branch, "--single-branch",
remote.git_url, str(sources_dir),
exception=None, cwd=sources_dir, logger=Sources.logger)
exception=None, cwd=sources_dir, logger=instance.logger)
else:
Sources.logger.warning("%s is not initialized, but no remote provided", sources_dir)
# it will cause an exception later
instance.logger.error("%s is not initialized, but no remote provided", sources_dir)
# and now force reset to our branch
Sources._check_output("git", "checkout", "--force", branch,
exception=None, cwd=sources_dir, logger=Sources.logger)
exception=None, cwd=sources_dir, logger=instance.logger)
Sources._check_output("git", "reset", "--hard", f"origin/{branch}",
exception=None, cwd=sources_dir, logger=Sources.logger)
exception=None, cwd=sources_dir, logger=instance.logger)
# move content if required
# we are using full path to source directory in order to make append possible
pkgbuild_dir = remote.pkgbuild_dir if remote is not None else sources_dir.resolve()
Sources.move((sources_dir / pkgbuild_dir).resolve(), sources_dir)
instance.move((sources_dir / pkgbuild_dir).resolve(), sources_dir)
@staticmethod
def has_remotes(sources_dir: Path) -> bool:
@ -127,7 +114,8 @@ class Sources:
Returns:
bool: True in case if there is any remote and false otherwise
"""
remotes = Sources._check_output("git", "remote", exception=None, cwd=sources_dir, logger=Sources.logger)
instance = Sources()
remotes = Sources._check_output("git", "remote", exception=None, cwd=sources_dir, logger=instance.logger)
return bool(remotes)
@staticmethod
@ -138,53 +126,30 @@ class Sources:
Args:
sources_dir(Path): local path to sources
"""
Sources._check_output("git", "init", "--initial-branch", Sources.DEFAULT_BRANCH,
exception=None, cwd=sources_dir, logger=Sources.logger)
instance = Sources()
Sources._check_output("git", "init", "--initial-branch", instance.DEFAULT_BRANCH,
exception=None, cwd=sources_dir, logger=instance.logger)
@staticmethod
def load(sources_dir: Path, remote: Optional[RemoteSource], patch: Optional[str]) -> None:
def load(sources_dir: Path, package: Package, patch: Optional[str], paths: RepositoryPaths) -> None:
"""
fetch sources from remote and apply patches
Args:
sources_dir(Path): local path to fetch
remote(Optional[RemoteSource]): remote target (from where to fetch)
package(Package): package definitions
patch(Optional[str]): optional patch to be applied
paths(RepositoryPaths): repository paths instance
"""
Sources.fetch(sources_dir, remote)
if patch is None:
Sources.logger.info("no patches found")
return
Sources.patch_apply(sources_dir, patch)
instance = Sources()
if (cache_dir := paths.cache_for(package.base)).is_dir() and cache_dir != sources_dir:
# no need to clone whole repository, just copy from cache first
shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True)
instance.fetch(sources_dir, package.remote)
@staticmethod
def move(pkgbuild_dir: Path, sources_dir: Path) -> None:
"""
move content from pkgbuild_dir to sources_dir
Args:
pkgbuild_dir(Path): path to directory with pkgbuild from which need to move
sources_dir(Path): path to target directory
"""
if pkgbuild_dir == sources_dir:
return # directories are the same, no need to move
for src in walk(pkgbuild_dir):
dst = sources_dir / src.relative_to(pkgbuild_dir)
shutil.move(src, dst)
@staticmethod
def patch_apply(sources_dir: Path, patch: str) -> None:
"""
apply patches if any
Args:
sources_dir(Path): local path to directory with git sources
patch(str): patch to be applied
"""
# create patch
Sources.logger.info("apply patch from database")
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
exception=None, cwd=sources_dir, input_data=patch, logger=Sources.logger)
if patch is not None:
instance.patch_apply(sources_dir, patch)
instance.extend_architectures(sources_dir, paths.architecture)
@staticmethod
def patch_create(sources_dir: Path, *pattern: str) -> str:
@ -198,6 +163,67 @@ class Sources:
Returns:
str: patch as plain text
"""
Sources.add(sources_dir, *pattern)
diff = Sources.diff(sources_dir)
instance = Sources()
instance.add(sources_dir, *pattern)
diff = instance.diff(sources_dir)
return f"{diff}\n" # otherwise, patch will be broken
def add(self, sources_dir: Path, *pattern: str) -> None:
"""
track found files via git
Args:
sources_dir(Path): local path to git repository
*pattern(str): glob patterns
"""
# glob directory to find files which match the specified patterns
found_files: List[Path] = []
for glob in pattern:
found_files.extend(sources_dir.glob(glob))
if not found_files:
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],
exception=None, cwd=sources_dir, logger=self.logger)
def diff(self, sources_dir: Path) -> str:
"""
generate diff from the current version and write it to the output file
Args:
sources_dir(Path): local path to git repository
Returns:
str: patch as plain string
"""
return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=self.logger)
def move(self, pkgbuild_dir: Path, sources_dir: Path) -> None:
"""
move content from pkgbuild_dir to sources_dir
Args:
pkgbuild_dir(Path): path to directory with pkgbuild from which need to move
sources_dir(Path): path to target directory
"""
del self
if pkgbuild_dir == sources_dir:
return # directories are the same, no need to move
for src in walk(pkgbuild_dir):
dst = sources_dir / src.relative_to(pkgbuild_dir)
shutil.move(src, dst)
def patch_apply(self, sources_dir: Path, patch: str) -> None:
"""
apply patches if any
Args:
sources_dir(Path): local path to directory with git sources
patch(str): patch to be applied
"""
# create patch
self.logger.info("apply patch from database")
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
exception=None, cwd=sources_dir, input_data=patch, logger=self.logger)

View File

@ -17,9 +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 logging
import shutil
from pathlib import Path
from typing import List
@ -27,18 +24,17 @@ 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 BuildFailed
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.util import check_output
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
class Task:
class Task(LazyLogging):
"""
base package build task
Attributes:
build_logger(logging.Logger): logger for build process
logger(logging.Logger): class logger
package(Package): package definitions
paths(RepositoryPaths): repository paths instance
uid(int): uid of the repository owner user
@ -55,8 +51,6 @@ class Task:
configuration(Configuration): configuration instance
paths(RepositoryPaths): repository paths instance
"""
self.logger = logging.getLogger("root")
self.build_logger = logging.getLogger("build_details")
self.package = package
self.paths = paths
self.uid, _ = paths.root_owner
@ -66,12 +60,12 @@ class Task:
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
def build(self, sources_path: Path) -> List[Path]:
def build(self, sources_dir: Path) -> List[Path]:
"""
run package build
Args:
sources_path(Path): path to where sources are
sources_dir(Path): path to where sources are
Returns:
List[Path]: paths of produced packages
@ -85,26 +79,23 @@ class Task:
Task._check_output(
*command,
exception=BuildFailed(self.package.base),
cwd=sources_path,
logger=self.build_logger,
cwd=sources_dir,
logger=self.logger,
user=self.uid)
# well it is not actually correct, but we can deal with it
packages = Task._check_output("makepkg", "--packagelist",
exception=BuildFailed(self.package.base),
cwd=sources_path,
logger=self.build_logger).splitlines()
cwd=sources_dir,
logger=self.logger).splitlines()
return [Path(package) for package in packages]
def init(self, path: Path, database: SQLite) -> None:
def init(self, sources_dir: Path, database: SQLite) -> None:
"""
fetch package from git
Args:
path(Path): local path to fetch
sources_dir(Path): local path to fetch
database(SQLite): database instance
"""
if self.paths.cache_for(self.package.base).is_dir():
# no need to clone whole repository, just copy from cache first
shutil.copytree(self.paths.cache_for(self.package.base), path, dirs_exist_ok=True)
Sources.load(path, self.package.remote, database.patches_get(self.package.base))
Sources.load(sources_dir, self.package, database.patches_get(self.package.base), self.paths)

View File

@ -125,11 +125,11 @@ class Configuration(configparser.RawConfigParser):
Returns:
Configuration: configuration instance
"""
config = cls()
config.load(path)
config.merge_sections(architecture)
config.load_logging(quiet)
return config
configuration = cls()
configuration.load(path)
configuration.merge_sections(architecture)
configuration.load_logging(quiet)
return configuration
@staticmethod
def __convert_list(value: str) -> List[str]:
@ -220,7 +220,7 @@ class Configuration(configparser.RawConfigParser):
}
# pylint and mypy are too stupid to find these methods
# pylint: disable=missing-function-docstring,multiple-statements,unused-argument,no-self-use
# pylint: disable=missing-function-docstring,multiple-statements,unused-argument
def getlist(self, *args: Any, **kwargs: Any) -> List[str]: ...
def getpath(self, *args: Any, **kwargs: Any) -> Path: ...

View File

@ -36,7 +36,7 @@ def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> N
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
from ahriman.core.database.operations.package_operations import PackageOperations
from ahriman.core.database.operations import PackageOperations
def insert_remote(base: str, remote: RemoteSource) -> None:
connection.execute(

View File

@ -19,8 +19,6 @@
#
from __future__ import annotations
import logging
from importlib import import_module
from pathlib import Path
from pkgutil import iter_modules
@ -29,11 +27,12 @@ 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.models.migration import Migration
from ahriman.models.migration_result import MigrationResult
class Migrations:
class Migrations(LazyLogging):
"""
simple migration wrapper for the sqlite
idea comes from https://www.ash.dev/blog/simple-migration-system-in-sqlite/
@ -41,7 +40,6 @@ class Migrations:
Attributes:
configuration(Configuration): configuration instance
connection(Connection): database connection
logger(logging.Logger): class logger
"""
def __init__(self, connection: Connection, configuration: Configuration) -> None:
@ -54,7 +52,6 @@ class Migrations:
"""
self.connection = connection
self.configuration = configuration
self.logger = logging.getLogger("database")
@classmethod
def migrate(cls: Type[Migrations], connection: Connection, configuration: Configuration) -> MigrationResult:
@ -86,7 +83,7 @@ class Migrations:
module = import_module(f"{__name__}.{module_name}")
steps: List[str] = getattr(module, "steps", [])
self.logger.debug("found migration %s at index %s with steps count %s", module_name, index, len(steps))
migrations.append(Migration(index, module_name, steps))
migrations.append(Migration(index=index, name=module_name, steps=steps))
return migrations
@ -100,7 +97,7 @@ class Migrations:
migrations = self.migrations()
current_version = self.user_version()
expected_version = len(migrations)
result = MigrationResult(current_version, expected_version)
result = MigrationResult(old_version=current_version, new_version=expected_version)
if not result.is_outdated:
self.logger.info("no migrations required")

View File

@ -0,0 +1,33 @@
#
# 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 = [
"""
update users set access = 'read' where access = 'safe'
""",
"""
update users set access = 'reporter' where access = 'read'
""",
"""
update users set access = 'full' where access = 'write'
""",
]

View File

@ -17,3 +17,9 @@
# 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.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.package_operations import PackageOperations
from ahriman.core.database.operations.patch_operations import PatchOperations

View File

@ -20,7 +20,7 @@
from sqlite3 import Connection
from typing import List, Optional
from ahriman.core.database.operations.operations import Operations
from ahriman.core.database.operations import Operations
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
@ -58,7 +58,7 @@ class AuthOperations(Operations):
def run(connection: Connection) -> List[User]:
return [
User(cursor["username"], cursor["password"], UserAccess(cursor["access"]))
User(username=cursor["username"], password=cursor["password"], access=UserAccess(cursor["access"]))
for cursor in connection.execute(
"""
select * from users

View File

@ -20,7 +20,7 @@
from sqlite3 import Connection
from typing import List, Optional
from ahriman.core.database.operations.operations import Operations
from ahriman.core.database.operations import Operations
from ahriman.models.package import Package

View File

@ -17,23 +17,22 @@
# 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 sqlite3
from pathlib import Path
from sqlite3 import Connection, Cursor
from typing import Any, Dict, Tuple, TypeVar, Callable
from ahriman.core.lazy_logging import LazyLogging
T = TypeVar("T")
class Operations:
class Operations(LazyLogging):
"""
base operation class
Attributes:
logger(logging.Logger): class logger
path(Path): path to the database file
"""
@ -45,7 +44,6 @@ class Operations:
path(Path): path to the database file
"""
self.path = path
self.logger = logging.getLogger("database")
@staticmethod
def factory(cursor: Cursor, row: Tuple[Any, ...]) -> Dict[str, Any]:

View File

@ -20,7 +20,7 @@
from sqlite3 import Connection
from typing import Dict, Generator, Iterable, List, Tuple
from ahriman.core.database.operations.operations import Operations
from ahriman.core.database.operations import Operations
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
@ -154,7 +154,11 @@ class PackageOperations(Operations):
Dict[str, Package]: map of the package base to its descriptor (without packages themselves)
"""
return {
row["package_base"]: Package(row["package_base"], row["version"], RemoteSource.from_json(row), {})
row["package_base"]: Package(
base=row["package_base"],
version=row["version"],
remote=RemoteSource.from_json(row),
packages={})
for row in connection.execute("""select * from package_bases""")
}

View File

@ -20,7 +20,7 @@
from sqlite3 import Connection
from typing import Dict, Optional
from ahriman.core.database.operations.operations import Operations
from ahriman.core.database.operations import Operations
class PatchOperations(Operations):

View File

@ -27,10 +27,7 @@ from typing import Type
from ahriman.core.configuration import Configuration
from ahriman.core.database.migrations import Migrations
from ahriman.core.database.operations.auth_operations import AuthOperations
from ahriman.core.database.operations.build_operations import BuildOperations
from ahriman.core.database.operations.package_operations import PackageOperations
from ahriman.core.database.operations.patch_operations import PatchOperations
from ahriman.core.database.operations import AuthOperations, BuildOperations, PackageOperations, PatchOperations
class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations):

View File

@ -70,6 +70,12 @@ class InitializeException(RuntimeError):
RuntimeError.__init__(self, f"Could not load service: {details}")
class InvalidExtension(RuntimeError):
"""
exception being raised by trigger load in case of errors
"""
class InvalidOption(ValueError):
"""
exception which will be raised on configuration errors

View File

@ -27,3 +27,4 @@ from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter
from ahriman.core.formatters.version_printer import VersionPrinter

View File

@ -44,7 +44,7 @@ class Printer:
continue
log_fn(f"\t{prop.name}{separator}{prop.value}")
def properties(self) -> List[Property]: # pylint: disable=no-self-use
def properties(self) -> List[Property]:
"""
convert content into printable data

View File

@ -25,6 +25,9 @@ from ahriman.core.formatters import Printer
class StringPrinter(Printer):
"""
print content of the random string
Attributes:
content(str): any content string
"""
def __init__(self, content: str) -> None:

View File

@ -0,0 +1,55 @@
#
# 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 typing import Dict, List
from ahriman.core.formatters import StringPrinter
from ahriman.models.property import Property
class VersionPrinter(StringPrinter):
"""
print content of the python package versions
Attributes:
packages(Dict[str, str]): map of package name to its version
"""
def __init__(self, title: str, packages: Dict[str, str]) -> None:
"""
default constructor
Args:
title(str): title of the message
packages(Dict[str, str]): map of package name to its version
"""
StringPrinter.__init__(self, title)
self.packages = packages
def properties(self) -> List[Property]:
"""
convert content into printable data
Returns:
List[Property]: list of content properties
"""
return [
Property(package, version, is_required=True)
for package, version in sorted(self.packages.items())
]

View File

@ -0,0 +1,64 @@
#
# 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 typing import Any
class LazyLogging:
"""
wrapper for the logger library inspired by scala lazy logging module
Attributes:
logger(logging.Logger): class logger instance
"""
logger: logging.Logger
def __getattr__(self, item: str) -> Any:
"""
logger extractor
Args:
item(str) property name:
Returns:
Any: attribute by its name
Raises:
AttributeError: in case if no such attribute found
"""
if item == "logger":
logger = logging.getLogger(self.logger_name)
setattr(self, item, logger)
return logger
raise AttributeError(f"'{self.__class__.__qualname__}' object has no attribute '{item}'")
@property
def logger_name(self) -> str:
"""
extract logger name for the class
Returns:
str: logger name as combination of module name and class name
"""
clazz = self.__class__
prefix = "" if clazz.__module__ is None else f"{clazz.__module__}."
return f"{prefix}{self.__class__.__qualname__}"

View File

@ -18,9 +18,4 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.report.report import Report
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.console import Console
from ahriman.core.report.email import Email
from ahriman.core.report.html import HTML
from ahriman.core.report.telegram import Telegram
from ahriman.core.report.report_trigger import ReportTrigger

View File

@ -25,7 +25,8 @@ from email.mime.text import MIMEText
from typing import Dict, Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.report import JinjaTemplate, Report
from ahriman.core.report import Report
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.util import pretty_datetime
from ahriman.models.package import Package
from ahriman.models.result import Result
@ -85,7 +86,7 @@ class Email(Report, JinjaTemplate):
message = MIMEMultipart()
message["From"] = self.sender
message["To"] = ", ".join(self.receivers)
message["Subject"] = f"{self.name} build report at {pretty_datetime(datetime.datetime.utcnow().timestamp())}"
message["Subject"] = f"{self.name} build report at {pretty_datetime(datetime.datetime.utcnow())}"
message.attach(MIMEText(text, "html"))
for filename, content in attachment.items():

View File

@ -20,7 +20,8 @@
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.report import JinjaTemplate, Report
from ahriman.core.report import Report
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.models.package import Package
from ahriman.models.result import Result

View File

@ -19,25 +19,23 @@
#
from __future__ import annotations
import logging
from typing import Iterable, Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportFailed
from ahriman.core.lazy_logging import LazyLogging
from ahriman.models.package import Package
from ahriman.models.report_settings import ReportSettings
from ahriman.models.result import Result
class Report:
class Report(LazyLogging):
"""
base report generator
Attributes:
architecture(str): repository architecture
configuration(Configuration): configuration instance
logger(logging.Logger): class logger
Examples:
``Report`` classes provide several method in order to operate with the report generation and additional class
@ -67,7 +65,6 @@ class Report:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
self.logger = logging.getLogger("root")
self.architecture = architecture
self.configuration = configuration
@ -87,16 +84,16 @@ class Report:
section, provider_name = configuration.gettype(target, architecture)
provider = ReportSettings.from_option(provider_name)
if provider == ReportSettings.HTML:
from ahriman.core.report import HTML
from ahriman.core.report.html import HTML
return HTML(architecture, configuration, section)
if provider == ReportSettings.Email:
from ahriman.core.report import Email
from ahriman.core.report.email import Email
return Email(architecture, configuration, section)
if provider == ReportSettings.Console:
from ahriman.core.report import Console
from ahriman.core.report.console import Console
return Console(architecture, configuration, section)
if provider == ReportSettings.Telegram:
from ahriman.core.report import Telegram
from ahriman.core.report.telegram import Telegram
return Telegram(architecture, configuration, section)
return cls(architecture, configuration) # should never happen
@ -109,13 +106,13 @@ class Report:
result(Result): build result
"""
def run(self, packages: Iterable[Package], result: Result) -> None:
def run(self, result: Result, packages: Iterable[Package]) -> None:
"""
run report generation
Args:
packages(Iterable[Package]): list of packages to generate report
result(Result): build result
packages(Iterable[Package]): list of packages to generate report
Raises:
ReportFailed: in case of any report unmatched exception

View File

@ -0,0 +1,58 @@
#
# 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 typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger
from ahriman.core.report import Report
from ahriman.models.package import Package
from ahriman.models.result import Result
class ReportTrigger(Trigger):
"""
report trigger
Attributes:
targets(List[str]): report target list
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, architecture, configuration)
self.targets = configuration.getlist("report", "target")
def run(self, result: Result, packages: Iterable[Package]) -> None:
"""
run trigger
Args:
result(Result): build result
packages(Iterable[Package]): list of all available packages
"""
for target in self.targets:
runner = Report.load(self.architecture, self.configuration, target)
runner.run(result, packages)

View File

@ -23,7 +23,8 @@ import requests
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.report import JinjaTemplate, Report
from ahriman.core.report import Report
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.util import exception_response_text
from ahriman.models.package import Package
from ahriman.models.result import Result
@ -40,6 +41,7 @@ class Telegram(Report, JinjaTemplate):
chat_id(str): chat id to post message, either string with @ or integer
template_path(Path): path to template for built packages
template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown
timeout(int): HTTP request timeout in seconds
"""
TELEGRAM_API_URL = "https://api.telegram.org"
@ -61,6 +63,7 @@ class Telegram(Report, JinjaTemplate):
self.chat_id = configuration.get(section, "chat_id")
self.template_path = configuration.getpath(section, "template_path")
self.template_type = configuration.get(section, "template_type", fallback="HTML")
self.timeout = configuration.getint(section, "timeout", fallback=30)
def _send(self, text: str) -> None:
"""
@ -72,7 +75,8 @@ class Telegram(Report, JinjaTemplate):
try:
response = requests.post(
f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage",
data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type})
data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type},
timeout=self.timeout)
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not perform request: %s", exception_response_text(e))
@ -94,8 +98,11 @@ class Telegram(Report, JinjaTemplate):
text = self.make_html(result, self.template_path)
# telegram content is limited by 4096 symbols, so we are going to split the message by new lines
# to fit into this restriction
if len(text) > self.TELEGRAM_MAX_CONTENT_LENGTH:
while len(text) > self.TELEGRAM_MAX_CONTENT_LENGTH:
position = text.rfind("\n", 0, self.TELEGRAM_MAX_CONTENT_LENGTH)
if position == -1:
# normally should not happen, but we allow templates editing
raise ValueError("substring not found")
portion, text = text[:position], text[position + 1:] # +1 to exclude newline we split
self._send(portion)
# send remaining (or full in case if size is less than max length) text

View File

@ -20,14 +20,14 @@
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Iterable, List, Optional, Set
from ahriman.core.build_tools.task import Task
from ahriman.core.report import Report
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.upload import Upload
from ahriman.core.util import tmpdir
from ahriman.core.util import safe_filename
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.result import Result
@ -84,7 +84,8 @@ class Executor(Cleaner):
result = Result()
for single in updates:
with tmpdir() as build_dir:
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, \
(build_dir := Path(dir_name)): # pylint: disable=confusing-with-statement
try:
build_single(single, build_dir)
result.add_success(single)
@ -124,16 +125,16 @@ class Executor(Cleaner):
for local in self.packages():
if local.base in packages or all(package in requested for package in local.packages):
to_remove = {
package: Path(properties.filename)
package: properties.filepath
for package, properties in local.packages.items()
if properties.filename is not None
if properties.filepath is not None
}
remove_base(local.base)
elif requested.intersection(local.packages.keys()):
to_remove = {
package: Path(properties.filename)
package: properties.filepath
for package, properties in local.packages.items()
if package in requested and properties.filename is not None
if package in requested and properties.filepath is not None
}
else:
to_remove = {}
@ -143,35 +144,14 @@ class Executor(Cleaner):
return self.repo.repo_path
def process_report(self, targets: Optional[Iterable[str]], result: Result) -> None:
def process_triggers(self, result: Result) -> None:
"""
generate reports
process triggers setup by settings
Args:
targets(Optional[Iterable[str]]): list of targets to generate reports. Configuration option will be used
if it is not set
result(Result): build result
"""
if targets is None:
targets = self.configuration.getlist("report", "target")
for target in targets:
runner = Report.load(self.architecture, self.configuration, target)
runner.run(self.packages(), result)
def process_sync(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None:
"""
process synchronization to remote servers
Args:
targets(Optional[Iterable[str]]): list of targets to sync. Configuration option will be used
if it is not set
built_packages(Iterable[Package]): list of packages which has just been built
"""
if targets is None:
targets = self.configuration.getlist("upload", "target")
for target in targets:
runner = Upload.load(self.architecture, self.configuration, target)
runner.run(self.paths.repository, built_packages)
self.triggers.process(result, self.packages())
def process_update(self, packages: Iterable[Path]) -> Result:
"""
@ -183,17 +163,25 @@ class Executor(Cleaner):
Returns:
Result: path to repository database
"""
def rename(archive: PackageDescription, base: str) -> None:
if archive.filename is None:
self.logger.warning("received empty package name for base %s", 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:
if name is None:
self.logger.warning("received empty package name for base %s", base)
return # suppress type checking, it never can be none actually
# in theory it might be NOT packages directory, but we suppose it is
# 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)
for src in files:
dst = self.paths.repository / src.name
dst = self.paths.repository / safe_filename(src.name)
shutil.move(src, dst)
package_path = self.paths.repository / name
package_path = self.paths.repository / safe_filename(name)
self.repo.add(package_path)
current_packages = self.packages()
@ -204,6 +192,7 @@ class Executor(Cleaner):
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)

View File

@ -99,7 +99,7 @@ class Repository(Executor, UpdateHandler):
"""
return list(filter(package_like, self.paths.packages.iterdir()))
def packages_depends_on(self, depends_on: Optional[Iterable[str]]) -> List[Package]:
def packages_depend_on(self, depends_on: Optional[Iterable[str]]) -> List[Package]:
"""
extract list of packages which depends on specified package

View File

@ -17,19 +17,19 @@
# 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 ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.repo import Repo
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnsafeRun
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client
from ahriman.core.triggers import TriggerLoader
from ahriman.core.util import check_user
class RepositoryProperties:
class RepositoryProperties(LazyLogging):
"""
repository internal objects holder
@ -38,13 +38,13 @@ class RepositoryProperties:
configuration(Configuration): configuration instance
database(SQLite): database instance
ignore_list(List[str]): package bases which will be ignored during auto updates
logger(logging.Logger): class logger
name(str): repository name
pacman(Pacman): alpm wrapper instance
paths(RepositoryPaths): repository paths instance
repo(Repo): repo commands wrapper instance
reporter(Client): build status reporter instance
sign(GPG): GPG wrapper instance
triggers(TriggerLoader): triggers holder
"""
def __init__(self, architecture: str, configuration: Configuration, database: SQLite,
@ -59,7 +59,6 @@ class RepositoryProperties:
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
self.logger = logging.getLogger("root")
self.architecture = architecture
self.configuration = configuration
self.database = database
@ -78,3 +77,4 @@ class RepositoryProperties:
self.sign = GPG(architecture, configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client() if no_report else Client.load(configuration)
self.triggers = TriggerLoader(architecture, configuration)

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