Compare commits

...

50 Commits

Author SHA1 Message Date
2414686f3c review fixes 2022-04-17 17:59:39 +03:00
60bb880f9f make identation a bit more readable 2022-04-17 17:27:38 +03:00
3016a919c5 fix part of warnings for sphinx 2022-04-17 06:55:31 +03:00
e2f7e9cf28 add raises note
Also change behaviour of the `from_option` method to fallback to
disabled instead of raising exception on unknown option
2022-04-17 05:44:46 +03:00
5b6ba721fe migrate docstrings from reST to google format 2022-04-16 21:13:17 +03:00
0db619136d Release 2.0.0rc7 2022-04-11 00:48:08 +03:00
208a9b920d docs update 2022-04-11 00:46:46 +03:00
cb63bc08ff add backup and restore subcommands 2022-04-10 21:34:34 +03:00
6551c8d983 merge restore to rebuild commannd 2022-04-10 01:51:12 +03:00
a6c8d64053 Release 2.0.0rc6 2022-04-09 17:34:23 +03:00
fd78f2b5e2 do not render failed packages in jinja (#57)
basic templates require package info which is unavailable if package
wasn't built
2022-04-09 17:31:13 +03:00
900907cdaa Release 2.0.0rc5 2022-04-08 04:42:05 +03:00
5ff2f43506 change telegram default index to telegram-index 2022-04-08 04:32:34 +03:00
dd521b49b5 force git run from the same dir to clone 2022-04-08 04:04:06 +03:00
5b1f5a8473 fix users migration 2022-04-08 03:45:17 +03:00
86af13f09e add telegram integraion 2022-04-08 03:41:07 +03:00
733c014229 Release 2.0.0rc4 2022-04-08 01:14:35 +03:00
783c16b2ed trim versions before dependency list calculation
When dependencies list contains same package with version it tries to
find packages which don't exists
2022-04-07 20:32:55 +03:00
2536b8dc1f add support of repository restoration 2022-04-07 04:49:07 +03:00
e200ac9776 add support of officiall repositories api 2022-04-07 04:19:37 +03:00
6946745153 fix descriptions 2022-04-06 01:48:03 +03:00
6de75377c3 Release 2.0.0rc3 2022-04-04 02:40:17 +03:00
a734b86e66 allow any tag for push 2022-04-04 02:39:56 +03:00
74906d084a Release 2.0.0rc2 2022-04-04 02:27:19 +03:00
22d1d835af change regex for sed 2022-04-04 02:26:42 +03:00
b7cd83502a Release 2.0.0-rc1 2022-04-04 02:18:31 +03:00
2d2cd73010 better processing for subcommands
Old versions cached full output to memory and only after that printed it
into log. This behaviour causes issues in case if operation stucks and
you would need to find the step at which it does. New check_output
method uses Popen directly and iterates over stdout lines

Also changed behaviour from merging stderr into stdout to separate
stderr logging.

Any other behaviour of the function must be the same.

Also changed GPG.key_import method to disable local signing since it
seems it is useless (and may break process in case if there is no
private key)
2022-04-04 00:03:22 +03:00
40b84b810d docs update 2022-04-03 01:44:11 +03:00
7188cfaf78 remove implicit user from docker 2022-04-01 18:39:24 +03:00
e909e4f570 add configurable exit codes to some commands (#55) 2022-04-01 18:30:11 +03:00
06ec16ac77 create user in docker container
also fix user reading and add nocolor flag by default
2022-03-31 03:25:53 +03:00
fed2f3aab8 configure docker to work with tags 2022-03-31 02:19:48 +03:00
2bc2b63a8a add help command 2022-03-31 02:18:39 +03:00
0c5a90cd66 update license years 2022-03-31 01:49:31 +03:00
28cc38aaa5 port part of settings to database (#54) 2022-03-31 01:48:06 +03:00
fb02e676af migrate to tox
this also requires to move default configuration files to share/ahriman.
Thus the following features have been added
* default configuration is not stored in /usr/share/ahriman/settings
* package installed via PKGBUILD now copies files from /usr
* configuration class now fallbacks to default in /usr
2022-03-21 04:38:42 +03:00
13121298f5 deprecate init/repo-init command
In current workflow you need to run setup to run init (because of
repository name), but you need to run init before setup (because of
repository tree rights).

New solution just add `Repo.init()` method call to setup subcommand
after config reload to make sure that repository name has been applied.
In addition chown method as well as setuid method for check_output have
been added.
2022-03-21 01:32:11 +03:00
63e79ec57a enable privileged mode for the docker 2022-03-20 04:05:43 +03:00
b83bc5a732 fallback to AUR source in case of PermissionError exception 2022-03-20 03:46:56 +03:00
8374590b62 add fallback for utf option 2022-03-20 03:16:33 +03:00
4cc665f6ee replace pspec with spec 2022-03-20 03:08:51 +03:00
57f25c309a add console printer
also add python-requests as explicit dependency and escape symbols in
repository name for badges in default tempate
2022-03-20 02:31:50 +03:00
d33844e7b2 ALLOW_AUTO_ARCHITECTURE_RUN flag for unsafe commands 2022-03-19 03:08:45 +03:00
e9886efaa2 docker hub integration 2022-03-14 00:08:21 +03:00
61efbb71a2 add docker support (#52)
* add docker support

* make shellcheck happy
2022-03-13 23:43:25 +03:00
9f4acacada strict test checks 2022-03-04 02:04:52 +03:00
026f74121a set success status on up to date packages 2022-02-06 06:22:58 +03:00
e414616bbd Release 1.8.0 2022-02-06 05:45:50 +03:00
60a2e25b9a update for new aiohttp api 2022-02-06 04:05:33 +03:00
683abca9e5 use own aur wrapper (#49) 2022-02-06 03:44:57 +03:00
320 changed files with 14343 additions and 6112 deletions

View File

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

42
.github/workflows/docker-image.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: docker image
on:
push:
branches: [ master ]
tags:
- '*'
- '!*rc*'
jobs:
docker-image:
runs-on: ubuntu-latest
steps:
- name: extract docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: |
arcan1s/ahriman
tags: |
type=ref,event=tag
type=edge
- name: setup QEMU
uses: docker/setup-qemu-action@v1
- name: setup docker buildx
uses: docker/setup-buildx-action@v1
- name: login to docker hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: build an image and push
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ steps.meta.outputs.tags }}

View File

@ -3,7 +3,7 @@ name: release
on:
push:
tags:
- '*.*.*'
- '*'
jobs:
make-release:
@ -12,20 +12,24 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: extract version
id: version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: create changelog
id: changelog
uses: jaywcjlove/changelog-generator@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
filter: 'Release \d+\.\d+\.\d+'
- name: create archive
run: make archive
env:
VERSION: ${{ steps.version.outputs.VERSION }}
- name: Release
- name: release
uses: softprops/action-gh-release@v1
with:
body: |

View File

@ -11,11 +11,14 @@ jobs:
runs-on: ubuntu-latest
container:
image: archlinux:latest
volumes:
- ${{ github.workspace }}:/build
options: --privileged -w /build
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
run: .github/workflows/setup.sh

View File

@ -11,11 +11,14 @@ jobs:
runs-on: ubuntu-latest
container:
image: archlinux:latest
volumes:
- ${{ github.workspace }}:/build
options: -w /build
steps:
- uses: actions/checkout@v2
- name: run check and tests in arch linux container
run: |
docker run \
-v ${{ github.workspace }}:/build -w /build \
archlinux:latest \
.github/workflows/tests.sh
run: .github/workflows/tests.sh

View File

@ -10,7 +10,7 @@ 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
pacman --noconfirm -Sy python-build python-installer python-wheel
# optional dependencies
# VCS support
pacman --noconfirm -Sy breezy darcs mercurial subversion
@ -25,25 +25,28 @@ make VERSION=1.0.0 archlinux # well, it does not really matter which version we
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
sudo -u nobody -- makepkg -cf --skipchecksums --noconfirm
pacman --noconfirm -U ahriman-1.0.0-1-any.pkg.tar.zst
# create machine-id which is required by build tools
systemd-machine-id-setup
# 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
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 &
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
# the build itself does not really work in the container
sudo -u ahriman -- ahriman package-add --now yay
# check if package was actually installed
#test -n "$(find "/var/lib/ahriman/repository/x86_64" -name "yay*pkg*")"
# run package check
sudo -u ahriman ahriman repo-update
sudo -u ahriman -- ahriman repo-update
# stop web service lol
kill $WEBPID

View File

@ -4,13 +4,7 @@
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]
pacman --noconfirm -Syu base-devel python-pip python-tox
# run test and check targets
make check tests

3
.gitignore vendored
View File

@ -94,3 +94,6 @@ ENV/
.venv/
*.tar.xz
status_cache.json
*.db

52
Dockerfile Normal file
View File

@ -0,0 +1,52 @@
FROM archlinux:base-devel
# image configuration
ENV AHRIMAN_ARCHITECTURE="x86_64"
ENV AHRIMAN_DEBUG=""
ENV AHRIMAN_FORCE_ROOT=""
ENV AHRIMAN_HOST="0.0.0.0"
ENV AHRIMAN_OUTPUT="syslog"
ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
ENV AHRIMAN_PORT=""
ENV AHRIMAN_REPOSITORY="aur-clone"
ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman"
ENV AHRIMAN_USER="ahriman"
# install environment
## install git which is required for AUR interaction and go for yay
RUN pacman --noconfirm -Syu git go
## create build user
RUN useradd -m -d /home/build -s /usr/bin/nologin build && \
echo "build ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/build
## install AUR helper
RUN YAY_DIR="$(runuser -u build -- mktemp -d)" && \
git clone https://aur.archlinux.org/yay.git "$YAY_DIR" && \
cd "$YAY_DIR" && \
runuser -u build -- makepkg --noconfirm --install && \
cd - && rm -r "$YAY_DIR"
## install package dependencies
RUN runuser -u build -- yay --noconfirm -Sy devtools git pyalpm python-inflection python-passlib python-requests python-srcinfo && \
runuser -u build -- yay --noconfirm -Sy python-build python-installer python-wheel && \
runuser -u build -- yay --noconfirm -Sy breezy darcs mercurial python-aioauth-client python-aiohttp \
python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security \
python-aiohttp-session python-boto3 python-cryptography python-jinja \
rsync subversion
# install ahriman
## copy tree
COPY --chown=build . "/home/build/ahriman"
## create package archive and install it
RUN cd "/home/build/ahriman" && \
make VERSION=$(python -c "from src.ahriman.version import __version__; print(__version__)") archlinux && \
cp ./*-src.tar.xz "package/archlinux" && \
cd "package/archlinux" && \
runuser -u build -- makepkg --noconfirm --install --skipchecksums && \
cd - && rm -r "/home/build/ahriman"
VOLUME ["/var/lib/ahriman"]
# minimal runtime ahriman setup
COPY "docker/entrypoint.sh" "/usr/local/bin/entrypoint"
ENTRYPOINT ["entrypoint"]
# default command
CMD ["repo-update"]

View File

@ -3,7 +3,7 @@
PROJECT := ahriman
FILES := AUTHORS COPYING README.md docs package src setup.cfg setup.py web.png
FILES := AUTHORS COPYING README.md docs package src setup.py tox.ini web.png
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
IGNORE_FILES := package/archlinux src/.mypy_cache
@ -24,13 +24,10 @@ archive_directory: $(TARGET_FILES)
find "$(PROJECT)" -depth -type d -name "*.egg-info" -execdir rm -rf {} +
archlinux: archive
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
sed -i "s/pkgver=.*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
check: clean mypy
autopep8 --exit-code --max-line-length 120 -aa -i -j 0 -r "src/$(PROJECT)" "tests/$(PROJECT)"
pylint --rcfile=.pylintrc "src/$(PROJECT)"
bandit -c .bandit.yml -r "src/$(PROJECT)"
bandit -c .bandit-test.yml -r "tests/$(PROJECT)"
check: clean
tox -e check
clean:
find . -type f -name "$(PROJECT)-*-src.tar.xz" -delete
@ -42,10 +39,6 @@ directory: clean
man:
cd src && PYTHONPATH=. argparse-manpage --module ahriman.application.ahriman --function _parser --author "ahriman team" --project-name ahriman --author-email "" --url https://github.com/arcan1s/ahriman --output ../docs/ahriman.1
mypy:
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)" --install-types --non-interactive || true
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
push: architecture man archlinux
git add package/archlinux/PKGBUILD src/ahriman/version.py docs/ahriman-architecture.svg docs/ahriman.1
git commit -m "Release $(VERSION)"
@ -54,10 +47,10 @@ push: architecture man archlinux
git push --tags
tests: clean
python setup.py test
tox -e tests
version:
ifndef VERSION
$(error VERSION is required, but not set)
endif
sed -i '/__version__ = "[0-9.]*/s/[^"][^)]*/__version__ = "$(VERSION)"/' src/ahriman/version.py
sed -i '/__version__ = .*/s/[^"][^)]*/__version__ = "$(VERSION)"/' src/ahriman/version.py

View File

@ -2,6 +2,7 @@
[![tests status](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
[![setup status](https://github.com/arcan1s/ahriman/actions/workflows/run-setup.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/run-setup.yml)
[![docker image](https://github.com/arcan1s/ahriman/actions/workflows/docker-image.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/docker-image.yml)
[![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).
@ -12,7 +13,7 @@ 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 github) and report generation (html).
* Synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Dependency manager.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Repository status interface with optional authorization and control options:

49
docker/entrypoint.sh Executable file
View File

@ -0,0 +1,49 @@
#!/bin/bash
set -e
[ -n "$AHRIMAN_DEBUG" ] && set -x
# configuration tune
sed -i "s|root = /var/lib/ahriman|root = $AHRIMAN_REPOSITORY_ROOT|g" "/etc/ahriman.ini"
sed -i "s|database = /var/lib/ahriman/ahriman.db|database = $AHRIMAN_REPOSITORY_ROOT/ahriman.db|g" "/etc/ahriman.ini"
sed -i "s|host = 127.0.0.1|host = $AHRIMAN_HOST|g" "/etc/ahriman.ini"
sed -i "s|handlers = syslog_handler|handlers = ${AHRIMAN_OUTPUT}_handler|g" "/etc/ahriman.ini.d/logging.ini"
AHRIMAN_DEFAULT_ARGS=("--architecture" "$AHRIMAN_ARCHITECTURE")
if [[ "$AHRIMAN_OUTPUT" == "syslog" ]]; then
if [ ! -e "/dev/log" ]; then
# by default ahriman uses syslog which is not available inside container
# to make noise less we force quiet mode in case if /dev/log was not mounted
AHRIMAN_DEFAULT_ARGS+=("--quiet")
fi
fi
# create repository root inside the [[mounted]] directory and set correct ownership
[ -d "$AHRIMAN_REPOSITORY_ROOT" ] || mkdir "$AHRIMAN_REPOSITORY_ROOT"
chown "$AHRIMAN_USER":"$AHRIMAN_USER" "$AHRIMAN_REPOSITORY_ROOT"
# run built-in setup command
AHRIMAN_SETUP_ARGS=("--build-as-user" "$AHRIMAN_USER")
AHRIMAN_SETUP_ARGS+=("--packager" "$AHRIMAN_PACKAGER")
AHRIMAN_SETUP_ARGS+=("--repository" "$AHRIMAN_REPOSITORY")
if [ -n "$AHRIMAN_PORT" ]; then
# in addition it must be handled in docker run command
AHRIMAN_SETUP_ARGS+=("--web-port" "$AHRIMAN_PORT")
fi
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" repo-setup "${AHRIMAN_SETUP_ARGS[@]}"
# refresh database
runuser -u build -- yay --noconfirm -Syy &> /dev/null
# create machine-id which is required by build tools
systemd-machine-id-setup &> /dev/null
# if AHRIMAN_FORCE_ROOT is set or command is unsafe we can run without sudo
# otherwise we prepend executable by sudo command
if [ -n "$AHRIMAN_FORCE_ROOT" ]; then
AHRIMAN_EXECUTABLE=("ahriman")
elif ahriman help-commands-unsafe --command="$*" &> /dev/null; then
AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman")
else
AHRIMAN_EXECUTABLE=("ahriman")
fi
exec "${AHRIMAN_EXECUTABLE[@]}" "${AHRIMAN_DEFAULT_ARGS[@]}" "$@"

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 540 KiB

View File

@ -3,7 +3,7 @@
ahriman
.SH SYNOPSIS
.B ahriman
[-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} ...
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-update,update,user-add,user-list,user-remove,web} ...
.SH DESCRIPTION
ArcH Linux ReposItory MANager
.SH OPTIONS
@ -46,6 +46,12 @@ show program's version number and exit
\fBahriman\fR \fI\,aur-search\/\fR
search for package
.TP
\fBahriman\fR \fI\,help\/\fR
show help message
.TP
\fBahriman\fR \fI\,help-commands-unsafe\/\fR
list unsafe commands
.TP
\fBahriman\fR \fI\,key-import\/\fR
import PGP key
.TP
@ -73,6 +79,9 @@ list patch sets
\fBahriman\fR \fI\,patch-remove\/\fR
remove patch set
.TP
\fBahriman\fR \fI\,repo-backup\/\fR
backup repository data
.TP
\fBahriman\fR \fI\,repo-check\/\fR
check for updates
.TP
@ -82,9 +91,6 @@ clean local caches
\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
@ -94,6 +100,9 @@ remove unknown packages
\fBahriman\fR \fI\,repo-report\/\fR
generate report
.TP
\fBahriman\fR \fI\,repo-restore\/\fR
restore repository data
.TP
\fBahriman\fR \fI\,repo-setup\/\fR
initial service configuration
.TP
@ -112,14 +121,17 @@ update packages
\fBahriman\fR \fI\,user-add\/\fR
create or update user
.TP
\fBahriman\fR \fI\,user-list\/\fR
user known users and their access
.TP
\fBahriman\fR \fI\,user-remove\/\fR
remove user
.TP
\fBahriman\fR \fI\,web\/\fR
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}]
usage: ahriman aur-search [-h] [-e] [-i]
[--sort-by {description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,url,url_path,version}]
search [search ...]
search for package in AUR using API
@ -128,18 +140,22 @@ search for package in AUR using API
\fBsearch\fR
search terms, can be specified multiple times, result will match all terms
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-i\fR, \fB\-\-info\fR
show additional package information
.TP
\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}
\fB\-\-sort\-by\fR {description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,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}]
usage: ahriman aur-search [-h] [-e] [-i]
[--sort-by {description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,url,url_path,version}]
search [search ...]
search for package in AUR using API
@ -148,15 +164,40 @@ search for package in AUR using API
\fBsearch\fR
search terms, can be specified multiple times, result will match all terms
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-i\fR, \fB\-\-info\fR
show additional package information
.TP
\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}
\fB\-\-sort\-by\fR {description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,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 help'
usage: ahriman help [-h] [command]
show help message for application or command and exit
.TP
\fBcommand\fR
show help message for specific command
.SH OPTIONS 'ahriman help-commands-unsafe'
usage: ahriman help-commands-unsafe [-h] [--command COMMAND]
list unsafe commands as defined in default args
.TP
\fB\-\-command\fR \fI\,COMMAND\/\fR
instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1
otherwise
.SH OPTIONS 'ahriman key-import'
usage: ahriman key-import [-h] [--key-server KEY_SERVER] key
@ -171,8 +212,8 @@ PGP key to import from public server
key server for key import
.SH OPTIONS 'ahriman package-add'
usage: ahriman package-add [-h] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}]
usage: ahriman package-add [-h] [-e] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}]
[--without-dependencies]
package [package ...]
@ -182,12 +223,16 @@ add existing or new package to the build queue
\fBpackage\fR
package source (base name, path to local files, remote URL)
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.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}
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}
explicitly specify the package source for this command
.TP
@ -195,8 +240,8 @@ explicitly specify the package source for this command
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}]
usage: ahriman package-add [-h] [-e] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}]
[--without-dependencies]
package [package ...]
@ -206,12 +251,16 @@ add existing or new package to the build queue
\fBpackage\fR
package source (base name, path to local files, remote URL)
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.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}
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}
explicitly specify the package source for this command
.TP
@ -219,8 +268,8 @@ explicitly specify the package source for this command
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}]
usage: ahriman package-add [-h] [-e] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}]
[--without-dependencies]
package [package ...]
@ -230,12 +279,16 @@ add existing or new package to the build queue
\fBpackage\fR
package source (base name, path to local files, remote URL)
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.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}
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}
explicitly specify the package source for this command
.TP
@ -263,7 +316,7 @@ package name or base
.SH OPTIONS 'ahriman package-status'
usage: ahriman package-status [-h] [--ahriman] [-i]
usage: ahriman package-status [-h] [--ahriman] [-e] [-i]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
@ -277,6 +330,10 @@ filter status by package base
\fB\-\-ahriman\fR
get service status itself
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-i\fR, \fB\-\-info\fR
show additional package information
@ -286,7 +343,7 @@ show additional package information
filter packages by status
.SH OPTIONS 'ahriman status'
usage: ahriman package-status [-h] [--ahriman] [-i]
usage: ahriman package-status [-h] [--ahriman] [-e] [-i]
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
[package ...]
@ -300,6 +357,10 @@ filter status by package base
\fB\-\-ahriman\fR
get service status itself
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-i\fR, \fB\-\-info\fR
show additional package information
@ -362,7 +423,7 @@ path to directory with changed files for patch addition/update
files which has to be tracked
.SH OPTIONS 'ahriman patch-list'
usage: ahriman patch-list [-h] package
usage: ahriman patch-list [-h] [-e] [package]
list available patches for the package
@ -370,6 +431,9 @@ list available patches for the package
\fBpackage\fR
package base
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.SH OPTIONS 'ahriman patch-remove'
usage: ahriman patch-remove [-h] package
@ -381,8 +445,18 @@ remove patches for the package
package base
.SH OPTIONS 'ahriman repo-backup'
usage: ahriman repo-backup [-h] path
backup settings and database
.TP
\fBpath\fR
path of the output archive
.SH OPTIONS 'ahriman repo-check'
usage: ahriman repo-check [-h] [--no-vcs] [package ...]
usage: ahriman repo-check [-h] [-e] [--no-vcs] [package ...]
check for packages updates. Same as update \-\-dry\-run \-\-no\-manual
@ -390,12 +464,16 @@ check for packages updates. Same as update \-\-dry\-run \-\-no\-manual
\fBpackage\fR
filter check by package base
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-\-no\-vcs\fR
do not check VCS packages
.SH OPTIONS 'ahriman check'
usage: ahriman repo-check [-h] [--no-vcs] [package ...]
usage: ahriman repo-check [-h] [-e] [--no-vcs] [package ...]
check for packages updates. Same as update \-\-dry\-run \-\-no\-manual
@ -403,20 +481,20 @@ check for packages updates. Same as update \-\-dry\-run \-\-no\-manual
\fBpackage\fR
filter check by package base
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.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]
usage: ahriman repo-clean [-h] [--cache] [--chroot] [--manual] [--packages]
remove local caches
.TP
\fB\-\-build\fR
clear directory with package sources
.TP
\fB\-\-cache\fR
clear directory with package caches
@ -427,26 +505,18 @@ clear build chroot
.TP
\fB\-\-manual\fR
clear directory with manually added packages
clear manually added packages queue
.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]
usage: ahriman repo-clean [-h] [--cache] [--chroot] [--manual] [--packages]
remove local caches
.TP
\fB\-\-build\fR
clear directory with package sources
.TP
\fB\-\-cache\fR
clear directory with package caches
@ -457,16 +527,12 @@ clear build chroot
.TP
\fB\-\-manual\fR
clear directory with manually added packages
clear manually added packages queue
.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]
@ -481,22 +547,8 @@ 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] [--dry-run]
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON] [--dry-run] [--from-database] [-e]
force rebuild whole repository
@ -509,8 +561,18 @@ only rebuild packages that depend on specified package
\fB\-\-dry\-run\fR
just perform check for packages without rebuild process itself
.TP
\fB\-\-from\-database\fR
read packages from database instead of filesystem. This feature in particular is required in case if you would like to
restore repository from another repository instance. Note however that in order to restore packages you need to have
original ahriman instance run with web service and have run repo\-update at least once.
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.SH OPTIONS 'ahriman rebuild'
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON] [--dry-run]
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON] [--dry-run] [--from-database] [-e]
force rebuild whole repository
@ -523,6 +585,16 @@ only rebuild packages that depend on specified package
\fB\-\-dry\-run\fR
just perform check for packages without rebuild process itself
.TP
\fB\-\-from\-database\fR
read packages from database instead of filesystem. This feature in particular is required in case if you would like to
restore repository from another repository instance. Note however that in order to restore packages you need to have
original ahriman instance run with web service and have run repo\-update at least once.
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.SH OPTIONS 'ahriman repo-remove-unknown'
usage: ahriman repo-remove-unknown [-h] [--dry-run] [-i]
@ -571,14 +643,122 @@ generate repository report according to current settings
target to generate report
.SH OPTIONS 'ahriman repo-restore'
usage: ahriman repo-restore [-h] [-o OUTPUT] path
restore settings and database
.TP
\fBpath\fR
path of the input archive
.TP
\fB\-o\fR \fI\,OUTPUT\/\fR, \fB\-\-output\fR \fI\,OUTPUT\/\fR
root path of the extracted files
.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]
usage: ahriman repo-setup [-h] [--build-as-user BUILD_AS_USER] [--build-command BUILD_COMMAND]
[--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository
REPOSITORY [--sign-key SIGN_KEY]
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
create initial service configuration, requires root
.TP
\fB\-\-build\-as\-user\fR \fI\,BUILD_AS_USER\/\fR
force makepkg user to the specific one
.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 init'
usage: ahriman repo-setup [-h] [--build-as-user BUILD_AS_USER] [--build-command BUILD_COMMAND]
[--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository
REPOSITORY [--sign-key SIGN_KEY]
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
create initial service configuration, requires root
.TP
\fB\-\-build\-as\-user\fR \fI\,BUILD_AS_USER\/\fR
force makepkg user to the specific one
.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-init'
usage: ahriman repo-setup [-h] [--build-as-user BUILD_AS_USER] [--build-command BUILD_COMMAND]
[--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository
REPOSITORY [--sign-key SIGN_KEY]
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
create initial service configuration, requires root
.TP
\fB\-\-build\-as\-user\fR \fI\,BUILD_AS_USER\/\fR
force makepkg user to the specific one
.TP
\fB\-\-build\-command\fR \fI\,BUILD_COMMAND\/\fR
build command prefix
@ -612,13 +792,18 @@ sign options
port of the web service
.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]
usage: ahriman repo-setup [-h] [--build-as-user BUILD_AS_USER] [--build-command BUILD_COMMAND]
[--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository
REPOSITORY [--sign-key SIGN_KEY]
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
create initial service configuration, requires root
.TP
\fB\-\-build\-as\-user\fR \fI\,BUILD_AS_USER\/\fR
force makepkg user to the specific one
.TP
\fB\-\-build\-command\fR \fI\,BUILD_COMMAND\/\fR
build command prefix
@ -703,7 +888,7 @@ target to sync
.SH OPTIONS 'ahriman repo-update'
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
usage: ahriman repo-update [-h] [--dry-run] [-e] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
check for packages updates and run build process if requested
@ -715,6 +900,10 @@ filter check by package base
\fB\-\-dry\-run\fR
just perform check for updates, same as check command
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-\-no\-aur\fR
do not check for AUR updates. Implies \-\-no\-vcs
@ -732,7 +921,7 @@ do not include manual updates
do not check VCS packages
.SH OPTIONS 'ahriman update'
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
usage: ahriman repo-update [-h] [--dry-run] [-e] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
check for packages updates and run build process if requested
@ -744,6 +933,10 @@ filter check by package base
\fB\-\-dry\-run\fR
just perform check for updates, same as check command
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-\-no\-aur\fR
do not check for AUR updates. Implies \-\-no\-vcs
@ -761,8 +954,7 @@ do not include manual updates
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]
usage: ahriman user-add [-h] [--as-service] [-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
@ -775,10 +967,6 @@ username for web service
\fB\-\-as\-service\fR
add user as service user
.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. Blank password will be treated as empty password, which is in particular must be used for OAuth2
@ -792,8 +980,25 @@ user access level
\fB\-s\fR, \fB\-\-secure\fR
set file permissions to user\-only
.SH OPTIONS 'ahriman user-list'
usage: ahriman user-list [-h] [-e] [-r {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [username]
list users from the user mapping and their roles
.TP
\fBusername\fR
filter users by username
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-r\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}, \fB\-\-role\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}
filter users by role
.SH OPTIONS 'ahriman user-remove'
usage: ahriman user-remove [-h] [--no-reload] [-s] username
usage: ahriman user-remove [-h] [-s] username
remove user from the user mapping and update the configuration
@ -801,10 +1006,6 @@ remove user from the user mapping and update the configuration
\fBusername\fR
username for web service
.TP
\fB\-\-no\-reload\fR
do not reload authentication module
.TP
\fB\-s\fR, \fB\-\-secure\fR
set file permissions to user\-only

View File

@ -16,8 +16,6 @@ This package contains application (aka executable) related classes and everythin
`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
@ -27,6 +25,8 @@ This package contains everything which is required for any time of application r
* `ahriman.core.alpm` package controls pacman related functions. It provides wrappers for `pyalpm` library and safe calls for repository tools (`repo-add` and `repo-remove`).
* `ahriman.core.auth` package provides classes for authorization methods used by web mostly. Base class is `ahriman.core.auth.auth.Auth` which must be called by `load` method.
* `ahriman.core.build_tools` is a package which provides wrapper for `devtools` commands.
* `ahriman.core.database` is everything including data and schema migrations for database.
* `ahriman.core.formatters` package provides `Printer` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
* `ahriman.core.report` is a package with reporting classes. Usually it must be called by `ahriman.core.report.report.Report.load` method.
* `ahriman.core.repository` contains several traits and base repository (`ahriman.core.repository.repository.Repository` class) implementation.
* `ahriman.core.sign` package provides sign feature (only gpg calls are available).
@ -61,11 +61,34 @@ Web application. It is important that this package is isolated from any other to
* In each child process call lock functions.
* After success checks pass control to `Handler.run` method defined by specific handler class.
* Return result (success or failure) of each subprocess and exit from application.
* Some handlers may override their status and throw `ExitCode` exception. This exception is just silently suppressed and changes application exit code to `1`.
In most cases handlers spawn god class `ahriman.application.application.Application` class and call required methods.
Application is designed to run from `systemd` services and provides parametrized by architecture timer and service file for that.
# Database
The service uses SQLite database in order to store some internal info.
## Database instance
All methods related to specific part of database (basically operations per table) are split into different traits located inside `ahriman.core.database.operations` package. The base trait `ahriman.core.database.operations.operations.Operations` also provides generic methods for database access (e.g. row converters and transactional support).
The `ahriman.core.database.sqlite.SQLite` class itself derives from all of these traits and implements methods for initialization, including migrations.
## Schema and data migrations
The schema migration are applied according to current `pragma user_info` values, located at `ahriman.core.database.migrations` package and named as `m000_migration_name.py` (the preceding `m` is required in order to import migration content for tests). Additional class `ahriman.core.database.migrations.Migrations` reads all migrations autmatically and applies them in alphabetical order.
There are also data migrations which are located at `ahriman.core.database.data` package and move data from old-style (e.g. json files in filesystem, directory trees, etc) to the database. They are also part of migration and (unlike schema migrations) are applied only at specific version breakpoints (e.g. if `user_version` is more than 0 no initial migration will be applied).
## Type conversions
By default, it parses rows into python dictionary. In addition, the following pseudo-types are supported:
* `Dict[str, Any]`, `List[Any]` - for storing JSON data structures in database (technically there is no restriction on types for dictionary keys and values, but it is recommended to use only string keys). The type is stored as `json` datatype and `json.loads` and `json.dumps` methods are used in order to read and write from/to database respectively.
# Basic flows
## Add new packages or rebuild existing
@ -122,21 +145,21 @@ The package provides several authorization methods: disabled, based on configura
Disabled (default) authorization provider just allows everything for everyone and does not have any specific configuration (it uses some default configuration parameters though). It also provides generic interface for derived classes.
Mapping (aka configuration) provider uses hashed passwords with salt from configuration file in order to authenticate users. This provider also enables user permission checking (read/write) (authorization). Thus, it defines the following methods:
Mapping (aka configuration) provider uses hashed passwords with salt from the database in order to authenticate users. This provider also enables user permission checking (read/write) (authorization). Thus, it defines the following methods:
* `check_credentials` - user password validation (authentication).
* `verify_access` - user permission validation (authorization).
Passwords must be stored in configuration as `hash(password + salt)`, where `password` is user defined password (taken from user input), `salt` is random string (any length) defined globally in configuration and `hash` is secure hash function. Thus, the following configuration
Passwords must be stored in database as `hash(password + salt)`, where `password` is user defined password (taken from user input), `salt` is random string (any length) defined globally in configuration and `hash` is secure hash function. Thus, the following configuration
```ini
[auth:read]
username = $6$rounds=656000$mWBiecMPrHAL1VgX$oU4Y5HH8HzlvMaxwkNEJjK13ozElyU1wAHBoO/WW5dAaE4YEfnB0X3FxbynKMl4FBdC3Ovap0jINz4LPkNADg0
```csv
"username","password","access"
"username","$6$rounds=656000$mWBiecMPrHAL1VgX$oU4Y5HH8HzlvMaxwkNEJjK13ozElyU1wAHBoO/WW5dAaE4YEfnB0X3FxbynKMl4FBdC3Ovap0jINz4LPkNADg0","read"
```
means that there is user `username` with `read` access and password `password` hashed by `sha512` with salt `salt`.
OAuth provider uses library definitions (`aioauth-client`) in order _authenticate_ users. It still requires user permission to be set in configuration, thus it inherits mapping provider without any changes. Whereas we could override `check_credentials` (authentication method) by something custom, OAuth flow is a bit more complex than just forward request, thus we have to implement the flow in login form.
OAuth provider uses library definitions (`aioauth-client`) in order _authenticate_ users. It still requires user permission to be set in database, thus it inherits mapping provider without any changes. Whereas we could override `check_credentials` (authentication method) by something custom, OAuth flow is a bit more complex than just forward request, thus we have to implement the flow in login form.
OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature is used by service reporting (aka robots).

View File

@ -2,18 +2,21 @@
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build:x86_64` groups it will use the option from `build:x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
Some values have list of strings type. Those values will be read in the same way as shell does:
There are two variable types which have been added to default ones, they are paths and lists. List values will be read in the same way as shell does:
* By default, it splits value by spaces excluding empty elements.
* In case if quotation mark (`"` or `'`) will be found, any spaces inside will be ignored.
* In order to use quotation mark inside value it is required to put it to another quotation mark, e.g. `wor"'"d "with quote"` will be parsed as `["wor'd", "with quote"]` and vice versa.
* Unclosed quotation mark is not allowed and will rise an exception.
Path values, except for casting to `pathlib.Path` type, will be also expanded to absolute paths relative to the configuration path. E.g. if path is set to `ahriman.ini.d/logging.ini` and root configuration path is `/etc/ahriman.ini`, the value will be expanded to `/etc/ahriman.ini.d/logging.ini`. In order to disable path expand, use the full path, e.g. `/etc/ahriman.ini.d/logging.ini`.
## `settings` group
Base configuration settings.
* `include` - path to directory with configuration files overrides, string, required.
* `database` - path to SQLite database, string, required.
* `logging` - path to logging configuration, string, required. Check `logging.ini` for reference.
## `alpm` group
@ -38,14 +41,7 @@ Base authorization settings. `OAuth` provider requires `aioauth-client` library
* `safe_build_status` - allow requesting status page without authorization, boolean, required.
* `salt` - password hash salt, string, required in case if authorization enabled (automatically generated by `create-user` subcommand).
## `auth:*` groups
Authorization mapping. Group name must refer to user access level, i.e. it should be one of `auth:read` (read hidden pages), `auth:write` (everything is allowed).
Key is always username (case-insensitive), option value depends on authorization provider:
* `OAuth` - by default requires only usernames and ignores values. But in case of direct login method call (via POST request) it will act as `Mapping` authorization method.
* `Mapping` (default) - reads salted password hashes from values, uses SHA512 in order to hash passwords. Password can be set by using `user-add` subcommand.
Authorized users are stored inside internal database, if any of external provides are used the password field for non-service users must be empty.
## `build:*` groups
@ -84,6 +80,12 @@ Type will be read from several ways:
* Otherwise, it will look for type from section name removing architecture name.
* And finally, it will use section name as type.
### `console` type
Section name must be either `console` (plus optional architecture name, e.g. `console:x86_64`) or random name with `type` set.
* `use_utf` - use utf8 symbols in output if set and ascii otherwise, boolean, optional, default `yes`.
### `email` type
Section name must be either `email` (plus optional architecture name, e.g. `email:x86_64`) or random name with `type` set.
@ -112,6 +114,18 @@ Section name must be either `html` (plus optional architecture name, e.g. `html:
* `link_path` - prefix for HTML links, string, required.
* `template_path` - path to Jinja2 template, string, required.
### `telegram` type
Section name must be either `telegram` (plus optional architecture name, e.g. `telegram:x86_64`) or random name with `type` set.
* `type` - type of the report, string, optional, must be set to `telegram` if exists.
* `api_key` - telegram bot API key, string, required. Please refer FAQ about how to create chat and bot
* `chat_id` - telegram chat id, either string with `@` or integer value, required.
* `homepage` - link to homepage, string, optional.
* `link_path` - prefix for HTML links, string, required.
* `template_path` - path to Jinja2 template, string, required.
* `template_type` - `parse_mode` to be passed to telegram API, one of `MarkdownV2`, `HTML`, `Markdown`, string, optional, default `HTML`.
## `upload` group
Remote synchronization settings.

View File

@ -12,7 +12,6 @@ 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
```
@ -198,6 +197,67 @@ server {
}
```
## Docker image
We provide official images which can be found under `arcan1s/ahriman` repository. Docker image is being updated on each master commit as well as on each version. If you would like to use last (probably unstable) build you can use `edge` tag or `latest` for any tagged versions; otherwise you can use any version tag available.
The default action (in case if no arguments provided) is `repo-update`. Basically the idea is to run container, e.g.:
```shell
docker run --privileged -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
```
`--privileged` flag is required to make mount possible inside container. In addition, you can pass own configuration overrides by using the same `-v` flag, e.g.:
```shell
docker run -v /path/to/local/repo:/var/lib/ahriman -v /etc/ahriman.ini:/etc/ahriman.ini.d/10-overrides.ini arcan1s/ahriman:latest
```
By default, it runs `repo-update`, but it can be overwritten to any other command you would like to, e.g.:
```shell
docker run arcan1s/ahriman:latest package-add ahriman --now
```
For more details please refer to docker FAQ.
### Environment variables
The following environment variables are supported:
* `AHRIMAN_ARCHITECTURE` - architecture of the repository, default is `x86_64`.
* `AHRIMAN_DEBUG` - if set all commands will be logged to console.
* `AHRIMAN_FORCE_ROOT` - force run ahriman as root instead of guessing by subcommand.
* `AHRIMAN_HOST` - host for the web interface, default is `0.0.0.0`.
* `AHRIMAN_OUTPUT` - controls logging handler, e.g. `syslog`, `console`. The name must be found in logging configuration. Note that if `syslog` (the default) handler is used you will need to mount `/dev/log` inside container because it is not available there.
* `AHRIMAN_PACKAGER` - packager name from which packages will be built, default is `ahriman bot <ahriman@example.com>`.
* `AHRIMAN_PORT` - HTTP server port if any, default is empty.
* `AHRIMAN_REPOSITORY` - repository name, default is `aur-clone`.
* `AHRIMAN_REPOSITORY_ROOT` - repository root. Because of filesystem rights it is required to override default repository root. By default, it uses `ahriman` directory inside ahriman's home, which can be passed as mount volume.
* `AHRIMAN_USER` - ahriman user, usually must not be overwritten, default is `ahriman`.
You can pass any of these variables by using `-e` argument, e.g.:
```shell
docker run -e AHRIMAN_PORT=8080 arcan1s/ahriman:latest
```
### Web service setup
Well for that you would need to have web container instance running forever; it can be achieved by the following command:
```shell
docker run -p 8080:8080 -e AHRIMAN_PORT=8080 -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
```
Note about `AHRIMAN_PORT` environment variable which is required in order to enable web service. An additional port bind by `-p 8080:8080` is required to pass docker port outside of container.
For every next container run use arguments `-e AHRIMAN_PORT=8080 --net=host`, e.g.:
```shell
docker run --privileged -e AHRIMAN_PORT=8080 --net=host -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
```
## Remote synchronization
### Wait I would like to use the repository from another server
@ -342,6 +402,46 @@ There are several choices:
After these steps `index.html` file will be automatically synced to S3
### I would like to get messages to my telegram account/channel
1. It still requires additional dependencies:
```shell
yay -S python-jinja
```
2. Register bot in telegram. You can do it by talking with [@BotFather](https://t.me/botfather). For more details please refer to [official documentation](https://core.telegram.org/bots).
3. Optionally (if you want to post message in chat):
1. Create telegram channel.
2. Invite your bot into the channel.
3. Make your channel public
4. Get chat id if you want to use by numerical id or just use id prefixed with `@` (e.g. `@ahriman`). If you are not using chat the chat id is your user id. If you don't want to make channel public you can use [this guide](https://stackoverflow.com/a/33862907).
5. Configure the service:
```ini
[report]
target = telegram
[telegram]
api_key = aaAAbbBBccCC
chat_id = @ahriman
link_path = http://example.com/x86_64
```
`api_key` is the one sent by [@BotFather](https://t.me/botfather), `chat_id` is the value retrieved from previous step.
If you did everything fine you should receive the message with the next update. Quick credentials check can be done by using the following command:
```shell
curl 'https://api.telegram.org/bot${CHAT_ID}/sendMessage?chat_id=${API_KEY}&text=hello'
```
(replace `${CHAT_ID}` and `${API_KEY}` with the values from configuration).
## Web service
### Readme mentions web interface, how do I use it?
@ -419,6 +519,36 @@ After these steps `index.html` file will be automatically synced to S3
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`.
## Backup and restore
The service provides several commands aim to do easy repository backup and restore. If you would like to move repository from the server `server1.example.com` to another `server2.example.com` you have to perform the following steps:
1. On the source server `server1.example.com` run `repo-backup` command, e.g.:
```shell
sudo ahriman repo-backup /tmp/repo.tar.gz
```
This command will pack all configuration files together with database file into the archive specified as command line argument (i.e. `/tmp/repo.tar.gz`). In addition it will also archive `cache` directory (the one which contains local clones used by e.g. local packages) and `.gnupg` of the `ahriman` user.
2. Copy created archive from source server `server1.example.com` to target `server2.example.com`.
3. Install ahriman as usual on the target server `server2.example.com` if you didn't yet.
4. Extract archive e.g. by using subcommand:
```shell
sudo ahriman repo-restore /tmp/repo.tar.gz
```
An additional argument `-o`/`--output` can be used to specify extraction root (`/` by default).
5. Rebuild repository:
```shell
sudo -u ahriman ahriman repo-rebuild --from-database
```
## Other topics
### How does it differ from %another-manager%?
@ -458,6 +588,10 @@ Though originally I've created ahriman by trying to improve the project, it stil
`repo-scripts` also have bad architecture and bad quality code and uses out-of-dated `yaourt` and `package-query`.
#### [toolbox](https://github.com/chaotic-aur/toolbox)
It is automation tools for `repoctl` mentioned above. Except for using shell it looks pretty cool and also offers some additional features like patches, remote synchronization (isn't it?) and reporting.
### 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).
@ -468,7 +602,7 @@ You can also edit configuration and forward logs to `stderr`, just change `handl
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.
You can even configure logging as you wish, but kindly refer to python `logging` module [configuration](https://docs.python.org/3/library/logging.config.html).
### Html customization

View File

@ -5,11 +5,10 @@
3. TL;DR
```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:
`repo-setup` literally does the following steps:
1. Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):

View File

@ -1,14 +1,14 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=1.7.0
pkgver=2.0.0rc7
pkgrel=1
pkgdesc="ArcH Linux ReposItory MANager"
arch=('any')
url="https://github.com/arcan1s/ahriman"
license=('GPL3')
depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-passlib' 'python-srcinfo')
makedepends=('python-pip')
depends=('devtools' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo')
makedepends=('python-build' 'python-installer' 'python-wheel')
optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support'
'mercurial: -hg packages support'
@ -32,18 +32,23 @@ backup=('etc/ahriman.ini'
build() {
cd "$pkgname"
python setup.py build
python -m build --wheel --no-isolation
}
package() {
cd "$pkgname"
python setup.py install --root="$pkgdir"
python -m installer --destdir="$pkgdir" "dist/$pkgname-$pkgver-py3-none-any.whl"
# python-installer actually thinks that you cannot just copy files to root
# thus we need to copy them manually
install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini"
install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini.d/logging.ini" "$pkgdir/etc/ahriman.ini.d/logging.ini"
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
}
sha512sums=('6ab741bfb42f92ab00d1b6ecfc44426c00e5c433486e014efbdb585715d9a12dbbafc280e5a9f85b941c8681b13a9dad41327a3e3c44a9683ae30c1d6f017f50'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
sha512sums=('112b0d8aac68e5330bbdd2b86a59c8a9af8ab7a7c636489623c8460bb90f1318585851edd2a97a8ce20e2d2ad93b847b522685df707c190aa39d23ab908fa8ef'
'53d37efec812afebf86281716259f9ea78a307b83897166c72777251c3eebcb587ecee375d907514781fb2a5c808cbb24ef9f3f244f12740155d0603bf213131'
'62b2eccc352d33853ef243c9cddd63663014aa97b87242f1b5bc5099a7dbd69ff3821f24ffc58e1b7f2387bd4e9e9712cc4c67f661b1724ad99cdf09b3717794')

View File

@ -1,6 +1,7 @@
[settings]
include = /etc/ahriman.ini.d
logging = /etc/ahriman.ini.d/logging.ini
include = ahriman.ini.d
logging = ahriman.ini.d/logging.ini
database = /var/lib/ahriman/ahriman.db
[alpm]
aur_url = https://aur.archlinux.org
@ -20,7 +21,7 @@ archbuild_flags =
build_command = extra-x86_64-build
ignore_packages =
makechrootpkg_flags =
makepkg_flags =
makepkg_flags = --nocolor
[repository]
name = aur-clone
@ -30,16 +31,22 @@ root = /var/lib/ahriman
target =
[report]
target =
target = console
[console]
use_utf = yes
[email]
full_template_path = /usr/share/ahriman/repo-index.jinja2
full_template_path = /usr/share/ahriman/templates/repo-index.jinja2
no_empty_report = yes
template_path = /usr/share/ahriman/email-index.jinja2
template_path = /usr/share/ahriman/templates/email-index.jinja2
ssl = disabled
[html]
template_path = /usr/share/ahriman/repo-index.jinja2
template_path = /usr/share/ahriman/templates/repo-index.jinja2
[telegram]
template_path = /usr/share/ahriman/templates/telegram-index.jinja2
[upload]
target =
@ -55,5 +62,5 @@ debug = no
debug_check_host = no
debug_allowed_hosts =
host = 127.0.0.1
static_path = /usr/share/ahriman/static
templates = /usr/share/ahriman
static_path = /usr/share/ahriman/templates/static
templates = /usr/share/ahriman/templates

View File

@ -1,5 +1,5 @@
[loggers]
keys = root,build_details,http,stderr,boto3,botocore,nose,s3transfer
keys = root,build_details,database,http,stderr,boto3,botocore,nose,s3transfer
[handlers]
keys = console_handler,syslog_handler
@ -38,6 +38,12 @@ handlers = syslog_handler
qualname = build_details
propagate = 0
[logger_database]
level = DEBUG
handlers = syslog_handler
qualname = database
propagate = 0
[logger_http]
level = DEBUG
handlers = syslog_handler

View File

@ -16,7 +16,7 @@
<h1>ahriman
{% if auth.authenticated %}
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
<img src="https://img.shields.io/badge/repository-{{ repository }}-informational" alt="{{ repository }}">
<img src="https://img.shields.io/badge/repository-{{ repository | replace("-", "--") }}-informational" alt="{{ repository }}">
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
{% endif %}

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -0,0 +1,4 @@
{#simplified version of full report#}
<b>{{ repository }} update</b>
{% for package in packages %}
<a href="{{ link_path }}/{{ package.filename }}">{{ package.name }}</a> {{ package.version }}{% endfor %}

View File

@ -1,5 +0,0 @@
[aliases]
test = pytest
[tool:pytest]
addopts = --cov=ahriman --cov-report term-missing:skip-covered --pspec

View File

@ -29,14 +29,13 @@ setup(
dependency_links=[
],
install_requires=[
"aur",
"inflection",
"passlib",
"pyalpm",
"requests",
"srcinfo",
],
setup_requires=[
"pytest-runner",
],
tests_require=[
"pytest",
@ -44,7 +43,7 @@ setup(
"pytest-cov",
"pytest-helpers-namespace",
"pytest-mock",
"pytest-pspec",
"pytest-spec",
"pytest-resource-path",
],
@ -53,33 +52,34 @@ setup(
"package/bin/ahriman",
],
data_files=[
("/etc", [
"package/etc/ahriman.ini",
("share/ahriman/settings", [
"package/share/ahriman/settings/ahriman.ini",
]),
("/etc/ahriman.ini.d", [
"package/etc/ahriman.ini.d/logging.ini",
("share/ahriman/settings/ahriman.ini.d", [
"package/share/ahriman/settings/ahriman.ini.d/logging.ini",
]),
("lib/systemd/system", [
"package/lib/systemd/system/ahriman@.service",
"package/lib/systemd/system/ahriman@.timer",
"package/lib/systemd/system/ahriman-web@.service",
]),
("share/ahriman", [
"package/share/ahriman/build-status.jinja2",
"package/share/ahriman/email-index.jinja2",
"package/share/ahriman/repo-index.jinja2",
("share/ahriman/templates", [
"package/share/ahriman/templates/build-status.jinja2",
"package/share/ahriman/templates/email-index.jinja2",
"package/share/ahriman/templates/repo-index.jinja2",
"package/share/ahriman/templates/telegram-index.jinja2",
]),
("share/ahriman/build-status", [
"package/share/ahriman/build-status/login-modal.jinja2",
"package/share/ahriman/build-status/package-actions-modals.jinja2",
"package/share/ahriman/build-status/package-actions-script.jinja2",
("share/ahriman/templates/build-status", [
"package/share/ahriman/templates/build-status/login-modal.jinja2",
"package/share/ahriman/templates/build-status/package-actions-modals.jinja2",
"package/share/ahriman/templates/build-status/package-actions-script.jinja2",
]),
("share/ahriman/static", [
"package/share/ahriman/static/favicon.ico",
("share/ahriman/templates/static", [
"package/share/ahriman/templates/static/favicon.ico",
]),
("share/ahriman/utils", [
"package/share/ahriman/utils/bootstrap-scripts.jinja2",
"package/share/ahriman/utils/style.jinja2",
("share/ahriman/templates/utils", [
"package/share/ahriman/templates/utils/bootstrap-scripts.jinja2",
"package/share/ahriman/templates/utils/style.jinja2",
]),
("share/man/man1", [
"docs/ahriman.1",
@ -96,14 +96,14 @@ setup(
"s3": [
"boto3",
],
"test": [
"tests": [
"pytest",
"pytest-aiohttp",
"pytest-cov",
"pytest-helpers-namespace",
"pytest-mock",
"pytest-pspec",
"pytest-resource-path",
"pytest-spec",
],
"web": [
"Jinja2",

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -43,8 +43,12 @@ SubParserAction = TypeVar("SubParserAction", bound="argparse._SubParsersAction[a
def _formatter(prog: str) -> argparse.HelpFormatter:
"""
formatter for the help message
:param prog: application name
:return: formatter used by default
Args:
prog(str): application name
Returns:
argparse.HelpFormatter: formatter used by default
"""
return argparse.ArgumentDefaultsHelpFormatter(prog, width=120)
@ -52,7 +56,9 @@ def _formatter(prog: str) -> argparse.HelpFormatter:
def _parser() -> argparse.ArgumentParser:
"""
command line parser generator
:return: command line parser for the application
Returns:
argparse.ArgumentParser: command line parser for the application
"""
parser = argparse.ArgumentParser(prog="ahriman", description="ArcH Linux ReposItory MANager",
epilog="Argument list can also be read from file by using @ prefix.",
@ -72,6 +78,8 @@ def _parser() -> argparse.ArgumentParser:
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
_set_aur_search_parser(subparsers)
_set_help_parser(subparsers)
_set_help_commands_unsafe_parser(subparsers)
_set_key_import_parser(subparsers)
_set_package_add_parser(subparsers)
_set_package_remove_parser(subparsers)
@ -81,19 +89,21 @@ def _parser() -> argparse.ArgumentParser:
_set_patch_add_parser(subparsers)
_set_patch_list_parser(subparsers)
_set_patch_remove_parser(subparsers)
_set_repo_backup_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_restore_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_list_parser(subparsers)
_set_user_remove_parser(subparsers)
_set_web_parser(subparsers)
@ -103,13 +113,18 @@ def _parser() -> argparse.ArgumentParser:
def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for AUR search subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
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("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.add_argument("--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",
@ -118,11 +133,53 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing help subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help", help="show help message",
description="show help message for application or command and exit",
formatter_class=_formatter)
parser.add_argument("command", help="show help message for specific command", nargs="?")
parser.set_defaults(handler=handlers.Help, architecture=[""], lock=None, no_report=True, quiet=True,
unsafe=True, parser=_parser)
return parser
def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing unsafe commands
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args", formatter_class=_formatter)
parser.add_argument("--command", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, no_report=True, quiet=True,
unsafe=True, parser=_parser)
return parser
def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for key import subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("key-import", help="import PGP key",
description="import PGP key from public sources to the repository user",
@ -140,8 +197,12 @@ def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package addition subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-add", aliases=["add", "package-update"], help="add package",
description="add existing or new package to the build queue",
@ -157,6 +218,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"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("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=PackageSource, default=PackageSource.Auto)
@ -168,8 +230,12 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_package_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package removal subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-remove", aliases=["remove"], help="remove package",
description="remove package from the repository", formatter_class=_formatter)
@ -181,8 +247,12 @@ def _set_package_remove_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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-status", aliases=["status"], help="get package status",
description="request status of the package",
@ -190,6 +260,7 @@ def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser
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("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status",
type=BuildStatusEnum, choices=BuildStatusEnum)
@ -200,8 +271,12 @@ def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser
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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-status-remove", help="remove package status",
description="remove the package from the status page",
@ -217,8 +292,12 @@ def _set_package_status_remove_parser(root: SubParserAction) -> argparse.Argumen
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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: 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)
@ -235,8 +314,12 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen
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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: 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 "
@ -255,12 +338,17 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: 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.add_argument("package", help="package base", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True)
return parser
@ -268,8 +356,12 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package",
formatter_class=_formatter)
@ -278,16 +370,38 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository backup subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-backup", help="backup repository data",
description="backup settings and database", formatter_class=_formatter)
parser.add_argument("path", help="path of the output archive", type=Path)
parser.set_defaults(handler=handlers.Backup, architecture=[""], lock=None, no_report=True, unsafe=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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: 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("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
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_local=False, no_manual=True)
return parser
@ -296,8 +410,12 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-clean", aliases=["clean"], help="clean local caches",
description="remove local caches",
@ -305,12 +423,10 @@ def _set_repo_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
"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("--manual", help="clear manually added packages queue", 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
@ -318,8 +434,12 @@ def _set_repo_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for config subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-config", aliases=["config"], help="dump configuration",
description="dump configuration for the specified architecture",
@ -328,30 +448,28 @@ def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: 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.add_argument("--dry-run", help="just perform check for packages without rebuild process itself",
action="store_true")
parser.add_argument("--from-database",
help="read packages from database instead of filesystem. This feature in particular is "
"required in case if you would like to restore repository from another repository "
"instance. Note however that in order to restore packages you need to have original "
"ahriman instance run with web service and have run repo-update at least once.",
action="store_true")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.set_defaults(handler=handlers.Rebuild)
return parser
@ -359,8 +477,12 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
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",
@ -374,8 +496,12 @@ def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentP
def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for report subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-report", aliases=["report"], help="generate report",
description="generate repository report according to current settings",
@ -386,16 +512,39 @@ def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository restore subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-restore", help="restore repository data",
description="restore settings and database", formatter_class=_formatter)
parser.add_argument("path", help="path of the input archive", type=Path)
parser.add_argument("-o", "--output", help="root path of the extracted files", type=Path, default=Path("/"))
parser.set_defaults(handler=handlers.Restore, architecture=[""], lock=None, no_report=True, unsafe=True)
return parser
def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for setup subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-setup", aliases=["setup"], help="initial service configuration",
parser = root.add_parser("repo-setup", aliases=["init", "repo-init", "setup"], help="initial service configuration",
description="create initial service configuration, requires root",
epilog="Create _minimal_ configuration for the service according to provided options.",
formatter_class=_formatter)
parser.add_argument("--build-as-user", help="force makepkg user to the specific one")
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"))
@ -413,8 +562,12 @@ def _set_repo_setup_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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-sign", aliases=["sign"], help="sign packages",
description="(re-)sign packages and repository database according to current settings",
@ -428,8 +581,12 @@ def _set_repo_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository status update subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-status-update", help="update repository status",
description="update repository status on the status page", formatter_class=_formatter)
@ -443,8 +600,12 @@ def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentPa
def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository sync subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-sync", aliases=["sync"], help="sync repository",
description="sync repository files to remote server according to current settings",
@ -458,14 +619,19 @@ def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository update subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
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("-e", "--exit-code", help="return non-zero exit status if result is empty", 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-local", help="do not check local packages for updates", action="store_true")
parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
@ -477,8 +643,12 @@ def _set_repo_update_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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("user-add", help="create or update user",
description="update user for web services with the given password and role. "
@ -486,7 +656,6 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
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("--no-reload", help="do not reload authentication module", action="store_true")
parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, "
"which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-r", "--role", help="user access level",
@ -497,28 +666,56 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for user list subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("user-list", help="user known users and their access",
description="list users from the user mapping and their roles",
formatter_class=_formatter)
parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=UserAccess)
parser.set_defaults(handler=handlers.User, action=Action.List, architecture=[""], lock=None, no_report=True, # nosec
password="", 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
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: 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)
password="", quiet=True, unsafe=True)
return parser
def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for web subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
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)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -17,11 +17,11 @@
# 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 typing import Set
from ahriman.application.application.packages import Packages
from ahriman.application.application.repository import Repository
from ahriman.models.package import Package
from ahriman.models.result import Result
class Application(Packages, Repository):
@ -29,17 +29,22 @@ class Application(Packages, Repository):
base application class
"""
def _finalize(self, built_packages: Iterable[Package]) -> None:
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
"""
self.report([], built_packages)
self.sync([], built_packages)
self.report([], result)
self.sync([], result.success)
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
:return: list of known packages
Returns:
Set[str]: list of known packages
"""
known_packages: Set[str] = set()
# local set

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -25,9 +25,10 @@ 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.core.util import package_like, tmpdir
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result
class Packages(Properties):
@ -35,23 +36,36 @@ class Packages(Properties):
package control class
"""
def _finalize(self, built_packages: Iterable[Package]) -> None:
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
:return: list of known packages
Returns:
Set[str]: list of known packages
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def _add_archive(self, source: str, *_: Any) -> None:
"""
add package from archive
:param source: path to package archive
Args:
source(str): path to package archive
"""
local_path = Path(source)
dst = self.repository.paths.packages / local_path.name
@ -60,20 +74,26 @@ class Packages(Properties):
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
Args:
source(str): package base name
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
"""
package = Package.load(source, PackageSource.AUR, self.repository.pacman, self.repository.aur_url)
local_path = self.repository.paths.manual_for(package.base)
Sources.load(local_path, package.git_url, self.repository.paths.patches_for(package.base))
self._process_dependencies(local_path, known_packages, without_dependencies)
self.database.build_queue_insert(package)
with tmpdir() as local_path:
Sources.load(local_path, package.git_url, self.database.patches_get(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
Args:
source(str): path to local directory
"""
local_path = Path(source)
for full_path in filter(package_like, local_path.iterdir()):
@ -82,23 +102,27 @@ class Packages(Properties):
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
Args:
source(str): path to directory with local source files
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
"""
package = Package.load(source, PackageSource.Local, self.repository.pacman, self.repository.aur_url)
cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(Path(source), 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)
self.database.build_queue_insert(package)
self._process_dependencies(cache_dir, 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
Args:
source(str): remote URL of the package archive
"""
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
response = requests.get(source, stream=True)
@ -111,9 +135,11 @@ class Packages(Properties):
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
Args:
local_path(Path): path to local package sources (i.e. cloned AUR repository)
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
"""
if without_dependencies:
return
@ -124,9 +150,11 @@ class Packages(Properties):
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
Args:
names(Iterable[str]): list of package bases to add
source(PackageSource): package source to add
without_dependencies(bool): if set, dependency check will be disabled
"""
known_packages = self._known_packages() # speedup dependencies processing
@ -138,7 +166,9 @@ class Packages(Properties):
def remove(self, names: Iterable[str]) -> None:
"""
remove packages from repository
:param names: list of packages (either base or name) to remove
Args:
names(Iterable[str]): list of packages (either base or name) to remove
"""
self.repository.process_remove(names)
self._finalize([])
self._finalize(Result())

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -20,26 +20,34 @@
import logging
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
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
Attributes:
architecture(str): repository architecture
configuration(Configuration): configuration instance
database(SQLite): database instance
logger(logging.Logger): application logger
repository(Repository): repository instance
"""
def __init__(self, architecture: str, configuration: Configuration, no_report: bool) -> None:
def __init__(self, architecture: str, configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
self.logger = logging.getLogger("root")
self.configuration = configuration
self.architecture = architecture
self.repository = Repository(architecture, configuration, no_report)
self.database = SQLite.load(configuration)
self.repository = Repository(architecture, configuration, self.database, no_report, unsafe)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -23,11 +23,11 @@ from pathlib import Path
from typing import Callable, Iterable, List
from ahriman.application.application.properties import Properties
from ahriman.application.formatters.update_printer import UpdatePrinter
from ahriman.core.build_tools.sources import Sources
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.tree import Tree
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result
class Repository(Properties):
@ -35,48 +35,54 @@ class Repository(Properties):
repository control class
"""
def _finalize(self, built_packages: Iterable[Package]) -> None:
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def clean(self, build: bool, cache: bool, chroot: bool, manual: bool, packages: bool, patches: bool) -> None:
def clean(self, cache: bool, chroot: bool, manual: bool, packages: bool) -> None:
"""
run all clean methods. Warning: some functions might not be available under non-root
: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
Args:
cache(bool): clear directory with package caches
chroot(bool): clear build chroot
manual(bool): clear directory with manually added packages
packages(bool): clear directory with built packages
"""
if build:
self.repository.clear_build()
if cache:
self.repository.clear_cache()
if chroot:
self.repository.clear_chroot()
if manual:
self.repository.clear_manual()
self.repository.clear_queue()
if packages:
self.repository.clear_packages()
if patches:
self.repository.clear_patches()
def report(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
def report(self, target: Iterable[str], result: Result) -> None:
"""
generate report
:param target: list of targets to run (e.g. html)
:param built_packages: list of packages which has just been built
Args:
target(Iterable[str]): list of targets to run (e.g. html)
result(Result): build result
"""
targets = target or None
self.repository.process_report(targets, built_packages)
self.repository.process_report(targets, result)
def sign(self, packages: Iterable[str]) -> None:
"""
sign packages and repository
:param packages: only sign specified packages
Args:
packages(Iterable[str]): only sign specified packages
"""
# copy to prebuilt directory
for package in self.repository.packages():
@ -94,13 +100,15 @@ class Repository(Properties):
self.update([])
# sign repository database if set
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
self._finalize([])
self._finalize(Result())
def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
"""
sync to remote server
:param target: list of targets to run (e.g. s3)
:param built_packages: list of packages which has just been built
Args:
target(Iterable[str]): list of targets to run (e.g. s3)
built_packages(Iterable[Package]): list of packages which has just been built
"""
targets = target or None
self.repository.process_sync(targets, built_packages)
@ -108,7 +116,9 @@ class Repository(Properties):
def unknown(self) -> List[str]:
"""
get packages which were not found in AUR
:return: unknown package archive list
Returns:
List[str]: unknown package archive list
"""
def has_local(probe: Package) -> bool:
cache_dir = self.repository.paths.cache_for(probe.base)
@ -137,43 +147,52 @@ class Repository(Properties):
result.extend(unknown_aur(package)) # local package not found
return result
def update(self, updates: Iterable[Package]) -> None:
def update(self, updates: Iterable[Package]) -> Result:
"""
run package updates
:param updates: list of packages to update
Args:
updates(Iterable[Package]): list of packages to update
Returns:
Result: update result
"""
def process_update(paths: Iterable[Path]) -> None:
def process_update(paths: Iterable[Path], result: Result) -> None:
if not paths:
return # don't need to process if no update supplied
updated = [
Package.load(str(path), PackageSource.Archive, self.repository.pacman, self.repository.aur_url)
for path in paths
]
self.repository.process_update(paths)
self._finalize(updated)
update_result = self.repository.process_update(paths)
self._finalize(result.merge(update_result))
# process built packages
build_result = Result()
packages = self.repository.packages_built()
process_update(packages)
process_update(packages, build_result)
# process manual packages
tree = Tree.load(updates, self.repository.paths)
tree = Tree.load(updates, self.database)
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)
build_result = self.repository.process_build(level)
packages = self.repository.packages_built()
process_update(packages, build_result)
return build_result
def updates(self, filter_packages: Iterable[str], no_aur: bool, no_local: 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_local: do not check local packages for 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
Args:
filter_packages(Iterable[str]): do not check every package just specified in the list
no_aur(bool): do not check for aur updates
no_local(bool): do not check local packages for updates
no_manual(bool): do not check for manual updates
no_vcs(bool): do not check VCS packages
log_fn(Callable[[str], None]): logger function to log updates
Returns:
List[Package]: list of out-of-dated packages
"""
updates = {}

View File

@ -1,62 +0,0 @@
#
# 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

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -20,21 +20,24 @@
from ahriman.application.handlers.handler import Handler
from ahriman.application.handlers.add import Add
from ahriman.application.handlers.backup import Backup
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.help import Help
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
from ahriman.application.handlers.report import Report
from ahriman.application.handlers.restore import Restore
from ahriman.application.handlers.search import Search
from ahriman.application.handlers.setup import Setup
from ahriman.application.handlers.sign import Sign
from ahriman.application.handlers.status import Status
from ahriman.application.handlers.status_update import StatusUpdate
from ahriman.application.handlers.sync import Sync
from ahriman.application.handlers.unsafe_commands import UnsafeCommands
from ahriman.application.handlers.update import Update
from ahriman.application.handlers.user import User
from ahriman.application.handlers.web import Web

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -33,18 +33,22 @@ class Add(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report)
application = Application(architecture, configuration, no_report, unsafe)
application.add(args.package, args.source, args.without_dependencies)
if not args.now:
return
packages = application.updates(args.package, True, True, False, True, application.logger.info)
application.update(packages)
result = application.update(packages)
Add.check_if_empty(args.exit_code, result.is_empty)

View File

@ -0,0 +1,86 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import pwd
from pathlib import Path
from tarfile import TarFile
from typing import Set, Type
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
class Backup(Handler):
"""
backup 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, unsafe: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
backup_paths = Backup.get_paths(configuration)
with TarFile(args.path, mode="w") as archive: # well we don't actually use compression
for backup_path in backup_paths:
archive.add(backup_path)
@staticmethod
def get_paths(configuration: Configuration) -> Set[Path]:
"""
extract paths to backup
Args:
configuration(Configuration): configuration instance
Returns:
Set[Path]: map of the filesystem paths
"""
paths = set(configuration.include.glob("*.ini"))
root, _ = configuration.check_loaded()
paths.add(root) # the configuration itself
paths.add(SQLite.database_path(configuration)) # database
# local caches
repository_paths = configuration.repository_paths
if repository_paths.cache.is_dir():
paths.add(repository_paths.cache)
# gnupg home with imported keys
uid, _ = repository_paths.root_owner
system_user = pwd.getpwuid(uid)
gnupg_home = Path(system_user.pw_dir) / ".gnupg"
if gnupg_home.is_dir():
paths.add(gnupg_home)
return paths

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -33,13 +33,16 @@ class Clean(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report).clean(
args.build, args.cache, args.chroot, args.manual, args.packages, args.patches)
Application(architecture, configuration, no_report, unsafe).clean(
args.cache, args.chroot, args.manual, args.packages)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -21,9 +21,9 @@ 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
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
class Dump(Handler):
@ -35,13 +35,16 @@ class Dump(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
dump = configuration.dump()
for section, values in sorted(dump.items()):

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -23,37 +23,46 @@ import argparse
import logging
from multiprocessing import Pool
from typing import Set, Type
from typing import List, Type
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitecture, MultipleArchitecture
from ahriman.core.exceptions import ExitCode, MissingArchitecture, MultipleArchitectures
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
Attributes:
ALLOW_AUTO_ARCHITECTURE_RUN(bool): (class attribute) allow defining architecture from existing repositories
ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running 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]:
def architectures_extract(cls: Type[Handler], args: argparse.Namespace) -> List[str]:
"""
get known architectures
:param args: command line args
:return: list of architectures for which tree is created
Args:
args(argparse.Namespace): command line args
Returns:
List[str]: list of architectures for which tree is created
Raises:
MissingArchitecture: if no architecture set and automatic detection is not allowed or failed
"""
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)
return sorted(set(args.architecture))
config = Configuration()
config.load(args.configuration)
@ -63,21 +72,27 @@ class Handler:
if not architectures: # well we did not find anything
raise MissingArchitecture(args.command)
return architectures
return sorted(architectures)
@classmethod
def call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
"""
additional function to wrap all calls for multiprocessing library
:param args: command line args
:param architecture: repository architecture
:return: True on success, False otherwise
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
Returns:
bool: True on success, False otherwise
"""
try:
configuration = Configuration.from_path(args.configuration, architecture, args.quiet)
with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration, args.no_report)
cls.run(args, architecture, configuration, args.no_report, args.unsafe)
return True
except ExitCode:
return False
except Exception:
# we are basically always want to print error to stderr instead of default logger
logging.getLogger("stderr").exception("process exception")
@ -87,15 +102,22 @@ class Handler:
def execute(cls: Type[Handler], args: argparse.Namespace) -> int:
"""
execute function for all aru
:param args: command line args
:return: 0 on success, 1 otherwise
Args:
args(argparse.Namespace): command line args
Returns:
int: 0 on success, 1 otherwise
Raises:
MultipleArchitectures: if more than one architecture supplied and no multi architecture supported
"""
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:
if not cls.ALLOW_MULTI_ARCHITECTURE_RUN:
raise MultipleArchitecture(args.command)
raise MultipleArchitectures(args.command)
with Pool(len(architectures)) as pool:
result = pool.starmap(
@ -107,12 +129,33 @@ class Handler:
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
@staticmethod
def check_if_empty(enabled: bool, predicate: bool) -> None:
"""
check condition and flag and raise ExitCode exception in case if it is enabled and condition match
Args:
enabled(bool): if False no check will be performed
predicate(bool): indicates condition on which exception should be thrown
Raises:
ExitCode: if result is empty and check is enabled
"""
if enabled and predicate:
raise ExitCode()

View File

@ -0,0 +1,52 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
from typing import Type
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
class Help(Handler):
"""
help handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
parser: argparse.ArgumentParser = args.parser()
if args.command is None:
parser.parse_args(["--help"])
else:
parser.parse_args([args.command, "--help"])

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -35,12 +35,16 @@ class KeyImport(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report).repository.sign.key_import(args.key_server, args.key)
Application(architecture, configuration, no_report, unsafe).repository.sign.key_import(
args.key_server, args.key)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -18,15 +18,15 @@
# 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 typing import List, Optional, 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.core.formatters.string_printer import StringPrinter
from ahriman.models.action import Action
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
@ -37,22 +37,23 @@ 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:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report)
application = Application(architecture, configuration, no_report, unsafe)
if args.action == Action.List:
Patch.patch_set_list(application, args.package)
Patch.patch_set_list(application, args.package, args.exit_code)
elif args.action == Action.Remove:
Patch.patch_set_remove(application, args.package)
elif args.action == Action.Update:
@ -62,38 +63,41 @@ class Patch(Handler):
def patch_set_create(application: Application, sources_dir: str, 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
Args:
application(Application): application instance
sources_dir(str): path to directory with the package sources
track(List[str]): track files which match the glob before creating the patch
"""
package = Package.load(sources_dir, PackageSource.Local, 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(Path(sources_dir), patch_dir / "00-main.patch", *track)
patch = Sources.patch_create(Path(sources_dir), *track)
application.database.patches_insert(package.base, patch)
@staticmethod
def patch_set_list(application: Application, package_base: str) -> None:
def patch_set_list(application: Application, package_base: Optional[str], exit_code: bool) -> None:
"""
list patches available for the package base
:param application: application instance
:param package_base: package base
Args:
application(Application): application instance
package_base(Optional[str]): package base
exit_code(bool): exit with error on empty search result
"""
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)
patches = application.database.patches_list(package_base)
Patch.check_if_empty(exit_code, not patches)
for base, patch in patches.items():
content = base if package_base is None else patch
StringPrinter(content).print(verbose=True)
@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
Args:
application(Application): application instance
package_base(str): package base
"""
patch_dir = application.repository.paths.patches_for(package_base)
shutil.rmtree(patch_dir, ignore_errors=True)
application.database.patches_remove(package_base)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -19,12 +19,13 @@
#
import argparse
from typing import Type
from typing import List, Type
from ahriman.application.application import Application
from ahriman.application.formatters.update_printer import UpdatePrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.models.package import Package
class Rebuild(Handler):
@ -34,21 +35,43 @@ class Rebuild(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
depends_on = set(args.depends_on) if args.depends_on else None
application = Application(architecture, configuration, no_report)
updates = application.repository.packages_depends_on(depends_on)
application = Application(architecture, configuration, no_report, unsafe)
if args.from_database:
updates = Rebuild.extract_packages(application)
else:
updates = application.repository.packages_depends_on(depends_on)
Rebuild.check_if_empty(args.exit_code, not updates)
if args.dry_run:
for package in updates:
UpdatePrinter(package, package.version).print(verbose=True)
return
application.update(updates)
result = application.update(updates)
Rebuild.check_if_empty(args.exit_code, result.is_empty)
@staticmethod
def extract_packages(application: Application) -> List[Package]:
"""
extract packages from database file
Args:
application(Application): application instance
Returns:
List[Package]: list of packages which were stored in database
"""
return [package for (package, _) in application.database.packages_get()]

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -33,12 +33,15 @@ class Remove(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report).remove(args.package)
Application(architecture, configuration, no_report, unsafe).remove(args.package)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -22,9 +22,9 @@ import argparse
from typing import Type
from ahriman.application.application import Application
from ahriman.application.formatters.string_printer import StringPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters.string_printer import StringPrinter
class RemoveUnknown(Handler):
@ -34,16 +34,20 @@ class RemoveUnknown(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report)
application = Application(architecture, configuration, no_report, unsafe)
unknown_packages = application.unknown()
if args.dry_run:
for package in sorted(unknown_packages):
StringPrinter(package).print(args.info)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -24,6 +24,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.result import Result
class Report(Handler):
@ -33,12 +34,15 @@ class Report(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report).report(args.target, [])
Application(architecture, configuration, no_report, unsafe).report(args.target, Result())

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -20,27 +20,31 @@
import argparse
from typing import Type
from tarfile import TarFile
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
class Init(Handler):
class Restore(Handler):
"""
repository init handler
restore packages handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
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:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report).repository.repo.init()
with TarFile(args.path) as archive:
archive.extractall(path=args.output)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -18,51 +18,71 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import aur # type: ignore
from dataclasses import fields
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.alpm.remote.aur import AUR
from ahriman.core.alpm.remote.official import Official
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidOption
from ahriman.core.util import aur_search
from ahriman.core.formatters.aur_printer import AurPrinter
from ahriman.models.aur_package import AURPackage
class Search(Handler):
"""
packages search handler
Attributes:
SORT_FIELDS(Set[str]): (class attribute) allowed fields to sort the package list
"""
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)
SORT_FIELDS = {field.name for field in fields(AURPackage) if field.default_factory is not list}
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
packages_list = aur_search(*args.search)
for package in Search.sort(packages_list, args.sort_by):
AurPrinter(package).print(args.info)
official_packages_list = Official.multisearch(*args.search)
aur_packages_list = AUR.multisearch(*args.search)
Search.check_if_empty(args.exit_code, not official_packages_list and not aur_packages_list)
for packages_list in (official_packages_list, aur_packages_list):
# keep sorting by packages source
for package in Search.sort(packages_list, args.sort_by):
AurPrinter(package).print(args.info)
@staticmethod
def sort(packages: Iterable[aur.Package], sort_by: str) -> List[aur.Package]:
def sort(packages: Iterable[AURPackage], sort_by: str) -> List[AURPackage]:
"""
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
Args:
packages(Iterable[AURPackage]): packages list to sort
sort_by(str): AUR package field name to sort by
Returns:
List[AURPackage]: sorted list for packages
Raises:
InvalidOption: if search fields is not in list of allowed ones
"""
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]] =\
comparator: Callable[[AURPackage], Tuple[str, str]] =\
lambda package: (getattr(package, sort_by), package.name)
return sorted(packages, key=comparator)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -31,10 +31,12 @@ from ahriman.models.repository_paths import RepositoryPaths
class Setup(Handler):
"""
setup handler
:cvar ARCHBUILD_COMMAND_PATH: default devtools command
:cvar BIN_DIR_PATH: directory for custom binaries
:cvar MIRRORLIST_PATH: path to pacman default mirrorlist (used by multilib repository)
:cvar SUDOERS_PATH: path to sudoers.d include configuration
Attributes:
ARCHBUILD_COMMAND_PATH(Path): (class attribute) default devtools command
BIN_DIR_PATH(Path): (class attribute) directory for custom binaries
MIRRORLIST_PATH(Path): (class attribute) path to pacman default mirrorlist (used by multilib repository)
SUDOERS_PATH(Path): (class attribute) path to sudoers.d include configuration
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@ -46,29 +48,41 @@ class Setup(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report)
Setup.configuration_create_ahriman(args, architecture, args.repository, configuration.include)
configuration.reload()
application = Application(architecture, configuration, no_report, unsafe)
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.configuration_create_ahriman(args, architecture, args.repository, configuration.include)
Setup.configuration_create_sudo(args.build_command, architecture)
application.repository.repo.init()
@staticmethod
def build_command(prefix: str, architecture: str) -> Path:
"""
generate build command name
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
:return: valid devtools command name
Args:
prefix(str): command prefix in {prefix}-{architecture}-build
architecture(str): repository architecture
Returns:
Path: valid devtools command name
"""
return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build"
@ -77,16 +91,20 @@ class Setup(Handler):
include_path: Path) -> None:
"""
create service specific configuration
:param args: command line args
:param architecture: repository architecture
:param repository: repository name
:param include_path: path to directory with configuration includes
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
repository(str): repository name
include_path(Path): path to directory with configuration includes
"""
configuration = Configuration()
section = Configuration.section_name("build", architecture)
configuration.set_option(section, "build_command", str(Setup.build_command(args.build_command, architecture)))
configuration.set_option("repository", "name", repository)
if args.build_as_user is not None:
configuration.set_option(section, "makechrootpkg_flags", f"-U {args.build_as_user}")
if args.sign_key is not None:
section = Configuration.section_name("sign", architecture)
@ -106,12 +124,14 @@ class Setup(Handler):
no_multilib: bool, repository: str, paths: RepositoryPaths) -> None:
"""
create configuration for devtools based on `source` configuration
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
:param source: path to source configuration file
:param no_multilib: do not add multilib repository
:param repository: repository name
:param paths: repository paths instance
Args:
prefix(str): command prefix in {prefix}-{architecture}-build
architecture(str): repository architecture
source(Path): path to source configuration file
no_multilib(bool): do not add multilib repository
repository(str): repository name
paths(RepositoryPaths): repository paths instance
"""
configuration = Configuration()
# preserve case
@ -141,17 +161,21 @@ class Setup(Handler):
def configuration_create_makepkg(packager: str, paths: RepositoryPaths) -> None:
"""
create configuration for makepkg
:param packager: packager identifier (e.g. name, email)
:param paths: repository paths instance
Args:
packager(str): packager identifier (e.g. name, email)
paths(RepositoryPaths): repository paths instance
"""
(paths.root / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n")
(paths.root / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n", encoding="utf8")
@staticmethod
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
:param architecture: repository architecture
Args:
prefix(str): command prefix in {prefix}-{architecture}-build
architecture(str): repository architecture
"""
command = Setup.build_command(prefix, architecture)
Setup.SUDOERS_PATH.write_text(f"ahriman ALL=(ALL) NOPASSWD: {command} *\n", encoding="utf8")
@ -161,8 +185,10 @@ class Setup(Handler):
def executable_create(prefix: str, architecture: str) -> None:
"""
create executable for the service
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
Args:
prefix(str): command prefix in {prefix}-{architecture}-build
architecture(str): repository architecture
"""
command = Setup.build_command(prefix, architecture)
command.unlink(missing_ok=True)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -33,12 +33,15 @@ class Sign(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report).sign(args.package)
Application(architecture, configuration, no_report, unsafe).sign(args.package)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -22,10 +22,10 @@ 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.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
@ -39,16 +39,19 @@ class Status(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
# we are using reporter here
client = Application(architecture, configuration, no_report=False).repository.reporter
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter
if args.ahriman:
ahriman = client.get_self()
StatusPrinter(ahriman).print(args.info)
@ -59,6 +62,8 @@ class Status(Handler):
else:
packages = client.get(None)
Status.check_if_empty(args.exit_code, not packages)
comparator: Callable[[Tuple[Package, BuildStatus]], str] = lambda item: item[0].base
filter_fn: Callable[[Tuple[Package, BuildStatus]], bool] =\
lambda item: args.status is None or item[1].status == args.status

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -36,16 +36,19 @@ class StatusUpdate(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
# we are using reporter here
client = Application(architecture, configuration, no_report=False).repository.reporter
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter
if args.action == Action.Update and args.package:
# update packages statuses

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -28,17 +28,20 @@ from ahriman.core.configuration import Configuration
class Sync(Handler):
"""
remove sync handler
remote sync handler
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report).sync(args.target, [])
Application(architecture, configuration, no_report, unsafe).sync(args.target, [])

View File

@ -0,0 +1,84 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import shlex
from typing import List, Type
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters.string_printer import StringPrinter
class UnsafeCommands(Handler):
"""
unsafe command help parser
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
parser = args.parser()
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
if args.command is None:
for command in unsafe_commands:
StringPrinter(command).print(verbose=True)
else:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
@staticmethod
def check_unsafe(command: str, unsafe_commands: List[str], parser: argparse.ArgumentParser) -> None:
"""
check if command is unsafe
Args:
command(str): command to check
unsafe_commands(List[str]): list of unsafe commands
parser(argparse.ArgumentParser): generated argument parser
"""
args = parser.parse_args(shlex.split(command))
UnsafeCommands.check_if_empty(True, args.command in unsafe_commands)
@staticmethod
def get_unsafe_commands(parser: argparse.ArgumentParser) -> List[str]:
"""
extract unsafe commands from argument parser
Args:
parser(argparse.ArgumentParser): generated argument parser
Returns:
List[str]: list of commands with default unsafe flag
"""
# pylint: disable=protected-access
subparser = next(action for action in parser._actions if isinstance(action, argparse._SubParsersAction))
return [action_name for action_name, action in subparser.choices.items() if action.get_default("unsafe")]

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -33,29 +33,38 @@ class Update(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report)
application = Application(architecture, configuration, no_report, unsafe)
packages = application.updates(args.package, args.no_aur, args.no_local, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run))
Update.check_if_empty(args.exit_code, not packages)
if args.dry_run:
return
application.update(packages)
result = application.update(packages)
Update.check_if_empty(args.exit_code, result.is_empty)
@staticmethod
def log_fn(application: Application, dry_run: bool) -> Callable[[str], None]:
"""
package updates log function
:param application: application instance
:param dry_run: do not perform update itself
:return: in case if dry_run is set it will return print, logger otherwise
Args:
application(Application): application instance
dry_run(bool): do not perform update itself
Returns:
Callable[[str], None]: in case if dry_run is set it will return print, logger otherwise
"""
def inner(line: str) -> None:
return print(line) if dry_run else application.logger.info(line)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -23,12 +23,12 @@ import getpass
from pathlib import Path
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.database.sqlite import SQLite
from ahriman.core.formatters.user_printer import UserPrinter
from ahriman.models.action import Action
from ahriman.models.user import User as MUser
from ahriman.models.user_access import UserAccess
class User(Handler):
@ -40,102 +40,114 @@ class User(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
salt = User.get_salt(configuration)
user = User.user_create(args)
auth_configuration = User.configuration_get(configuration.include)
database = SQLite.load(configuration)
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)
salt = User.get_salt(configuration)
user = User.user_create(args)
if not args.no_reload:
client = Application(architecture, configuration, no_report=False).repository.reporter
client.reload_auth()
auth_configuration = User.configuration_get(configuration.include)
User.configuration_create(auth_configuration, user, salt, args.as_service, args.secure)
database.user_update(user.hash_password(salt))
elif args.action == Action.List:
users = database.user_list(args.username, args.role)
User.check_if_empty(args.exit_code, not users)
for user in users:
UserPrinter(user).print(verbose=True)
elif args.action == Action.Remove:
database.user_remove(args.username)
@staticmethod
def configuration_create(configuration: Configuration, user: MUser, salt: str, as_service_user: bool) -> None:
def configuration_create(configuration: Configuration, user: MUser, salt: str,
as_service_user: bool, secure: bool) -> None:
"""
put new user to configuration
:param configuration: configuration instance
:param user: user descriptor
:param salt: password hash salt
:param as_service_user: add user as service user, also set password and user to configuration
"""
section = Configuration.section_name("auth", user.access.value)
configuration.set_option("auth", "salt", salt)
configuration.set_option(section, user.username, user.hash_password(salt))
enable configuration if it has been disabled
Args:
configuration(Configuration): configuration instance
user(MUser): user descriptor
salt(str): password hash salt
as_service_user(bool): add user as service user, also set password and user to configuration
secure(bool): if true then set file permissions to 0o600
"""
configuration.set_option("auth", "salt", salt)
if as_service_user:
configuration.set_option("web", "username", user.username)
configuration.set_option("web", "password", user.password)
User.configuration_write(configuration, secure)
@staticmethod
def configuration_get(include_path: Path) -> Configuration:
"""
create configuration instance
:param include_path: path to directory with configuration includes
:return: configuration instance. In case if there are local settings they will be loaded
Args:
include_path(Path): path to directory with configuration includes
Returns:
Configuration: configuration instance. In case if there are local settings they will be loaded
"""
target = include_path / "auth.ini"
configuration = Configuration()
configuration.load(target)
configuration.architecture = "" # not user anyway
return configuration
@staticmethod
def configuration_write(configuration: Configuration, secure: bool) -> None:
"""
write configuration file
:param configuration: configuration instance
:param secure: if true then set file permissions to 0o600
Args:
configuration(Configuration): configuration instance
secure(bool): if true then set file permissions to 0o600
"""
if configuration.path is None:
return # should never happen actually
with configuration.path.open("w") as ahriman_configuration:
path, _ = configuration.check_loaded()
with path.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)
if secure:
configuration.path.chmod(0o600)
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
Args:
configuration(Configuration): configuration instance
salt_length(int, optional): salt length (Default value = 20)
Returns:
str: 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
Args:
args(argparse.Namespace): command line args
Returns:
MUser: built user descriptor
"""
user = MUser(args.username, args.password, args.role)
if user.password is None:

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -36,13 +36,16 @@ class Web(Handler):
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
configuration: Configuration, no_report: bool, unsafe: 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
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
# we are using local import for optional dependencies
from ahriman.web.web import run_server, setup_service

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -37,36 +37,40 @@ from ahriman.models.build_status import BuildStatusEnum
class Lock:
"""
wrapper for application lock file
:ivar force: remove lock file on start if any
:ivar path: path to lock file if any
:ivar reporter: build status reporter instance
:ivar root: repository root (i.e. ahriman home)
:ivar unsafe: skip user check
Attributes:
force(bool): remove lock file on start if any
path(Path): path to lock file if any
reporter(Client): build status reporter instance
paths(RepositoryPaths): repository paths instance
unsafe(bool): skip user check
"""
def __init__(self, args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
self.path = Path(f"{args.lock}_{architecture}") if args.lock is not None else None
self.force = args.force
self.unsafe = args.unsafe
self.root = Path(configuration.get("repository", "root"))
self.paths = configuration.repository_paths
self.reporter = Client() if args.no_report else Client.load(configuration)
def __enter__(self) -> Lock:
"""
default workflow is the following:
check user UID
check if there is lock file
check web status watcher status
create lock file
report to web if enabled
1. Check user UID
2. Check if there is lock file
3. Check web status watcher status
4. Create lock file
5. Report to status page if enabled
"""
self.check_user()
self.check_version()
@ -78,10 +82,14 @@ class Lock:
exc_tb: TracebackType) -> Literal[False]:
"""
remove lock file when done
:param exc_type: exception type name if any
:param exc_val: exception raised if any
:param exc_tb: exception traceback if any
:return: always False (do not suppress any exception)
Args:
exc_type(Optional[Type[Exception]]): exception type name if any
exc_val(Optional[Exception]): exception raised if any
exc_tb(TracebackType): exception traceback if any
Returns:
Literal[False]: always False (do not suppress any exception)
"""
self.clear()
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
@ -103,9 +111,7 @@ class Lock:
"""
check if current user is actually owner of ahriman root
"""
if self.unsafe:
return
check_user(self.root)
check_user(self.paths, self.unsafe)
def clear(self) -> None:
"""
@ -118,6 +124,9 @@ class Lock:
def create(self) -> None:
"""
create lock file
Raises:
DuplicateRun: if lock exists and no force flag supplied
"""
if self.path is None:
return

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -26,13 +26,17 @@ from ahriman.core.configuration import Configuration
class Pacman:
"""
alpm wrapper
:ivar handle: pyalpm root `Handle`
Attributes:
handle(Handle): pyalpm root `Handle`
"""
def __init__(self, configuration: Configuration) -> None:
"""
default constructor
:param configuration: configuration instance
Args:
configuration(Configuration): configuration instance
"""
root = configuration.get("alpm", "root")
pacman_root = configuration.getpath("alpm", "database")
@ -43,7 +47,9 @@ class Pacman:
def all_packages(self) -> Set[str]:
"""
get list of packages known for alpm
:return: list of package names
Returns:
Set[str]: list of package names
"""
result: Set[str] = set()
for database in self.handle.get_syncdbs():

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -0,0 +1,136 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import requests
from typing import Any, Dict, List, Optional
from ahriman.core.alpm.remote.remote import Remote
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import exception_response_text
from ahriman.models.aur_package import AURPackage
class AUR(Remote):
"""
AUR RPC wrapper
Attributes:
DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url
DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version
rpc_url(str): AUR RPC url
rpc_version(str): AUR RPC version
"""
DEFAULT_RPC_URL = "https://aur.archlinux.org/rpc"
DEFAULT_RPC_VERSION = "5"
def __init__(self, rpc_url: Optional[str] = None, rpc_version: Optional[str] = None) -> None:
"""
default constructor
Args:
rpc_url(Optional[str], optional): AUR RPC url (Default value = None)
rpc_version(Optional[str], optional): AUR RPC version (Default value = None)
"""
Remote.__init__(self)
self.rpc_url = rpc_url or self.DEFAULT_RPC_URL
self.rpc_version = rpc_version or self.DEFAULT_RPC_VERSION
@staticmethod
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
"""
parse RPC response to package list
Args:
response(Dict[str, Any]): RPC response json
Returns:
List[AURPackage]: list of parsed packages
Raises:
InvalidPackageInfo: for error API response
"""
response_type = response["type"]
if response_type == "error":
error_details = response.get("error", "Unknown API error")
raise InvalidPackageInfo(error_details)
return [AURPackage.from_json(package) for package in response["results"]]
def make_request(self, request_type: str, *args: str, **kwargs: str) -> List[AURPackage]:
"""
perform request to AUR RPC
Args:
request_type(str): AUR request type, e.g. search, info
*args(str): list of arguments to be passed as args query parameter
**kwargs(str): list of additional named parameters like by
Returns:
List[AURPackage]: response parsed to package list
"""
query: Dict[str, Any] = {
"type": request_type,
"v": self.rpc_version
}
arg_query = "arg[]" if len(args) > 1 else "arg"
query[arg_query] = list(args)
for key, value in kwargs.items():
query[key] = value
try:
response = requests.get(self.rpc_url, params=query)
response.raise_for_status()
return self.parse_response(response.json())
except requests.HTTPError as e:
self.logger.exception(
"could not perform request by using type %s: %s",
request_type,
exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request by using type %s", request_type)
raise
def package_info(self, package_name: str) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
Returns:
AURPackage: package which match the package name
"""
packages = self.make_request("info", package_name)
return next(package for package in packages if package.name == package_name)
def package_search(self, *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
Returns:
List[AURPackage]: list of packages which match the criteria
"""
return self.make_request("search", *keywords, by="name-desc")

View File

@ -0,0 +1,114 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import requests
from typing import Any, Dict, List, Optional
from ahriman.core.alpm.remote.remote import Remote
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import exception_response_text
from ahriman.models.aur_package import AURPackage
class Official(Remote):
"""
official repository RPC wrapper
Attributes:
DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url
rpc_url(str): AUR RPC url
"""
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
def __init__(self, rpc_url: Optional[str] = None) -> None:
"""
default constructor
Args:
rpc_url(Optional[str], optional): AUR RPC url (Default value = None)
"""
Remote.__init__(self)
self.rpc_url = rpc_url or self.DEFAULT_RPC_URL
@staticmethod
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
"""
parse RPC response to package list
Args:
response(Dict[str, Any]): RPC response json
Returns:
List[AURPackage]: list of parsed packages
Raises:
InvalidPackageInfo: for error API response
"""
if not response["valid"]:
raise InvalidPackageInfo("API validation error")
return [AURPackage.from_repo(package) for package in response["results"]]
def make_request(self, *args: str, by: str) -> List[AURPackage]:
"""
perform request to official repositories RPC
Args:
*args(str): list of arguments to be passed as args query parameter
by(str): search by the field
Returns:
List[AURPackage]: response parsed to package list
"""
try:
response = requests.get(self.rpc_url, params={by: args})
response.raise_for_status()
return self.parse_response(response.json())
except requests.HTTPError as e:
self.logger.exception("could not perform request: %s", exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request")
raise
def package_info(self, package_name: str) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
Returns:
AURPackage: package which match the package name
"""
packages = self.make_request(package_name, by="name")
return next(package for package in packages if package.name == package_name)
def package_search(self, *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
Returns:
List[AURPackage]: list of packages which match the criteria
"""
return self.make_request(*keywords, by="q")

View File

@ -0,0 +1,120 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import logging
from typing import Dict, List, Type
from ahriman.models.aur_package import AURPackage
class Remote:
"""
base class for remote package search
Attributes:
logger(logging.Logger): class logger
"""
def __init__(self) -> None:
"""
default constructor
"""
self.logger = logging.getLogger("build_details")
@classmethod
def info(cls: Type[Remote], package_name: str) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
Returns:
AURPackage: package which match the package name
"""
return cls().package_info(package_name)
@classmethod
def multisearch(cls: Type[Remote], *keywords: str) -> List[AURPackage]:
"""
search in remote repository 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
Args:
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
Returns:
List[AURPackage]: list of packages each of them matches all search terms
"""
instance = cls()
packages: Dict[str, AURPackage] = {}
for term in filter(lambda word: len(word) > 3, keywords):
portion = instance.search(term)
packages = {
package.name: package # not mistake to group them by name
for package in portion
if package.name in packages or not packages
}
return list(packages.values())
@classmethod
def search(cls: Type[Remote], *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
Returns:
List[AURPackage]: list of packages which match the criteria
"""
return cls().package_search(*keywords)
def package_info(self, package_name: str) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
Returns:
AURPackage: package which match the package name
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_search(self, *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
Returns:
List[AURPackage]: list of packages which match the criteria
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -30,10 +30,13 @@ from ahriman.models.repository_paths import RepositoryPaths
class Repo:
"""
repo-add and repo-remove wrapper
:ivar logger: class logger
:ivar name: repository name
:ivar paths: repository paths instance
:ivar sign_args: additional args which have to be used to sign repository archive
Attributes:
logger(logging.Logger): class logger
name(str): repository name
paths(RepositoryPaths): repository paths instance
sign_args(List[str]): additional args which have to be used to sign repository archive
uid(int): uid of the repository owner user
"""
_check_output = check_output
@ -41,32 +44,39 @@ class Repo:
def __init__(self, name: str, paths: RepositoryPaths, sign_args: List[str]) -> None:
"""
default constructor
:param name: repository name
:param paths: repository paths instance
:param sign_args: additional args which have to be used to sign repository archive
Args:
name(str): repository name
paths(RepositoryPaths): repository paths instance
sign_args(List[str]): additional args which have to be used to sign repository archive
"""
self.logger = logging.getLogger("build_details")
self.name = name
self.paths = paths
self.uid, _ = paths.root_owner
self.sign_args = sign_args
@property
def repo_path(self) -> Path:
"""
:return: path to repository database
Returns:
Path: path to repository database
"""
return self.paths.repository / f"{self.name}.db.tar.gz"
def add(self, path: Path) -> None:
"""
add new package to repository
:param path: path to archive to add
Args:
path(Path): path to archive to add
"""
Repo._check_output(
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
exception=BuildFailed(path.name),
cwd=self.paths.repository,
logger=self.logger)
logger=self.logger,
user=self.uid)
def init(self) -> None:
"""
@ -76,13 +86,16 @@ class Repo:
"repo-add", *self.sign_args, str(self.repo_path),
exception=None,
cwd=self.paths.repository,
logger=self.logger)
logger=self.logger,
user=self.uid)
def remove(self, package: str, filename: Path) -> None:
"""
remove package from repository
:param package: package name to remove
:param filename: package filename to remove
Args:
package(str): package name to remove
filename(Path): package filename to remove
"""
# remove package and signature (if any) from filesystem
for full_path in self.paths.repository.glob(f"{filename}*"):
@ -93,4 +106,5 @@ class Repo:
"repo-remove", *self.sign_args, str(self.repo_path), package,
exception=BuildFailed(package),
cwd=self.paths.repository,
logger=self.logger)
logger=self.logger,
user=self.uid)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -21,28 +21,32 @@ from __future__ import annotations
import logging
from typing import Dict, Optional, Type
from typing import Optional, Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateUser
from ahriman.core.database.sqlite import SQLite
from ahriman.models.auth_settings import AuthSettings
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
class Auth:
"""
helper to deal with user authorization
:ivar enabled: indicates if authorization is enabled
:ivar max_age: session age in seconds. It will be used for both client side and server side checks
:ivar safe_build_status: allow read only access to the index page
Attributes:
enabled(bool): indicates if authorization is enabled
logger(logging.Logger): class logger
max_age(int): session age in seconds. It will be used for both client side and server side checks
safe_build_status(bool): allow read only access to the index page
"""
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
"""
default constructor
:param configuration: configuration instance
:param provider: authorization type definition
Args:
configuration(Configuration): configuration instance
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.Disabled)
"""
self.logger = logging.getLogger("http")
@ -58,51 +62,43 @@ class Auth:
In case of internal authentication it must provide an interface (modal form) to login with button sends POST
request. But for an external providers behaviour can be different: e.g. OAuth provider requires sending GET
request to external resource
:return: login control as html code to insert
Returns:
str: login control as html code to insert
"""
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>"""
@classmethod
def load(cls: Type[Auth], configuration: Configuration) -> Auth:
def load(cls: Type[Auth], configuration: Configuration, database: SQLite) -> Auth:
"""
load authorization module from settings
:param configuration: configuration instance
:return: authorization module according to current settings
Args:
configuration(Configuration): configuration instance
database(SQLite): database instance
Returns:
Auth: authorization module according to current settings
"""
provider = AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled"))
if provider == AuthSettings.Configuration:
from ahriman.core.auth.mapping import Mapping
return Mapping(configuration)
return Mapping(configuration, database)
if provider == AuthSettings.OAuth:
from ahriman.core.auth.oauth import OAuth
return OAuth(configuration)
return OAuth(configuration, database)
return cls(configuration)
@staticmethod
def get_users(configuration: Configuration) -> Dict[str, User]:
"""
load users from settings
:param configuration: configuration instance
:return: map of username to its descriptor
"""
users: Dict[str, User] = {}
for role in UserAccess:
section = configuration.section_name("auth", role.value)
if not configuration.has_section(section):
continue
for user, password in configuration[section].items():
normalized_user = user.lower()
if normalized_user in users:
raise DuplicateUser(normalized_user)
users[normalized_user] = User(normalized_user, password, role)
return users
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use
"""
validate user password
:param username: username
:param password: entered password
:return: True in case if password matches, False otherwise
Args:
username(Optional[str]): username
password(Optional[str]): entered password
Returns:
bool: True in case if password matches, False otherwise
"""
del username, password
return True
@ -110,8 +106,12 @@ class Auth:
async def known_username(self, username: Optional[str]) -> bool: # pylint: disable=no-self-use
"""
check if user is known
:param username: username
:return: True in case if user is known and can be authorized and False otherwise
Args:
username(Optional[str]): username
Returns:
bool: True in case if user is known and can be authorized and False otherwise
"""
del username
return True
@ -119,10 +119,14 @@ class Auth:
async def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool: # pylint: disable=no-self-use
"""
validate if user has access to requested resource
:param username: username
:param required: required access level
:param context: URI request path
:return: True in case if user is allowed to do this request and False otherwise
Args:
username(str): username
required(UserAccess): required access level
context(Optional[str]): URI request path
Returns:
bool: True in case if user is allowed to do this request and False otherwise
"""
del username, required, context
return True

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -29,8 +29,12 @@ except ImportError:
async def authorized_userid(*args: Any) -> Any:
"""
handle aiohttp security methods
:param args: argument list as provided by authorized_userid function
:return: None in case if no aiohttp_security module found and function call otherwise
Args:
*args(Any): argument list as provided by authorized_userid function
Returns:
Any: None in case if no aiohttp_security module found and function call otherwise
"""
if _has_aiohttp_security:
return await aiohttp_security.authorized_userid(*args) # pylint: disable=no-value-for-parameter
@ -40,8 +44,12 @@ async def authorized_userid(*args: Any) -> Any:
async def check_authorized(*args: Any) -> Any:
"""
handle aiohttp security methods
:param args: argument list as provided by check_authorized function
:return: None in case if no aiohttp_security module found and function call otherwise
Args:
*args(Any): argument list as provided by check_authorized function
Returns:
Any: None in case if no aiohttp_security module found and function call otherwise
"""
if _has_aiohttp_security:
return await aiohttp_security.check_authorized(*args) # pylint: disable=no-value-for-parameter
@ -51,8 +59,12 @@ async def check_authorized(*args: Any) -> Any:
async def forget(*args: Any) -> Any:
"""
handle aiohttp security methods
:param args: argument list as provided by forget function
:return: None in case if no aiohttp_security module found and function call otherwise
Args:
*args(Any): argument list as provided by forget function
Returns:
Any: None in case if no aiohttp_security module found and function call otherwise
"""
if _has_aiohttp_security:
return await aiohttp_security.forget(*args) # pylint: disable=no-value-for-parameter
@ -62,8 +74,12 @@ async def forget(*args: Any) -> Any:
async def remember(*args: Any) -> Any:
"""
handle disabled auth
:param args: argument list as provided by remember function
:return: None in case if no aiohttp_security module found and function call otherwise
Args:
*args(Any): argument list as provided by remember function
Returns:
Any: None in case if no aiohttp_security module found and function call otherwise
"""
if _has_aiohttp_security:
return await aiohttp_security.remember(*args) # pylint: disable=no-value-for-parameter

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -20,7 +20,9 @@
from typing import Optional
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.models.auth_settings import AuthSettings
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
@ -29,26 +31,36 @@ from ahriman.models.user_access import UserAccess
class Mapping(Auth):
"""
user authorization based on mapping from configuration file
:ivar salt: random generated string to salt passwords
:ivar _users: map of username to its descriptor
Attributes:
salt(str): random generated string to salt passwords
database(SQLite): database instance
"""
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Configuration) -> None:
def __init__(self, configuration: Configuration, database: SQLite,
provider: AuthSettings = AuthSettings.Configuration) -> None:
"""
default constructor
:param configuration: configuration instance
:param provider: authorization type definition
Args:
configuration(Configuration): configuration instance
database(SQLite): database instance
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.Configuration)
"""
Auth.__init__(self, configuration, provider)
self.database = database
self.salt = configuration.get("auth", "salt")
self._users = self.get_users(configuration)
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool:
"""
validate user password
:param username: username
:param password: entered password
:return: True in case if password matches, False otherwise
Args:
username(Optional[str]): username
password(Optional[str]): entered password
Returns:
bool: True in case if password matches, False otherwise
"""
if username is None or password is None:
return False # invalid data supplied
@ -58,27 +70,38 @@ class Mapping(Auth):
def get_user(self, username: str) -> Optional[User]:
"""
retrieve user from in-memory mapping
:param username: username
:return: user descriptor if username is known and None otherwise
Args:
username(str): username
Returns:
Optional[User]: user descriptor if username is known and None otherwise
"""
normalized_user = username.lower()
return self._users.get(normalized_user)
return self.database.user_get(username)
async def known_username(self, username: Optional[str]) -> bool:
"""
check if user is known
:param username: username
:return: True in case if user is known and can be authorized and False otherwise
Args:
username(Optional[str]): username
Returns:
bool: True in case if user is known and can be authorized and False otherwise
"""
return username is not None and self.get_user(username) is not None
async def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool:
"""
validate if user has access to requested resource
:param username: username
:param required: required access level
:param context: URI request path
:return: True in case if user is allowed to do this request and False otherwise
Args:
username(str): username
required(UserAccess): required access level
context(Optional[str]): URI request path
Returns:
bool: True in case if user is allowed to do this request and False otherwise
"""
user = self.get_user(username)
return user is not None and user.verify_access(required)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -23,6 +23,7 @@ from typing import Optional, Type
from ahriman.core.auth.mapping import Mapping
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import InvalidOption
from ahriman.models.auth_settings import AuthSettings
@ -31,20 +32,26 @@ class OAuth(Mapping):
"""
OAuth user authorization.
It is required to create application first and put application credentials.
:ivar client_id: application client id
:ivar client_secret: application client secret key
:ivar provider: provider class, should be one of aiohttp-client provided classes
:ivar redirect_uri: redirect URI registered in provider
:ivar scopes: list of scopes required by the application
Attributes:
client_id(str): application client id
client_secret(str): application client secret key
provider(aioauth_client.OAuth2Client): provider class, should be one of aiohttp-client provided classes
redirect_uri(str): redirect URI registered in provider
scopes(str): list of scopes required by the application
"""
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.OAuth) -> None:
def __init__(self, configuration: Configuration, database: SQLite,
provider: AuthSettings = AuthSettings.OAuth) -> None:
"""
default constructor
:param configuration: configuration instance
:param provider: authorization type definition
Args:
configuration(Configuration): configuration instance
database(SQLite): database instance
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.OAuth)
"""
Mapping.__init__(self, configuration, provider)
Mapping.__init__(self, configuration, database, provider)
self.client_id = configuration.get("auth", "client_id")
self.client_secret = configuration.get("auth", "client_secret")
# in order to use OAuth feature the service must be publicity available
@ -57,7 +64,8 @@ class OAuth(Mapping):
@property
def auth_control(self) -> str:
"""
:return: login control as html code to insert
Returns:
str: login control as html code to insert
"""
return """<a class="nav-link" href="/user-api/v1/login" title="login via OAuth2">login</a>"""
@ -65,8 +73,15 @@ class OAuth(Mapping):
def get_provider(name: str) -> Type[aioauth_client.OAuth2Client]:
"""
load OAuth2 provider by name
:param name: name of the provider. Must be valid class defined in aioauth-client library
:return: loaded provider type
Args:
name(str): name of the provider. Must be valid class defined in aioauth-client library
Returns:
Type[aioauth_client.OAuth2Client]: loaded provider type
Raises:
InvalidOption: in case if invalid OAuth provider name supplied
"""
provider: Type[aioauth_client.OAuth2Client] = getattr(aioauth_client, name)
try:
@ -80,14 +95,18 @@ class OAuth(Mapping):
def get_client(self) -> aioauth_client.OAuth2Client:
"""
load client from parameters
:return: generated client according to current settings
Returns:
aioauth_client.OAuth2Client: generated client according to current settings
"""
return self.provider(client_id=self.client_id, client_secret=self.client_secret)
def get_oauth_url(self) -> str:
"""
get authorization URI for the specified settings
:return: authorization URI as a string
Returns:
str: authorization URI as a string
"""
client = self.get_client()
uri: str = client.get_authorize_url(scope=self.scopes, redirect_uri=self.redirect_uri)
@ -96,8 +115,12 @@ class OAuth(Mapping):
async def get_oauth_username(self, code: str) -> Optional[str]:
"""
extract OAuth username from remote
:param code: authorization code provided by external service
:return: username as is in OAuth provider
Args:
code(str): authorization code provided by external service
Returns:
Optional[str]: username as is in OAuth provider
"""
try:
client = self.get_client()

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -28,7 +28,9 @@ from ahriman.core.util import check_output
class Sources:
"""
helper to download package sources (PKGBUILD etc)
:cvar logger: class logger
Attributes:
logger(logging.Logger): (class attribute) class logger
"""
logger = logging.getLogger("build_details")
@ -40,13 +42,17 @@ class Sources:
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
Args:
sources_dir(Path): local path to git repository
*pattern(str): glob patterns
"""
# glob directory to find files which match the specified patterns
found_files: List[Path] = []
for glob in pattern:
found_files.extend(sources_dir.glob(glob))
if not found_files:
return # no additional files found
Sources.logger.info("found matching files %s", found_files)
# add them to index
Sources._check_output("git", "add", "--intent-to-add",
@ -54,21 +60,26 @@ class Sources:
exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod
def diff(sources_dir: Path, patch_path: Path) -> None:
def diff(sources_dir: Path) -> str:
"""
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
Args:
sources_dir(Path): local path to git repository
Returns:
str: patch as plain string
"""
patch = Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
patch_path.write_text(patch)
return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod
def fetch(sources_dir: Path, remote: Optional[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)
Args:
sources_dir(Path): local path to fetch
remote(Optional[str]): remote target (from where to fetch)
"""
# local directory exists and there is .git directory
is_initialized_git = (sources_dir / ".git").is_dir()
@ -85,7 +96,8 @@ class Sources:
Sources.logger.warning("%s is not initialized, but no remote provided", sources_dir)
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)
Sources._check_output("git", "clone", remote, str(sources_dir),
exception=None, cwd=sources_dir, 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)
@ -96,8 +108,12 @@ class Sources:
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
Args:
sources_dir(Path): local path to git repository
Returns:
bool: True in case if there is any remote and false otherwise
"""
remotes = Sources._check_output("git", "remote", exception=None, cwd=sources_dir, logger=Sources.logger)
return bool(remotes)
@ -106,47 +122,55 @@ class Sources:
def init(sources_dir: Path) -> None:
"""
create empty git repository at the specified path
:param sources_dir: local path to sources
Args:
sources_dir(Path): 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:
def load(sources_dir: Path, remote: str, patch: Optional[str]) -> 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
Args:
sources_dir(Path): local path to fetch
remote(str): remote target (from where to fetch)
patch(Optional[str]): optional patch to be applied
"""
Sources.fetch(sources_dir, remote)
Sources.patch_apply(sources_dir, patch_dir)
if patch is None:
Sources.logger.info("no patches found")
return
Sources.patch_apply(sources_dir, patch)
@staticmethod
def patch_apply(sources_dir: Path, patch_dir: Path) -> None:
def patch_apply(sources_dir: Path, patch: str) -> None:
"""
apply patches if any
:param sources_dir: local path to directory with git sources
:param patch_dir: path to directory with package patches
Args:
sources_dir(Path): local path to directory with git sources
patch(str): patch to be applied
"""
# 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)
# create patch
Sources.logger.info("apply patch from database")
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
exception=None, cwd=sources_dir, input_data=patch, logger=Sources.logger)
@staticmethod
def patch_create(sources_dir: Path, patch_path: Path, *pattern: str) -> None:
def patch_create(sources_dir: Path, *pattern: str) -> str:
"""
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
Args:
sources_dir(Path): local path to git repository
*pattern(str): glob patterns
Returns:
str: patch as plain text
"""
Sources.add(sources_dir, *pattern)
Sources.diff(sources_dir, patch_path)
diff = Sources.diff(sources_dir)
return f"{diff}\n" # otherwise, patch will be broken

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -21,10 +21,11 @@ import logging
import shutil
from pathlib import Path
from typing import List, Optional
from typing import List
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import BuildFailed
from ahriman.core.util import check_output
from ahriman.models.package import Package
@ -34,10 +35,13 @@ from ahriman.models.repository_paths import RepositoryPaths
class Task:
"""
base package build task
:ivar build_logger: logger for build process
:ivar logger: class logger
:ivar package: package definitions
:ivar paths: repository paths instance
Attributes:
build_logger(logging.Logger): logger for build process
logger(logging.Logger): class logger
package(Package): package definitions
paths(RepositoryPaths): repository paths instance
uid(int): uid of the repository owner user
"""
_check_output = check_output
@ -45,24 +49,32 @@ class Task:
def __init__(self, package: Package, configuration: Configuration, paths: RepositoryPaths) -> None:
"""
default constructor
:param package: package definitions
:param configuration: configuration instance
:param paths: repository paths instance
Args:
package(Package): package definitions
configuration(Configuration): configuration instance
paths(RepositoryPaths): repository paths instance
"""
self.logger = logging.getLogger("root")
self.build_logger = logging.getLogger("build_details")
self.package = package
self.paths = paths
self.uid, _ = paths.root_owner
self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[])
self.build_command = configuration.get("build", "build_command")
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
def build(self) -> List[Path]:
def build(self, sources_path: Path) -> List[Path]:
"""
run package build
:return: paths of produced packages
Args:
sources_path(Path): path to where sources are
Returns:
List[Path]: paths of produced packages
"""
command = [self.build_command, "-r", str(self.paths.chroot)]
command.extend(self.archbuild_flags)
@ -73,23 +85,26 @@ class Task:
Task._check_output(
*command,
exception=BuildFailed(self.package.base),
cwd=self.paths.sources_for(self.package.base),
logger=self.build_logger)
cwd=sources_path,
logger=self.build_logger,
user=self.uid)
# well it is not actually correct, but we can deal with it
packages = Task._check_output("makepkg", "--packagelist",
exception=BuildFailed(self.package.base),
cwd=self.paths.sources_for(self.package.base),
cwd=sources_path,
logger=self.build_logger).splitlines()
return [Path(package) for package in packages]
def init(self, path: Optional[Path] = None) -> None:
def init(self, path: Path, database: SQLite) -> None:
"""
fetch package from git
:param path: optional local path to fetch. If not set default path will be used
Args:
path(Path): local path to fetch
database(SQLite): database instance
"""
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.paths.cache_for(self.package.base), git_path)
Sources.load(git_path, self.package.git_url, self.paths.patches_for(self.package.base))
shutil.copytree(self.paths.cache_for(self.package.base), path, dirs_exist_ok=True)
Sources.load(path, self.package.git_url, database.patches_get(self.package.base))

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -21,28 +21,35 @@ from __future__ import annotations
import configparser
import logging
import sys
from logging.config import fileConfig
from pathlib import Path
from typing import Any, Dict, Generator, List, Optional, Tuple, Type
from ahriman.core.exceptions import InitializeException
from ahriman.models.repository_paths import RepositoryPaths
class Configuration(configparser.RawConfigParser):
"""
extension for built-in configuration parser
:ivar architecture: repository architecture
:ivar path: path to root configuration file
:cvar ARCHITECTURE_SPECIFIC_SECTIONS: known sections which can be architecture specific (required by dump)
:cvar DEFAULT_LOG_FORMAT: default log format (in case of fallback)
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
Attributes:
ARCHITECTURE_SPECIFIC_SECTIONS(List[str]): (class attribute) known sections which can be architecture specific.
Required by dump and merging functions
DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback)
DEFAULT_LOG_LEVEL(int): (class attribute) default log level (in case of fallback)
SYSTEM_CONFIGURATION_PATH(Path): (class attribute) default system configuration path distributed by package
architecture(Optional[str]): repository architecture
path(Optional[Path]): path to root configuration file
"""
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
def __init__(self) -> None:
"""
@ -58,25 +65,40 @@ class Configuration(configparser.RawConfigParser):
@property
def include(self) -> Path:
"""
:return: path to directory with configuration includes
Returns:
Path: path to directory with configuration includes
"""
return self.getpath("settings", "include")
@property
def logging_path(self) -> Path:
"""
:return: path to logging configuration
Returns:
Path: path to logging configuration
"""
return self.getpath("settings", "logging")
@property
def repository_paths(self) -> RepositoryPaths:
"""
Returns:
RepositoryPaths: repository paths instance
"""
_, architecture = self.check_loaded()
return RepositoryPaths(self.getpath("repository", "root"), architecture)
@classmethod
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 quiet: force disable any log messages
:return: configuration instance
Args:
path(Path): path to root configuration file
architecture(str): repository architecture
quiet(bool): force disable any log messages
Returns:
Configuration: configuration instance
"""
config = cls()
config.load(path)
@ -88,8 +110,15 @@ class Configuration(configparser.RawConfigParser):
def __convert_list(value: str) -> List[str]:
"""
convert string value to list of strings
:param value: string configuration value
:return: list of string from the parsed string
Args:
value(str): string configuration value
Returns:
List[str]: list of string from the parsed string
Raises:
ValueError: in case if option value contains unclosed quotes
"""
def generator() -> Generator[str, None, None]:
quote_mark = None
@ -99,7 +128,7 @@ class Configuration(configparser.RawConfigParser):
quote_mark = char
elif char == quote_mark: # quoted part ended, reset quotation
quote_mark = None
elif char == " " and quote_mark is None: # found space outside of the quotation, yield the word
elif char == " " and quote_mark is None: # found space outside the quotation, yield the word
yield word
word = ""
else: # append character to the buffer
@ -114,27 +143,51 @@ class Configuration(configparser.RawConfigParser):
def section_name(section: str, suffix: str) -> str:
"""
generate section name for sections which depends on context
:param section: section name
:param suffix: session suffix, e.g. repository architecture
:return: correct section name for repository specific section
Args:
section(str): section name
suffix(str): session suffix, e.g. repository architecture
Returns:
str: correct section name for repository specific section
"""
return f"{section}:{suffix}"
def __convert_path(self, value: str) -> Path:
"""
convert string value to path object
:param value: string configuration value
:return: path object which represents the configuration value
Args:
value(str): string configuration value
Returns:
Path: path object which represents the configuration value
"""
path = Path(value)
if self.path is None or path.is_absolute():
return path
return self.path.parent / path
def check_loaded(self) -> Tuple[Path, str]:
"""
check if service was actually loaded
Returns:
Tuple[Path, str]: configuration root path and architecture if loaded
Raises:
InitializeException: in case if architecture and/or path are not set
"""
if self.path is None or self.architecture is None:
raise InitializeException("Configuration path and/or architecture are not set")
return self.path, self.architecture
def dump(self) -> Dict[str, Dict[str, str]]:
"""
dump configuration to dictionary
:return: configuration dump for specific architecture
Returns:
Dict[str, Dict[str, str]]: configuration dump for specific architecture
"""
return {
section: dict(self[section])
@ -151,9 +204,16 @@ class Configuration(configparser.RawConfigParser):
"""
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
Args:
section(str): section name
architecture(str): repository architecture
Returns:
Tuple[str, str]: section name and found type name
Raises:
configparser.NoSectionError: in case if no section found
"""
group_type = self.get(section, "type", fallback=None) # new-style logic
if group_type is not None:
@ -170,8 +230,12 @@ class Configuration(configparser.RawConfigParser):
def load(self, path: Path) -> None:
"""
fully load configuration
:param path: path to root configuration file
Args:
path(Path): path to root configuration file
"""
if not path.is_file(): # fallback to the system file
path = self.SYSTEM_CONFIGURATION_PATH
self.path = path
self.read(self.path)
self.load_includes()
@ -191,7 +255,9 @@ class Configuration(configparser.RawConfigParser):
def load_logging(self, quiet: bool) -> None:
"""
setup logging settings from configuration
:param quiet: force disable any log messages
Args:
quiet(bool): force disable any log messages
"""
try:
path = self.logging_path
@ -206,7 +272,9 @@ class Configuration(configparser.RawConfigParser):
def merge_sections(self, architecture: str) -> None:
"""
merge architecture specific sections into main configuration
:param architecture: repository architecture
Args:
architecture(str): repository architecture
"""
self.architecture = architecture
for section in self.ARCHITECTURE_SPECIFIC_SECTIONS:
@ -228,19 +296,20 @@ class Configuration(configparser.RawConfigParser):
"""
reload configuration if possible or raise exception otherwise
"""
if self.path is None or self.architecture is None:
raise InitializeException("Configuration path and/or architecture are not set")
path, architecture = self.check_loaded()
for section in self.sections(): # clear current content
self.remove_section(section)
self.load(self.path)
self.merge_sections(self.architecture)
self.load(path)
self.merge_sections(architecture)
def set_option(self, section: str, option: str, value: Optional[str]) -> None:
"""
set option. Unlike default `configparser.RawConfigParser.set` it also creates section if it does not exist
:param section: section name
:param option: option name
:param value: option value as string in parsable format
Args:
section(str): section name
option(str): option name
value(Optional[str]): option value as string in parsable format
"""
if not self.has_section(section):
self.add_section(section)

View File

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

View File

@ -0,0 +1,45 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.core.database.data.package_statuses import migrate_package_statuses
from ahriman.core.database.data.patches import migrate_patches
from ahriman.core.database.data.users import migrate_users_data
from ahriman.models.migration_result import MigrationResult
from ahriman.models.repository_paths import RepositoryPaths
def migrate_data(result: MigrationResult, connection: Connection,
configuration: Configuration, paths: RepositoryPaths) -> None:
"""
perform data migration
Args:
result(MigrationResult): result of the schema migration
connection(Connection): database connection
configuration(Configuration): configuration instance
paths(RepositoryPaths): repository paths instance
"""
# initial data migration
if result.old_version <= 0:
migrate_package_statuses(connection, paths)
migrate_patches(connection, paths)
migrate_users_data(connection, configuration)

View File

@ -0,0 +1,82 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import json
from sqlite3 import Connection
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for package statuses
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
def insert_base(metadata: Package, last_status: BuildStatus) -> None:
connection.execute(
"""
insert into package_bases
(package_base, version, aur_url)
values
(:package_base, :version, :aur_url)
""",
dict(package_base=metadata.base, version=metadata.version, aur_url=metadata.aur_url))
connection.execute(
"""
insert into package_statuses
(package_base, status, last_updated)
values
(:package_base, :status, :last_updated)""",
dict(package_base=metadata.base, status=last_status.status.value, last_updated=last_status.timestamp))
def insert_packages(metadata: Package) -> None:
package_list = []
for name, description in metadata.packages.items():
package_list.append(dict(package=name, package_base=metadata.base, **description.view()))
connection.executemany(
"""
insert into packages
(package, package_base, architecture, archive_size, build_date, depends, description,
filename, "groups", installed_size, licenses, provides, url)
values
(:package, :package_base, :architecture, :archive_size, :build_date, :depends, :description,
:filename, :groups, :installed_size, :licenses, :provides, :url)
""",
package_list)
cache_path = paths.root / "status_cache.json"
if not cache_path.is_file():
return # no file found
with cache_path.open() as cache:
dump = json.load(cache)
for item in dump.get("packages", []):
package = Package.from_json(item["package"])
status = BuildStatus.from_json(item["status"])
insert_base(package, status)
insert_packages(package)
connection.commit()
cache_path.unlink()

View File

@ -0,0 +1,46 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.models.repository_paths import RepositoryPaths
def migrate_patches(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for patches
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
root = paths.root / "patches"
if not root.is_dir():
return # no directory found
for package in root.iterdir():
patch_path = package / "00-main.patch"
if not patch_path.is_file():
continue # not exist
content = patch_path.read_text(encoding="utf8")
connection.execute(
"""insert into patches (package_base, patch) values (:package_base, :patch)""",
{"package_base": package.name, "patch": content})
connection.commit()

View File

@ -0,0 +1,42 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
def migrate_users_data(connection: Connection, configuration: Configuration) -> None:
"""
perform migration for users
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
for section in configuration.sections():
for option, value in configuration[section].items():
if not section.startswith("auth:"):
continue
access = section[5:]
connection.execute(
"""insert into users (username, access, password) values (:username, :access, :password)""",
{"username": option.lower(), "access": access, "password": value})
connection.commit()

View File

@ -0,0 +1,139 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import logging
from importlib import import_module
from pathlib import Path
from pkgutil import iter_modules
from sqlite3 import Connection
from typing import List, Type
from ahriman.models.migration import Migration
from ahriman.models.migration_result import MigrationResult
class Migrations:
"""
simple migration wrapper for the sqlite
idea comes from https://www.ash.dev/blog/simple-migration-system-in-sqlite/
Attributes:
connection(Connection): database connection
logger(logging.Logger): class logger
"""
def __init__(self, connection: Connection) -> None:
"""
default constructor
Args:
connection(Connection): database connection
"""
self.connection = connection
self.logger = logging.getLogger("database")
@classmethod
def migrate(cls: Type[Migrations], connection: Connection) -> MigrationResult:
"""
perform migrations implicitly
Args:
connection(Connection): database connection
Returns:
MigrationResult: current schema version
"""
return cls(connection).run()
def migrations(self) -> List[Migration]:
"""
extract all migrations from the current package
idea comes from https://julienharbulot.com/python-dynamical-import.html
Returns:
List[Migration]: list of found migrations
"""
migrations: List[Migration] = []
package_dir = Path(__file__).resolve().parent
modules = [module_name for (_, module_name, _) in iter_modules([str(package_dir)])]
for index, module_name in enumerate(sorted(modules)):
module = import_module(f"{__name__}.{module_name}")
steps: List[str] = getattr(module, "steps", [])
self.logger.debug("found migration %s at index %s with steps count %s", module_name, index, len(steps))
migrations.append(Migration(index, module_name, steps))
return migrations
def run(self) -> MigrationResult:
"""
perform migrations
Return:
MigrationResult: current schema version
"""
migrations = self.migrations()
current_version = self.user_version()
expected_version = len(migrations)
result = MigrationResult(current_version, expected_version)
if not result.is_outdated:
self.logger.info("no migrations required")
return result
previous_isolation = self.connection.isolation_level
try:
self.connection.isolation_level = None
cursor = self.connection.cursor()
try:
cursor.execute("begin exclusive")
for migration in migrations[current_version:]:
self.logger.info("applying migration %s at index %s", migration.name, migration.index)
for statement in migration.steps:
cursor.execute(statement)
self.logger.info("migration %s at index %s has been applied", migration.name, migration.index)
cursor.execute(f"pragma user_version = {expected_version}") # no support for ? placeholders
except Exception:
self.logger.exception("migration failed with exception")
cursor.execute("rollback")
raise
else:
cursor.execute("commit")
finally:
cursor.close()
finally:
self.connection.isolation_level = previous_isolation
self.logger.info("migrations have been performed from version %s to %s", result.old_version, result.new_version)
return result
def user_version(self) -> int:
"""
get schema version from sqlite database
Returns:
int: current schema version
"""
cursor = self.connection.execute("pragma user_version")
current_version: int = cursor.fetchone()["user_version"]
return current_version

View File

@ -0,0 +1,73 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
steps = [
"""
create table build_queue (
package_base text not null unique,
properties json not null
)
""",
"""
create table package_bases (
package_base text not null unique,
version text not null,
aur_url text not null
)
""",
"""
create table package_statuses (
package_base text not null unique,
status text not null,
last_updated integer
)
""",
"""
create table packages (
package text not null,
package_base text not null,
architecture text,
archive_size integer,
build_date integer,
depends json,
description text,
filename text,
"groups" json,
installed_size integer,
licenses json,
provides json,
url text,
unique (package, architecture)
)
""",
"""
create table patches (
package_base text not null unique,
patch blob not null
)
""",
"""
create table users (
username text not null unique,
access text not null,
password text
)
""",
]

View File

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

View File

@ -0,0 +1,103 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from typing import List, Optional
from ahriman.core.database.operations.operations import Operations
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
class AuthOperations(Operations):
"""
authorization operations
"""
def user_get(self, username: str) -> Optional[User]:
"""
get user by username
Args:
username(str): username
Returns:
Optional[User]: user if it was found
"""
return next(iter(self.user_list(username, None)), None)
def user_list(self, username: Optional[str], access: Optional[UserAccess]) -> List[User]:
"""
get users by filter
Args:
username(Optional[str]): optional filter by username
access(Optional[UserAccess]): optional filter by role
Returns:
List[User]: list of users who match criteria
"""
username_filter = username.lower() if username is not None else username
access_filter = access.value if access is not None else access
def run(connection: Connection) -> List[User]:
return [
User(cursor["username"], cursor["password"], UserAccess(cursor["access"]))
for cursor in connection.execute(
"""
select * from users
where (:username is null or username = :username) and (:access is null or access = :access)
""",
{"username": username_filter, "access": access_filter})
]
return self.with_connection(run)
def user_remove(self, username: str) -> None:
"""
remove user from storage
Args:
username(str): username
"""
def run(connection: Connection) -> None:
connection.execute("""delete from users where username = :username""", {"username": username.lower()})
return self.with_connection(run, commit=True)
def user_update(self, user: User) -> None:
"""
update user by username
Args:
user(User): user descriptor
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into users
(username, access, password)
values
(:username, :access, :password)
on conflict (username) do update set
access = :access, password = :password
""",
{"username": user.username.lower(), "access": user.access.value, "password": user.password})
self.with_connection(run, commit=True)

View File

@ -0,0 +1,83 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from typing import List, Optional
from ahriman.core.database.operations.operations import Operations
from ahriman.models.package import Package
class BuildOperations(Operations):
"""
operations for main functions
"""
def build_queue_clear(self, package_base: Optional[str]) -> None:
"""
remove packages from build queue
Args:
package_base(Optional[str]): optional filter by package base
"""
def run(connection: Connection) -> None:
connection.execute(
"""
delete from build_queue
where :package_base is null or package_base = :package_base
""",
{"package_base": package_base})
return self.with_connection(run, commit=True)
def build_queue_get(self) -> List[Package]:
"""
retrieve packages from build queue
Return:
List[Package]: list of packages to be built
"""
def run(connection: Connection) -> List[Package]:
return [
Package.from_json(row["properties"])
for row in connection.execute("""select * from build_queue""")
]
return self.with_connection(run)
def build_queue_insert(self, package: Package) -> None:
"""
insert packages to build queue
Args:
package(Package): package to be inserted
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into build_queue
(package_base, properties)
values
(:package_base, :properties)
on conflict (package_base) do update set
properties = :properties
""",
{"package_base": package.base, "properties": package.view()})
return self.with_connection(run, commit=True)

View File

@ -0,0 +1,83 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import logging
import sqlite3
from pathlib import Path
from sqlite3 import Connection, Cursor
from typing import Any, Dict, Tuple, TypeVar, Callable
T = TypeVar("T")
class Operations:
"""
base operation class
Attributes:
logger(logging.Logger): class logger
path(Path): path to the database file
"""
def __init__(self, path: Path) -> None:
"""
default constructor
Args:
path(Path): path to the database file
"""
self.path = path
self.logger = logging.getLogger("database")
@staticmethod
def factory(cursor: Cursor, row: Tuple[Any, ...]) -> Dict[str, Any]:
"""
dictionary factory based on official documentation
Args:
cursor(Cursor): cursor descriptor
row(Tuple[Any, ...]): fetched row
Returns:
Dict[str, Any]: row converted to dictionary
"""
result = {}
for index, column in enumerate(cursor.description):
result[column[0]] = row[index]
return result
def with_connection(self, query: Callable[[Connection], T], commit: bool = False) -> T:
"""
perform operation in connection
Args:
query(Callable[[Connection], T]): function to be called with connection
commit(bool, optional): if True commit() will be called on success (Default value = False)
Returns:
T: result of the `query` call
"""
with sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES) as connection:
connection.row_factory = self.factory
result = query(connection)
if commit:
connection.commit()
return result

View File

@ -0,0 +1,227 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from typing import Dict, Generator, Iterable, List, Tuple
from ahriman.core.database.operations.operations import Operations
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
class PackageOperations(Operations):
"""
package operations
"""
@staticmethod
def _package_remove_package_base(connection: Connection, package_base: str) -> None:
"""
remove package base information
Args:
connection(Connection): database connection
package_base(str): package base name
"""
connection.execute("""delete from package_statuses where package_base = :package_base""",
{"package_base": package_base})
connection.execute("""delete from package_bases where package_base = :package_base""",
{"package_base": package_base})
@staticmethod
def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str]) -> None:
"""
remove packages belong to the package base
Args:
connection(Connection): database connection
package_base(str): package base name
current_packages(Iterable[str]): current packages list which has to be left in database
"""
packages = [
package
for package in connection.execute(
"""select package from packages where package_base = :package_base""", {"package_base": package_base})
if package["package"] not in current_packages
]
connection.executemany("""delete from packages where package = :package""", packages)
@staticmethod
def _package_update_insert_base(connection: Connection, package: Package) -> None:
"""
insert base package into table
Args:
connection(Connection): database connection
package(Package): package properties
"""
connection.execute(
"""
insert into package_bases
(package_base, version, aur_url)
values
(:package_base, :version, :aur_url)
on conflict (package_base) do update set
version = :version, aur_url = :aur_url
""",
dict(package_base=package.base, version=package.version, aur_url=package.aur_url))
@staticmethod
def _package_update_insert_packages(connection: Connection, package: Package) -> None:
"""
insert packages into table
Args:
connection(Connection): database connection
package(Package): package properties
"""
package_list = []
for name, description in package.packages.items():
package_list.append(dict(package=name, package_base=package.base, **description.view()))
connection.executemany(
"""
insert into packages
(package, package_base, architecture, archive_size,
build_date, depends, description, filename,
"groups", installed_size, licenses, provides, url)
values
(:package, :package_base, :architecture, :archive_size,
:build_date, :depends, :description, :filename,
:groups, :installed_size, :licenses, :provides, :url)
on conflict (package, architecture) do update set
package_base = :package_base, archive_size = :archive_size,
build_date = :build_date, depends = :depends, description = :description, filename = :filename,
"groups" = :groups, installed_size = :installed_size, licenses = :licenses, provides = :provides, url = :url
""",
package_list)
@staticmethod
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus) -> None:
"""
insert base package status into table
Args:
connection(Connection): database connection
package_base(str): package base name
status(BuildStatus): new build status
"""
connection.execute(
"""
insert into package_statuses (package_base, status, last_updated)
values
(:package_base, :status, :last_updated)
on conflict (package_base) do update set
status = :status, last_updated = :last_updated
""",
dict(package_base=package_base, status=status.status.value, last_updated=status.timestamp))
@staticmethod
def _packages_get_select_package_bases(connection: Connection) -> Dict[str, Package]:
"""
select package bases from the table
Args:
connection(Connection): database connection
Returns:
Dict[str, Package]: map of the package base to its descriptor (without packages themselves)
"""
return {
row["package_base"]: Package(row["package_base"], row["version"], row["aur_url"], {})
for row in connection.execute("""select * from package_bases""")
}
@staticmethod
def _packages_get_select_packages(connection: Connection, packages: Dict[str, Package]) -> Dict[str, Package]:
"""
select packages from the table
Args:
connection(Connection): database connection
packages(Dict[str, Package]): packages descriptor map
Returns:
Dict[str, Package]: map of the package base to its descriptor including individual packages
"""
for row in connection.execute("""select * from packages"""):
if row["package_base"] not in packages:
continue # normally must never happen though
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
return packages
@staticmethod
def _packages_get_select_statuses(connection: Connection) -> Dict[str, BuildStatus]:
"""
select package build statuses from the table
Args:
connection(Connection): database connection
Returns:
Dict[str, BuildStatus]: map of the package base to its status
"""
return {
row["package_base"]: BuildStatus.from_json({"status": row["status"], "timestamp": row["last_updated"]})
for row in connection.execute("""select * from package_statuses""")
}
def package_remove(self, package_base: str) -> None:
"""
remove package from database
Args:
package_base(str): package base name
"""
def run(connection: Connection) -> None:
self._package_remove_packages(connection, package_base, [])
self._package_remove_package_base(connection, package_base)
return self.with_connection(run, commit=True)
def package_update(self, package: Package, status: BuildStatus) -> None:
"""
update package status
Args:
package(Package): package properties
status(BuildStatus): new build status
"""
def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package)
self._package_update_insert_status(connection, package.base, status)
self._package_update_insert_packages(connection, package)
self._package_remove_packages(connection, package.base, package.packages.keys())
return self.with_connection(run, commit=True)
def packages_get(self) -> List[Tuple[Package, BuildStatus]]:
"""
get package list and their build statuses from database
Return:
List[Tuple[Package, BuildStatus]]: list of package properties and their statuses
"""
def run(connection: Connection) -> Generator[Tuple[Package, BuildStatus], None, None]:
packages = self._packages_get_select_package_bases(connection)
statuses = self._packages_get_select_statuses(connection)
for package_base, package in self._packages_get_select_packages(connection, packages).items():
yield package, statuses.get(package_base, BuildStatus())
return self.with_connection(lambda connection: list(run(connection)))

View File

@ -0,0 +1,97 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from typing import Dict, Optional
from ahriman.core.database.operations.operations import Operations
class PatchOperations(Operations):
"""
operations for patches
"""
def patches_get(self, package_base: str) -> Optional[str]:
"""
retrieve patches for the package
Args:
package_base(str): package base to search for patches
Returns:
Optional[str]: plain text patch for the package
"""
return self.patches_list(package_base).get(package_base)
def patches_insert(self, package_base: str, patch: str) -> None:
"""
insert or update patch in database
Args:
package_base(str): package base to insert
patch(str): patch content
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into patches
(package_base, patch)
values
(:package_base, :patch)
on conflict (package_base) do update set
patch = :patch
""",
{"package_base": package_base, "patch": patch})
return self.with_connection(run, commit=True)
def patches_list(self, package_base: Optional[str]) -> Dict[str, str]:
"""
extract all patches
Args:
package_base(Optional[str]): optional filter by package base
Returns:
Dict[str, str]: map of package base to patch content
"""
def run(connection: Connection) -> Dict[str, str]:
return {
cursor["package_base"]: cursor["patch"]
for cursor in connection.execute(
"""select * from patches where :package_base is null or package_base = :package_base""",
{"package_base": package_base})
}
return self.with_connection(run)
def patches_remove(self, package_base: str) -> None:
"""
remove patch set
Args:
package_base(str): package base to clear patches
"""
def run(connection: Connection) -> None:
connection.execute(
"""delete from patches where package_base = :package_base""",
{"package_base": package_base})
return self.with_connection(run, commit=True)

View File

@ -0,0 +1,91 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import json
import sqlite3
from pathlib import Path
from sqlite3 import Connection
from typing import Type
from ahriman.core.configuration import Configuration
from ahriman.core.database.data import migrate_data
from ahriman.core.database.migrations import Migrations
from ahriman.core.database.operations.auth_operations import AuthOperations
from ahriman.core.database.operations.build_operations import BuildOperations
from ahriman.core.database.operations.package_operations import PackageOperations
from ahriman.core.database.operations.patch_operations import PatchOperations
class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations):
"""
wrapper for sqlite3 database
"""
@classmethod
def load(cls: Type[SQLite], configuration: Configuration) -> SQLite:
"""
construct instance from configuration
Args:
configuration(Configuration): configuration instance
Returns:
SQLite: fully initialized instance of the database
"""
path = cls.database_path(configuration)
database = cls(path)
database.init(configuration)
return database
@staticmethod
def database_path(configuration: Configuration) -> Path:
"""
read database from configuration
Args:
configuration(Configuration): configuration instance
Returns:
Path: database path according to the configuration
"""
return configuration.getpath("settings", "database")
def init(self, configuration: Configuration) -> None:
"""
perform database migrations
Args:
configuration(Configuration): configuration instance
"""
# custom types support
sqlite3.register_adapter(dict, json.dumps)
sqlite3.register_adapter(list, json.dumps)
sqlite3.register_converter("json", json.loads)
paths = configuration.repository_paths
def run(connection: Connection) -> None:
result = Migrations.migrate(connection)
migrate_data(result, connection, configuration, paths)
self.with_connection(run)
paths.chown(self.path)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 ahriman team.
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@ -17,6 +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 pathlib import Path
from typing import Any
@ -25,12 +26,14 @@ class BuildFailed(RuntimeError):
base exception for failed builds
"""
def __init__(self, package: str) -> None:
def __init__(self, package_base: str) -> None:
"""
default constructor
:param package: package base raised exception
Args:
package_base(str): package base raised exception
"""
RuntimeError.__init__(self, f"Package {package} build failed, check logs for details")
RuntimeError.__init__(self, f"Package {package_base} build failed, check logs for details")
class DuplicateRun(RuntimeError):
@ -46,18 +49,11 @@ class DuplicateRun(RuntimeError):
self, "Another application instance is run. This error can be suppressed by using --force flag.")
class DuplicateUser(ValueError):
class ExitCode(RuntimeError):
"""
exception which will be thrown in case if there are two users with different settings
special exception which has to be thrown to return non-zero status without error message
"""
def __init__(self, username: str) -> None:
"""
default constructor
:param username: username with duplicates
"""
ValueError.__init__(self, f"Found duplicate user with username {username}")
class InitializeException(RuntimeError):
"""
@ -67,7 +63,9 @@ class InitializeException(RuntimeError):
def __init__(self, details: str) -> None:
"""
default constructor
:param details: details of the exception
Args:
details(str): details of the exception
"""
RuntimeError.__init__(self, f"Could not load service: {details}")
@ -80,11 +78,29 @@ class InvalidOption(ValueError):
def __init__(self, value: Any) -> None:
"""
default constructor
:param value: option value
Args:
value(Any): option value
"""
ValueError.__init__(self, f"Invalid or unknown option value `{value}`")
class InvalidPath(ValueError):
"""
exception which will be raised on path which is not belong to root directory
"""
def __init__(self, path: Path, root: Path) -> None:
"""
default constructor
Args:
path(Path): path which raised an exception
root(Path): repository root (i.e. ahriman home)
"""
ValueError.__init__(self, f"Path `{path}` does not belong to repository root `{root}`")
class InvalidPackageInfo(RuntimeError):
"""
exception which will be raised on package load errors
@ -93,11 +109,28 @@ class InvalidPackageInfo(RuntimeError):
def __init__(self, details: Any) -> None:
"""
default constructor
:param details: error details
Args:
details(Any): error details
"""
RuntimeError.__init__(self, f"There are errors during reading package information: `{details}`")
class MigrationError(RuntimeError):
"""
exception which will be raised on migration error
"""
def __init__(self, details: str) -> None:
"""
default constructor
Args:
details(str): error details
"""
RuntimeError.__init__(self, details)
class MissingArchitecture(ValueError):
"""
exception which will be raised if architecture is required, but missing
@ -106,12 +139,14 @@ class MissingArchitecture(ValueError):
def __init__(self, command: str) -> None:
"""
default constructor
:param command: command name which throws exception
Args:
command(str): command name which throws exception
"""
ValueError.__init__(self, f"Architecture required for subcommand {command}, but missing")
class MultipleArchitecture(ValueError):
class MultipleArchitectures(ValueError):
"""
exception which will be raised if multiple architectures are not supported by the handler
"""
@ -119,7 +154,9 @@ class MultipleArchitecture(ValueError):
def __init__(self, command: str) -> None:
"""
default constructor
:param command: command name which throws exception
Args:
command(str): command name which throws exception
"""
ValueError.__init__(self, f"Multiple architectures are not supported by subcommand {command}")
@ -136,6 +173,21 @@ class ReportFailed(RuntimeError):
RuntimeError.__init__(self, "Report failed")
class SuccessFailed(ValueError):
"""
exception for merging invalid statues
"""
def __init__(self, package_base: str) -> None:
"""
default constructor
Args:
package_base(str): package base name
"""
ValueError.__init__(self, f"Package base {package_base} had status failed, but new status is success")
class SyncFailed(RuntimeError):
"""
remote synchronization exception
@ -156,7 +208,9 @@ class UnknownPackage(ValueError):
def __init__(self, package_base: str) -> None:
"""
default constructor
:param package_base: package base name
Args:
package_base(str): package base name
"""
ValueError.__init__(self, f"Package base {package_base} is unknown")
@ -169,8 +223,10 @@ class UnsafeRun(RuntimeError):
def __init__(self, current_uid: int, root_uid: int) -> None:
"""
default constructor
:param current_uid: current user ID
:param root_uid: ID of the owner of root directory
Args:
current_uid(int): current user ID
root_uid(int): ID of the owner of root directory
"""
RuntimeError.__init__(self, f"Current UID {current_uid} differs from root owner {root_uid}. "
f"Note that for the most actions it is unsafe to run application as different user."

View File

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

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