Compare commits

...

12 Commits

Author SHA1 Message Date
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
130 changed files with 4745 additions and 4060 deletions

View File

@ -11,9 +11,9 @@ assignees: ''
A clear and concise description of what the bug is. 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 ### Expected behavior
@ -21,4 +21,8 @@ A clear and concise description of what you expected to happen.
### Logs ### 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 ### 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 ### Proposed changes and/or features

View File

@ -60,17 +60,7 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have # --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes # no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W". # --disable=W".
disable=print-statement, disable=raw-checker-failed,
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,
bad-inline-option, bad-inline-option,
locally-disabled, locally-disabled,
file-ignored, file-ignored,
@ -78,67 +68,6 @@ disable=print-statement,
useless-suppression, useless-suppression,
deprecated-pragma, deprecated-pragma,
use-symbolic-message-instead, 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, missing-module-docstring,
line-too-long, line-too-long,
no-name-in-module, no-name-in-module,

66
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,66 @@
# 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 `ahriman.core.util.tmpdir` function provides wrapper for the directories.
* 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.
### 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,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 .DEFAULT_GOAL := archlinux
PROJECT := ahriman 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)) TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
IGNORE_FILES := package/archlinux src/.mypy_cache IGNORE_FILES := package/archlinux src/.mypy_cache
@ -33,22 +33,22 @@ clean:
directory: clean directory: clean
mkdir "$(PROJECT)" 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 add package/archlinux/PKGBUILD src/ahriman/version.py docs/ahriman-architecture.svg docs/ahriman.1
git commit -m "Release $(VERSION)" git commit -m "Release $(VERSION)"
git tag "$(VERSION)" git tag "$(VERSION)"
git push git push
git push --tags git push --tags
spec: specification:
# make sure that old files are removed # make sure that old files are removed
find docs -type f -name "$(PROJECT)*.rst" -delete find docs -type f -name "$(PROJECT)*.rst" -delete
tox -e docs tox -e docs
spec-html: spec
rm -rf docs/html
tox -e docs-html
tests: clean tests: clean
tox -e tests tox -e tests

View File

@ -10,23 +10,26 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
## Features ## Features
* Install-configure-forget manager for own repository. * Install-configure-forget manager for the very own repository.
* Multi-architecture support. * 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) and even ability to write own extensions.
* Dependency manager. * Dependency manager.
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs. * 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: * Repository status interface with optional authorization and control options:
![web interface](web.png) ![web interface](web.png)
## Installation and run ## 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 ## 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, though additional steps (like configuring build toolchain and sudoers) is recommended and can be easily achieved by following install instructions.
## [FAQ](https://ahriman.readthedocs.io/en/latest/faq.html)

View File

@ -37,7 +37,6 @@ And the ``repository`` instance can be used to perform repository maintenance
built_packages = repository.packages_built() built_packages = repository.packages_built()
update_result = repository.process_update(built_packages) update_result = repository.process_update(built_packages)
repository.process_report(None, update_result) repository.process_triggers(update_result)
repository.process_sync(None, update_result.success)
For the more info please refer to the classes documentation. For the more info please refer to the classes documentation.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 517 KiB

After

Width:  |  Height:  |  Size: 522 KiB

View File

@ -3,9 +3,9 @@
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
.B ahriman .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-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-triggers,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 .SH DESCRIPTION
ArcH Linux ReposItory MANager ArcH linux ReposItory MANager
.SH OPTIONS .SH OPTIONS
.TP .TP
@ -37,7 +37,7 @@ force disable any logging
allow to run ahriman as non\-ahriman user. Some actions might be unavailable allow to run ahriman as non\-ahriman user. Some actions might be unavailable
.TP .TP
\fB\-v\fR, \fB\-\-version\fR \fB\-V\fR, \fB\-\-version\fR
show program's version number and exit show program's version number and exit
.SH .SH
@ -97,6 +97,9 @@ rebuild repository
\fBahriman\fR \fI\,repo-remove-unknown\/\fR \fBahriman\fR \fI\,repo-remove-unknown\/\fR
remove unknown packages remove unknown packages
.TP .TP
\fBahriman\fR \fI\,repo-report\/\fR
generate report
.TP
\fBahriman\fR \fI\,repo-restore\/\fR \fBahriman\fR \fI\,repo-restore\/\fR
restore repository data restore repository data
.TP .TP
@ -109,12 +112,18 @@ sign packages
\fBahriman\fR \fI\,repo-status-update\/\fR \fBahriman\fR \fI\,repo-status-update\/\fR
update repository status update repository status
.TP .TP
\fBahriman\fR \fI\,repo-sync\/\fR
sync repository
.TP
\fBahriman\fR \fI\,repo-triggers\/\fR \fBahriman\fR \fI\,repo-triggers\/\fR
run triggers run triggers
.TP .TP
\fBahriman\fR \fI\,repo-update\/\fR \fBahriman\fR \fI\,repo-update\/\fR
update packages update packages
.TP .TP
\fBahriman\fR \fI\,shell\/\fR
envoke python shell
.TP
\fBahriman\fR \fI\,user-add\/\fR \fBahriman\fR \fI\,user-add\/\fR
create or update user create or update user
.TP .TP
@ -124,6 +133,9 @@ user known users and their access
\fBahriman\fR \fI\,user-remove\/\fR \fBahriman\fR \fI\,user-remove\/\fR
remove user remove user
.TP .TP
\fBahriman\fR \fI\,version\/\fR
application version
.TP
\fBahriman\fR \fI\,web\/\fR \fBahriman\fR \fI\,web\/\fR
web server web server
.SH COMMAND \fI\,'ahriman aur-search'\/\fR .SH COMMAND \fI\,'ahriman aur-search'\/\fR
@ -186,8 +198,7 @@ PGP key to import from public server
key server for key import key server for key import
.SH COMMAND \fI\,'ahriman package-add'\/\fR .SH COMMAND \fI\,'ahriman package-add'\/\fR
usage: ahriman package-add [-h] [-e] [-n] usage: ahriman package-add [-h] [-e] [-n] [-s {auto,archive,aur,directory,local,remote,repository}]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}]
[--without-dependencies] [--without-dependencies]
package [package ...] package [package ...]
@ -207,7 +218,7 @@ return non\-zero exit status if result is empty
run update function after run update function after
.TP .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 explicitly specify the package source for this command
.TP .TP
@ -224,9 +235,7 @@ remove package from the repository
package name or base package name or base
.SH COMMAND \fI\,'ahriman package-status'\/\fR .SH COMMAND \fI\,'ahriman package-status'\/\fR
usage: ahriman package-status [-h] [--ahriman] [-e] [-i] usage: ahriman package-status [-h] [--ahriman] [-e] [-i] [-s {unknown,pending,building,failed,success}] [package ...]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
request status of the package request status of the package
@ -248,7 +257,7 @@ return non\-zero exit status if result is empty
show additional package information show additional package information
.TP .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 filter packages by status
.SH COMMAND \fI\,'ahriman package-status-remove'\/\fR .SH COMMAND \fI\,'ahriman package-status-remove'\/\fR
@ -261,9 +270,7 @@ remove the package from the status page
remove specified packages remove specified packages
.SH COMMAND \fI\,'ahriman package-status-update'\/\fR .SH COMMAND \fI\,'ahriman package-status-update'\/\fR
usage: ahriman package-status-update [-h] usage: ahriman package-status-update [-h] [-s {unknown,pending,building,failed,success}] [package ...]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
update package status on the status page update package status on the status page
@ -273,7 +280,7 @@ set status for specified packages. If no packages supplied, service status will
.SH OPTIONS \fI\,'ahriman package-status-update'\/\fR .SH OPTIONS \fI\,'ahriman package-status-update'\/\fR
.TP .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 new status
.SH COMMAND \fI\,'ahriman patch-add'\/\fR .SH COMMAND \fI\,'ahriman patch-add'\/\fR
@ -405,6 +412,11 @@ just perform check for packages without removal
\fB\-i\fR, \fB\-\-info\fR \fB\-i\fR, \fB\-\-info\fR
show additional package information show additional package information
.SH COMMAND \fI\,'ahriman repo-report'\/\fR
usage: ahriman repo-report [-h]
generate repository report according to current settings
.SH COMMAND \fI\,'ahriman repo-restore'\/\fR .SH COMMAND \fI\,'ahriman repo-restore'\/\fR
usage: ahriman repo-restore [-h] [-o OUTPUT] path usage: ahriman repo-restore [-h] [-o OUTPUT] path
@ -422,8 +434,7 @@ root path of the extracted files
.SH COMMAND \fI\,'ahriman repo-setup'\/\fR .SH COMMAND \fI\,'ahriman repo-setup'\/\fR
usage: ahriman repo-setup [-h] [--build-as-user BUILD_AS_USER] [--build-command BUILD_COMMAND] usage: ahriman repo-setup [-h] [--build-as-user BUILD_AS_USER] [--build-command BUILD_COMMAND]
[--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository [--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository
REPOSITORY [--sign-key SIGN_KEY] REPOSITORY [--sign-key SIGN_KEY] [--sign-target {disabled,pacakges,repository}]
[--sign-target {SignSettings.Disabled,SignSettings.Packages,SignSettings.Repository}]
[--web-port WEB_PORT] [--web-port WEB_PORT]
create initial service configuration, requires root create initial service configuration, requires root
@ -458,7 +469,7 @@ repository name
sign key id sign key id
.TP .TP
\fB\-\-sign\-target\fR \fI\,{SignSettings.Disabled,SignSettings.Packages,SignSettings.Repository}\/\fR \fB\-\-sign\-target\fR \fI\,{disabled,pacakges,repository}\/\fR
sign options sign options
.TP .TP
@ -475,21 +486,29 @@ usage: ahriman repo-sign [-h] [package ...]
sign only specified packages sign only specified packages
.SH COMMAND \fI\,'ahriman repo-status-update'\/\fR .SH COMMAND \fI\,'ahriman repo-status-update'\/\fR
usage: ahriman repo-status-update [-h] usage: ahriman repo-status-update [-h] [-s {unknown,pending,building,failed,success}]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
update repository status on the status page update repository status on the status page
.SH OPTIONS \fI\,'ahriman repo-status-update'\/\fR .SH OPTIONS \fI\,'ahriman repo-status-update'\/\fR
.TP .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 new status
.SH COMMAND \fI\,'ahriman repo-sync'\/\fR
usage: ahriman repo-sync [-h]
sync repository files to remote server according to current settings
.SH COMMAND \fI\,'ahriman repo-triggers'\/\fR .SH COMMAND \fI\,'ahriman repo-triggers'\/\fR
usage: ahriman repo-triggers [-h] usage: ahriman repo-triggers [-h] [trigger ...]
run triggers on empty build result as configured by settings run triggers on empty build result as configured by settings
.TP
\fBtrigger\fR
instead of running all triggers as set by configuration, just process specified ones oin order of metion
.SH COMMAND \fI\,'ahriman repo-update'\/\fR .SH COMMAND \fI\,'ahriman repo-update'\/\fR
usage: ahriman repo-update [-h] [--dry-run] [-e] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...] usage: ahriman repo-update [-h] [--dry-run] [-e] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
@ -524,9 +543,13 @@ do not include manual updates
\fB\-\-no\-vcs\fR \fB\-\-no\-vcs\fR
do not check VCS packages 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 .SH COMMAND \fI\,'ahriman user-add'\/\fR
usage: ahriman user-add [-h] [--as-service] [-p PASSWORD] [-r {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [-s] usage: ahriman user-add [-h] [--as-service] [-p PASSWORD] [-r {unauthorized,read,reporter,full}] [-s] username
username
update user for web services with the given password and role. In case if password was not entered it will be asked interactively update user for web services with the given password and role. In case if password was not entered it will be asked interactively
@ -545,7 +568,7 @@ user password. Blank password will be treated as empty password, which is in par
authorization type. authorization type.
.TP .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 user access level
.TP .TP
@ -553,7 +576,7 @@ user access level
set file permissions to user\-only set file permissions to user\-only
.SH COMMAND \fI\,'ahriman user-list'\/\fR .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 list users from the user mapping and their roles
@ -567,7 +590,7 @@ filter users by username
return non\-zero exit status if result is empty return non\-zero exit status if result is empty
.TP .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 filter users by role
.SH COMMAND \fI\,'ahriman user-remove'\/\fR .SH COMMAND \fI\,'ahriman user-remove'\/\fR
@ -584,6 +607,11 @@ username for web service
\fB\-s\fR, \fB\-\-secure\fR \fB\-s\fR, \fB\-\-secure\fR
set file permissions to user\-only 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 .SH COMMAND \fI\,'ahriman web'\/\fR
usage: ahriman web [-h] usage: ahriman web [-h]

View File

@ -116,6 +116,14 @@ ahriman.application.handlers.setup module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.application.handlers.shell module
-----------------------------------------
.. automodule:: ahriman.application.handlers.shell
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.sign module ahriman.application.handlers.sign module
---------------------------------------- ----------------------------------------
@ -172,6 +180,14 @@ ahriman.application.handlers.users module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.application.handlers.versions module
--------------------------------------------
.. automodule:: ahriman.application.handlers.versions
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.web module ahriman.application.handlers.web module
--------------------------------------- ---------------------------------------

View File

@ -20,6 +20,14 @@ ahriman.core.database.migrations.m001\_package\_source module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 Module contents
--------------- ---------------

View File

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

View File

@ -4,14 +4,6 @@ ahriman.web.views.status package
Submodules 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 ahriman.web.views.status.package module
--------------------------------------- ---------------------------------------

View File

@ -19,27 +19,28 @@ Full dependency diagram:
``ahriman.application`` package ``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.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 ``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.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.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.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.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.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.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.Repository`` class) implementation. * ``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.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.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: 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. * Parse command line arguments, find command and related handler which is set by parser.
* Call ``Handler.execute`` method. * 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. * In each child process call lock functions.
* After success checks pass control to ``Handler.run`` method defined by specific handler class. * 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. * 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 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 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). 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).
@ -114,7 +115,7 @@ Idea is to copy package to the directory from which it will be handled at the ne
* 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 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 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 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. 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.
@ -154,6 +155,13 @@ 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 Utils
^^^^^ ^^^^^
@ -262,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. 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. 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

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

