Compare commits

...

34 Commits
1.4.0 ... 1.6.0

Author SHA1 Message Date
fcb130e226 Release 1.6.0 2021-10-27 01:59:36 +03:00
ae99fe4535 drop no-quiet option and change tree_create message error to warn 2021-10-27 01:57:54 +03:00
ec23e3f912 remove help sample from readme because it changes faster than om able to maintain it 2021-10-26 04:53:45 +03:00
d3ea81d234 unify aur.search method
due to specific of the AUR API in order to reduce the code we are using
own wrapper and work with it instead of direct library calls
2021-10-26 04:49:55 +03:00
09b0f2914d Add ability to show more info in search and status subcommands
This feature also introduces the followiing changes
* aur-search command now works as expected with multiterms
* printer classes for managing of data print
* --sort-by argument for aur-search subcommand instead of using package
  name
* --quiet argument now has also --no-quite option
* if --quite is supplied, the log level will be set to warn instead of
  critical to be able to see error messages
* pretty_datetime function now also supports datetime objects
* BuildStatus is now pure dataclass
2021-10-26 04:27:36 +03:00
7351e20104 always update environnment before any action 2021-10-24 04:14:57 +03:00
dfd87c502f split application class into traits 2021-10-23 13:44:57 +03:00
0b9ab09879 add patches to clean command 2021-10-20 03:22:16 +03:00
47c54f0b40 add ability to download package from external links (e.g. HTTP) 2021-10-20 03:09:58 +03:00
a2f2fa0354 add ability to read argument list from file 2021-10-20 02:15:59 +03:00
4d68080c05 logger improvements
* remove build log since it has no usages actually (replaced by root
  logger)
* decrease boto3 log levels to INFO by default to reduce noice
2021-10-20 02:12:49 +03:00
eb16ef12f3 always return json in responses 2021-10-20 02:12:39 +03:00
e10e362dae Release 1.5.0 2021-10-18 03:48:24 +03:00
e59fdd1ccc minor architecture description update 2021-10-18 03:35:39 +03:00
22d92e3b4e add repo-status-update subcommand 2021-10-17 06:38:49 +03:00
56b77a84a6 allow to use multiple upload and report targets with the same name
In this feature target option must allways point to section name instead
of type. Type will be read from type option. In case if type option is
not presented it will try to check if section with architecture exists
(e.g. target = email, section = email:x86_64); if it does, the correct
section name and type will be used. Otherwise it will check if the
specified section exists; if it does, seection name and type will be
returned.
2021-10-17 06:06:08 +03:00
a5a99ec0b8 split github upload into generic http method and github specific
We might use some features from the http upload for another parser
2021-10-15 23:36:26 +03:00
04bbabe898 docs update 2021-10-15 04:55:46 +03:00
4521c2adde disallow to create tree in case of unsafe run 2021-10-14 04:53:09 +03:00
5c5e54228f use generic removal method 2021-10-14 04:08:21 +03:00
6514924b2d change method spelling
in order to sort method correctly we are going to use the following
namiing schema:

{subject}_{action}_{details}

This schema still have some exceptions, e.g. single word methods, bool
methods (is_) and getters in case if they are singular (i.e. there is
no any other method with this subject)
2021-10-14 04:01:54 +03:00
16aa977fa8 add test for every file 2021-10-14 03:34:12 +03:00
6e377e7261 aggressive small case 2021-10-14 03:13:15 +03:00
4502931c39 exactly one called with instead of last call check 2021-10-14 03:12:45 +03:00
fcb167b1a3 github upload support (#41) 2021-10-14 02:30:13 +03:00
72b26603bf add ability to add manually stored packages (#40)
* add ability to add manually stored packages

* update tests

* handle manual packages in remove-unknown method

* live fixes

also rename branches to has_remotes method and change return type
2021-10-12 21:15:35 +03:00
ab8ca16981 improve scripts
move logic to separated shell scripts and also create shell script for
repository setup

Also force create directory according to systemd recommendations
2021-10-11 02:20:16 +03:00
7c4f84fbc7 more verbose help messages 2021-10-07 01:56:22 +03:00
3b6b2efcb1 patch control subcommands 2021-10-05 08:57:42 +03:00
9f99dd3ff2 patch support (#35) 2021-10-03 15:20:36 +03:00
bee97df87f do not write anything on httpexceptions in log 2021-10-03 02:05:28 +03:00
6becd01803 replace no-log with quiet
Also behavior of the flag has been changed: now it disables logs at all
2021-10-03 01:59:33 +03:00
db195391e4 Release 1.4.1 2021-10-03 01:02:41 +03:00
59f2992559 do not use set_defaults for architecture arguments
according to the source code defaults always updates the values
dictionary. This in this specific case it is impossible to override the
value it will be always empty list.

In order to handle it we are adding another property to the Handler
class which allows to run with None architecture list.

This particular set_defaults behaviour is still useful for other cases
when we have to run command without any specific architecture
2021-10-03 00:59:24 +03:00
180 changed files with 9537 additions and 5037 deletions

21
.github/workflows/run-setup.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: setup
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
run-setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: setup the service in arch linux container
run: |
docker run \
-v ${{ github.workspace }}:/build -w /build \
archlinux:latest \
.github/workflows/setup.sh

View File

@ -18,9 +18,4 @@ jobs:
docker run \
-v ${{ github.workspace }}:/build -w /build \
archlinux:latest \
/bin/bash -c "pacman --noconfirm -Syu base-devel python-argparse-manpage python-pip && \
pip install -e .[web] && \
pip install -e .[check] && \
pip install -e .[s3] && \
pip install -e .[test] && \
make check tests"
.github/workflows/tests.sh

49
.github/workflows/setup.sh vendored Executable file
View File

@ -0,0 +1,49 @@
#!/bin/bash
# Install the package and run main install commands
set -ex
# install dependencies
echo -e '[arcanisrepo]\nServer = http://repo.arcanis.me/$arch\nSigLevel = Never' | tee -a /etc/pacman.conf
# refresh the image
pacman --noconfirm -Syu
# main dependencies
pacman --noconfirm -Sy base-devel devtools git pyalpm python-aur python-passlib python-srcinfo sudo
# make dependencies
pacman --noconfirm -Sy python-pip
# optional dependencies
# VCS support
pacman --noconfirm -Sy breezy darcs mercurial subversion
# web server
pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
# additional features
pacman --noconfirm -Sy gnupg python-boto3 rsync
# create fresh tarball
make VERSION=1.0.0 archlinux # well, it does not really matter which version we will put here
# run makepkg
mv ahriman-*-src.tar.xz package/archlinux
chmod +777 package/archlinux # because fuck you that's why
cd package/archlinux
sudo -u nobody makepkg -cf --skipchecksums --noconfirm
pacman --noconfirm -U ahriman-1.0.0-1-any.pkg.tar.zst
# special thing for the container, because /dev/log interface is not available there
sed -i 's/handlers = syslog_handler/handlers = console_handler/g' /etc/ahriman.ini.d/logging.ini
# initial setup command as root
sudo -u ahriman ahriman -a x86_64 init
ahriman -a x86_64 repo-setup --packager "ahriman bot <ahriman@example.com>" --repository "github" --web-port 8080
# enable services
systemctl enable ahriman-web@x86_64
systemctl enable ahriman@x86_64.timer
# run web service (detached)
sudo -u ahriman ahriman -a x86_64 web &
WEBPID=$!
sleep 15s # wait for the web service activation
# add the first package
# the build itself does not really work in the container because it requires procfs
sudo -u ahriman ahriman package-add yay
# run package check
sudo -u ahriman ahriman repo-update
# stop web service lol
kill $WEBPID

16
.github/workflows/tests.sh vendored Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
# Install dependencies and run test in container
set -ex
# install dependencies
pacman --noconfirm -Syu base-devel python-pip
# install python packages
pip install -e .[web]
pip install -e .[check]
pip install -e .[s3]
pip install -e .[test]
# run test and check targets
make check tests

View File

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

View File

@ -1,6 +1,7 @@
# ArcH Linux ReposItory MANager
[![build status](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
[![tests status](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
[![setup status](https://github.com/arcan1s/ahriman/actions/workflows/run-setup.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/run-setup.yml)
[![CodeFactor](https://www.codefactor.io/repository/github/arcan1s/ahriman/badge)](https://www.codefactor.io/repository/github/arcan1s/ahriman)
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
@ -11,62 +12,19 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
* Multi-architecture support.
* VCS packages support.
* Sign support with gpg (repository, package, per package settings).
* Synchronization to remote services (rsync, s3) and report generation (html).
* Synchronization to remote services (rsync, s3 and github) and report generation (html).
* Dependency manager.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Repository status interface with optional authorization and control options:
![web interface](web.png)
## Installation and run
For installation details please refer to the [documentation](docs/setup.md). For command help, `--help` subcommand must be used, e.g.:
```shell
$ ahriman --help
usage: ahriman [-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-log] [--no-report] [--unsafe] [-v]
{add,check,clean,config,create-user,init,key-import,rebuild,remove,remove-unknown,report,search,setup,sign,status,status-update,sync,update,web} ...
ArcH Linux ReposItory MANager
optional arguments:
-h, --help show this help message and exit
-a ARCHITECTURE, --architecture ARCHITECTURE
target architectures (can be used multiple times) (default: None)
-c CONFIGURATION, --configuration CONFIGURATION
configuration path (default: /etc/ahriman.ini)
--force force run, remove file lock (default: False)
-l LOCK, --lock LOCK lock file (default: /tmp/ahriman.lock)
--no-log redirect all log messages to stderr (default: False)
--no-report force disable reporting to web service (default: False)
--unsafe allow to run ahriman as non-ahriman user (default: False)
-v, --version show program's version number and exit
command:
{add,check,clean,config,create-user,init,key-import,rebuild,remove,remove-unknown,report,search,setup,sign,status,status-update,sync,update,web}
command to run
add add package
check check for updates
clean clean local caches
config dump configuration
create-user create user for web services
init create repository tree
key-import import PGP key
rebuild rebuild repository
remove remove package
remove-unknown remove unknown packages
report generate report
search search for package
setup initial service configuration
sign sign packages
status get package status
status-update update package status
sync sync repository
update update packages
web start web server
```
Subcommands have own help message as well.
For installation details please refer to the [documentation](docs/setup.md). 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).
## Configuration
Every available option is described in the [documentation](docs/configuration.md).
## [FAQ](docs/faq.md)

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 388 KiB

View File

@ -3,7 +3,7 @@
ahriman
.SH SYNOPSIS
.B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-log] [--no-report] [--unsafe] [-v] {add,check,clean,config,init,key-import,rebuild,remove,remove-unknown,report,search,setup,sign,status,status-update,sync,update,user,web} ...
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,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-check,check,repo-clean,clean,repo-config,config,repo-init,init,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-setup,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-update,update,user-add,user-remove,web} ...
.SH DESCRIPTION
ArcH Linux ReposItory MANager
.SH OPTIONS
@ -24,17 +24,17 @@ force run, remove file lock
\fB\-l\fR \fI\,LOCK\/\fR, \fB\-\-lock\fR \fI\,LOCK\/\fR
lock file
.TP
\fB\-\-no\-log\fR
redirect all log messages to stderr
.TP
\fB\-\-no\-report\fR
force disable reporting to web service
.TP
\fB\-q\fR, \fB\-\-quiet\fR
force disable any logging
.TP
\fB\-\-unsafe\fR
allow to run ahriman as non\-ahriman user
allow to run ahriman as non\-ahriman user. Some actions might be unavailable
.TP
\fB\-v\fR, \fB\-\-version\fR
@ -43,140 +43,124 @@ show program's version number and exit
.SS
\fBSub-commands\fR
.TP
\fBahriman\fR \fI\,add\/\fR
add package
.TP
\fBahriman\fR \fI\,check\/\fR
check for updates
.TP
\fBahriman\fR \fI\,clean\/\fR
clean local caches
.TP
\fBahriman\fR \fI\,config\/\fR
dump configuration
.TP
\fBahriman\fR \fI\,init\/\fR
create repository tree
\fBahriman\fR \fI\,aur-search\/\fR
search for package
.TP
\fBahriman\fR \fI\,key-import\/\fR
import PGP key
.TP
\fBahriman\fR \fI\,rebuild\/\fR
rebuild repository
\fBahriman\fR \fI\,package-add\/\fR
add package
.TP
\fBahriman\fR \fI\,remove\/\fR
\fBahriman\fR \fI\,package-remove\/\fR
remove package
.TP
\fBahriman\fR \fI\,remove-unknown\/\fR
remove unknown packages
.TP
\fBahriman\fR \fI\,report\/\fR
generate report
.TP
\fBahriman\fR \fI\,search\/\fR
search for package
.TP
\fBahriman\fR \fI\,setup\/\fR
initial service configuration
.TP
\fBahriman\fR \fI\,sign\/\fR
sign packages
.TP
\fBahriman\fR \fI\,status\/\fR
\fBahriman\fR \fI\,package-status\/\fR
get package status
.TP
\fBahriman\fR \fI\,status-update\/\fR
\fBahriman\fR \fI\,package-status-remove\/\fR
remove package status
.TP
\fBahriman\fR \fI\,package-status-update\/\fR
update package status
.TP
\fBahriman\fR \fI\,sync\/\fR
\fBahriman\fR \fI\,patch-add\/\fR
add patch set
.TP
\fBahriman\fR \fI\,patch-list\/\fR
list patch sets
.TP
\fBahriman\fR \fI\,patch-remove\/\fR
remove patch set
.TP
\fBahriman\fR \fI\,repo-check\/\fR
check for updates
.TP
\fBahriman\fR \fI\,repo-clean\/\fR
clean local caches
.TP
\fBahriman\fR \fI\,repo-config\/\fR
dump configuration
.TP
\fBahriman\fR \fI\,repo-init\/\fR
create repository tree
.TP
\fBahriman\fR \fI\,repo-rebuild\/\fR
rebuild repository
.TP
\fBahriman\fR \fI\,repo-remove-unknown\/\fR
remove unknown packages
.TP
\fBahriman\fR \fI\,repo-report\/\fR
generate report
.TP
\fBahriman\fR \fI\,repo-setup\/\fR
initial service configuration
.TP
\fBahriman\fR \fI\,repo-sign\/\fR
sign packages
.TP
\fBahriman\fR \fI\,repo-status-update\/\fR
update repository status
.TP
\fBahriman\fR \fI\,repo-sync\/\fR
sync repository
.TP
\fBahriman\fR \fI\,update\/\fR
\fBahriman\fR \fI\,repo-update\/\fR
update packages
.TP
\fBahriman\fR \fI\,user\/\fR
manage users for web services
\fBahriman\fR \fI\,user-add\/\fR
create or update user
.TP
\fBahriman\fR \fI\,user-remove\/\fR
remove user
.TP
\fBahriman\fR \fI\,web\/\fR
start web server
.SH OPTIONS 'ahriman add'
usage: ahriman add [-h] [--now] [--source {PackageSource.Auto,PackageSource.Archive,PackageSource.Directory,PackageSource.AUR}] [--without-dependencies] package [package ...]
web server
.SH OPTIONS 'ahriman aur-search'
usage: ahriman aur-search [-h] [-i]
[--sort-by {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}]
search [search ...]
add package
search for package in AUR using API
.TP
\fBpackage\fR
package base/name or archive path
\fBsearch\fR
search terms, can be specified multiple times, result will match all terms
.TP
\fB\-\-now\fR
run update function after
\fB\-i\fR, \fB\-\-info\fR
show additional package information
.TP
\fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.Directory,PackageSource.AUR}
package source
\fB\-\-sort\-by\fR {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}
sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted
by name
.SH OPTIONS 'ahriman search'
usage: ahriman aur-search [-h] [-i]
[--sort-by {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}]
search [search ...]
search for package in AUR using API
.TP
\fB\-\-without\-dependencies\fR
do not add dependencies
.SH OPTIONS 'ahriman check'
usage: ahriman check [-h] [--no-vcs] [package ...]
check for updates. Same as update \-\-dry\-run \-\-no\-manual
\fBsearch\fR
search terms, can be specified multiple times, result will match all terms
.TP
\fBpackage\fR
filter check by package base
\fB\-i\fR, \fB\-\-info\fR
show additional package information
.TP
\fB\-\-no\-vcs\fR
do not check VCS packages
.SH OPTIONS 'ahriman clean'
usage: ahriman clean [-h] [--no-build] [--no-cache] [--no-chroot] [--no-manual] [--no-packages]
clear local caches
.TP
\fB\-\-no\-build\fR
do not clear directory with package sources
.TP
\fB\-\-no\-cache\fR
do not clear directory with package caches
.TP
\fB\-\-no\-chroot\fR
do not clear build chroot
.TP
\fB\-\-no\-manual\fR
do not clear directory with manually added packages
.TP
\fB\-\-no\-packages\fR
do not clear directory with built packages
.SH OPTIONS 'ahriman config'
usage: ahriman config [-h]
dump configuration for specified architecture
.SH OPTIONS 'ahriman init'
usage: ahriman init [-h]
create empty repository tree. Optional command for auto architecture support
\fB\-\-sort\-by\fR {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}
sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted
by name
.SH OPTIONS 'ahriman key-import'
usage: ahriman key-import [-h] [--key-server KEY_SERVER] key
import PGP key from public sources to repository user
import PGP key from public sources to the repository user
.TP
\fBkey\fR
@ -186,59 +170,403 @@ PGP key to import from public server
\fB\-\-key\-server\fR \fI\,KEY_SERVER\/\fR
key server for key import
.SH OPTIONS 'ahriman rebuild'
usage: ahriman rebuild [-h] [--depends-on DEPENDS_ON]
rebuild whole repository
.SH OPTIONS 'ahriman package-add'
usage: ahriman package-add [-h] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}]
[--without-dependencies]
package [package ...]
add existing or new package to the build queue
.TP
\fB\-\-depends\-on\fR \fI\,DEPENDS_ON\/\fR
only rebuild packages that depend on specified package
\fBpackage\fR
package source (base name, path to local files, remote URL)
.SH OPTIONS 'ahriman remove'
usage: ahriman remove [-h] package [package ...]
.TP
\fB\-n\fR, \fB\-\-now\fR
run update function after
remove package
.TP
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}
explicitly specify the package source for this command
.TP
\fB\-\-without\-dependencies\fR
do not add dependencies
.SH OPTIONS 'ahriman add'
usage: ahriman package-add [-h] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}]
[--without-dependencies]
package [package ...]
add existing or new package to the build queue
.TP
\fBpackage\fR
package source (base name, path to local files, remote URL)
.TP
\fB\-n\fR, \fB\-\-now\fR
run update function after
.TP
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}
explicitly specify the package source for this command
.TP
\fB\-\-without\-dependencies\fR
do not add dependencies
.SH OPTIONS 'ahriman package-update'
usage: ahriman package-add [-h] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}]
[--without-dependencies]
package [package ...]
add existing or new package to the build queue
.TP
\fBpackage\fR
package source (base name, path to local files, remote URL)
.TP
\fB\-n\fR, \fB\-\-now\fR
run update function after
.TP
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}
explicitly specify the package source for this command
.TP
\fB\-\-without\-dependencies\fR
do not add dependencies
.SH OPTIONS 'ahriman package-remove'
usage: ahriman package-remove [-h] package [package ...]
remove package from the repository
.TP
\fBpackage\fR
package name or base
.SH OPTIONS 'ahriman remove-unknown'
usage: ahriman remove-unknown [-h] [--dry-run]
.SH OPTIONS 'ahriman remove'
usage: ahriman package-remove [-h] package [package ...]
remove packages which are missing in AUR
remove package from the repository
.TP
\fBpackage\fR
package name or base
.SH OPTIONS 'ahriman package-status'
usage: ahriman package-status [-h] [--ahriman] [-i]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
request status of the package
.TP
\fBpackage\fR
filter status by package base
.TP
\fB\-\-ahriman\fR
get service status itself
.TP
\fB\-i\fR, \fB\-\-info\fR
show additional package information
.TP
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
filter packages by status
.SH OPTIONS 'ahriman status'
usage: ahriman package-status [-h] [--ahriman] [-i]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
request status of the package
.TP
\fBpackage\fR
filter status by package base
.TP
\fB\-\-ahriman\fR
get service status itself
.TP
\fB\-i\fR, \fB\-\-info\fR
show additional package information
.TP
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
filter packages by status
.SH OPTIONS 'ahriman package-status-remove'
usage: ahriman package-status-remove [-h] package [package ...]
remove the package from the status page
.TP
\fBpackage\fR
remove specified packages
.SH OPTIONS 'ahriman package-status-update'
usage: ahriman package-status-update [-h]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
update package status on the status page
.TP
\fBpackage\fR
set status for specified packages. If no packages supplied, service status will be updated
.TP
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
new status
.SH OPTIONS 'ahriman status-update'
usage: ahriman package-status-update [-h]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
update package status on the status page
.TP
\fBpackage\fR
set status for specified packages. If no packages supplied, service status will be updated
.TP
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
new status
.SH OPTIONS 'ahriman patch-add'
usage: ahriman patch-add [-h] [-t TRACK] package
create or update source patches
.TP
\fBpackage\fR
path to directory with changed files for patch addition/update
.TP
\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR
files which has to be tracked
.SH OPTIONS 'ahriman patch-list'
usage: ahriman patch-list [-h] package
list available patches for the package
.TP
\fBpackage\fR
package base
.SH OPTIONS 'ahriman patch-remove'
usage: ahriman patch-remove [-h] package
remove patches for the package
.TP
\fBpackage\fR
package base
.SH OPTIONS 'ahriman repo-check'
usage: ahriman repo-check [-h] [--no-vcs] [package ...]
check for packages updates. Same as update \-\-dry\-run \-\-no\-manual
.TP
\fBpackage\fR
filter check by package base
.TP
\fB\-\-no\-vcs\fR
do not check VCS packages
.SH OPTIONS 'ahriman check'
usage: ahriman repo-check [-h] [--no-vcs] [package ...]
check for packages updates. Same as update \-\-dry\-run \-\-no\-manual
.TP
\fBpackage\fR
filter check by package base
.TP
\fB\-\-no\-vcs\fR
do not check VCS packages
.SH OPTIONS 'ahriman repo-clean'
usage: ahriman repo-clean [-h] [--build] [--cache] [--chroot] [--manual] [--packages] [--patches]
remove local caches
.TP
\fB\-\-build\fR
clear directory with package sources
.TP
\fB\-\-cache\fR
clear directory with package caches
.TP
\fB\-\-chroot\fR
clear build chroot
.TP
\fB\-\-manual\fR
clear directory with manually added packages
.TP
\fB\-\-packages\fR
clear directory with built packages
.TP
\fB\-\-patches\fR
clear directory with patches
.SH OPTIONS 'ahriman clean'
usage: ahriman repo-clean [-h] [--build] [--cache] [--chroot] [--manual] [--packages] [--patches]
remove local caches
.TP
\fB\-\-build\fR
clear directory with package sources
.TP
\fB\-\-cache\fR
clear directory with package caches
.TP
\fB\-\-chroot\fR
clear build chroot
.TP
\fB\-\-manual\fR
clear directory with manually added packages
.TP
\fB\-\-packages\fR
clear directory with built packages
.TP
\fB\-\-patches\fR
clear directory with patches
.SH OPTIONS 'ahriman repo-config'
usage: ahriman repo-config [-h]
dump configuration for the specified architecture
.SH OPTIONS 'ahriman config'
usage: ahriman repo-config [-h]
dump configuration for the specified architecture
.SH OPTIONS 'ahriman repo-init'
usage: ahriman repo-init [-h]
create empty repository tree. Optional command for auto architecture support
.SH OPTIONS 'ahriman init'
usage: ahriman repo-init [-h]
create empty repository tree. Optional command for auto architecture support
.SH OPTIONS 'ahriman repo-rebuild'
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON]
force rebuild whole repository
.TP
\fB\-\-depends\-on\fR \fI\,DEPENDS_ON\/\fR
only rebuild packages that depend on specified package
.SH OPTIONS 'ahriman rebuild'
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON]
force rebuild whole repository
.TP
\fB\-\-depends\-on\fR \fI\,DEPENDS_ON\/\fR
only rebuild packages that depend on specified package
.SH OPTIONS 'ahriman repo-remove-unknown'
usage: ahriman repo-remove-unknown [-h] [--dry-run] [-i]
remove packages which are missing in AUR and do not have local PKGBUILDs
.TP
\fB\-\-dry\-run\fR
just perform check for packages without removal
.SH OPTIONS 'ahriman report'
usage: ahriman report [-h] [target ...]
.TP
\fB\-i\fR, \fB\-\-info\fR
show additional package information
generate report
.SH OPTIONS 'ahriman remove-unknown'
usage: ahriman repo-remove-unknown [-h] [--dry-run] [-i]
remove packages which are missing in AUR and do not have local PKGBUILDs
.TP
\fB\-\-dry\-run\fR
just perform check for packages without removal
.TP
\fB\-i\fR, \fB\-\-info\fR
show additional package information
.SH OPTIONS 'ahriman repo-report'
usage: ahriman repo-report [-h] [target ...]
generate repository report according to current settings
.TP
\fBtarget\fR
target to generate report
.SH OPTIONS 'ahriman search'
usage: ahriman search [-h] search [search ...]
.SH OPTIONS 'ahriman report'
usage: ahriman repo-report [-h] [target ...]
search for package in AUR using API
generate repository report according to current settings
.TP
\fBsearch\fR
search terms, can be specified multiple times
\fBtarget\fR
target to generate report
.SH OPTIONS 'ahriman setup'
usage: ahriman setup [-h] [--build-command BUILD_COMMAND] [--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository REPOSITORY [--sign-key SIGN_KEY]
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
.SH OPTIONS 'ahriman repo-setup'
usage: ahriman repo-setup [-h] [--build-command BUILD_COMMAND] [--from-configuration FROM_CONFIGURATION] [--no-multilib]
--packager PACKAGER --repository REPOSITORY [--sign-key SIGN_KEY]
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
create initial service configuration, requires root
@ -275,64 +603,101 @@ sign options
\fB\-\-web\-port\fR \fI\,WEB_PORT\/\fR
port of the web service
.SH OPTIONS 'ahriman sign'
usage: ahriman sign [-h] [package ...]
.SH OPTIONS 'ahriman setup'
usage: ahriman repo-setup [-h] [--build-command BUILD_COMMAND] [--from-configuration FROM_CONFIGURATION] [--no-multilib]
--packager PACKAGER --repository REPOSITORY [--sign-key SIGN_KEY]
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
(re\-)sign packages and repository database
create initial service configuration, requires root
.TP
\fB\-\-build\-command\fR \fI\,BUILD_COMMAND\/\fR
build command prefix
.TP
\fB\-\-from\-configuration\fR \fI\,FROM_CONFIGURATION\/\fR
path to default devtools pacman configuration
.TP
\fB\-\-no\-multilib\fR
do not add multilib repository
.TP
\fB\-\-packager\fR \fI\,PACKAGER\/\fR
packager name and email
.TP
\fB\-\-repository\fR \fI\,REPOSITORY\/\fR
repository name
.TP
\fB\-\-sign\-key\fR \fI\,SIGN_KEY\/\fR
sign key id
.TP
\fB\-\-sign\-target\fR {SignSettings.Packages,SignSettings.Repository}
sign options
.TP
\fB\-\-web\-port\fR \fI\,WEB_PORT\/\fR
port of the web service
.SH OPTIONS 'ahriman repo-sign'
usage: ahriman repo-sign [-h] [package ...]
(re\-)sign packages and repository database according to current settings
.TP
\fBpackage\fR
sign only specified packages
.SH OPTIONS 'ahriman status'
usage: ahriman status [-h] [--ahriman] [--status {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}] [package ...]
.SH OPTIONS 'ahriman sign'
usage: ahriman repo-sign [-h] [package ...]
request status of the package
(re\-)sign packages and repository database according to current settings
.TP
\fBpackage\fR
filter status by package base
sign only specified packages
.SH OPTIONS 'ahriman repo-status-update'
usage: ahriman repo-status-update [-h]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
update repository status on the status page
.TP
\fB\-\-ahriman\fR
get service status itself
.TP
\fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
filter packages by status
.SH OPTIONS 'ahriman status-update'
usage: ahriman status-update [-h] [--status {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}] [--remove] [package ...]
request status of the package
.TP
\fBpackage\fR
set status for specified packages. If no packages supplied, service status will be updated
.TP
\fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
new status
.TP
\fB\-\-remove\fR
remove package status page
.SH OPTIONS 'ahriman repo-sync'
usage: ahriman repo-sync [-h] [target ...]
.SH OPTIONS 'ahriman sync'
usage: ahriman sync [-h] [target ...]
sync packages to remote server
sync repository files to remote server according to current settings
.TP
\fBtarget\fR
target to sync
.SH OPTIONS 'ahriman update'
usage: ahriman update [-h] [--dry-run] [--no-aur] [--no-manual] [--no-vcs] [package ...]
.SH OPTIONS 'ahriman sync'
usage: ahriman repo-sync [-h] [target ...]
run updates
sync repository files to remote server according to current settings
.TP
\fBtarget\fR
target to sync
.SH OPTIONS 'ahriman repo-update'
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-manual] [--no-vcs] [package ...]
check for packages updates and run build process if requested
.TP
\fBpackage\fR
@ -354,10 +719,37 @@ do not include manual updates
\fB\-\-no\-vcs\fR
do not check VCS packages
.SH OPTIONS 'ahriman user'
usage: ahriman user [-h] [--as-service] [-a {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [--no-reload] [-p PASSWORD] [-r] [--secure] username
.SH OPTIONS 'ahriman update'
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-manual] [--no-vcs] [package ...]
manage users for web services with password and role. In case if password was not entered it will be asked interactively
check for packages updates and run build process if requested
.TP
\fBpackage\fR
filter check by package base
.TP
\fB\-\-dry\-run\fR
just perform check for updates, same as check command
.TP
\fB\-\-no\-aur\fR
do not check for AUR updates. Implies \-\-no\-vcs
.TP
\fB\-\-no\-manual\fR
do not include manual updates
.TP
\fB\-\-no\-vcs\fR
do not check VCS packages
.SH OPTIONS 'ahriman user-add'
usage: ahriman user-add [-h] [--as-service] [--no-reload] [-p PASSWORD]
[-r {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [-s]
username
update user for web services with the given password and role. In case if password was not entered it will be asked interactively
.TP
\fBusername\fR
@ -368,23 +760,37 @@ username for web service
add user as service user
.TP
\fB\-a\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}, \fB\-\-access\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}
\fB\-\-no\-reload\fR
do not reload authentication module
.TP
\fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR
user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2
authorization type.
.TP
\fB\-r\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}, \fB\-\-role\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}
user access level
.TP
\fB\-s\fR, \fB\-\-secure\fR
set file permissions to user\-only
.SH OPTIONS 'ahriman user-remove'
usage: ahriman user-remove [-h] [--no-reload] [-s] username
remove user from the user mapping and update the configuration
.TP
\fBusername\fR
username for web service
.TP
\fB\-\-no\-reload\fR
do not reload authentication module
.TP
\fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR
user password
.TP
\fB\-r\fR, \fB\-\-remove\fR
remove user from configuration
.TP
\fB\-\-secure\fR
\fB\-s\fR, \fB\-\-secure\fR
set file permissions to user\-only
.SH OPTIONS 'ahriman web'
@ -392,6 +798,11 @@ usage: ahriman web [-h]
start web server
.SH COMMENTS
Argument list can also be read from file by using @ prefix.
.SH AUTHORS
.B ahriman
was written by ahriman team <>.

