mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 23:37:18 +00:00
Compare commits
84 Commits
Author | SHA1 | Date | |
---|---|---|---|
a5455b697d | |||
0bfb763b2a | |||
9f3566a150 | |||
16a6c4fdd7 | |||
91f66fdcee | |||
bb45b1d868 | |||
3d10fa472b | |||
a90c93bbc4 | |||
41a3c08d9f | |||
cb328ad797 | |||
810091cde9 | |||
fc0474fa8f | |||
b94179e071 | |||
9c5a9f5837 | |||
83047d8270 | |||
990d5dda81 | |||
48e79ce39c | |||
375d7c55e5 | |||
db52b8e844 | |||
50af309c80 | |||
581401d60f | |||
c2685f4746 | |||
952b55f707 | |||
b9b012be53 | |||
b8036649ab | |||
c90e20587e | |||
3e020ec141 | |||
783b7d043d | |||
5c297d1c67 | |||
b0d1f3c091 | |||
50e219fda5 | |||
75298d1b8a | |||
8196dcc8a0 | |||
f634f1df58 | |||
32df4fc54f | |||
11ae930c59 | |||
9c332c23d2 | |||
4ed0a49a44 | |||
50f532a48a | |||
c6ccf53768 | |||
ce0c07cbd9 | |||
912a76d5cb | |||
76d0b0bc6d | |||
27d018e721 | |||
a0e20ffb77 | |||
96e4abc3c0 | |||
6df60498aa | |||
eb0a4b6b4a | |||
8f469e7eac | |||
535e955814 | |||
0bd3ba626a | |||
ffe6aec190 | |||
56c600e5ac | |||
461883217d | |||
62d55eff19 | |||
534b5600b4 | |||
32cbafd12b | |||
880c70bd58 | |||
d449eb3c2e | |||
17b5cd0751 | |||
2aef906fc8 | |||
e034327501 | |||
5d79fcca22 | |||
6e9dcca254 | |||
fbf6748d4a | |||
2260e52d5c | |||
bd2b61494f | |||
7280d30748 | |||
710274065d | |||
e0b09cefad | |||
3b93510aad | |||
5003cabeb5 | |||
bc6af9256b | |||
1ac7c87317 | |||
803b7bee1e | |||
646190121a | |||
10e4f3b629 | |||
80a1f37c85 | |||
751676a07e | |||
e1a7071ce5 | |||
1605d185e2 | |||
2fdf910e78 | |||
63dc43366b | |||
74a244f06c |
1
.bandit-test.yml
Normal file
1
.bandit-test.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
skips: ['B101', 'B404']
|
1
.bandit.yml
Normal file
1
.bandit.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
skips: ['B404', 'B603']
|
24
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
|
||||||
|
Steps to reproduce the behavior (commands, environment etc)
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
Add logs to help explain your problem. Logs to stderr can be generated by using `--no-log` command line option.
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
12
.github/ISSUE_TEMPLATE/discussion.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/discussion.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Create an issue to get help with project
|
||||||
|
title: ''
|
||||||
|
labels: question
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Describe your question below
|
||||||
|
|
||||||
|
A clear and concise description of your issue for which you would like to get help.
|
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature summary
|
||||||
|
|
||||||
|
Brief description of the feature required
|
||||||
|
|
||||||
|
### Cause of the feature request
|
||||||
|
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
### Proposed changes and/or features
|
||||||
|
|
||||||
|
A clear and concise description of what you want to happen.
|
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
Brief description of the pull request. Try to provide clear explanation for major changes.
|
||||||
|
|
||||||
|
Please make sure that branch called either `feature/feature-name` for feature-related pull requests or `bug/bug-name` for bug-related ones.
|
||||||
|
|
||||||
|
Put `closes #ISSUE` in case if the pull requests solves one of the opened issues.
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
- [ ] Tests to cover new code
|
||||||
|
- [ ] `make check` passed
|
||||||
|
- [ ] `make tests` passed
|
37
.github/workflows/release.yml
vendored
Normal file
37
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
make-release:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
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
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
body: |
|
||||||
|
${{ steps.changelog.outputs.compareurl }}
|
||||||
|
${{ steps.changelog.outputs.changelog }}
|
||||||
|
files: ahriman-*-src.tar.xz
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
26
.github/workflows/run-tests.yml
vendored
Normal file
26
.github/workflows/run-tests.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: run check and tests in archlinux container
|
||||||
|
run: |
|
||||||
|
docker run \
|
||||||
|
-v ${{ github.workspace }}:/build -w /build \
|
||||||
|
archlinux:latest \
|
||||||
|
/bin/bash -c "pacman --noconfirm -Syu base-devel python-argparse-manpage python-pip && \
|
||||||
|
pip install -e .[web] && \
|
||||||
|
pip install -e .[check] && \
|
||||||
|
pip install -e .[s3] && \
|
||||||
|
pip install -e .[test] && \
|
||||||
|
make check tests"
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -94,3 +94,5 @@ ENV/
|
|||||||
.venv/
|
.venv/
|
||||||
|
|
||||||
*.tar.xz
|
*.tar.xz
|
||||||
|
|
||||||
|
man/
|
||||||
|
@ -22,7 +22,7 @@ ignore-patterns=
|
|||||||
|
|
||||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||||
# number of processors available to use.
|
# number of processors available to use.
|
||||||
jobs=1
|
jobs=0
|
||||||
|
|
||||||
# Control the amount of potential inferred values when inferring a single
|
# Control the amount of potential inferred values when inferring a single
|
||||||
# object. This can help the performance when dealing with large functions or
|
# object. This can help the performance when dealing with large functions or
|
||||||
@ -149,7 +149,6 @@ disable=print-statement,
|
|||||||
too-few-public-methods,
|
too-few-public-methods,
|
||||||
too-many-instance-attributes,
|
too-many-instance-attributes,
|
||||||
broad-except,
|
broad-except,
|
||||||
logging-fstring-interpolation,
|
|
||||||
too-many-ancestors,
|
too-many-ancestors,
|
||||||
fixme,
|
fixme,
|
||||||
too-many-arguments,
|
too-many-arguments,
|
||||||
|
2
AUTHORS
Normal file
2
AUTHORS
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Current developers:
|
||||||
|
Evgenii Alekseev aka arcanis <esalexeev (at) gmail (dot) com>
|
@ -1,6 +1,6 @@
|
|||||||
# ahriman configuration
|
# ahriman configuration
|
||||||
|
|
||||||
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build_x86_64` groups it will use the `build_x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority).
|
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.
|
||||||
|
|
||||||
## `settings` group
|
## `settings` group
|
||||||
|
|
||||||
@ -18,9 +18,9 @@ libalpm and AUR related configuration.
|
|||||||
* `repositories` - list of pacman repositories, space separated list of strings, required.
|
* `repositories` - list of pacman repositories, space separated list of strings, required.
|
||||||
* `root` - root for alpm library, string, required.
|
* `root` - root for alpm library, string, required.
|
||||||
|
|
||||||
## `build_*` groups
|
## `build:*` groups
|
||||||
|
|
||||||
Build related configuration. Group name must refer to architecture, e.g. it should be `build_x86_64` for x86_64 architecture.
|
Build related configuration. Group name must refer to architecture, e.g. it should be `build:x86_64` for x86_64 architecture.
|
||||||
|
|
||||||
* `archbuild_flags` - additional flags passed to `archbuild` command, space separated list of strings, optional.
|
* `archbuild_flags` - additional flags passed to `archbuild` command, space separated list of strings, optional.
|
||||||
* `build_command` - default build command, string, required.
|
* `build_command` - default build command, string, required.
|
||||||
@ -35,9 +35,9 @@ Base repository settings.
|
|||||||
* `name` - repository name, string, required.
|
* `name` - repository name, string, required.
|
||||||
* `root` - root path for application, string, required.
|
* `root` - root path for application, string, required.
|
||||||
|
|
||||||
## `sign_*` groups
|
## `sign:*` groups
|
||||||
|
|
||||||
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign_x86_64` for x86_64 architecture.
|
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign:x86_64` for x86_64 architecture.
|
||||||
|
|
||||||
* `target` - configuration flag to enable signing, space separated list of strings, required. Allowed values are `package` (sign each package separately), `repository` (sign repository database file).
|
* `target` - configuration flag to enable signing, space separated list of strings, required. Allowed values are `package` (sign each package separately), `repository` (sign repository database file).
|
||||||
* `key` - default PGP key, string, required. This key will also be used for database signing if enabled.
|
* `key` - default PGP key, string, required. This key will also be used for database signing if enabled.
|
||||||
@ -47,11 +47,27 @@ Settings for signing packages or repository. Group name must refer to architectu
|
|||||||
|
|
||||||
Report generation settings.
|
Report generation settings.
|
||||||
|
|
||||||
* `target` - list of reports to be generated, space separated list of strings, optional. Allowed values are `html`.
|
* `target` - list of reports to be generated, space separated list of strings, optional. Allowed values are `html`, `email`.
|
||||||
|
|
||||||
### `html_*` groups
|
### `email:*` groups
|
||||||
|
|
||||||
Group name must refer to architecture, e.g. it should be `html_x86_64` for x86_64 architecture.
|
Group name must refer to architecture, e.g. it should be `email:x86_64` for x86_64 architecture.
|
||||||
|
|
||||||
|
* `homepage` - link to homepage, string, optional.
|
||||||
|
* `host` - SMTP host for sending emails, string, required.
|
||||||
|
* `link_path` - prefix for HTML links, string, required.
|
||||||
|
* `no_empty_report` - skip report generation for empty packages list, boolean, optional, default `yes`.
|
||||||
|
* `password` - SMTP password to authenticate, string, optional.
|
||||||
|
* `port` - SMTP port for sending emails, int, required.
|
||||||
|
* `receivers` - SMTP receiver addresses, space separated list of strings, required.
|
||||||
|
* `sender` - SMTP sender address, string, required.
|
||||||
|
* `ssl` - SSL mode for SMTP connection, one of `ssl`, `starttls`, `disabled`, optional, default `disabled`.
|
||||||
|
* `template_path` - path to Jinja2 template, string, required.
|
||||||
|
* `user` - SMTP user to authenticate, string, optional.
|
||||||
|
|
||||||
|
### `html:*` groups
|
||||||
|
|
||||||
|
Group name must refer to architecture, e.g. it should be `html:x86_64` for x86_64 architecture.
|
||||||
|
|
||||||
* `path` - path to html report file, string, required.
|
* `path` - path to html report file, string, required.
|
||||||
* `homepage` - link to homepage, string, optional.
|
* `homepage` - link to homepage, string, optional.
|
||||||
@ -64,21 +80,26 @@ Remote synchronization settings.
|
|||||||
|
|
||||||
* `target` - list of synchronizations to be used, space separated list of strings, optional. Allowed values are `rsync`, `s3`.
|
* `target` - list of synchronizations to be used, space separated list of strings, optional. Allowed values are `rsync`, `s3`.
|
||||||
|
|
||||||
### `rsync_*` groups
|
### `rsync:*` groups
|
||||||
|
|
||||||
Group name must refer to architecture, e.g. it should be `rsync_x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`.
|
Group name must refer to architecture, e.g. it should be `rsync:x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`.
|
||||||
|
|
||||||
|
* `command` - rsync command to run, space separated list of string, required.
|
||||||
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
|
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
|
||||||
|
|
||||||
### `s3_*` groups
|
### `s3:*` groups
|
||||||
|
|
||||||
Group name must refer to architecture, e.g. it should be `s3_x86_64` for x86_64 architecture. Requires `aws-cli` package to be installed. Do not forget to configure it for user `ahriman`.
|
Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64 architecture.
|
||||||
|
|
||||||
* `bucket` - bucket name (e.g. `s3://bucket/path`), string, required.
|
* `access_key` - AWS access key ID, string, required.
|
||||||
|
* `bucket` - bucket name (e.g. `bucket`), string, required.
|
||||||
|
* `chunk_size` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024.
|
||||||
|
* `region` - bucket region (e.g. `eu-central-1`), string, required.
|
||||||
|
* `secret_key` - AWS secret access key, string, required.
|
||||||
|
|
||||||
## `web_*` groups
|
## `web:*` groups
|
||||||
|
|
||||||
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web_x86_64` for x86_64 architecture.
|
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture.
|
||||||
|
|
||||||
* `host` - host to bind, string, optional.
|
* `host` - host to bind, string, optional.
|
||||||
* `port` - port to bind, int, optional.
|
* `port` - port to bind, int, optional.
|
||||||
|
33
Makefile
33
Makefile
@ -1,9 +1,9 @@
|
|||||||
.PHONY: archive archive_directory archlinux check clean directory push version
|
.PHONY: archive archive_directory archlinux check clean directory push tests version
|
||||||
.DEFAULT_GOAL := archlinux
|
.DEFAULT_GOAL := archlinux
|
||||||
|
|
||||||
PROJECT := ahriman
|
PROJECT := ahriman
|
||||||
|
|
||||||
FILES := COPYING CONFIGURING.md README.md package src setup.py
|
FILES := AUTHORS COPYING CONFIGURING.md README.md package src setup.cfg setup.py
|
||||||
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
|
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
|
||||||
IGNORE_FILES := package/archlinux src/.mypy_cache
|
IGNORE_FILES := package/archlinux src/.mypy_cache
|
||||||
|
|
||||||
@ -16,35 +16,42 @@ archive: archive_directory
|
|||||||
|
|
||||||
archive_directory: $(TARGET_FILES)
|
archive_directory: $(TARGET_FILES)
|
||||||
rm -fr $(addprefix $(PROJECT)/, $(IGNORE_FILES))
|
rm -fr $(addprefix $(PROJECT)/, $(IGNORE_FILES))
|
||||||
find $(PROJECT) -type f -name '*.pyc' -delete
|
find "$(PROJECT)" -type f -name "*.pyc" -delete
|
||||||
find $(PROJECT) -depth -type d -name '__pycache__' -execdir rm -rf {} +
|
find "$(PROJECT)" -depth -type d -name "__pycache__" -execdir rm -rf {} +
|
||||||
find $(PROJECT) -depth -type d -name '*.egg-info' -execdir rm -rf {} +
|
find "$(PROJECT)" -depth -type d -name "*.egg-info" -execdir rm -rf {} +
|
||||||
|
|
||||||
archlinux: archive
|
archlinux: archive
|
||||||
sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$$(sha512sum $(PROJECT)-$(VERSION)-src.tar.xz | awk '{print $$1}')'/" package/archlinux/PKGBUILD
|
|
||||||
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
|
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
|
||||||
|
|
||||||
check:
|
check: clean mypy
|
||||||
cd src && mypy --implicit-reexport --strict -p $(PROJECT)
|
autopep8 --exit-code --max-line-length 120 -aa -i -j 0 -r "src/$(PROJECT)" "tests/$(PROJECT)"
|
||||||
cd src && find $(PROJECT) -name '*.py' -execdir autopep8 --max-line-length 120 -aa -i {} +
|
pylint --rcfile=.pylintrc "src/$(PROJECT)"
|
||||||
cd src && pylint --rcfile=../.pylintrc $(PROJECT)
|
bandit -c .bandit.yml -r "src/$(PROJECT)"
|
||||||
|
bandit -c .bandit-test.yml -r "tests/$(PROJECT)"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find . -type f -name '$(PROJECT)-*-src.tar.xz' -delete
|
find . -type f -name "$(PROJECT)-*-src.tar.xz" -delete
|
||||||
rm -rf "$(PROJECT)"
|
rm -rf "$(PROJECT)"
|
||||||
|
|
||||||
directory: clean
|
directory: clean
|
||||||
mkdir "$(PROJECT)"
|
mkdir "$(PROJECT)"
|
||||||
|
|
||||||
|
mypy:
|
||||||
|
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)" --install-types --non-interactive || true
|
||||||
|
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
|
||||||
|
|
||||||
push: archlinux
|
push: archlinux
|
||||||
git add package/archlinux/PKGBUILD src/ahriman/version.py
|
git add package/archlinux/PKGBUILD src/ahriman/version.py
|
||||||
git commit -m "Release $(VERSION)"
|
git commit -m "Release $(VERSION)"
|
||||||
git push
|
|
||||||
git tag "$(VERSION)"
|
git tag "$(VERSION)"
|
||||||
|
git push
|
||||||
git push --tags
|
git push --tags
|
||||||
|
|
||||||
|
tests: clean
|
||||||
|
python setup.py test
|
||||||
|
|
||||||
version:
|
version:
|
||||||
ifndef VERSION
|
ifndef VERSION
|
||||||
$(error VERSION is required, but not set)
|
$(error VERSION is required, but not set)
|
||||||
endif
|
endif
|
||||||
sed -i "/__version__ = '[0-9.]*/s/[^'][^)]*/__version__ = '$(VERSION)'/" src/ahriman/version.py
|
sed -i '/__version__ = "[0-9.]*/s/[^"][^)]*/__version__ = "$(VERSION)"/' src/ahriman/version.py
|
||||||
|
11
README.md
11
README.md
@ -1,5 +1,8 @@
|
|||||||
# ArcHlinux ReposItory MANager
|
# ArcHlinux ReposItory MANager
|
||||||
|
|
||||||
|
[](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
|
||||||
|
[](https://www.codefactor.io/repository/github/arcan1s/ahriman)
|
||||||
|
|
||||||
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
|
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@ -19,14 +22,14 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
|
|||||||
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
|
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
echo 'PACKAGES="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
|
echo 'PACKAGER="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
* Configure build tools (it is required for correct dependency management system):
|
* Configure build tools (it is required for correct dependency management system):
|
||||||
|
|
||||||
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build` (you can choose any name for command, basically it should be `{name}-{arch}-build`);
|
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build` (you can choose any name for command, basically it should be `{name}-{arch}-build`);
|
||||||
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`);
|
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`);
|
||||||
* change configuration file, add your own repository, add multilib repository etc. Hint: you can use `Include` option as well;
|
* change configuration file, add your own repository, add multilib repository etc;
|
||||||
* set `build_command` option to point to your command;
|
* set `build_command` option to point to your command;
|
||||||
* configure `/etc/sudoers.d/ahriman` to allow running command without a password.
|
* configure `/etc/sudoers.d/ahriman` to allow running command without a password.
|
||||||
|
|
||||||
@ -64,5 +67,7 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
|
|||||||
* Add packages by using `ahriman add {package}` command:
|
* Add packages by using `ahriman add {package}` command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo -u ahriman ahriman -a x86_64 add yay
|
sudo -u ahriman ahriman -a x86_64 add yay --now
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that initial service configuration can be done by running `ahriman setup` with specific arguments.
|
||||||
|
@ -1,31 +1,27 @@
|
|||||||
# Maintainer: Evgeniy Alekseev
|
# Maintainer: Evgeniy Alekseev
|
||||||
|
|
||||||
pkgname='ahriman'
|
pkgname='ahriman'
|
||||||
pkgver=0.15.0
|
pkgver=1.2.5
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="ArcHlinux ReposItory MANager"
|
pkgdesc="ArcHlinux ReposItory MANager"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://github.com/arcan1s/ahriman"
|
url="https://github.com/arcan1s/ahriman"
|
||||||
license=('GPL3')
|
license=('GPL3')
|
||||||
depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-srcinfo')
|
depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-srcinfo')
|
||||||
makedepends=('python-pip')
|
makedepends=('python-argparse-manpage' 'python-pip')
|
||||||
optdepends=('aws-cli: sync to s3'
|
optdepends=('breezy: -bzr packages support'
|
||||||
'breezy: -bzr packages support'
|
|
||||||
'darcs: -darcs packages support'
|
'darcs: -darcs packages support'
|
||||||
'gnupg: package and repository sign'
|
'gnupg: package and repository sign'
|
||||||
'mercurial: -hg packages support'
|
'mercurial: -hg packages support'
|
||||||
'python-aiohttp: web server'
|
'python-aiohttp: web server'
|
||||||
'python-aiohttp-jinja2: web server'
|
'python-aiohttp-jinja2: web server'
|
||||||
|
'python-boto3: sync to s3'
|
||||||
'python-jinja: html report generation'
|
'python-jinja: html report generation'
|
||||||
'python-requests: web server'
|
|
||||||
'rsync: sync by using rsync'
|
'rsync: sync by using rsync'
|
||||||
'subversion: -svn packages support')
|
'subversion: -svn packages support')
|
||||||
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
|
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
|
||||||
'ahriman.sysusers'
|
'ahriman.sysusers'
|
||||||
'ahriman.tmpfiles')
|
'ahriman.tmpfiles')
|
||||||
sha512sums=('a1db44390ce1785da3d535e3cfd2242d8d56070228eb9b3c1d5629163b65941d60753c481c0fdc69e475e534a828ceea39568dc6711abeee092616dac08e31a9'
|
|
||||||
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
|
|
||||||
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
|
|
||||||
backup=('etc/ahriman.ini'
|
backup=('etc/ahriman.ini'
|
||||||
'etc/ahriman.ini.d/logging.ini')
|
'etc/ahriman.ini.d/logging.ini')
|
||||||
|
|
||||||
@ -43,3 +39,7 @@ package() {
|
|||||||
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
|
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
|
||||||
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
|
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sha512sums=('6ab741bfb42f92ab00d1b6ecfc44426c00e5c433486e014efbdb585715d9a12dbbafc280e5a9f85b941c8681b13a9dad41327a3e3c44a9683ae30c1d6f017f50'
|
||||||
|
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
|
||||||
|
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
|
||||||
|
@ -1,2 +1 @@
|
|||||||
d /var/lib/ahriman 0775 ahriman log
|
|
||||||
d /var/log/ahriman 0755 ahriman ahriman
|
d /var/log/ahriman 0755 ahriman ahriman
|
@ -13,7 +13,7 @@ archbuild_flags =
|
|||||||
build_command = extra-x86_64-build
|
build_command = extra-x86_64-build
|
||||||
ignore_packages =
|
ignore_packages =
|
||||||
makechrootpkg_flags =
|
makechrootpkg_flags =
|
||||||
makepkg_flags = --skippgpcheck
|
makepkg_flags =
|
||||||
|
|
||||||
[repository]
|
[repository]
|
||||||
name = aur-clone
|
name = aur-clone
|
||||||
@ -21,27 +21,27 @@ root = /var/lib/ahriman
|
|||||||
|
|
||||||
[sign]
|
[sign]
|
||||||
target =
|
target =
|
||||||
key =
|
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
target =
|
target =
|
||||||
|
|
||||||
|
[email]
|
||||||
|
no_empty_report = yes
|
||||||
|
template_path = /usr/share/ahriman/repo-index.jinja2
|
||||||
|
ssl = disabled
|
||||||
|
|
||||||
[html]
|
[html]
|
||||||
path =
|
|
||||||
homepage =
|
|
||||||
link_path =
|
|
||||||
template_path = /usr/share/ahriman/repo-index.jinja2
|
template_path = /usr/share/ahriman/repo-index.jinja2
|
||||||
|
|
||||||
[upload]
|
[upload]
|
||||||
target =
|
target =
|
||||||
|
|
||||||
[rsync]
|
[rsync]
|
||||||
remote =
|
command = rsync --archive --compress --partial --delete
|
||||||
|
|
||||||
[s3]
|
[s3]
|
||||||
bucket =
|
chunk_size = 8388608
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
host =
|
host = 127.0.0.1
|
||||||
port =
|
|
||||||
templates = /usr/share/ahriman
|
templates = /usr/share/ahriman
|
@ -2,10 +2,10 @@
|
|||||||
keys = root,builder,build_details,http
|
keys = root,builder,build_details,http
|
||||||
|
|
||||||
[handlers]
|
[handlers]
|
||||||
keys = console_handler,build_file_handler,file_handler,http_handler
|
keys = console_handler,build_file_handler,file_handler,http_handler,syslog_handler
|
||||||
|
|
||||||
[formatters]
|
[formatters]
|
||||||
keys = generic_format
|
keys = generic_format,syslog_format
|
||||||
|
|
||||||
[handler_console_handler]
|
[handler_console_handler]
|
||||||
class = StreamHandler
|
class = StreamHandler
|
||||||
@ -17,43 +17,53 @@ args = (sys.stderr,)
|
|||||||
class = logging.handlers.RotatingFileHandler
|
class = logging.handlers.RotatingFileHandler
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
formatter = generic_format
|
formatter = generic_format
|
||||||
args = ('/var/log/ahriman/ahriman.log', 'a', 20971520, 20)
|
args = ("/var/log/ahriman/ahriman.log", "a", 20971520, 20)
|
||||||
|
|
||||||
[handler_build_file_handler]
|
[handler_build_file_handler]
|
||||||
class = logging.handlers.RotatingFileHandler
|
class = logging.handlers.RotatingFileHandler
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
formatter = generic_format
|
formatter = generic_format
|
||||||
args = ('/var/log/ahriman/build.log', 'a', 20971520, 20)
|
args = ("/var/log/ahriman/build.log", "a", 20971520, 20)
|
||||||
|
|
||||||
[handler_http_handler]
|
[handler_http_handler]
|
||||||
class = logging.handlers.RotatingFileHandler
|
class = logging.handlers.RotatingFileHandler
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
formatter = generic_format
|
formatter = generic_format
|
||||||
args = ('/var/log/ahriman/http.log', 'a', 20971520, 20)
|
args = ("/var/log/ahriman/http.log", "a", 20971520, 20)
|
||||||
|
|
||||||
|
[handler_syslog_handler]
|
||||||
|
class = logging.handlers.SysLogHandler
|
||||||
|
level = DEBUG
|
||||||
|
formatter = syslog_format
|
||||||
|
args = ("/dev/log",)
|
||||||
|
|
||||||
[formatter_generic_format]
|
[formatter_generic_format]
|
||||||
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||||
datefmt =
|
datefmt =
|
||||||
|
|
||||||
|
[formatter_syslog_format]
|
||||||
|
format = [%(levelname)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||||
|
datefmt =
|
||||||
|
|
||||||
[logger_root]
|
[logger_root]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = file_handler
|
handlers = syslog_handler
|
||||||
qualname = root
|
qualname = root
|
||||||
|
|
||||||
[logger_builder]
|
[logger_builder]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = file_handler
|
handlers = syslog_handler
|
||||||
qualname = builder
|
qualname = builder
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
|
||||||
[logger_build_details]
|
[logger_build_details]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = build_file_handler
|
handlers = syslog_handler
|
||||||
qualname = build_details
|
qualname = build_details
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
|
||||||
[logger_http]
|
[logger_http]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
handlers = http_handler
|
handlers = syslog_handler
|
||||||
qualname = http
|
qualname = http
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ repository|e }}</title>
|
<title>{{ repository }}</title>
|
||||||
|
|
||||||
{% include "style.jinja2" %}
|
{% include "style.jinja2" %}
|
||||||
|
|
||||||
@ -12,9 +12,9 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<h1>ahriman
|
<h1>ahriman
|
||||||
<img src="https://img.shields.io/badge/version-{{ version|e }}-informational" alt="{{ version|e }}">
|
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
|
||||||
<img src="https://img.shields.io/badge/architecture-{{ architecture|e }}-informational" alt="{{ architecture|e }}">
|
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
|
||||||
<img src="https://img.shields.io/badge/service%20status-{{ service.status|e }}-{{ service.status_color|e }}" alt="{{ service.status|e }}" title="{{ service.timestamp|e }}">
|
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{% include "search-line.jinja2" %}
|
{% include "search-line.jinja2" %}
|
||||||
@ -31,11 +31,11 @@
|
|||||||
|
|
||||||
{% for package in packages %}
|
{% for package in packages %}
|
||||||
<tr class="package">
|
<tr class="package">
|
||||||
<td class="include-search"><a href="{{ package.web_url|e }}" title="{{ package.base|e }}">{{ package.base|e }}</a></td>
|
<td class="include-search"><a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a></td>
|
||||||
<td class="include-search">{{ package.packages|join("<br>"|safe) }}</td>
|
<td class="include-search">{{ package.packages|join("<br>"|safe) }}</td>
|
||||||
<td>{{ package.version|e }}</td>
|
<td>{{ package.version }}</td>
|
||||||
<td>{{ package.timestamp|e }}</td>
|
<td>{{ package.timestamp }}</td>
|
||||||
<td class="status package-{{ package.status|e }}">{{ package.status|e }}</td>
|
<td class="status package-{{ package.status }}">{{ package.status }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -1,32 +1,36 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ repository|e }}</title>
|
<title>{{ repository }}</title>
|
||||||
|
|
||||||
{% include "style.jinja2" %}
|
{% include "style.jinja2" %}
|
||||||
|
|
||||||
{% include "sorttable.jinja2" %}
|
{% if extended_report %}
|
||||||
{% include "search.jinja2" %}
|
{% include "sorttable.jinja2" %}
|
||||||
|
{% include "search.jinja2" %}
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<h1>Archlinux user repository</h1>
|
{% if extended_report %}
|
||||||
|
<h1>Archlinux user repository</h1>
|
||||||
|
|
||||||
<section class="element">
|
<section class="element">
|
||||||
{% if pgp_key is not none %}
|
{% if pgp_key is not none %}
|
||||||
<p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}&fingerprint=on&op=index" title="key search">{{ pgp_key|e }}</a> by default.</p>
|
<p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<code>
|
<code>
|
||||||
$ cat /etc/pacman.conf<br>
|
$ cat /etc/pacman.conf<br>
|
||||||
[{{ repository|e }}]<br>
|
[{{ repository }}]<br>
|
||||||
Server = {{ link_path|e }}<br>
|
Server = {{ link_path }}<br>
|
||||||
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
|
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
|
||||||
</code>
|
</code>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% include "search-line.jinja2" %}
|
{% include "search-line.jinja2" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<section class="element">
|
<section class="element">
|
||||||
<table class="sortable search-table">
|
<table class="sortable search-table">
|
||||||
@ -40,23 +44,25 @@
|
|||||||
|
|
||||||
{% for package in packages %}
|
{% for package in packages %}
|
||||||
<tr class="package">
|
<tr class="package">
|
||||||
<td class="include-search"><a href="{{ link_path|e }}/{{ package.filename|e }}" title="{{ package.name|e }}">{{ package.name|e }}</a></td>
|
<td class="include-search"><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td>
|
||||||
<td>{{ package.version|e }}</td>
|
<td>{{ package.version }}</td>
|
||||||
<td>{{ package.archive_size|e }}</td>
|
<td>{{ package.archive_size }}</td>
|
||||||
<td>{{ package.installed_size|e }}</td>
|
<td>{{ package.installed_size }}</td>
|
||||||
<td>{{ package.build_date|e }}</td>
|
<td>{{ package.build_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
{% if extended_report %}
|
||||||
<ul class="navigation">
|
<footer>
|
||||||
{% if homepage is not none %}
|
<ul class="navigation">
|
||||||
<li><a href="{{ homepage|e }}" title="homepage">Homepage</a></li>
|
{% if homepage is not none %}
|
||||||
{% endif %}
|
<li><a href="{{ homepage }}" title="homepage">Homepage</a></li>
|
||||||
</ul>
|
{% endif %}
|
||||||
</footer>
|
</ul>
|
||||||
|
</footer>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
<section class="element">
|
<section class="element">
|
||||||
<input type="search" id="search" onkeyup="searchInTable()" placeholder="search for package" title="search for package"/>
|
<input type="search" id="search" onkeyup="searchInTable()" placeholder="search for package" title="search for package"/>
|
||||||
</section>
|
</section>
|
||||||
|
@ -5,21 +5,22 @@
|
|||||||
const tables = document.getElementsByClassName("search-table");
|
const tables = document.getElementsByClassName("search-table");
|
||||||
|
|
||||||
for (let i = 0; i < tables.length; i++) {
|
for (let i = 0; i < tables.length; i++) {
|
||||||
const tr = tables[i].getElementsByTagName("tr");
|
const trs = tables[i].getElementsByTagName("tr");
|
||||||
// from 1 coz of header
|
// from 1 coz of header
|
||||||
for (let i = 1; i < tr.length; i++) {
|
for (let i = 1; i < trs.length; i++) {
|
||||||
let td = tr[i].getElementsByClassName("include-search");
|
let tr = trs[i].getElementsByClassName("include-search");
|
||||||
let display = "none";
|
let display = "none";
|
||||||
for (let j = 0; j < td.length; j++) {
|
for (let j = 0; j < tr.length; j++) {
|
||||||
if (td[j].tagName.toLowerCase() === "td") {
|
if (tr[j].tagName.toLowerCase() === "td") {
|
||||||
if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) {
|
let contains = (element) => tr[j].innerHTML.toLowerCase().indexOf(element) > -1
|
||||||
|
if (filter.some(contains)) {
|
||||||
display = "";
|
display = "";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr[i].style.display = display;
|
trs[i].style.display = display;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1 +1 @@
|
|||||||
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
|
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
|
||||||
|
@ -133,4 +133,4 @@
|
|||||||
ul.navigation li a:hover {
|
ul.navigation li a:hover {
|
||||||
background-color: rgba(var(--color-hover), 1.0);
|
background-color: rgba(var(--color-hover), 1.0);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
8
setup.cfg
Normal file
8
setup.cfg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[aliases]
|
||||||
|
test = pytest
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
addopts = --cov=ahriman --cov-report term-missing:skip-covered --pspec
|
||||||
|
|
||||||
|
[build_manpages]
|
||||||
|
manpages = man/ahriman.1:module=ahriman.application.ahriman:function=_parser
|
112
setup.py
112
setup.py
@ -1,72 +1,106 @@
|
|||||||
from distutils.util import convert_path
|
from build_manpages import build_manpages
|
||||||
|
from pathlib import Path
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
from os import path
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
metadata_path = Path(__file__).resolve().parent / "src/ahriman/version.py"
|
||||||
|
metadata: Dict[str, Any] = dict()
|
||||||
|
with metadata_path.open() as metadata_file:
|
||||||
|
exec(metadata_file.read(), metadata) # pylint: disable=exec-used
|
||||||
|
|
||||||
here = path.abspath(path.dirname(__file__))
|
|
||||||
metadata = dict()
|
|
||||||
with open(convert_path('src/ahriman/version.py')) as metadata_file:
|
|
||||||
exec(metadata_file.read(), metadata)
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='ahriman',
|
name="ahriman",
|
||||||
|
|
||||||
version=metadata['__version__'],
|
version=metadata["__version__"],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
|
||||||
description='ArcHlinux ReposItory MANager',
|
description="ArcHlinux ReposItory MANager",
|
||||||
|
|
||||||
author='arcanis',
|
author="arcanis",
|
||||||
author_email='',
|
author_email="",
|
||||||
url='https://github.com/arcan1s/ahriman',
|
url="https://github.com/arcan1s/ahriman",
|
||||||
|
|
||||||
license='GPL3',
|
license="GPL3",
|
||||||
|
|
||||||
packages=find_packages('src'),
|
packages=find_packages("src"),
|
||||||
package_dir={'': 'src'},
|
package_dir={"": "src"},
|
||||||
|
|
||||||
dependency_links=[
|
dependency_links=[
|
||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'aur',
|
"aur",
|
||||||
'pyalpm',
|
"pyalpm",
|
||||||
'srcinfo',
|
"requests",
|
||||||
|
"srcinfo",
|
||||||
],
|
],
|
||||||
setup_requires=[
|
setup_requires=[
|
||||||
'pytest-runner',
|
"pytest-runner",
|
||||||
],
|
],
|
||||||
tests_require=[
|
tests_require=[
|
||||||
'pytest',
|
"pytest",
|
||||||
|
"pytest-aiohttp",
|
||||||
|
"pytest-cov",
|
||||||
|
"pytest-helpers-namespace",
|
||||||
|
"pytest-mock",
|
||||||
|
"pytest-pspec",
|
||||||
|
"pytest-resource-path",
|
||||||
],
|
],
|
||||||
|
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
scripts=[
|
scripts=[
|
||||||
'package/bin/ahriman',
|
"package/bin/ahriman",
|
||||||
],
|
],
|
||||||
data_files=[
|
data_files=[
|
||||||
('/etc', [
|
("/etc", [
|
||||||
'package/etc/ahriman.ini',
|
"package/etc/ahriman.ini",
|
||||||
]),
|
]),
|
||||||
('/etc/ahriman.ini.d', [
|
("/etc/ahriman.ini.d", [
|
||||||
'package/etc/ahriman.ini.d/logging.ini',
|
"package/etc/ahriman.ini.d/logging.ini",
|
||||||
]),
|
]),
|
||||||
('lib/systemd/system', [
|
("lib/systemd/system", [
|
||||||
'package/lib/systemd/system/ahriman@.service',
|
"package/lib/systemd/system/ahriman@.service",
|
||||||
'package/lib/systemd/system/ahriman@.timer',
|
"package/lib/systemd/system/ahriman@.timer",
|
||||||
'package/lib/systemd/system/ahriman-web@.service',
|
"package/lib/systemd/system/ahriman-web@.service",
|
||||||
]),
|
]),
|
||||||
('share/ahriman', [
|
("share/ahriman", [
|
||||||
'package/share/ahriman/build-status.jinja2',
|
"package/share/ahriman/build-status.jinja2",
|
||||||
'package/share/ahriman/repo-index.jinja2',
|
"package/share/ahriman/repo-index.jinja2",
|
||||||
'package/share/ahriman/search.jinja2',
|
"package/share/ahriman/search.jinja2",
|
||||||
'package/share/ahriman/search-line.jinja2',
|
"package/share/ahriman/search-line.jinja2",
|
||||||
'package/share/ahriman/sorttable.jinja2',
|
"package/share/ahriman/sorttable.jinja2",
|
||||||
'package/share/ahriman/style.jinja2',
|
"package/share/ahriman/style.jinja2",
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|
||||||
extras_require={
|
extras_require={
|
||||||
'html-templates': ['Jinja2'],
|
"check": [
|
||||||
'test': ['coverage', 'pytest'],
|
"autopep8",
|
||||||
'web': ['Jinja2', 'aiohttp', 'aiohttp_jinja2', 'requests'],
|
"bandit",
|
||||||
|
"mypy",
|
||||||
|
"pylint",
|
||||||
|
],
|
||||||
|
"s3": [
|
||||||
|
"boto3",
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"pytest",
|
||||||
|
"pytest-aiohttp",
|
||||||
|
"pytest-cov",
|
||||||
|
"pytest-helpers-namespace",
|
||||||
|
"pytest-mock",
|
||||||
|
"pytest-pspec",
|
||||||
|
"pytest-resource-path",
|
||||||
|
],
|
||||||
|
"web": [
|
||||||
|
"Jinja2",
|
||||||
|
"aiohttp",
|
||||||
|
"aiohttp_jinja2",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cmdclass={
|
||||||
|
"build_manpages": build_manpages.build_manpages,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -19,89 +19,327 @@
|
|||||||
#
|
#
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import ahriman.application.handlers as handlers
|
from pathlib import Path
|
||||||
import ahriman.version as version
|
|
||||||
|
from ahriman import version
|
||||||
|
from ahriman.application import handlers
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.models.sign_settings import SignSettings
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
# pylint thinks it is bad idea, but get the fuck off
|
||||||
parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager')
|
SubParserAction = argparse._SubParsersAction # pylint: disable=protected-access
|
||||||
|
|
||||||
|
|
||||||
|
def _parser() -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
command line parser generator
|
||||||
|
:return: command line parser for the application
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
|
||||||
|
action="append")
|
||||||
|
parser.add_argument("-c", "--configuration", help="configuration path", type=Path, default=Path("/etc/ahriman.ini"))
|
||||||
|
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-a',
|
"-l",
|
||||||
'--architecture',
|
"--lock",
|
||||||
help='target architectures (can be used multiple times)',
|
help="lock file",
|
||||||
action='append')
|
type=Path,
|
||||||
parser.add_argument('-c', '--config', help='configuration path', default='/etc/ahriman.ini')
|
default=Path(tempfile.gettempdir()) / "ahriman.lock")
|
||||||
parser.add_argument('--force', help='force run, remove file lock', action='store_true')
|
parser.add_argument("--no-log", help="redirect all log messages to stderr", action="store_true")
|
||||||
parser.add_argument('--lock', help='lock file', default='/tmp/ahriman.lock')
|
parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
|
||||||
parser.add_argument('--no-log', help='redirect all log messages to stderr', action='store_true')
|
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
|
||||||
parser.add_argument('--no-report', help='force disable reporting to web service', action='store_true')
|
parser.add_argument("-v", "--version", action="version", version=version.__version__)
|
||||||
parser.add_argument('--unsafe', help='allow to run ahriman as non-ahriman user', action='store_true')
|
|
||||||
parser.add_argument('-v', '--version', action='version', version=version.__version__)
|
|
||||||
subparsers = parser.add_subparsers(title='command')
|
|
||||||
|
|
||||||
add_parser = subparsers.add_parser('add', description='add package')
|
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
|
||||||
add_parser.add_argument('package', help='package base/name or archive path', nargs='+')
|
|
||||||
add_parser.add_argument('--without-dependencies', help='do not add dependencies', action='store_true')
|
|
||||||
add_parser.set_defaults(handler=handlers.Add)
|
|
||||||
|
|
||||||
check_parser = subparsers.add_parser('check', description='check for updates. Same as update --dry-run --no-manual')
|
_set_add_parser(subparsers)
|
||||||
check_parser.add_argument('package', help='filter check by package base', nargs='*')
|
_set_check_parser(subparsers)
|
||||||
check_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true')
|
_set_clean_parser(subparsers)
|
||||||
check_parser.set_defaults(handler=handlers.Update, no_aur=False, no_manual=True, dry_run=True)
|
_set_config_parser(subparsers)
|
||||||
|
_set_init_parser(subparsers)
|
||||||
|
_set_key_import_parser(subparsers)
|
||||||
|
_set_rebuild_parser(subparsers)
|
||||||
|
_set_remove_parser(subparsers)
|
||||||
|
_set_report_parser(subparsers)
|
||||||
|
_set_search_parser(subparsers)
|
||||||
|
_set_setup_parser(subparsers)
|
||||||
|
_set_sign_parser(subparsers)
|
||||||
|
_set_status_parser(subparsers)
|
||||||
|
_set_status_update_parser(subparsers)
|
||||||
|
_set_sync_parser(subparsers)
|
||||||
|
_set_update_parser(subparsers)
|
||||||
|
_set_web_parser(subparsers)
|
||||||
|
|
||||||
clean_parser = subparsers.add_parser('clean', description='clear all local caches')
|
return parser
|
||||||
clean_parser.add_argument('--no-build', help='do not clear directory with package sources', action='store_true')
|
|
||||||
clean_parser.add_argument('--no-cache', help='do not clear directory with package caches', action='store_true')
|
|
||||||
clean_parser.add_argument('--no-chroot', help='do not clear build chroot', action='store_true')
|
|
||||||
clean_parser.add_argument(
|
|
||||||
'--no-manual',
|
|
||||||
help='do not clear directory with manually added packages',
|
|
||||||
action='store_true')
|
|
||||||
clean_parser.add_argument('--no-packages', help='do not clear directory with built packages', action='store_true')
|
|
||||||
clean_parser.set_defaults(handler=handlers.Clean)
|
|
||||||
|
|
||||||
config_parser = subparsers.add_parser('config', description='dump configuration for specified architecture')
|
|
||||||
config_parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True)
|
|
||||||
|
|
||||||
rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository')
|
def _set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
rebuild_parser.set_defaults(handler=handlers.Rebuild)
|
"""
|
||||||
|
add parser for add subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("add", help="add package", description="add package",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("package", help="package base/name or archive path", nargs="+")
|
||||||
|
parser.add_argument("--now", help="run update function after", action="store_true")
|
||||||
|
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
|
||||||
|
parser.set_defaults(handler=handlers.Add, architecture=[])
|
||||||
|
return parser
|
||||||
|
|
||||||
remove_parser = subparsers.add_parser('remove', description='remove package')
|
|
||||||
remove_parser.add_argument('package', help='package name or base', nargs='+')
|
|
||||||
remove_parser.set_defaults(handler=handlers.Remove)
|
|
||||||
|
|
||||||
report_parser = subparsers.add_parser('report', description='generate report')
|
def _set_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
report_parser.add_argument('target', help='target to generate report', nargs='*')
|
"""
|
||||||
report_parser.set_defaults(handler=handlers.Report)
|
add parser for check subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("check", help="check for updates",
|
||||||
|
description="check for updates. Same as update --dry-run --no-manual",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||||
|
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
|
||||||
|
parser.set_defaults(handler=handlers.Update, architecture=[], no_aur=False, no_manual=True, dry_run=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
status_parser = subparsers.add_parser('status', description='request status of the package')
|
|
||||||
status_parser.add_argument('--ahriman', help='get service status itself', action='store_true')
|
|
||||||
status_parser.add_argument('package', help='filter status by package base', nargs='*')
|
|
||||||
status_parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, unsafe=True)
|
|
||||||
|
|
||||||
sync_parser = subparsers.add_parser('sync', description='sync packages to remote server')
|
def _set_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
sync_parser.add_argument('target', help='target to sync', nargs='*')
|
"""
|
||||||
sync_parser.set_defaults(handler=handlers.Sync)
|
add parser for clean subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("clean", help="clean local caches", description="clear local caches",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true")
|
||||||
|
parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true")
|
||||||
|
parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true")
|
||||||
|
parser.add_argument("--no-manual", help="do not clear directory with manually added packages", action="store_true")
|
||||||
|
parser.add_argument("--no-packages", help="do not clear directory with built packages", action="store_true")
|
||||||
|
parser.set_defaults(handler=handlers.Clean, architecture=[], no_log=True, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
update_parser = subparsers.add_parser('update', description='run updates')
|
|
||||||
update_parser.add_argument('package', help='filter check by package base', nargs='*')
|
|
||||||
update_parser.add_argument(
|
|
||||||
'--dry-run', help='just perform check for updates, same as check command', action='store_true')
|
|
||||||
update_parser.add_argument('--no-aur', help='do not check for AUR updates. Implies --no-vcs', action='store_true')
|
|
||||||
update_parser.add_argument('--no-manual', help='do not include manual updates', action='store_true')
|
|
||||||
update_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true')
|
|
||||||
update_parser.set_defaults(handler=handlers.Update)
|
|
||||||
|
|
||||||
web_parser = subparsers.add_parser('web', description='start web server')
|
def _set_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
web_parser.set_defaults(handler=handlers.Web, lock=None, no_report=True)
|
"""
|
||||||
|
add parser for config subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("config", help="dump configuration",
|
||||||
|
description="dump configuration for specified architecture",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.set_defaults(handler=handlers.Dump, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
if 'handler' not in args:
|
|
||||||
parser.print_help()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
handler: handlers.Handler = args.handler
|
def _set_init_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
status = handler.execute(args)
|
"""
|
||||||
|
add parser for init subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("init", help="create repository tree",
|
||||||
|
description="create empty repository tree. Optional command for auto architecture support",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.set_defaults(handler=handlers.Init, no_report=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
sys.exit(status)
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("key-import", help="import PGP key",
|
||||||
|
description="import PGP key from public sources to repository user",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("--key-server", help="key server for key import", default="keys.gnupg.net")
|
||||||
|
parser.add_argument("key", help="PGP key to import from public server")
|
||||||
|
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, no_report=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for rebuild subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("rebuild", help="rebuild repository", description="rebuild whole repository",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
|
||||||
|
parser.set_defaults(handler=handlers.Rebuild, architecture=[])
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for remove subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("remove", help="remove package", description="remove package",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("package", help="package name or base", nargs="+")
|
||||||
|
parser.set_defaults(handler=handlers.Remove, architecture=[])
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for report subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("report", help="generate report", description="generate report",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("target", help="target to generate report", nargs="*")
|
||||||
|
parser.set_defaults(handler=handlers.Report, architecture=[])
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for search subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("search", help="search for package", description="search for package in AUR using API")
|
||||||
|
parser.add_argument("search", help="search terms, can be specified multiple times", nargs="+")
|
||||||
|
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, no_log=True, no_report=True, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for setup subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("setup", help="initial service configuration",
|
||||||
|
description="create initial service configuration, requires root",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("--build-command", help="build command prefix", default="ahriman")
|
||||||
|
parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
|
||||||
|
type=Path, default=Path("/usr/share/devtools/pacman-extra.conf"))
|
||||||
|
parser.add_argument("--no-multilib", help="do not add multilib repository", action="store_true")
|
||||||
|
parser.add_argument("--packager", help="packager name and email", required=True)
|
||||||
|
parser.add_argument("--repository", help="repository name", required=True)
|
||||||
|
parser.add_argument("--sign-key", help="sign key id")
|
||||||
|
parser.add_argument("--sign-target", help="sign options", type=SignSettings.from_option,
|
||||||
|
choices=SignSettings, action="append")
|
||||||
|
parser.add_argument("--web-port", help="port of the web service", type=int)
|
||||||
|
parser.set_defaults(handler=handlers.Setup, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for sign subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("sign", help="sign packages", description="(re-)sign packages and repository database",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("package", help="sign only specified packages", nargs="*")
|
||||||
|
parser.set_defaults(handler=handlers.Sign, architecture=[])
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for status subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("status", help="get package status", description="request status of the package",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
|
||||||
|
parser.add_argument("package", help="filter status by package base", nargs="*")
|
||||||
|
parser.set_defaults(handler=handlers.Status, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for status update subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("status-update", help="update package status", description="request status of the package",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument(
|
||||||
|
"package",
|
||||||
|
help="set status for specified packages. If no packages supplied, service status will be updated",
|
||||||
|
nargs="*")
|
||||||
|
parser.add_argument("--status", help="new status", choices=BuildStatusEnum,
|
||||||
|
type=BuildStatusEnum, default=BuildStatusEnum.Success)
|
||||||
|
parser.add_argument("--remove", help="remove package status page", action="store_true")
|
||||||
|
parser.set_defaults(handler=handlers.StatusUpdate, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for sync subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("sync", help="sync repository", description="sync packages to remote server",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("target", help="target to sync", nargs="*")
|
||||||
|
parser.set_defaults(handler=handlers.Sync, architecture=[])
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for update subcommand
|
||||||
|
:param root: subparsers for the commands
|
||||||
|
:return: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("update", help="update packages", description="run updates",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||||
|
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
|
||||||
|
parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
|
||||||
|
parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
|
||||||
|
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
|
||||||
|
parser.set_defaults(handler=handlers.Update, architecture=[])
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("web", help="start web server", description="start web server",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.set_defaults(handler=handlers.Web, lock=None, no_report=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
"""
|
||||||
|
run application instance
|
||||||
|
"""
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args_parser = _parser()
|
||||||
|
args = args_parser.parse_args()
|
||||||
|
|
||||||
|
handler: handlers.Handler = args.handler
|
||||||
|
status = handler.execute(args)
|
||||||
|
|
||||||
|
sys.exit(status)
|
||||||
|
|
||||||
|
|
||||||
|
run()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -18,10 +18,10 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from typing import Callable, Iterable, List, Optional, Set
|
from pathlib import Path
|
||||||
|
from typing import Callable, Iterable, List, Set
|
||||||
|
|
||||||
from ahriman.core.build_tools.task import Task
|
from ahriman.core.build_tools.task import Task
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
@ -32,47 +32,49 @@ from ahriman.models.package import Package
|
|||||||
|
|
||||||
|
|
||||||
class Application:
|
class Application:
|
||||||
'''
|
"""
|
||||||
base application class
|
base application class
|
||||||
:ivar architecture: repository architecture
|
:ivar architecture: repository architecture
|
||||||
:ivar config: configuration instance
|
:ivar configuration: configuration instance
|
||||||
:ivar logger: application logger
|
:ivar logger: application logger
|
||||||
:ivar repository: repository instance
|
:ivar repository: repository instance
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
self.logger = logging.getLogger('root')
|
self.logger = logging.getLogger("root")
|
||||||
self.config = config
|
self.configuration = configuration
|
||||||
self.architecture = architecture
|
self.architecture = architecture
|
||||||
self.repository = Repository(architecture, config)
|
self.repository = Repository(architecture, configuration)
|
||||||
|
|
||||||
|
def _finalize(self, built_packages: Iterable[Package]) -> None:
|
||||||
|
"""
|
||||||
|
generate report and sync to remote server
|
||||||
|
"""
|
||||||
|
self.report([], built_packages)
|
||||||
|
self.sync([], built_packages)
|
||||||
|
|
||||||
def _known_packages(self) -> Set[str]:
|
def _known_packages(self) -> Set[str]:
|
||||||
'''
|
"""
|
||||||
load packages from repository and pacman repositories
|
load packages from repository and pacman repositories
|
||||||
:return: list of known packages
|
:return: list of known packages
|
||||||
'''
|
"""
|
||||||
known_packages: Set[str] = set()
|
known_packages: Set[str] = set()
|
||||||
# local set
|
# local set
|
||||||
for package in self.repository.packages():
|
for base in self.repository.packages():
|
||||||
known_packages.update(package.packages.keys())
|
for package, properties in base.packages.items():
|
||||||
|
known_packages.add(package)
|
||||||
|
known_packages.update(properties.provides)
|
||||||
known_packages.update(self.repository.pacman.all_packages())
|
known_packages.update(self.repository.pacman.all_packages())
|
||||||
return known_packages
|
return known_packages
|
||||||
|
|
||||||
def _finalize(self) -> None:
|
|
||||||
'''
|
|
||||||
generate report and sync to remote server
|
|
||||||
'''
|
|
||||||
self.report()
|
|
||||||
self.sync()
|
|
||||||
|
|
||||||
def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
|
def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
|
||||||
log_fn: Callable[[str], None]) -> List[Package]:
|
log_fn: Callable[[str], None]) -> List[Package]:
|
||||||
'''
|
"""
|
||||||
get list of packages to run update process
|
get list of packages to run update process
|
||||||
:param filter_packages: do not check every package just specified in the list
|
:param filter_packages: do not check every package just specified in the list
|
||||||
:param no_aur: do not check for aur updates
|
:param no_aur: do not check for aur updates
|
||||||
@ -80,7 +82,7 @@ class Application:
|
|||||||
:param no_vcs: do not check VCS packages
|
:param no_vcs: do not check VCS packages
|
||||||
:param log_fn: logger function to log updates
|
:param log_fn: logger function to log updates
|
||||||
:return: list of out-of-dated packages
|
:return: list of out-of-dated packages
|
||||||
'''
|
"""
|
||||||
updates = []
|
updates = []
|
||||||
|
|
||||||
if not no_aur:
|
if not no_aur:
|
||||||
@ -89,60 +91,60 @@ class Application:
|
|||||||
updates.extend(self.repository.updates_manual())
|
updates.extend(self.repository.updates_manual())
|
||||||
|
|
||||||
for package in updates:
|
for package in updates:
|
||||||
log_fn(f'{package.base} = {package.version}')
|
log_fn(f"{package.base} = {package.version}")
|
||||||
|
|
||||||
return updates
|
return updates
|
||||||
|
|
||||||
def add(self, names: Iterable[str], without_dependencies: bool) -> None:
|
def add(self, names: Iterable[str], without_dependencies: bool) -> None:
|
||||||
'''
|
"""
|
||||||
add packages for the next build
|
add packages for the next build
|
||||||
:param names: list of package bases to add
|
:param names: list of package bases to add
|
||||||
:param without_dependencies: if set, dependency check will be disabled
|
:param without_dependencies: if set, dependency check will be disabled
|
||||||
'''
|
"""
|
||||||
known_packages = self._known_packages()
|
known_packages = self._known_packages()
|
||||||
|
|
||||||
def add_directory(path: str) -> None:
|
def add_directory(path: Path) -> None:
|
||||||
for package in filter(package_like, os.listdir(path)):
|
for full_path in filter(package_like, path.iterdir()):
|
||||||
full_path = os.path.join(path, package)
|
add_archive(full_path)
|
||||||
add_manual(full_path)
|
|
||||||
|
|
||||||
def add_manual(name: str) -> str:
|
def add_manual(src: str) -> Path:
|
||||||
package = Package.load(name, self.repository.pacman, self.config.get('alpm', 'aur_url'))
|
package = Package.load(src, self.repository.pacman, self.configuration.get("alpm", "aur_url"))
|
||||||
path = os.path.join(self.repository.paths.manual, package.base)
|
path = self.repository.paths.manual / package.base
|
||||||
Task.fetch(path, package.git_url)
|
Task.fetch(path, package.git_url)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def add_archive(src: str) -> None:
|
def add_archive(src: Path) -> None:
|
||||||
dst = os.path.join(self.repository.paths.packages, os.path.basename(src))
|
dst = self.repository.paths.packages / src.name
|
||||||
shutil.move(src, dst)
|
shutil.move(src, dst)
|
||||||
|
|
||||||
def process_dependencies(path: str) -> None:
|
def process_dependencies(path: Path) -> None:
|
||||||
if without_dependencies:
|
if without_dependencies:
|
||||||
return
|
return
|
||||||
dependencies = Package.dependencies(path)
|
dependencies = Package.dependencies(path)
|
||||||
self.add(dependencies.difference(known_packages), without_dependencies)
|
self.add(dependencies.difference(known_packages), without_dependencies)
|
||||||
|
|
||||||
def process_single(name: str) -> None:
|
def process_single(src: str) -> None:
|
||||||
if os.path.isdir(name):
|
maybe_path = Path(src)
|
||||||
add_directory(name)
|
if maybe_path.is_dir():
|
||||||
elif os.path.isfile(name):
|
add_directory(maybe_path)
|
||||||
add_archive(name)
|
elif maybe_path.is_file():
|
||||||
|
add_archive(maybe_path)
|
||||||
else:
|
else:
|
||||||
path = add_manual(name)
|
path = add_manual(src)
|
||||||
process_dependencies(path)
|
process_dependencies(path)
|
||||||
|
|
||||||
for name in names:
|
for name in names:
|
||||||
process_single(name)
|
process_single(name)
|
||||||
|
|
||||||
def clean(self, no_build: bool, no_cache: bool, no_chroot: bool, no_manual: bool, no_packages: bool) -> None:
|
def clean(self, no_build: bool, no_cache: bool, no_chroot: bool, no_manual: bool, no_packages: bool) -> None:
|
||||||
'''
|
"""
|
||||||
run all clean methods. Warning: some functions might not be available under non-root
|
run all clean methods. Warning: some functions might not be available under non-root
|
||||||
:param no_build: do not clear directory with package sources
|
:param no_build: do not clear directory with package sources
|
||||||
:param no_cache: do not clear directory with package caches
|
:param no_cache: do not clear directory with package caches
|
||||||
:param no_chroot: do not clear build chroot
|
:param no_chroot: do not clear build chroot
|
||||||
:param no_manual: do not clear directory with manually added packages
|
:param no_manual: do not clear directory with manually added packages
|
||||||
:param no_packages: do not clear directory with built packages
|
:param no_packages: do not clear directory with built packages
|
||||||
'''
|
"""
|
||||||
if not no_build:
|
if not no_build:
|
||||||
self.repository.clear_build()
|
self.repository.clear_build()
|
||||||
if not no_cache:
|
if not no_cache:
|
||||||
@ -155,46 +157,73 @@ class Application:
|
|||||||
self.repository.clear_packages()
|
self.repository.clear_packages()
|
||||||
|
|
||||||
def remove(self, names: Iterable[str]) -> None:
|
def remove(self, names: Iterable[str]) -> None:
|
||||||
'''
|
"""
|
||||||
remove packages from repository
|
remove packages from repository
|
||||||
:param names: list of packages (either base or name) to remove
|
:param names: list of packages (either base or name) to remove
|
||||||
'''
|
"""
|
||||||
self.repository.process_remove(names)
|
self.repository.process_remove(names)
|
||||||
self._finalize()
|
self._finalize([])
|
||||||
|
|
||||||
def report(self, target: Optional[Iterable[str]] = None) -> None:
|
def report(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
|
||||||
'''
|
"""
|
||||||
generate report
|
generate report
|
||||||
:param target: list of targets to run (e.g. html)
|
:param target: list of targets to run (e.g. html)
|
||||||
'''
|
:param built_packages: list of packages which has just been built
|
||||||
|
"""
|
||||||
targets = target or None
|
targets = target or None
|
||||||
self.repository.process_report(targets)
|
self.repository.process_report(targets, built_packages)
|
||||||
|
|
||||||
def sync(self, target: Optional[Iterable[str]] = None) -> None:
|
def sign(self, packages: Iterable[str]) -> None:
|
||||||
'''
|
"""
|
||||||
|
sign packages and repository
|
||||||
|
:param packages: only sign specified packages
|
||||||
|
"""
|
||||||
|
# copy to prebuilt directory
|
||||||
|
for package in self.repository.packages():
|
||||||
|
# no one requested this package
|
||||||
|
if packages and package.base not in packages:
|
||||||
|
continue
|
||||||
|
for archive in package.packages.values():
|
||||||
|
if archive.filepath is None:
|
||||||
|
self.logger.warning("filepath is empty for %s", package.base)
|
||||||
|
continue # avoid mypy warning
|
||||||
|
src = self.repository.paths.repository / archive.filepath
|
||||||
|
dst = self.repository.paths.packages / archive.filepath
|
||||||
|
shutil.copy(src, dst)
|
||||||
|
# run generic update function
|
||||||
|
self.update([])
|
||||||
|
# sign repository database if set
|
||||||
|
self.repository.sign.sign_repository(self.repository.repo.repo_path)
|
||||||
|
self._finalize([])
|
||||||
|
|
||||||
|
def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
|
||||||
|
"""
|
||||||
sync to remote server
|
sync to remote server
|
||||||
:param target: list of targets to run (e.g. s3)
|
:param target: list of targets to run (e.g. s3)
|
||||||
'''
|
:param built_packages: list of packages which has just been built
|
||||||
|
"""
|
||||||
targets = target or None
|
targets = target or None
|
||||||
self.repository.process_sync(targets)
|
self.repository.process_sync(targets, built_packages)
|
||||||
|
|
||||||
def update(self, updates: Iterable[Package]) -> None:
|
def update(self, updates: Iterable[Package]) -> None:
|
||||||
'''
|
"""
|
||||||
run package updates
|
run package updates
|
||||||
:param updates: list of packages to update
|
:param updates: list of packages to update
|
||||||
'''
|
"""
|
||||||
def process_update(paths: Iterable[str]) -> None:
|
def process_update(paths: Iterable[Path]) -> None:
|
||||||
|
if not paths:
|
||||||
|
return # don't need to process if no update supplied
|
||||||
|
updated = [Package.load(path, self.repository.pacman, self.repository.aur_url) for path in paths]
|
||||||
self.repository.process_update(paths)
|
self.repository.process_update(paths)
|
||||||
self._finalize()
|
self._finalize(updated)
|
||||||
|
|
||||||
# process built packages
|
# process built packages
|
||||||
packages = self.repository.packages_built()
|
packages = self.repository.packages_built()
|
||||||
process_update(packages)
|
process_update(packages)
|
||||||
|
|
||||||
# process manual packages
|
# process manual packages
|
||||||
tree = Tree()
|
tree = Tree.load(updates)
|
||||||
tree.load(updates)
|
|
||||||
for num, level in enumerate(tree.levels()):
|
for num, level in enumerate(tree.levels()):
|
||||||
self.logger.info(f'processing level #{num} {[package.base for package in level]}')
|
self.logger.info("processing level #%i %s", num, [package.base for package in level])
|
||||||
packages = self.repository.process_build(level)
|
packages = self.repository.process_build(level)
|
||||||
process_update(packages)
|
process_update(packages)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -22,10 +22,16 @@ from ahriman.application.handlers.handler import Handler
|
|||||||
from ahriman.application.handlers.add import Add
|
from ahriman.application.handlers.add import Add
|
||||||
from ahriman.application.handlers.clean import Clean
|
from ahriman.application.handlers.clean import Clean
|
||||||
from ahriman.application.handlers.dump import Dump
|
from ahriman.application.handlers.dump import Dump
|
||||||
|
from ahriman.application.handlers.init import Init
|
||||||
|
from ahriman.application.handlers.key_import import KeyImport
|
||||||
from ahriman.application.handlers.rebuild import Rebuild
|
from ahriman.application.handlers.rebuild import Rebuild
|
||||||
from ahriman.application.handlers.remove import Remove
|
from ahriman.application.handlers.remove import Remove
|
||||||
from ahriman.application.handlers.report import Report
|
from ahriman.application.handlers.report import Report
|
||||||
|
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 import Status
|
||||||
|
from ahriman.application.handlers.status_update import StatusUpdate
|
||||||
from ahriman.application.handlers.sync import Sync
|
from ahriman.application.handlers.sync import Sync
|
||||||
from ahriman.application.handlers.update import Update
|
from ahriman.application.handlers.update import Update
|
||||||
from ahriman.application.handlers.web import Web
|
from ahriman.application.handlers.web import Web
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -27,16 +27,22 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
class Add(Handler):
|
class Add(Handler):
|
||||||
'''
|
"""
|
||||||
add packages handler
|
add packages handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
Application(architecture, config).add(args.package, args.without_dependencies)
|
application = Application(architecture, configuration)
|
||||||
|
application.add(args.package, args.without_dependencies)
|
||||||
|
if not args.now:
|
||||||
|
return
|
||||||
|
|
||||||
|
packages = application.get_updates(args.package, True, False, True, application.logger.info)
|
||||||
|
application.update(packages)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -27,17 +27,17 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
class Clean(Handler):
|
class Clean(Handler):
|
||||||
'''
|
"""
|
||||||
clean caches handler
|
clean caches handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
Application(architecture, config).clean(args.no_build, args.no_cache, args.no_chroot,
|
Application(architecture, configuration).clean(args.no_build, args.no_cache, args.no_chroot,
|
||||||
args.no_manual, args.no_packages)
|
args.no_manual, args.no_packages)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -26,21 +26,23 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
class Dump(Handler):
|
class Dump(Handler):
|
||||||
'''
|
"""
|
||||||
dump config handler
|
dump configuration handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
_print = print
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
config_dump = config.dump(architecture)
|
dump = configuration.dump()
|
||||||
for section, values in sorted(config_dump.items()):
|
for section, values in sorted(dump.items()):
|
||||||
print(f'[{section}]')
|
Dump._print(f"[{section}]")
|
||||||
for key, value in sorted(values.items()):
|
for key, value in sorted(values.items()):
|
||||||
print(f'{key} = {value}')
|
Dump._print(f"{key} = {value}")
|
||||||
print()
|
Dump._print()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -21,55 +21,78 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
from multiprocessing import Pool
|
|
||||||
|
|
||||||
from typing import Type
|
from multiprocessing import Pool
|
||||||
|
from typing import Set, Type
|
||||||
|
|
||||||
from ahriman.application.lock import Lock
|
from ahriman.application.lock import Lock
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import MissingArchitecture
|
||||||
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
class Handler:
|
class Handler:
|
||||||
'''
|
"""
|
||||||
base handler class for command callbacks
|
base handler class for command callbacks
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> bool:
|
def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
|
||||||
'''
|
"""
|
||||||
additional function to wrap all calls for multiprocessing library
|
additional function to wrap all calls for multiprocessing library
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
|
||||||
:return: True on success, False otherwise
|
:return: True on success, False otherwise
|
||||||
'''
|
"""
|
||||||
try:
|
try:
|
||||||
with Lock(args, architecture, config):
|
configuration = Configuration.from_path(args.configuration, architecture, not args.no_log)
|
||||||
cls.run(args, architecture, config)
|
with Lock(args, architecture, configuration):
|
||||||
|
cls.run(args, architecture, configuration)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.getLogger('root').exception('process exception', exc_info=True)
|
logging.getLogger("root").exception("process exception")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls: Type[Handler], args: argparse.Namespace) -> int:
|
def execute(cls: Type[Handler], args: argparse.Namespace) -> int:
|
||||||
'''
|
"""
|
||||||
execute function for all aru
|
execute function for all aru
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:return: 0 on success, 1 otherwise
|
:return: 0 on success, 1 otherwise
|
||||||
'''
|
"""
|
||||||
configuration = Configuration.from_path(args.config, not args.no_log)
|
architectures = cls.extract_architectures(args)
|
||||||
with Pool(len(args.architecture)) as pool:
|
with Pool(len(architectures)) as pool:
|
||||||
result = pool.starmap(
|
result = pool.starmap(
|
||||||
cls._call, [(args, architecture, configuration) for architecture in args.architecture])
|
cls._call, [(args, architecture) for architecture in architectures])
|
||||||
return 0 if all(result) else 1
|
return 0 if all(result) else 1
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def extract_architectures(cls: Type[Handler], args: argparse.Namespace) -> Set[str]:
|
||||||
'''
|
"""
|
||||||
|
get known architectures
|
||||||
|
:param args: command line args
|
||||||
|
:return: list of architectures for which tree is created
|
||||||
|
"""
|
||||||
|
if args.architecture is None:
|
||||||
|
raise MissingArchitecture(args.command)
|
||||||
|
if args.architecture:
|
||||||
|
return set(args.architecture)
|
||||||
|
|
||||||
|
config = Configuration()
|
||||||
|
config.load(args.configuration)
|
||||||
|
root = config.getpath("repository", "root")
|
||||||
|
architectures = RepositoryPaths.known_architectures(root)
|
||||||
|
|
||||||
|
if not architectures:
|
||||||
|
raise MissingArchitecture(args.command)
|
||||||
|
return architectures
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
42
src/ahriman/application/handlers/init.py
Normal file
42
src/ahriman/application/handlers/init.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
class Init(Handler):
|
||||||
|
"""
|
||||||
|
repository init handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
:param args: command line args
|
||||||
|
:param architecture: repository architecture
|
||||||
|
:param configuration: configuration instance
|
||||||
|
"""
|
||||||
|
Application(architecture, configuration).repository.repo.init()
|
42
src/ahriman/application/handlers/key_import.py
Normal file
42
src/ahriman/application/handlers/key_import.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
class KeyImport(Handler):
|
||||||
|
"""
|
||||||
|
key import packages handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
:param args: command line args
|
||||||
|
:param architecture: repository architecture
|
||||||
|
:param configuration: configuration instance
|
||||||
|
"""
|
||||||
|
Application(architecture, configuration).repository.sign.import_key(args.key_server, args.key)
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -27,18 +27,24 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
class Rebuild(Handler):
|
class Rebuild(Handler):
|
||||||
'''
|
"""
|
||||||
make world handler
|
make world handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
application = Application(architecture, config)
|
depends_on = set(args.depends_on) if args.depends_on else None
|
||||||
packages = application.repository.packages()
|
|
||||||
|
application = Application(architecture, configuration)
|
||||||
|
packages = [
|
||||||
|
package
|
||||||
|
for package in application.repository.packages()
|
||||||
|
if depends_on is None or depends_on.intersection(package.depends)
|
||||||
|
] # we have to use explicit list here for testing purpose
|
||||||
application.update(packages)
|
application.update(packages)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -27,16 +27,16 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
class Remove(Handler):
|
class Remove(Handler):
|
||||||
'''
|
"""
|
||||||
remove packages handler
|
remove packages handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
Application(architecture, config).remove(args.package)
|
Application(architecture, configuration).remove(args.package)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -27,16 +27,16 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
class Report(Handler):
|
class Report(Handler):
|
||||||
'''
|
"""
|
||||||
generate report handler
|
generate report handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
Application(architecture, config).report(args.target)
|
Application(architecture, configuration).report(args.target, [])
|
||||||
|
58
src/ahriman/application/handlers/search.py
Normal file
58
src/ahriman/application/handlers/search.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import aur # type: ignore
|
||||||
|
|
||||||
|
from typing import Callable, Type
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
class Search(Handler):
|
||||||
|
"""
|
||||||
|
packages search handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
:param args: command line args
|
||||||
|
:param architecture: repository architecture
|
||||||
|
:param configuration: configuration instance
|
||||||
|
"""
|
||||||
|
search = " ".join(args.search)
|
||||||
|
packages = aur.search(search)
|
||||||
|
|
||||||
|
# it actually always should return string
|
||||||
|
# explicit cast to string just to avoid mypy warning for untyped library
|
||||||
|
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
|
||||||
|
for package in sorted(packages, key=comparator):
|
||||||
|
Search.log_fn(package)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_fn(package: aur.Package) -> None:
|
||||||
|
"""
|
||||||
|
log package information
|
||||||
|
:param package: package object as from AUR
|
||||||
|
"""
|
||||||
|
print(f"=> {package.package_base} {package.version}")
|
||||||
|
print(f" {package.description}")
|
173
src/ahriman/application/handlers/setup.py
Normal file
173
src/ahriman/application/handlers/setup.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
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.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
|
||||||
|
"""
|
||||||
|
|
||||||
|
ARCHBUILD_COMMAND_PATH = Path("/usr/bin/archbuild")
|
||||||
|
BIN_DIR_PATH = Path("/usr/local/bin")
|
||||||
|
MIRRORLIST_PATH = Path("/etc/pacman.d/mirrorlist")
|
||||||
|
SUDOERS_PATH = Path("/etc/sudoers.d/ahriman")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
:param args: command line args
|
||||||
|
:param architecture: repository architecture
|
||||||
|
:param configuration: configuration instance
|
||||||
|
"""
|
||||||
|
application = Application(architecture, configuration)
|
||||||
|
Setup.create_makepkg_configuration(args.packager, application.repository.paths)
|
||||||
|
Setup.create_executable(args.build_command, architecture)
|
||||||
|
Setup.create_devtools_configuration(args.build_command, architecture, args.from_configuration,
|
||||||
|
args.no_multilib, args.repository, application.repository.paths)
|
||||||
|
Setup.create_ahriman_configuration(args, architecture, args.repository, configuration.include)
|
||||||
|
Setup.create_sudo_configuration(args.build_command, architecture)
|
||||||
|
|
||||||
|
@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
|
||||||
|
"""
|
||||||
|
return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_ahriman_configuration(args: argparse.Namespace, architecture: str, repository: str,
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
configuration = configparser.ConfigParser()
|
||||||
|
|
||||||
|
section = Configuration.section_name("build", architecture)
|
||||||
|
configuration.add_section(section)
|
||||||
|
configuration.set(section, "build_command", str(Setup.build_command(args.build_command, architecture)))
|
||||||
|
|
||||||
|
configuration.add_section("repository")
|
||||||
|
configuration.set("repository", "name", repository)
|
||||||
|
|
||||||
|
if args.sign_key is not None:
|
||||||
|
section = Configuration.section_name("sign", architecture)
|
||||||
|
configuration.add_section(section)
|
||||||
|
configuration.set(section, "target", " ".join([target.name.lower() for target in args.sign_target]))
|
||||||
|
configuration.set(section, "key", args.sign_key)
|
||||||
|
|
||||||
|
if args.web_port is not None:
|
||||||
|
section = Configuration.section_name("web", architecture)
|
||||||
|
configuration.add_section(section)
|
||||||
|
configuration.set(section, "port", str(args.web_port))
|
||||||
|
|
||||||
|
target = include_path / "setup-overrides.ini"
|
||||||
|
with target.open("w") as ahriman_configuration:
|
||||||
|
configuration.write(ahriman_configuration)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_devtools_configuration(prefix: str, architecture: str, source: Path,
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
configuration = configparser.ConfigParser()
|
||||||
|
# preserve case
|
||||||
|
# stupid mypy thinks that it is impossible
|
||||||
|
configuration.optionxform = lambda key: key # type: ignore
|
||||||
|
|
||||||
|
# load default configuration first
|
||||||
|
# we cannot use Include here because it will be copied to new chroot, thus no includes there
|
||||||
|
configuration.read(source)
|
||||||
|
|
||||||
|
# set our architecture now
|
||||||
|
configuration.set("options", "Architecture", architecture)
|
||||||
|
|
||||||
|
# add multilib
|
||||||
|
if not no_multilib:
|
||||||
|
configuration.add_section("multilib")
|
||||||
|
configuration.set("multilib", "Include", str(Setup.MIRRORLIST_PATH))
|
||||||
|
|
||||||
|
# add repository itself
|
||||||
|
configuration.add_section(repository)
|
||||||
|
configuration.set(repository, "SigLevel", "Optional TrustAll") # we don't care
|
||||||
|
configuration.set(repository, "Server", f"file://{paths.repository}")
|
||||||
|
|
||||||
|
target = source.parent / f"pacman-{prefix}-{architecture}.conf"
|
||||||
|
with target.open("w") as devtools_configuration:
|
||||||
|
configuration.write(devtools_configuration)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_makepkg_configuration(packager: str, paths: RepositoryPaths) -> None:
|
||||||
|
"""
|
||||||
|
create configuration for makepkg
|
||||||
|
:param packager: packager identifier (e.g. name, email)
|
||||||
|
:param paths: repository paths instance
|
||||||
|
"""
|
||||||
|
(paths.root / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_sudo_configuration(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
|
||||||
|
"""
|
||||||
|
command = Setup.build_command(prefix, architecture)
|
||||||
|
Setup.SUDOERS_PATH.write_text(f"ahriman ALL=(ALL) NOPASSWD: {command} *\n")
|
||||||
|
Setup.SUDOERS_PATH.chmod(0o400) # security!
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_executable(prefix: str, architecture: str) -> None:
|
||||||
|
"""
|
||||||
|
create executable for the service
|
||||||
|
:param prefix: command prefix in {prefix}-{architecture}-build
|
||||||
|
:param architecture: repository architecture
|
||||||
|
"""
|
||||||
|
command = Setup.build_command(prefix, architecture)
|
||||||
|
command.unlink(missing_ok=True)
|
||||||
|
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,21 +17,26 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from dataclasses import dataclass
|
import argparse
|
||||||
from typing import Optional
|
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class Sign(Handler):
|
||||||
class PackageDescription:
|
"""
|
||||||
'''
|
(re-)sign handler
|
||||||
package specific properties
|
"""
|
||||||
:ivar archive_size: package archive size
|
|
||||||
:ivar build_date: package build date
|
|
||||||
:ivar filename: package archive name
|
|
||||||
:ivar installed_size: package installed size
|
|
||||||
'''
|
|
||||||
|
|
||||||
archive_size: Optional[int] = None
|
@classmethod
|
||||||
build_date: Optional[int] = None
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
filename: Optional[str] = None
|
"""
|
||||||
installed_size: Optional[int] = None
|
callback for command line
|
||||||
|
:param args: command line args
|
||||||
|
:param architecture: repository architecture
|
||||||
|
:param configuration: configuration instance
|
||||||
|
"""
|
||||||
|
Application(architecture, configuration).sign(args.package)
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -29,19 +29,19 @@ from ahriman.models.package import Package
|
|||||||
|
|
||||||
|
|
||||||
class Status(Handler):
|
class Status(Handler):
|
||||||
'''
|
"""
|
||||||
package status handler
|
package status handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
application = Application(architecture, config)
|
application = Application(architecture, configuration)
|
||||||
if args.ahriman:
|
if args.ahriman:
|
||||||
ahriman = application.repository.reporter.get_self()
|
ahriman = application.repository.reporter.get_self()
|
||||||
print(ahriman.pretty_print())
|
print(ahriman.pretty_print())
|
||||||
@ -54,5 +54,5 @@ class Status(Handler):
|
|||||||
packages = application.repository.reporter.get(None)
|
packages = application.repository.reporter.get(None)
|
||||||
for package, package_status in sorted(packages, key=lambda item: item[0].base):
|
for package, package_status in sorted(packages, key=lambda item: item[0].base):
|
||||||
print(package.pretty_print())
|
print(package.pretty_print())
|
||||||
print(f'\t{package.version}')
|
print(f"\t{package.version}")
|
||||||
print(f'\t{package_status.pretty_print()}')
|
print(f"\t{package_status.pretty_print()}")
|
||||||
|
50
src/ahriman/application/handlers/status_update.py
Normal file
50
src/ahriman/application/handlers/status_update.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from typing import Callable, Type
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
class StatusUpdate(Handler):
|
||||||
|
"""
|
||||||
|
status update handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
:param args: command line args
|
||||||
|
:param architecture: repository architecture
|
||||||
|
:param configuration: configuration instance
|
||||||
|
"""
|
||||||
|
client = Application(architecture, configuration).repository.reporter
|
||||||
|
callback: Callable[[str], None] = lambda p: client.remove(p) if args.remove else client.update(p, args.status)
|
||||||
|
if args.package:
|
||||||
|
# update packages statuses
|
||||||
|
for package in args.package:
|
||||||
|
callback(package)
|
||||||
|
else:
|
||||||
|
# update service status
|
||||||
|
client.update_self(args.status)
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -27,16 +27,16 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
class Sync(Handler):
|
class Sync(Handler):
|
||||||
'''
|
"""
|
||||||
remove sync handler
|
remove sync handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
Application(architecture, config).sync(args.target)
|
Application(architecture, configuration).sync(args.target, [])
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -19,7 +19,7 @@
|
|||||||
#
|
#
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from typing import Type
|
from typing import Callable, Type
|
||||||
|
|
||||||
from ahriman.application.application import Application
|
from ahriman.application.application import Application
|
||||||
from ahriman.application.handlers.handler import Handler
|
from ahriman.application.handlers.handler import Handler
|
||||||
@ -27,25 +27,34 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
class Update(Handler):
|
class Update(Handler):
|
||||||
'''
|
"""
|
||||||
package update handler
|
package update handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
# typing workaround
|
application = Application(architecture, configuration)
|
||||||
def log_fn(line: str) -> None:
|
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
|
||||||
return print(line) if args.dry_run else application.logger.info(line)
|
Update.log_fn(application, args.dry_run))
|
||||||
|
|
||||||
application = Application(architecture, config)
|
|
||||||
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
return
|
return
|
||||||
|
|
||||||
application.update(packages)
|
application.update(packages)
|
||||||
|
|
||||||
|
@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
|
||||||
|
"""
|
||||||
|
def inner(line: str) -> None:
|
||||||
|
return print(line) if dry_run else application.logger.info(line)
|
||||||
|
return inner
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -26,18 +26,18 @@ from ahriman.core.configuration import Configuration
|
|||||||
|
|
||||||
|
|
||||||
class Web(Handler):
|
class Web(Handler):
|
||||||
'''
|
"""
|
||||||
web server handler
|
web server handler
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
callback for command line
|
callback for command line
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
from ahriman.web.web import run_server, setup_service
|
from ahriman.web.web import run_server, setup_service
|
||||||
application = setup_service(architecture, config)
|
application = setup_service(architecture, configuration)
|
||||||
run_server(application, architecture)
|
run_server(application)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -20,106 +20,111 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Literal, Optional, Type
|
from typing import Literal, Optional, Type
|
||||||
|
|
||||||
|
from ahriman import version
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
||||||
from ahriman.core.watcher.client import Client
|
from ahriman.core.status.client import Client
|
||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
|
||||||
|
|
||||||
class Lock:
|
class Lock:
|
||||||
'''
|
"""
|
||||||
wrapper for application lock file
|
wrapper for application lock file
|
||||||
:ivar force: remove lock file on start if any
|
:ivar force: remove lock file on start if any
|
||||||
:ivar path: path to lock file if any
|
:ivar path: path to lock file if any
|
||||||
:ivar reporter: build status reporter instance
|
:ivar reporter: build status reporter instance
|
||||||
:ivar root: repository root (i.e. ahriman home)
|
:ivar root: repository root (i.e. ahriman home)
|
||||||
:ivar unsafe: skip user check
|
:ivar unsafe: skip user check
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
def __init__(self, args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param args: command line args
|
:param args: command line args
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
self.path = f'{args.lock}_{architecture}' if args.lock is not None else None
|
self.path = Path(f"{args.lock}_{architecture}") if args.lock is not None else None
|
||||||
self.force = args.force
|
self.force = args.force
|
||||||
self.unsafe = args.unsafe
|
self.unsafe = args.unsafe
|
||||||
|
|
||||||
self.root = config.get('repository', 'root')
|
self.root = Path(configuration.get("repository", "root"))
|
||||||
self.reporter = Client() if args.no_report else Client.load(architecture, config)
|
self.reporter = Client() if args.no_report else Client.load(configuration)
|
||||||
|
|
||||||
def __enter__(self) -> Lock:
|
def __enter__(self) -> Lock:
|
||||||
'''
|
"""
|
||||||
default workflow is the following:
|
default workflow is the following:
|
||||||
|
|
||||||
check user UID
|
check user UID
|
||||||
remove lock file if force flag is set
|
|
||||||
check if there is lock file
|
check if there is lock file
|
||||||
|
check web status watcher status
|
||||||
create lock file
|
create lock file
|
||||||
report to web if enabled
|
report to web if enabled
|
||||||
'''
|
"""
|
||||||
self.check_user()
|
self.check_user()
|
||||||
if self.force:
|
self.check_version()
|
||||||
self.remove()
|
|
||||||
self.check()
|
|
||||||
self.create()
|
self.create()
|
||||||
self.reporter.update_self(BuildStatusEnum.Building)
|
self.reporter.update_self(BuildStatusEnum.Building)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type: Optional[Type[Exception]], exc_val: Optional[Exception],
|
def __exit__(self, exc_type: Optional[Type[Exception]], exc_val: Optional[Exception],
|
||||||
exc_tb: TracebackType) -> Literal[False]:
|
exc_tb: TracebackType) -> Literal[False]:
|
||||||
'''
|
"""
|
||||||
remove lock file when done
|
remove lock file when done
|
||||||
:param exc_type: exception type name if any
|
:param exc_type: exception type name if any
|
||||||
:param exc_val: exception raised if any
|
:param exc_val: exception raised if any
|
||||||
:param exc_tb: exception traceback if any
|
:param exc_tb: exception traceback if any
|
||||||
:return: always False (do not suppress any exception)
|
:return: always False (do not suppress any exception)
|
||||||
'''
|
"""
|
||||||
self.remove()
|
self.clear()
|
||||||
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
||||||
self.reporter.update_self(status)
|
self.reporter.update_self(status)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check(self) -> None:
|
def check_version(self) -> None:
|
||||||
'''
|
"""
|
||||||
check if lock file exists, raise exception if it does
|
check web server version
|
||||||
'''
|
"""
|
||||||
if self.path is None:
|
status = self.reporter.get_internal()
|
||||||
return
|
if status.version is not None and status.version != version.__version__:
|
||||||
if os.path.exists(self.path):
|
logging.getLogger("root").warning(
|
||||||
raise DuplicateRun()
|
"status watcher version mismatch, our %s, their %s",
|
||||||
|
version.__version__,
|
||||||
|
status.version)
|
||||||
|
|
||||||
def check_user(self) -> None:
|
def check_user(self) -> None:
|
||||||
'''
|
"""
|
||||||
check if current user is actually owner of ahriman root
|
check if current user is actually owner of ahriman root
|
||||||
'''
|
"""
|
||||||
if self.unsafe:
|
if self.unsafe:
|
||||||
return
|
return
|
||||||
current_uid = os.getuid()
|
current_uid = os.getuid()
|
||||||
root_uid = os.stat(self.root).st_uid
|
root_uid = self.root.stat().st_uid
|
||||||
if current_uid != root_uid:
|
if current_uid != root_uid:
|
||||||
raise UnsafeRun(current_uid, root_uid)
|
raise UnsafeRun(current_uid, root_uid)
|
||||||
|
|
||||||
def create(self) -> None:
|
def clear(self) -> None:
|
||||||
'''
|
"""
|
||||||
create lock file
|
|
||||||
'''
|
|
||||||
if self.path is None:
|
|
||||||
return
|
|
||||||
open(self.path, 'w').close()
|
|
||||||
|
|
||||||
def remove(self) -> None:
|
|
||||||
'''
|
|
||||||
remove lock file
|
remove lock file
|
||||||
'''
|
"""
|
||||||
if self.path is None:
|
if self.path is None:
|
||||||
return
|
return
|
||||||
if os.path.exists(self.path):
|
self.path.unlink(missing_ok=True)
|
||||||
os.remove(self.path)
|
|
||||||
|
def create(self) -> None:
|
||||||
|
"""
|
||||||
|
create lock file
|
||||||
|
"""
|
||||||
|
if self.path is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.path.touch(exist_ok=self.force)
|
||||||
|
except FileExistsError:
|
||||||
|
raise DuplicateRun()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -18,35 +18,37 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from pyalpm import Handle # type: ignore
|
from pyalpm import Handle # type: ignore
|
||||||
from typing import List, Set
|
from typing import Set
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
class Pacman:
|
class Pacman:
|
||||||
'''
|
"""
|
||||||
alpm wrapper
|
alpm wrapper
|
||||||
:ivar handle: pyalpm root `Handle`
|
:ivar handle: pyalpm root `Handle`
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, config: Configuration) -> None:
|
def __init__(self, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
root = config.get('alpm', 'root')
|
root = configuration.get("alpm", "root")
|
||||||
pacman_root = config.get('alpm', 'database')
|
pacman_root = configuration.getpath("alpm", "database")
|
||||||
self.handle = Handle(root, pacman_root)
|
self.handle = Handle(root, str(pacman_root))
|
||||||
for repository in config.getlist('alpm', 'repositories'):
|
for repository in configuration.getlist("alpm", "repositories"):
|
||||||
self.handle.register_syncdb(repository, 0) # 0 is pgp_level
|
self.handle.register_syncdb(repository, 0) # 0 is pgp_level
|
||||||
|
|
||||||
def all_packages(self) -> List[str]:
|
def all_packages(self) -> Set[str]:
|
||||||
'''
|
"""
|
||||||
get list of packages known for alpm
|
get list of packages known for alpm
|
||||||
:return: list of package names
|
:return: list of package names
|
||||||
'''
|
"""
|
||||||
result: Set[str] = set()
|
result: Set[str] = set()
|
||||||
for database in self.handle.get_syncdbs():
|
for database in self.handle.get_syncdbs():
|
||||||
result.update({package.name for package in database.pkgcache})
|
for package in database.pkgcache:
|
||||||
|
result.add(package.name) # package itself
|
||||||
|
result.update(package.provides) # provides list for meta-packages
|
||||||
|
|
||||||
return list(result)
|
return result
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -18,8 +18,8 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ahriman.core.exceptions import BuildFailed
|
from ahriman.core.exceptions import BuildFailed
|
||||||
@ -28,56 +28,69 @@ from ahriman.models.repository_paths import RepositoryPaths
|
|||||||
|
|
||||||
|
|
||||||
class Repo:
|
class Repo:
|
||||||
'''
|
"""
|
||||||
repo-add and repo-remove wrapper
|
repo-add and repo-remove wrapper
|
||||||
:ivar logger: class logger
|
:ivar logger: class logger
|
||||||
:ivar name: repository name
|
:ivar name: repository name
|
||||||
:ivar paths: repository paths instance
|
:ivar paths: repository paths instance
|
||||||
:ivar sign_args: additional args which have to be used to sign repository archive
|
:ivar sign_args: additional args which have to be used to sign repository archive
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
_check_output = check_output
|
||||||
|
|
||||||
def __init__(self, name: str, paths: RepositoryPaths, sign_args: List[str]) -> None:
|
def __init__(self, name: str, paths: RepositoryPaths, sign_args: List[str]) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param name: repository name
|
:param name: repository name
|
||||||
:param paths: repository paths instance
|
:param paths: repository paths instance
|
||||||
:param sign_args: additional args which have to be used to sign repository archive
|
:param sign_args: additional args which have to be used to sign repository archive
|
||||||
'''
|
"""
|
||||||
self.logger = logging.getLogger('build_details')
|
self.logger = logging.getLogger("build_details")
|
||||||
self.name = name
|
self.name = name
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
self.sign_args = sign_args
|
self.sign_args = sign_args
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def repo_path(self) -> str:
|
def repo_path(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: path to repository database
|
:return: path to repository database
|
||||||
'''
|
"""
|
||||||
return os.path.join(self.paths.repository, f'{self.name}.db.tar.gz')
|
return self.paths.repository / f"{self.name}.db.tar.gz"
|
||||||
|
|
||||||
def add(self, path: str) -> None:
|
def add(self, path: Path) -> None:
|
||||||
'''
|
"""
|
||||||
add new package to repository
|
add new package to repository
|
||||||
:param path: path to archive to add
|
:param path: path to archive to add
|
||||||
'''
|
"""
|
||||||
check_output(
|
Repo._check_output(
|
||||||
'repo-add', *self.sign_args, '-R', self.repo_path, path,
|
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
|
||||||
exception=BuildFailed(path),
|
exception=BuildFailed(path.name),
|
||||||
cwd=self.paths.repository,
|
cwd=self.paths.repository,
|
||||||
logger=self.logger)
|
logger=self.logger)
|
||||||
|
|
||||||
def remove(self, package: str) -> None:
|
def init(self) -> None:
|
||||||
'''
|
"""
|
||||||
|
create empty repository database
|
||||||
|
"""
|
||||||
|
Repo._check_output(
|
||||||
|
"repo-add", *self.sign_args, str(self.repo_path),
|
||||||
|
exception=None,
|
||||||
|
cwd=self.paths.repository,
|
||||||
|
logger=self.logger)
|
||||||
|
|
||||||
|
def remove(self, package: str, filename: Path) -> None:
|
||||||
|
"""
|
||||||
remove package from repository
|
remove package from repository
|
||||||
:param package: package name to remove
|
:param package: package name to remove
|
||||||
'''
|
:param filename: package filename to remove
|
||||||
|
"""
|
||||||
# remove package and signature (if any) from filesystem
|
# remove package and signature (if any) from filesystem
|
||||||
for fn in filter(lambda f: f.startswith(package), os.listdir(self.paths.repository)):
|
for full_path in self.paths.repository.glob(f"{filename}*"):
|
||||||
full_path = os.path.join(self.paths.repository, fn)
|
full_path.unlink()
|
||||||
os.remove(full_path)
|
|
||||||
# remove package from registry
|
# remove package from registry
|
||||||
check_output(
|
Repo._check_output(
|
||||||
'repo-remove', *self.sign_args, self.repo_path, package,
|
"repo-remove", *self.sign_args, str(self.repo_path), package,
|
||||||
exception=BuildFailed(package),
|
exception=BuildFailed(package),
|
||||||
cwd=self.paths.repository,
|
cwd=self.paths.repository,
|
||||||
logger=self.logger)
|
logger=self.logger)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,10 +17,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
@ -31,94 +31,96 @@ from ahriman.models.repository_paths import RepositoryPaths
|
|||||||
|
|
||||||
|
|
||||||
class Task:
|
class Task:
|
||||||
'''
|
"""
|
||||||
base package build task
|
base package build task
|
||||||
:ivar build_logger: logger for build process
|
:ivar build_logger: logger for build process
|
||||||
:ivar logger: class logger
|
:ivar logger: class logger
|
||||||
:ivar package: package definitions
|
:ivar package: package definitions
|
||||||
:ivar paths: repository paths instance
|
:ivar paths: repository paths instance
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, package: Package, architecture: str, config: Configuration, paths: RepositoryPaths) -> None:
|
_check_output = check_output
|
||||||
'''
|
|
||||||
|
def __init__(self, package: Package, configuration: Configuration, paths: RepositoryPaths) -> None:
|
||||||
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param package: package definitions
|
:param package: package definitions
|
||||||
:param architecture: repository architecture
|
:param configuration: configuration instance
|
||||||
:param config: configuration instance
|
|
||||||
:param paths: repository paths instance
|
:param paths: repository paths instance
|
||||||
'''
|
"""
|
||||||
self.logger = logging.getLogger('builder')
|
self.logger = logging.getLogger("builder")
|
||||||
self.build_logger = logging.getLogger('build_details')
|
self.build_logger = logging.getLogger("build_details")
|
||||||
self.package = package
|
self.package = package
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
|
|
||||||
section = config.get_section_name('build', architecture)
|
self.archbuild_flags = configuration.getlist("build", "archbuild_flags")
|
||||||
self.archbuild_flags = config.getlist(section, 'archbuild_flags')
|
self.build_command = configuration.get("build", "build_command")
|
||||||
self.build_command = config.get(section, 'build_command')
|
self.makepkg_flags = configuration.getlist("build", "makepkg_flags")
|
||||||
self.makepkg_flags = config.getlist(section, 'makepkg_flags')
|
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags")
|
||||||
self.makechrootpkg_flags = config.getlist(section, 'makechrootpkg_flags')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cache_path(self) -> str:
|
def cache_path(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: path to cached packages
|
:return: path to cached packages
|
||||||
'''
|
"""
|
||||||
return os.path.join(self.paths.cache, self.package.base)
|
return self.paths.cache / self.package.base
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def git_path(self) -> str:
|
def git_path(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: path to clone package from git
|
:return: path to clone package from git
|
||||||
'''
|
"""
|
||||||
return os.path.join(self.paths.sources, self.package.base)
|
return self.paths.sources / self.package.base
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fetch(local: str, remote: str, branch: str = 'master') -> None:
|
def fetch(local: Path, remote: str, branch: str = "master") -> None:
|
||||||
'''
|
"""
|
||||||
either clone repository or update it to origin/`branch`
|
either clone repository or update it to origin/`branch`
|
||||||
:param local: local path to fetch
|
:param local: local path to fetch
|
||||||
:param remote: remote target (from where to fetch)
|
:param remote: remote target (from where to fetch)
|
||||||
:param branch: branch name to checkout, master by default
|
:param branch: branch name to checkout, master by default
|
||||||
'''
|
"""
|
||||||
logger = logging.getLogger('build_details')
|
logger = logging.getLogger("build_details")
|
||||||
# local directory exists and there is .git directory
|
# local directory exists and there is .git directory
|
||||||
if os.path.isdir(os.path.join(local, '.git')):
|
if (local / ".git").is_dir():
|
||||||
check_output('git', 'fetch', 'origin', branch, exception=None, cwd=local, logger=logger)
|
Task._check_output("git", "fetch", "origin", branch, exception=None, cwd=local, logger=logger)
|
||||||
else:
|
else:
|
||||||
check_output('git', 'clone', remote, local, exception=None, logger=logger)
|
Task._check_output("git", "clone", remote, str(local), exception=None, logger=logger)
|
||||||
# and now force reset to our branch
|
# and now force reset to our branch
|
||||||
check_output('git', 'reset', '--hard', f'origin/{branch}', exception=None, cwd=local, logger=logger)
|
Task._check_output("git", "checkout", "--force", branch, exception=None, cwd=local, logger=logger)
|
||||||
|
Task._check_output("git", "reset", "--hard", f"origin/{branch}", exception=None, cwd=local, logger=logger)
|
||||||
|
|
||||||
def build(self) -> List[str]:
|
def build(self) -> List[Path]:
|
||||||
'''
|
"""
|
||||||
run package build
|
run package build
|
||||||
:return: paths of produced packages
|
:return: paths of produced packages
|
||||||
'''
|
"""
|
||||||
cmd = [self.build_command, '-r', self.paths.chroot]
|
command = [self.build_command, "-r", str(self.paths.chroot)]
|
||||||
cmd.extend(self.archbuild_flags)
|
command.extend(self.archbuild_flags)
|
||||||
cmd.extend(['--'] + self.makechrootpkg_flags)
|
command.extend(["--"] + self.makechrootpkg_flags)
|
||||||
cmd.extend(['--'] + self.makepkg_flags)
|
command.extend(["--"] + self.makepkg_flags)
|
||||||
self.logger.info(f'using {cmd} for {self.package.base}')
|
self.logger.info("using %s for %s", command, self.package.base)
|
||||||
|
|
||||||
check_output(
|
Task._check_output(
|
||||||
*cmd,
|
*command,
|
||||||
exception=BuildFailed(self.package.base),
|
exception=BuildFailed(self.package.base),
|
||||||
cwd=self.git_path,
|
cwd=self.git_path,
|
||||||
logger=self.build_logger)
|
logger=self.build_logger)
|
||||||
|
|
||||||
# well it is not actually correct, but we can deal with it
|
# well it is not actually correct, but we can deal with it
|
||||||
return check_output('makepkg', '--packagelist',
|
packages = Task._check_output("makepkg", "--packagelist",
|
||||||
exception=BuildFailed(self.package.base),
|
exception=BuildFailed(self.package.base),
|
||||||
cwd=self.git_path,
|
cwd=self.git_path,
|
||||||
logger=self.build_logger).splitlines()
|
logger=self.build_logger).splitlines()
|
||||||
|
return [Path(package) for package in packages]
|
||||||
|
|
||||||
def init(self, path: Optional[str] = None) -> None:
|
def init(self, path: Optional[Path] = None) -> None:
|
||||||
'''
|
"""
|
||||||
fetch package from git
|
fetch package from git
|
||||||
:param path: optional local path to fetch. If not set default path will be used
|
:param path: optional local path to fetch. If not set default path will be used
|
||||||
'''
|
"""
|
||||||
git_path = path or self.git_path
|
git_path = path or self.git_path
|
||||||
if os.path.isdir(self.cache_path):
|
if self.cache_path.is_dir():
|
||||||
# no need to clone whole repository, just copy from cache first
|
# no need to clone whole repository, just copy from cache first
|
||||||
shutil.copytree(self.cache_path, git_path)
|
shutil.copytree(self.cache_path, git_path)
|
||||||
return Task.fetch(git_path, self.package.git_url)
|
return self.fetch(git_path, self.package.git_url)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -21,132 +21,167 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Type
|
from typing import Dict, List, Optional, Type
|
||||||
|
|
||||||
|
|
||||||
class Configuration(configparser.RawConfigParser):
|
class Configuration(configparser.RawConfigParser):
|
||||||
'''
|
"""
|
||||||
extension for built-in configuration parser
|
extension for built-in configuration parser
|
||||||
:ivar path: path to root configuration file
|
:ivar path: path to root configuration file
|
||||||
:cvar ARCHITECTURE_SPECIFIC_SECTIONS: known sections which can be architecture specific (required by dump)
|
: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_FORMAT: default log format (in case of fallback)
|
||||||
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
|
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
|
||||||
:cvar STATIC_SECTIONS: known sections which are not architecture specific (required by dump)
|
"""
|
||||||
'''
|
|
||||||
|
|
||||||
DEFAULT_LOG_FORMAT = '[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s'
|
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s"
|
||||||
DEFAULT_LOG_LEVEL = logging.DEBUG
|
DEFAULT_LOG_LEVEL = logging.DEBUG
|
||||||
|
|
||||||
STATIC_SECTIONS = ['alpm', 'report', 'repository', 'settings', 'upload']
|
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"]
|
||||||
ARCHITECTURE_SPECIFIC_SECTIONS = ['build', 'html', 'rsync', 's3', 'sign', 'web']
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor. In the most cases must not be called directly
|
default constructor. In the most cases must not be called directly
|
||||||
'''
|
"""
|
||||||
configparser.RawConfigParser.__init__(self, allow_no_value=True)
|
configparser.RawConfigParser.__init__(self, allow_no_value=True)
|
||||||
self.path: Optional[str] = None
|
self.path: Optional[Path] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def include(self) -> str:
|
def include(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: path to directory with configuration includes
|
:return: path to directory with configuration includes
|
||||||
'''
|
"""
|
||||||
return self.get('settings', 'include')
|
return self.getpath("settings", "include")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logging_path(self) -> Path:
|
||||||
|
"""
|
||||||
|
:return: path to logging configuration
|
||||||
|
"""
|
||||||
|
return self.getpath("settings", "logging")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_path(cls: Type[Configuration], path: str, logfile: bool) -> Configuration:
|
def from_path(cls: Type[Configuration], path: Path, architecture: str, logfile: bool) -> Configuration:
|
||||||
'''
|
"""
|
||||||
constructor with full object initialization
|
constructor with full object initialization
|
||||||
:param path: path to root configuration file
|
:param path: path to root configuration file
|
||||||
|
:param architecture: repository architecture
|
||||||
:param logfile: use log file to output messages
|
:param logfile: use log file to output messages
|
||||||
:return: configuration instance
|
:return: configuration instance
|
||||||
'''
|
"""
|
||||||
config = cls()
|
config = cls()
|
||||||
config.load(path)
|
config.load(path)
|
||||||
|
config.merge_sections(architecture)
|
||||||
config.load_logging(logfile)
|
config.load_logging(logfile)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def dump(self, architecture: str) -> Dict[str, Dict[str, str]]:
|
@staticmethod
|
||||||
'''
|
def section_name(section: str, architecture: str) -> str:
|
||||||
dump configuration to dictionary
|
"""
|
||||||
|
generate section name for architecture specific sections
|
||||||
|
:param section: section name
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:return: configuration dump for specific architecture
|
:return: correct section name for repository specific section
|
||||||
'''
|
"""
|
||||||
result: Dict[str, Dict[str, str]] = {}
|
return f"{section}:{architecture}"
|
||||||
for section in Configuration.STATIC_SECTIONS:
|
|
||||||
if not self.has_section(section):
|
|
||||||
continue
|
|
||||||
result[section] = dict(self[section])
|
|
||||||
for group in Configuration.ARCHITECTURE_SPECIFIC_SECTIONS:
|
|
||||||
section = self.get_section_name(group, architecture)
|
|
||||||
if not self.has_section(section):
|
|
||||||
continue
|
|
||||||
result[section] = dict(self[section])
|
|
||||||
|
|
||||||
return result
|
def dump(self) -> Dict[str, Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
dump configuration to dictionary
|
||||||
|
:return: configuration dump for specific architecture
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
section: dict(self[section])
|
||||||
|
for section in self.sections()
|
||||||
|
}
|
||||||
|
|
||||||
def getlist(self, section: str, key: str) -> List[str]:
|
def getlist(self, section: str, key: str) -> List[str]:
|
||||||
'''
|
"""
|
||||||
get space separated string list option
|
get space separated string list option
|
||||||
:param section: section name
|
:param section: section name
|
||||||
:param key: key name
|
:param key: key name
|
||||||
:return: list of string if option is set, empty list otherwise
|
:return: list of string if option is set, empty list otherwise
|
||||||
'''
|
"""
|
||||||
raw = self.get(section, key, fallback=None)
|
raw = self.get(section, key, fallback=None)
|
||||||
if not raw: # empty string or none
|
if not raw: # empty string or none
|
||||||
return []
|
return []
|
||||||
return raw.split()
|
return raw.split()
|
||||||
|
|
||||||
def get_section_name(self, prefix: str, suffix: str) -> str:
|
def getpath(self, section: str, key: str) -> Path:
|
||||||
'''
|
"""
|
||||||
check if there is `prefix`_`suffix` section and return it on success. Return `prefix` otherwise
|
helper to generate absolute configuration path for relative settings value
|
||||||
:param prefix: section name prefix
|
:param section: section name
|
||||||
:param suffix: section name suffix (e.g. architecture name)
|
:param key: key name
|
||||||
:return: found section name
|
:return: absolute path according to current path configuration
|
||||||
'''
|
"""
|
||||||
probe = f'{prefix}_{suffix}'
|
value = Path(self.get(section, key))
|
||||||
return probe if self.has_section(probe) else prefix
|
if self.path is None or value.is_absolute():
|
||||||
|
return value
|
||||||
|
return self.path.parent / value
|
||||||
|
|
||||||
def load(self, path: str) -> None:
|
def load(self, path: Path) -> None:
|
||||||
'''
|
"""
|
||||||
fully load configuration
|
fully load configuration
|
||||||
:param path: path to root configuration file
|
:param path: path to root configuration file
|
||||||
'''
|
"""
|
||||||
self.path = path
|
self.path = path
|
||||||
self.read(self.path)
|
self.read(self.path)
|
||||||
self.load_includes()
|
self.load_includes()
|
||||||
|
|
||||||
def load_includes(self) -> None:
|
def load_includes(self) -> None:
|
||||||
'''
|
"""
|
||||||
load configuration includes
|
load configuration includes
|
||||||
'''
|
"""
|
||||||
try:
|
try:
|
||||||
for conf in filter(lambda p: p.endswith('.ini'), sorted(os.listdir(self.include))):
|
for path in sorted(self.include.glob("*.ini")):
|
||||||
self.read(os.path.join(self.include, conf))
|
if path == self.logging_path:
|
||||||
except (FileNotFoundError, configparser.NoOptionError):
|
continue # we don't want to load logging explicitly
|
||||||
|
self.read(path)
|
||||||
|
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def load_logging(self, logfile: bool) -> None:
|
def load_logging(self, logfile: bool) -> None:
|
||||||
'''
|
"""
|
||||||
setup logging settings from configuration
|
setup logging settings from configuration
|
||||||
:param logfile: use log file to output messages
|
:param logfile: use log file to output messages
|
||||||
'''
|
"""
|
||||||
def file_logger() -> None:
|
def file_logger() -> None:
|
||||||
try:
|
try:
|
||||||
fileConfig(self.get('settings', 'logging'))
|
path = self.logging_path
|
||||||
except PermissionError:
|
fileConfig(path)
|
||||||
|
except (FileNotFoundError, PermissionError):
|
||||||
console_logger()
|
console_logger()
|
||||||
logging.error('could not create logfile, fallback to stderr', exc_info=True)
|
logging.exception("could not create logfile, fallback to stderr")
|
||||||
|
|
||||||
def console_logger() -> None:
|
def console_logger() -> None:
|
||||||
logging.basicConfig(filename=None, format=Configuration.DEFAULT_LOG_FORMAT,
|
logging.basicConfig(filename=None, format=self.DEFAULT_LOG_FORMAT,
|
||||||
level=Configuration.DEFAULT_LOG_LEVEL)
|
level=self.DEFAULT_LOG_LEVEL)
|
||||||
|
|
||||||
if logfile:
|
if logfile:
|
||||||
file_logger()
|
file_logger()
|
||||||
else:
|
else:
|
||||||
console_logger()
|
console_logger()
|
||||||
|
|
||||||
|
def merge_sections(self, architecture: str) -> None:
|
||||||
|
"""
|
||||||
|
merge architecture specific sections into main configuration
|
||||||
|
:param architecture: repository architecture
|
||||||
|
"""
|
||||||
|
for section in self.ARCHITECTURE_SPECIFIC_SECTIONS:
|
||||||
|
if not self.has_section(section):
|
||||||
|
self.add_section(section) # add section if not exists
|
||||||
|
# get overrides
|
||||||
|
specific = self.section_name(section, architecture)
|
||||||
|
if self.has_section(specific):
|
||||||
|
# if there is no such section it means that there is no overrides for this arch
|
||||||
|
# but we anyway will have to delete sections for others archs
|
||||||
|
for key, value in self[specific].items():
|
||||||
|
self.set(section, key, value)
|
||||||
|
# remove any arch specific section
|
||||||
|
for foreign in self.sections():
|
||||||
|
# we would like to use lambda filter here, but pylint is too dumb
|
||||||
|
if not foreign.startswith(f"{section}:"):
|
||||||
|
continue
|
||||||
|
self.remove_section(foreign)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -21,103 +21,125 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
class BuildFailed(Exception):
|
class BuildFailed(Exception):
|
||||||
'''
|
"""
|
||||||
base exception for failed builds
|
base exception for failed builds
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, package: str) -> None:
|
def __init__(self, package: str) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param package: package base raised exception
|
:param package: package base raised exception
|
||||||
'''
|
"""
|
||||||
Exception.__init__(self, f'Package {package} build failed, check logs for details')
|
Exception.__init__(self, f"Package {package} build failed, check logs for details")
|
||||||
|
|
||||||
|
|
||||||
class DuplicateRun(Exception):
|
class DuplicateRun(Exception):
|
||||||
'''
|
"""
|
||||||
exception which will be raised if there is another application instance
|
exception which will be raised if there is another application instance
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
'''
|
"""
|
||||||
Exception.__init__(self, 'Another application instance is run')
|
Exception.__init__(self, "Another application instance is run")
|
||||||
|
|
||||||
|
|
||||||
class InitializeException(Exception):
|
class InitializeException(Exception):
|
||||||
'''
|
"""
|
||||||
base service initialization exception
|
base service initialization exception
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
'''
|
"""
|
||||||
Exception.__init__(self, 'Could not load service')
|
Exception.__init__(self, "Could not load service")
|
||||||
|
|
||||||
|
|
||||||
class InvalidOption(Exception):
|
class InvalidOption(Exception):
|
||||||
'''
|
"""
|
||||||
exception which will be raised on configuration errors
|
exception which will be raised on configuration errors
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, value: Any) -> None:
|
def __init__(self, value: Any) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param value: option value
|
:param value: option value
|
||||||
'''
|
"""
|
||||||
Exception.__init__(self, f'Invalid or unknown option value `{value}`')
|
Exception.__init__(self, f"Invalid or unknown option value `{value}`")
|
||||||
|
|
||||||
|
|
||||||
class InvalidPackageInfo(Exception):
|
class InvalidPackageInfo(Exception):
|
||||||
'''
|
"""
|
||||||
exception which will be raised on package load errors
|
exception which will be raised on package load errors
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, details: Any) -> None:
|
def __init__(self, details: Any) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param details: error details
|
:param details: error details
|
||||||
'''
|
"""
|
||||||
Exception.__init__(self, f'There are errors during reading package information: `{details}`')
|
Exception.__init__(self, f"There are errors during reading package information: `{details}`")
|
||||||
|
|
||||||
|
|
||||||
|
class MissingArchitecture(Exception):
|
||||||
|
"""
|
||||||
|
exception which will be raised if architecture is required, but missing
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, command: str) -> None:
|
||||||
|
"""
|
||||||
|
default constructor
|
||||||
|
:param command: command name which throws exception
|
||||||
|
"""
|
||||||
|
Exception.__init__(self, f"Architecture required for subcommand {command}, but missing")
|
||||||
|
|
||||||
|
|
||||||
class ReportFailed(Exception):
|
class ReportFailed(Exception):
|
||||||
'''
|
"""
|
||||||
report generation exception
|
report generation exception
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
'''
|
"""
|
||||||
Exception.__init__(self, 'Report failed')
|
Exception.__init__(self, "Report failed")
|
||||||
|
|
||||||
|
|
||||||
class SyncFailed(Exception):
|
class SyncFailed(Exception):
|
||||||
'''
|
"""
|
||||||
remote synchronization exception
|
remote synchronization exception
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
'''
|
"""
|
||||||
Exception.__init__(self, 'Sync failed')
|
Exception.__init__(self, "Sync failed")
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownPackage(Exception):
|
||||||
|
"""
|
||||||
|
exception for status watcher which will be thrown on unknown package
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base: str) -> None:
|
||||||
|
Exception.__init__(self, f"Package base {base} is unknown")
|
||||||
|
|
||||||
|
|
||||||
class UnsafeRun(Exception):
|
class UnsafeRun(Exception):
|
||||||
'''
|
"""
|
||||||
exception which will be raised in case if user is not owner of repository
|
exception which will be raised in case if user is not owner of repository
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, current_uid: int, root_uid: int) -> None:
|
def __init__(self, current_uid: int, root_uid: int) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
'''
|
"""
|
||||||
Exception.__init__(
|
Exception.__init__(
|
||||||
self,
|
self,
|
||||||
f'''Current UID {current_uid} differs from root owner {root_uid}.
|
f"""Current UID {current_uid} differs from root owner {root_uid}.
|
||||||
Note that for the most actions it is unsafe to run application as different user.
|
Note that for the most actions it is unsafe to run application as different user.
|
||||||
If you are 100% sure that it must be there try --unsafe option''')
|
If you are 100% sure that it must be there try --unsafe option""")
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
105
src/ahriman/core/report/email.py
Normal file
105
src/ahriman/core/report/email.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#
|
||||||
|
# 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 datetime
|
||||||
|
import smtplib
|
||||||
|
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from typing import Dict, Iterable
|
||||||
|
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.report.jinja_template import JinjaTemplate
|
||||||
|
from ahriman.core.report.report import Report
|
||||||
|
from ahriman.core.util import pretty_datetime
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.smtp_ssl_settings import SmtpSSLSettings
|
||||||
|
|
||||||
|
|
||||||
|
class Email(Report, JinjaTemplate):
|
||||||
|
"""
|
||||||
|
email report generator
|
||||||
|
:ivar host: SMTP host to connect
|
||||||
|
:ivar no_empty_report: skip empty report generation
|
||||||
|
:ivar password: password to authenticate via SMTP
|
||||||
|
:ivar port: SMTP port to connect
|
||||||
|
:ivar receivers: list of receivers emails
|
||||||
|
:ivar sender: sender email address
|
||||||
|
:ivar ssl: SSL mode for SMTP connection
|
||||||
|
:ivar user: username to authenticate via SMTP
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
default constructor
|
||||||
|
:param architecture: repository architecture
|
||||||
|
:param configuration: configuration instance
|
||||||
|
"""
|
||||||
|
Report.__init__(self, architecture, configuration)
|
||||||
|
JinjaTemplate.__init__(self, "email", configuration)
|
||||||
|
|
||||||
|
# base smtp settings
|
||||||
|
self.host = configuration.get("email", "host")
|
||||||
|
self.no_empty_report = configuration.getboolean("email", "no_empty_report", fallback=True)
|
||||||
|
self.password = configuration.get("email", "password", fallback=None)
|
||||||
|
self.port = configuration.getint("email", "port")
|
||||||
|
self.receivers = configuration.getlist("email", "receivers")
|
||||||
|
self.sender = configuration.get("email", "sender")
|
||||||
|
self.ssl = SmtpSSLSettings.from_option(configuration.get("email", "ssl", fallback="disabled"))
|
||||||
|
self.user = configuration.get("email", "user", fallback=None)
|
||||||
|
|
||||||
|
def _send(self, text: str, attachment: Dict[str, str]) -> None:
|
||||||
|
"""
|
||||||
|
send email callback
|
||||||
|
:param text: email body text
|
||||||
|
:param attachment: map of attachment filename to attachment content
|
||||||
|
"""
|
||||||
|
message = MIMEMultipart()
|
||||||
|
message["From"] = self.sender
|
||||||
|
message["To"] = ", ".join(self.receivers)
|
||||||
|
message["Subject"] = f"{self.name} build report at {pretty_datetime(datetime.datetime.utcnow().timestamp())}"
|
||||||
|
|
||||||
|
message.attach(MIMEText(text, "html"))
|
||||||
|
for filename, content in attachment.items():
|
||||||
|
attach = MIMEText(content, "html")
|
||||||
|
attach.add_header("Content-Disposition", "attachment", filename=filename)
|
||||||
|
message.attach(attach)
|
||||||
|
|
||||||
|
if self.ssl != SmtpSSLSettings.SSL:
|
||||||
|
session = smtplib.SMTP(self.host, self.port)
|
||||||
|
if self.ssl == SmtpSSLSettings.STARTTLS:
|
||||||
|
session.starttls()
|
||||||
|
else:
|
||||||
|
session = smtplib.SMTP_SSL(self.host, self.port)
|
||||||
|
if self.user is not None and self.password is not None:
|
||||||
|
session.login(self.user, self.password)
|
||||||
|
session.sendmail(self.sender, self.receivers, message.as_string())
|
||||||
|
session.quit()
|
||||||
|
|
||||||
|
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None:
|
||||||
|
"""
|
||||||
|
generate report for the specified packages
|
||||||
|
:param packages: list of packages to generate report
|
||||||
|
:param built_packages: list of packages which has just been built
|
||||||
|
"""
|
||||||
|
if self.no_empty_report and not built_packages:
|
||||||
|
return
|
||||||
|
text = self.make_html(built_packages, False)
|
||||||
|
attachments = {"index.html": self.make_html(packages, True)}
|
||||||
|
self._send(text, attachments)
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,92 +17,36 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import jinja2
|
from typing import Iterable
|
||||||
import os
|
|
||||||
|
|
||||||
from typing import Callable, Dict, Iterable
|
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.report.jinja_template import JinjaTemplate
|
||||||
from ahriman.core.report.report import Report
|
from ahriman.core.report.report import Report
|
||||||
from ahriman.core.util import pretty_size, pretty_datetime
|
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.sign_settings import SignSettings
|
|
||||||
|
|
||||||
|
|
||||||
class HTML(Report):
|
class HTML(Report, JinjaTemplate):
|
||||||
'''
|
"""
|
||||||
html report generator
|
html report generator
|
||||||
|
|
||||||
It uses jinja2 templates for report generation, the following variables are allowed:
|
|
||||||
|
|
||||||
homepage - link to homepage, string, optional
|
|
||||||
link_path - prefix fo packages to download, string, required
|
|
||||||
has_package_signed - True in case if package sign enabled, False otherwise, required
|
|
||||||
has_repo_signed - True in case if repository database sign enabled, False otherwise, required
|
|
||||||
packages - sorted list of packages properties: archive_size, build_date, filename, installed_size, name, version. Required
|
|
||||||
pgp_key - default PGP key ID, string, optional
|
|
||||||
repository - repository name, string, required
|
|
||||||
|
|
||||||
:ivar homepage: homepage link if any (for footer)
|
|
||||||
:ivar link_path: prefix fo packages to download
|
|
||||||
:ivar name: repository name
|
|
||||||
:ivar pgp_key: default PGP key
|
|
||||||
:ivar report_path: output path to html report
|
:ivar report_path: output path to html report
|
||||||
:ivar sign_targets: targets to sign enabled in configuration
|
"""
|
||||||
:ivar tempate_path: path to directory with jinja templates
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
Report.__init__(self, architecture, config)
|
Report.__init__(self, architecture, configuration)
|
||||||
section = config.get_section_name('html', architecture)
|
JinjaTemplate.__init__(self, "html", configuration)
|
||||||
self.report_path = config.get(section, 'path')
|
|
||||||
self.link_path = config.get(section, 'link_path')
|
|
||||||
self.template_path = config.get(section, 'template_path')
|
|
||||||
|
|
||||||
# base template vars
|
self.report_path = configuration.getpath("html", "path")
|
||||||
self.homepage = config.get(section, 'homepage', fallback=None)
|
|
||||||
self.name = config.get('repository', 'name')
|
|
||||||
|
|
||||||
sign_section = config.get_section_name('sign', architecture)
|
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None:
|
||||||
self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist(sign_section, 'target')]
|
"""
|
||||||
self.pgp_key = config.get(sign_section, 'key') if self.sign_targets else None
|
|
||||||
|
|
||||||
def generate(self, packages: Iterable[Package]) -> None:
|
|
||||||
'''
|
|
||||||
generate report for the specified packages
|
generate report for the specified packages
|
||||||
:param packages: list of packages to generate report
|
:param packages: list of packages to generate report
|
||||||
'''
|
:param built_packages: list of packages which has just been built
|
||||||
# idea comes from https://stackoverflow.com/a/38642558
|
"""
|
||||||
templates_dir, template_name = os.path.split(self.template_path)
|
html = self.make_html(packages, True)
|
||||||
loader = jinja2.FileSystemLoader(searchpath=templates_dir)
|
self.report_path.write_text(html)
|
||||||
environment = jinja2.Environment(loader=loader)
|
|
||||||
template = environment.get_template(template_name)
|
|
||||||
|
|
||||||
content = [
|
|
||||||
{
|
|
||||||
'archive_size': pretty_size(properties.archive_size),
|
|
||||||
'build_date': pretty_datetime(properties.build_date),
|
|
||||||
'filename': properties.filename,
|
|
||||||
'installed_size': pretty_size(properties.installed_size),
|
|
||||||
'name': package,
|
|
||||||
'version': base.version
|
|
||||||
} for base in packages for package, properties in base.packages.items()
|
|
||||||
]
|
|
||||||
comparator: Callable[[Dict[str, str]], str] = lambda item: item['filename']
|
|
||||||
|
|
||||||
html = template.render(
|
|
||||||
homepage=self.homepage,
|
|
||||||
link_path=self.link_path,
|
|
||||||
has_package_signed=SignSettings.SignPackages in self.sign_targets,
|
|
||||||
has_repo_signed=SignSettings.SignRepository in self.sign_targets,
|
|
||||||
packages=sorted(content, key=comparator),
|
|
||||||
pgp_key=self.pgp_key,
|
|
||||||
repository=self.name)
|
|
||||||
|
|
||||||
with open(self.report_path, 'w') as out:
|
|
||||||
out.write(html)
|
|
||||||
|
117
src/ahriman/core/report/jinja_template.py
Normal file
117
src/ahriman/core/report/jinja_template.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
#
|
||||||
|
# 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 jinja2
|
||||||
|
|
||||||
|
from typing import Callable, Dict, Iterable
|
||||||
|
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.sign.gpg import GPG
|
||||||
|
from ahriman.core.util import pretty_datetime, pretty_size
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.sign_settings import SignSettings
|
||||||
|
|
||||||
|
|
||||||
|
class JinjaTemplate:
|
||||||
|
"""
|
||||||
|
jinja based report generator
|
||||||
|
|
||||||
|
It uses jinja2 templates for report generation, the following variables are allowed:
|
||||||
|
|
||||||
|
homepage - link to homepage, string, optional
|
||||||
|
link_path - prefix fo packages to download, string, required
|
||||||
|
has_package_signed - True in case if package sign enabled, False otherwise, required
|
||||||
|
has_repo_signed - True in case if repository database sign enabled, False otherwise, required
|
||||||
|
packages - sorted list of packages properties, required
|
||||||
|
* architecture, string
|
||||||
|
* archive_size, pretty printed size, string
|
||||||
|
* build_date, pretty printed datetime, string
|
||||||
|
* depends, sorted list of strings
|
||||||
|
* description, string
|
||||||
|
* filename, string,
|
||||||
|
* groups, sorted list of strings
|
||||||
|
* installed_size, pretty printed datetime, string
|
||||||
|
* licenses, sorted list of strings
|
||||||
|
* name, string
|
||||||
|
* url, string
|
||||||
|
* version, string
|
||||||
|
pgp_key - default PGP key ID, string, optional
|
||||||
|
repository - repository name, string, required
|
||||||
|
|
||||||
|
:ivar homepage: homepage link if any (for footer)
|
||||||
|
:ivar link_path: prefix fo packages to download
|
||||||
|
:ivar name: repository name
|
||||||
|
:ivar default_pgp_key: default PGP key
|
||||||
|
:ivar sign_targets: targets to sign enabled in configuration
|
||||||
|
:ivar template_path: path to directory with jinja templates
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, section: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
default constructor
|
||||||
|
:param section: settings section name
|
||||||
|
:param configuration: configuration instance
|
||||||
|
"""
|
||||||
|
self.link_path = configuration.get(section, "link_path")
|
||||||
|
self.template_path = configuration.getpath(section, "template_path")
|
||||||
|
|
||||||
|
# base template vars
|
||||||
|
self.homepage = configuration.get(section, "homepage", fallback=None)
|
||||||
|
self.name = configuration.get("repository", "name")
|
||||||
|
|
||||||
|
self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration)
|
||||||
|
|
||||||
|
def make_html(self, packages: Iterable[Package], extended_report: bool) -> str:
|
||||||
|
"""
|
||||||
|
generate report for the specified packages
|
||||||
|
:param packages: list of packages to generate report
|
||||||
|
:param extended_report: include additional blocks to the report
|
||||||
|
"""
|
||||||
|
# idea comes from https://stackoverflow.com/a/38642558
|
||||||
|
loader = jinja2.FileSystemLoader(searchpath=self.template_path.parent)
|
||||||
|
environment = jinja2.Environment(loader=loader, autoescape=True)
|
||||||
|
template = environment.get_template(self.template_path.name)
|
||||||
|
|
||||||
|
content = [
|
||||||
|
{
|
||||||
|
"architecture": properties.architecture or "",
|
||||||
|
"archive_size": pretty_size(properties.archive_size),
|
||||||
|
"build_date": pretty_datetime(properties.build_date),
|
||||||
|
"depends": properties.depends,
|
||||||
|
"description": properties.description or "",
|
||||||
|
"filename": properties.filename,
|
||||||
|
"groups": properties.groups,
|
||||||
|
"installed_size": pretty_size(properties.installed_size),
|
||||||
|
"licenses": properties.licenses,
|
||||||
|
"name": package,
|
||||||
|
"url": properties.url or "",
|
||||||
|
"version": base.version
|
||||||
|
} for base in packages for package, properties in base.packages.items()
|
||||||
|
]
|
||||||
|
comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"]
|
||||||
|
|
||||||
|
return template.render(
|
||||||
|
extended_report=extended_report,
|
||||||
|
homepage=self.homepage,
|
||||||
|
link_path=self.link_path,
|
||||||
|
has_package_signed=SignSettings.Packages in self.sign_targets,
|
||||||
|
has_repo_signed=SignSettings.Repository in self.sign_targets,
|
||||||
|
packages=sorted(content, key=comparator),
|
||||||
|
pgp_key=self.default_pgp_key,
|
||||||
|
repository=self.name)
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,9 +17,11 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import Iterable
|
from typing import Iterable, Type
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.exceptions import ReportFailed
|
from ahriman.core.exceptions import ReportFailed
|
||||||
@ -28,47 +30,56 @@ from ahriman.models.report_settings import ReportSettings
|
|||||||
|
|
||||||
|
|
||||||
class Report:
|
class Report:
|
||||||
'''
|
"""
|
||||||
base report generator
|
base report generator
|
||||||
:ivar architecture: repository architecture
|
:ivar architecture: repository architecture
|
||||||
:ivar config: configuration instance
|
:ivar configuration: configuration instance
|
||||||
:ivar logger: class logger
|
:ivar logger: class logger
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
self.logger = logging.getLogger('builder')
|
self.logger = logging.getLogger("builder")
|
||||||
self.architecture = architecture
|
self.architecture = architecture
|
||||||
self.config = config
|
self.configuration = configuration
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def run(architecture: str, config: Configuration, target: str, packages: Iterable[Package]) -> None:
|
def load(cls: Type[Report], architecture: str, configuration: Configuration, target: str) -> Report:
|
||||||
'''
|
"""
|
||||||
run report generation
|
load client from settings
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
:param target: target to generate report (e.g. html)
|
:param target: target to generate report (e.g. html)
|
||||||
:param packages: list of packages to generate report
|
:return: client according to current settings
|
||||||
'''
|
"""
|
||||||
provider = ReportSettings.from_option(target)
|
provider = ReportSettings.from_option(target)
|
||||||
if provider == ReportSettings.HTML:
|
if provider == ReportSettings.HTML:
|
||||||
from ahriman.core.report.html import HTML
|
from ahriman.core.report.html import HTML
|
||||||
report: Report = HTML(architecture, config)
|
return HTML(architecture, configuration)
|
||||||
else:
|
if provider == ReportSettings.Email:
|
||||||
report = Report(architecture, config)
|
from ahriman.core.report.email import Email
|
||||||
|
return Email(architecture, configuration)
|
||||||
|
return cls(architecture, configuration) # should never happen
|
||||||
|
|
||||||
try:
|
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None:
|
||||||
report.generate(packages)
|
"""
|
||||||
except Exception:
|
|
||||||
report.logger.exception('report generation failed', exc_info=True)
|
|
||||||
raise ReportFailed()
|
|
||||||
|
|
||||||
def generate(self, packages: Iterable[Package]) -> None:
|
|
||||||
'''
|
|
||||||
generate report for the specified packages
|
generate report for the specified packages
|
||||||
:param packages: list of packages to generate report
|
:param packages: list of packages to generate report
|
||||||
'''
|
:param built_packages: list of packages which has just been built
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None:
|
||||||
|
"""
|
||||||
|
run report generation
|
||||||
|
:param packages: list of packages to generate report
|
||||||
|
:param built_packages: list of packages which has just been built
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.generate(packages, built_packages)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("report generation failed")
|
||||||
|
raise ReportFailed()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,62 +17,62 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ahriman.core.repository.properties import Properties
|
from ahriman.core.repository.properties import Properties
|
||||||
|
|
||||||
|
|
||||||
class Cleaner(Properties):
|
class Cleaner(Properties):
|
||||||
'''
|
"""
|
||||||
trait to clean common repository objects
|
trait to clean common repository objects
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def packages_built(self) -> List[str]:
|
def packages_built(self) -> List[Path]:
|
||||||
'''
|
"""
|
||||||
get list of files in built packages directory
|
get list of files in built packages directory
|
||||||
:return: list of filenames from the directory
|
:return: list of filenames from the directory
|
||||||
'''
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def clear_build(self) -> None:
|
def clear_build(self) -> None:
|
||||||
'''
|
"""
|
||||||
clear sources directory
|
clear sources directory
|
||||||
'''
|
"""
|
||||||
self.logger.info('clear package sources directory')
|
self.logger.info("clear package sources directory")
|
||||||
for package in os.listdir(self.paths.sources):
|
for package in self.paths.sources.iterdir():
|
||||||
shutil.rmtree(os.path.join(self.paths.sources, package))
|
shutil.rmtree(package)
|
||||||
|
|
||||||
def clear_cache(self) -> None:
|
def clear_cache(self) -> None:
|
||||||
'''
|
"""
|
||||||
clear cache directory
|
clear cache directory
|
||||||
'''
|
"""
|
||||||
self.logger.info('clear packages sources cache directory')
|
self.logger.info("clear packages sources cache directory")
|
||||||
for package in os.listdir(self.paths.cache):
|
for package in self.paths.cache.iterdir():
|
||||||
shutil.rmtree(os.path.join(self.paths.cache, package))
|
shutil.rmtree(package)
|
||||||
|
|
||||||
def clear_chroot(self) -> None:
|
def clear_chroot(self) -> None:
|
||||||
'''
|
"""
|
||||||
clear cache directory. Warning: this method is architecture independent and will clear every chroot
|
clear cache directory. Warning: this method is architecture independent and will clear every chroot
|
||||||
'''
|
"""
|
||||||
self.logger.info('clear build chroot directory')
|
self.logger.info("clear build chroot directory")
|
||||||
for chroot in os.listdir(self.paths.chroot):
|
for chroot in self.paths.chroot.iterdir():
|
||||||
shutil.rmtree(os.path.join(self.paths.chroot, chroot))
|
shutil.rmtree(chroot)
|
||||||
|
|
||||||
def clear_manual(self) -> None:
|
def clear_manual(self) -> None:
|
||||||
'''
|
"""
|
||||||
clear directory with manual package updates
|
clear directory with manual package updates
|
||||||
'''
|
"""
|
||||||
self.logger.info('clear manual packages')
|
self.logger.info("clear manual packages")
|
||||||
for package in os.listdir(self.paths.manual):
|
for package in self.paths.manual.iterdir():
|
||||||
shutil.rmtree(os.path.join(self.paths.manual, package))
|
shutil.rmtree(package)
|
||||||
|
|
||||||
def clear_packages(self) -> None:
|
def clear_packages(self) -> None:
|
||||||
'''
|
"""
|
||||||
clear directory with built packages (NOT repository itself)
|
clear directory with built packages (NOT repository itself)
|
||||||
'''
|
"""
|
||||||
self.logger.info('clear built packages directory')
|
self.logger.info("clear built packages directory")
|
||||||
for package in self.packages_built():
|
for package in self.packages_built():
|
||||||
os.remove(package)
|
package.unlink()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,126 +17,140 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import Dict, Iterable, List, Optional
|
from typing import Dict, Iterable, List, Optional
|
||||||
|
|
||||||
from ahriman.core.build_tools.task import Task
|
from ahriman.core.build_tools.task import Task
|
||||||
from ahriman.core.report.report import Report
|
from ahriman.core.report.report import Report
|
||||||
from ahriman.core.repository.cleaner import Cleaner
|
from ahriman.core.repository.cleaner import Cleaner
|
||||||
from ahriman.core.upload.uploader import Uploader
|
from ahriman.core.upload.upload import Upload
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
class Executor(Cleaner):
|
class Executor(Cleaner):
|
||||||
'''
|
"""
|
||||||
trait for common repository update processes
|
trait for common repository update processes
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def packages(self) -> List[Package]:
|
def packages(self) -> List[Package]:
|
||||||
'''
|
"""
|
||||||
generate list of repository packages
|
generate list of repository packages
|
||||||
:return: list of packages properties
|
:return: list of packages properties
|
||||||
'''
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def process_build(self, updates: Iterable[Package]) -> List[str]:
|
def process_build(self, updates: Iterable[Package]) -> List[Path]:
|
||||||
'''
|
"""
|
||||||
build packages
|
build packages
|
||||||
:param updates: list of packages properties to build
|
:param updates: list of packages properties to build
|
||||||
:return: `packages_built`
|
:return: `packages_built`
|
||||||
'''
|
"""
|
||||||
def build_single(package: Package) -> None:
|
def build_single(package: Package) -> None:
|
||||||
self.reporter.set_building(package.base)
|
self.reporter.set_building(package.base)
|
||||||
task = Task(package, self.architecture, self.config, self.paths)
|
task = Task(package, self.configuration, self.paths)
|
||||||
task.init()
|
task.init()
|
||||||
built = task.build()
|
built = task.build()
|
||||||
for src in built:
|
for src in built:
|
||||||
dst = os.path.join(self.paths.packages, os.path.basename(src))
|
dst = self.paths.packages / src.name
|
||||||
shutil.move(src, dst)
|
shutil.move(src, dst)
|
||||||
|
|
||||||
for package in updates:
|
for single in updates:
|
||||||
try:
|
try:
|
||||||
build_single(package)
|
build_single(single)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.reporter.set_failed(package.base)
|
self.reporter.set_failed(single.base)
|
||||||
self.logger.exception(f'{package.base} ({self.architecture}) build exception', exc_info=True)
|
self.logger.exception("%s (%s) build exception", single.base, self.architecture)
|
||||||
continue
|
|
||||||
self.clear_build()
|
self.clear_build()
|
||||||
|
|
||||||
return self.packages_built()
|
return self.packages_built()
|
||||||
|
|
||||||
def process_remove(self, packages: Iterable[str]) -> str:
|
def process_remove(self, packages: Iterable[str]) -> Path:
|
||||||
'''
|
"""
|
||||||
remove packages from list
|
remove packages from list
|
||||||
:param packages: list of package names or bases to rmeove
|
:param packages: list of package names or bases to remove
|
||||||
:return: path to repository database
|
:return: path to repository database
|
||||||
'''
|
"""
|
||||||
def remove_single(package: str) -> None:
|
def remove_single(package: str, fn: Path) -> None:
|
||||||
try:
|
try:
|
||||||
self.repo.remove(package)
|
self.repo.remove(package, fn)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception(f'could not remove {package}', exc_info=True)
|
self.logger.exception("could not remove %s", package)
|
||||||
|
|
||||||
requested = set(packages)
|
requested = set(packages)
|
||||||
for local in self.packages():
|
for local in self.packages():
|
||||||
if local.base in packages:
|
if local.base in packages or all(package in requested for package in local.packages):
|
||||||
to_remove = set(local.packages.keys())
|
to_remove = {
|
||||||
|
package: Path(properties.filename)
|
||||||
|
for package, properties in local.packages.items()
|
||||||
|
if properties.filename is not None
|
||||||
|
}
|
||||||
self.reporter.remove(local.base) # we only update status page in case of base removal
|
self.reporter.remove(local.base) # we only update status page in case of base removal
|
||||||
elif requested.intersection(local.packages.keys()):
|
elif requested.intersection(local.packages.keys()):
|
||||||
to_remove = requested.intersection(local.packages.keys())
|
to_remove = {
|
||||||
|
package: Path(properties.filename)
|
||||||
|
for package, properties in local.packages.items()
|
||||||
|
if package in requested and properties.filename is not None
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
to_remove = set()
|
to_remove = dict()
|
||||||
for package in to_remove:
|
for package, filename in to_remove.items():
|
||||||
remove_single(package)
|
remove_single(package, filename)
|
||||||
|
|
||||||
return self.repo.repo_path
|
return self.repo.repo_path
|
||||||
|
|
||||||
def process_report(self, targets: Optional[Iterable[str]]) -> None:
|
def process_report(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None:
|
||||||
'''
|
"""
|
||||||
generate reports
|
generate reports
|
||||||
:param targets: list of targets to generate reports. Configuration option will be used if it is not set
|
:param targets: list of targets to generate reports. Configuration option will be used if it is not set
|
||||||
'''
|
:param built_packages: list of packages which has just been built
|
||||||
|
"""
|
||||||
if targets is None:
|
if targets is None:
|
||||||
targets = self.config.getlist('report', 'target')
|
targets = self.configuration.getlist("report", "target")
|
||||||
for target in targets:
|
for target in targets:
|
||||||
Report.run(self.architecture, self.config, target, self.packages())
|
runner = Report.load(self.architecture, self.configuration, target)
|
||||||
|
runner.run(self.packages(), built_packages)
|
||||||
|
|
||||||
def process_sync(self, targets: Optional[Iterable[str]]) -> None:
|
def process_sync(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None:
|
||||||
'''
|
"""
|
||||||
process synchronization to remote servers
|
process synchronization to remote servers
|
||||||
:param targets: list of targets to sync. Configuration option will be used if it is not set
|
:param targets: list of targets to sync. Configuration option will be used if it is not set
|
||||||
'''
|
:param built_packages: list of packages which has just been built
|
||||||
|
"""
|
||||||
if targets is None:
|
if targets is None:
|
||||||
targets = self.config.getlist('upload', 'target')
|
targets = self.configuration.getlist("upload", "target")
|
||||||
for target in targets:
|
for target in targets:
|
||||||
Uploader.run(self.architecture, self.config, target, self.paths.repository)
|
runner = Upload.load(self.architecture, self.configuration, target)
|
||||||
|
runner.run(self.paths.repository, built_packages)
|
||||||
|
|
||||||
def process_update(self, packages: Iterable[str]) -> str:
|
def process_update(self, packages: Iterable[Path]) -> Path:
|
||||||
'''
|
"""
|
||||||
sign packages, add them to repository and update repository database
|
sign packages, add them to repository and update repository database
|
||||||
:param packages: list of filenames to run
|
:param packages: list of filenames to run
|
||||||
:return: path to repository database
|
:return: path to repository database
|
||||||
'''
|
"""
|
||||||
def update_single(fn: Optional[str], base: str) -> None:
|
def update_single(fn: Optional[str], base: str) -> None:
|
||||||
if fn is None:
|
if fn is None:
|
||||||
self.logger.warning(f'received empty package name for base {base}')
|
self.logger.warning("received empty package name for base %s", base)
|
||||||
return # suppress type checking, it never can be none actually
|
return # suppress type checking, it never can be none actually
|
||||||
# in theory it might be NOT packages directory, but we suppose it is
|
# in theory it might be NOT packages directory, but we suppose it is
|
||||||
full_path = os.path.join(self.paths.packages, fn)
|
full_path = self.paths.packages / fn
|
||||||
files = self.sign.sign_package(full_path, base)
|
files = self.sign.sign_package(full_path, base)
|
||||||
for src in files:
|
for src in files:
|
||||||
dst = os.path.join(self.paths.repository, os.path.basename(src))
|
dst = self.paths.repository / src.name
|
||||||
shutil.move(src, dst)
|
shutil.move(src, dst)
|
||||||
package_path = os.path.join(self.paths.repository, fn)
|
package_path = self.paths.repository / fn
|
||||||
self.repo.add(package_path)
|
self.repo.add(package_path)
|
||||||
|
|
||||||
# we are iterating over bases, not single packages
|
# we are iterating over bases, not single packages
|
||||||
updates: Dict[str, Package] = {}
|
updates: Dict[str, Package] = {}
|
||||||
for fn in packages:
|
for filename in packages:
|
||||||
local = Package.load(fn, self.pacman, self.aur_url)
|
try:
|
||||||
updates.setdefault(local.base, local).packages.update(local.packages)
|
local = Package.load(filename, self.pacman, self.aur_url)
|
||||||
|
updates.setdefault(local.base, local).packages.update(local.packages)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("could not load package from %s", filename)
|
||||||
|
|
||||||
for local in updates.values():
|
for local in updates.values():
|
||||||
try:
|
try:
|
||||||
@ -145,7 +159,7 @@ class Executor(Cleaner):
|
|||||||
self.reporter.set_success(local)
|
self.reporter.set_success(local)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.reporter.set_failed(local.base)
|
self.reporter.set_failed(local.base)
|
||||||
self.logger.exception(f'could not process {local.base}', exc_info=True)
|
self.logger.exception("could not process %s", local.base)
|
||||||
self.clear_packages()
|
self.clear_packages()
|
||||||
|
|
||||||
return self.repo.repo_path
|
return self.repo.repo_path
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -23,16 +23,17 @@ from ahriman.core.alpm.pacman import Pacman
|
|||||||
from ahriman.core.alpm.repo import Repo
|
from ahriman.core.alpm.repo import Repo
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.sign.gpg import GPG
|
from ahriman.core.sign.gpg import GPG
|
||||||
from ahriman.core.watcher.client import Client
|
from ahriman.core.status.client import Client
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
class Properties:
|
class Properties:
|
||||||
'''
|
"""
|
||||||
repository internal objects holder
|
repository internal objects holder
|
||||||
:ivar architecture: repository architecture
|
:ivar architecture: repository architecture
|
||||||
:ivar aur_url: base AUR url
|
:ivar aur_url: base AUR url
|
||||||
:ivar config: configuration instance
|
:ivar configuration: configuration instance
|
||||||
|
:ivar ignore_list: package bases which will be ignored during auto updates
|
||||||
:ivar logger: class logger
|
:ivar logger: class logger
|
||||||
:ivar name: repository name
|
:ivar name: repository name
|
||||||
:ivar pacman: alpm wrapper instance
|
:ivar pacman: alpm wrapper instance
|
||||||
@ -40,20 +41,21 @@ class Properties:
|
|||||||
:ivar repo: repo commands wrapper instance
|
:ivar repo: repo commands wrapper instance
|
||||||
:ivar reporter: build status reporter instance
|
:ivar reporter: build status reporter instance
|
||||||
:ivar sign: GPG wrapper instance
|
:ivar sign: GPG wrapper instance
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
self.logger = logging.getLogger('builder')
|
self.logger = logging.getLogger("builder")
|
||||||
self.architecture = architecture
|
self.architecture = architecture
|
||||||
self.config = config
|
self.configuration = configuration
|
||||||
|
|
||||||
self.aur_url = config.get('alpm', 'aur_url')
|
self.aur_url = configuration.get("alpm", "aur_url")
|
||||||
self.name = config.get('repository', 'name')
|
self.name = configuration.get("repository", "name")
|
||||||
|
|
||||||
self.paths = RepositoryPaths(config.get('repository', 'root'), architecture)
|
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
|
||||||
self.paths.create_tree()
|
self.paths.create_tree()
|
||||||
|
|
||||||
self.pacman = Pacman(config)
|
self.ignore_list = configuration.getlist("build", "ignore_packages")
|
||||||
self.sign = GPG(architecture, config)
|
self.pacman = Pacman(configuration)
|
||||||
|
self.sign = GPG(architecture, configuration)
|
||||||
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
|
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
|
||||||
self.reporter = Client.load(architecture, config)
|
self.reporter = Client.load(configuration)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,8 +17,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import os
|
from pathlib import Path
|
||||||
|
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from ahriman.core.repository.executor import Executor
|
from ahriman.core.repository.executor import Executor
|
||||||
@ -28,34 +27,28 @@ from ahriman.models.package import Package
|
|||||||
|
|
||||||
|
|
||||||
class Repository(Executor, UpdateHandler):
|
class Repository(Executor, UpdateHandler):
|
||||||
'''
|
"""
|
||||||
base repository control class
|
base repository control class
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def packages(self) -> List[Package]:
|
def packages(self) -> List[Package]:
|
||||||
'''
|
"""
|
||||||
generate list of repository packages
|
generate list of repository packages
|
||||||
:return: list of packages properties
|
:return: list of packages properties
|
||||||
'''
|
"""
|
||||||
result: Dict[str, Package] = {}
|
result: Dict[str, Package] = {}
|
||||||
for fn in os.listdir(self.paths.repository):
|
for full_path in filter(package_like, self.paths.repository.iterdir()):
|
||||||
if not package_like(fn):
|
|
||||||
continue
|
|
||||||
full_path = os.path.join(self.paths.repository, fn)
|
|
||||||
try:
|
try:
|
||||||
local = Package.load(full_path, self.pacman, self.aur_url)
|
local = Package.load(full_path, self.pacman, self.aur_url)
|
||||||
result.setdefault(local.base, local).packages.update(local.packages)
|
result.setdefault(local.base, local).packages.update(local.packages)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception(f'could not load package from {fn}', exc_info=True)
|
self.logger.exception("could not load package from %s", full_path)
|
||||||
continue
|
continue
|
||||||
return list(result.values())
|
return list(result.values())
|
||||||
|
|
||||||
def packages_built(self) -> List[str]:
|
def packages_built(self) -> List[Path]:
|
||||||
'''
|
"""
|
||||||
get list of files in built packages directory
|
get list of files in built packages directory
|
||||||
:return: list of filenames from the directory
|
:return: list of filenames from the directory
|
||||||
'''
|
"""
|
||||||
return [
|
return list(filter(package_like, self.paths.packages.iterdir()))
|
||||||
os.path.join(self.paths.packages, fn)
|
|
||||||
for fn in os.listdir(self.paths.packages)
|
|
||||||
]
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,8 +17,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import os
|
|
||||||
|
|
||||||
from typing import Iterable, List
|
from typing import Iterable, List
|
||||||
|
|
||||||
from ahriman.core.repository.cleaner import Cleaner
|
from ahriman.core.repository.cleaner import Cleaner
|
||||||
@ -26,31 +24,28 @@ from ahriman.models.package import Package
|
|||||||
|
|
||||||
|
|
||||||
class UpdateHandler(Cleaner):
|
class UpdateHandler(Cleaner):
|
||||||
'''
|
"""
|
||||||
trait to get package update list
|
trait to get package update list
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def packages(self) -> List[Package]:
|
def packages(self) -> List[Package]:
|
||||||
'''
|
"""
|
||||||
generate list of repository packages
|
generate list of repository packages
|
||||||
:return: list of packages properties
|
:return: list of packages properties
|
||||||
'''
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def updates_aur(self, filter_packages: Iterable[str], no_vcs: bool) -> List[Package]:
|
def updates_aur(self, filter_packages: Iterable[str], no_vcs: bool) -> List[Package]:
|
||||||
'''
|
"""
|
||||||
check AUR for updates
|
check AUR for updates
|
||||||
:param filter_packages: do not check every package just specified in the list
|
:param filter_packages: do not check every package just specified in the list
|
||||||
:param no_vcs: do not check VCS packages
|
:param no_vcs: do not check VCS packages
|
||||||
:return: list of packages which are out-of-dated
|
:return: list of packages which are out-of-dated
|
||||||
'''
|
"""
|
||||||
result: List[Package] = []
|
result: List[Package] = []
|
||||||
|
|
||||||
build_section = self.config.get_section_name('build', self.architecture)
|
|
||||||
ignore_list = self.config.getlist(build_section, 'ignore_packages')
|
|
||||||
|
|
||||||
for local in self.packages():
|
for local in self.packages():
|
||||||
if local.base in ignore_list:
|
if local.base in self.ignore_list:
|
||||||
continue
|
continue
|
||||||
if local.is_vcs and no_vcs:
|
if local.is_vcs and no_vcs:
|
||||||
continue
|
continue
|
||||||
@ -64,29 +59,29 @@ class UpdateHandler(Cleaner):
|
|||||||
result.append(remote)
|
result.append(remote)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.reporter.set_failed(local.base)
|
self.reporter.set_failed(local.base)
|
||||||
self.logger.exception(f'could not load remote package {local.base}', exc_info=True)
|
self.logger.exception("could not load remote package %s", local.base)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def updates_manual(self) -> List[Package]:
|
def updates_manual(self) -> List[Package]:
|
||||||
'''
|
"""
|
||||||
check for packages for which manual update has been requested
|
check for packages for which manual update has been requested
|
||||||
:return: list of packages which are out-of-dated
|
:return: list of packages which are out-of-dated
|
||||||
'''
|
"""
|
||||||
result: List[Package] = []
|
result: List[Package] = []
|
||||||
known_bases = {package.base for package in self.packages()}
|
known_bases = {package.base for package in self.packages()}
|
||||||
|
|
||||||
for fn in os.listdir(self.paths.manual):
|
for fn in self.paths.manual.iterdir():
|
||||||
try:
|
try:
|
||||||
local = Package.load(os.path.join(self.paths.manual, fn), self.pacman, self.aur_url)
|
local = Package.load(fn, self.pacman, self.aur_url)
|
||||||
result.append(local)
|
result.append(local)
|
||||||
if local.base not in known_bases:
|
if local.base not in known_bases:
|
||||||
self.reporter.set_unknown(local)
|
self.reporter.set_unknown(local)
|
||||||
else:
|
else:
|
||||||
self.reporter.set_pending(local.base)
|
self.reporter.set_pending(local.base)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception(f'could not add package from {fn}', exc_info=True)
|
self.logger.exception("could not add package from %s", fn)
|
||||||
self.clear_manual()
|
self.clear_manual()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -18,90 +18,144 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
import os
|
import requests
|
||||||
|
|
||||||
from typing import List
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Set, Tuple
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.exceptions import BuildFailed
|
from ahriman.core.exceptions import BuildFailed
|
||||||
from ahriman.core.util import check_output
|
from ahriman.core.util import check_output, exception_response_text
|
||||||
from ahriman.models.sign_settings import SignSettings
|
from ahriman.models.sign_settings import SignSettings
|
||||||
|
|
||||||
|
|
||||||
class GPG:
|
class GPG:
|
||||||
'''
|
"""
|
||||||
gnupg wrapper
|
gnupg wrapper
|
||||||
:ivar architecture: repository architecture
|
:ivar architecture: repository architecture
|
||||||
:ivar config: configuration instance
|
:ivar configuration: configuration instance
|
||||||
:ivar default_key: default PGP key ID to use
|
:ivar default_key: default PGP key ID to use
|
||||||
:ivar logger: class logger
|
:ivar logger: class logger
|
||||||
:ivar target: list of targets to sign (repository, package etc)
|
:ivar targets: list of targets to sign (repository, package etc)
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
_check_output = check_output
|
||||||
'''
|
|
||||||
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
self.logger = logging.getLogger('build_details')
|
self.logger = logging.getLogger("build_details")
|
||||||
self.config = config
|
self.architecture = architecture
|
||||||
self.section = config.get_section_name('sign', architecture)
|
self.configuration = configuration
|
||||||
self.target = [SignSettings.from_option(opt) for opt in config.getlist(self.section, 'target')]
|
self.targets, self.default_key = self.sign_options(configuration)
|
||||||
self.default_key = config.get(self.section, 'key') if self.target else ''
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def repository_sign_args(self) -> List[str]:
|
def repository_sign_args(self) -> List[str]:
|
||||||
'''
|
"""
|
||||||
:return: command line arguments for repo-add command to sign database
|
:return: command line arguments for repo-add command to sign database
|
||||||
'''
|
"""
|
||||||
if SignSettings.SignRepository not in self.target:
|
if SignSettings.Repository not in self.targets:
|
||||||
return []
|
return []
|
||||||
return ['--sign', '--key', self.default_key]
|
if self.default_key is None:
|
||||||
|
self.logger.error("no default key set, skip repository sign")
|
||||||
|
return []
|
||||||
|
return ["--sign", "--key", self.default_key]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sign_cmd(path: str, key: str) -> List[str]:
|
def sign_command(path: Path, key: str) -> List[str]:
|
||||||
'''
|
"""
|
||||||
gpg command to run
|
gpg command to run
|
||||||
:param path: path to file to sign
|
:param path: path to file to sign
|
||||||
:param key: PGP key ID
|
:param key: PGP key ID
|
||||||
:return: gpg command with all required arguments
|
:return: gpg command with all required arguments
|
||||||
'''
|
"""
|
||||||
return ['gpg', '-u', key, '-b', path]
|
return ["gpg", "-u", key, "-b", str(path)]
|
||||||
|
|
||||||
def process(self, path: str, key: str) -> List[str]:
|
@staticmethod
|
||||||
'''
|
def sign_options(configuration: Configuration) -> Tuple[Set[SignSettings], Optional[str]]:
|
||||||
|
"""
|
||||||
|
extract default sign options from configuration
|
||||||
|
:param configuration: configuration instance
|
||||||
|
:return: tuple of sign targets and default PGP key
|
||||||
|
"""
|
||||||
|
targets = {
|
||||||
|
SignSettings.from_option(option)
|
||||||
|
for option in configuration.getlist("sign", "target")
|
||||||
|
}
|
||||||
|
default_key = configuration.get("sign", "key") if targets else None
|
||||||
|
return targets, default_key
|
||||||
|
|
||||||
|
def download_key(self, server: str, key: str) -> str:
|
||||||
|
"""
|
||||||
|
download key from public PGP server
|
||||||
|
:param server: public PGP server which will be used to download the key
|
||||||
|
:param key: key ID to download
|
||||||
|
:return: key as plain text
|
||||||
|
"""
|
||||||
|
key = key if key.startswith("0x") else f"0x{key}"
|
||||||
|
try:
|
||||||
|
response = requests.get(f"http://{server}/pks/lookup", params={
|
||||||
|
"op": "get",
|
||||||
|
"options": "mr",
|
||||||
|
"search": key
|
||||||
|
})
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
self.logger.exception("could not download key %s from %s: %s", key, server, exception_response_text(e))
|
||||||
|
raise
|
||||||
|
return response.text
|
||||||
|
|
||||||
|
def import_key(self, server: str, key: str) -> None:
|
||||||
|
"""
|
||||||
|
import key to current user and sign it locally
|
||||||
|
:param server: public PGP server which will be used to download the key
|
||||||
|
:param key: key ID to import
|
||||||
|
"""
|
||||||
|
key_body = self.download_key(server, key)
|
||||||
|
GPG._check_output("gpg", "--import", input_data=key_body, exception=None, logger=self.logger)
|
||||||
|
GPG._check_output("gpg", "--quick-lsign-key", key, exception=None, logger=self.logger)
|
||||||
|
|
||||||
|
def process(self, path: Path, key: str) -> List[Path]:
|
||||||
|
"""
|
||||||
gpg command wrapper
|
gpg command wrapper
|
||||||
:param path: path to file to sign
|
:param path: path to file to sign
|
||||||
:param key: PGP key ID
|
:param key: PGP key ID
|
||||||
:return: list of generated files including original file
|
:return: list of generated files including original file
|
||||||
'''
|
"""
|
||||||
check_output(
|
GPG._check_output(
|
||||||
*GPG.sign_cmd(path, key),
|
*GPG.sign_command(path, key),
|
||||||
exception=BuildFailed(path),
|
exception=BuildFailed(path.name),
|
||||||
cwd=os.path.dirname(path),
|
|
||||||
logger=self.logger)
|
logger=self.logger)
|
||||||
return [path, f'{path}.sig']
|
return [path, path.parent / f"{path.name}.sig"]
|
||||||
|
|
||||||
def sign_package(self, path: str, base: str) -> List[str]:
|
def sign_package(self, path: Path, base: str) -> List[Path]:
|
||||||
'''
|
"""
|
||||||
sign package if required by configuration
|
sign package if required by configuration
|
||||||
:param path: path to file to sign
|
:param path: path to file to sign
|
||||||
:param base: package base required to check for key overrides
|
:param base: package base required to check for key overrides
|
||||||
:return: list of generated files including original file
|
:return: list of generated files including original file
|
||||||
'''
|
"""
|
||||||
if SignSettings.SignPackages not in self.target:
|
if SignSettings.Packages not in self.targets:
|
||||||
|
return [path]
|
||||||
|
key = self.configuration.get("sign", f"key_{base}", fallback=self.default_key)
|
||||||
|
if key is None:
|
||||||
|
self.logger.error("no default key set, skip package %s sign", path)
|
||||||
return [path]
|
return [path]
|
||||||
key = self.config.get(self.section, f'key_{base}', fallback=self.default_key)
|
|
||||||
return self.process(path, key)
|
return self.process(path, key)
|
||||||
|
|
||||||
def sign_repository(self, path: str) -> List[str]:
|
def sign_repository(self, path: Path) -> List[Path]:
|
||||||
'''
|
"""
|
||||||
sign repository if required by configuration
|
sign repository if required by configuration
|
||||||
:note: more likely you just want to pass `repository_sign_args` to repo wrapper
|
:note: more likely you just want to pass `repository_sign_args` to repo wrapper
|
||||||
:param path: path to repository database
|
:param path: path to repository database
|
||||||
:return: list of generated files including original file
|
:return: list of generated files including original file
|
||||||
'''
|
"""
|
||||||
if SignSettings.SignRepository not in self.target:
|
if SignSettings.Repository not in self.targets:
|
||||||
|
return [path]
|
||||||
|
if self.default_key is None:
|
||||||
|
self.logger.error("no default key set, skip repository sign")
|
||||||
return [path]
|
return [path]
|
||||||
return self.process(path, self.default_key)
|
return self.process(path, self.default_key)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -19,110 +19,113 @@
|
|||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple, Type
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
'''
|
"""
|
||||||
base build status reporter client
|
base build status reporter client
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls: Type[Client], configuration: Configuration) -> Client:
|
||||||
|
"""
|
||||||
|
load client from settings
|
||||||
|
:param configuration: configuration instance
|
||||||
|
:return: client according to current settings
|
||||||
|
"""
|
||||||
|
host = configuration.get("web", "host", fallback=None)
|
||||||
|
port = configuration.getint("web", "port", fallback=None)
|
||||||
|
if host is not None and port is not None:
|
||||||
|
from ahriman.core.status.web_client import WebClient
|
||||||
|
return WebClient(host, port)
|
||||||
|
return cls()
|
||||||
|
|
||||||
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||||
'''
|
"""
|
||||||
add new package with status
|
add new package with status
|
||||||
:param package: package properties
|
:param package: package properties
|
||||||
:param status: current package build status
|
:param status: current package build status
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# pylint: disable=R0201
|
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]: # pylint: disable=no-self-use
|
||||||
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
|
"""
|
||||||
'''
|
|
||||||
get package status
|
get package status
|
||||||
:param base: package base to get
|
:param base: package base to get
|
||||||
:return: list of current package description and status if it has been found
|
:return: list of current package description and status if it has been found
|
||||||
'''
|
"""
|
||||||
del base
|
del base
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# pylint: disable=R0201
|
def get_internal(self) -> InternalStatus: # pylint: disable=no-self-use
|
||||||
def get_self(self) -> BuildStatus:
|
"""
|
||||||
'''
|
get internal service status
|
||||||
|
:return: current internal (web) service status
|
||||||
|
"""
|
||||||
|
return InternalStatus()
|
||||||
|
|
||||||
|
def get_self(self) -> BuildStatus: # pylint: disable=no-self-use
|
||||||
|
"""
|
||||||
get ahriman status itself
|
get ahriman status itself
|
||||||
:return: current ahriman status
|
:return: current ahriman status
|
||||||
'''
|
"""
|
||||||
return BuildStatus()
|
return BuildStatus()
|
||||||
|
|
||||||
def remove(self, base: str) -> None:
|
def remove(self, base: str) -> None:
|
||||||
'''
|
"""
|
||||||
remove packages from watcher
|
remove packages from watcher
|
||||||
:param base: package base to remove
|
:param base: package base to remove
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def update(self, base: str, status: BuildStatusEnum) -> None:
|
def update(self, base: str, status: BuildStatusEnum) -> None:
|
||||||
'''
|
"""
|
||||||
update package build status. Unlike `add` it does not update package properties
|
update package build status. Unlike `add` it does not update package properties
|
||||||
:param base: package base to update
|
:param base: package base to update
|
||||||
:param status: current package build status
|
:param status: current package build status
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def update_self(self, status: BuildStatusEnum) -> None:
|
def update_self(self, status: BuildStatusEnum) -> None:
|
||||||
'''
|
"""
|
||||||
update ahriman status itself
|
update ahriman status itself
|
||||||
:param status: current ahriman status
|
:param status: current ahriman status
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def set_building(self, base: str) -> None:
|
def set_building(self, base: str) -> None:
|
||||||
'''
|
"""
|
||||||
set package status to building
|
set package status to building
|
||||||
:param base: package base to update
|
:param base: package base to update
|
||||||
'''
|
"""
|
||||||
return self.update(base, BuildStatusEnum.Building)
|
return self.update(base, BuildStatusEnum.Building)
|
||||||
|
|
||||||
def set_failed(self, base: str) -> None:
|
def set_failed(self, base: str) -> None:
|
||||||
'''
|
"""
|
||||||
set package status to failed
|
set package status to failed
|
||||||
:param base: package base to update
|
:param base: package base to update
|
||||||
'''
|
"""
|
||||||
return self.update(base, BuildStatusEnum.Failed)
|
return self.update(base, BuildStatusEnum.Failed)
|
||||||
|
|
||||||
def set_pending(self, base: str) -> None:
|
def set_pending(self, base: str) -> None:
|
||||||
'''
|
"""
|
||||||
set package status to pending
|
set package status to pending
|
||||||
:param base: package base to update
|
:param base: package base to update
|
||||||
'''
|
"""
|
||||||
return self.update(base, BuildStatusEnum.Pending)
|
return self.update(base, BuildStatusEnum.Pending)
|
||||||
|
|
||||||
def set_success(self, package: Package) -> None:
|
def set_success(self, package: Package) -> None:
|
||||||
'''
|
"""
|
||||||
set package status to success
|
set package status to success
|
||||||
:param package: current package properties
|
:param package: current package properties
|
||||||
'''
|
"""
|
||||||
return self.add(package, BuildStatusEnum.Success)
|
return self.add(package, BuildStatusEnum.Success)
|
||||||
|
|
||||||
def set_unknown(self, package: Package) -> None:
|
def set_unknown(self, package: Package) -> None:
|
||||||
'''
|
"""
|
||||||
set package status to unknown
|
set package status to unknown
|
||||||
:param package: current package properties
|
:param package: current package properties
|
||||||
'''
|
"""
|
||||||
return self.add(package, BuildStatusEnum.Unknown)
|
return self.add(package, BuildStatusEnum.Unknown)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load(architecture: str, config: Configuration) -> Client:
|
|
||||||
'''
|
|
||||||
load client from settings
|
|
||||||
:param architecture: repository architecture
|
|
||||||
:param config: configuration instance
|
|
||||||
:return: client according to current settings
|
|
||||||
'''
|
|
||||||
section = config.get_section_name('web', architecture)
|
|
||||||
host = config.get(section, 'host', fallback=None)
|
|
||||||
port = config.getint(section, 'port', fallback=None)
|
|
||||||
if host is None or port is None:
|
|
||||||
return Client()
|
|
||||||
|
|
||||||
from ahriman.core.watcher.web_client import WebClient
|
|
||||||
return WebClient(host, port)
|
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -19,103 +19,111 @@
|
|||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import UnknownPackage
|
||||||
from ahriman.core.repository.repository import Repository
|
from ahriman.core.repository.repository import Repository
|
||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
class Watcher:
|
class Watcher:
|
||||||
'''
|
"""
|
||||||
package status watcher
|
package status watcher
|
||||||
:ivar architecture: repository architecture
|
:ivar architecture: repository architecture
|
||||||
:ivar known: list of known packages. For the most cases `packages` should be used instead
|
:ivar known: list of known packages. For the most cases `packages` should be used instead
|
||||||
:ivar logger: class logger
|
:ivar logger: class logger
|
||||||
:ivar repository: repository object
|
:ivar repository: repository object
|
||||||
:ivar status: daemon status
|
:ivar status: daemon status
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
self.logger = logging.getLogger('http')
|
self.logger = logging.getLogger("http")
|
||||||
|
|
||||||
self.architecture = architecture
|
self.architecture = architecture
|
||||||
self.repository = Repository(architecture, config)
|
self.repository = Repository(architecture, configuration)
|
||||||
|
|
||||||
self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
|
self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
|
||||||
self.status = BuildStatus()
|
self.status = BuildStatus()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cache_path(self) -> str:
|
def cache_path(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: path to dump with json cache
|
:return: path to dump with json cache
|
||||||
'''
|
"""
|
||||||
return os.path.join(self.repository.paths.root, 'status_cache.json')
|
return self.repository.paths.root / "status_cache.json"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def packages(self) -> List[Tuple[Package, BuildStatus]]:
|
def packages(self) -> List[Tuple[Package, BuildStatus]]:
|
||||||
'''
|
"""
|
||||||
:return: list of packages together with their statuses
|
:return: list of packages together with their statuses
|
||||||
'''
|
"""
|
||||||
return list(self.known.values())
|
return list(self.known.values())
|
||||||
|
|
||||||
def _cache_load(self) -> None:
|
def _cache_load(self) -> None:
|
||||||
'''
|
"""
|
||||||
update current state from cache
|
update current state from cache
|
||||||
'''
|
"""
|
||||||
def parse_single(properties: Dict[str, Any]) -> None:
|
def parse_single(properties: Dict[str, Any]) -> None:
|
||||||
package = Package.from_json(properties['package'])
|
package = Package.from_json(properties["package"])
|
||||||
status = BuildStatus.from_json(properties['status'])
|
status = BuildStatus.from_json(properties["status"])
|
||||||
if package.base in self.known:
|
if package.base in self.known:
|
||||||
self.known[package.base] = (package, status)
|
self.known[package.base] = (package, status)
|
||||||
|
|
||||||
if not os.path.isfile(self.cache_path):
|
if not self.cache_path.is_file():
|
||||||
return
|
return
|
||||||
with open(self.cache_path) as cache:
|
with self.cache_path.open() as cache:
|
||||||
dump = json.load(cache)
|
try:
|
||||||
for item in dump['packages']:
|
dump = json.load(cache)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("cannot parse json from file")
|
||||||
|
dump = {}
|
||||||
|
for item in dump.get("packages", []):
|
||||||
try:
|
try:
|
||||||
parse_single(item)
|
parse_single(item)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception(f'cannot parse item f{item} to package', exc_info=True)
|
self.logger.exception("cannot parse item %s to package", item)
|
||||||
|
|
||||||
def _cache_save(self) -> None:
|
def _cache_save(self) -> None:
|
||||||
'''
|
"""
|
||||||
dump current cache to filesystem
|
dump current cache to filesystem
|
||||||
'''
|
"""
|
||||||
dump = {
|
dump = {
|
||||||
'packages': [
|
"packages": [
|
||||||
{
|
{
|
||||||
'package': package.view(),
|
"package": package.view(),
|
||||||
'status': status.view()
|
"status": status.view()
|
||||||
} for package, status in self.packages
|
} for package, status in self.packages
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
with open(self.cache_path, 'w') as cache:
|
with self.cache_path.open("w") as cache:
|
||||||
json.dump(dump, cache)
|
json.dump(dump, cache)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception('cannot dump cache', exc_info=True)
|
self.logger.exception("cannot dump cache")
|
||||||
|
|
||||||
def get(self, base: str) -> Tuple[Package, BuildStatus]:
|
def get(self, base: str) -> Tuple[Package, BuildStatus]:
|
||||||
'''
|
"""
|
||||||
get current package base build status
|
get current package base build status
|
||||||
:return: package and its status
|
:return: package and its status
|
||||||
'''
|
"""
|
||||||
return self.known[base]
|
try:
|
||||||
|
return self.known[base]
|
||||||
|
except KeyError:
|
||||||
|
raise UnknownPackage(base)
|
||||||
|
|
||||||
def load(self) -> None:
|
def load(self) -> None:
|
||||||
'''
|
"""
|
||||||
load packages from local repository. In case if last status is known, it will use it
|
load packages from local repository. In case if last status is known, it will use it
|
||||||
'''
|
"""
|
||||||
for package in self.repository.packages():
|
for package in self.repository.packages():
|
||||||
# get status of build or assign unknown
|
# get status of build or assign unknown
|
||||||
current = self.known.get(package.base)
|
current = self.known.get(package.base)
|
||||||
@ -127,29 +135,32 @@ class Watcher:
|
|||||||
self._cache_load()
|
self._cache_load()
|
||||||
|
|
||||||
def remove(self, base: str) -> None:
|
def remove(self, base: str) -> None:
|
||||||
'''
|
"""
|
||||||
remove package base from known list if any
|
remove package base from known list if any
|
||||||
:param base: package base
|
:param base: package base
|
||||||
'''
|
"""
|
||||||
self.known.pop(base, None)
|
self.known.pop(base, None)
|
||||||
self._cache_save()
|
self._cache_save()
|
||||||
|
|
||||||
def update(self, base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
|
def update(self, base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
|
||||||
'''
|
"""
|
||||||
update package status and description
|
update package status and description
|
||||||
:param base: package base to update
|
:param base: package base to update
|
||||||
:param status: new build status
|
:param status: new build status
|
||||||
:param package: optional new package description. In case if not set current properties will be used
|
:param package: optional new package description. In case if not set current properties will be used
|
||||||
'''
|
"""
|
||||||
if package is None:
|
if package is None:
|
||||||
package, _ = self.known[base]
|
try:
|
||||||
|
package, _ = self.known[base]
|
||||||
|
except KeyError:
|
||||||
|
raise UnknownPackage(base)
|
||||||
full_status = BuildStatus(status)
|
full_status = BuildStatus(status)
|
||||||
self.known[base] = (package, full_status)
|
self.known[base] = (package, full_status)
|
||||||
self._cache_save()
|
self._cache_save()
|
||||||
|
|
||||||
def update_self(self, status: BuildStatusEnum) -> None:
|
def update_self(self, status: BuildStatusEnum) -> None:
|
||||||
'''
|
"""
|
||||||
update service status
|
update service status
|
||||||
:param status: new service status
|
:param status: new service status
|
||||||
'''
|
"""
|
||||||
self.status = BuildStatus(status)
|
self.status = BuildStatus(status)
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -18,93 +18,119 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from ahriman.core.watcher.client import Client
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
from ahriman.core.status.client import Client
|
||||||
|
from ahriman.core.util import exception_response_text
|
||||||
from ahriman.models.build_status import BuildStatusEnum, BuildStatus
|
from ahriman.models.build_status import BuildStatusEnum, BuildStatus
|
||||||
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
class WebClient(Client):
|
class WebClient(Client):
|
||||||
'''
|
"""
|
||||||
build status reporter web client
|
build status reporter web client
|
||||||
:ivar host: host of web service
|
:ivar host: host of web service
|
||||||
:ivar logger: class logger
|
:ivar logger: class logger
|
||||||
:ivar port: port of web service
|
:ivar port: port of web service
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, host: str, port: int) -> None:
|
def __init__(self, host: str, port: int) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param host: host of web service
|
:param host: host of web service
|
||||||
:param port: port of web service
|
:param port: port of web service
|
||||||
'''
|
"""
|
||||||
self.logger = logging.getLogger('http')
|
self.logger = logging.getLogger("http")
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
|
|
||||||
def _ahriman_url(self) -> str:
|
def _ahriman_url(self) -> str:
|
||||||
'''
|
"""
|
||||||
url generator
|
url generator
|
||||||
:return: full url for web service for ahriman service itself
|
:return: full url for web service for ahriman service itself
|
||||||
'''
|
"""
|
||||||
return f'http://{self.host}:{self.port}/api/v1/ahriman'
|
return f"http://{self.host}:{self.port}/api/v1/ahriman"
|
||||||
|
|
||||||
def _package_url(self, base: str = '') -> str:
|
def _package_url(self, base: str = "") -> str:
|
||||||
'''
|
"""
|
||||||
url generator
|
url generator
|
||||||
:param base: package base to generate url
|
:param base: package base to generate url
|
||||||
:return: full url of web service for specific package base
|
:return: full url of web service for specific package base
|
||||||
'''
|
"""
|
||||||
return f'http://{self.host}:{self.port}/api/v1/packages/{base}'
|
return f"http://{self.host}:{self.port}/api/v1/packages/{base}"
|
||||||
|
|
||||||
|
def _status_url(self) -> str:
|
||||||
|
"""
|
||||||
|
url generator
|
||||||
|
:return: full url for web service for status
|
||||||
|
"""
|
||||||
|
return f"http://{self.host}:{self.port}/api/v1/status"
|
||||||
|
|
||||||
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||||
'''
|
"""
|
||||||
add new package with status
|
add new package with status
|
||||||
:param package: package properties
|
:param package: package properties
|
||||||
:param status: current package build status
|
:param status: current package build status
|
||||||
'''
|
"""
|
||||||
payload = {
|
payload = {
|
||||||
'status': status.value,
|
"status": status.value,
|
||||||
'package': package.view()
|
"package": package.view()
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(self._package_url(package.base), json=payload)
|
response = requests.post(self._package_url(package.base), json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
self.logger.exception(f'could not add {package.base}: {e.response.text}', exc_info=True)
|
self.logger.exception("could not add %s: %s", package.base, exception_response_text(e))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception(f'could not add {package.base}', exc_info=True)
|
self.logger.exception("could not add %s", package.base)
|
||||||
|
|
||||||
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
|
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
|
||||||
'''
|
"""
|
||||||
get package status
|
get package status
|
||||||
:param base: package base to get
|
:param base: package base to get
|
||||||
:return: list of current package description and status if it has been found
|
:return: list of current package description and status if it has been found
|
||||||
'''
|
"""
|
||||||
try:
|
try:
|
||||||
response = requests.get(self._package_url(base or ''))
|
response = requests.get(self._package_url(base or ""))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
status_json = response.json()
|
status_json = response.json()
|
||||||
return [
|
return [
|
||||||
(Package.from_json(package['package']), BuildStatus.from_json(package['status']))
|
(Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
|
||||||
for package in status_json
|
for package in status_json
|
||||||
]
|
]
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
self.logger.exception(f'could not get {base}: {e.response.text}', exc_info=True)
|
self.logger.exception("could not get %s: %s", base, exception_response_text(e))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception(f'could not get {base}', exc_info=True)
|
self.logger.exception("could not get %s", base)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_internal(self) -> InternalStatus:
|
||||||
|
"""
|
||||||
|
get internal service status
|
||||||
|
:return: current internal (web) service status
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(self._status_url())
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
status_json = response.json()
|
||||||
|
return InternalStatus.from_json(status_json)
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
self.logger.exception("could not get web service status: %s", exception_response_text(e))
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("could not get web service status")
|
||||||
|
return InternalStatus()
|
||||||
|
|
||||||
def get_self(self) -> BuildStatus:
|
def get_self(self) -> BuildStatus:
|
||||||
'''
|
"""
|
||||||
get ahriman status itself
|
get ahriman status itself
|
||||||
:return: current ahriman status
|
:return: current ahriman status
|
||||||
'''
|
"""
|
||||||
try:
|
try:
|
||||||
response = requests.get(self._ahriman_url())
|
response = requests.get(self._ahriman_url())
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@ -112,51 +138,51 @@ class WebClient(Client):
|
|||||||
status_json = response.json()
|
status_json = response.json()
|
||||||
return BuildStatus.from_json(status_json)
|
return BuildStatus.from_json(status_json)
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
self.logger.exception(f'could not get service status: {e.response.text}', exc_info=True)
|
self.logger.exception("could not get service status: %s", exception_response_text(e))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception('could not get service status', exc_info=True)
|
self.logger.exception("could not get service status")
|
||||||
return BuildStatus()
|
return BuildStatus()
|
||||||
|
|
||||||
def remove(self, base: str) -> None:
|
def remove(self, base: str) -> None:
|
||||||
'''
|
"""
|
||||||
remove packages from watcher
|
remove packages from watcher
|
||||||
:param base: basename to remove
|
:param base: basename to remove
|
||||||
'''
|
"""
|
||||||
try:
|
try:
|
||||||
response = requests.delete(self._package_url(base))
|
response = requests.delete(self._package_url(base))
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
self.logger.exception(f'could not delete {base}: {e.response.text}', exc_info=True)
|
self.logger.exception("could not delete %s: %s", base, exception_response_text(e))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception(f'could not delete {base}', exc_info=True)
|
self.logger.exception("could not delete %s", base)
|
||||||
|
|
||||||
def update(self, base: str, status: BuildStatusEnum) -> None:
|
def update(self, base: str, status: BuildStatusEnum) -> None:
|
||||||
'''
|
"""
|
||||||
update package build status. Unlike `add` it does not update package properties
|
update package build status. Unlike `add` it does not update package properties
|
||||||
:param base: package base to update
|
:param base: package base to update
|
||||||
:param status: current package build status
|
:param status: current package build status
|
||||||
'''
|
"""
|
||||||
payload = {'status': status.value}
|
payload = {"status": status.value}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(self._package_url(base), json=payload)
|
response = requests.post(self._package_url(base), json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
self.logger.exception(f'could not update {base}: {e.response.text}', exc_info=True)
|
self.logger.exception("could not update %s: %s", base, exception_response_text(e))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception(f'could not update {base}', exc_info=True)
|
self.logger.exception("could not update %s", base)
|
||||||
|
|
||||||
def update_self(self, status: BuildStatusEnum) -> None:
|
def update_self(self, status: BuildStatusEnum) -> None:
|
||||||
'''
|
"""
|
||||||
update ahriman status itself
|
update ahriman status itself
|
||||||
:param status: current ahriman status
|
:param status: current ahriman status
|
||||||
'''
|
"""
|
||||||
payload = {'status': status.value}
|
payload = {"status": status.value}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(self._ahriman_url(), json=payload)
|
response = requests.post(self._ahriman_url(), json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
self.logger.exception(f'could not update service status: {e.response.text}', exc_info=True)
|
self.logger.exception("could not update service status: %s", exception_response_text(e))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception('could not update service status', exc_info=True)
|
self.logger.exception("could not update service status")
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -22,74 +22,90 @@ from __future__ import annotations
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from typing import Iterable, List, Set
|
from pathlib import Path
|
||||||
|
from typing import Iterable, List, Set, Type
|
||||||
|
|
||||||
from ahriman.core.build_tools.task import Task
|
from ahriman.core.build_tools.task import Task
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
class Leaf:
|
class Leaf:
|
||||||
'''
|
"""
|
||||||
tree leaf implementation
|
tree leaf implementation
|
||||||
:ivar dependencies: list of package dependencies
|
:ivar dependencies: list of package dependencies
|
||||||
:ivar package: leaf package properties
|
:ivar package: leaf package properties
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, package: Package) -> None:
|
def __init__(self, package: Package, dependencies: Set[str]) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param package: package properties
|
:param package: package properties
|
||||||
'''
|
:param dependencies: package dependencies
|
||||||
|
"""
|
||||||
self.package = package
|
self.package = package
|
||||||
self.dependencies: Set[str] = set()
|
self.dependencies = dependencies
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def items(self) -> Iterable[str]:
|
def items(self) -> Iterable[str]:
|
||||||
'''
|
"""
|
||||||
:return: packages containing in this leaf
|
:return: packages containing in this leaf
|
||||||
'''
|
"""
|
||||||
return self.package.packages.keys()
|
return self.package.packages.keys()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls: Type[Leaf], package: Package) -> Leaf:
|
||||||
|
"""
|
||||||
|
load leaf from package with dependencies
|
||||||
|
:param package: package properties
|
||||||
|
:return: loaded class
|
||||||
|
"""
|
||||||
|
clone_dir = Path(tempfile.mkdtemp())
|
||||||
|
try:
|
||||||
|
Task.fetch(clone_dir, package.git_url)
|
||||||
|
dependencies = Package.dependencies(clone_dir)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(clone_dir, ignore_errors=True)
|
||||||
|
return cls(package, dependencies)
|
||||||
|
|
||||||
def is_root(self, packages: Iterable[Leaf]) -> bool:
|
def is_root(self, packages: Iterable[Leaf]) -> bool:
|
||||||
'''
|
"""
|
||||||
check if package depends on any other package from list of not
|
check if package depends on any other package from list of not
|
||||||
:param packages: list of known leaves
|
:param packages: list of known leaves
|
||||||
:return: True if any of packages is dependency of the leaf, False otherwise
|
:return: True if any of packages is dependency of the leaf, False otherwise
|
||||||
'''
|
"""
|
||||||
for leaf in packages:
|
for leaf in packages:
|
||||||
if self.dependencies.intersection(leaf.items):
|
if self.dependencies.intersection(leaf.items):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def load_dependencies(self) -> None:
|
|
||||||
'''
|
|
||||||
load dependencies for the leaf
|
|
||||||
'''
|
|
||||||
clone_dir = tempfile.mkdtemp()
|
|
||||||
try:
|
|
||||||
Task.fetch(clone_dir, self.package.git_url)
|
|
||||||
self.dependencies = Package.dependencies(clone_dir)
|
|
||||||
finally:
|
|
||||||
shutil.rmtree(clone_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Tree:
|
class Tree:
|
||||||
'''
|
"""
|
||||||
dependency tree implementation
|
dependency tree implementation
|
||||||
:ivar leaves: list of tree leaves
|
:ivar leaves: list of tree leaves
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, leaves: List[Leaf]) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
'''
|
:param leaves: leaves to build the tree
|
||||||
self.leaves: List[Leaf] = []
|
"""
|
||||||
|
self.leaves = leaves
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls: Type[Tree], packages: Iterable[Package]) -> Tree:
|
||||||
|
"""
|
||||||
|
load tree from packages
|
||||||
|
:param packages: packages list
|
||||||
|
:return: loaded class
|
||||||
|
"""
|
||||||
|
return cls([Leaf.load(package) for package in packages])
|
||||||
|
|
||||||
def levels(self) -> List[List[Package]]:
|
def levels(self) -> List[List[Package]]:
|
||||||
'''
|
"""
|
||||||
get build levels starting from the packages which do not require any other package to build
|
get build levels starting from the packages which do not require any other package to build
|
||||||
:return: list of packages lists
|
:return: list of packages lists
|
||||||
'''
|
"""
|
||||||
result: List[List[Package]] = []
|
result: List[List[Package]] = []
|
||||||
|
|
||||||
unprocessed = self.leaves[:]
|
unprocessed = self.leaves[:]
|
||||||
@ -98,13 +114,3 @@ class Tree:
|
|||||||
unprocessed = [leaf for leaf in unprocessed if not leaf.is_root(unprocessed)]
|
unprocessed = [leaf for leaf in unprocessed if not leaf.is_root(unprocessed)]
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def load(self, packages: Iterable[Package]) -> None:
|
|
||||||
'''
|
|
||||||
load tree from packages
|
|
||||||
:param packages: packages list
|
|
||||||
'''
|
|
||||||
for package in packages:
|
|
||||||
leaf = Leaf(package)
|
|
||||||
leaf.load_dependencies()
|
|
||||||
self.leaves.append(leaf)
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,32 +17,38 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.upload.uploader import Uploader
|
from ahriman.core.upload.upload import Upload
|
||||||
from ahriman.core.util import check_output
|
from ahriman.core.util import check_output
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
class Rsync(Uploader):
|
class Rsync(Upload):
|
||||||
'''
|
"""
|
||||||
rsync wrapper
|
rsync wrapper
|
||||||
|
:ivar command: command arguments for sync
|
||||||
:ivar remote: remote address to sync
|
:ivar remote: remote address to sync
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
_check_output = check_output
|
||||||
'''
|
|
||||||
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
Uploader.__init__(self, architecture, config)
|
Upload.__init__(self, architecture, configuration)
|
||||||
section = config.get_section_name('rsync', architecture)
|
self.command = configuration.getlist("rsync", "command")
|
||||||
self.remote = config.get(section, 'remote')
|
self.remote = configuration.get("rsync", "remote")
|
||||||
|
|
||||||
def sync(self, path: str) -> None:
|
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
|
||||||
'''
|
"""
|
||||||
sync data to remote server
|
sync data to remote server
|
||||||
:param path: local path to sync
|
:param path: local path to sync
|
||||||
'''
|
:param built_packages: list of packages which has just been built
|
||||||
check_output('rsync', '--archive', '--verbose', '--compress', '--partial', '--delete', path, self.remote,
|
"""
|
||||||
exception=None,
|
Rsync._check_output(*self.command, str(path), self.remote, exception=None, logger=self.logger)
|
||||||
logger=self.logger)
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,33 +17,137 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import boto3 # type: ignore
|
||||||
|
import hashlib
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Generator, Iterable
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.upload.uploader import Uploader
|
from ahriman.core.upload.upload import Upload
|
||||||
from ahriman.core.util import check_output
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
class S3(Uploader):
|
class S3(Upload):
|
||||||
'''
|
"""
|
||||||
aws-cli wrapper
|
aws-cli wrapper
|
||||||
:ivar bucket: full bucket name
|
:ivar bucket: boto3 S3 bucket object
|
||||||
'''
|
:ivar chunk_size: chunk size for calculating checksums
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
Uploader.__init__(self, architecture, config)
|
Upload.__init__(self, architecture, configuration)
|
||||||
section = config.get_section_name('s3', architecture)
|
self.bucket = self.get_bucket(configuration)
|
||||||
self.bucket = config.get(section, 'bucket')
|
self.chunk_size = configuration.getint("s3", "chunk_size", fallback=8 * 1024 * 1024)
|
||||||
|
|
||||||
def sync(self, path: str) -> None:
|
@staticmethod
|
||||||
'''
|
def calculate_etag(path: Path, chunk_size: int) -> str:
|
||||||
|
"""
|
||||||
|
calculate amazon s3 etag
|
||||||
|
credits to https://teppen.io/2018/10/23/aws_s3_verify_etags/
|
||||||
|
For this method we have to define nosec because it is out of any security context and provided by AWS
|
||||||
|
:param path: path to local file
|
||||||
|
:param chunk_size: read chunk size, which depends on client settings
|
||||||
|
:return: calculated entity tag for local file
|
||||||
|
"""
|
||||||
|
md5s = []
|
||||||
|
with path.open("rb") as local_file:
|
||||||
|
for chunk in iter(lambda: local_file.read(chunk_size), b""):
|
||||||
|
md5s.append(hashlib.md5(chunk)) # nosec
|
||||||
|
|
||||||
|
# in case if there is only one chunk it must be just this checksum
|
||||||
|
# and checksum of joined digest otherwise (including empty list)
|
||||||
|
checksum = md5s[0] if len(md5s) == 1 else hashlib.md5(b"".join(md5.digest() for md5 in md5s)) # nosec
|
||||||
|
# in case if there are more than one chunk it should be appended with amount of chunks
|
||||||
|
suffix = f"-{len(md5s)}" if len(md5s) > 1 else ""
|
||||||
|
return f"{checksum.hexdigest()}{suffix}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_bucket(configuration: Configuration) -> Any:
|
||||||
|
"""
|
||||||
|
create resource client from configuration
|
||||||
|
:param configuration: configuration instance
|
||||||
|
:return: amazon client
|
||||||
|
"""
|
||||||
|
client = boto3.resource(service_name="s3",
|
||||||
|
region_name=configuration.get("s3", "region"),
|
||||||
|
aws_access_key_id=configuration.get("s3", "access_key"),
|
||||||
|
aws_secret_access_key=configuration.get("s3", "secret_key"))
|
||||||
|
return client.Bucket(configuration.get("s3", "bucket"))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_files(local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
|
||||||
|
"""
|
||||||
|
remove files which have been removed locally
|
||||||
|
:param local_files: map of local path object to its checksum
|
||||||
|
:param remote_objects: map of remote path object to the remote s3 object
|
||||||
|
"""
|
||||||
|
for local_file, remote_object in remote_objects.items():
|
||||||
|
if local_file in local_files:
|
||||||
|
continue
|
||||||
|
remote_object.delete()
|
||||||
|
|
||||||
|
def get_local_files(self, path: Path) -> Dict[Path, str]:
|
||||||
|
"""
|
||||||
|
get all local files and their calculated checksums
|
||||||
|
:param path: local path to sync
|
||||||
|
:return: map of path object to its checksum
|
||||||
|
"""
|
||||||
|
# credits to https://stackoverflow.com/a/64915960
|
||||||
|
def walk(directory_path: Path) -> Generator[Path, None, None]:
|
||||||
|
for element in directory_path.iterdir():
|
||||||
|
if element.is_dir():
|
||||||
|
yield from walk(element)
|
||||||
|
continue
|
||||||
|
yield element
|
||||||
|
return {
|
||||||
|
local_file.relative_to(path): self.calculate_etag(local_file, self.chunk_size)
|
||||||
|
for local_file in walk(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_remote_objects(self) -> Dict[Path, Any]:
|
||||||
|
"""
|
||||||
|
get all remote objects and their checksums
|
||||||
|
:return: map of path object to the remote s3 object
|
||||||
|
"""
|
||||||
|
objects = self.bucket.objects.filter(Prefix=self.architecture)
|
||||||
|
return {Path(item.key).relative_to(self.architecture): item for item in objects}
|
||||||
|
|
||||||
|
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
|
||||||
|
"""
|
||||||
sync data to remote server
|
sync data to remote server
|
||||||
:param path: local path to sync
|
:param path: local path to sync
|
||||||
'''
|
:param built_packages: list of packages which has just been built
|
||||||
# TODO rewrite to boto, but it is bullshit
|
"""
|
||||||
check_output('aws', 's3', 'sync', '--quiet', '--delete', path, self.bucket,
|
remote_objects = self.get_remote_objects()
|
||||||
exception=None,
|
local_files = self.get_local_files(path)
|
||||||
logger=self.logger)
|
|
||||||
|
self.upload_files(path, local_files, remote_objects)
|
||||||
|
self.remove_files(local_files, remote_objects)
|
||||||
|
|
||||||
|
def upload_files(self, path: Path, local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
|
||||||
|
"""
|
||||||
|
upload changed files to s3
|
||||||
|
:param path: local path to sync
|
||||||
|
:param local_files: map of local path object to its checksum
|
||||||
|
:param remote_objects: map of remote path object to the remote s3 object
|
||||||
|
"""
|
||||||
|
for local_file, checksum in local_files.items():
|
||||||
|
remote_object = remote_objects.get(local_file)
|
||||||
|
# 0 and -1 elements are " (double quote)
|
||||||
|
remote_checksum = remote_object.e_tag[1:-1] if remote_object is not None else None
|
||||||
|
if remote_checksum == checksum:
|
||||||
|
continue
|
||||||
|
|
||||||
|
local_path = path / local_file
|
||||||
|
remote_path = Path(self.architecture) / local_file
|
||||||
|
(mime, _) = mimetypes.guess_type(local_path)
|
||||||
|
extra_args = {"Content-Type": mime} if mime is not None else None
|
||||||
|
|
||||||
|
self.bucket.upload_file(Filename=str(local_path), Key=str(remote_path), ExtraArgs=extra_args)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,58 +17,70 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable, Type
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.exceptions import SyncFailed
|
from ahriman.core.exceptions import SyncFailed
|
||||||
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.upload_settings import UploadSettings
|
from ahriman.models.upload_settings import UploadSettings
|
||||||
|
|
||||||
|
|
||||||
class Uploader:
|
class Upload:
|
||||||
'''
|
"""
|
||||||
base remote sync class
|
base remote sync class
|
||||||
:ivar architecture: repository architecture
|
:ivar architecture: repository architecture
|
||||||
:ivar config: configuration instance
|
:ivar configuration: configuration instance
|
||||||
:ivar logger: application logger
|
:ivar logger: application logger
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
'''
|
"""
|
||||||
self.logger = logging.getLogger('builder')
|
self.logger = logging.getLogger("builder")
|
||||||
self.architecture = architecture
|
self.architecture = architecture
|
||||||
self.config = config
|
self.config = configuration
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def run(architecture: str, config: Configuration, target: str, path: str) -> None:
|
def load(cls: Type[Upload], architecture: str, configuration: Configuration, target: str) -> Upload:
|
||||||
'''
|
"""
|
||||||
run remote sync
|
load client from settings
|
||||||
:param architecture: repository architecture
|
:param architecture: repository architecture
|
||||||
:param config: configuration instance
|
:param configuration: configuration instance
|
||||||
:param target: target to run sync (e.g. s3)
|
:param target: target to run sync (e.g. s3)
|
||||||
:param path: local path to sync
|
:return: client according to current settings
|
||||||
'''
|
"""
|
||||||
provider = UploadSettings.from_option(target)
|
provider = UploadSettings.from_option(target)
|
||||||
if provider == UploadSettings.Rsync:
|
if provider == UploadSettings.Rsync:
|
||||||
from ahriman.core.upload.rsync import Rsync
|
from ahriman.core.upload.rsync import Rsync
|
||||||
uploader: Uploader = Rsync(architecture, config)
|
return Rsync(architecture, configuration)
|
||||||
elif provider == UploadSettings.S3:
|
if provider == UploadSettings.S3:
|
||||||
from ahriman.core.upload.s3 import S3
|
from ahriman.core.upload.s3 import S3
|
||||||
uploader = S3(architecture, config)
|
return S3(architecture, configuration)
|
||||||
else:
|
return cls(architecture, configuration) # should never happen
|
||||||
uploader = Uploader(architecture, config)
|
|
||||||
|
|
||||||
|
def run(self, path: Path, built_packages: Iterable[Package]) -> None:
|
||||||
|
"""
|
||||||
|
run remote sync
|
||||||
|
:param path: local path to sync
|
||||||
|
:param built_packages: list of packages which has just been built
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
uploader.sync(path)
|
self.sync(path, built_packages)
|
||||||
except Exception:
|
except Exception:
|
||||||
uploader.logger.exception('remote sync failed', exc_info=True)
|
self.logger.exception("remote sync failed")
|
||||||
raise SyncFailed()
|
raise SyncFailed()
|
||||||
|
|
||||||
def sync(self, path: str) -> None:
|
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
|
||||||
'''
|
"""
|
||||||
sync data to remote server
|
sync data to remote server
|
||||||
:param path: local path to sync
|
:param path: local path to sync
|
||||||
'''
|
:param built_packages: list of packages which has just been built
|
||||||
|
"""
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -19,76 +19,90 @@
|
|||||||
#
|
#
|
||||||
import datetime
|
import datetime
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import requests
|
||||||
|
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from typing import Optional
|
from pathlib import Path
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
from ahriman.core.exceptions import InvalidOption
|
from ahriman.core.exceptions import InvalidOption
|
||||||
|
|
||||||
|
|
||||||
def check_output(*args: str, exception: Optional[Exception],
|
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
|
||||||
cwd: Optional[str] = None, stderr: int = subprocess.STDOUT,
|
input_data: Optional[str] = None, logger: Optional[Logger] = None) -> str:
|
||||||
logger: Optional[Logger] = None) -> str:
|
"""
|
||||||
'''
|
|
||||||
subprocess wrapper
|
subprocess wrapper
|
||||||
:param args: command line arguments
|
:param args: command line arguments
|
||||||
:param exception: exception which has to be reraised instead of default subprocess exception
|
:param exception: exception which has to be reraised instead of default subprocess exception
|
||||||
:param cwd: current working directory
|
:param cwd: current working directory
|
||||||
:param stderr: standard error output mode
|
:param input_data: data which will be written to command stdin
|
||||||
:param logger: logger to log command result if required
|
:param logger: logger to log command result if required
|
||||||
:return: command output
|
:return: command output
|
||||||
'''
|
"""
|
||||||
try:
|
try:
|
||||||
result = subprocess.check_output(args, cwd=cwd, stderr=stderr).decode('utf8').strip()
|
# universal_newlines is required to read input from string
|
||||||
|
result: str = subprocess.check_output(args, cwd=cwd, input=input_data, stderr=subprocess.STDOUT,
|
||||||
|
universal_newlines=True).strip()
|
||||||
if logger is not None:
|
if logger is not None:
|
||||||
for line in result.splitlines():
|
for line in result.splitlines():
|
||||||
logger.debug(line)
|
logger.debug(line)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if e.output is not None and logger is not None:
|
if e.output is not None and logger is not None:
|
||||||
for line in e.output.decode('utf8').splitlines():
|
for line in e.output.splitlines():
|
||||||
logger.debug(line)
|
logger.debug(line)
|
||||||
raise exception or e
|
raise exception or e
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def package_like(filename: str) -> bool:
|
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
|
||||||
'''
|
"""
|
||||||
|
safe response exception text generation
|
||||||
|
:param exception: exception raised
|
||||||
|
:return: text of the response if it is not None and empty string otherwise
|
||||||
|
"""
|
||||||
|
result: str = exception.response.text if exception.response is not None else ""
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def package_like(filename: Path) -> bool:
|
||||||
|
"""
|
||||||
check if file looks like package
|
check if file looks like package
|
||||||
:param filename: name of file to check
|
:param filename: name of file to check
|
||||||
:return: True in case if name contains `.pkg.` and not signature, False otherwise
|
:return: True in case if name contains `.pkg.` and not signature, False otherwise
|
||||||
'''
|
"""
|
||||||
return '.pkg.' in filename and not filename.endswith('.sig')
|
name = filename.name
|
||||||
|
return ".pkg." in name and not name.endswith(".sig")
|
||||||
|
|
||||||
|
|
||||||
def pretty_datetime(timestamp: Optional[int]) -> str:
|
def pretty_datetime(timestamp: Optional[Union[float, int]]) -> str:
|
||||||
'''
|
"""
|
||||||
convert datetime object to string
|
convert datetime object to string
|
||||||
:param timestamp: datetime to convert
|
:param timestamp: datetime to convert
|
||||||
:return: pretty printable datetime as string
|
:return: pretty printable datetime as string
|
||||||
'''
|
"""
|
||||||
return '' if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
|
return "" if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
def pretty_size(size: Optional[float], level: int = 0) -> str:
|
def pretty_size(size: Optional[float], level: int = 0) -> str:
|
||||||
'''
|
"""
|
||||||
convert size to string
|
convert size to string
|
||||||
:param size: size to convert
|
:param size: size to convert
|
||||||
:param level: represents current units, 0 is B, 1 is KiB etc
|
:param level: represents current units, 0 is B, 1 is KiB etc
|
||||||
:return: pretty printable size as string
|
:return: pretty printable size as string
|
||||||
'''
|
"""
|
||||||
def str_level() -> str:
|
def str_level() -> str:
|
||||||
if level == 0:
|
if level == 0:
|
||||||
return 'B'
|
return "B"
|
||||||
if level == 1:
|
if level == 1:
|
||||||
return 'KiB'
|
return "KiB"
|
||||||
if level == 2:
|
if level == 2:
|
||||||
return 'MiB'
|
return "MiB"
|
||||||
if level == 3:
|
if level == 3:
|
||||||
return 'GiB'
|
return "GiB"
|
||||||
raise InvalidOption(level) # I hope it will not be more than 1024 GiB
|
raise InvalidOption(level) # must never happen actually
|
||||||
|
|
||||||
if size is None:
|
if size is None:
|
||||||
return ''
|
return ""
|
||||||
if size < 1024:
|
if size < 1024 or level >= 3:
|
||||||
return f'{round(size, 2)} {str_level()}'
|
return f"{size:.1f} {str_level()}"
|
||||||
return pretty_size(size / 1024, level + 1)
|
return pretty_size(size / 1024, level + 1)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -28,83 +28,93 @@ from ahriman.core.util import pretty_datetime
|
|||||||
|
|
||||||
|
|
||||||
class BuildStatusEnum(Enum):
|
class BuildStatusEnum(Enum):
|
||||||
'''
|
"""
|
||||||
build status enumeration
|
build status enumeration
|
||||||
:cvar Unknown: build status is unknown
|
:cvar Unknown: build status is unknown
|
||||||
:cvar Pending: package is out-of-dated and will be built soon
|
:cvar Pending: package is out-of-dated and will be built soon
|
||||||
:cvar Building: package is building right now
|
:cvar Building: package is building right now
|
||||||
:cvar Failed: package build failed
|
:cvar Failed: package build failed
|
||||||
:cvar Success: package has been built without errors
|
:cvar Success: package has been built without errors
|
||||||
'''
|
"""
|
||||||
|
|
||||||
Unknown = 'unknown'
|
Unknown = "unknown"
|
||||||
Pending = 'pending'
|
Pending = "pending"
|
||||||
Building = 'building'
|
Building = "building"
|
||||||
Failed = 'failed'
|
Failed = "failed"
|
||||||
Success = 'success'
|
Success = "success"
|
||||||
|
|
||||||
def badges_color(self) -> str:
|
def badges_color(self) -> str:
|
||||||
'''
|
"""
|
||||||
convert itself to shield.io badges color
|
convert itself to shield.io badges color
|
||||||
:return: shields.io color
|
:return: shields.io color
|
||||||
'''
|
"""
|
||||||
if self == BuildStatusEnum.Pending:
|
if self == BuildStatusEnum.Pending:
|
||||||
return 'yellow'
|
return "yellow"
|
||||||
if self == BuildStatusEnum.Building:
|
if self == BuildStatusEnum.Building:
|
||||||
return 'yellow'
|
return "yellow"
|
||||||
if self == BuildStatusEnum.Failed:
|
if self == BuildStatusEnum.Failed:
|
||||||
return 'critical'
|
return "critical"
|
||||||
if self == BuildStatusEnum.Success:
|
if self == BuildStatusEnum.Success:
|
||||||
return 'success'
|
return "success"
|
||||||
return 'inactive'
|
return "inactive"
|
||||||
|
|
||||||
|
|
||||||
class BuildStatus:
|
class BuildStatus:
|
||||||
'''
|
"""
|
||||||
build status holder
|
build status holder
|
||||||
:ivar status: build status
|
:ivar status: build status
|
||||||
:ivar _timestamp: build status update time
|
:ivar timestamp: build status update time
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
|
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
|
||||||
timestamp: Optional[int] = None) -> None:
|
timestamp: Optional[int] = None) -> None:
|
||||||
'''
|
"""
|
||||||
default constructor
|
default constructor
|
||||||
:param status: current build status if known. `BuildStatusEnum.Unknown` will be used if not set
|
:param status: current build status if known. `BuildStatusEnum.Unknown` will be used if not set
|
||||||
:param timestamp: build status timestamp. Current timestamp will be used if not set
|
:param timestamp: build status timestamp. Current timestamp will be used if not set
|
||||||
'''
|
"""
|
||||||
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
|
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
|
||||||
self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())
|
self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
|
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
|
||||||
'''
|
"""
|
||||||
construct status properties from json dump
|
construct status properties from json dump
|
||||||
:param dump: json dump body
|
:param dump: json dump body
|
||||||
:return: status properties
|
:return: status properties
|
||||||
'''
|
"""
|
||||||
return cls(dump.get('status'), dump.get('timestamp'))
|
return cls(dump.get("status"), dump.get("timestamp"))
|
||||||
|
|
||||||
def pretty_print(self) -> str:
|
def pretty_print(self) -> str:
|
||||||
'''
|
"""
|
||||||
generate pretty string representation
|
generate pretty string representation
|
||||||
:return: print-friendly string
|
:return: print-friendly string
|
||||||
'''
|
"""
|
||||||
return f'{self.status.value} ({pretty_datetime(self.timestamp)})'
|
return f"{self.status.value} ({pretty_datetime(self.timestamp)})"
|
||||||
|
|
||||||
def view(self) -> Dict[str, Any]:
|
def view(self) -> Dict[str, Any]:
|
||||||
'''
|
"""
|
||||||
generate json status view
|
generate json status view
|
||||||
:return: json-friendly dictionary
|
:return: json-friendly dictionary
|
||||||
'''
|
"""
|
||||||
return {
|
return {
|
||||||
'status': self.status.value,
|
"status": self.status.value,
|
||||||
'timestamp': self.timestamp
|
"timestamp": self.timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
"""
|
||||||
|
compare object to other
|
||||||
|
:param other: other object to compare
|
||||||
|
:return: True in case if objects are equal
|
||||||
|
"""
|
||||||
|
if not isinstance(other, BuildStatus):
|
||||||
|
return False
|
||||||
|
return self.status == other.status and self.timestamp == other.timestamp
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
'''
|
"""
|
||||||
generate string representation of object
|
generate string representation of object
|
||||||
:return: unique string representation
|
:return: unique string representation
|
||||||
'''
|
"""
|
||||||
return f'BuildStatus(status={self.status.value}, timestamp={self.timestamp})'
|
return f"BuildStatus(status={self.status.value}, timestamp={self.timestamp})"
|
||||||
|
71
src/ahriman/models/counters.py
Normal file
71
src/ahriman/models/counters.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, fields
|
||||||
|
from typing import Any, Dict, List, Tuple, Type
|
||||||
|
|
||||||
|
from ahriman.models.build_status import BuildStatus
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Counters:
|
||||||
|
"""
|
||||||
|
package counters
|
||||||
|
:ivar total: total packages count
|
||||||
|
:ivar unknown: packages in unknown status count
|
||||||
|
:ivar pending: packages in pending status count
|
||||||
|
:ivar building: packages in building status count
|
||||||
|
:ivar failed: packages in failed status count
|
||||||
|
:ivar success: packages in success status count
|
||||||
|
"""
|
||||||
|
total: int
|
||||||
|
unknown: int = 0
|
||||||
|
pending: int = 0
|
||||||
|
building: int = 0
|
||||||
|
failed: int = 0
|
||||||
|
success: int = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls: Type[Counters], dump: Dict[str, Any]) -> Counters:
|
||||||
|
"""
|
||||||
|
construct counters from json dump
|
||||||
|
:param dump: json dump body
|
||||||
|
:return: status counters
|
||||||
|
"""
|
||||||
|
# filter to only known fields
|
||||||
|
known_fields = [pair.name for pair in fields(cls)]
|
||||||
|
dump = {key: value for key, value in dump.items() if key in known_fields}
|
||||||
|
return cls(**dump)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_packages(cls: Type[Counters], packages: List[Tuple[Package, BuildStatus]]) -> Counters:
|
||||||
|
"""
|
||||||
|
construct counters from packages statuses
|
||||||
|
:param packages: list of package and their status as per watcher property
|
||||||
|
:return: status counters
|
||||||
|
"""
|
||||||
|
per_status = {"total": len(packages)}
|
||||||
|
for _, status in packages:
|
||||||
|
key = status.status.name.lower()
|
||||||
|
per_status.setdefault(key, 0)
|
||||||
|
per_status[key] += 1
|
||||||
|
return cls(**per_status)
|
60
src/ahriman/models/internal_status.py
Normal file
60
src/ahriman/models/internal_status.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from typing import Any, Dict, Optional, Type
|
||||||
|
|
||||||
|
from ahriman.models.counters import Counters
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InternalStatus:
|
||||||
|
"""
|
||||||
|
internal server status
|
||||||
|
:ivar architecture: repository architecture
|
||||||
|
:ivar packages: packages statuses counter object
|
||||||
|
:ivar repository: repository name
|
||||||
|
:ivar version: service version
|
||||||
|
"""
|
||||||
|
architecture: Optional[str] = None
|
||||||
|
packages: Counters = field(default=Counters(total=0))
|
||||||
|
repository: Optional[str] = None
|
||||||
|
version: Optional[str] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls: Type[InternalStatus], dump: Dict[str, Any]) -> InternalStatus:
|
||||||
|
"""
|
||||||
|
construct internal status from json dump
|
||||||
|
:param dump: json dump body
|
||||||
|
:return: internal status
|
||||||
|
"""
|
||||||
|
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
|
||||||
|
return cls(architecture=dump.get("architecture"),
|
||||||
|
packages=counters,
|
||||||
|
repository=dump.get("repository"),
|
||||||
|
version=dump.get("version"))
|
||||||
|
|
||||||
|
def view(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
generate json status view
|
||||||
|
:return: json-friendly dictionary
|
||||||
|
"""
|
||||||
|
return asdict(self)
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -21,227 +21,264 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import aur # type: ignore
|
import aur # type: ignore
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
|
from pathlib import Path
|
||||||
from pyalpm import vercmp # type: ignore
|
from pyalpm import vercmp # type: ignore
|
||||||
from srcinfo.parse import parse_srcinfo # type: ignore
|
from srcinfo.parse import parse_srcinfo # type: ignore
|
||||||
from typing import Any, Dict, List, Optional, Set, Type
|
from typing import Any, Dict, List, Optional, Set, Type, Union
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
from ahriman.core.alpm.pacman import Pacman
|
||||||
from ahriman.core.exceptions import InvalidPackageInfo
|
from ahriman.core.exceptions import InvalidPackageInfo
|
||||||
from ahriman.core.util import check_output
|
from ahriman.core.util import check_output
|
||||||
from ahriman.models.package_desciption import PackageDescription
|
from ahriman.models.package_description import PackageDescription
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Package:
|
class Package:
|
||||||
'''
|
"""
|
||||||
package properties representation
|
package properties representation
|
||||||
:ivar aurl_url: AUR root url
|
:ivar aur_url: AUR root url
|
||||||
:ivar base: package base name
|
:ivar base: package base name
|
||||||
:ivar packages: map of package names to their properties. Filled only on load from archive
|
:ivar packages: map of package names to their properties. Filled only on load from archive
|
||||||
:ivar version: package full version
|
:ivar version: package full version
|
||||||
'''
|
"""
|
||||||
|
|
||||||
base: str
|
base: str
|
||||||
version: str
|
version: str
|
||||||
aur_url: str
|
aur_url: str
|
||||||
packages: Dict[str, PackageDescription]
|
packages: Dict[str, PackageDescription]
|
||||||
|
|
||||||
|
_check_output = check_output
|
||||||
|
|
||||||
|
@property
|
||||||
|
def depends(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
:return: sum of dependencies per arch package
|
||||||
|
"""
|
||||||
|
return sorted(set(sum([package.depends for package in self.packages.values()], start=[])))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def git_url(self) -> str:
|
def git_url(self) -> str:
|
||||||
'''
|
"""
|
||||||
:return: package git url to clone
|
:return: package git url to clone
|
||||||
'''
|
"""
|
||||||
return f'{self.aur_url}/{self.base}.git'
|
return f"{self.aur_url}/{self.base}.git"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def groups(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
:return: sum of groups per each package
|
||||||
|
"""
|
||||||
|
return sorted(set(sum([package.groups for package in self.packages.values()], start=[])))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_single_package(self) -> bool:
|
def is_single_package(self) -> bool:
|
||||||
'''
|
"""
|
||||||
:return: true in case if this base has only one package with the same name
|
:return: true in case if this base has only one package with the same name
|
||||||
'''
|
"""
|
||||||
return self.base in self.packages and len(self.packages) == 1
|
return self.base in self.packages and len(self.packages) == 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_vcs(self) -> bool:
|
def is_vcs(self) -> bool:
|
||||||
'''
|
"""
|
||||||
:return: True in case if package base looks like VCS package and false otherwise
|
:return: True in case if package base looks like VCS package and false otherwise
|
||||||
'''
|
"""
|
||||||
return self.base.endswith('-bzr') \
|
return self.base.endswith("-bzr") \
|
||||||
or self.base.endswith('-csv')\
|
or self.base.endswith("-csv")\
|
||||||
or self.base.endswith('-darcs')\
|
or self.base.endswith("-darcs")\
|
||||||
or self.base.endswith('-git')\
|
or self.base.endswith("-git")\
|
||||||
or self.base.endswith('-hg')\
|
or self.base.endswith("-hg")\
|
||||||
or self.base.endswith('-svn')
|
or self.base.endswith("-svn")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def licenses(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
:return: sum of licenses per each package
|
||||||
|
"""
|
||||||
|
return sorted(set(sum([package.licenses for package in self.packages.values()], start=[])))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def web_url(self) -> str:
|
def web_url(self) -> str:
|
||||||
'''
|
"""
|
||||||
:return: package AUR url
|
:return: package AUR url
|
||||||
'''
|
"""
|
||||||
return f'{self.aur_url}/packages/{self.base}'
|
return f"{self.aur_url}/packages/{self.base}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_archive(cls: Type[Package], path: str, pacman: Pacman, aur_url: str) -> Package:
|
def from_archive(cls: Type[Package], path: Path, pacman: Pacman, aur_url: str) -> Package:
|
||||||
'''
|
"""
|
||||||
construct package properties from package archive
|
construct package properties from package archive
|
||||||
:param path: path to package archive
|
:param path: path to package archive
|
||||||
:param pacman: alpm wrapper instance
|
:param pacman: alpm wrapper instance
|
||||||
:param aur_url: AUR root url
|
:param aur_url: AUR root url
|
||||||
:return: package properties
|
:return: package properties
|
||||||
'''
|
"""
|
||||||
package = pacman.handle.load_pkg(path)
|
package = pacman.handle.load_pkg(str(path))
|
||||||
properties = PackageDescription(package.size, package.builddate, os.path.basename(path), package.isize)
|
return cls(package.base, package.version, aur_url,
|
||||||
return cls(package.base, package.version, aur_url, {package.name: properties})
|
{package.name: PackageDescription.from_package(package, path)})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_aur(cls: Type[Package], name: str, aur_url: str) -> Package:
|
def from_aur(cls: Type[Package], name: str, aur_url: str) -> Package:
|
||||||
'''
|
"""
|
||||||
construct package properties from AUR page
|
construct package properties from AUR page
|
||||||
:param name: package name (either base or normal name)
|
:param name: package name (either base or normal name)
|
||||||
:param aur_url: AUR root url
|
:param aur_url: AUR root url
|
||||||
:return: package properties
|
:return: package properties
|
||||||
'''
|
"""
|
||||||
package = aur.info(name)
|
package = aur.info(name)
|
||||||
return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()})
|
return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_build(cls: Type[Package], path: str, aur_url: str) -> Package:
|
def from_build(cls: Type[Package], path: Path, aur_url: str) -> Package:
|
||||||
'''
|
"""
|
||||||
construct package properties from sources directory
|
construct package properties from sources directory
|
||||||
:param path: path to package sources directory
|
:param path: path to package sources directory
|
||||||
:param aur_url: AUR root url
|
:param aur_url: AUR root url
|
||||||
:return: package properties
|
:return: package properties
|
||||||
'''
|
"""
|
||||||
with open(os.path.join(path, '.SRCINFO')) as srcinfo_file:
|
srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text())
|
||||||
srcinfo, errors = parse_srcinfo(srcinfo_file.read())
|
|
||||||
if errors:
|
if errors:
|
||||||
raise InvalidPackageInfo(errors)
|
raise InvalidPackageInfo(errors)
|
||||||
packages = {key: PackageDescription() for key in srcinfo['packages']}
|
packages = {key: PackageDescription() for key in srcinfo["packages"]}
|
||||||
version = cls.full_version(srcinfo.get('epoch'), srcinfo['pkgver'], srcinfo['pkgrel'])
|
version = cls.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
|
||||||
|
|
||||||
return cls(srcinfo['pkgbase'], version, aur_url, packages)
|
return cls(srcinfo["pkgbase"], version, aur_url, packages)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls: Type[Package], dump: Dict[str, Any]) -> Package:
|
def from_json(cls: Type[Package], dump: Dict[str, Any]) -> Package:
|
||||||
'''
|
"""
|
||||||
construct package properties from json dump
|
construct package properties from json dump
|
||||||
:param dump: json dump body
|
:param dump: json dump body
|
||||||
:return: package properties
|
:return: package properties
|
||||||
'''
|
"""
|
||||||
packages = {
|
packages = {
|
||||||
key: PackageDescription(**value)
|
key: PackageDescription.from_json(value)
|
||||||
for key, value in dump.get('packages', {}).items()
|
for key, value in dump.get("packages", {}).items()
|
||||||
}
|
}
|
||||||
return Package(
|
return Package(
|
||||||
base=dump['base'],
|
base=dump["base"],
|
||||||
version=dump['version'],
|
version=dump["version"],
|
||||||
aur_url=dump['aur_url'],
|
aur_url=dump["aur_url"],
|
||||||
packages=packages)
|
packages=packages)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def dependencies(path: str) -> Set[str]:
|
def load(cls: Type[Package], path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
|
||||||
'''
|
"""
|
||||||
load dependencies from package sources
|
|
||||||
:param path: path to package sources directory
|
|
||||||
:return: list of package dependencies including makedepends array, but excluding packages from this base
|
|
||||||
'''
|
|
||||||
with open(os.path.join(path, '.SRCINFO')) as srcinfo_file:
|
|
||||||
srcinfo, errors = parse_srcinfo(srcinfo_file.read())
|
|
||||||
if errors:
|
|
||||||
raise InvalidPackageInfo(errors)
|
|
||||||
makedepends = srcinfo.get('makedepends', [])
|
|
||||||
# sum over each package
|
|
||||||
depends: List[str] = srcinfo.get('depends', [])
|
|
||||||
for package in srcinfo['packages'].values():
|
|
||||||
depends.extend(package.get('depends', []))
|
|
||||||
# we are not interested in dependencies inside pkgbase
|
|
||||||
packages = set(srcinfo['packages'].keys())
|
|
||||||
return set(depends + makedepends) - packages
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def full_version(epoch: Optional[str], pkgver: str, pkgrel: str) -> str:
|
|
||||||
'''
|
|
||||||
generate full version from components
|
|
||||||
:param epoch: package epoch if any
|
|
||||||
:param pkgver: package version
|
|
||||||
:param pkgrel: package release version (archlinux specific)
|
|
||||||
:return: generated version
|
|
||||||
'''
|
|
||||||
prefix = f'{epoch}:' if epoch else ''
|
|
||||||
return f'{prefix}{pkgver}-{pkgrel}'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load(path: str, pacman: Pacman, aur_url: str) -> Package:
|
|
||||||
'''
|
|
||||||
package constructor from available sources
|
package constructor from available sources
|
||||||
:param path: one of path to sources directory, path to archive or package name/base
|
:param path: one of path to sources directory, path to archive or package name/base
|
||||||
:param pacman: alpm wrapper instance (required to load from archive)
|
:param pacman: alpm wrapper instance (required to load from archive)
|
||||||
:param aur_url: AUR root url
|
:param aur_url: AUR root url
|
||||||
:return: package properties
|
:return: package properties
|
||||||
'''
|
"""
|
||||||
try:
|
try:
|
||||||
if os.path.isdir(path):
|
maybe_path = Path(path)
|
||||||
package: Package = Package.from_build(path, aur_url)
|
if maybe_path.is_dir():
|
||||||
elif os.path.exists(path):
|
return cls.from_build(maybe_path, aur_url)
|
||||||
package = Package.from_archive(path, pacman, aur_url)
|
if maybe_path.is_file():
|
||||||
else:
|
return cls.from_archive(maybe_path, pacman, aur_url)
|
||||||
package = Package.from_aur(path, aur_url)
|
return cls.from_aur(str(path), aur_url)
|
||||||
return package
|
|
||||||
except InvalidPackageInfo:
|
except InvalidPackageInfo:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise InvalidPackageInfo(str(e))
|
raise InvalidPackageInfo(str(e))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dependencies(path: Path) -> Set[str]:
|
||||||
|
"""
|
||||||
|
load dependencies from package sources
|
||||||
|
:param path: path to package sources directory
|
||||||
|
:return: list of package dependencies including makedepends array, but excluding packages from this base
|
||||||
|
"""
|
||||||
|
# additional function to remove versions from dependencies
|
||||||
|
def trim_version(name: str) -> str:
|
||||||
|
for symbol in ("<", "=", ">"):
|
||||||
|
name = name.split(symbol)[0]
|
||||||
|
return name
|
||||||
|
|
||||||
|
srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text())
|
||||||
|
if errors:
|
||||||
|
raise InvalidPackageInfo(errors)
|
||||||
|
makedepends = srcinfo.get("makedepends", [])
|
||||||
|
# sum over each package
|
||||||
|
depends: List[str] = srcinfo.get("depends", [])
|
||||||
|
for package in srcinfo["packages"].values():
|
||||||
|
depends.extend(package.get("depends", []))
|
||||||
|
# we are not interested in dependencies inside pkgbase
|
||||||
|
packages = set(srcinfo["packages"].keys())
|
||||||
|
full_list = set(depends + makedepends) - packages
|
||||||
|
return {trim_version(package_name) for package_name in full_list}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def full_version(epoch: Optional[str], pkgver: str, pkgrel: str) -> str:
|
||||||
|
"""
|
||||||
|
generate full version from components
|
||||||
|
:param epoch: package epoch if any
|
||||||
|
:param pkgver: package version
|
||||||
|
:param pkgrel: package release version (archlinux specific)
|
||||||
|
:return: generated version
|
||||||
|
"""
|
||||||
|
prefix = f"{epoch}:" if epoch else ""
|
||||||
|
return f"{prefix}{pkgver}-{pkgrel}"
|
||||||
|
|
||||||
def actual_version(self, paths: RepositoryPaths) -> str:
|
def actual_version(self, paths: RepositoryPaths) -> str:
|
||||||
'''
|
"""
|
||||||
additional method to handle VCS package versions
|
additional method to handle VCS package versions
|
||||||
:param paths: repository paths instance
|
:param paths: repository paths instance
|
||||||
:return: package version if package is not VCS and current version according to VCS otherwise
|
:return: package version if package is not VCS and current version according to VCS otherwise
|
||||||
'''
|
"""
|
||||||
if not self.is_vcs:
|
if not self.is_vcs:
|
||||||
return self.version
|
return self.version
|
||||||
|
|
||||||
from ahriman.core.build_tools.task import Task
|
from ahriman.core.build_tools.task import Task
|
||||||
|
|
||||||
clone_dir = os.path.join(paths.cache, self.base)
|
clone_dir = paths.cache / self.base
|
||||||
logger = logging.getLogger('build_details')
|
logger = logging.getLogger("build_details")
|
||||||
Task.fetch(clone_dir, self.git_url)
|
Task.fetch(clone_dir, self.git_url)
|
||||||
|
|
||||||
# update pkgver first
|
try:
|
||||||
check_output('makepkg', '--nodeps', '--nobuild', exception=None, cwd=clone_dir, logger=logger)
|
# update pkgver first
|
||||||
# generate new .SRCINFO and put it to parser
|
Package._check_output("makepkg", "--nodeps", "--nobuild", exception=None, cwd=clone_dir, logger=logger)
|
||||||
srcinfo_source = check_output('makepkg', '--printsrcinfo', exception=None, cwd=clone_dir, logger=logger)
|
# generate new .SRCINFO and put it to parser
|
||||||
srcinfo, errors = parse_srcinfo(srcinfo_source)
|
srcinfo_source = Package._check_output(
|
||||||
if errors:
|
"makepkg",
|
||||||
raise InvalidPackageInfo(errors)
|
"--printsrcinfo",
|
||||||
|
exception=None,
|
||||||
|
cwd=clone_dir,
|
||||||
|
logger=logger)
|
||||||
|
srcinfo, errors = parse_srcinfo(srcinfo_source)
|
||||||
|
if errors:
|
||||||
|
raise InvalidPackageInfo(errors)
|
||||||
|
|
||||||
return self.full_version(srcinfo.get('epoch'), srcinfo['pkgver'], srcinfo['pkgrel'])
|
return self.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
|
||||||
|
except Exception:
|
||||||
|
logger.exception("cannot determine version of VCS package, make sure that you have VCS tools installed")
|
||||||
|
|
||||||
|
return self.version
|
||||||
|
|
||||||
def is_outdated(self, remote: Package, paths: RepositoryPaths) -> bool:
|
def is_outdated(self, remote: Package, paths: RepositoryPaths) -> bool:
|
||||||
'''
|
"""
|
||||||
check if package is out-of-dated
|
check if package is out-of-dated
|
||||||
:param remote: package properties from remote source
|
:param remote: package properties from remote source
|
||||||
:param paths: repository paths instance. Required for VCS packages cache
|
:param paths: repository paths instance. Required for VCS packages cache
|
||||||
:return: True if the package is out-of-dated and False otherwise
|
:return: True if the package is out-of-dated and False otherwise
|
||||||
'''
|
"""
|
||||||
remote_version = remote.actual_version(paths) # either normal version or updated VCS
|
remote_version = remote.actual_version(paths) # either normal version or updated VCS
|
||||||
result: int = vercmp(self.version, remote_version)
|
result: int = vercmp(self.version, remote_version)
|
||||||
return result < 0
|
return result < 0
|
||||||
|
|
||||||
def pretty_print(self) -> str:
|
def pretty_print(self) -> str:
|
||||||
'''
|
"""
|
||||||
generate pretty string representation
|
generate pretty string representation
|
||||||
:return: print-friendly string
|
:return: print-friendly string
|
||||||
'''
|
"""
|
||||||
details = '' if self.is_single_package else f''' ({' '.join(sorted(self.packages.keys()))})'''
|
details = "" if self.is_single_package else f""" ({" ".join(sorted(self.packages.keys()))})"""
|
||||||
return f'{self.base}{details}'
|
return f"{self.base}{details}"
|
||||||
|
|
||||||
def view(self) -> Dict[str, Any]:
|
def view(self) -> Dict[str, Any]:
|
||||||
'''
|
"""
|
||||||
generate json package view
|
generate json package view
|
||||||
:return: json-friendly dictionary
|
:return: json-friendly dictionary
|
||||||
'''
|
"""
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
95
src/ahriman/models/package_description.py
Normal file
95
src/ahriman/models/package_description.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field, fields
|
||||||
|
from pathlib import Path
|
||||||
|
from pyalpm import Package # type: ignore
|
||||||
|
from typing import Any, Dict, List, Optional, Type
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PackageDescription:
|
||||||
|
"""
|
||||||
|
package specific properties
|
||||||
|
:ivar architecture: package architecture
|
||||||
|
:ivar archive_size: package archive size
|
||||||
|
:ivar build_date: package build date
|
||||||
|
:ivar depends: package dependencies list
|
||||||
|
:ivar description: package description
|
||||||
|
:ivar filename: package archive name
|
||||||
|
:ivar groups: package groups
|
||||||
|
:ivar installed_size: package installed size
|
||||||
|
:ivar licenses: package licenses list
|
||||||
|
:ivar provides: list of provided packages
|
||||||
|
:ivar url: package url
|
||||||
|
"""
|
||||||
|
|
||||||
|
architecture: Optional[str] = None
|
||||||
|
archive_size: Optional[int] = None
|
||||||
|
build_date: Optional[int] = None
|
||||||
|
depends: List[str] = field(default_factory=list)
|
||||||
|
description: Optional[str] = None
|
||||||
|
filename: Optional[str] = None
|
||||||
|
groups: List[str] = field(default_factory=list)
|
||||||
|
installed_size: Optional[int] = None
|
||||||
|
licenses: List[str] = field(default_factory=list)
|
||||||
|
provides: List[str] = field(default_factory=list)
|
||||||
|
url: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filepath(self) -> Optional[Path]:
|
||||||
|
"""
|
||||||
|
:return: path object for current filename
|
||||||
|
"""
|
||||||
|
return Path(self.filename) if self.filename is not None else None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls: Type[PackageDescription], dump: Dict[str, Any]) -> PackageDescription:
|
||||||
|
"""
|
||||||
|
construct package properties from json dump
|
||||||
|
:param dump: json dump body
|
||||||
|
:return: package properties
|
||||||
|
"""
|
||||||
|
# filter to only known fields
|
||||||
|
known_fields = [pair.name for pair in fields(cls)]
|
||||||
|
dump = {key: value for key, value in dump.items() if key in known_fields}
|
||||||
|
return cls(**dump)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:
|
||||||
|
"""
|
||||||
|
construct class from alpm package class
|
||||||
|
:param package: alpm generated object
|
||||||
|
:param path: path to package archive
|
||||||
|
:return: package properties based on tarball
|
||||||
|
"""
|
||||||
|
return cls(
|
||||||
|
architecture=package.arch,
|
||||||
|
archive_size=package.size,
|
||||||
|
build_date=package.builddate,
|
||||||
|
depends=package.depends,
|
||||||
|
description=package.desc,
|
||||||
|
filename=path.name,
|
||||||
|
groups=package.groups,
|
||||||
|
installed_size=package.isize,
|
||||||
|
licenses=package.licenses,
|
||||||
|
provides=package.provides,
|
||||||
|
url=package.url)
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -20,25 +20,32 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from ahriman.core.exceptions import InvalidOption
|
from ahriman.core.exceptions import InvalidOption
|
||||||
|
|
||||||
|
|
||||||
class ReportSettings(Enum):
|
class ReportSettings(Enum):
|
||||||
'''
|
"""
|
||||||
report targets enumeration
|
report targets enumeration
|
||||||
|
:cvar Disabled: option which generates no report for testing purpose
|
||||||
:cvar HTML: html report generation
|
:cvar HTML: html report generation
|
||||||
'''
|
:cvar Email: email report generation
|
||||||
|
"""
|
||||||
|
|
||||||
|
Disabled = auto() # for testing purpose
|
||||||
HTML = auto()
|
HTML = auto()
|
||||||
|
Email = auto()
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_option(value: str) -> ReportSettings:
|
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
|
||||||
'''
|
"""
|
||||||
construct value from configuration
|
construct value from configuration
|
||||||
:param value: configuration value
|
:param value: configuration value
|
||||||
:return: parsed value
|
:return: parsed value
|
||||||
'''
|
"""
|
||||||
if value.lower() in ('html',):
|
if value.lower() in ("html",):
|
||||||
return ReportSettings.HTML
|
return cls.HTML
|
||||||
|
if value.lower() in ("email",):
|
||||||
|
return cls.Email
|
||||||
raise InvalidOption(value)
|
raise InvalidOption(value)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,72 +17,88 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import os
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Set, Type
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RepositoryPaths:
|
class RepositoryPaths:
|
||||||
'''
|
"""
|
||||||
repository paths holder. For the most operations with paths you want to use this object
|
repository paths holder. For the most operations with paths you want to use this object
|
||||||
:ivar root: repository root (i.e. ahriman home)
|
:ivar root: repository root (i.e. ahriman home)
|
||||||
:ivar architecture: repository architecture
|
:ivar architecture: repository architecture
|
||||||
'''
|
"""
|
||||||
|
|
||||||
root: str
|
root: Path
|
||||||
architecture: str
|
architecture: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cache(self) -> str:
|
def cache(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: directory for packages cache (mainly used for VCS packages)
|
:return: directory for packages cache (mainly used for VCS packages)
|
||||||
'''
|
"""
|
||||||
return os.path.join(self.root, 'cache')
|
return self.root / "cache"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chroot(self) -> str:
|
def chroot(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: directory for devtools chroot
|
:return: directory for devtools chroot
|
||||||
'''
|
"""
|
||||||
# for the chroot directory devtools will create own tree and we don't have to specify architecture here
|
# for the chroot directory devtools will create own tree and we don"t have to specify architecture here
|
||||||
return os.path.join(self.root, 'chroot')
|
return self.root / "chroot"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def manual(self) -> str:
|
def manual(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: directory for manual updates (i.e. from add command)
|
:return: directory for manual updates (i.e. from add command)
|
||||||
'''
|
"""
|
||||||
return os.path.join(self.root, 'manual', self.architecture)
|
return self.root / "manual" / self.architecture
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def packages(self) -> str:
|
def packages(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: directory for built packages
|
:return: directory for built packages
|
||||||
'''
|
"""
|
||||||
return os.path.join(self.root, 'packages', self.architecture)
|
return self.root / "packages" / self.architecture
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def repository(self) -> str:
|
def repository(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: repository directory
|
:return: repository directory
|
||||||
'''
|
"""
|
||||||
return os.path.join(self.root, 'repository', self.architecture)
|
return self.root / "repository" / self.architecture
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sources(self) -> str:
|
def sources(self) -> Path:
|
||||||
'''
|
"""
|
||||||
:return: directory for downloaded PKGBUILDs for current build
|
:return: directory for downloaded PKGBUILDs for current build
|
||||||
'''
|
"""
|
||||||
return os.path.join(self.root, 'sources', self.architecture)
|
return self.root / "sources" / self.architecture
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def known_architectures(cls: Type[RepositoryPaths], root: Path) -> Set[str]:
|
||||||
|
"""
|
||||||
|
get known architectures
|
||||||
|
:param root: repository root
|
||||||
|
:return: list of architectures for which tree is created
|
||||||
|
"""
|
||||||
|
paths = cls(root, "")
|
||||||
|
return {
|
||||||
|
path.name
|
||||||
|
for path in paths.repository.iterdir()
|
||||||
|
if path.is_dir()
|
||||||
|
}
|
||||||
|
|
||||||
def create_tree(self) -> None:
|
def create_tree(self) -> None:
|
||||||
'''
|
"""
|
||||||
create ahriman working tree
|
create ahriman working tree
|
||||||
'''
|
"""
|
||||||
os.makedirs(self.cache, mode=0o755, exist_ok=True)
|
self.cache.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||||
os.makedirs(self.chroot, mode=0o755, exist_ok=True)
|
self.chroot.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||||
os.makedirs(self.manual, mode=0o755, exist_ok=True)
|
self.manual.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||||
os.makedirs(self.packages, mode=0o755, exist_ok=True)
|
self.packages.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||||
os.makedirs(self.repository, mode=0o755, exist_ok=True)
|
self.repository.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||||
os.makedirs(self.sources, mode=0o755, exist_ok=True)
|
self.sources.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -20,29 +20,30 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from ahriman.core.exceptions import InvalidOption
|
from ahriman.core.exceptions import InvalidOption
|
||||||
|
|
||||||
|
|
||||||
class SignSettings(Enum):
|
class SignSettings(Enum):
|
||||||
'''
|
"""
|
||||||
sign targets enumeration
|
sign targets enumeration
|
||||||
:cvar SignPackages: sign each package
|
:cvar Packages: sign each package
|
||||||
:cvar SignRepository: sign repository database file
|
:cvar Repository: sign repository database file
|
||||||
'''
|
"""
|
||||||
|
|
||||||
SignPackages = auto()
|
Packages = auto()
|
||||||
SignRepository = auto()
|
Repository = auto()
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_option(value: str) -> SignSettings:
|
def from_option(cls: Type[SignSettings], value: str) -> SignSettings:
|
||||||
'''
|
"""
|
||||||
construct value from configuration
|
construct value from configuration
|
||||||
:param value: configuration value
|
:param value: configuration value
|
||||||
:return: parsed value
|
:return: parsed value
|
||||||
'''
|
"""
|
||||||
if value.lower() in ('package', 'packages', 'sign-package'):
|
if value.lower() in ("package", "packages", "sign-package"):
|
||||||
return SignSettings.SignPackages
|
return cls.Packages
|
||||||
if value.lower() in ('repository', 'sign-repository'):
|
if value.lower() in ("repository", "sign-repository"):
|
||||||
return SignSettings.SignRepository
|
return cls.Repository
|
||||||
raise InvalidOption(value)
|
raise InvalidOption(value)
|
||||||
|
49
src/ahriman/models/smtp_ssl_settings.py
Normal file
49
src/ahriman/models/smtp_ssl_settings.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
|
||||||
|
class SmtpSSLSettings(Enum):
|
||||||
|
"""
|
||||||
|
SMTP SSL mode enumeration
|
||||||
|
:cvar Disabled: no SSL enabled
|
||||||
|
:cvar SSL: use SMTP_SSL instead of normal SMTP client
|
||||||
|
:cvar STARTTLS: use STARTTLS in normal SMTP client
|
||||||
|
"""
|
||||||
|
|
||||||
|
Disabled = auto()
|
||||||
|
SSL = auto()
|
||||||
|
STARTTLS = auto()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_option(cls: Type[SmtpSSLSettings], value: str) -> SmtpSSLSettings:
|
||||||
|
"""
|
||||||
|
construct value from configuration
|
||||||
|
:param value: configuration value
|
||||||
|
:return: parsed value
|
||||||
|
"""
|
||||||
|
if value.lower() in ("ssl", "ssl/tls"):
|
||||||
|
return cls.SSL
|
||||||
|
if value.lower() in ("starttls",):
|
||||||
|
return cls.STARTTLS
|
||||||
|
return cls.Disabled
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -20,29 +20,32 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from ahriman.core.exceptions import InvalidOption
|
from ahriman.core.exceptions import InvalidOption
|
||||||
|
|
||||||
|
|
||||||
class UploadSettings(Enum):
|
class UploadSettings(Enum):
|
||||||
'''
|
"""
|
||||||
remote synchronization targets enumeration
|
remote synchronization targets enumeration
|
||||||
|
:cvar Disabled: no sync will be performed, required for testing purpose
|
||||||
:cvar Rsync: sync via rsync
|
:cvar Rsync: sync via rsync
|
||||||
:cvar S3: sync to Amazon S3
|
:cvar S3: sync to Amazon S3
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
Disabled = auto() # for testing purpose
|
||||||
Rsync = auto()
|
Rsync = auto()
|
||||||
S3 = auto()
|
S3 = auto()
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_option(value: str) -> UploadSettings:
|
def from_option(cls: Type[UploadSettings], value: str) -> UploadSettings:
|
||||||
'''
|
"""
|
||||||
construct value from configuration
|
construct value from configuration
|
||||||
:param value: configuration value
|
:param value: configuration value
|
||||||
:return: parsed value
|
:return: parsed value
|
||||||
'''
|
"""
|
||||||
if value.lower() in ('rsync',):
|
if value.lower() in ("rsync",):
|
||||||
return UploadSettings.Rsync
|
return cls.Rsync
|
||||||
if value.lower() in ('s3',):
|
if value.lower() in ("s3",):
|
||||||
return UploadSettings.S3
|
return cls.S3
|
||||||
raise InvalidOption(value)
|
raise InvalidOption(value)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -17,4 +17,4 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = '0.15.0'
|
__version__ = "1.2.5"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -28,11 +28,11 @@ HandlerType = Callable[[Request], Awaitable[StreamResponse]]
|
|||||||
|
|
||||||
|
|
||||||
def exception_handler(logger: Logger) -> Callable[[Request, HandlerType], Awaitable[StreamResponse]]:
|
def exception_handler(logger: Logger) -> Callable[[Request, HandlerType], Awaitable[StreamResponse]]:
|
||||||
'''
|
"""
|
||||||
exception handler middleware. Just log any exception (except for client ones)
|
exception handler middleware. Just log any exception (except for client ones)
|
||||||
:param logger: class logger
|
:param logger: class logger
|
||||||
:return: built middleware
|
:return: built middleware
|
||||||
'''
|
"""
|
||||||
@middleware
|
@middleware
|
||||||
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
||||||
try:
|
try:
|
||||||
@ -40,7 +40,7 @@ def exception_handler(logger: Logger) -> Callable[[Request, HandlerType], Awaita
|
|||||||
except HTTPClientError:
|
except HTTPClientError:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(f'exception during performing request to {request.path}', exc_info=True)
|
logger.exception("exception during performing request to %s", request.path)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return handle
|
return handle
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -23,10 +23,11 @@ from ahriman.web.views.ahriman import AhrimanView
|
|||||||
from ahriman.web.views.index import IndexView
|
from ahriman.web.views.index import IndexView
|
||||||
from ahriman.web.views.package import PackageView
|
from ahriman.web.views.package import PackageView
|
||||||
from ahriman.web.views.packages import PackagesView
|
from ahriman.web.views.packages import PackagesView
|
||||||
|
from ahriman.web.views.status import StatusView
|
||||||
|
|
||||||
|
|
||||||
def setup_routes(application: Application) -> None:
|
def setup_routes(application: Application) -> None:
|
||||||
'''
|
"""
|
||||||
setup all defined routes
|
setup all defined routes
|
||||||
|
|
||||||
Available routes are:
|
Available routes are:
|
||||||
@ -44,17 +45,21 @@ def setup_routes(application: Application) -> None:
|
|||||||
GET /api/v1/package/:base get package base status
|
GET /api/v1/package/:base get package base status
|
||||||
POST /api/v1/package/:base update package base status
|
POST /api/v1/package/:base update package base status
|
||||||
|
|
||||||
|
GET /api/v1/status get web service status itself
|
||||||
|
|
||||||
:param application: web application instance
|
:param application: web application instance
|
||||||
'''
|
"""
|
||||||
application.router.add_get('/', IndexView)
|
application.router.add_get("/", IndexView)
|
||||||
application.router.add_get('/index.html', IndexView)
|
application.router.add_get("/index.html", IndexView)
|
||||||
|
|
||||||
application.router.add_get('/api/v1/ahriman', AhrimanView)
|
application.router.add_get("/api/v1/ahriman", AhrimanView)
|
||||||
application.router.add_post('/api/v1/ahriman', AhrimanView)
|
application.router.add_post("/api/v1/ahriman", AhrimanView)
|
||||||
|
|
||||||
application.router.add_get('/api/v1/packages', PackagesView)
|
application.router.add_get("/api/v1/packages", PackagesView)
|
||||||
application.router.add_post('/api/v1/packages', PackagesView)
|
application.router.add_post("/api/v1/packages", PackagesView)
|
||||||
|
|
||||||
application.router.add_delete('/api/v1/packages/{package}', PackageView)
|
application.router.add_delete("/api/v1/packages/{package}", PackageView)
|
||||||
application.router.add_get('/api/v1/packages/{package}', PackageView)
|
application.router.add_get("/api/v1/packages/{package}", PackageView)
|
||||||
application.router.add_post('/api/v1/packages/{package}', PackageView)
|
application.router.add_post("/api/v1/packages/{package}", PackageView)
|
||||||
|
|
||||||
|
application.router.add_get("/api/v1/status", StatusView)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -24,19 +24,19 @@ from ahriman.web.views.base import BaseView
|
|||||||
|
|
||||||
|
|
||||||
class AhrimanView(BaseView):
|
class AhrimanView(BaseView):
|
||||||
'''
|
"""
|
||||||
service status web view
|
service status web view
|
||||||
'''
|
"""
|
||||||
|
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
'''
|
"""
|
||||||
get current service status
|
get current service status
|
||||||
:return: 200 with service status object
|
:return: 200 with service status object
|
||||||
'''
|
"""
|
||||||
return json_response(self.service.status.view())
|
return json_response(self.service.status.view())
|
||||||
|
|
||||||
async def post(self) -> Response:
|
async def post(self) -> Response:
|
||||||
'''
|
"""
|
||||||
update service status
|
update service status
|
||||||
|
|
||||||
JSON body must be supplied, the following model is used:
|
JSON body must be supplied, the following model is used:
|
||||||
@ -45,11 +45,11 @@ class AhrimanView(BaseView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
:return: 204 on success
|
:return: 204 on success
|
||||||
'''
|
"""
|
||||||
data = await self.request.json()
|
data = await self.request.json()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status = BuildStatusEnum(data['status'])
|
status = BuildStatusEnum(data["status"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPBadRequest(text=str(e))
|
raise HTTPBadRequest(text=str(e))
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@ -19,18 +19,18 @@
|
|||||||
#
|
#
|
||||||
from aiohttp.web import View
|
from aiohttp.web import View
|
||||||
|
|
||||||
from ahriman.core.watcher.watcher import Watcher
|
from ahriman.core.status.watcher import Watcher
|
||||||
|
|
||||||
|
|
||||||
class BaseView(View):
|
class BaseView(View):
|
||||||
'''
|
"""
|
||||||
base web view to make things typed
|
base web view to make things typed
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def service(self) -> Watcher:
|
def service(self) -> Watcher:
|
||||||
'''
|
"""
|
||||||
:return: build status watcher instance
|
:return: build status watcher instance
|
||||||
'''
|
"""
|
||||||
watcher: Watcher = self.request.app['watcher']
|
watcher: Watcher = self.request.app["watcher"]
|
||||||
return watcher
|
return watcher
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user