View File

@ -1,7 +1,7 @@
Configuration 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: 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:
@ -36,12 +36,12 @@ libalpm and AUR related configuration.
Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` library to be installed. 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_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. * ``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. * ``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_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. * ``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.
* ``salt`` - password hash salt, string, required in case if authorization enabled (automatically generated by ``create-user`` subcommand). * ``salt`` - password hash salt, string, required in case if authorization enabled (automatically generated by ``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. 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,7 +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. * ``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. * ``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. * ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
* ``triggers`` - list of trigger classes (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. * ``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 ``repository`` group
-------------------- --------------------
@ -80,7 +80,7 @@ Settings for signing packages or repository. Group name can refer to architectur
Report generation settings. 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: Type will be read from several ways:
@ -93,6 +93,7 @@ Type will be read from several ways:
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``. * ``use_utf`` - use utf8 symbols in output if set and ascii otherwise, boolean, optional, default ``yes``.
``email`` type ``email`` type
@ -120,9 +121,9 @@ Section name must be either ``email`` (plus optional architecture name, e.g. ``e
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. * ``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. * ``homepage`` - link to homepage, string, optional.
* ``link_path`` - prefix for HTML links, string, required. * ``link_path`` - prefix for HTML links, string, required.
* ``path`` - path to html report file, string, required.
* ``template_path`` - path to Jinja2 template, string, required. * ``template_path`` - path to Jinja2 template, string, required.
``telegram`` type ``telegram`` type

View File

@ -130,7 +130,9 @@ So it is the same as adding any other package, but due to restrictions you must
.. code-block:: shell .. 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 Package build fails because it cannot validate PGP signature of source files
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -6,13 +6,14 @@ Wrapper for managing custom repository inspired by `repo-scripts <https://github
Features Features
-------- --------
* Install-configure-forget manager for own repository. * Install-configure-forget manager for the very own repository.
* Multi-architecture support. * 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) and even ability to write own extensions.
* Dependency manager. * Dependency manager.
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs. * 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. * Repository status interface with optional authorization and control options.
Contents Contents

View File

@ -6,7 +6,7 @@ Initial setup
#. #.
Change settings if required, see :doc:`configuration reference <configuration>` for more details. Change settings if required, see :doc:`configuration reference <configuration>` for more details.
#. #.
TL;DR TL;DR:
.. code-block:: shell .. code-block:: shell
@ -25,21 +25,24 @@ Initial setup
Configure build tools (it is required for correct dependency management system): 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 build command (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.
.. code-block:: shell .. code-block:: shell
ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build 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 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 '[multilib]' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman-ahriman.conf echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman-ahriman.conf
@ -47,9 +50,19 @@ Initial setup
echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman-ahriman.conf 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 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]' | tee -a /etc/ahriman.ini.d/build.ini
echo 'build_command = ahriman-x86_64-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 '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 echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman chmod 400 /etc/sudoers.d/ahriman

View File

@ -1,16 +1,16 @@
Triggers 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 ``Trigger`` class and implement ``run`` method 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 Trigger example
--------------- ---------------
Lets consider example of reporting trigger (e.g. `slack <https://slack.com/>`_, which provides easy HTTP API for triggers for integrations). 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``). It can be retrieved from the same application instance. 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 As it has been mentioned, our trigger must derive from specific class:
.. code-block:: python .. code-block:: python
@ -24,9 +24,7 @@ As it has been mentioned, our trigger must derive from specific class
self.channel = configuration.get("slack", "channel") self.channel = configuration.get("slack", "channel")
self.username = configuration.get("slack", "username") self.username = configuration.get("slack", "username")
By now we have class with all required variables. By now we have class with all required variables. Lets implement run method. Slack API requires positing data with specific payload by HTTP, thus:
Lets implement run method. Slack API requires positing data with specific payload by HTTP, thus
.. code-block:: python .. code-block:: python
@ -39,7 +37,7 @@ Lets implement run method. Slack API requires positing data with specific payloa
response = requests.post(slack_url, data={"payload": json.dumps(payload)}) response = requests.post(slack_url, data={"payload": json.dumps(payload)})
response.raise_for_status() 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 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 .. code-block:: python
@ -52,7 +50,7 @@ Obviously you can implement the specified method in class, but for guide purpose
self.username = configuration.get("slack", "username") self.username = configuration.get("slack", "username")
def run(self, result, packages): def run(self, result, packages):
notify(result, self.slack_url, channel, username) notify(result, self.slack_url, self.channel, self.username)
Setup the trigger Setup the trigger
----------------- -----------------

View File

@ -1,9 +1,9 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=2.0.0rc11 pkgver=2.0.0
pkgrel=1 pkgrel=1
pkgdesc="ArcH Linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')
url="https://github.com/arcan1s/ahriman" url="https://github.com/arcan1s/ahriman"
license=('GPL3') license=('GPL3')

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] [Unit]
Description=ArcH Linux ReposItory MANager web server (%I architecture) Description=ArcH linux ReposItory MANager web server (%I architecture)
After=network.target After=network.target
[Service] [Service]

View File

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

View File

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

View File

@ -13,7 +13,7 @@ target = disabled
max_age = 604800 max_age = 604800
oauth_provider = GoogleClient oauth_provider = GoogleClient
oauth_scopes = https://www.googleapis.com/auth/userinfo.email oauth_scopes = https://www.googleapis.com/auth/userinfo.email
safe_build_status = yes allow_read_only = yes
[build] [build]
archbuild_flags = archbuild_flags =
@ -37,7 +37,6 @@ target = console
use_utf = yes use_utf = yes
[email] [email]
full_template_path = /usr/share/ahriman/templates/repo-index.jinja2
no_empty_report = yes no_empty_report = yes
template_path = /usr/share/ahriman/templates/email-index.jinja2 template_path = /usr/share/ahriman/templates/email-index.jinja2
ssl = disabled ssl = disabled

View File