View File

@ -12,7 +12,13 @@ Full dependency diagram:
## `ahriman.application` package
This package contains application (aka executable) related classes and everything for that. It also contains package called `ahriman.application.handlers` in which all available subcommands are described as separated classes derived from base `ahriman.application.handlers.handler.Handler` class. `ahriman.application.ahriman` contains only command line parses and executes specified `Handler` on success, `ahriman.application.application.Application` is a god class which provides interfaces for all repository related actions. `ahriman.application.lock.Lock` is additional class which provides file-based lock and also performs some common checks.
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.
`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.formatters` package provides `Printer` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
`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
@ -67,8 +73,11 @@ Application is designed to run from `systemd` services and provides parametrized
Idea is to copy package to the directory from which it will be handled at the next update run. Different variants are supported:
* If supplied argument is file then application moves the file to the directory with built packages. Same rule applies for directory, but in this case it copies every package-like file from the specified directory.
* If supplied argument is directory and there is `PKGBUILD` file there it will be treated as local package. In this case it will queue this package to build and copy source files (`PKGBUILD` and `.SRCINFO`) to caches.
* If supplied argument iis not file then application tries to lookup for the specified name in AUR and clones it into the directory with manual updates. This scenario can also handle package dependencies which are missing in repositories.
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.
## Rebuild packages
Same as add function for every package in repository. Optional filter by reverse dependency can be supplied.
@ -97,7 +106,7 @@ After any step any package data is being removed.
## 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 recommended to read class related settings from the class, not outside.
`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.
## Utils
@ -105,7 +114,7 @@ For every external command run (which is actually not recommended if possible) c
## Submodules
Some packages provide different behaviour depending on configuration settings. In this cases inheritance is used and recommended way to deal with them is to call class method `load` from base classes.
Some packages provide different behaviour depending on configuration settings. In these cases inheritance is used and recommended way to deal with them is to call class method `load` from base classes.
## Authorization
@ -131,7 +140,17 @@ OAuth provider uses library definitions (`aioauth-client`) in order _authenticat
OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature is used by service reporting (aka robots).
In order to configure users there is special command.
In order to configure users there are special commands.
## Remote synchronization
There are several supported synchronization providers, currently they are `rsync`, `s3`, `github`.
`rsync` provider does not have any specific logic except for running external rsync application with configured arguments. The service does not handle SSH configuration, thus it has to be configured before running application manually.
`s3` provider uses `boto3` package and implements sync feature. The files are stored in architecture directory (e.g. if bucket is `repository`, packages will be stored in `repository/x86_64` for the `x86_64` architecture), bucket must be created before any action and API key must have permissions to write to the bucket. No external configuration required. In order to upload only changed files the service compares calculated hashes with the Amazon ETags, used realization is described [here](https://teppen.io/2018/10/23/aws_s3_verify_etags/).
`github` provider authenticates through basic auth, API key with repository write permissions is required. There will be created a release with the name of the architecture in case if it does not exist; files will be uploaded to the release assets. It also stores array of files and their MD5 checksums in release body in order to upload only changed ones. According to the Github API in case if there is already uploaded asset with the same name (e.g. database files), asset will be removed first.
## Additional features

View File

@ -69,12 +69,19 @@ Settings for signing packages or repository. Group name must refer to architectu
Report generation settings.
* `target` - list of reports to be generated, space separated list of strings, required. Allowed values are `html`, `email`.
* `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` (with architecture it has higher priority).
### `email:*` groups
Type will be read from several ways:
Group name must refer to architecture, e.g. it should be `email:x86_64` for x86_64 architecture.
* In case if `type` option set inside the section, it will be used.
* Otherwise, it will look for type from section name removing architecture name.
* And finally, it will use section name as type.
### `email` type
Section name must be either `email` (plus optional architecture name, e.g. `email:x86_64`) or random name with `type` set.
* `type` - type of the report, string, optional, must be set to `email` if exists.
* `full_template_path` - path to Jinja2 template for full package description index, string, optional.
* `homepage` - link to homepage, string, optional.
* `host` - SMTP host for sending emails, string, required.
@ -88,10 +95,11 @@ Group name must refer to architecture, e.g. it should be `email:x86_64` for x86_
* `template_path` - path to Jinja2 template, string, required.
* `user` - SMTP user to authenticate, string, optional.
### `html:*` groups
### `html` type
Group name must refer to architecture, e.g. it should be `html:x86_64` for x86_64 architecture.
Section name must be either `html` (plus optional architecture name, e.g. `html:x86_64`) or random name with `type` set.
* `type` - type of the report, string, optional, must be set to `html` if exists.
* `path` - path to html report file, string, required.
* `homepage` - link to homepage, string, optional.
* `link_path` - prefix for HTML links, string, required.
@ -101,19 +109,41 @@ Group name must refer to architecture, e.g. it should be `html:x86_64` for x86_6
Remote synchronization settings.
* `target` - list of synchronizations to be used, space separated list of strings, required. Allowed values are `rsync`, `s3`.
* `target` - list of synchronizations to be used, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. `somerandomname` must point to existing section, `github` must point to one of `github` of `github:x86_64` (with architecture it has higher priority).
### `rsync:*` groups
Type will be read from several ways:
Group name must refer to architecture, e.g. it should be `rsync:x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`.
* In case if `type` option set inside the section, it will be used.
* Otherwise, it will look for type from section name removing architecture name.
* And finally, it will use section name as type.
### `github` type
This feature requires Github key creation (see below). Section name must be either `github` (plus optional architecture name, e.g. `github:x86_64`) or random name with `type` set.
* `type` - type of the upload, string, optional, must be set to `github` if exists.
* `owner` - Github repository owner, string, required.
* `password` - created Github API key. In order to create it do the following:
1. Go to [settings page](https://github.com/settings/profile).
2. Switch to [developers settings](https://github.com/settings/apps).
3. Switch to [personal access tokens](https://github.com/settings/tokens).
4. Generate new token. Required scope is `public_repo` (or `repo` for private repository support).
* `repository` - Github repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme).
* `username` - Github authorization user, string, required. Basically the same as `owner`.
### `rsync` type
Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`. Section name must be either `rsync` (plus optional architecture name, e.g. `rsync:x86_64`) or random name with `type` set.
* `type` - type of the upload, string, optional, must be set to `rsync` if exists.
* `command` - rsync command to run, space separated list of string, required.
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
* `remote` - remote server to rsync (e.g. `1.2.3.4:path/to/sync`), string, required.
### `s3:*` groups
### `s3` type
Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64 architecture.
Requires `boto3` library to be installed. Section name must be either `s3` (plus optional architecture name, e.g. `s3:x86_64`) or random name with `type` set.
* `type` - type of the upload, string, optional, must be set to `github` if exists.
* `access_key` - AWS access key ID, string, required.
* `bucket` - bucket name (e.g. `bucket`), string, required.
* `chunk_size` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024.
@ -122,7 +152,7 @@ Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64
## `web:*` groups
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture.
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture. This feature requires `aiohttp` libraries to be installed.
* `address` - optional address in form `proto://host:port` (`port` can be omitted in case of default `proto` ports), will be used instead of `http://{host}:{port}` in case if set, string, optional. This option is required in case if `OAuth` provider is used.
* `debug` - enable debug toolbar, boolean, optional, default `no`.

444
docs/faq.md Normal file
View File

@ -0,0 +1,444 @@
# FAQ
## General topics
### What is the purpose of the project?
This project has been created in order to maintain self-hosted Arch Linux user repository without manual intervention - checking for updates and building packages.
### How do I install it?
TL;DR
```shell
yay -S ahriman
sudo -u ahriman ahriman -a x86_64 init
sudo ahriman -a x86_64 repo-setup --packager "ahriman bot <ahriman@example.com>" --repository "repository"
systemctl enable --now ahriman@x86_64.timer
```
#### Long answer
The idea is to install the package as usual, create working directory tree, create configuration for `sudo` and `devtools`. Detailed description of the setup instruction can be found [here](setup.md).
### What does "architecture specific" mean? / How to configure for different architectures?
Some sections can be configured per architecture. The service will merge architecture specific values into common settings. In order to specify settings for specific architecture you must point it in section name.
For example, the section
```ini
[build]
build_command = extra-x86_64-build
```
states that default build command is `extra-x86_64-build`. But if there is section
```ini
[build:i686]
build_command = extra-i686-build
```
the `extra-i686-build` command will be used for `i686` architecture.
### How to use reporter/upload settings?
Normally you probably like to generate only one report for the specific type, e.g. only one email report. In order to do it you will need to have the following configuration:
```ini
[report]
target = email
[email]
...
```
or in case of multiple architectures and _different_ reporting settings:
```ini
[report]
target = email
[email:i686]
...
[email:x86_64]
...
```
But for some cases you would like to have multiple different reports with the same type (e.g. sending different templates to different addresses). For these cases you will need to specify section name in target and type in section, e.g. the following configuration can be used:
```ini
[report]
target = email_1 email_2
[email_1]
type = email
...
[email_2]
type = email
...
```
### Okay, I've installed ahriman, how do I add new package?
```shell
sudo -u ahriman ahriman package-add ahriman --now
```
`--now` flag is totally optional and just run `repo-update` subcommand after the registering the new package, Thus the extended flow is the following:
```shell
sudo -u ahriman ahriman package-add ahriman
sudo -u ahriman ahriman repo-update
```
### AUR is fine, but I would like to create package from local PKGBUILD
TL;DR
```shell
sudo -u ahriman ahriman package-add /path/to/local/directory/with/PKGBUILD --now
```
Before using this command you will need to create local directory, put `PKGBUILD` there and generate `.SRCINFO` by using `makepkg --printsrcinfo > .SRCINFO` command. These packages will be stored locally and _will be ignored_ during automatic update; in order to update the package you will need to run `package-add` command again.
### But I just wanted to change PKGBUILD from AUR a bit!
Well it is supported also.
1. Clone sources from AUR.
2. Make changes you would like to (e.g. edit `PKGBUILD`, add external patches).
3. Run `sudo -u ahriman ahriman patch-add /path/to/local/directory/with/PKGBUILD`.
The last command will calculate diff from current tree to the `HEAD` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).
### Package build fails because it cannot validate PGP signature of source files
TL;DR
```shell
sudo -u ahriman ahriman key-import ...
```
### How do I check if there are new commits for VCS packages?
Normally the service handles VCS packages correctly, but it requires additional dependencies:
```shell
pacman -S breezy darcs mercurial subversion
```
### I would like to remove package because it is no longer needed/moved to official repositories
```shell
sudo -u ahriman ahriman package-remove ahriman
```
Also, there is command `repo-remove-unknown` which checks packages in AUR and local storage and removes ones which have been removed.
Remove commands also remove any package files (patches, caches etc).
### There is new major release of %library-name%, how do I rebuild packages?
TL;DR
```shell
sudo -u ahriman ahriman repo-rebuild --depends-on python
```
You can even rebuild the whole repository (which is particular useful in case if you would like to change packager) if you do not supply `--depends-on` option.
However, note that you do not need to rebuild repository in case if you just changed signing option, just use `repo-sign` command instead.
### Hmm, I have packages built, but how can I use it?
Add the following lines to your `pacman.conf`:
```ini
[repository]
Server = file:///var/lib/ahriman/repository/x86_64
```
(You might need to add `SigLevel` option according to the pacman documentation.)
## Remote synchronization
### Wait I would like to use the repository from another server
There are several choices:
1. Easy and cheap, just share your local files through the internet, e.g. for `nginx`:
```
server {
location /x86_64 {
root /var/lib/ahriman/repository/x86_64;
autoindex on;
}
}
```
2. You can also upload your packages using `rsync` to any available server. In order to use it you would need to configure ahriman first:
```ini
[upload]
target = rsync
[rsync]
remote = 192.168.0.1:/srv/repo
```
After that just add `/srv/repo` to the `pacman.conf` as usual. You can also upload to S3 (e.g. `Server = https://s3.eu-central-1.amazonaws.com/repository/x86_64`) or to Github (e.g. `Server = https://github.com/ahriman/repository/releases/download/x86_64`).
### How do I configure S3?
1. Install dependencies:
```shell
pacman -S python-boto3
```
3. Create a bucket.
4. Create user with write access to the bucket:
```
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListObjectsInBucket",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::repository"
]
},
{
"Sid": "AllObjectActions",
"Effect": "Allow",
"Action": "s3:*Object",
"Resource": [
"arn:aws:s3:::repository/*"
]
}
]
}
```
5. Create an API key for the user and store it.
6. Configure the service as following:
```ini
[upload]
target = s3
[s3]
access_key = ...
bucket = repository
region = eu-central-1
secret_key = ...
```
### How do I configure Github?
1. Create a repository.
2. [Create API key](https://github.com/settings/tokens) with scope `public_repo`.
3. Configure the service as following:
```ini
[upload]
target = github
[github]
owner = ahriman
password = ...
repository = repository
username = ahriman
```
## Reporting
### I would like to get report to email
1. Install dependencies:
```shell
yay -S python-jinja
```
2. Configure the service:
```ini
[report]
target = email
[email]
host = smtp.example.com
link_path = http://example.com/x86_64
password = ...
port = 465
receivers = me@example.com
sender = me@example.com
user = me@example.com
```
### I'm using synchronization to S3 and would like to generate index page
1. Install dependencies:
```shell
yay -S python-jinja
```
2. Configure the service:
```ini
[report]
target = html
[html]
path = /var/lib/ahriman/repository/x86_64/index.html
link_path = http://example.com/x86_64
```
After these steps `index.html` file will be automatically synced to S3
## Web service
### Readme mentions web interface, how do I use it?
1. Install dependencies:
```shell
yay -S python-aiohttp python-aiohttp-jinja2
```
2. Configure service:
```ini
[web]
port = 8080
```
3. Start the web service `systemctl enable --now ahriman-web@x86_64`.
### I would like to limit user access to the status page
1. Install dependencies 😊:
```shell
yay -S python-aiohttp-security python-aiohttp-session python-cryptography
```
2. Configure the service to enable authorization:
```ini
[auth]
target = configuration
```
3. Create user for the service:
```shell
sudo -u ahriman ahriman user-add --as-service -r write api
```
This command will ask for the password, just type it in stdin; _do not_ leave the field blank, user will not be able to authorize.
4. Create end-user `sudo -u ahriman ahriman user-add -r write my-first-user` with password.
5. Restart web service `systemctl restart ahriman-web@x86_64`.
### I would like to use OAuth
1. Create OAuth web application, download its `client_id` and `client_secret`.
2. Guess what? Install dependencies:
```shell
yay -S python-aiohttp-security python-aiohttp-session python-cryptography python-aioauth-client
```
3. Configure the service:
```ini
[auth]
target = oauth
client_id = ...
client_secret = ...
[web]
address = https://example.com
```
Configure `oauth_provider` and `oauth_scopes` in case if you would like to use different from Google provider. Scope must grant access to user email. `web.address` is required to make callback URL available from internet.
4. Create service user:
```shell
sudo -u ahriman ahriman user-add --as-service -r write api
```
5. Create end-user `sudo -u ahriman ahriman user-add -r write my-first-user`. When it will ask for the password leave it blank.
6. Restart web service `systemctl restart ahriman-web@x86_64`.
## Other topics
### How does it differ from %another-manager%?
Short answer - I do not know.
#### [archrepo2](https://github.com/lilydjwg/archrepo2)
Don't know, haven't tried it. But it lacks of documentation at least.
* Web interface.
* No synchronization and reporting.
* `archrepo2` actively uses direct shell calls and `yaourt` components.
* It has constantly running process instead of timer process (it is not pro or con).
#### [repoctl](https://github.com/cassava/repoctl)
* Web interface.
* No reporting.
* Local packages and patches support.
* Some actions are not fully automated (e.g. package update still requires manual intervention for the build itself).
* `repoctl` has better AUR interaction features. With colors!
* `repoctl` has much easier configuration and even completion.
* `repoctl` is able to store old packages.
* Ability to host repository from same command vs external services (e.g. nginx) in `ahriman`.
#### [repo-scripts](https://github.com/arcan1s/repo-scripts)
Though originally I've created ahriman by trying to improve the project, it still lacks a lot of features:
* Web interface.
* Better reporting with template support.
* Synchronization features (there was only `rsync` based).
* Local packages and patches support.
* No dependency management.
* And so on.
`repo-scripts` also have bad architecture and bad quality code and uses out-of-dated `yaourt` and `package-query`.
### I would like to check service logs
By default, the service writes logs to `/dev/log` which can be accessed by using `journalctl` command (logs are written to the journal of the user under which command is run).
You can also edit configuration and forward logs to `stderr`, just change `handlers` value, e.g.:
```shell
sed -i 's/handlers = syslog_handler/handlers = console_handler/g' /etc/ahriman.ini.d/logging.ini
```
You can even configure logging as you wish, but kindly refer to python `logging` module configuration.
### Html customization
It is possible to customize html templates. In order to do so, create files somewhere (refer to Jinja2 documentation and the service source code for available parameters) and put `template_path` to configuration pointing to this directory.
### I did not find my question
[Create an issue](https://github.com/arcan1s/ahriman/issues) with type **Question**.

View File

@ -2,59 +2,66 @@
1. Install package as usual.
2. Change settings if required, see [configuration reference](configuration.md) for more details.
3. Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
3. TL;DR
```shell
echo 'PACKAGER="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
```
```shell
sudo -u ahriman ahriman -a x86_64 repo-init
sudo ahriman -a x86_64 repo-setup ...
```
`repo-init` subcommand is required to create the repository tree with correct rights. `repo-setup` literally does the following steps:
4. Configure build tools (it is required for correct dependency management system):
1. Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
1. 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`).
2. Create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`).
3. Change configuration file, add your own repository, add multilib repository etc;
4. Set `build_command` option to point to your command.
5. Configure `/etc/sudoers.d/ahriman` to allow running command without a password.
```shell
echo 'PACKAGER="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
```
```shell
ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build
cp /usr/share/devtools/pacman-{extra,ahriman}.conf
2. Configure build tools (it is required for correct dependency management system):
echo '[multilib]' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman-ahriman.conf
1. 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`).
2. Create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`).
3. Change configuration file, add your own repository, add multilib repository etc;
4. Set `build_command` option to point to your command.
5. Configure `/etc/sudoers.d/ahriman` to allow running command without a password.
echo '[aur-clone]' | 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
```shell
ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build
cp /usr/share/devtools/pacman-{extra,ahriman}.conf
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 '[multilib]' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman
```
echo '[aur-clone]' | 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
5. Start and enable `ahriman@.timer` via `systemctl`:
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 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman
```
4. Start and enable `ahriman@.timer` via `systemctl`:
```shell
systemctl enable --now ahriman@x86_64.timer
```
6. Start and enable status page:
5. Start and enable status page:
```shell
systemctl enable --now ahriman-web@x86_64
```
7. Add packages by using `ahriman add {package}` command:
6. Add packages by using `ahriman package-add {package}` command:
```shell
sudo -u ahriman ahriman -a x86_64 add yay --now
sudo -u ahriman ahriman -a x86_64 package-add ahriman --now
```
Note that initial service configuration can be done by running `ahriman setup` with specific arguments.
## User creation
`create-user` subcommand is recommended for new user creation.
`user-add` subcommand is recommended for new user creation.

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=1.4.0
pkgver=1.6.0
pkgrel=1
pkgdesc="ArcH Linux ReposItory MANager"
arch=('any')
@ -11,7 +11,6 @@ depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-passlib' 'python-srcinfo
makedepends=('python-pip')
optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support'
'gnupg: package and repository sign'
'mercurial: -hg packages support'
'python-aioauth-client: web server with OAuth2 authorization'
'python-aiohttp: web server'

View File

@ -1 +1,2 @@
d /var/lib/ahriman 0755 ahriman ahriman
d /var/log/ahriman 0755 ahriman ahriman

View File

@ -1,5 +1,5 @@
[loggers]
keys = root,builder,build_details,http
keys = root,build_details,http,stderr,boto3,botocore,nose,s3transfer
[handlers]
keys = console_handler,syslog_handler
@ -20,11 +20,11 @@ formatter = syslog_format
args = ("/dev/log",)
[formatter_generic_format]
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s
datefmt =
[formatter_syslog_format]
format = [%(levelname)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
format = [%(levelname)s] [%(name)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
datefmt =
[logger_root]
@ -32,12 +32,6 @@ level = DEBUG
handlers = syslog_handler
qualname = root
[logger_builder]
level = DEBUG
handlers = syslog_handler
qualname = builder
propagate = 0
[logger_build_details]
level = DEBUG
handlers = syslog_handler
@ -49,3 +43,32 @@ level = DEBUG
handlers = syslog_handler
qualname = http
propagate = 0
[logger_stderr]
level = DEBUG
handlers = console_handler
qualname = stderr
[logger_boto3]
level = INFO
handlers = syslog_handler
qualname = boto3
propagate = 0
[logger_botocore]
level = INFO
handlers = syslog_handler
qualname = botocore
propagate = 0
[logger_nose]
level = INFO
handlers = syslog_handler
qualname = nose
propagate = 0
[logger_s3transfer]
level = INFO
handlers = syslog_handler
qualname = s3transfer
propagate = 0

View File

@ -27,13 +27,13 @@
<div id="toolbar">
{% if not auth.enabled or auth.username is not none %}
<button id="add" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addForm">
<i class="fa fa-plus"></i> Add
<i class="fa fa-plus"></i> add
</button>
<button id="update" class="btn btn-secondary" onclick="updatePackages()" disabled>
<i class="fa fa-play"></i> Update
<i class="fa fa-play"></i> update
</button>
<button id="remove" class="btn btn-danger" onclick="removePackages()" disabled>
<i class="fa fa-trash"></i> Remove
<i class="fa fa-trash"></i> remove
</button>
{% endif %}
</div>

View File

@ -3,25 +3,25 @@
<div class="modal-content">
<form action="/user-api/v1/login" method="post">
<div class="modal-header">
<h4 class="modal-title">Login</h4>
<h4 class="modal-title">login</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">Username</label>
<label for="username" class="col-sm-2 col-form-label">username</label>
<div class="col-sm-10">
<input id="username" type="text" class="form-control" placeholder="enter username" name="username" required>
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">Password</label>
<label for="password" class="col-sm-2 col-form-label">password</label>
<div class="col-sm-10">
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary">Login</button>
<button class="btn btn-primary">login</button>
</div>
</form>
</div>

View File

@ -2,12 +2,12 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Add new packages</h4>
<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>
<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>
@ -15,9 +15,9 @@
</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>
<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>
@ -27,7 +27,7 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-danger">
<h4 class="modal-title">Failed</h4>
<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">
@ -35,7 +35,7 @@
<p id="errorDetails"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
</div>
</div>
</div>
@ -45,7 +45,7 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-success">
<h4 class="modal-title">Success</h4>
<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">
@ -53,7 +53,7 @@
<ul id="successDetails"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
</div>
</div>
</div>

View File

@ -82,7 +82,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
{% if homepage is not none %}
<li><a class="nav-link" href="{{ homepage }}" title="homepage">Homepage</a></li>
<li><a class="nav-link" href="{{ homepage }}" title="homepage">homepage</a></li>
{% endif %}
</ul>
</footer>

View File

@ -10,3 +10,14 @@
<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.18.3/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script>
$("#packages").bootstrapTable({
formatClearSearch: function () {
return "Clear search";
},
formatSearch: function () {
return "search";
}
})
</script>

View File

@ -17,7 +17,7 @@ setup(
description="ArcH Linux ReposItory MANager",
author="arcanis",
author="ahriman team",
author_email="",
url="https://github.com/arcan1s/ahriman",

View File

@ -25,6 +25,7 @@ from pathlib import Path
from ahriman import version
from ahriman.application import handlers
from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package_source import PackageSource
from ahriman.models.sign_settings import SignSettings
@ -35,125 +36,81 @@ from ahriman.models.user_access import UserAccess
SubParserAction = argparse._SubParsersAction # pylint: disable=protected-access
def _formatter(prog: str) -> argparse.HelpFormatter:
"""
formatter for the help message
:param prog: application name
:return: formatter used by default
"""
return argparse.ArgumentDefaultsHelpFormatter(prog, width=120)
def _parser() -> argparse.ArgumentParser:
"""
command line parser generator
:return: command line parser for the application
"""
parser = argparse.ArgumentParser(prog="ahriman", description="ArcH Linux ReposItory MANager",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
epilog="Argument list can also be read from file by using @ prefix.",
fromfile_prefix_chars="@", formatter_class=_formatter)
parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
action="append")
parser.add_argument("-c", "--configuration", help="configuration path", type=Path, default=Path("/etc/ahriman.ini"))
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
parser.add_argument(
"-l",
"--lock",
help="lock file",
type=Path,
default=Path(tempfile.gettempdir()) / "ahriman.lock")
parser.add_argument("--no-log", help="redirect all log messages to stderr", action="store_true")
parser.add_argument("-l", "--lock", help="lock file", type=Path,
default=Path(tempfile.gettempdir()) / "ahriman.lock")
parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", 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",
action="store_true")
parser.add_argument("-v", "--version", action="version", version=version.__version__)
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
_set_add_parser(subparsers)
_set_check_parser(subparsers)
_set_clean_parser(subparsers)
_set_config_parser(subparsers)
_set_init_parser(subparsers)
_set_aur_search_parser(subparsers)
_set_key_import_parser(subparsers)
_set_rebuild_parser(subparsers)
_set_remove_parser(subparsers)
_set_remove_unknown_parser(subparsers)
_set_report_parser(subparsers)
_set_search_parser(subparsers)
_set_setup_parser(subparsers)
_set_sign_parser(subparsers)
_set_status_parser(subparsers)
_set_status_update_parser(subparsers)
_set_sync_parser(subparsers)
_set_update_parser(subparsers)
_set_user_parser(subparsers)
_set_package_add_parser(subparsers)
_set_package_remove_parser(subparsers)
_set_package_status_parser(subparsers)
_set_package_status_remove_parser(subparsers)
_set_package_status_update_parser(subparsers)
_set_patch_add_parser(subparsers)
_set_patch_list_parser(subparsers)
_set_patch_remove_parser(subparsers)
_set_repo_check_parser(subparsers)
_set_repo_clean_parser(subparsers)
_set_repo_config_parser(subparsers)
_set_repo_init_parser(subparsers)
_set_repo_rebuild_parser(subparsers)
_set_repo_remove_unknown_parser(subparsers)
_set_repo_report_parser(subparsers)
_set_repo_setup_parser(subparsers)
_set_repo_sign_parser(subparsers)
_set_repo_status_update_parser(subparsers)
_set_repo_sync_parser(subparsers)
_set_repo_update_parser(subparsers)
_set_user_add_parser(subparsers)
_set_user_remove_parser(subparsers)
_set_web_parser(subparsers)
return parser
def _set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for add subcommand
add parser for AUR search subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("add", help="add package", description="add package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="package base/name or archive path", nargs="+")
parser.add_argument("--now", help="run update function after", action="store_true")
parser.add_argument("--source", help="package source", choices=PackageSource, type=PackageSource,
default=PackageSource.Auto)
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add, architecture=[])
return parser
def _set_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for check subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("check", help="check for updates",
description="check for updates. Same as update --dry-run --no-manual",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="filter check by package base", nargs="*")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update, architecture=[], no_aur=False, no_manual=True, dry_run=True)
return parser
def _set_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for clean subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("clean", help="clean local caches", description="clear local caches",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true")
parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true")
parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true")
parser.add_argument("--no-manual", help="do not clear directory with manually added packages", action="store_true")
parser.add_argument("--no-packages", help="do not clear directory with built packages", action="store_true")
parser.set_defaults(handler=handlers.Clean, architecture=[], no_log=True, unsafe=True)
return parser
def _set_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for config subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("config", help="dump configuration",
description="dump configuration for specified architecture",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.set_defaults(handler=handlers.Dump, lock=None, no_log=True, no_report=True, unsafe=True)
return parser
def _set_init_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for init subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("init", help="create repository tree",
description="create empty repository tree. Optional command for auto architecture support",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.set_defaults(handler=handlers.Init, no_report=True)
parser = root.add_parser("aur-search", aliases=["search"], help="search for package",
description="search for package in AUR using API", formatter_class=_formatter)
parser.add_argument("search", help="search terms, can be specified multiple times, result will match all terms",
nargs="+")
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.add_argument("--sort-by", help="sort field by this field. In case if two packages have the same value of "
"the specified field, they will be always sorted by name",
default="name", choices=sorted(handlers.Search.SORT_FIELDS))
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, no_report=True, quiet=True, unsafe=True)
return parser
@ -164,88 +121,275 @@ def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
:return: created argument parser
"""
parser = root.add_parser("key-import", help="import PGP key",
description="import PGP key from public sources to repository user",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
description="import PGP key from public sources to the repository user",
epilog="By default ahriman runs build process with package sources validation "
"(in case if signature and keys are available in PKGBUILD). This process will "
"fail in case if key is not known for build user. This subcommand can be used "
"in order to import the PGP key to user keychain.",
formatter_class=_formatter)
parser.add_argument("--key-server", help="key server for key import", default="pgp.mit.edu")
parser.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, no_report=True)
return parser
def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for rebuild subcommand
add parser for package addition subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("rebuild", help="rebuild repository", description="rebuild whole repository",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
parser.set_defaults(handler=handlers.Rebuild, architecture=[])
parser = root.add_parser("package-add", aliases=["add", "package-update"], help="add package",
description="add existing or new package to the build queue",
epilog="This subcommand should be used for new package addition. It also supports flag "
"--now in case if you would like to build the package immediately. "
"You can add new package from one of supported sources: "
"1) if it is already built package you can specify the path to the archive; "
"2) you can also add built packages from the directory (e.g. during the migration "
"from another repository source); "
"3) it is also possible to add package from local PKGBUILD, but in this case it "
"will be ignored during the next automatic updates; "
"4) ahriman supports downloading archives from remote (e.g. HTTP) sources; "
"5) and finally you can add package from AUR.",
formatter_class=_formatter)
parser.add_argument("package", help="package source (base name, path to local files, remote URL)", nargs="+")
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=PackageSource, default=PackageSource.Auto)
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add)
return parser
def _set_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_package_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for remove subcommand
add parser for package removal subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("remove", help="remove package", description="remove package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser = root.add_parser("package-remove", aliases=["remove"], help="remove package",
description="remove package from the repository", formatter_class=_formatter)
parser.add_argument("package", help="package name or base", nargs="+")
parser.set_defaults(handler=handlers.Remove, architecture=[])
parser.set_defaults(handler=handlers.Remove)
return parser
def _set_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package status subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("package-status", aliases=["status"], help="get package status",
description="request status of the package",
epilog="This feature requests package status from the web interface if it is available.",
formatter_class=_formatter)
parser.add_argument("package", help="filter status by package base", nargs="*")
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status",
type=BuildStatusEnum, choices=BuildStatusEnum)
parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, quiet=True, unsafe=True)
return parser
def _set_package_status_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package status remove subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("package-status-remove", help="remove package status",
description="remove the package from the status page",
epilog="Please note that this subcommand does not remove the package itself, it just "
"clears the status page.",
formatter_class=_formatter)
parser.add_argument("package", help="remove specified packages", nargs="+")
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Remove, lock=None, no_report=True, quiet=True,
unsafe=True)
return parser
def _set_package_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package status update subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("package-status-update", aliases=["status-update"], help="update package status",
description="update package status on the status page", formatter_class=_formatter)
parser.add_argument("package", help="set status for specified packages. "
"If no packages supplied, service status will be updated",
nargs="*")
parser.add_argument("-s", "--status", help="new status",
type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success)
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, quiet=True,
unsafe=True)
return parser
def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for new patch subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("patch-add", help="add patch set", description="create or update source patches",
epilog="In order to add a patch set for the package you will need to clone "
"the AUR package manually, add required changes (e.g. external patches, "
"edit PKGBUILD) and run command, e.g. `ahriman patch path/to/directory`. "
"By default it tracks *.patch and *.diff files, but this behavior can be changed "
"by using --track option",
formatter_class=_formatter)
parser.add_argument("package", help="path to directory with changed files for patch addition/update")
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
default=["*.diff", "*.patch"])
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True)
return parser
def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for list patches subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("patch-list", help="list patch sets",
description="list available patches for the package", formatter_class=_formatter)
parser.add_argument("package", help="package base")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True)
return parser
def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for remove patches subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package",
formatter_class=_formatter)
parser.add_argument("package", help="package base")
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, no_report=True)
return parser
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository check subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("repo-check", aliases=["check"], help="check for updates",
description="check for packages updates. Same as update --dry-run --no-manual",
formatter_class=_formatter)
parser.add_argument("package", help="filter check by package base", nargs="*")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update, dry_run=True, no_aur=False, no_manual=True)
return parser
def _set_repo_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository clean subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("repo-clean", aliases=["clean"], help="clean local caches",
description="remove local caches",
epilog="The subcommand clears every temporary directories (builds, caches etc). Normally "
"you should not run this command manually. Also in case if you are going to clear "
"the chroot directories you will need root privileges.",
formatter_class=_formatter)
parser.add_argument("--build", help="clear directory with package sources", action="store_true")
parser.add_argument("--cache", help="clear directory with package caches", action="store_true")
parser.add_argument("--chroot", help="clear build chroot", action="store_true")
parser.add_argument("--manual", help="clear directory with manually added packages", action="store_true")
parser.add_argument("--packages", help="clear directory with built packages", action="store_true")
parser.add_argument("--patches", help="clear directory with patches", action="store_true")
parser.set_defaults(handler=handlers.Clean, quiet=True, unsafe=True)
return parser
def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for config subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("repo-config", aliases=["config"], help="dump configuration",
description="dump configuration for the specified architecture",
formatter_class=_formatter)
parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, quiet=True, unsafe=True)
return parser
def _set_repo_init_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository init subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("repo-init", aliases=["init"], help="create repository tree",
description="create empty repository tree. Optional command for auto architecture support",
formatter_class=_formatter)
parser.set_defaults(handler=handlers.Init, no_report=True)
return parser
def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository rebuild subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("repo-rebuild", aliases=["rebuild"], help="rebuild repository",
description="force rebuild whole repository", formatter_class=_formatter)
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
parser.set_defaults(handler=handlers.Rebuild)
return parser
def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for remove unknown packages subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("remove-unknown", help="remove unknown packages",
description="remove packages which are missing in AUR",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser = root.add_parser("repo-remove-unknown", aliases=["remove-unknown"], help="remove unknown packages",
description="remove packages which are missing in AUR and do not have local PKGBUILDs",
formatter_class=_formatter)
parser.add_argument("--dry-run", help="just perform check for packages without removal", action="store_true")
parser.set_defaults(handler=handlers.RemoveUnknown, architecture=[])
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.set_defaults(handler=handlers.RemoveUnknown)
return parser
def _set_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for report subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("report", help="generate report", description="generate report",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
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.add_argument("target", help="target to generate report", nargs="*")
parser.set_defaults(handler=handlers.Report, architecture=[])
parser.set_defaults(handler=handlers.Report)
return parser
def _set_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for search subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("search", help="search for package", description="search for package in AUR using API")
parser.add_argument("search", help="search terms, can be specified multiple times", nargs="+")
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, no_log=True, no_report=True, unsafe=True)
return parser
def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for setup subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("setup", help="initial service configuration",
parser = root.add_parser("repo-setup", aliases=["setup"], help="initial service configuration",
description="create initial service configuration, requires root",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
epilog="Create _minimal_ configuration for the service according to provided options.",
formatter_class=_formatter)
parser.add_argument("--build-command", help="build command prefix", default="ahriman")
parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
type=Path, default=Path("/usr/share/devtools/pacman-extra.conf"))
@ -253,115 +397,113 @@ def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--packager", help="packager name and email", required=True)
parser.add_argument("--repository", help="repository name", required=True)
parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", type=SignSettings.from_option,
choices=SignSettings, action="append")
parser.add_argument("--sign-target", help="sign options", action="append",
type=SignSettings.from_option, choices=SignSettings)
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.set_defaults(handler=handlers.Setup, lock=None, no_log=True, no_report=True, unsafe=True)
parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, quiet=True, unsafe=True)
return parser
def _set_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for sign subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("sign", help="sign packages", description="(re-)sign packages and repository database",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser = root.add_parser("repo-sign", aliases=["sign"], help="sign packages",
description="(re-)sign packages and repository database according to current settings",
epilog="Sign repository and/or packages as configured.",
formatter_class=_formatter)
parser.add_argument("package", help="sign only specified packages", nargs="*")
parser.set_defaults(handler=handlers.Sign, architecture=[])
parser.set_defaults(handler=handlers.Sign)
return parser
def _set_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for status subcommand
add parser for repository status update subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("status", help="get package status", description="request status of the package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
parser.add_argument("--status", help="filter packages by status", choices=BuildStatusEnum, type=BuildStatusEnum)
parser.add_argument("package", help="filter status by package base", nargs="*")
parser.set_defaults(handler=handlers.Status, lock=None, no_log=True, no_report=True, unsafe=True)
parser = root.add_parser("repo-status-update", help="update repository status",
description="update repository status on the status page", formatter_class=_formatter)
parser.add_argument("-s", "--status", help="new status",
type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success)
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, package=[],
quiet=True, unsafe=True)
return parser
def _set_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for status update subcommand
add parser for repository sync subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("status-update", help="update package status", description="request status of the package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
"package",
help="set status for specified packages. If no packages supplied, service status will be updated",
nargs="*")
parser.add_argument("--status", help="new status", choices=BuildStatusEnum,
type=BuildStatusEnum, default=BuildStatusEnum.Success)
parser.add_argument("--remove", help="remove package status page", action="store_true")
parser.set_defaults(handler=handlers.StatusUpdate, lock=None, no_log=True, no_report=True, unsafe=True)
return parser
def _set_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for sync subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("sync", help="sync repository", description="sync packages to remote server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
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.add_argument("target", help="target to sync", nargs="*")
parser.set_defaults(handler=handlers.Sync, architecture=[])
parser.set_defaults(handler=handlers.Sync)
return parser
def _set_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for update subcommand
add parser for repository update subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("update", help="update packages", description="run updates",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser = root.add_parser("repo-update", aliases=["update"], help="update packages",
description="check for packages updates and run build process if requested",
formatter_class=_formatter)
parser.add_argument("package", help="filter check by package base", nargs="*")
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update, architecture=[])
parser.set_defaults(handler=handlers.Update)
return parser
def _set_user_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for create user subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser(
"user",
help="manage users for web services",
description="manage users for web services with password and role. In case if password was not entered it will be asked interactively",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser = root.add_parser("user-add", help="create or update user",
description="update user for web services with the given password and role. "
"In case if password was not entered it will be asked interactively",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("--as-service", help="add user as service user", action="store_true")
parser.add_argument(
"-a",
"--access",
help="user access level",
type=UserAccess,
choices=UserAccess,
default=UserAccess.Read)
parser.add_argument("--no-reload", help="do not reload authentication module", action="store_true")
parser.add_argument("-p", "--password", help="user password")
parser.add_argument("-r", "--remove", help="remove user from configuration", action="store_true")
parser.add_argument("--secure", help="set file permissions to user-only", action="store_true")
parser.set_defaults(handler=handlers.User, architecture=[""], lock=None, no_log=True, no_report=True, unsafe=True)
parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, "
"which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-r", "--role", help="user access level",
type=UserAccess, choices=UserAccess, default=UserAccess.Read)
parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true")
parser.set_defaults(handler=handlers.User, action=Action.Update, architecture=[""], lock=None, no_report=True,
quiet=True, unsafe=True)
return parser
def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for user removal subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("user-remove", help="remove user",
description="remove user from the user mapping and update the configuration",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("--no-reload", help="do not reload authentication module", action="store_true")
parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true")
parser.set_defaults(handler=handlers.User, action=Action.Remove, architecture=[""], lock=None, no_report=True, # nosec
password="", quiet=True, role=UserAccess.Read, unsafe=True)
return parser
@ -371,8 +513,7 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("web", help="start web server", description="start web server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser = root.add_parser("web", help="web server", description="start web server", formatter_class=_formatter)
parser.set_defaults(handler=handlers.Web, lock=None, no_report=True, parser=_parser)
return parser

View File

@ -0,0 +1,20 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.application.application.application import Application

View File

@ -0,0 +1,51 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Iterable, Set
from ahriman.application.application.packages import Packages
from ahriman.application.application.repository import Repository
from ahriman.models.package import Package
class Application(Packages, Repository):
"""
base application class
"""
def _finalize(self, built_packages: Iterable[Package]) -> None:
"""
generate report and sync to remote server
"""
self.report([], built_packages)
self.sync([], built_packages)
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
:return: list of known packages
"""
known_packages: Set[str] = set()
# local set
for base in self.repository.packages():
for package, properties in base.packages.items():
known_packages.add(package)
known_packages.update(properties.provides)
known_packages.update(self.repository.pacman.all_packages())
return known_packages

View File

@ -0,0 +1,148 @@
#
# Copyright (c) 2021 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 requests
import shutil
from pathlib import Path
from typing import Any, Iterable, Set
from ahriman.application.application.properties import Properties
from ahriman.core.build_tools.sources import Sources
from ahriman.core.util import package_like
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class Packages(Properties):
"""
package control class
"""
def _finalize(self, built_packages: Iterable[Package]) -> None:
"""
generate report and sync to remote server
"""
raise NotImplementedError
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
:return: list of known packages
"""
raise NotImplementedError
def _add_archive(self, source: str, *_: Any) -> None:
"""
add package from archive
:param source: path to package archive
"""
local_path = Path(source)
dst = self.repository.paths.packages / local_path.name
shutil.copy(local_path, dst)
def _add_aur(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None:
"""
add package from AUR
:param source: package base name
:param known_packages: list of packages which are known by the service
:param without_dependencies: if set, dependency check will be disabled
"""
aur_url = self.configuration.get("alpm", "aur_url")
package = Package.load(source, self.repository.pacman, aur_url)
Sources.load(self.repository.paths.manual_for(package.base), package.git_url,
self.repository.paths.patches_for(package.base))
local_path = self.repository.paths.manual_for(package.base)
self._process_dependencies(local_path, known_packages, without_dependencies)
def _add_directory(self, source: str, *_: Any) -> None:
"""
add packages from directory
:param source: path to local directory
"""
local_path = Path(source)
for full_path in filter(package_like, local_path.iterdir()):
self._add_archive(str(full_path))
def _add_local(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None:
"""
add package from local PKGBUILDs
:param source: path to directory with local source files
:param known_packages: list of packages which are known by the service
:param without_dependencies: if set, dependency check will be disabled
"""
local_path = Path(source)
aur_url = self.configuration.get("alpm", "aur_url")
package = Package.load(local_path, self.repository.pacman, aur_url)
cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(local_path, cache_dir) # copy package to store in caches
Sources.init(cache_dir) # we need to run init command in directory where we do have permissions
dst = self.repository.paths.manual_for(package.base)
shutil.copytree(cache_dir, dst) # copy package for the build
self._process_dependencies(dst, known_packages, without_dependencies)
def _add_remote(self, source: str, *_: Any) -> None:
"""
add package from remote sources (e.g. HTTP)
:param remote_url: remote URL to the package archive
"""
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
response = requests.get(source, stream=True)
response.raise_for_status()
with dst.open("wb") as local_file:
for chunk in response.iter_content(chunk_size=1024):
local_file.write(chunk)
def _process_dependencies(self, local_path: Path, known_packages: Set[str], without_dependencies: bool) -> None:
"""
process package dependencies
:param local_path: path to local package sources (i.e. cloned AUR repository)
:param known_packages: list of packages which are known by the service
:param without_dependencies: if set, dependency check will be disabled
"""
if without_dependencies:
return
dependencies = Package.dependencies(local_path)
self.add(dependencies.difference(known_packages), PackageSource.AUR, without_dependencies)
def add(self, names: Iterable[str], source: PackageSource, without_dependencies: bool) -> None:
"""
add packages for the next build
:param names: list of package bases to add
:param source: package source to add
:param without_dependencies: if set, dependency check will be disabled
"""
known_packages = self._known_packages() # speedup dependencies processing
for name in names:
resolved_source = source.resolve(name)
fn = getattr(self, f"_add_{resolved_source.value}")
fn(name, known_packages, without_dependencies)
def remove(self, names: Iterable[str]) -> None:
"""
remove packages from repository
:param names: list of packages (either base or name) to remove
"""
self.repository.process_remove(names)
self._finalize([])

View File

@ -0,0 +1,45 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import logging
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
class Properties:
"""
application base properties class
:ivar architecture: repository architecture
:ivar configuration: configuration instance
:ivar logger: application logger
:ivar repository: repository instance
"""
def __init__(self, architecture: str, configuration: Configuration, no_report: bool) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
"""
self.logger = logging.getLogger("root")
self.configuration = configuration
self.architecture = architecture
self.repository = Repository(architecture, configuration, no_report)

View File

@ -17,155 +17,50 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import logging
import shutil
from pathlib import Path
from typing import Callable, Iterable, List, Set
from typing import Callable, Iterable, List
from ahriman.core.build_tools.task import Task
from ahriman.core.configuration import Configuration
from ahriman.core.repository.repository import Repository
from ahriman.application.application.properties import Properties
from ahriman.core.build_tools.sources import Sources
from ahriman.core.tree import Tree
from ahriman.core.util import package_like
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class Application:
class Repository(Properties):
"""
base application class
:ivar architecture: repository architecture
:ivar configuration: configuration instance
:ivar logger: application logger
:ivar repository: repository instance
repository control class
"""
def __init__(self, architecture: str, configuration: Configuration, no_report: bool) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
"""
self.logger = logging.getLogger("root")
self.configuration = configuration
self.architecture = architecture
self.repository = Repository(architecture, configuration, no_report)
def _finalize(self, built_packages: Iterable[Package]) -> None:
"""
generate report and sync to remote server
"""
self.report([], built_packages)
self.sync([], built_packages)
raise NotImplementedError
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
:return: list of known packages
"""
known_packages: Set[str] = set()
# local set
for base in self.repository.packages():
for package, properties in base.packages.items():
known_packages.add(package)
known_packages.update(properties.provides)
known_packages.update(self.repository.pacman.all_packages())
return known_packages
def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]:
"""
get list of packages to run update process
:param filter_packages: do not check every package just specified in the list
:param no_aur: do not check for aur updates
:param no_manual: do not check for manual updates
:param no_vcs: do not check VCS packages
:param log_fn: logger function to log updates
:return: list of out-of-dated packages
"""
updates = []
if not no_aur:
updates.extend(self.repository.updates_aur(filter_packages, no_vcs))
if not no_manual:
updates.extend(self.repository.updates_manual())
for package in updates:
log_fn(f"{package.base} = {package.version}")
return updates
def add(self, names: Iterable[str], source: PackageSource, without_dependencies: bool) -> None:
"""
add packages for the next build
:param names: list of package bases to add
:param source: package source to add
:param without_dependencies: if set, dependency check will be disabled
"""
known_packages = self._known_packages()
def add_directory(path: Path) -> None:
for full_path in filter(package_like, path.iterdir()):
add_archive(full_path)
def add_manual(src: str) -> Path:
package = Package.load(src, self.repository.pacman, self.configuration.get("alpm", "aur_url"))
path = self.repository.paths.manual / package.base
Task.fetch(path, package.git_url)
return path
def add_archive(src: Path) -> None:
dst = self.repository.paths.packages / src.name
shutil.move(src, dst)
def process_dependencies(path: Path) -> None:
if without_dependencies:
return
dependencies = Package.dependencies(path)
self.add(dependencies.difference(known_packages), PackageSource.AUR, without_dependencies)
def process_single(src: str) -> None:
resolved_source = source.resolve(src)
if resolved_source == PackageSource.Directory:
add_directory(Path(src))
elif resolved_source == PackageSource.Archive:
add_archive(Path(src))
else:
path = add_manual(src)
process_dependencies(path)
for name in names:
process_single(name)
def clean(self, no_build: bool, no_cache: bool, no_chroot: bool, no_manual: bool, no_packages: bool) -> None:
def clean(self, build: bool, cache: bool, chroot: bool, manual: bool, packages: bool, patches: bool) -> None:
"""
run all clean methods. Warning: some functions might not be available under non-root
:param no_build: do not clear directory with package sources
:param no_cache: do not clear directory with package caches
:param no_chroot: do not clear build chroot
:param no_manual: do not clear directory with manually added packages
:param no_packages: do not clear directory with built packages
:param build: clear directory with package sources
:param cache: clear directory with package caches
:param chroot: clear build chroot
:param manual: clear directory with manually added packages
:param packages: clear directory with built packages
:param patches: clear directory with patches
"""
if not no_build:
if build:
self.repository.clear_build()
if not no_cache:
if cache:
self.repository.clear_cache()
if not no_chroot:
if chroot:
self.repository.clear_chroot()
if not no_manual:
if manual:
self.repository.clear_manual()
if not no_packages:
if packages:
self.repository.clear_packages()
def remove(self, names: Iterable[str]) -> None:
"""
remove packages from repository
:param names: list of packages (either base or name) to remove
"""
self.repository.process_remove(names)
self._finalize([])
if patches:
self.repository.clear_patches()
def report(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
"""
@ -196,7 +91,7 @@ class Application:
# run generic update function
self.update([])
# sign repository database if set
self.repository.sign.sign_repository(self.repository.repo.repo_path)
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
self._finalize([])
def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
@ -213,13 +108,22 @@ class Application:
get packages which were not found in AUR
:return: unknown package list
"""
packages = []
for base in self.repository.packages():
def has_aur(package_base: str, aur_url: str) -> bool:
try:
_ = Package.from_aur(base.base, base.aur_url)
_ = Package.from_aur(package_base, aur_url)
except Exception:
packages.append(base)
return packages
return False
return True
def has_local(package_base: str) -> bool:
cache_dir = self.repository.paths.cache_for(package_base)
return cache_dir.is_dir() and not Sources.has_remotes(cache_dir)
return [
package
for package in self.repository.packages()
if not has_aur(package.base, package.aur_url) and not has_local(package.base)
]
def update(self, updates: Iterable[Package]) -> None:
"""
@ -238,8 +142,31 @@ class Application:
process_update(packages)
# process manual packages
tree = Tree.load(updates)
tree = Tree.load(updates, self.repository.paths)
for num, level in enumerate(tree.levels()):
self.logger.info("processing level #%i %s", num, [package.base for package in level])
packages = self.repository.process_build(level)
process_update(packages)
def updates(self, filter_packages: Iterable[str], no_aur: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]:
"""
get list of packages to run update process
:param filter_packages: do not check every package just specified in the list
:param no_aur: do not check for aur updates
:param no_manual: do not check for manual updates
:param no_vcs: do not check VCS packages
:param log_fn: logger function to log updates
:return: list of out-of-dated packages
"""
updates = []
if not no_aur:
updates.extend(self.repository.updates_aur(filter_packages, no_vcs))
if not no_manual:
updates.extend(self.repository.updates_manual())
for package in updates:
log_fn(f"{package.base} = {package.version}")
return updates

View File

@ -0,0 +1,19 @@
#
# Copyright (c) 2021 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/>.
#

View File

@ -0,0 +1,62 @@
#
# Copyright (c) 2021 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 aur # type: ignore
from typing import List, Optional
from ahriman.application.formatters.printer import Printer
from ahriman.core.util import pretty_datetime
from ahriman.models.property import Property
class AurPrinter(Printer):
"""
print content of the AUR package
"""
def __init__(self, package: aur.Package) -> None:
"""
default constructor
:param package: AUR package description
"""
self.content = package
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return [
Property("Package base", self.content.package_base),
Property("Description", self.content.description, is_required=True),
Property("Upstream URL", self.content.url),
Property("Licenses", self.content.license), # it should be actually a list
Property("Maintainer", self.content.maintainer or ""), # I think it is optional
Property("First submitted", pretty_datetime(self.content.first_submitted)),
Property("Last updated", pretty_datetime(self.content.last_modified)),
# more fields coming https://github.com/cdown/aur/pull/29
]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return f"{self.content.name} {self.content.version} ({self.content.num_votes})"

View File

@ -0,0 +1,55 @@
#
# Copyright (c) 2021 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, Optional
from ahriman.application.formatters.printer import Printer
from ahriman.models.property import Property
class ConfigurationPrinter(Printer):
"""
print content of the configuration section
"""
def __init__(self, section: str, values: Dict[str, str]) -> None:
"""
default constructor
:param section: section name
:param values: configuration values dictionary
"""
self.section = section
self.content = values
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return [
Property(key, value, is_required=True)
for key, value in sorted(self.content.items())
]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return f"[{self.section}]"

View File

@ -0,0 +1,60 @@
#
# Copyright (c) 2021 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 List, Optional
from ahriman.application.formatters.printer import Printer
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.property import Property
class PackagePrinter(Printer):
"""
print content of the internal package object
"""
def __init__(self, package: Package, status: BuildStatus) -> None:
"""
default constructor
:param package: package description
:param status: build status
"""
self.content = package
self.status = status
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return [
Property("Version", self.content.version, is_required=True),
Property("Groups", " ".join(self.content.groups)),
Property("Licenses", " ".join(self.content.licenses)),
Property("Depends", " ".join(self.content.depends)),
Property("Status", self.status.pretty_print(), is_required=True),
]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return self.content.pretty_print()

View File

@ -0,0 +1,55 @@
#
# Copyright (c) 2021 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 Callable, List, Optional
from ahriman.models.property import Property
class Printer:
"""
base class for formatters
"""
def print(self, verbose: bool, log_fn: Callable[[str], None] = print, separator: str = ": ") -> None:
"""
print content
:param verbose: print all fields
:param log_fn: logger function to log data
:param separator: separator for property name and property value
"""
if (title := self.title()) is not None:
log_fn(title)
for prop in self.properties():
if not verbose and not prop.is_required:
continue
log_fn(f"\t{prop.name}{separator}{prop.value}")
def properties(self) -> List[Property]: # pylint: disable=no-self-use
"""
convert content into printable data
:return: list of content properties
"""
return []
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""

View File

@ -0,0 +1,51 @@
#
# Copyright (c) 2021 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 List, Optional
from ahriman.application.formatters.printer import Printer
from ahriman.models.build_status import BuildStatus
from ahriman.models.property import Property
class StatusPrinter(Printer):
"""
print content of the status object
"""
def __init__(self, status: BuildStatus) -> None:
"""
default constructor
:param status: build status
"""
self.content = status
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return []
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return self.content.pretty_print()

View File

@ -24,6 +24,7 @@ from ahriman.application.handlers.clean import Clean
from ahriman.application.handlers.dump import Dump
from ahriman.application.handlers.init import Init
from ahriman.application.handlers.key_import import KeyImport
from ahriman.application.handlers.patch import Patch
from ahriman.application.handlers.rebuild import Rebuild
from ahriman.application.handlers.remove import Remove
from ahriman.application.handlers.remove_unknown import RemoveUnknown

View File

@ -46,5 +46,5 @@ class Add(Handler):
if not args.now:
return
packages = application.get_updates(args.package, True, False, True, application.logger.info)
packages = application.updates(args.package, True, False, True, application.logger.info)
application.update(packages)

View File

@ -41,5 +41,5 @@ class Clean(Handler):
:param configuration: configuration instance
:param no_report: force disable reporting
"""
Application(architecture, configuration, no_report).clean(args.no_build, args.no_cache, args.no_chroot,
args.no_manual, args.no_packages)
Application(architecture, configuration, no_report).clean(
args.build, args.cache, args.chroot, args.manual, args.packages, args.patches)

View File

@ -21,6 +21,7 @@ import argparse
from typing import Type
from ahriman.application.formatters.configuration_printer import ConfigurationPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
@ -30,7 +31,7 @@ class Dump(Handler):
dump configuration handler
"""
_print = print
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
@ -44,7 +45,4 @@ class Dump(Handler):
"""
dump = configuration.dump()
for section, values in sorted(dump.items()):
Dump._print(f"[{section}]")
for key, value in sorted(values.items()):
Dump._print(f"{key} = {value}")
Dump._print()
ConfigurationPrinter(section, values).print(verbose=False, separator=" = ")

View File

@ -34,11 +34,37 @@ from ahriman.models.repository_paths import RepositoryPaths
class Handler:
"""
base handler class for command callbacks
:cvar ALLOW_AUTO_ARCHITECTURE_RUN: allow to define architecture from existing repositories
:cvar ALLOW_MULTI_ARCHITECTURE_RUN: allow to run with multiple architectures
"""
ALLOW_AUTO_ARCHITECTURE_RUN = True
ALLOW_MULTI_ARCHITECTURE_RUN = True
@classmethod
def architectures_extract(cls: Type[Handler], args: argparse.Namespace) -> Set[str]:
"""
get known architectures
:param args: command line args
:return: list of architectures for which tree is created
"""
if not cls.ALLOW_AUTO_ARCHITECTURE_RUN and args.architecture is None:
# for some parsers (e.g. config) we need to run with specific architecture
# for those cases architecture must be set explicitly
raise MissingArchitecture(args.command)
if args.architecture: # architecture is specified explicitly
return set(args.architecture)
config = Configuration()
config.load(args.configuration)
# wtf???
root = config.getpath("repository", "root") # pylint: disable=assignment-from-no-return
architectures = RepositoryPaths.known_architectures(root)
if not architectures: # well we did not find anything
raise MissingArchitecture(args.command)
return architectures
@classmethod
def call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
"""
@ -48,12 +74,13 @@ class Handler:
:return: True on success, False otherwise
"""
try:
configuration = Configuration.from_path(args.configuration, architecture, not args.no_log)
configuration = Configuration.from_path(args.configuration, architecture, args.quiet)
with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration, args.no_report)
return True
except Exception:
logging.getLogger("root").exception("process exception")
# we are basically always want to print error to stderr instead of default logger
logging.getLogger("stderr").exception("process exception")
return False
@classmethod
@ -63,7 +90,7 @@ class Handler:
:param args: command line args
:return: 0 on success, 1 otherwise
"""
architectures = cls.extract_architectures(args)
architectures = cls.architectures_extract(args)
# actually we do not have to spawn another process if it is single-process application, do we?
if len(architectures) > 1:
@ -78,28 +105,6 @@ class Handler:
return 0 if all(result) else 1
@classmethod
def extract_architectures(cls: Type[Handler], args: argparse.Namespace) -> Set[str]:
"""
get known architectures
:param args: command line args
:return: list of architectures for which tree is created
"""
if args.architecture is None:
raise MissingArchitecture(args.command)
if args.architecture:
return set(args.architecture)
config = Configuration()
config.load(args.configuration)
# wtf???
root = config.getpath("repository", "root") # pylint: disable=assignment-from-no-return
architectures = RepositoryPaths.known_architectures(root)
if not architectures:
raise MissingArchitecture(args.command)
return architectures
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:

View File

@ -31,6 +31,8 @@ class Init(Handler):
repository init handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:

View File

@ -31,6 +31,8 @@ class KeyImport(Handler):
key import packages 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) -> None:
@ -41,4 +43,4 @@ class KeyImport(Handler):
:param configuration: configuration instance
:param no_report: force disable reporting
"""
Application(architecture, configuration, no_report).repository.sign.import_key(args.key_server, args.key)
Application(architecture, configuration, no_report).repository.sign.key_import(args.key_server, args.key)

View File

@ -0,0 +1,97 @@
#
# Copyright (c) 2021 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 shutil
from pathlib import Path
from typing import List, Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.models.action import Action
from ahriman.models.package import Package
class Patch(Handler):
"""
patch control handler
"""
_print = print
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
"""
application = Application(architecture, configuration, no_report)
if args.action == Action.List:
Patch.patch_set_list(application, args.package)
elif args.action == Action.Remove:
Patch.patch_set_remove(application, args.package)
elif args.action == Action.Update:
Patch.patch_set_create(application, Path(args.package), args.track)
@staticmethod
def patch_set_create(application: Application, sources_dir: Path, track: List[str]) -> None:
"""
create patch set for the package base
:param application: application instance
:param sources_dir: path to directory with the package sources
:param track: track files which match the glob before creating the patch
"""
package = Package.load(sources_dir, application.repository.pacman, application.repository.aur_url)
patch_dir = application.repository.paths.patches_for(package.base)
Patch.patch_set_remove(application, package.base) # remove old patches
patch_dir.mkdir(mode=0o755, parents=True)
Sources.patch_create(sources_dir, patch_dir / "00-main.patch", *track)
@staticmethod
def patch_set_list(application: Application, package_base: str) -> None:
"""
list patches available for the package base
:param application: application instance
:param package_base: package base
"""
patch_dir = application.repository.paths.patches_for(package_base)
if not patch_dir.is_dir():
return
for patch_path in sorted(patch_dir.glob("*.patch")):
Patch._print(patch_path.name)
@staticmethod
def patch_set_remove(application: Application, package_base: str) -> None:
"""
remove patch set for the package base
:param application: application instance
:param package_base: package base
"""
patch_dir = application.repository.paths.patches_for(package_base)
shutil.rmtree(patch_dir, ignore_errors=True)

View File

@ -22,9 +22,10 @@ import argparse
from typing import Type
from ahriman.application.application import Application
from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.package import Package
from ahriman.models.build_status import BuildStatus
class RemoveUnknown(Handler):
@ -46,16 +47,7 @@ class RemoveUnknown(Handler):
unknown_packages = application.unknown()
if args.dry_run:
for package in unknown_packages:
RemoveUnknown.log_fn(package)
PackagePrinter(package, BuildStatus()).print(args.info)
return
application.remove(package.base for package in unknown_packages)
@staticmethod
def log_fn(package: Package) -> None:
"""
log package information
:param package: package object to log
"""
print(f"=> {package.base} {package.version}")
print(f" {package.web_url}")

View File

@ -20,10 +20,13 @@
import argparse
import aur # type: ignore
from typing import Callable, Type
from typing import Callable, Iterable, List, Tuple, Type
from ahriman.application.formatters.aur_printer import AurPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidOption
from ahriman.core.util import aur_search
class Search(Handler):
@ -31,6 +34,9 @@ class Search(Handler):
packages search handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
SORT_FIELDS = set(aur.Package._fields) # later we will have to remove some fields from here (lists)
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
@ -41,20 +47,22 @@ class Search(Handler):
:param configuration: configuration instance
:param no_report: force disable reporting
"""
search = " ".join(args.search)
packages = aur.search(search)
# it actually always should return string
# explicit cast to string just to avoid mypy warning for untyped library
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
for package in sorted(packages, key=comparator):
Search.log_fn(package)
packages_list = aur_search(*args.search)
for package in Search.sort(packages_list, args.sort_by):
AurPrinter(package).print(args.info)
@staticmethod
def log_fn(package: aur.Package) -> None:
def sort(packages: Iterable[aur.Package], sort_by: str) -> List[aur.Package]:
"""
log package information
:param package: package object as from AUR
sort package list by specified field
:param packages: packages list to sort
:param sort_by: AUR package field name to sort by
:return: sorted list for packages
"""
print(f"=> {package.package_base} {package.version}")
print(f" {package.description}")
if sort_by not in Search.SORT_FIELDS:
raise InvalidOption(sort_by)
# always sort by package name at the last
# well technically it is not a string, but we can deal with it
comparator: Callable[[aur.Package], Tuple[str, str]] =\
lambda package: (getattr(package, sort_by), package.name)
return sorted(packages, key=comparator)

View File

@ -37,6 +37,8 @@ class Setup(Handler):
:cvar SUDOERS_PATH: path to sudoers.d include configuration
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ARCHBUILD_COMMAND_PATH = Path("/usr/bin/archbuild")
BIN_DIR_PATH = Path("/usr/local/bin")
MIRRORLIST_PATH = Path("/etc/pacman.d/mirrorlist")
@ -53,12 +55,12 @@ class Setup(Handler):
:param no_report: force disable reporting
"""
application = Application(architecture, configuration, no_report)
Setup.create_makepkg_configuration(args.packager, application.repository.paths)
Setup.create_executable(args.build_command, architecture)
Setup.create_devtools_configuration(args.build_command, architecture, args.from_configuration,
Setup.configuration_create_makepkg(args.packager, application.repository.paths)
Setup.executable_create(args.build_command, architecture)
Setup.configuration_create_devtools(args.build_command, architecture, args.from_configuration,
args.no_multilib, args.repository, application.repository.paths)
Setup.create_ahriman_configuration(args, architecture, args.repository, configuration.include)
Setup.create_sudo_configuration(args.build_command, architecture)
Setup.configuration_create_ahriman(args, architecture, args.repository, configuration.include)
Setup.configuration_create_sudo(args.build_command, architecture)
@staticmethod
def build_command(prefix: str, architecture: str) -> Path:
@ -71,7 +73,7 @@ class Setup(Handler):
return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build"
@staticmethod
def create_ahriman_configuration(args: argparse.Namespace, architecture: str, repository: str,
def configuration_create_ahriman(args: argparse.Namespace, architecture: str, repository: str,
include_path: Path) -> None:
"""
create service specific configuration
@ -100,7 +102,7 @@ class Setup(Handler):
configuration.write(ahriman_configuration)
@staticmethod
def create_devtools_configuration(prefix: str, architecture: str, source: Path,
def configuration_create_devtools(prefix: str, architecture: str, source: Path,
no_multilib: bool, repository: str, paths: RepositoryPaths) -> None:
"""
create configuration for devtools based on `source` configuration
@ -136,7 +138,7 @@ class Setup(Handler):
configuration.write(devtools_configuration)
@staticmethod
def create_makepkg_configuration(packager: str, paths: RepositoryPaths) -> None:
def configuration_create_makepkg(packager: str, paths: RepositoryPaths) -> None:
"""
create configuration for makepkg
:param packager: packager identifier (e.g. name, email)
@ -145,7 +147,7 @@ class Setup(Handler):
(paths.root / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n")
@staticmethod
def create_sudo_configuration(prefix: str, architecture: str) -> None:
def configuration_create_sudo(prefix: str, architecture: str) -> None:
"""
create configuration to run build command with sudo without password
:param prefix: command prefix in {prefix}-{architecture}-build
@ -156,7 +158,7 @@ class Setup(Handler):
Setup.SUDOERS_PATH.chmod(0o400) # security!
@staticmethod
def create_executable(prefix: str, architecture: str) -> None:
def executable_create(prefix: str, architecture: str) -> None:
"""
create executable for the service
:param prefix: command prefix in {prefix}-{architecture}-build

View File

@ -22,6 +22,8 @@ import argparse
from typing import Callable, Iterable, Tuple, Type
from ahriman.application.application import Application
from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.formatters.status_printer import StatusPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus
@ -33,6 +35,8 @@ class Status(Handler):
package status handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
@ -47,8 +51,7 @@ class Status(Handler):
client = Application(architecture, configuration, no_report=False).repository.reporter
if args.ahriman:
ahriman = client.get_self()
print(ahriman.pretty_print())
print()
StatusPrinter(ahriman).print(args.info)
if args.package:
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
[client.get(base) for base in args.package],
@ -60,6 +63,4 @@ class Status(Handler):
filter_fn: Callable[[Tuple[Package, BuildStatus]], bool] =\
lambda item: args.status is None or item[1].status == args.status
for package, package_status in sorted(filter(filter_fn, packages), key=comparator):
print(package.pretty_print())
print(f"\t{package.version}")
print(f"\t{package_status.pretty_print()}")
PackagePrinter(package, package_status).print(args.info)

View File

@ -19,12 +19,12 @@
#
import argparse
from typing import Callable, Type
from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidCommand
from ahriman.models.action import Action
class StatusUpdate(Handler):
@ -32,6 +32,8 @@ class StatusUpdate(Handler):
status update handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
@ -44,13 +46,14 @@ class StatusUpdate(Handler):
"""
# we are using reporter here
client = Application(architecture, configuration, no_report=False).repository.reporter
callback: Callable[[str], None] = lambda p: client.remove(p) if args.remove else client.update(p, args.status)
if args.package:
if args.action == Action.Update and args.package:
# update packages statuses
for package in args.package:
callback(package)
elif args.remove:
raise InvalidCommand("Remove option is supplied, but no packages set")
else:
client.update(package, args.status)
elif args.action == Action.Update:
# update service status
client.update_self(args.status)
elif args.action == Action.Remove:
for package in args.package:
client.remove(package)

View File

@ -42,8 +42,8 @@ class Update(Handler):
:param no_report: force disable reporting
"""
application = Application(architecture, configuration, no_report)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run))
packages = application.updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run))
if args.dry_run:
return

View File

@ -26,6 +26,7 @@ from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.action import Action
from ahriman.models.user import User as MUser
from ahriman.models.user_access import UserAccess
@ -35,6 +36,8 @@ class User(Handler):
user management 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) -> None:
@ -46,33 +49,20 @@ class User(Handler):
:param no_report: force disable reporting
"""
salt = User.get_salt(configuration)
user = User.create_user(args)
auth_configuration = User.get_auth_configuration(configuration.include)
user = User.user_create(args)
auth_configuration = User.configuration_get(configuration.include)
User.clear_user(auth_configuration, user)
if not args.remove:
User.create_configuration(auth_configuration, user, salt, args.as_service)
User.write_configuration(auth_configuration, args.secure)
User.user_clear(auth_configuration, user)
if args.action == Action.Update:
User.configuration_create(auth_configuration, user, salt, args.as_service)
User.configuration_write(auth_configuration, args.secure)
if not args.no_reload:
client = Application(architecture, configuration, no_report=False).repository.reporter
client.reload_auth()
@staticmethod
def clear_user(configuration: Configuration, user: MUser) -> None:
"""
remove user user from configuration file in case if it exists
:param configuration: configuration instance
:param user: user descriptor
"""
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
if not configuration.has_option(section, user.username):
continue
configuration.remove_option(section, user.username)
@staticmethod
def create_configuration(configuration: Configuration, user: MUser, salt: str, as_service_user: bool) -> None:
def configuration_create(configuration: Configuration, user: MUser, salt: str, as_service_user: bool) -> None:
"""
put new user to configuration
:param configuration: configuration instance
@ -89,19 +79,7 @@ class User(Handler):
configuration.set_option("web", "password", user.password)
@staticmethod
def create_user(args: argparse.Namespace) -> MUser:
"""
create user descriptor from arguments
:param args: command line args
:return: built user descriptor
"""
user = MUser(args.username, args.password, args.access)
if user.password is None:
user.password = getpass.getpass()
return user
@staticmethod
def get_auth_configuration(include_path: Path) -> Configuration:
def configuration_get(include_path: Path) -> Configuration:
"""
create configuration instance
:param include_path: path to directory with configuration includes
@ -114,20 +92,7 @@ class User(Handler):
return configuration
@staticmethod
def get_salt(configuration: Configuration, salt_length: int = 20) -> str:
"""
get salt from configuration or create new string
:param configuration: configuration instance
:param salt_length: salt length
:return: current salt
"""
salt = configuration.get("auth", "salt", fallback=None)
if salt:
return salt
return MUser.generate_password(salt_length)
@staticmethod
def write_configuration(configuration: Configuration, secure: bool) -> None:
def configuration_write(configuration: Configuration, secure: bool) -> None:
"""
write configuration file
:param configuration: configuration instance
@ -139,3 +104,40 @@ class User(Handler):
configuration.write(ahriman_configuration)
if secure:
configuration.path.chmod(0o600)
@staticmethod
def get_salt(configuration: Configuration, salt_length: int = 20) -> str:
"""
get salt from configuration or create new string
:param configuration: configuration instance
:param salt_length: salt length
:return: current salt
"""
if salt := configuration.get("auth", "salt", fallback=None):
return salt
return MUser.generate_password(salt_length)
@staticmethod
def user_clear(configuration: Configuration, user: MUser) -> None:
"""
remove user user from configuration file in case if it exists
:param configuration: configuration instance
:param user: user descriptor
"""
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
if not configuration.has_option(section, user.username):
continue
configuration.remove_option(section, user.username)
@staticmethod
def user_create(args: argparse.Namespace) -> MUser:
"""
create user descriptor from arguments
:param args: command line args
:return: built user descriptor
"""
user = MUser(args.username, args.password, args.role)
if user.password is None:
user.password = getpass.getpass()
return user