@ -14,28 +14,29 @@
<div class="container"> <div class="container">
<h1>ahriman <h1>ahriman
{% if auth.authenticated %} <img id="badge-version" src="https://img.shields.io/badge/version-unknown-informational" alt="unknown">
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}"> <img id="badge-repository" src="https://img.shields.io/badge/repository-unknown-informational" alt="unknown">
<img src="https://img.shields.io/badge/repository-{{ repository | replace("-", "--") }}-informational" alt="{{ repository }}"> <img id="badge-architecture" src="https://img.shields.io/badge/architecture-unknown-informational" alt="unknown">
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}"> <img id="badge-status" src="https://img.shields.io/badge/service%20status-unknown-inactive" alt="unknown">
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
{% endif %}
</h1> </h1>
</div> </div>
<div class="container"> <div class="container">
<div id="toolbar"> <div id="toolbar">
{% if not auth.enabled or auth.username is not none %} {% 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"> <button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-form" hidden>
<i class="fa fa-plus"></i> add <i class="bi bi-plus"></i> add
</button> </button>
<button id="update" class="btn btn-secondary" onclick="updatePackages()" disabled> <button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" disabled hidden>
<i class="fa fa-play"></i> update <i class="bi bi-play"></i> update
</button> </button>
<button id="remove" class="btn btn-danger" onclick="removePackages()" disabled> <button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
<i class="fa fa-trash"></i> remove <i class="bi bi-trash"></i> remove
</button> </button>
{% endif %} {% endif %}
<button class="btn btn-secondary" onclick="reload()">
<i class="bi bi-arrow-clockwise"></i> reload
</button>
</div> </div>
<table id="packages" class="table table-striped table-hover" <table id="packages" class="table table-striped table-hover"
@ -53,42 +54,22 @@
data-show-fullscreen="true" data-show-fullscreen="true"
data-show-search-clear-button="true" data-show-search-clear-button="true"
data-sortable="true" data-sortable="true"
data-sort-reset="true" data-sort-name="base"
data-sort-order="asc"
data-toggle="table" data-toggle="table"
data-toolbar="#toolbar"> data-toolbar="#toolbar">
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>
<th data-checkbox="true"></th> <th data-checkbox="true"></th>
<th data-sortable="true" data-switchable="false">package base</th> <th data-sortable="true" data-switchable="false" data-field="base">package base</th>
<th data-sortable="true">version</th> <th data-sortable="true" data-field="version">version</th>
<th data-sortable="true">packages</th> <th data-sortable="true" data-field="packages">packages</th>
<th data-sortable="true" data-visible="false">groups</th> <th data-sortable="true" data-visible="false" data-field="groups">groups</th>
<th data-sortable="true" data-visible="false">licenses</th> <th data-sortable="true" data-visible="false" data-field="licenses">licenses</th>
<th data-sortable="true">last update</th> <th data-sortable="true" data-field="timestamp">last update</th>
<th data-sortable="true">status</th> <th data-sortable="true" data-cell-style="statusFormat" data-field="status">status</th>
</tr> </tr>
</thead> </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> </table>
</div> </div>
@ -110,7 +91,7 @@
{% if auth.username is none %} {% if auth.username is none %}
{{ auth.control|safe }} {{ auth.control|safe }}
{% else %} {% 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> <button class="btn btn-link" style="text-decoration: none">logout ({{ auth.username }})</button>
</form> </form>
{% endif %} {% endif %}
@ -122,11 +103,14 @@
{% include "build-status/login-modal.jinja2" %} {% include "build-status/login-modal.jinja2" %}
{% endif %} {% endif %}
{% include "build-status/package-actions-modals.jinja2" %}
{% include "utils/bootstrap-scripts.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> </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 id="loginForm" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<form action="/user-api/v1/login" method="post"> <form action="/api/v1/login" method="post">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">login</h4> <h4 class="modal-title">login</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
@ -26,4 +26,4 @@
</form> </form>
</div> </div>
</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,147 @@
<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().join("<br>"); };
const payload = response.map(description => {
const package_base = description.package.base;
const web_url = description.package.remote?.web_url;
return {
id: description.package.base,
base: web_url ? `<a href="${web_url}" title="${package_base}">${package_base}</a>` : package_base,
version: 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) => {
hideControls(true);
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%">${text}</td></tr>`);
table.bootstrapTable("hideLoading");
} else {
// other errors
showFailure(errorThrown);
}
},
});
$.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 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> <body>
<div class="container"> <div class="container">
<h1>Arch Linux user repository</h1> <h1>Arch linux user repository</h1>
</div> </div>
<div class="container"> <div class="container">
@ -40,7 +40,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
data-show-fullscreen="true" data-show-fullscreen="true"
data-show-search-clear-button="true" data-show-search-clear-button="true"
data-sortable="true" data-sortable="true"
data-sort-reset="true" data-sort-name="base"
data-sort-order="asc"
data-toggle="table"> data-toggle="table">
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>

View File

@ -0,0 +1,18 @@

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

View File

@ -4,12 +4,12 @@
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script> <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://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.18.3/dist/bootstrap-table.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.19.1/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.19.1/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 src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script> <script>
$("#packages").bootstrapTable({ $("#packages").bootstrapTable({

View File

@ -1,7 +1,7 @@
<script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script> <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 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-icons@1.7.2/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.19.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet"> <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 pathlib import Path
from setuptools import setup, find_packages from setuptools import find_packages, setup
from typing import Any, Dict from typing import Any, Dict
@ -15,7 +15,7 @@ setup(
version=metadata["__version__"], version=metadata["__version__"],
zip_safe=False, zip_safe=False,
description="ArcH Linux ReposItory MANager", description="ArcH linux ReposItory MANager",
author="ahriman team", author="ahriman team",
author_email="", author_email="",
@ -66,12 +66,15 @@ setup(
"package/share/ahriman/templates/build-status.jinja2", "package/share/ahriman/templates/build-status.jinja2",
"package/share/ahriman/templates/email-index.jinja2", "package/share/ahriman/templates/email-index.jinja2",
"package/share/ahriman/templates/repo-index.jinja2", "package/share/ahriman/templates/repo-index.jinja2",
"package/share/ahriman/templates/shell",
"package/share/ahriman/templates/telegram-index.jinja2", "package/share/ahriman/templates/telegram-index.jinja2",
]), ]),
("share/ahriman/templates/build-status", [ ("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/login-modal.jinja2",
"package/share/ahriman/templates/build-status/package-actions-modals.jinja2", "package/share/ahriman/templates/build-status/package-add-modal.jinja2",
"package/share/ahriman/templates/build-status/package-actions-script.jinja2", "package/share/ahriman/templates/build-status/success-modal.jinja2",
"package/share/ahriman/templates/build-status/table.jinja2",
]), ]),
("share/ahriman/templates/static", [ ("share/ahriman/templates/static", [
"package/share/ahriman/templates/static/favicon.ico", "package/share/ahriman/templates/static/favicon.ico",

View File

@ -26,6 +26,7 @@ from typing import List, TypeVar
from ahriman import version from ahriman import version
from ahriman.application import handlers from ahriman.application import handlers
from ahriman.core.util import enum_values
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
@ -63,7 +64,7 @@ def _parser() -> argparse.ArgumentParser:
Returns: Returns:
argparse.ArgumentParser: command line parser for the application 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.", epilog="Argument list can also be read from file by using @ prefix.",
fromfile_prefix_chars="@", formatter_class=_formatter) fromfile_prefix_chars="@", formatter_class=_formatter)
parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)", 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("-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", parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
action="store_true") 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) subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
@ -98,15 +99,19 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_config_parser(subparsers) _set_repo_config_parser(subparsers)
_set_repo_rebuild_parser(subparsers) _set_repo_rebuild_parser(subparsers)
_set_repo_remove_unknown_parser(subparsers) _set_repo_remove_unknown_parser(subparsers)
_set_repo_report_parser(subparsers)
_set_repo_restore_parser(subparsers) _set_repo_restore_parser(subparsers)
_set_repo_setup_parser(subparsers) _set_repo_setup_parser(subparsers)
_set_repo_sign_parser(subparsers) _set_repo_sign_parser(subparsers)
_set_repo_status_update_parser(subparsers) _set_repo_status_update_parser(subparsers)
_set_repo_sync_parser(subparsers)
_set_repo_triggers_parser(subparsers) _set_repo_triggers_parser(subparsers)
_set_repo_update_parser(subparsers) _set_repo_update_parser(subparsers)
_set_shell_parser(subparsers)
_set_user_add_parser(subparsers) _set_user_add_parser(subparsers)
_set_user_list_parser(subparsers) _set_user_list_parser(subparsers)
_set_user_remove_parser(subparsers) _set_user_remove_parser(subparsers)
_set_version_parser(subparsers)
_set_web_parser(subparsers) _set_web_parser(subparsers)
return parser return parser
@ -223,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("-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("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command", 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.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add) parser.set_defaults(handler=handlers.Add)
return parser return parser
@ -265,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("-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("-i", "--info", help="show additional package information", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status", 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) parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, quiet=True, unsafe=True)
return parser return parser
@ -307,7 +312,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen
"If no packages supplied, service status will be updated", "If no packages supplied, service status will be updated",
nargs="*") nargs="*")
parser.add_argument("-s", "--status", help="new status", 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, parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, quiet=True,
unsafe=True) unsafe=True)
return parser return parser
@ -495,6 +500,24 @@ def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentP
return parser return parser
def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for report subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-report", aliases=["report"], help="generate report",
description="generate repository report according to current settings",
epilog="Create and/or update repository report as configured.",
formatter_class=_formatter)
parser.set_defaults(handler=handlers.Triggers, trigger=["ahriman.core.report.ReportTrigger"])
return parser
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for repository restore subcommand add parser for repository restore subcommand
@ -536,7 +559,7 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--repository", help="repository name", required=True) parser.add_argument("--repository", help="repository name", required=True)
parser.add_argument("--sign-key", help="sign key id") parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", action="append", 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.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) parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, quiet=True, unsafe=True)
return parser return parser
@ -574,16 +597,34 @@ def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentPa
parser = root.add_parser("repo-status-update", help="update repository status", parser = root.add_parser("repo-status-update", help="update repository status",
description="update repository status on the status page", formatter_class=_formatter) description="update repository status on the status page", formatter_class=_formatter)
parser.add_argument("-s", "--status", help="new status", 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=[], parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, package=[],
quiet=True, unsafe=True) quiet=True, unsafe=True)
return parser return parser
def _set_repo_triggers_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for repository sync subcommand add parser for repository sync subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-sync", aliases=["sync"], help="sync repository",
description="sync repository files to remote server according to current settings",
epilog="Synchronize the repository to remote services as configured.",
formatter_class=_formatter)
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: Args:
root(SubParserAction): subparsers for the commands root(SubParserAction): subparsers for the commands
@ -593,6 +634,8 @@ def _set_repo_triggers_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("repo-triggers", help="run triggers", parser = root.add_parser("repo-triggers", help="run triggers",
description="run triggers on empty build result as configured by settings", description="run triggers on empty build result as configured by settings",
formatter_class=_formatter) 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) parser.set_defaults(handler=handlers.Triggers)
return parser return parser
@ -621,6 +664,24 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser 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: def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for create user subcommand add parser for create user subcommand
@ -640,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, " 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.") "which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-r", "--role", help="user access level", 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.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, parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture=[""], lock=None, no_report=True,
quiet=True, unsafe=True) quiet=True, unsafe=True)
@ -662,7 +723,7 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("username", help="filter users by username", nargs="?") 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("-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 parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, no_report=True, # nosec
password="", quiet=True, unsafe=True) password="", quiet=True, unsafe=True)
return parser return parser
@ -688,6 +749,23 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser 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: def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for web subcommand add parser for web subcommand

View File

@ -32,6 +32,7 @@ from ahriman.application.handlers.remove_unknown import RemoveUnknown
from ahriman.application.handlers.restore import Restore from ahriman.application.handlers.restore import Restore
from ahriman.application.handlers.search import Search from ahriman.application.handlers.search import Search
from ahriman.application.handlers.setup import Setup 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.sign import Sign
from ahriman.application.handlers.status import Status from ahriman.application.handlers.status import Status
from ahriman.application.handlers.status_update import StatusUpdate from ahriman.application.handlers.status_update import StatusUpdate
@ -39,4 +40,5 @@ from ahriman.application.handlers.triggers import Triggers
from ahriman.application.handlers.unsafe_commands import UnsafeCommands from ahriman.application.handlers.unsafe_commands import UnsafeCommands
from ahriman.application.handlers.update import Update from ahriman.application.handlers.update import Update
from ahriman.application.handlers.users import Users from ahriman.application.handlers.users import Users
from ahriman.application.handlers.versions import Versions
from ahriman.application.handlers.web import Web from ahriman.application.handlers.web import Web

View File

@ -0,0 +1,59 @@
#
# 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 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 Shell(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:
"""
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
"""
# 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 # we are using reporter here
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter
if args.ahriman: if args.ahriman:
ahriman = client.get_self() service_status = client.get_internal()
StatusPrinter(ahriman).print(args.info) StatusPrinter(service_status.status).print(args.info)
if args.package: if args.package:
packages: Iterable[Tuple[Package, BuildStatus]] = sum( packages: Iterable[Tuple[Package, BuildStatus]] = sum(
[client.get(base) for base in args.package], (client.get(base) for base in args.package),
start=[]) start=[])
else: else:
packages = client.get(None) packages = client.get(None)

View File

@ -45,4 +45,8 @@ class Triggers(Handler):
no_report(bool): force disable reporting 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
""" """
Application(architecture, configuration, no_report, unsafe).repository.process_triggers(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

@ -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

@ -37,7 +37,7 @@ class Auth:
enabled(bool): indicates if authorization is enabled enabled(bool): indicates if authorization is enabled
logger(logging.Logger): class logger logger(logging.Logger): class logger
max_age(int): session age in seconds. It will be used for both client side and server side checks 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: def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
@ -50,7 +50,7 @@ class Auth:
""" """
self.logger = logging.getLogger("http") 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.enabled = provider.is_enabled
self.max_age = configuration.getint("auth", "max_age", fallback=7 * 24 * 3600) self.max_age = configuration.getint("auth", "max_age", fallback=7 * 24 * 3600)
@ -89,7 +89,7 @@ class Auth:
return OAuth(configuration, database) return OAuth(configuration, database)
return cls(configuration) 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 validate user password
@ -103,7 +103,7 @@ class Auth:
del username, password del username, password
return True 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 check if user is known
@ -116,7 +116,7 @@ class Auth:
del username del username
return True 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 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") self.client_secret = configuration.get("auth", "client_secret")
# in order to use OAuth feature the service must be publicity available # in order to use OAuth feature the service must be publicity available
# thus we expect that address is set # 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")) self.provider = self.get_provider(configuration.get("auth", "oauth_provider"))
# it is list but we will have to convert to string it anyway # it is list but we will have to convert to string it anyway
self.scopes = configuration.get("auth", "oauth_scopes") self.scopes = configuration.get("auth", "oauth_scopes")
@ -69,7 +69,7 @@ class OAuth(Mapping):
Returns: Returns:
str: login control as html code to insert 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 @staticmethod
def get_provider(name: str) -> Type[aioauth_client.OAuth2Client]: def get_provider(name: str) -> Type[aioauth_client.OAuth2Client]:

View File

@ -220,7 +220,7 @@ class Configuration(configparser.RawConfigParser):
} }
# pylint and mypy are too stupid to find these methods # 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 getlist(self, *args: Any, **kwargs: Any) -> List[str]: ...
def getpath(self, *args: Any, **kwargs: Any) -> Path: ... 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 connection(Connection): database connection
paths(RepositoryPaths): repository paths instance 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: def insert_remote(base: str, remote: RemoteSource) -> None:
connection.execute( connection.execute(

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 # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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 sqlite3 import Connection
from typing import List, Optional 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 import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess

View File

@ -20,7 +20,7 @@
from sqlite3 import Connection from sqlite3 import Connection
from typing import List, Optional 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 from ahriman.models.package import Package

View File

@ -20,7 +20,7 @@
from sqlite3 import Connection from sqlite3 import Connection
from typing import Dict, Generator, Iterable, List, Tuple 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.build_status import BuildStatus
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription from ahriman.models.package_description import PackageDescription

View File

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

View File

@ -27,10 +27,7 @@ from typing import Type
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.migrations import Migrations from ahriman.core.database.migrations import Migrations
from ahriman.core.database.operations.auth_operations import AuthOperations from ahriman.core.database.operations import AuthOperations, BuildOperations, PackageOperations, PatchOperations
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
class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations): class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations):

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.status_printer import StatusPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter 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 continue
log_fn(f"\t{prop.name}{separator}{prop.value}") 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 convert content into printable data

View File

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

@ -85,7 +85,7 @@ class Email(Report, JinjaTemplate):
message = MIMEMultipart() message = MIMEMultipart()
message["From"] = self.sender message["From"] = self.sender
message["To"] = ", ".join(self.receivers) 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")) message.attach(MIMEText(text, "html"))
for filename, content in attachment.items(): for filename, content in attachment.items():

View File

@ -72,8 +72,6 @@ class UpdateHandler(Cleaner):
if local.is_outdated(remote, self.paths): if local.is_outdated(remote, self.paths):
self.reporter.set_pending(local.base) self.reporter.set_pending(local.base)
result.append(remote) result.append(remote)
else:
self.reporter.set_success(local)
except Exception: except Exception:
self.reporter.set_failed(local.base) self.reporter.set_failed(local.base)
self.logger.exception("could not load remote package %s", local.base) self.logger.exception("could not load remote package %s", local.base)
@ -103,8 +101,6 @@ class UpdateHandler(Cleaner):
elif local.is_outdated(remote, self.paths): elif local.is_outdated(remote, self.paths):
self.reporter.set_pending(local.base) self.reporter.set_pending(local.base)
result.append(remote) result.append(remote)
else:
self.reporter.set_success(local)
except Exception: except Exception:
self.logger.exception("could not process package at %s", dirname) self.logger.exception("could not process package at %s", dirname)

View File

@ -60,7 +60,7 @@ class Client:
status(BuildStatusEnum): current package build status status(BuildStatusEnum): current package build status
""" """
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]: # pylint: disable=no-self-use def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
""" """
get package status get package status
@ -73,23 +73,14 @@ class Client:
del base del base
return [] return []
def get_internal(self) -> InternalStatus: # pylint: disable=no-self-use def get_internal(self) -> InternalStatus:
""" """
get internal service status get internal service status
Returns: Returns:
InternalStatus: current internal (web) service status InternalStatus: current internal (web) service status
""" """
return InternalStatus() return InternalStatus(BuildStatus())
def get_self(self) -> BuildStatus: # pylint: disable=no-self-use
"""
get ahriman status itself
Returns:
BuildStatus: current ahriman status
"""
return BuildStatus()
def remove(self, base: str) -> None: def remove(self, base: str) -> None:
""" """

View File

@ -57,16 +57,6 @@ class WebClient(Client):
self.__session = requests.session() self.__session = requests.session()
self._login() self._login()
@property
def _ahriman_url(self) -> str:
"""
get url for the service status api
Returns:
str: full url for web service for ahriman service itself
"""
return f"{self.address}/status-api/v1/ahriman"
@property @property
def _login_url(self) -> str: def _login_url(self) -> str:
""" """
@ -75,7 +65,7 @@ class WebClient(Client):
Returns: Returns:
str: full url for web service to login str: full url for web service to login
""" """
return f"{self.address}/user-api/v1/login" return f"{self.address}/api/v1/login"
@property @property
def _status_url(self) -> str: def _status_url(self) -> str:
@ -85,7 +75,7 @@ class WebClient(Client):
Returns: Returns:
str: full url for web service for status str: full url for web service for status
""" """
return f"{self.address}/status-api/v1/status" return f"{self.address}/api/v1/status"
@staticmethod @staticmethod
def parse_address(configuration: Configuration) -> str: def parse_address(configuration: Configuration) -> str:
@ -136,7 +126,7 @@ class WebClient(Client):
Returns: Returns:
str: full url of web service for specific package base str: full url of web service for specific package base
""" """
return f"{self.address}/status-api/v1/packages/{base}" return f"{self.address}/api/v1/packages/{base}"
def add(self, package: Package, status: BuildStatusEnum) -> None: def add(self, package: Package, status: BuildStatusEnum) -> None:
""" """
@ -201,26 +191,7 @@ class WebClient(Client):
self.logger.exception("could not get web service status: %s", exception_response_text(e)) self.logger.exception("could not get web service status: %s", exception_response_text(e))
except Exception: except Exception:
self.logger.exception("could not get web service status") self.logger.exception("could not get web service status")
return InternalStatus() return InternalStatus(BuildStatus())
def get_self(self) -> BuildStatus:
"""
get ahriman status itself
Returns:
BuildStatus: current ahriman status
"""
try:
response = self.__session.get(self._ahriman_url)
response.raise_for_status()
status_json = response.json()
return BuildStatus.from_json(status_json)
except requests.HTTPError as e:
self.logger.exception("could not get service status: %s", exception_response_text(e))
except Exception:
self.logger.exception("could not get service status")
return BuildStatus()
def remove(self, base: str) -> None: def remove(self, base: str) -> None:
""" """
@ -265,7 +236,7 @@ class WebClient(Client):
payload = {"status": status.value} payload = {"status": status.value}
try: try:
response = self.__session.post(self._ahriman_url, json=payload) response = self.__session.post(self._status_url, json=payload)
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e: except requests.HTTPError as e:
self.logger.exception("could not update service status: %s", exception_response_text(e)) self.logger.exception("could not update service status: %s", exception_response_text(e))