View File

@ -31,6 +31,7 @@ class Web(Handler):
web server handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
@classmethod

View File

@ -21,7 +21,6 @@ from __future__ import annotations
import argparse
import logging
import os
from pathlib import Path
from types import TracebackType
@ -29,8 +28,9 @@ from typing import Literal, Optional, Type
from ahriman import version
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
from ahriman.core.exceptions import DuplicateRun
from ahriman.core.status.client import Client
from ahriman.core.util import check_user
from ahriman.models.build_status import BuildStatusEnum
@ -105,10 +105,7 @@ class Lock:
"""
if self.unsafe:
return
current_uid = os.getuid()
root_uid = self.root.stat().st_uid
if current_uid != root_uid:
raise UnsafeRun(current_uid, root_uid)
check_user(self.root)
def clear(self) -> None:
"""

View File

@ -104,7 +104,6 @@ class OAuth(Mapping):
access_token, _ = await client.get_access_token(code, redirect_uri=self.redirect_uri)
client.access_token = access_token
print(f"HEEELOOOO {client}")
user, _ = await client.user_info()
username: str = user.email # type: ignore
return username

View File

@ -0,0 +1,150 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import logging
from pathlib import Path
from typing import List
from ahriman.core.util import check_output
class Sources:
"""
helper to download package sources (PKGBUILD etc)
:cvar logger: class logger
"""
logger = logging.getLogger("build_details")
_branch = "master" # in case if BLM would like to change it
_check_output = check_output
@staticmethod
def add(sources_dir: Path, *pattern: str) -> None:
"""
track found files via git
:param sources_dir: local path to git repository
:param pattern: glob patterns
"""
# glob directory to find files which match the specified patterns
found_files: List[Path] = []
for glob in pattern:
found_files.extend(sources_dir.glob(glob))
Sources.logger.info("found matching files %s", found_files)
# add them to index
Sources._check_output("git", "add", "--intent-to-add",
*[str(fn.relative_to(sources_dir)) for fn in found_files],
exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod
def diff(sources_dir: Path, patch_path: Path) -> None:
"""
generate diff from the current version and write it to the output file
:param sources_dir: local path to git repository
:param patch_path: path to result patch
"""
patch = Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
patch_path.write_text(patch)
@staticmethod
def fetch(sources_dir: Path, remote: str) -> None:
"""
either clone repository or update it to origin/`branch`
:param sources_dir: local path to fetch
:param remote: remote target (from where to fetch)
"""
# local directory exists and there is .git directory
is_initialized_git = (sources_dir / ".git").is_dir()
if is_initialized_git and not Sources.has_remotes(sources_dir):
# there is git repository, but no remote configured so far
Sources.logger.info("skip update at %s because there are no branches configured", sources_dir)
return
if is_initialized_git:
Sources.logger.info("update HEAD to remote at %s", sources_dir)
Sources._check_output("git", "fetch", "origin", Sources._branch,
exception=None, cwd=sources_dir, logger=Sources.logger)
else:
Sources.logger.info("clone remote %s to %s", remote, sources_dir)
Sources._check_output("git", "clone", remote, str(sources_dir), exception=None, logger=Sources.logger)
# and now force reset to our branch
Sources._check_output("git", "checkout", "--force", Sources._branch,
exception=None, cwd=sources_dir, logger=Sources.logger)
Sources._check_output("git", "reset", "--hard", f"origin/{Sources._branch}",
exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod
def has_remotes(sources_dir: Path) -> bool:
"""
check if there are remotes for the repository
:param sources_dir: local path to git repository
:return: True in case if there is any remote and false otherwise
"""
remotes = Sources._check_output("git", "remote", exception=None, cwd=sources_dir, logger=Sources.logger)
return bool(remotes)
@staticmethod
def init(sources_dir: Path) -> None:
"""
create empty git repository at the specified path
:param sources_dir: local path to sources
"""
Sources._check_output("git", "init", "--initial-branch", Sources._branch,
exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod
def load(sources_dir: Path, remote: str, patch_dir: Path) -> None:
"""
fetch sources from remote and apply patches
:param sources_dir: local path to fetch
:param remote: remote target (from where to fetch)
:param patch_dir: path to directory with package patches
"""
Sources.fetch(sources_dir, remote)
Sources.patch_apply(sources_dir, patch_dir)
@staticmethod
def patch_apply(sources_dir: Path, patch_dir: Path) -> None:
"""
apply patches if any
:param sources_dir: local path to directory with git sources
:param patch_dir: path to directory with package patches
"""
# check if even there are patches
if not patch_dir.is_dir():
return # no patches provided
# find everything that looks like patch and sort it
patches = sorted(patch_dir.glob("*.patch"))
Sources.logger.info("found %s patches", patches)
for patch in patches:
Sources.logger.info("apply patch %s", patch.name)
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", str(patch),
exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod
def patch_create(sources_dir: Path, patch_path: Path, *pattern: str) -> None:
"""
create patch set for the specified local path
:param sources_dir: local path to git repository
:param patch_path: path to result patch
:param pattern: glob patterns
"""
Sources.add(sources_dir, *pattern)
Sources.diff(sources_dir, patch_path)

View File

@ -23,6 +23,7 @@ import shutil
from pathlib import Path
from typing import List, Optional
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import BuildFailed
from ahriman.core.util import check_output
@ -48,7 +49,7 @@ class Task:
:param configuration: configuration instance
:param paths: repository paths instance
"""
self.logger = logging.getLogger("builder")
self.logger = logging.getLogger("root")
self.build_logger = logging.getLogger("build_details")
self.package = package
self.paths = paths
@ -58,38 +59,6 @@ class Task:
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
@property
def cache_path(self) -> Path:
"""
:return: path to cached packages
"""
return self.paths.cache / self.package.base
@property
def git_path(self) -> Path:
"""
:return: path to clone package from git
"""
return self.paths.sources / self.package.base
@staticmethod
def fetch(local: Path, remote: str, branch: str = "master") -> None:
"""
either clone repository or update it to origin/`branch`
:param local: local path to fetch
:param remote: remote target (from where to fetch)
:param branch: branch name to checkout, master by default
"""
logger = logging.getLogger("build_details")
# local directory exists and there is .git directory
if (local / ".git").is_dir():
Task._check_output("git", "fetch", "origin", branch, exception=None, cwd=local, logger=logger)
else:
Task._check_output("git", "clone", remote, str(local), exception=None, logger=logger)
# and now force reset to our branch
Task._check_output("git", "checkout", "--force", branch, exception=None, cwd=local, logger=logger)
Task._check_output("git", "reset", "--hard", f"origin/{branch}", exception=None, cwd=local, logger=logger)
def build(self) -> List[Path]:
"""
run package build
@ -104,13 +73,13 @@ class Task:
Task._check_output(
*command,
exception=BuildFailed(self.package.base),
cwd=self.git_path,
cwd=self.paths.sources_for(self.package.base),
logger=self.build_logger)
# well it is not actually correct, but we can deal with it
packages = Task._check_output("makepkg", "--packagelist",
exception=BuildFailed(self.package.base),
cwd=self.git_path,
cwd=self.paths.sources_for(self.package.base),
logger=self.build_logger).splitlines()
return [Path(package) for package in packages]
@ -119,8 +88,8 @@ class Task:
fetch package from git
:param path: optional local path to fetch. If not set default path will be used
"""
git_path = path or self.git_path
if self.cache_path.is_dir():
git_path = path or self.paths.sources_for(self.package.base)
if self.paths.cache_for(self.package.base).is_dir():
# no need to clone whole repository, just copy from cache first
shutil.copytree(self.cache_path, git_path)
return self.fetch(git_path, self.package.git_url)
shutil.copytree(self.paths.cache_for(self.package.base), git_path)
Sources.load(git_path, self.package.git_url, self.paths.patches_for(self.package.base))

View File

@ -24,7 +24,7 @@ import logging
from logging.config import fileConfig
from pathlib import Path
from typing import Any, Dict, List, Optional, Type
from typing import Any, Dict, List, Optional, Tuple, Type
from ahriman.core.exceptions import InitializeException
@ -39,10 +39,10 @@ class Configuration(configparser.RawConfigParser):
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
"""
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s"
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"]
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
def __init__(self) -> None:
"""
@ -70,18 +70,18 @@ class Configuration(configparser.RawConfigParser):
return self.getpath("settings", "logging")
@classmethod
def from_path(cls: Type[Configuration], path: Path, architecture: str, logfile: bool) -> Configuration:
def from_path(cls: Type[Configuration], path: Path, architecture: str, quiet: bool) -> Configuration:
"""
constructor with full object initialization
:param path: path to root configuration file
:param architecture: repository architecture
:param logfile: use log file to output messages
:param quiet: force disable any log messages
:return: configuration instance
"""
config = cls()
config.load(path)
config.merge_sections(architecture)
config.load_logging(logfile)
config.load_logging(quiet)
return config
@staticmethod
@ -121,6 +121,26 @@ class Configuration(configparser.RawConfigParser):
def getpath(self, *args: Any, **kwargs: Any) -> Path: ...
def gettype(self, section: str, architecture: str) -> Tuple[str, str]:
"""
get type variable with fallback to old logic
Despite the fact that it has same semantics as other get* methods, but it has different argument list
:param section: section name
:param architecture: repository architecture
:return: section name and found type name
"""
group_type = self.get(section, "type", fallback=None) # new-style logic
if group_type is not None:
return section, group_type
# okay lets check for the section with architecture name
full_section = self.section_name(section, architecture)
if self.has_section(full_section):
return full_section, section
# okay lets just use section as type
if not self.has_section(section):
raise configparser.NoSectionError(section)
return section, section
def load(self, path: Path) -> None:
"""
fully load configuration
@ -142,27 +162,20 @@ class Configuration(configparser.RawConfigParser):
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
pass
def load_logging(self, logfile: bool) -> None:
def load_logging(self, quiet: bool) -> None:
"""
setup logging settings from configuration
:param logfile: use log file to output messages
:param quiet: force disable any log messages
"""
def file_logger() -> None:
try:
path = self.logging_path
fileConfig(path)
except (FileNotFoundError, PermissionError):
console_logger()
logging.exception("could not create logfile, fallback to stderr")
def console_logger() -> None:
try:
path = self.logging_path
fileConfig(path)
except Exception:
logging.basicConfig(filename=None, format=self.DEFAULT_LOG_FORMAT,
level=self.DEFAULT_LOG_LEVEL)
if logfile:
file_logger()
else:
console_logger()
logging.exception("could not load logging from configuration, fallback to stderr")
if quiet:
logging.disable(logging.WARNING) # only print errors here
def merge_sections(self, architecture: str) -> None:
"""

View File

@ -42,7 +42,8 @@ class DuplicateRun(RuntimeError):
"""
default constructor
"""
RuntimeError.__init__(self, "Another application instance is run")
RuntimeError.__init__(
self, "Another application instance is run. This error can be suppressed by using --force flag.")
class DuplicateUser(ValueError):
@ -71,19 +72,6 @@ class InitializeException(RuntimeError):
RuntimeError.__init__(self, f"Could not load service: {details}")
class InvalidCommand(ValueError):
"""
exception raised on invalid command line options
"""
def __init__(self, details: Any) -> None:
"""
default constructor
:param details" error details
"""
ValueError.__init__(self, details)
class InvalidOption(ValueError):
"""
exception which will be raised on configuration errors

View File

@ -45,27 +45,28 @@ class Email(Report, JinjaTemplate):
:ivar user: username to authenticate via SMTP
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
"""
Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, "email", configuration)
JinjaTemplate.__init__(self, section, configuration)
self.full_template_path = configuration.getpath("email", "full_template_path", fallback=None)
self.template_path = configuration.getpath("email", "template_path")
self.full_template_path = configuration.getpath(section, "full_template_path", fallback=None)
self.template_path = configuration.getpath(section, "template_path")
# base smtp settings
self.host = configuration.get("email", "host")
self.no_empty_report = configuration.getboolean("email", "no_empty_report", fallback=True)
self.password = configuration.get("email", "password", fallback=None)
self.port = configuration.getint("email", "port")
self.receivers = configuration.getlist("email", "receivers")
self.sender = configuration.get("email", "sender")
self.ssl = SmtpSSLSettings.from_option(configuration.get("email", "ssl", fallback="disabled"))
self.user = configuration.get("email", "user", fallback=None)
self.host = configuration.get(section, "host")
self.no_empty_report = configuration.getboolean(section, "no_empty_report", fallback=True)
self.password = configuration.get(section, "password", fallback=None)
self.port = configuration.getint(section, "port")
self.receivers = configuration.getlist(section, "receivers")
self.sender = configuration.get(section, "sender")
self.ssl = SmtpSSLSettings.from_option(configuration.get(section, "ssl", fallback="disabled"))
self.user = configuration.get(section, "user", fallback=None)
def _send(self, text: str, attachment: Dict[str, str]) -> None:
"""

View File

@ -31,17 +31,18 @@ class HTML(Report, JinjaTemplate):
:ivar report_path: output path to html report
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
"""
Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, "html", configuration)
JinjaTemplate.__init__(self, section, configuration)
self.report_path = configuration.getpath("html", "path")
self.template_path = configuration.getpath("html", "template_path")
self.report_path = configuration.getpath(section, "path")
self.template_path = configuration.getpath(section, "template_path")
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None:
"""

View File

@ -43,7 +43,7 @@ class Report:
:param architecture: repository architecture
:param configuration: configuration instance
"""
self.logger = logging.getLogger("builder")
self.logger = logging.getLogger("root")
self.architecture = architecture
self.configuration = configuration
@ -53,16 +53,17 @@ class Report:
load client from settings
:param architecture: repository architecture
:param configuration: configuration instance
:param target: target to generate report (e.g. html)
:param target: target to generate report aka section name (e.g. html)
:return: client according to current settings
"""
provider = ReportSettings.from_option(target)
section, provider_name = configuration.gettype(target, architecture)
provider = ReportSettings.from_option(provider_name)
if provider == ReportSettings.HTML:
from ahriman.core.report.html import HTML
return HTML(architecture, configuration)
return HTML(architecture, configuration, section)
if provider == ReportSettings.Email:
from ahriman.core.report.email import Email
return Email(architecture, configuration)
return Email(architecture, configuration, section)
return cls(architecture, configuration) # should never happen
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None:

View File

@ -17,3 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.repository.repository import Repository

View File

@ -76,3 +76,11 @@ class Cleaner(Properties):
self.logger.info("clear built packages directory")
for package in self.packages_built():
package.unlink()
def clear_patches(self) -> None:
"""
clear directory with patches
"""
self.logger.info("clear patches directory")
for package in self.paths.patches.iterdir():
shutil.rmtree(package)

View File

@ -72,9 +72,16 @@ class Executor(Cleaner):
:param packages: list of package names or bases to remove
:return: path to repository database
"""
def remove_single(package: str, fn: Path) -> None:
def remove_base(package_base: str) -> None:
try:
self.repo.remove(package, fn)
self.paths.tree_clear(package_base) # remove all internal files
self.reporter.remove(package_base) # we only update status page in case of base removal
except Exception:
self.logger.exception("could not remove base %s", package_base)
def remove_package(package: str, fn: Path) -> None:
try:
self.repo.remove(package, fn) # remove the package itself
except Exception:
self.logger.exception("could not remove %s", package)
@ -86,7 +93,7 @@ class Executor(Cleaner):
for package, properties in local.packages.items()
if properties.filename is not None
}
self.reporter.remove(local.base) # we only update status page in case of base removal
remove_base(local.base)
elif requested.intersection(local.packages.keys()):
to_remove = {
package: Path(properties.filename)
@ -95,8 +102,9 @@ class Executor(Cleaner):
}
else:
to_remove = {}
for package, filename in to_remove.items():
remove_single(package, filename)
remove_package(package, filename)
return self.repo.repo_path
@ -130,17 +138,17 @@ class Executor(Cleaner):
:param packages: list of filenames to run
:return: path to repository database
"""
def update_single(fn: Optional[str], base: str) -> None:
if fn is None:
def update_single(name: Optional[str], base: str) -> None:
if name is None:
self.logger.warning("received empty package name for base %s", base)
return # suppress type checking, it never can be none actually
# in theory it might be NOT packages directory, but we suppose it is
full_path = self.paths.packages / fn
files = self.sign.sign_package(full_path, base)
full_path = self.paths.packages / name
files = self.sign.process_sign_package(full_path, base)
for src in files:
dst = self.paths.repository / src.name
shutil.move(src, dst)
package_path = self.paths.repository / fn
package_path = self.paths.repository / name
self.repo.add(package_path)
# we are iterating over bases, not single packages

View File

@ -22,8 +22,10 @@ import logging
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.repo import Repo
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import UnsafeRun
from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client
from ahriman.core.util import check_user
from ahriman.models.repository_paths import RepositoryPaths
@ -50,7 +52,7 @@ class Properties:
:param configuration: configuration instance
:param no_report: force disable reporting
"""
self.logger = logging.getLogger("builder")
self.logger = logging.getLogger("root")
self.architecture = architecture
self.configuration = configuration
@ -58,7 +60,11 @@ class Properties:
self.name = configuration.get("repository", "name")
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
self.paths.create_tree()
try:
check_user(self.paths.root)
self.paths.tree_create()
except UnsafeRun:
self.logger.warning("root owner differs from the current user, skipping tree creation")
self.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[])
self.pacman = Pacman(configuration)

View File

@ -72,16 +72,16 @@ class UpdateHandler(Cleaner):
result: List[Package] = []
known_bases = {package.base for package in self.packages()}
for fn in self.paths.manual.iterdir():
for filename in self.paths.manual.iterdir():
try:
local = Package.load(fn, self.pacman, self.aur_url)
local = Package.load(filename, self.pacman, self.aur_url)
result.append(local)
if local.base not in known_bases:
self.reporter.set_unknown(local)
else:
self.reporter.set_pending(local.base)
except Exception:
self.logger.exception("could not add package from %s", fn)
self.logger.exception("could not add package from %s", filename)
self.clear_manual()
return result

View File

@ -88,7 +88,7 @@ class GPG:
default_key = configuration.get("sign", "key") if targets else None
return targets, default_key
def download_key(self, server: str, key: str) -> str:
def key_download(self, server: str, key: str) -> str:
"""
download key from public PGP server
:param server: public PGP server which will be used to download the key
@ -108,13 +108,13 @@ class GPG:
raise
return response.text
def import_key(self, server: str, key: str) -> None:
def key_import(self, server: str, key: str) -> None:
"""
import key to current user and sign it locally
:param server: public PGP server which will be used to download the key
:param key: key ID to import
"""
key_body = self.download_key(server, key)
key_body = self.key_download(server, key)
GPG._check_output("gpg", "--import", input_data=key_body, exception=None, logger=self.logger)
GPG._check_output("gpg", "--quick-lsign-key", key, exception=None, logger=self.logger)
@ -131,7 +131,7 @@ class GPG:
logger=self.logger)
return [path, path.parent / f"{path.name}.sig"]
def sign_package(self, path: Path, base: str) -> List[Path]:
def process_sign_package(self, path: Path, base: str) -> List[Path]:
"""
sign package if required by configuration
:param path: path to file to sign
@ -146,7 +146,7 @@ class GPG:
return [path]
return self.process(path, key)
def sign_repository(self, path: Path) -> List[Path]:
def process_sign_repository(self, path: Path) -> List[Path]:
"""
sign repository if required by configuration
:note: more likely you just want to pass `repository_sign_args` to repo wrapper

View File

@ -25,7 +25,7 @@ from typing import Any, Dict, List, Optional, Tuple
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import UnknownPackage
from ahriman.core.repository.repository import Repository
from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
@ -126,11 +126,10 @@ class Watcher:
"""
for package in self.repository.packages():
# get status of build or assign unknown
current = self.known.get(package.base)
if current is None:
status = BuildStatus()
else:
if (current := self.known.get(package.base)) is not None:
_, status = current
else:
status = BuildStatus()
self.known[package.base] = (package, status)
self._cache_load()

View File

@ -111,7 +111,7 @@ class WebClient(Client):
try:
response = self.__session.post(self._login_url, json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
self.logger.exception("could not login as %s: %s", self.user, exception_response_text(e))
except Exception:
self.logger.exception("could not login as %s", self.user)
@ -138,7 +138,7 @@ class WebClient(Client):
try:
response = self.__session.post(self._package_url(package.base), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
self.logger.exception("could not add %s: %s", package.base, exception_response_text(e))
except Exception:
self.logger.exception("could not add %s", package.base)
@ -158,7 +158,7 @@ class WebClient(Client):
(Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
for package in status_json
]
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
self.logger.exception("could not get %s: %s", base, exception_response_text(e))
except Exception:
self.logger.exception("could not get %s", base)
@ -175,7 +175,7 @@ class WebClient(Client):
status_json = response.json()
return InternalStatus.from_json(status_json)
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
self.logger.exception("could not get web service status: %s", exception_response_text(e))
except Exception:
self.logger.exception("could not get web service status")
@ -192,7 +192,7 @@ class WebClient(Client):
status_json = response.json()
return BuildStatus.from_json(status_json)
except requests.exceptions.HTTPError as e:
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")
@ -205,7 +205,7 @@ class WebClient(Client):
try:
response = self.__session.post(self._reload_auth_url)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
self.logger.exception("could not reload auth module: %s", exception_response_text(e))
except Exception:
self.logger.exception("could not reload auth module")
@ -218,7 +218,7 @@ class WebClient(Client):
try:
response = self.__session.delete(self._package_url(base))
response.raise_for_status()
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
self.logger.exception("could not delete %s: %s", base, exception_response_text(e))
except Exception:
self.logger.exception("could not delete %s", base)
@ -234,7 +234,7 @@ class WebClient(Client):
try:
response = self.__session.post(self._package_url(base), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
self.logger.exception("could not update %s: %s", base, exception_response_text(e))
except Exception:
self.logger.exception("could not update %s", base)
@ -249,7 +249,7 @@ class WebClient(Client):
try:
response = self.__session.post(self._ahriman_url, json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
self.logger.exception("could not update service status: %s", exception_response_text(e))
except Exception:
self.logger.exception("could not update service status")

View File

@ -25,8 +25,9 @@ import tempfile
from pathlib import Path
from typing import Iterable, List, Set, Type
from ahriman.core.build_tools.task import Task
from ahriman.core.build_tools.sources import Sources
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
class Leaf:
@ -53,15 +54,16 @@ class Leaf:
return self.package.packages.keys()
@classmethod
def load(cls: Type[Leaf], package: Package) -> Leaf:
def load(cls: Type[Leaf], package: Package, paths: RepositoryPaths) -> Leaf:
"""
load leaf from package with dependencies
:param package: package properties
:param paths: repository paths instance
:return: loaded class
"""
clone_dir = Path(tempfile.mkdtemp())
try:
Task.fetch(clone_dir, package.git_url)
Sources.load(clone_dir, package.git_url, paths.patches_for(package.base))
dependencies = Package.dependencies(clone_dir)
finally:
shutil.rmtree(clone_dir, ignore_errors=True)
@ -93,13 +95,14 @@ class Tree:
self.leaves = leaves
@classmethod
def load(cls: Type[Tree], packages: Iterable[Package]) -> Tree:
def load(cls: Type[Tree], packages: Iterable[Package], paths: RepositoryPaths) -> Tree:
"""
load tree from packages
:param packages: packages list
:param paths: repository paths instance
:return: loaded class
"""
return cls([Leaf.load(package) for package in packages])
return cls([Leaf.load(package, paths) for package in packages])
def levels(self) -> List[List[Package]]:
"""

View File

@ -0,0 +1,164 @@
#
# Copyright (c) 2021 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 mimetypes
import requests
from pathlib import Path
from typing import Any, Dict, Iterable, Optional
from ahriman.core.configuration import Configuration
from ahriman.core.upload.http_upload import HttpUpload
from ahriman.core.util import walk
from ahriman.models.package import Package
class Github(HttpUpload):
"""
upload files to github releases
:ivar gh_owner: github repository owner
:ivar gh_repository: github repository name
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
"""
HttpUpload.__init__(self, architecture, configuration, section)
self.gh_owner = configuration.get(section, "owner")
self.gh_repository = configuration.get(section, "repository")
def asset_remove(self, release: Dict[str, Any], name: str) -> None:
"""
remove asset from the release by name
:param release: release object
:param name: asset name
"""
try:
asset = next(asset for asset in release["assets"] if asset["name"] == name)
self._request("DELETE", asset["url"])
except StopIteration:
self.logger.info("no asset %s found in release %s", name, release["name"])
def asset_upload(self, release: Dict[str, Any], path: Path) -> None:
"""
upload asset to the release
:param release: release object
:param path: path to local file
"""
exists = any(path.name == asset["name"] for asset in release["assets"])
if exists:
self.asset_remove(release, path.name)
(url, _) = release["upload_url"].split("{") # it is parametrized url
(mime, _) = mimetypes.guess_type(path)
headers = {"Content-Type": mime} if mime is not None else {"Content-Type": "application/octet-stream"}
self._request("POST", url, params={"name": path.name}, data=path.open("rb"), headers=headers)
def get_local_files(self, path: Path) -> Dict[Path, str]:
"""
get all local files and their calculated checksums
:param path: local path to sync
:return: map of path objects to its checksum
"""
return {
local_file: self.calculate_hash(local_file)
for local_file in walk(path)
}
def files_remove(self, release: Dict[str, Any], local_files: Dict[Path, str], remote_files: Dict[str, str]) -> None:
"""
remove files from github
:param release: release object
:param local_files: map of local file paths to its checksum
:param remote_files: map of the remote files and its checksum
"""
local_filenames = {local_file.name for local_file in local_files}
for remote_file in remote_files:
if remote_file in local_filenames:
continue
self.asset_remove(release, remote_file)
def files_upload(self, release: Dict[str, Any], local_files: Dict[Path, str], remote_files: Dict[str, str]) -> None:
"""
upload files to github
:param release: release object
:param local_files: map of local file paths to its checksum
:param remote_files: map of the remote files and its checksum
"""
for local_file, checksum in local_files.items():
remote_checksum = remote_files.get(local_file.name)
if remote_checksum == checksum:
continue
self.asset_upload(release, local_file)
def release_create(self) -> Dict[str, Any]:
"""
create empty release
:return: github API release object for the new release
"""
response = self._request("POST", f"https://api.github.com/repos/{self.gh_owner}/{self.gh_repository}/releases",
json={"tag_name": self.architecture, "name": self.architecture})
release: Dict[str, Any] = response.json()
return release
def release_get(self) -> Optional[Dict[str, Any]]:
"""
get release object if any
:return: github API release object if release found and None otherwise
"""
try:
response = self._request(
"GET",
f"https://api.github.com/repos/{self.gh_owner}/{self.gh_repository}/releases/tags/{self.architecture}")
release: Dict[str, Any] = response.json()
return release
except requests.HTTPError as e:
status_code = e.response.status_code if e.response is not None else None
if status_code == 404:
return None
raise
def release_update(self, release: Dict[str, Any], body: str) -> None:
"""
update release
:param release: release object
:param body: new release body
"""
self._request("POST", release["url"], json={"body": body})
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
"""
sync data to remote server
:param path: local path to sync
:param built_packages: list of packages which has just been built
"""
release = self.release_get()
if release is None:
release = self.release_create()
body: str = release.get("body") or ""
remote_files = self.get_hashes(body)
local_files = self.get_local_files(path)
self.files_upload(release, local_files, remote_files)
self.files_remove(release, local_files, remote_files)
self.release_update(release, self.get_body(local_files))

View File

@ -0,0 +1,96 @@
#
# Copyright (c) 2021 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 hashlib
import requests
from pathlib import Path
from typing import Any, Dict
from ahriman.core.configuration import Configuration
from ahriman.core.upload.upload import Upload
from ahriman.core.util import exception_response_text
class HttpUpload(Upload):
"""
helper for the http based uploads
:ivar auth: HTTP auth object
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: configuration section name
"""
Upload.__init__(self, architecture, configuration)
password = configuration.get(section, "password")
username = configuration.get(section, "username")
self.auth = (password, username)
@staticmethod
def calculate_hash(path: Path) -> str:
"""
calculate file checksum
:param path: path to local file
:return: calculated checksum of the file
"""
with path.open("rb") as local_file:
md5 = hashlib.md5(local_file.read()) # nosec
return md5.hexdigest()
@staticmethod
def get_body(local_files: Dict[Path, str]) -> str:
"""
generate release body from the checksums as returned from HttpUpload.get_hashes method
:param local_files: map of the paths to its checksum
:return: body to be inserted into release
"""
return "\n".join(f"{file.name} {md5}" for file, md5 in sorted(local_files.items()))
@staticmethod
def get_hashes(body: str) -> Dict[str, str]:
"""
get checksums of the content from the repository
:param body: release string body object
:return: map of the filename to its checksum as it is written in body
"""
files = {}
for line in body.splitlines():
file, md5 = line.split()
files[file] = md5
return files
def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
"""
request wrapper
:param method: request method
:param url: request url
:param kwargs: request parameters to be passed as is
:return: request response object
"""
try:
response = requests.request(method, url, auth=self.auth, **kwargs)
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not perform %s request to %s: %s", method, url, exception_response_text(e))
raise
return response

View File

@ -35,15 +35,16 @@ class Rsync(Upload):
_check_output = check_output
def __init__(self, architecture: str, configuration: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
"""
Upload.__init__(self, architecture, configuration)
self.command = configuration.getlist("rsync", "command")
self.remote = configuration.get("rsync", "remote")
self.command = configuration.getlist(section, "command")
self.remote = configuration.get(section, "remote")
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
"""

View File

@ -22,10 +22,11 @@ import hashlib
import mimetypes
from pathlib import Path
from typing import Any, Dict, Generator, Iterable
from typing import Any, Dict, Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.upload.upload import Upload
from ahriman.core.util import walk
from ahriman.models.package import Package
@ -36,15 +37,15 @@ class S3(Upload):
:ivar chunk_size: chunk size for calculating checksums
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
"""
Upload.__init__(self, architecture, configuration)
self.bucket = self.get_bucket(configuration)
self.chunk_size = configuration.getint("s3", "chunk_size", fallback=8 * 1024 * 1024)
self.bucket = self.get_bucket(configuration, section)
self.chunk_size = configuration.getint(section, "chunk_size", fallback=8 * 1024 * 1024)
@staticmethod
def calculate_etag(path: Path, chunk_size: int) -> str:
@ -69,20 +70,21 @@ class S3(Upload):
return f"{checksum.hexdigest()}{suffix}"
@staticmethod
def get_bucket(configuration: Configuration) -> Any:
def get_bucket(configuration: Configuration, section: str) -> Any:
"""
create resource client from configuration
:param configuration: configuration instance
:param section: settings section name
:return: amazon client
"""
client = boto3.resource(service_name="s3",
region_name=configuration.get("s3", "region"),
aws_access_key_id=configuration.get("s3", "access_key"),
aws_secret_access_key=configuration.get("s3", "secret_key"))
return client.Bucket(configuration.get("s3", "bucket"))
region_name=configuration.get(section, "region"),
aws_access_key_id=configuration.get(section, "access_key"),
aws_secret_access_key=configuration.get(section, "secret_key"))
return client.Bucket(configuration.get(section, "bucket"))
@staticmethod
def remove_files(local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
def files_remove(local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
"""
remove files which have been removed locally
:param local_files: map of local path object to its checksum
@ -93,19 +95,33 @@ class S3(Upload):
continue
remote_object.delete()
def files_upload(self, path: Path, local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
"""
upload changed files to s3
:param path: local path to sync
:param local_files: map of local path object to its checksum
:param remote_objects: map of remote path object to the remote s3 object
"""
for local_file, checksum in local_files.items():
remote_object = remote_objects.get(local_file)
# 0 and -1 elements are " (double quote)
remote_checksum = remote_object.e_tag[1:-1] if remote_object is not None else None
if remote_checksum == checksum:
continue
local_path = path / local_file
remote_path = Path(self.architecture) / local_file
(mime, _) = mimetypes.guess_type(local_path)
extra_args = {"ContentType": mime} if mime is not None else None
self.bucket.upload_file(Filename=str(local_path), Key=str(remote_path), ExtraArgs=extra_args)
def get_local_files(self, path: Path) -> Dict[Path, str]:
"""
get all local files and their calculated checksums
:param path: local path to sync
:return: map of path object to its checksum
"""
# credits to https://stackoverflow.com/a/64915960
def walk(directory_path: Path) -> Generator[Path, None, None]:
for element in directory_path.iterdir():
if element.is_dir():
yield from walk(element)
continue
yield element
return {
local_file.relative_to(path): self.calculate_etag(local_file, self.chunk_size)
for local_file in walk(path)
@ -128,26 +144,5 @@ class S3(Upload):
remote_objects = self.get_remote_objects()
local_files = self.get_local_files(path)
self.upload_files(path, local_files, remote_objects)
self.remove_files(local_files, remote_objects)
def upload_files(self, path: Path, local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
"""
upload changed files to s3
:param path: local path to sync
:param local_files: map of local path object to its checksum
:param remote_objects: map of remote path object to the remote s3 object
"""
for local_file, checksum in local_files.items():
remote_object = remote_objects.get(local_file)
# 0 and -1 elements are " (double quote)
remote_checksum = remote_object.e_tag[1:-1] if remote_object is not None else None
if remote_checksum == checksum:
continue
local_path = path / local_file
remote_path = Path(self.architecture) / local_file
(mime, _) = mimetypes.guess_type(local_path)
extra_args = {"ContentType": mime} if mime is not None else None
self.bucket.upload_file(Filename=str(local_path), Key=str(remote_path), ExtraArgs=extra_args)
self.files_upload(path, local_files, remote_objects)
self.files_remove(local_files, remote_objects)

View File

@ -44,7 +44,7 @@ class Upload:
:param architecture: repository architecture
:param configuration: configuration instance
"""
self.logger = logging.getLogger("builder")
self.logger = logging.getLogger("root")
self.architecture = architecture
self.config = configuration
@ -57,13 +57,17 @@ class Upload:
:param target: target to run sync (e.g. s3)
:return: client according to current settings
"""
provider = UploadSettings.from_option(target)
section, provider_name = configuration.gettype(target, architecture)
provider = UploadSettings.from_option(provider_name)
if provider == UploadSettings.Rsync:
from ahriman.core.upload.rsync import Rsync
return Rsync(architecture, configuration)
return Rsync(architecture, configuration, section)
if provider == UploadSettings.S3:
from ahriman.core.upload.s3 import S3
return S3(architecture, configuration)
return S3(architecture, configuration, section)
if provider == UploadSettings.Github:
from ahriman.core.upload.github import Github
return Github(architecture, configuration, section)
return cls(architecture, configuration) # should never happen
def run(self, path: Path, built_packages: Iterable[Package]) -> None:

View File

@ -17,15 +17,35 @@
# 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 aur # type: ignore
import datetime
import os
import subprocess
import requests
from logging import Logger
from pathlib import Path
from typing import Optional, Union
from typing import Any, Dict, Generator, Iterable, List, Optional, Union
from ahriman.core.exceptions import InvalidOption
from ahriman.core.exceptions import InvalidOption, UnsafeRun
def aur_search(*terms: str) -> List[aur.Package]:
"""
search in AUR by using API with multiple words. This method is required in order to handle
https://bugs.archlinux.org/task/49133. In addition short words will be dropped
:param terms: search terms, e.g. "ahriman", "is", "cool"
:return: list of packages each of them matches all search terms
"""
packages: Dict[str, aur.Package] = {}
for term in filter(lambda word: len(word) > 3, terms):
portion = aur.search(term)
packages = {
package.package_base: package
for package in portion
if package.package_base in packages or not packages
}
return list(packages.values())
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
@ -54,6 +74,19 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
raise exception or e
def check_user(root: Path) -> None:
"""
check if current user is the owner of the root
:param root: root directory (i.e. ahriman home)
"""
if not root.exists():
return # no directory found, skip check
current_uid = os.getuid()
root_uid = root.stat().st_uid
if current_uid != root_uid:
raise UnsafeRun(current_uid, root_uid)
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
"""
safe response exception text generation
@ -64,6 +97,16 @@ def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
return result
def filter_json(source: Dict[str, Any], known_fields: Iterable[str]) -> Dict[str, Any]:
"""
filter json object by fields used for json-to-object conversion
:param source: raw json object
:param known_fields: list of fields which have to be known for the target object
:return: json object without unknown and empty fields
"""
return {key: value for key, value in source.items() if key in known_fields and value is not None}
def package_like(filename: Path) -> bool:
"""
check if file looks like package
@ -74,13 +117,17 @@ def package_like(filename: Path) -> bool:
return ".pkg." in name and not name.endswith(".sig")
def pretty_datetime(timestamp: Optional[Union[float, int]]) -> str:
def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str:
"""
convert datetime object to string
:param timestamp: datetime to convert
:return: pretty printable datetime as string
"""
return "" if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
if timestamp is None:
return ""
if isinstance(timestamp, (int, float)):
timestamp = datetime.datetime.utcfromtimestamp(timestamp)
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
def pretty_size(size: Optional[float], level: int = 0) -> str:
@ -106,3 +153,17 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
if size < 1024 or level >= 3:
return f"{size:.1f} {str_level()}"
return pretty_size(size / 1024, level + 1)
def walk(directory_path: Path) -> Generator[Path, None, None]:
"""
list all file paths in given directory
Credits to https://stackoverflow.com/a/64915960
:param directory_path: root directory path
:return: all found files in given directory with full path
"""
for element in directory_path.iterdir():
if element.is_dir():
yield from walk(element)
continue
yield element

View File

@ -0,0 +1,33 @@
#
# Copyright (c) 2021 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 enum import Enum
class Action(Enum):
"""
base action enumeration
:cvar List: list available values
:cvar Remove: remove everything from local storage
:cvar Update: update local storage or add to
"""
List = "list"
Remove = "remove"
Update = "update"

View File

@ -19,7 +19,7 @@
#
from __future__ import annotations
from enum import Enum, auto
from enum import Enum
from typing import Type
from ahriman.core.exceptions import InvalidOption
@ -33,9 +33,9 @@ class AuthSettings(Enum):
:cvar OAuth: OAuth based provider
"""
Disabled = auto()
Configuration = auto()
OAuth = auto()
Disabled = "disabled"
Configuration = "configuration"
OAuth = "oauth2"
@classmethod
def from_option(cls: Type[AuthSettings], value: str) -> AuthSettings:

View File

@ -21,10 +21,11 @@ from __future__ import annotations
import datetime
from dataclasses import dataclass, fields
from enum import Enum
from typing import Any, Dict, Optional, Type, Union
from typing import Any, Dict, Type
from ahriman.core.util import pretty_datetime
from ahriman.core.util import filter_json, pretty_datetime
class BuildStatusEnum(Enum):
@ -74,6 +75,7 @@ class BuildStatusEnum(Enum):
return "secondary"
@dataclass
class BuildStatus:
"""
build status holder
@ -81,15 +83,14 @@ class BuildStatus:
:ivar timestamp: build status update time
"""
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
timestamp: Optional[int] = None) -> None:
status: BuildStatusEnum = BuildStatusEnum.Unknown
timestamp: int = int(datetime.datetime.utcnow().timestamp())
def __post_init__(self) -> None:
"""
default constructor
:param status: current build status if known. `BuildStatusEnum.Unknown` will be used if not set
:param timestamp: build status timestamp. Current timestamp will be used if not set
convert status to enum type
"""
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())
self.status = BuildStatusEnum(self.status)
@classmethod
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
@ -98,7 +99,8 @@ class BuildStatus:
:param dump: json dump body
:return: status properties
"""
return cls(dump.get("status"), dump.get("timestamp"))
known_fields = [pair.name for pair in fields(cls)]
return cls(**filter_json(dump, known_fields))
def pretty_print(self) -> str:
"""
@ -116,20 +118,3 @@ class BuildStatus:
"status": self.status.value,
"timestamp": self.timestamp
}
def __eq__(self, other: Any) -> bool:
"""
compare object to other
:param other: other object to compare
:return: True in case if objects are equal
"""
if not isinstance(other, BuildStatus):
return False
return self.status == other.status and self.timestamp == other.timestamp
def __repr__(self) -> str:
"""
generate string representation of object
:return: unique string representation
"""
return f"BuildStatus(status={self.status.value}, timestamp={self.timestamp})"

View File

@ -22,6 +22,7 @@ from __future__ import annotations
from dataclasses import dataclass, fields
from typing import Any, Dict, List, Tuple, Type
from ahriman.core.util import filter_json
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
@ -54,8 +55,7 @@ class Counters:
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
dump = {key: value for key, value in dump.items() if key in known_fields}
return cls(**dump)
return cls(**filter_json(dump, known_fields))
@classmethod
def from_packages(cls: Type[Counters], packages: List[Tuple[Package, BuildStatus]]) -> Counters:

View File

@ -231,22 +231,18 @@ class Package:
if not self.is_vcs:
return self.version
from ahriman.core.build_tools.task import Task
from ahriman.core.build_tools.sources import Sources
clone_dir = paths.cache / self.base
logger = logging.getLogger("build_details")
Task.fetch(clone_dir, self.git_url)
Sources.load(paths.cache_for(self.base), self.git_url, paths.patches_for(self.base))
try:
# update pkgver first
Package._check_output("makepkg", "--nodeps", "--nobuild", exception=None, cwd=clone_dir, logger=logger)
Package._check_output("makepkg", "--nodeps", "--nobuild",
exception=None, cwd=paths.cache_for(self.base), logger=logger)
# generate new .SRCINFO and put it to parser
srcinfo_source = Package._check_output(
"makepkg",
"--printsrcinfo",
exception=None,
cwd=clone_dir,
logger=logger)
srcinfo_source = Package._check_output("makepkg", "--printsrcinfo",
exception=None, cwd=paths.cache_for(self.base), logger=logger)
srcinfo, errors = parse_srcinfo(srcinfo_source)
if errors:
raise InvalidPackageInfo(errors)

View File

@ -24,6 +24,8 @@ from pathlib import Path
from pyalpm import Package # type: ignore
from typing import Any, Dict, List, Optional, Type
from ahriman.core.util import filter_json
@dataclass
class PackageDescription:
@ -70,8 +72,7 @@ class PackageDescription:
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
dump = {key: value for key, value in dump.items() if key in known_fields}
return cls(**dump)
return cls(**filter_json(dump, known_fields))
@classmethod
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:

View File

@ -21,6 +21,7 @@ from __future__ import annotations
from enum import Enum
from pathlib import Path
from urllib.parse import urlparse
from ahriman.core.util import package_like
@ -30,14 +31,18 @@ class PackageSource(Enum):
package source for addition enumeration
:cvar Auto: automatically determine type of the source
:cvar Archive: source is a package archive
:cvar Directory: source is a directory which contains packages
:cvar AUR: source is an AUR package for which it should search
:cvar Directory: source is a directory which contains packages
:cvar Local: source is locally stored PKGBUILD
:cvar Remote: source is remote (http, ftp etc) link
"""
Auto = "auto"
Archive = "archive"
Directory = "directory"
AUR = "aur"
Directory = "directory"
Local = "local"
Remote = "remote"
def resolve(self, source: str) -> PackageSource:
"""
@ -47,9 +52,17 @@ class PackageSource(Enum):
"""
if self != PackageSource.Auto:
return self
maybe_path = Path(source)
maybe_url = urlparse(source) # handle file:// like paths
maybe_path = Path(maybe_url.path)
if maybe_url.scheme and maybe_url.scheme not in ("data", "file") and package_like(maybe_path):
return PackageSource.Remote
if (maybe_path / "PKGBUILD").is_file():
return PackageSource.Local
if maybe_path.is_dir():
return PackageSource.Directory
if maybe_path.is_file() and package_like(maybe_path):
return PackageSource.Archive
return PackageSource.AUR

View File

@ -0,0 +1,31 @@
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass
from typing import Any
@dataclass
class Property:
"""
holder of object properties descriptor
:ivar name: name of the property
:ivar value: property value
:ivar is_required: if set to True then this property is required
"""
name: str
value: Any
is_required: bool = False

View File

@ -19,7 +19,7 @@
#
from __future__ import annotations
from enum import Enum, auto
from enum import Enum
from typing import Type
from ahriman.core.exceptions import InvalidOption
@ -33,9 +33,9 @@ class ReportSettings(Enum):
:cvar Email: email report generation
"""
Disabled = auto() # for testing purpose
HTML = auto()
Email = auto()
Disabled = "disabled" # for testing purpose
HTML = "html"
Email = "email"
@classmethod
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:

View File

@ -19,6 +19,8 @@
#
from __future__ import annotations
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Set, Type
@ -64,6 +66,13 @@ class RepositoryPaths:
"""
return self.root / "packages" / self.architecture
@property
def patches(self) -> Path:
"""
:return: directory for source patches
"""
return self.root / "patches"
@property
def repository(self) -> Path:
"""
@ -92,13 +101,60 @@ class RepositoryPaths:
if path.is_dir()
}
def create_tree(self) -> None:
def cache_for(self, package_base: str) -> Path:
"""
get path to cached PKGBUILD and package sources for the package base
:param package_base: package base name
:return: full path to directory for specified package base cache
"""
return self.cache / package_base
def manual_for(self, package_base: str) -> Path:
"""
get manual path for specific package base
:param package_base: package base name
:return: full path to directory for specified package base manual updates
"""
return self.manual / package_base
def patches_for(self, package_base: str) -> Path:
"""
get patches path for specific package base
:param package_base: package base name
:return: full path to directory for specified package base patches
"""
return self.patches / package_base
def sources_for(self, package_base: str) -> Path:
"""
get path to directory from where build will start for the package base
:param package_base: package base name
:return: full path to directory for specified package base sources
"""
return self.sources / package_base
def tree_clear(self, package_base: str) -> None:
"""
clear package specific files
:param package_base: package base name
"""
for directory in (
self.cache_for(package_base),
self.manual_for(package_base),
self.patches_for(package_base),
self.sources_for(package_base)):
shutil.rmtree(directory, ignore_errors=True)
def tree_create(self) -> None:
"""
create ahriman working tree
"""
self.cache.mkdir(mode=0o755, parents=True, exist_ok=True)
self.chroot.mkdir(mode=0o755, parents=True, exist_ok=True)
self.manual.mkdir(mode=0o755, parents=True, exist_ok=True)
self.packages.mkdir(mode=0o755, parents=True, exist_ok=True)
self.repository.mkdir(mode=0o755, parents=True, exist_ok=True)
self.sources.mkdir(mode=0o755, parents=True, exist_ok=True)
for directory in (
self.cache,
self.chroot,
self.manual,
self.packages,
self.patches,
self.repository,
self.sources):
directory.mkdir(mode=0o755, parents=True, exist_ok=True)

View File

@ -19,7 +19,7 @@
#
from __future__ import annotations
from enum import Enum, auto
from enum import Enum
from typing import Type
from ahriman.core.exceptions import InvalidOption
@ -32,8 +32,8 @@ class SignSettings(Enum):
:cvar Repository: sign repository database file
"""
Packages = auto()
Repository = auto()
Packages = "pacakges"
Repository = "repository"
@classmethod
def from_option(cls: Type[SignSettings], value: str) -> SignSettings:

View File

@ -19,7 +19,7 @@
#
from __future__ import annotations
from enum import Enum, auto
from enum import Enum
from typing import Type
@ -31,9 +31,9 @@ class SmtpSSLSettings(Enum):
:cvar STARTTLS: use STARTTLS in normal SMTP client
"""
Disabled = auto()
SSL = auto()
STARTTLS = auto()
Disabled = "disabled"
SSL = "ssl"
STARTTLS = "starttls"
@classmethod
def from_option(cls: Type[SmtpSSLSettings], value: str) -> SmtpSSLSettings:

View File

@ -19,7 +19,7 @@
#
from __future__ import annotations
from enum import Enum, auto
from enum import Enum
from typing import Type
from ahriman.core.exceptions import InvalidOption
@ -31,11 +31,13 @@ class UploadSettings(Enum):
:cvar Disabled: no sync will be performed, required for testing purpose
:cvar Rsync: sync via rsync
:cvar S3: sync to Amazon S3
:cvar Github: sync to github releases page
"""
Disabled = auto() # for testing purpose
Rsync = auto()
S3 = auto()
Disabled = "disabled" # for testing purpose
Rsync = "rsync"
S3 = "s3"
Github = "github"
@classmethod
def from_option(cls: Type[UploadSettings], value: str) -> UploadSettings:
@ -48,4 +50,6 @@ class UploadSettings(Enum):
return cls.Rsync
if value.lower() in ("s3",):
return cls.S3
if value.lower() in ("github",):
return cls.Github
raise InvalidOption(value)

View File

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

View File

@ -81,8 +81,7 @@ def auth_handler() -> MiddlewareType:
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
permission_method = getattr(handler, "get_permission", None)
if permission_method is not None:
if (permission_method := getattr(handler, "get_permission", None)) is not None:
permission = await permission_method(request)
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
handler_instance = getattr(handler, "__self__", None)

View File

@ -18,8 +18,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import middleware, Request
from aiohttp.web_exceptions import HTTPClientError
from aiohttp.web_response import StreamResponse
from aiohttp.web_exceptions import HTTPClientError, HTTPException, HTTPServerError
from aiohttp.web_response import json_response, StreamResponse
from logging import Logger
from ahriman.web.middlewares import HandlerType, MiddlewareType
@ -35,10 +35,15 @@ def exception_handler(logger: Logger) -> MiddlewareType:
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
try:
return await handler(request)
except HTTPClientError:
raise
except Exception:
logger.exception("exception during performing request to %s", request.path)
raise
except HTTPClientError as e:
return json_response(data={"error": e.reason}, status=e.status_code)
except HTTPServerError as e:
logger.exception("server exception during performing request to %s", request.path)
return json_response(data={"error": e.reason}, status=e.status_code)
except HTTPException:
raise # just raise 2xx and 3xx codes
except Exception as e:
logger.exception("unknown exception during performing request to %s", request.path)
return json_response(data={"error": str(e)}, status=500)
return handle

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPFound, Response, json_response
from aiohttp.web import HTTPBadRequest, HTTPFound, Response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -42,12 +42,11 @@ class AddView(BaseView):
:return: redirect to main page on success
"""
data = await self.extract_data(["packages"])
try:
data = await self.extract_data(["packages"])
packages = data["packages"]
except Exception as e:
return json_response(data=str(e), status=400)
raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=True)

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPFound, Response, json_response
from aiohttp.web import HTTPBadRequest, HTTPFound, Response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -42,12 +42,11 @@ class RemoveView(BaseView):
:return: redirect to main page on success
"""
data = await self.extract_data(["packages"])
try:
data = await self.extract_data(["packages"])
packages = data["packages"]
except Exception as e:
return json_response(data=str(e), status=400)
raise HTTPBadRequest(reason=str(e))
self.spawner.packages_remove(packages)

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPFound, Response, json_response
from aiohttp.web import HTTPBadRequest, HTTPFound, Response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -42,12 +42,11 @@ class RequestView(BaseView):
:return: redirect to main page on success
"""
data = await self.extract_data(["packages"])
try:
data = await self.extract_data(["packages"])
packages = data["packages"]
except Exception as e:
return json_response(data=str(e), status=400)
raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=False)

View File

@ -19,9 +19,10 @@
#
import aur # type: ignore
from aiohttp.web import Response, json_response
from typing import Callable, Iterator
from aiohttp.web import HTTPNotFound, Response, json_response
from typing import Callable, List
from ahriman.core.util import aur_search
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -43,12 +44,10 @@ class SearchView(BaseView):
:return: 200 with found package bases and descriptions sorted by base
"""
search: Iterator[str] = filter(lambda s: len(s) > 3, self.request.query.getall("for", default=[]))
search_string = " ".join(search)
if not search_string:
return json_response(data="Search string must not be empty", status=400)
packages = aur.search(search_string)
search: List[str] = self.request.query.getall("for", default=[])
packages = aur_search(*search)
if not packages:
raise HTTPNotFound(reason=f"No packages found for terms: {search}")
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
response = [

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPNoContent, Response, json_response
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.user_access import UserAccess
@ -53,12 +53,11 @@ class AhrimanView(BaseView):
:return: 204 on success
"""
data = await self.extract_data()
try:
data = await self.extract_data()
status = BuildStatusEnum(data["status"])
except Exception as e:
return json_response(data=str(e), status=400)
raise HTTPBadRequest(reason=str(e))
self.service.update_self(status)

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
from ahriman.core.exceptions import UnknownPackage
from ahriman.models.build_status import BuildStatusEnum
@ -88,11 +88,11 @@ class PackageView(BaseView):
package = Package.from_json(data["package"]) if "package" in data else None
status = BuildStatusEnum(data["status"])
except Exception as e:
return json_response(data=str(e), status=400)
raise HTTPBadRequest(reason=str(e))
try:
self.service.update(base, status, package)
except UnknownPackage:
return json_response(data=f"Package {base} is unknown, but no package body set", status=400)
raise HTTPBadRequest(reason=f"Package {base} is unknown, but no package body set")
raise HTTPNoContent()

View File

@ -45,11 +45,11 @@ class LoginView(BaseView):
"""
from ahriman.core.auth.oauth import OAuth
code = self.request.query.getone("code", default=None)
oauth_provider = self.validator
if not isinstance(oauth_provider, OAuth): # there is actually property, but mypy does not like it anyway
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
code = self.request.query.getone("code", default=None)
if not code:
raise HTTPFound(oauth_provider.get_oauth_url())

View File

@ -0,0 +1,44 @@
import pytest
from pytest_mock import MockerFixture
from ahriman.application.application.packages import Packages
from ahriman.application.application.properties import Properties
from ahriman.application.application.repository import Repository
from ahriman.core.configuration import Configuration
@pytest.fixture
def application_packages(configuration: Configuration, mocker: MockerFixture) -> Packages:
"""
fixture for application with package functions
:param configuration: configuration fixture
:param mocker: mocker object
:return: application test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Packages("x86_64", configuration, no_report=True)
@pytest.fixture
def application_properties(configuration: Configuration, mocker: MockerFixture) -> Properties:
"""
fixture for application with properties only
:param configuration: configuration fixture
:param mocker: mocker object
:return: application test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Properties("x86_64", configuration, no_report=True)
@pytest.fixture
def application_repository(configuration: Configuration, mocker: MockerFixture) -> Repository:
"""
fixture for application with repository functions
:param configuration: configuration fixture
:param mocker: mocker object
:return: application test instance
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Repository("x86_64", configuration, no_report=True)

View File

@ -0,0 +1,26 @@
from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.models.package import Package
def test_finalize(application: Application, mocker: MockerFixture) -> None:
"""
must report and sync at the last
"""
report_mock = mocker.patch("ahriman.application.application.Application.report")
sync_mock = mocker.patch("ahriman.application.application.Application.sync")
application._finalize([])
report_mock.assert_called_once()
sync_mock.assert_called_once()
def test_known_packages(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return not empty list of known packages
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
packages = application._known_packages()
assert len(packages) > 1
assert package_ahriman.base in packages

View File

@ -0,0 +1,207 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from unittest.mock import MagicMock
from ahriman.application.application.packages import Packages
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
def test_finalize(application_packages: Packages) -> None:
"""
must raise NotImplemented for missing finalize method
"""
with pytest.raises(NotImplementedError):
application_packages._finalize([])
def test_known_packages(application_packages: Packages) -> None:
"""
must raise NotImplemented for missing finalize method
"""
with pytest.raises(NotImplementedError):
application_packages._known_packages()
def test_add_archive(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must add package from archive
"""
copy_mock = mocker.patch("shutil.copy")
application_packages._add_archive(package_ahriman.base)
copy_mock.assert_called_once()
def test_add_aur(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must add package from AUR
"""
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load")
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
application_packages._add_aur(package_ahriman.base, set(), False)
load_mock.assert_called_once()
dependencies_mock.assert_called_once()
def test_add_directory(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must add packages from directory
"""
iterdir_mock = mocker.patch("pathlib.Path.iterdir",
return_value=[package.filepath for package in package_ahriman.packages.values()])
copy_mock = mocker.patch("shutil.copy")
application_packages._add_directory(package_ahriman.base)
iterdir_mock.assert_called_once()
copy_mock.assert_called_once()
def test_add_local(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must add package from local sources
"""
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init")
copytree_mock = mocker.patch("shutil.copytree")
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
application_packages._add_local(package_ahriman.base, set(), False)
init_mock.assert_called_once()
copytree_mock.assert_has_calls([
mock.call(Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)),
mock.call(application_packages.repository.paths.cache_for(package_ahriman.base),
application_packages.repository.paths.manual_for(package_ahriman.base)),
])
dependencies_mock.assert_called_once()
def test_add_remote(application_packages: Packages, package_description_ahriman: PackageDescription,
mocker: MockerFixture) -> None:
"""
must add package from remote source
"""
response_mock = MagicMock()
response_mock.iter_content.return_value = ["chunk"]
open_mock = mocker.patch("pathlib.Path.open")
request_mock = mocker.patch("requests.get", return_value=response_mock)
url = f"https://host/{package_description_ahriman.filename}"
application_packages._add_remote(url)
open_mock.assert_called_once_with("wb")
request_mock.assert_called_once_with(url, stream=True)
response_mock.raise_for_status.assert_called_once()
def test_process_dependencies(application_packages: Packages, mocker: MockerFixture) -> None:
"""
must process dependencies addition
"""
missing = {"python"}
path = Path("local")
dependencies_mock = mocker.patch("ahriman.models.package.Package.dependencies", return_value=missing)
add_mock = mocker.patch("ahriman.application.application.packages.Packages.add")
application_packages._process_dependencies(path, set(), False)
dependencies_mock.assert_called_once_with(path)
add_mock.assert_called_once_with(missing, PackageSource.AUR, False)
def test_process_dependencies_missing(application_packages: Packages, mocker: MockerFixture) -> None:
"""
must process dependencies addition only for missing packages
"""
path = Path("local")
dependencies_mock = mocker.patch("ahriman.models.package.Package.dependencies",
return_value={"python", "python-aiohttp"})
add_mock = mocker.patch("ahriman.application.application.packages.Packages.add")
application_packages._process_dependencies(path, {"python"}, False)
dependencies_mock.assert_called_once_with(path)
add_mock.assert_called_once_with({"python-aiohttp"}, PackageSource.AUR, False)
def test_process_dependencies_skip(application_packages: Packages, mocker: MockerFixture) -> None:
"""
must skip dependencies processing
"""
dependencies_mock = mocker.patch("ahriman.models.package.Package.dependencies")
add_mock = mocker.patch("ahriman.application.application.packages.Packages.add")
application_packages._process_dependencies(Path("local"), set(), True)
dependencies_mock.assert_not_called()
add_mock.assert_not_called()
def test_add_add_archive(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must add package from archive via add function
"""
mocker.patch("ahriman.application.application.packages.Packages._known_packages", return_value=set())
add_mock = mocker.patch("ahriman.application.application.packages.Packages._add_archive")
application_packages.add([package_ahriman.base], PackageSource.Archive, False)
add_mock.assert_called_once()
def test_add_add_aur(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must add package from AUR via add function
"""
mocker.patch("ahriman.application.application.packages.Packages._known_packages", return_value=set())
add_mock = mocker.patch("ahriman.application.application.packages.Packages._add_aur")
application_packages.add([package_ahriman.base], PackageSource.AUR, True)
add_mock.assert_called_once()
def test_add_add_directory(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must add packages from directory via add function
"""
mocker.patch("ahriman.application.application.packages.Packages._known_packages", return_value=set())
add_mock = mocker.patch("ahriman.application.application.packages.Packages._add_directory")
application_packages.add([package_ahriman.base], PackageSource.Directory, False)
add_mock.assert_called_once()
def test_add_add_local(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must add package from local sources via add function
"""
mocker.patch("ahriman.application.application.packages.Packages._known_packages", return_value=set())
add_mock = mocker.patch("ahriman.application.application.packages.Packages._add_local")
application_packages.add([package_ahriman.base], PackageSource.Local, False)
add_mock.assert_called_once()
def test_add_add_remote(application_packages: Packages, package_description_ahriman: PackageDescription,
mocker: MockerFixture) -> None:
"""
must add package from remote source via add function
"""
mocker.patch("ahriman.application.application.packages.Packages._known_packages", return_value=set())
add_mock = mocker.patch("ahriman.application.application.packages.Packages._add_remote")
url = f"https://host/{package_description_ahriman.filename}"
application_packages.add([url], PackageSource.Remote, False)
add_mock.assert_called_once()
def test_remove(application_packages: Packages, mocker: MockerFixture) -> None:
"""
must remove package
"""
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
finalize_mock = mocker.patch("ahriman.application.application.packages.Packages._finalize")
application_packages.remove([])
executor_mock.assert_called_once()
finalize_mock.assert_called_once()

View File

@ -0,0 +1,8 @@
from ahriman.application.application.properties import Properties
def test_create_tree(application_properties: Properties) -> None:
"""
must have repository attribute
"""
assert application_properties.repository

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