View File

@ -71,7 +71,7 @@ class TriggerLoader:
self.configuration = configuration self.configuration = configuration
self.triggers = [ self.triggers = [
self._load_trigger(trigger) self.load_trigger(trigger)
for trigger in configuration.getlist("build", "triggers") for trigger in configuration.getlist("build", "triggers")
] ]
@ -104,6 +104,9 @@ class TriggerLoader:
Returns: Returns:
ModuleType: module loaded from the imported module ModuleType: module loaded from the imported module
Raises:
InvalidExtension: in case if module cannot be loaded from specified package
""" """
self.logger.info("load module from package %s", package) self.logger.info("load module from package %s", package)
try: try:
@ -111,7 +114,7 @@ class TriggerLoader:
except ModuleNotFoundError: except ModuleNotFoundError:
raise InvalidExtension(f"Module {package} not found") raise InvalidExtension(f"Module {package} not found")
def _load_trigger(self, module_path: str) -> Trigger: def load_trigger(self, module_path: str) -> Trigger:
""" """
load trigger by module path load trigger by module path
@ -119,13 +122,17 @@ class TriggerLoader:
module_path(str): module import path to load module_path(str): module import path to load
Returns: Returns:
Trigger: loaded trigger based on settings Trigger: loaded trigger based on settings
Raises:
InvalidExtension: in case if module cannot be loaded from the specified module path or is not a trigger
""" """
*package_path_parts, class_name = module_path.split(".") *package_path_parts, class_name = module_path.split(".")
package_or_path = ".".join(package_path_parts) package_or_path = ".".join(package_path_parts)
# it works for both missing permission and file does not exist # it works for both missing permission and file does not exist
if os.access(Path(package_or_path), os.R_OK): path_like = Path(package_or_path)
if os.access(path_like, os.R_OK) and path_like.is_file():
module = self._load_module_from_file(package_or_path, class_name) module = self._load_module_from_file(package_or_path, class_name)
else: else:
module = self._load_module_from_package(package_or_path) module = self._load_module_from_package(package_or_path)

View File

@ -18,7 +18,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import datetime import datetime
import io
import os import os
from enum import Enum
import requests import requests
import shutil import shutil
import subprocess import subprocess
@ -27,14 +30,14 @@ import tempfile
from contextlib import contextmanager from contextlib import contextmanager
from logging import Logger from logging import Logger
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Generator, Iterable, List, Optional, Union from typing import Any, Dict, Generator, IO, Iterable, List, Optional, Type, Union
from ahriman.core.exceptions import InvalidOption, UnsafeRun from ahriman.core.exceptions import InvalidOption, UnsafeRun
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
__all__ = ["check_output", "check_user", "exception_response_text", "filter_json", "full_version", "package_like", __all__ = ["check_output", "check_user", "exception_response_text", "filter_json", "full_version", "enum_values",
"pretty_datetime", "pretty_size", "tmpdir", "walk"] "package_like", "pretty_datetime", "pretty_size", "tmpdir", "walk"]
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None, def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
@ -73,6 +76,11 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
>>> check_output("false", exception=RuntimeError("An exception occurred")) >>> check_output("false", exception=RuntimeError("An exception occurred"))
""" """
# hack for Optional[IO[str]] handle
def get_io(proc: subprocess.Popen[str], channel_name: str) -> IO[str]:
channel: Optional[IO[str]] = getattr(proc, channel_name, None)
return channel if channel is not None else io.StringIO()
def log(single: str) -> None: def log(single: str) -> None:
if logger is not None: if logger is not None:
logger.debug(single) logger.debug(single)
@ -80,14 +88,15 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
# FIXME additional workaround for linter and type check which do not know that user arg is supported # FIXME additional workaround for linter and type check which do not know that user arg is supported
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
user=user, text=True, encoding="utf8", bufsize=1) as process: # type: ignore user=user, text=True, encoding="utf8", bufsize=1) as process:
if input_data is not None: if input_data is not None:
process.stdin.write(input_data) input_channel = get_io(process, "stdin")
process.stdin.close() input_channel.write(input_data)
input_channel.close()
# read stdout and append to output result # read stdout and append to output result
result: List[str] = [] result: List[str] = []
for line in iter(process.stdout.readline, ""): for line in iter(get_io(process, "stdout").readline, ""):
line = line.strip() line = line.strip()
if not line: # skip empty lines if not line: # skip empty lines
continue continue
@ -95,7 +104,7 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
log(line) log(line)
# read stderr and write info to logs # read stderr and write info to logs
for line in iter(process.stderr.readline, ""): for line in iter(get_io(process, "stderr").readline, ""):
log(line.strip()) log(line.strip())
process.terminate() # make sure that process is terminated process.terminate() # make sure that process is terminated
@ -134,6 +143,19 @@ def check_user(paths: RepositoryPaths, unsafe: bool) -> None:
raise UnsafeRun(current_uid, root_uid) raise UnsafeRun(current_uid, root_uid)
def enum_values(enum: Type[Enum]) -> List[str]:
"""
generate list of enumeration values from the source
Args:
enum(Type[Enum]): source enumeration class
Returns:
List[str]: available enumeration values as string
"""
return [key.value for key in enum]
def exception_response_text(exception: requests.exceptions.HTTPError) -> str: def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
""" """
safe response exception text generation safe response exception text generation

View File

@ -50,7 +50,7 @@ class AuthSettings(str, Enum):
""" """
if value.lower() in ("configuration", "mapping"): if value.lower() in ("configuration", "mapping"):
return cls.Configuration return cls.Configuration
if value.lower() in ('oauth', 'oauth2'): if value.lower() in ("oauth", "oauth2"):
return cls.OAuth return cls.OAuth
return cls.Disabled return cls.Disabled

View File

@ -46,40 +46,6 @@ class BuildStatusEnum(str, Enum):
Failed = "failed" Failed = "failed"
Success = "success" Success = "success"
def badges_color(self) -> str:
"""
convert itself to shield.io badges color
Returns:
str: shields.io color
"""
if self == BuildStatusEnum.Pending:
return "yellow"
if self == BuildStatusEnum.Building:
return "yellow"
if self == BuildStatusEnum.Failed:
return "critical"
if self == BuildStatusEnum.Success:
return "success"
return "inactive"
def bootstrap_color(self) -> str:
"""
converts itself to bootstrap color
Returns:
str: bootstrap color
"""
if self == BuildStatusEnum.Pending:
return "warning"
if self == BuildStatusEnum.Building:
return "warning"
if self == BuildStatusEnum.Failed:
return "danger"
if self == BuildStatusEnum.Success:
return "success"
return "secondary"
@dataclass @dataclass
class BuildStatus: class BuildStatus:

View File

@ -22,6 +22,7 @@ from __future__ import annotations
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from typing import Any, Dict, Optional, Type from typing import Any, Dict, Optional, Type
from ahriman.models.build_status import BuildStatus
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
@ -31,12 +32,14 @@ class InternalStatus:
internal server status internal server status
Attributes: Attributes:
status(BuildStatus): service status
architecture(Optional[str]): repository architecture architecture(Optional[str]): repository architecture
packages(Counters): packages statuses counter object packages(Counters): packages statuses counter object
repository(Optional[str]): repository name repository(Optional[str]): repository name
version(Optional[str]): service version version(Optional[str]): service version
""" """
status: BuildStatus
architecture: Optional[str] = None architecture: Optional[str] = None
packages: Counters = field(default=Counters(total=0)) packages: Counters = field(default=Counters(total=0))
repository: Optional[str] = None repository: Optional[str] = None
@ -54,7 +57,8 @@ class InternalStatus:
InternalStatus: internal status InternalStatus: internal status
""" """
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0) counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
return cls(architecture=dump.get("architecture"), return cls(status=BuildStatus.from_json(dump.get("status", {})),
architecture=dump.get("architecture"),
packages=counters, packages=counters,
repository=dump.get("repository"), repository=dump.get("repository"),
version=dump.get("version")) version=dump.get("version"))

View File

@ -85,7 +85,7 @@ class Package:
Returns: Returns:
List[str]: sum of dependencies per each package List[str]: sum of dependencies per each package
""" """
return sorted(set(sum([package.depends for package in self.packages.values()], start=[]))) return sorted(set(sum((package.depends for package in self.packages.values()), start=[])))
@property @property
def groups(self) -> List[str]: def groups(self) -> List[str]:
@ -95,7 +95,7 @@ class Package:
Returns: Returns:
List[str]: sum of groups per each package List[str]: sum of groups per each package
""" """
return sorted(set(sum([package.groups for package in self.packages.values()], start=[]))) return sorted(set(sum((package.groups for package in self.packages.values()), start=[])))
@property @property
def is_single_package(self) -> bool: def is_single_package(self) -> bool:
@ -130,7 +130,7 @@ class Package:
Returns: Returns:
List[str]: sum of licenses per each package List[str]: sum of licenses per each package
""" """
return sorted(set(sum([package.licenses for package in self.packages.values()], start=[]))) return sorted(set(sum((package.licenses for package in self.packages.values()), start=[])))
@classmethod @classmethod
def from_archive(cls: Type[Package], path: Path, pacman: Pacman, remote: Optional[RemoteSource]) -> Package: def from_archive(cls: Type[Package], path: Path, pacman: Pacman, remote: Optional[RemoteSource]) -> Package:

View File

@ -142,9 +142,7 @@ class User:
Returns: Returns:
bool: True in case if user is allowed to do this request and False otherwise bool: True in case if user is allowed to do this request and False otherwise
""" """
if self.access == UserAccess.Write: return self.access.permits(required)
return True # everything is allowed
return self.access == required
def __repr__(self) -> str: def __repr__(self) -> str:
""" """

View File

@ -17,6 +17,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from __future__ import annotations
from enum import Enum from enum import Enum
@ -25,12 +27,31 @@ class UserAccess(str, Enum):
web user access enumeration web user access enumeration
Attributes: Attributes:
Safe(UserAccess): (class attribute) user can access the page without authorization, Unauthorized(UserAccess): (class attribute) user can access specific resources which are marked as available
should not be used for user configuration without authorization (e.g. login, logout, static)
Read(UserAccess): (class attribute) user can read the page Read(UserAccess): (class attribute) user can read the page
Write(UserAccess): (class attribute) user can modify task and package list Reporter(UserAccess): (class attribute) user can read everything and is able to perform some modifications
Full(UserAccess): (class attribute) user has full access
""" """
Safe = "safe" Unauthorized = "unauthorized"
Read = "read" Read = "read"
Write = "write" Reporter = "reporter"
Full = "full"
def permits(self, other: UserAccess) -> bool:
"""
compare enumeration between each other and check if current permission allows the ``other``
Args:
other(UserAccess): other permission to compare
Returns:
bool: True in case if current permission allows the operation and False otherwise
"""
for member in UserAccess:
if member == other:
return True
if member == self:
return False
return False # must never happen

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
__version__ = "2.0.0rc11" __version__ = "2.0.0"

View File

@ -89,10 +89,13 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
return await self.validator.verify_access(user.username, permission, context) return await self.validator.verify_access(user.username, permission, context)
def auth_handler() -> MiddlewareType: def auth_handler(allow_read_only: bool) -> MiddlewareType:
""" """
authorization and authentication middleware authorization and authentication middleware
Args:
allow_read_only: allow
Returns: Returns:
MiddlewareType: built middleware MiddlewareType: built middleware
""" """
@ -102,10 +105,14 @@ def auth_handler() -> MiddlewareType:
permission = await permission_method(request) permission = await permission_method(request)
elif isinstance(handler, types.MethodType): # additional wrapper for static resources elif isinstance(handler, types.MethodType): # additional wrapper for static resources
handler_instance = getattr(handler, "__self__", None) handler_instance = getattr(handler, "__self__", None)
permission = UserAccess.Safe if isinstance(handler_instance, StaticResource) else UserAccess.Write permission = UserAccess.Unauthorized if isinstance(handler_instance, StaticResource) else UserAccess.Full
else:
permission = UserAccess.Full
if permission == UserAccess.Unauthorized: # explicit if elif else for better code coverage
pass
elif allow_read_only and UserAccess.Read.permits(permission):
pass
else: else:
permission = UserAccess.Write
if permission != UserAccess.Safe:
await aiohttp_security.check_permission(request, permission, request.path) await aiohttp_security.check_permission(request, permission, request.path)
return await handler(request) return await handler(request)
@ -133,6 +140,6 @@ def setup_auth(application: web.Application, validator: Auth) -> web.Application
identity_policy = aiohttp_security.SessionIdentityPolicy() identity_policy = aiohttp_security.SessionIdentityPolicy()
aiohttp_security.setup(application, identity_policy, authorization_policy) aiohttp_security.setup(application, identity_policy, authorization_policy)
application.middlewares.append(auth_handler()) application.middlewares.append(auth_handler(validator.allow_read_only))
return application return application

View File

@ -25,7 +25,6 @@ from ahriman.web.views.service.add import AddView
from ahriman.web.views.service.remove import RemoveView from ahriman.web.views.service.remove import RemoveView
from ahriman.web.views.service.request import RequestView from ahriman.web.views.service.request import RequestView
from ahriman.web.views.service.search import SearchView from ahriman.web.views.service.search import SearchView
from ahriman.web.views.status.ahriman import AhrimanView
from ahriman.web.views.status.package import PackageView from ahriman.web.views.status.package import PackageView
from ahriman.web.views.status.packages import PackagesView from ahriman.web.views.status.packages import PackagesView
from ahriman.web.views.status.status import StatusView from ahriman.web.views.status.status import StatusView
@ -42,34 +41,32 @@ def setup_routes(application: Application, static_path: Path) -> None:
Available routes are: Available routes are:
* GET / get build status page * ``GET /`` get build status page
* GET /index.html same as above * ``GET /index.html`` same as above
* POST /service-api/v1/add add new packages to repository * ``POST /api/v1/service/add`` add new packages to repository
* POST /service-api/v1/remove remove existing package from repository * ``POST /api/v1/service/remove`` remove existing package from repository
* POST /service-api/v1/request request to add new packages to repository * ``POST /api/v1/service/request`` request to add new packages to repository
* GET /service-api/v1/search search for substring in AUR * ``GET /api/v1/service/search`` search for substring in AUR
* POST /service-api/v1/update update packages in repository, actually it is just alias for add * ``POST /api/v1/service/update`` update packages in repository, actually it is just alias for add
* GET /status-api/v1/ahriman get current service status * ``GET /api/v1/packages`` get all known packages
* POST /status-api/v1/ahriman update service status * ``POST /api/v1/packages`` force update every package from repository
* GET /status-api/v1/packages get all known packages * ``DELETE /api/v1/package/:base`` delete package base from status page
* POST /status-api/v1/packages force update every package from repository * ``GET /api/v1/package/:base`` get package base status
* ``POST /api/v1/package/:base`` update package base status
* DELETE /status-api/v1/package/:base delete package base from status page * ``GET /api/v1/status`` get service status itself
* GET /status-api/v1/package/:base get package base status * ``POST /api/v1/status`` update service status itself
* POST /status-api/v1/package/:base update package base status
* GET /status-api/v1/status get web service status itself * ``GET /api/v1/login`` OAuth2 handler for login
* ``POST /api/v1/login`` login to service
* GET /user-api/v1/login OAuth2 handler for login * ``POST /api/v1/logout`` logout from service
* POST /user-api/v1/login login to service
* POST /user-api/v1/logout logout from service
Args: Args:
application(Application): web application instance application(Application): web application instance
@ -80,28 +77,26 @@ def setup_routes(application: Application, static_path: Path) -> None:
application.router.add_static("/static", static_path, follow_symlinks=True) application.router.add_static("/static", static_path, follow_symlinks=True)
application.router.add_post("/service-api/v1/add", AddView) application.router.add_post("/api/v1/service/add", AddView)
application.router.add_post("/service-api/v1/remove", RemoveView) application.router.add_post("/api/v1/service/remove", RemoveView)
application.router.add_post("/service-api/v1/request", RequestView) application.router.add_post("/api/v1/service/request", RequestView)
application.router.add_get("/service-api/v1/search", SearchView, allow_head=False) application.router.add_get("/api/v1/service/search", SearchView, allow_head=False)
application.router.add_post("/service-api/v1/update", AddView) application.router.add_post("/api/v1/service/update", AddView)
application.router.add_get("/status-api/v1/ahriman", AhrimanView, allow_head=True) application.router.add_get("/api/v1/packages", PackagesView, allow_head=True)
application.router.add_post("/status-api/v1/ahriman", AhrimanView) application.router.add_post("/api/v1/packages", PackagesView)
application.router.add_get("/status-api/v1/packages", PackagesView, allow_head=True) application.router.add_delete("/api/v1/packages/{package}", PackageView)
application.router.add_post("/status-api/v1/packages", PackagesView) application.router.add_get("/api/v1/packages/{package}", PackageView, allow_head=True)
application.router.add_post("/api/v1/packages/{package}", PackageView)
application.router.add_delete("/status-api/v1/packages/{package}", PackageView) application.router.add_get("/api/v1/status", StatusView, allow_head=True)
application.router.add_get("/status-api/v1/packages/{package}", PackageView, allow_head=True) application.router.add_post("/api/v1/status", StatusView)
application.router.add_post("/status-api/v1/packages/{package}", PackageView)
application.router.add_get("/status-api/v1/status", StatusView, allow_head=True) application.router.add_get("/api/v1/login", LoginView)
application.router.add_post("/api/v1/login", LoginView)
application.router.add_get("/user-api/v1/login", LoginView) application.router.add_post("/api/v1/logout", LogoutView)
application.router.add_post("/user-api/v1/login", LoginView)
application.router.add_post("/user-api/v1/logout", LogoutView)

View File

@ -101,7 +101,7 @@ class BaseView(View):
Returns: Returns:
UserAccess: extracted permission UserAccess: extracted permission
""" """
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Write) permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Full)
return permission return permission
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]: async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:

View File

@ -21,9 +21,7 @@ import aiohttp_jinja2
from typing import Any, Dict from typing import Any, Dict
from ahriman import version
from ahriman.core.auth.helpers import authorized_userid from ahriman.core.auth.helpers import authorized_userid
from ahriman.core.util import pretty_datetime
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -34,37 +32,19 @@ class IndexView(BaseView):
It uses jinja2 templates for report generation, the following variables are allowed: It uses jinja2 templates for report generation, the following variables are allowed:
* architecture - repository architecture, string, required
* auth - authorization descriptor, required * auth - authorization descriptor, required
* authenticated - alias to check if user can see the page, boolean, required
* control - HTML to insert for login control, HTML string, required * control - HTML to insert for login control, HTML string, required
* enabled - whether authorization is enabled by configuration or not, boolean, required * enabled - whether authorization is enabled by configuration or not, boolean, required
* username - authenticated username if any, string, null means not authenticated * username - authenticated username if any, string, null means not authenticated
* index_url - url to the repository index, string, optional * index_url - url to the repository index, string, optional
* packages - sorted list of packages properties, required
* base, string
* depends, sorted list of strings
* groups, sorted list of strings
* licenses, sorted list of strings
* packages, sorted list of strings
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
* version, string
* web_url, string
* repository - repository name, string, required * repository - repository name, string, required
* service - service status properties, required
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
* version - ahriman version, string, required
Attributes: Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
""" """
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Safe GET_PERMISSION = HEAD_PERMISSION = UserAccess.Unauthorized
@aiohttp_jinja2.template("build-status.jinja2") @aiohttp_jinja2.template("build-status.jinja2")
async def get(self) -> Dict[str, Any]: async def get(self) -> Dict[str, Any]:
@ -74,43 +54,15 @@ class IndexView(BaseView):
Returns: Returns:
Dict[str, Any]: parameters for jinja template Dict[str, Any]: parameters for jinja template
""" """
# some magic to make it jinja-friendly
packages = [
{
"base": package.base,
"depends": package.depends,
"groups": package.groups,
"licenses": package.licenses,
"packages": list(sorted(package.packages)),
"status": status.status.value,
"status_color": status.status.bootstrap_color(),
"timestamp": pretty_datetime(status.timestamp),
"version": package.version,
"web_url": package.remote.web_url if package.remote is not None else None,
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
]
service = {
"status": self.service.status.status.value,
"status_color": self.service.status.status.badges_color(),
"timestamp": pretty_datetime(self.service.status.timestamp),
}
# auth block
auth_username = await authorized_userid(self.request) auth_username = await authorized_userid(self.request)
authenticated = not self.validator.enabled or self.validator.safe_build_status or auth_username is not None
auth = { auth = {
"authenticated": authenticated,
"control": self.validator.auth_control, "control": self.validator.auth_control,
"enabled": self.validator.enabled, "enabled": self.validator.enabled,
"username": auth_username, "username": auth_username,
} }
return { return {
"architecture": self.service.architecture,
"auth": auth, "auth": auth,
"index_url": self.configuration.get("web", "index_url", fallback=None), "index_url": self.configuration.get("web", "index_url", fallback=None),
"packages": packages,
"repository": self.service.repository.name, "repository": self.service.repository.name,
"service": service,
"version": version.__version__,
} }

View File

@ -31,7 +31,7 @@ class AddView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
POST_PERMISSION = UserAccess.Write POST_PERMISSION = UserAccess.Full
async def post(self) -> None: async def post(self) -> None:
""" """

View File

@ -31,7 +31,7 @@ class RemoveView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
POST_PERMISSION = UserAccess.Write POST_PERMISSION = UserAccess.Full
async def post(self) -> None: async def post(self) -> None:
""" """

View File

@ -31,7 +31,7 @@ class RequestView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
POST_PERMISSION = UserAccess.Read POST_PERMISSION = UserAccess.Reporter
async def post(self) -> None: async def post(self) -> None:
""" """

View File

@ -35,7 +35,7 @@ class SearchView(BaseView):
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
""" """
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read GET_PERMISSION = HEAD_PERMISSION = UserAccess.Reporter
async def get(self) -> Response: async def get(self) -> Response:
""" """

View File

@ -1,71 +0,0 @@
#
# 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 aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class AhrimanView(BaseView):
"""
service status web view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Write
async def get(self) -> Response:
"""
get current service status
Returns:
Response: 200 with service status object
"""
return json_response(self.service.status.view())
async def post(self) -> None:
"""
update service status
JSON body must be supplied, the following model is used::
{
"status": "unknown", # service status string, must be valid ``BuildStatusEnum``
}
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
try:
data = await self.extract_data()
status = BuildStatusEnum(data["status"])
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.service.update_self(status)
raise HTTPNoContent()

View File

@ -37,7 +37,7 @@ class PackageView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Write DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
async def get(self) -> Response: async def get(self) -> Response:

View File

@ -34,7 +34,7 @@ class PackagesView(BaseView):
""" """
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Write POST_PERMISSION = UserAccess.Full
async def get(self) -> Response: async def get(self) -> Response:
""" """

View File

@ -17,9 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman import version from ahriman import version
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -33,9 +34,11 @@ class StatusView(BaseView):
Attributes: Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Full
async def get(self) -> Response: async def get(self) -> Response:
""" """
@ -46,9 +49,34 @@ class StatusView(BaseView):
""" """
counters = Counters.from_packages(self.service.packages) counters = Counters.from_packages(self.service.packages)
status = InternalStatus( status = InternalStatus(
status=self.service.status,
architecture=self.service.architecture, architecture=self.service.architecture,
packages=counters, packages=counters,
repository=self.service.repository.name, repository=self.service.repository.name,
version=version.__version__) version=version.__version__)
return json_response(status.view()) return json_response(status.view())
async def post(self) -> None:
"""
update service status
JSON body must be supplied, the following model is used::
{
"status": "unknown", # service status string, must be valid ``BuildStatusEnum``
}
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
try:
data = await self.extract_data()
status = BuildStatusEnum(data["status"])
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.service.update_self(status)
raise HTTPNoContent()

View File

@ -34,7 +34,7 @@ class LoginView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
GET_PERMISSION = POST_PERMISSION = UserAccess.Safe GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized
async def get(self) -> None: async def get(self) -> None:
""" """

View File

@ -32,7 +32,7 @@ class LogoutView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
POST_PERMISSION = UserAccess.Safe POST_PERMISSION = UserAccess.Unauthorized
async def post(self) -> None: async def post(self) -> None:
""" """

View File

@ -0,0 +1,48 @@
import argparse
import pytest
from pytest_mock import MockerFixture
from ahriman.application.handlers import Shell
from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.verbose = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("code.interact")
Shell.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int))
def test_run_verbose(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with verbose option
"""
args = _default_args(args)
args.verbose = True
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
application_mock = mocker.patch("code.interact")
Shell.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int))
print_mock.assert_called_once_with(verbose=False)

View File

@ -34,7 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, package_ahr
""" """
args = _default_args(args) args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.core.status.client.Client.get_self") application_mock = mocker.patch("ahriman.core.status.client.Client.get_internal")
packages_mock = mocker.patch("ahriman.core.status.client.Client.get", packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)), return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))]) (package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
@ -55,7 +55,7 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
args = _default_args(args) args = _default_args(args)
args.exit_code = True args.exit_code = True
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.status.client.Client.get_self") mocker.patch("ahriman.core.status.client.Client.get_internal")
mocker.patch("ahriman.core.status.client.Client.get", return_value=[]) mocker.patch("ahriman.core.status.client.Client.get", return_value=[])
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")

View File

@ -4,15 +4,48 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Triggers from ahriman.application.handlers import Triggers
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.trigger = []
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.core.repository.Repository.process_triggers") application_mock = mocker.patch("ahriman.core.repository.Repository.process_triggers")
Triggers.run(args, "x86_64", configuration, True, False) Triggers.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(Result()) application_mock.assert_called_once_with(Result())
def test_run_trigger(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must run triggers specified by command line
"""
args = _default_args(args)
args.trigger = ["ahriman.core.report.ReportTrigger"]
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.run")
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.run")
Triggers.run(args, "x86_64", configuration, True, False)
report_mock.assert_called_once_with(Result(), [package_ahriman])
upload_mock.assert_not_called()

View File

@ -28,7 +28,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.as_service = False args.as_service = False
args.exit_code = False args.exit_code = False
args.password = "pa55w0rd" args.password = "pa55w0rd"
args.role = UserAccess.Read args.role = UserAccess.Reporter
args.secure = False args.secure = False
return args return args

View File

@ -0,0 +1,38 @@
import argparse
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.application.handlers import Versions
from ahriman.core.configuration import Configuration
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
application_mock = mocker.patch("ahriman.application.handlers.Versions.package_dependencies")
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
Versions.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with("ahriman", ("pacman", "s3", "web"))
print_mock.assert_has_calls([mock.call(verbose=False, separator=" "), mock.call(verbose=False, separator=" ")])
def test_package_dependencies() -> None:
"""
must extract package dependencies
"""
packages = Versions.package_dependencies("srcinfo")
assert packages
assert packages.get("parse") is not None
def test_package_dependencies_missing() -> None:
"""
must extract package dependencies even if some of them are missing
"""
packages = Versions.package_dependencies("ahriman", ("docs", "pacman", "s3", "web"))
assert packages
assert packages.get("pyalpm") is not None
assert packages.get("Sphinx") is None

View File

@ -348,6 +348,24 @@ def test_subparsers_repo_remove_unknown_architecture(parser: argparse.ArgumentPa
assert args.architecture == ["x86_64"] assert args.architecture == ["x86_64"]
def test_subparsers_repo_report(parser: argparse.ArgumentParser) -> None:
"""
repo-report command must imply trigger
"""
args = parser.parse_args(["repo-report"])
assert args.trigger == ["ahriman.core.report.ReportTrigger"]
def test_subparsers_repo_report_architecture(parser: argparse.ArgumentParser) -> None:
"""
repo-report command must correctly parse architecture list
"""
args = parser.parse_args(["repo-report"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-report"])
assert args.architecture == ["x86_64"]
def test_subparsers_repo_restore(parser: argparse.ArgumentParser) -> None: def test_subparsers_repo_restore(parser: argparse.ArgumentParser) -> None:
""" """
repo-restore command must imply architecture list, lock, no-report and unsafe repo-restore command must imply architecture list, lock, no-report and unsafe
@ -436,6 +454,24 @@ def test_subparsers_repo_status_update_option_status(parser: argparse.ArgumentPa
assert isinstance(args.status, BuildStatusEnum) assert isinstance(args.status, BuildStatusEnum)
def test_subparsers_repo_sync(parser: argparse.ArgumentParser) -> None:
"""
repo-sync command must imply trigger
"""
args = parser.parse_args(["repo-sync"])
assert args.trigger == ["ahriman.core.upload.UploadTrigger"]
def test_subparsers_repo_sync_architecture(parser: argparse.ArgumentParser) -> None:
"""
repo-sync command must correctly parse architecture list
"""
args = parser.parse_args(["repo-report"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-report"])
assert args.architecture == ["x86_64"]
def test_subparsers_repo_triggers_architecture(parser: argparse.ArgumentParser) -> None: def test_subparsers_repo_triggers_architecture(parser: argparse.ArgumentParser) -> None:
""" """
repo-triggers command must correctly parse architecture list repo-triggers command must correctly parse architecture list
@ -456,6 +492,15 @@ def test_subparsers_repo_update_architecture(parser: argparse.ArgumentParser) ->
assert args.architecture == ["x86_64"] assert args.architecture == ["x86_64"]
def test_subparsers_shell(parser: argparse.ArgumentParser) -> None:
"""
shell command must imply lock and no-report
"""
args = parser.parse_args(["shell"])
assert args.lock is None
assert args.no_report
def test_subparsers_user_add(parser: argparse.ArgumentParser) -> None: def test_subparsers_user_add(parser: argparse.ArgumentParser) -> None:
""" """
user-add command must imply action, architecture, lock, no-report, quiet and unsafe user-add command must imply action, architecture, lock, no-report, quiet and unsafe
@ -483,7 +528,7 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non
""" """
args = parser.parse_args(["user-add", "username"]) args = parser.parse_args(["user-add", "username"])
assert isinstance(args.role, UserAccess) assert isinstance(args.role, UserAccess)
args = parser.parse_args(["user-add", "username", "--role", "write"]) args = parser.parse_args(["user-add", "username", "--role", "full"])
assert isinstance(args.role, UserAccess) assert isinstance(args.role, UserAccess)
@ -513,7 +558,7 @@ def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> No
""" """
user-list command must convert role option to useraccess instance user-list command must convert role option to useraccess instance
""" """
args = parser.parse_args(["user-list", "--role", "write"]) args = parser.parse_args(["user-list", "--role", "full"])
assert isinstance(args.role, UserAccess) assert isinstance(args.role, UserAccess)
@ -539,6 +584,26 @@ def test_subparsers_user_remove_architecture(parser: argparse.ArgumentParser) ->
assert args.architecture == [""] assert args.architecture == [""]
def test_subparsers_version(parser: argparse.ArgumentParser) -> None:
"""
version command must imply architecture, lock, no-report, quiet and unsafe
"""
args = parser.parse_args(["version"])
assert args.architecture == [""]
assert args.lock is None
assert args.no_report
assert args.quiet
assert args.unsafe
def test_subparsers_version_architecture(parser: argparse.ArgumentParser) -> None:
"""
version command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "version"])
assert args.architecture == [""]
def test_subparsers_web(parser: argparse.ArgumentParser) -> None: def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
""" """
web command must imply lock, no_report and parser web command must imply lock, no_report and parser

View File

@ -8,7 +8,7 @@ from unittest import mock
from ahriman import version from ahriman import version
from ahriman.application.lock import Lock from ahriman.application.lock import Lock
from ahriman.core.exceptions import DuplicateRun, UnsafeRun from ahriman.core.exceptions import DuplicateRun, UnsafeRun
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
@ -57,7 +57,7 @@ def test_check_version(lock: Lock, mocker: MockerFixture) -> None:
must check version correctly must check version correctly
""" """
mocker.patch("ahriman.core.status.client.Client.get_internal", mocker.patch("ahriman.core.status.client.Client.get_internal",
return_value=InternalStatus(version=version.__version__)) return_value=InternalStatus(status=BuildStatus(), version=version.__version__))
logging_mock = mocker.patch("logging.Logger.warning") logging_mock = mocker.patch("logging.Logger.warning")
lock.check_version() lock.check_version()
@ -69,7 +69,7 @@ def test_check_version_mismatch(lock: Lock, mocker: MockerFixture) -> None:
must check mismatched version correctly must check mismatched version correctly
""" """
mocker.patch("ahriman.core.status.client.Client.get_internal", mocker.patch("ahriman.core.status.client.Client.get_internal",
return_value=InternalStatus(version="version")) return_value=InternalStatus(status=BuildStatus(), version="version"))
logging_mock = mocker.patch("logging.Logger.warning") logging_mock = mocker.patch("logging.Logger.warning")
lock.check_version() lock.check_version()

View File

@ -101,7 +101,7 @@ def aur_package_ahriman() -> AURPackage:
package_base_id=165427, package_base_id=165427,
package_base="ahriman", package_base="ahriman",
version="1.7.0-1", version="1.7.0-1",
description="ArcH Linux ReposItory MANager", description="ArcH linux ReposItory MANager",
num_votes=0, num_votes=0,
popularity=0, popularity=0,
first_submitted=datetime.datetime.utcfromtimestamp(1618008285), first_submitted=datetime.datetime.utcfromtimestamp(1618008285),
@ -299,7 +299,7 @@ def package_description_ahriman() -> PackageDescription:
"python-passlib", "python-passlib",
"python-srcinfo", "python-srcinfo",
], ],
description="ArcH Linux ReposItory MANager", description="ArcH linux ReposItory MANager",
filename="ahriman-1.7.0-1-any.pkg.tar.zst", filename="ahriman-1.7.0-1-any.pkg.tar.zst",
groups=[], groups=[],
installed_size=4200000, installed_size=4200000,
@ -426,7 +426,7 @@ def user() -> User:
Returns: Returns:
User: user descriptor instance User: user descriptor instance
""" """
return User("user", "pa55w0rd", UserAccess.Read) return User("user", "pa55w0rd", UserAccess.Reporter)
@pytest.fixture @pytest.fixture

View File

@ -71,4 +71,4 @@ async def test_verify_access(auth: Auth, user: User) -> None:
must allow any access must allow any access
""" """
assert await auth.verify_access(user.username, user.access, None) assert await auth.verify_access(user.username, user.access, None)
assert await auth.verify_access(user.username, UserAccess.Write, None) assert await auth.verify_access(user.username, UserAccess.Full, None)

View File

@ -79,4 +79,4 @@ async def test_verify_access(mapping: Mapping, user: User, mocker: MockerFixture
""" """
mocker.patch("ahriman.core.database.SQLite.user_get", return_value=user) mocker.patch("ahriman.core.database.SQLite.user_get", return_value=user)
assert await mapping.verify_access(user.username, user.access, None) assert await mapping.verify_access(user.username, user.access, None)
assert not await mapping.verify_access(user.username, UserAccess.Write, None) assert not await mapping.verify_access(user.username, UserAccess.Full, None)

View File

@ -14,7 +14,7 @@ def test_migrate_package_remotes(package_ahriman: Package, connection: Connectio
must put package remotes to database must put package remotes to database
""" """
mocker.patch( mocker.patch(
"ahriman.core.database.operations.package_operations.PackageOperations._packages_get_select_package_bases", "ahriman.core.database.operations.PackageOperations._packages_get_select_package_bases",
return_value={package_ahriman.base: package_ahriman}) return_value={package_ahriman.base: package_ahriman})
mocker.patch("pathlib.Path.exists", return_value=False) mocker.patch("pathlib.Path.exists", return_value=False)
@ -28,7 +28,7 @@ def test_migrate_package_remotes_has_local(package_ahriman: Package, connection:
must skip processing for packages which have local cache must skip processing for packages which have local cache
""" """
mocker.patch( mocker.patch(
"ahriman.core.database.operations.package_operations.PackageOperations._packages_get_select_package_bases", "ahriman.core.database.operations.PackageOperations._packages_get_select_package_bases",
return_value={package_ahriman.base: package_ahriman}) return_value={package_ahriman.base: package_ahriman})
mocker.patch("pathlib.Path.exists", return_value=True) mocker.patch("pathlib.Path.exists", return_value=True)
@ -42,7 +42,7 @@ def test_migrate_package_remotes_vcs(package_ahriman: Package, connection: Conne
must process VCS packages with local cache must process VCS packages with local cache
""" """
mocker.patch( mocker.patch(
"ahriman.core.database.operations.package_operations.PackageOperations._packages_get_select_package_bases", "ahriman.core.database.operations.PackageOperations._packages_get_select_package_bases",
return_value={package_ahriman.base: package_ahriman}) return_value={package_ahriman.base: package_ahriman})
mocker.patch("pathlib.Path.exists", return_value=True) mocker.patch("pathlib.Path.exists", return_value=True)
mocker.patch.object(Package, "is_vcs", True) mocker.patch.object(Package, "is_vcs", True)
@ -57,7 +57,7 @@ def test_migrate_package_remotes_no_remotes(package_ahriman: Package, connection
must skip processing in case if no remotes generated (should never happen) must skip processing in case if no remotes generated (should never happen)
""" """
mocker.patch( mocker.patch(
"ahriman.core.database.operations.package_operations.PackageOperations._packages_get_select_package_bases", "ahriman.core.database.operations.PackageOperations._packages_get_select_package_bases",
return_value={package_ahriman.base: package_ahriman}) return_value={package_ahriman.base: package_ahriman})
mocker.patch("pathlib.Path.exists", return_value=False) mocker.patch("pathlib.Path.exists", return_value=False)
mocker.patch("ahriman.models.remote_source.RemoteSource.from_source", return_value=None) mocker.patch("ahriman.models.remote_source.RemoteSource.from_source", return_value=None)

View File

@ -0,0 +1,8 @@
from ahriman.core.database.migrations.m002_user_access import steps
def test_migration_package_source() -> None:
"""
migration must not be empty
"""
assert steps

View File

@ -29,7 +29,7 @@ def test_user_list_filter_by_username(database: SQLite) -> None:
must return users filtered by its id must return users filtered by its id
""" """
first = User("1", "", UserAccess.Read) first = User("1", "", UserAccess.Read)
second = User("2", "", UserAccess.Write) second = User("2", "", UserAccess.Full)
third = User("3", "", UserAccess.Read) third = User("3", "", UserAccess.Read)
database.user_update(first) database.user_update(first)
@ -46,7 +46,7 @@ def test_user_list_filter_by_access(database: SQLite) -> None:
must return users filtered by its access must return users filtered by its access
""" """
first = User("1", "", UserAccess.Read) first = User("1", "", UserAccess.Read)
second = User("2", "", UserAccess.Write) second = User("2", "", UserAccess.Full)
third = User("3", "", UserAccess.Read) third = User("3", "", UserAccess.Read)
database.user_update(first) database.user_update(first)
@ -64,7 +64,7 @@ def test_user_list_filter_by_username_access(database: SQLite) -> None:
must return users filtered by its access and username must return users filtered by its access and username
""" """
first = User("1", "", UserAccess.Read) first = User("1", "", UserAccess.Read)
second = User("2", "", UserAccess.Write) second = User("2", "", UserAccess.Full)
third = User("3", "", UserAccess.Read) third = User("3", "", UserAccess.Read)
database.user_update(first) database.user_update(first)
@ -72,7 +72,7 @@ def test_user_list_filter_by_username_access(database: SQLite) -> None:
database.user_update(third) database.user_update(third)
assert database.user_list("1", UserAccess.Read) == [first] assert database.user_list("1", UserAccess.Read) == [first]
assert not database.user_list("1", UserAccess.Write) assert not database.user_list("1", UserAccess.Full)
def test_user_remove_update(database: SQLite, user: User) -> None: def test_user_remove_update(database: SQLite, user: User) -> None:
@ -92,6 +92,6 @@ def test_user_update(database: SQLite, user: User) -> None:
assert database.user_get(user.username) == user assert database.user_get(user.username) == user
new_user = user.hash_password("salt") new_user = user.hash_password("salt")
new_user.access = UserAccess.Write new_user.access = UserAccess.Full
database.user_update(new_user) database.user_update(new_user)
assert database.user_get(new_user.username) == new_user assert database.user_get(new_user.username) == new_user

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