Compare commits

...

16 Commits

Author SHA1 Message Date
45b762e3d9 Release 0.11.2 2021-03-13 01:57:26 +03:00
c5db7e64ca process prepare call for vcs packages 2021-03-13 01:57:10 +03:00
0dd4d098f6 Release 0.11.1 2021-03-12 00:24:49 +03:00
4866548224 handle built packages during update 2021-03-12 00:24:26 +03:00
5d526e1bd8 Release 0.11.0 2021-03-12 00:15:21 +03:00
c66325ff38 fix interaction with web 2021-03-12 00:14:31 +03:00
371019f899 add depdendency manager and switch to pyalpm instead of expac 2021-03-12 00:04:37 +03:00
2d351fa94f allow to specify key overrides for packages 2021-03-11 04:06:20 +03:00
1770793e69 improvements
* multi-sign and multi-web configuration
* change default configuration to do not use architecture
* change units to be templated
* some refactoring
2021-03-11 03:57:23 +03:00
30ededb2cd Release 0.10.0 2021-03-11 01:59:05 +03:00
2fca108fa4 process null lock file 2021-03-11 01:58:33 +03:00
262d8d8647 multisign option 2021-03-11 01:39:45 +03:00
fd2049b334 web server support 2021-03-11 01:14:09 +03:00
422445da85 Release 0.9.1 2021-03-10 02:26:44 +03:00
aad893fe69 smart remove function and use built-in packages() function everywhere 2021-03-10 01:25:22 +03:00
8e72ee05ba Release 0.9.0 2021-03-08 16:19:12 +03:00
51 changed files with 1271 additions and 334 deletions

View File

@ -1,19 +1,22 @@
# ahriman configuration
Some groups can be specified for each architecture separately with default values. 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.
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).
## `settings` group
Base configuration settings:
Base configuration settings.
* `include` - path to directory with configuration files overrides, string, required.
* `logging` - path to logging configuration, string, required. Check `logging.ini` for reference.
## `aur` group
## `alpm` group
AUR related configuration:
libalpm and AUR related configuration.
* `url` - base url for AUR, string, required.
* `aur_url` - base url for AUR, string, required.
* `database` - path to pacman local database cache, string, required.
* `repositories` - list of pacman repositories, space separated list of strings, required.
* `root` - root for alpm library, string, required.
## `build_*` groups
@ -27,25 +30,26 @@ Build related configuration. Group name must refer to architecture, e.g. it shou
## `repository` group
Base repository settings:
Base repository settings.
* `name` - repository name, string, required.
* `root` - root path for application, string, required.
## `sign` group
## `sign_*` groups
Settings for signing packages or repository:
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign_x86_64` for x86_64 architecture.
* `enabled` - configuration flag to enable signing, string, required. Allowed values are `disabled`, `package` (sign each package separately), `repository` (sign repository database file).
* `key` - PGP key, string, optional.
* `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_*` settings - PGP key which will be used for specific packages, string, optional. For example, if there is `key_yay` option the specified key will be used for yay package and default key for others.
## `report` group
Report generation settings:
Report generation settings.
* `target` - list of reports to be generated, space separated list of strings, optional. Allowed values are `html`.
### `html_*` group
### `html_*` groups
Group name must refer to architecture, e.g. it should be `html_x86_64` for x86_64 architecture.
@ -56,18 +60,26 @@ Group name must refer to architecture, e.g. it should be `html_x86_64` for x86_6
## `upload` group
Remote synchronization settings:
Remote synchronization settings.
* `target` - list of synchronizations to be used, space separated list of strings, optional. Allowed values are `rsync`, `s3`.
### `s3_*` group
### `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`.
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
### `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`.
* `bucket` - bucket name (e.g. `s3://bucket/path`), string, required.
### `rsync_*` group
## `web_*` 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`.
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.
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
* `host` - host to bind, string, optional.
* `port` - port to bind, int, optional.
* `templates` - path to templates directory, string, required.

View File

@ -2,20 +2,26 @@
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
## Features
* Install-configure-forget manager for own repository
* Multi-architecture support
* VCS packages support
* Sign support with gpg (repository, package, per package settings)
* Synchronization to remote services (rsync, s3) and report generation (html)
* Dependency manager
* Repository status interface
## Installation and run
* Install package as usual.
* Change settings if required, see `CONFIGURING.md` for more details.
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`).
* Configure build tools (it might be required if your package will use any custom repositories):
* create build command if required, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/custom-x86_64-build` (you can choose any name for command);
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/custom-x86_64-build` (you can choose any name for command);
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,custom}.conf`;
* change configuration file: add your own repository, add multilib repository;
* set `build.build_command` to point to your command;
* configure `/etc/sudoers.d/ahriman` to allow to run command without password.
* change configuration file, add your own repository, add multilib repository etc;
* set `build.build_command` setting to point to your command;
* configure `/etc/sudoers.d/ahriman` to allow running command without password.
* Start and enable `ahriman.timer` via `systemctl`.
* Add packages by using `ahriman add {package}` command.
## Limitations
* It does not manage dependencies, so you have to add them before main package.

View File

@ -29,7 +29,7 @@ find . -type f -name '*src.tar.xz' -not -name "*$VERSION-src.tar.xz" -exec rm -f
read -p "Publish release? [Ny] " -n 1 -r
if [[ $REPLY =~ ^[Yy]$ ]]; then
git add package/archlinux/PKGBUILD
git add package/archlinux/PKGBUILD src/ahriman/version.py
git commit -m "Release $VERSION" && git push
git tag "$VERSION" && git push --tags
fi

View File

@ -1,28 +1,29 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=0.1.0
pkgver=0.11.2
pkgrel=1
pkgdesc="ArcHlinux ReposItory MANager"
arch=('any')
url="https://github.com/arcan1s/ahriman"
license=('GPL3')
depends=('devtools' 'expac' 'git' 'python-aur' 'python-srcinfo')
depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-srcinfo')
makedepends=('python-pip')
optdepends=('aws-cli: sync to s3'
'breezy: -bzr packages support'
'darcs: -darcs packages support'
'gnupg: package and repository sign'
'mercurial: -hg packages support'
'python-aiohttp: web server'
'python-aiohttp-jinja2: web server'
'python-jinja: html report generation'
'python-requests: web server'
'rsync: sync by using rsync'
'subversion: -svn packages support')
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
'ahriman.sudoers'
'ahriman.sysusers'
'ahriman.tmpfiles')
sha512sums=('f77b434f5174b2e7a8817bdcc1883e9ded3bd57c30be09b438ec55b65aa83bcd1326f1241ad9ca86552b5fb71f9a15637af7997b37636db9d9ee61c24d235c5c'
'8c9b5b63ac3f7b4d9debaf801a1e9c060877c33d3ecafe18010fcca778e5fa2f2e46909d3d0ff1b229ff8aa978445d8243fd36e1fc104117ed678d5e21901167'
sha512sums=('20348a0cf94893a461c0e5ca30da5e48821d276983fcb5ea92245e3e98bcc505cb0ba23ba08b827e2fbd717d807dd269de175536f26eddd6ae152f9aa0b418b7'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini'
@ -39,7 +40,6 @@ package() {
python setup.py install --root="$pkgdir"
install -Dm400 "$srcdir/$pkgname.sudoers" "$pkgdir/etc/sudoers.d/$pkgname"
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
}

View File

@ -1,4 +0,0 @@
# Used by ArcHlinux ReposItory MANager with default settings
Cmnd_Alias ARCHBUILD_CMD = /usr/bin/extra-x86_64-build *, /usr/bin/multilib-build *
ahriman ALL=(ALL) NOPASSWD: ARCHBUILD_CMD

View File

@ -2,10 +2,13 @@
include = /etc/ahriman.ini.d
logging = /etc/ahriman.ini.d/logging.ini
[aur]
url = https://aur.archlinux.org
[alpm]
aur_url = https://aur.archlinux.org
database = /var/lib/pacman
repositories = core extra community multilib
root = /
[build_x86_64]
[build]
archbuild_flags =
build_command = extra-x86_64-build
ignore_packages =
@ -17,23 +20,28 @@ name = aur-clone
root = /var/lib/ahriman
[sign]
enabled = disabled
target =
key =
[report]
target =
[html_x86_64]
[html]
path =
homepage =
link_path =
template_path = /usr/share/ahriman/index.jinja2
template_path = /usr/share/ahriman/repo-index.jinja2
[upload]
target =
[s3_x86_64]
[rsync]
remote =
[s3]
bucket =
[rsync_x86_64]
remote =
[web]
host =
port =
templates = /usr/share/ahriman

View File

@ -1,8 +1,8 @@
[loggers]
keys = root,builder,build_details
keys = root,builder,build_details,http
[handlers]
keys = console_handler,build_file_handler,file_handler
keys = console_handler,build_file_handler,file_handler,http_handler
[formatters]
keys = generic_format
@ -25,6 +25,12 @@ level = DEBUG
formatter = generic_format
args = ('/var/log/ahriman/build.log', 'a', 20971520, 20)
[handler_http_handler]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic_format
args = ('/var/log/ahriman/http.log', 'a', 20971520, 20)
[formatter_generic_format]
format = %(asctime)s : %(levelname)s : %(funcName)s : %(message)s
datefmt =
@ -45,3 +51,9 @@ level = DEBUG
handlers = build_file_handler
qualname = build_details
propagate = 0
[logger_http]
level = DEBUG
handlers = http_handler
qualname = http
propagate = 0

View File

@ -0,0 +1,15 @@
[Unit]
Description=ArcHlinux ReposItory MANager web server (%I architecture)
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/ahriman --architecture %i web
User=ahriman
Group=ahriman
KillSignal=SIGQUIT
SuccessExitStatus=SIGQUIT
[Install]
WantedBy=multi-user.target

View File

@ -1,7 +0,0 @@
[Unit]
Description=ArcHlinux ReposItory MANager
[Service]
ExecStart=/usr/bin/ahriman --architecture x86_64 update
User=ahriman
Group=ahriman

View File

@ -0,0 +1,7 @@
[Unit]
Description=ArcHlinux ReposItory MANager (%I architecture)
[Service]
ExecStart=/usr/bin/ahriman --architecture %i update
User=ahriman
Group=ahriman

View File

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

View File

@ -0,0 +1,86 @@
<!doctype html>
<html lang="en">
<head>
<title>{{ repository|e }}</title>
<style>
:root {
--color-building: 250, 255, 146;
--color-failed: 255, 94, 94;
--color-pending: 250, 255, 146;
--color-success: 121, 255, 94;
--color-unknown: 197, 197, 197;
}
@keyframes blink-building {
0% { background-color: rgba(var(--color-building), 1.0); }
10% { background-color: rgba(var(--color-building), 0.9); }
20% { background-color: rgba(var(--color-building), 0.8); }
30% { background-color: rgba(var(--color-building), 0.7); }
40% { background-color: rgba(var(--color-building), 0.6); }
50% { background-color: rgba(var(--color-building), 0.5); }
60% { background-color: rgba(var(--color-building), 0.4); }
70% { background-color: rgba(var(--color-building), 0.3); }
80% { background-color: rgba(var(--color-building), 0.2); }
90% { background-color: rgba(var(--color-building), 0.1); }
100% { background-color: rgba(var(--color-building), 0.0); }
}
table, th, td {
padding: 5px;
}
td.package {
font-weight: bolder;
}
td.package-unknown {
background-color: rgba(var(--color-unknown), 1.0);
}
td.package-pending {
background-color: rgba(var(--color-pending), 1.0);
}
td.package-building {
background-color: rgba(var(--color-building), 1.0);
animation-name: blink-building;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
}
td.package-failed {
background-color: rgba(var(--color-failed), 1.0);
}
td.package-success {
background-color: rgba(var(--color-success), 1.0);
}
</style>
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
</head>
<body>
<h1>ahriman {{ version|e }}</h1>
<table class="sortable" id="builds">
<tr>
<th>package base</th>
<th>packages</th>
<th>version</th>
<th>architecture</th>
<th>timestamp</th>
<th>status</th>
</tr>
{% for package in packages %}
<tr>
<td class="package"><a href="{{ package.web_url|e }}" title="{{ package.base|e }}">{{ package.base|e }}</a></td>
<td>{{ package.packages|join("<br>"|safe) }}</td>
<td>{{ package.version|e }}</td>
<td>{{ architecture|e }}</td>
<td>{{ package.timestamp|e }}</td>
<td class="package-{{ package.status|e }}">{{ package.status|e }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>

View File

@ -5,16 +5,17 @@
</head>
<body>
<h1>{{ repository|e }} ArchLinux custom repository</h1>
<h1>Archlinux custom repository</h1>
{% if pgp_key is not none %}
<p>All packages are signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}" title="key search">{{ pgp_key|e }}</a>.</p>
<p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}" title="key search">{{ pgp_key|e }}</a>.</p>
{% endif %}
<code>
$ cat /etc/pacman.conf<br>
[{{ repository|e }}]<br>
Server = {{ link_path|e }}
Server = {{ link_path|e }}<br>
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
</code>
<p>Packages:</p>
@ -28,4 +29,4 @@
<footer><a href="{{ homepage|e }}" title="homepage">Homepage</a></footer>
{% endif %}
</body>
</html>
</html>

View File

@ -28,6 +28,7 @@ setup(
],
install_requires=[
'aur',
'pyalpm',
'srcinfo',
],
setup_requires=[
@ -42,17 +43,26 @@ setup(
'package/bin/ahriman',
],
data_files=[
('/etc', ['package/etc/ahriman.ini']),
('/etc/ahriman.ini.d', ['package/etc/ahriman.ini.d/logging.ini']),
('lib/systemd/system', [
'package/lib/systemd/system/ahriman.service',
'package/lib/systemd/system/ahriman.timer'
('/etc', [
'package/etc/ahriman.ini',
]),
('/etc/ahriman.ini.d', [
'package/etc/ahriman.ini.d/logging.ini',
]),
('lib/systemd/system', [
'package/lib/systemd/system/ahriman@.service',
'package/lib/systemd/system/ahriman@.timer',
'package/lib/systemd/system/ahriman-web@.service',
]),
('share/ahriman', [
'package/share/ahriman/build-status.jinja2',
'package/share/ahriman/repo-index.jinja2',
]),
('share/ahriman', ['package/share/ahriman/index.jinja2']),
],
extras_require={
'html-templates': ['Jinja2'],
'test': ['coverage', 'pytest'],
'web': ['Jinja2', 'aiohttp', 'aiohttp_jinja2', 'requests'],
},
)

View File

@ -18,55 +18,38 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import os
import ahriman.version as version
from ahriman.application.application import Application
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
def _get_app(args: argparse.Namespace) -> Application:
config = _get_config(args.config)
return Application(args.architecture, config)
def _get_config(config_path: str) -> Configuration:
config = Configuration()
config.load(config_path)
config.load_logging()
return config
def _remove_lock(path: str) -> None:
if os.path.exists(path):
os.remove(path)
def add(args: argparse.Namespace) -> None:
_get_app(args).add(args.package)
Application.from_args(args).add(args.package, args.without_dependencies)
def rebuild(args: argparse.Namespace) -> None:
app = _get_app(args)
app = Application.from_args(args)
packages = app.repository.packages()
app.update(packages)
def remove(args: argparse.Namespace) -> None:
_get_app(args).remove(args.package)
Application.from_args(args).remove(args.package)
def report(args: argparse.Namespace) -> None:
_get_app(args).report(args.target)
Application.from_args(args).report(args.target)
def sync(args: argparse.Namespace) -> None:
_get_app(args).sync(args.target)
Application.from_args(args).sync(args.target)
def update(args: argparse.Namespace) -> None:
app = _get_app(args)
app = Application.from_args(args)
log_fn = lambda line: print(line) if args.dry_run else app.logger.info(line)
packages = app.get_updates(args.no_aur, args.no_manual, args.no_vcs, log_fn)
if args.dry_run:
@ -74,6 +57,13 @@ def update(args: argparse.Namespace) -> None:
app.update(packages)
def web(args: argparse.Namespace) -> None:
from ahriman.web.web import run_server, setup_service
config = Configuration.from_path(args.config)
app = setup_service(args.architecture, config)
run_server(app, args.architecture)
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager')
parser.add_argument('-a', '--architecture', help='target architecture', required=True)
@ -85,6 +75,7 @@ if __name__ == '__main__':
add_parser = subparsers.add_parser('add', description='add package')
add_parser.add_argument('package', help='package name or archive path', nargs='+')
add_parser.add_argument('--without-dependencies', help='do not add dependencies', action='store_true')
add_parser.set_defaults(fn=add)
check_parser = subparsers.add_parser('check', description='check for updates')
@ -112,20 +103,13 @@ if __name__ == '__main__':
update_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true')
update_parser.set_defaults(fn=update)
web_parser = subparsers.add_parser('web', description='start web server')
web_parser.set_defaults(fn=web, lock=None)
args = parser.parse_args()
if args.force:
_remove_lock(args.lock)
if os.path.exists(args.lock):
raise RuntimeError('Another application instance is run')
if 'fn' not in args:
parser.print_help()
exit(1)
try:
open(args.lock, 'w').close()
with Lock(args.lock, args.force):
args.fn(args)
finally:
_remove_lock(args.lock)

View File

@ -17,15 +17,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import argparse
import logging
import os
import shutil
from typing import Callable, List, Optional
from typing import Callable, Iterable, List, Optional, Set, Type
from ahriman.core.build_tools.task import Task
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.core.tree import Tree
from ahriman.models.package import Package
@ -37,6 +41,19 @@ class Application:
self.architecture = architecture
self.repository = Repository(architecture, config)
@classmethod
def from_args(cls: Type[Application], args: argparse.Namespace) -> Application:
config = Configuration.from_path(args.config)
return cls(args.architecture, config)
def _known_packages(self) -> Set[str]:
known_packages = set()
# local set
for package in self.repository.packages():
known_packages.update(package.packages)
known_packages.update(self.repository.pacman.all_packages())
return known_packages
def _finalize(self) -> None:
self.report()
self.sync()
@ -44,47 +61,71 @@ class Application:
def get_updates(self, no_aur: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]:
updates = []
checked: List[str] = []
if not no_aur:
updates.extend(self.repository.updates_aur(no_vcs, checked))
updates.extend(self.repository.updates_aur(no_vcs))
if not no_manual:
updates.extend(self.repository.updates_manual(checked))
updates.extend(self.repository.updates_manual())
for package in updates:
log_fn(f'{package.name} = {package.version}')
log_fn(f'{package.base} = {package.version}')
return updates
def add(self, names: List[str]) -> None:
def add_manual(name: str) -> None:
package = Package.load(name, self.config.get('aur', 'url'))
Task.fetch(os.path.join(self.repository.paths.manual, package.name), package.url)
def add(self, names: Iterable[str], without_dependencies: bool) -> None:
known_packages = self._known_packages()
def add_manual(name: str) -> str:
package = Package.load(name, self.repository.pacman, self.config.get('alpm', 'aur_url'))
path = os.path.join(self.repository.paths.manual, package.base)
Task.fetch(path, package.git_url)
return path
def add_archive(src: str) -> None:
dst = os.path.join(self.repository.paths.packages, os.path.basename(src))
shutil.move(src, dst)
for name in names:
if os.path.isfile(name):
add_archive(name)
else:
add_manual(name)
def process_dependencies(path: str) -> None:
if without_dependencies:
return
dependencies = Package.dependencies(path)
self.add(dependencies.difference(known_packages), without_dependencies)
def remove(self, names: List[str]) -> None:
def process_single(name: str) -> None:
if not os.path.isfile(name):
path = add_manual(name)
process_dependencies(path)
else:
add_archive(name)
for name in names:
process_single(name)
def remove(self, names: Iterable[str]) -> None:
self.repository.process_remove(names)
self._finalize()
def report(self, target: Optional[List[str]] = None) -> None:
def report(self, target: Optional[Iterable[str]] = None) -> None:
targets = target or None
self.repository.process_report(targets)
def sync(self, target: Optional[List[str]] = None) -> None:
def sync(self, target: Optional[Iterable[str]] = None) -> None:
targets = target or None
self.repository.process_sync(targets)
def update(self, updates: List[Package]) -> None:
packages = self.repository.process_build(updates)
self.repository.process_update(packages)
self._finalize()
def update(self, updates: Iterable[Package]) -> None:
def process_update(paths: Iterable[str]) -> None:
self.repository.process_update(paths)
self._finalize()
# process built packages
packages = self.repository.packages_built()
process_update(packages)
# process manual packages
tree = Tree()
tree.load(updates)
for num, level in enumerate(tree.levels()):
self.logger.info(f'processing level #{num} {[package.base for package in level]}')
packages = self.repository.process_build(level)
process_update(packages)

View File

@ -0,0 +1,58 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# 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 os
from typing import Optional
from ahriman.core.exceptions import DuplicateRun
class Lock:
def __init__(self, path: Optional[str], force: bool) -> None:
self.path = path
self.force = force
def __enter__(self):
if self.force:
self.remove()
self.check()
self.create()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.remove()
def check(self) -> None:
if self.path is None:
return
if os.path.exists(self.path):
raise DuplicateRun()
def create(self) -> None:
if self.path is None:
return
open(self.path, 'w').close()
def remove(self) -> None:
if self.path is None:
return
if os.path.exists(self.path):
os.remove(self.path)

View File

@ -0,0 +1,40 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# 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 pyalpm import Handle
from typing import List, Set
from ahriman.core.configuration import Configuration
class Pacman:
def __init__(self, config: Configuration) -> None:
root = config.get('alpm', 'root')
pacman_root = config.get('alpm', 'database')
self.handle = Handle(root, pacman_root)
for repository in config.getlist('alpm', 'repositories'):
self.handle.register_syncdb(repository, 0) # 0 is pgp_level
def all_packages(self) -> List[str]:
result: Set[str] = set()
for database in self.handle.get_syncdbs():
result.update({package.name for package in database.pkgcache})
return list(result)

View File

@ -27,7 +27,7 @@ from ahriman.core.util import check_output
from ahriman.models.repository_paths import RepositoryPaths
class RepoWrapper:
class Repo:
def __init__(self, name: str, paths: RepositoryPaths, sign_args: List[str]) -> None:
self.logger = logging.getLogger('build_details')
@ -46,13 +46,12 @@ class RepoWrapper:
cwd=self.paths.repository,
logger=self.logger)
def remove(self, path: str, package: str) -> None:
os.remove(path)
sign_path = f'{path}.sig'
if os.path.exists(sign_path):
os.remove(sign_path)
def remove(self, prefix: str, package: str) -> None:
for fn in filter(lambda f: f.startswith(prefix), os.listdir(self.paths.repository)):
full_path = os.path.join(self.paths.repository, fn)
os.remove(full_path)
check_output(
'repo-remove', *self.sign_args, self.repo_path, package,
exception=BuildFailed(path),
exception=BuildFailed(package),
cwd=self.paths.repository,
logger=self.logger)

View File

@ -39,14 +39,14 @@ class Task:
self.paths = paths
section = config.get_section_name('build', architecture)
self.archbuild_flags = config.get_list(section, 'archbuild_flags')
self.archbuild_flags = config.getlist(section, 'archbuild_flags')
self.build_command = config.get(section, 'build_command')
self.makepkg_flags = config.get_list(section, 'makepkg_flags')
self.makechrootpkg_flags = config.get_list(section, 'makechrootpkg_flags')
self.makepkg_flags = config.getlist(section, 'makepkg_flags')
self.makechrootpkg_flags = config.getlist(section, 'makechrootpkg_flags')
@property
def git_path(self) -> str:
return os.path.join(self.paths.sources, self.package.name)
return os.path.join(self.paths.sources, self.package.base)
@staticmethod
def fetch(local: str, remote: str) -> None:
@ -58,19 +58,19 @@ class Task:
cmd.extend(self.archbuild_flags)
cmd.extend(['--'] + self.makechrootpkg_flags)
cmd.extend(['--'] + self.makepkg_flags)
self.logger.info(f'using {cmd} for {self.package.name}')
self.logger.info(f'using {cmd} for {self.package.base}')
check_output(
*cmd,
exception=BuildFailed(self.package.name),
exception=BuildFailed(self.package.base),
cwd=self.git_path,
logger=self.build_logger)
# well it is not actually correct, but we can deal with it
return check_output('makepkg', '--packagelist',
exception=BuildFailed(self.package.name),
exception=BuildFailed(self.package.base),
cwd=self.git_path).splitlines()
def clone(self, path: Optional[str] = None) -> None:
git_path = path or self.git_path
return Task.fetch(git_path, self.package.url)
return Task.fetch(git_path, self.package.git_url)

View File

@ -17,11 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import configparser
import os
from logging.config import fileConfig
from typing import List, Optional
from typing import List, Optional, Type
# built-in configparser extension
@ -35,7 +37,14 @@ class Configuration(configparser.RawConfigParser):
def include(self) -> str:
return self.get('settings', 'include')
def get_list(self, section: str, key: str) -> List[str]:
@classmethod
def from_path(cls: Type[Configuration], path: str) -> Configuration:
config = cls()
config.load(path)
config.load_logging()
return config
def getlist(self, section: str, key: str) -> List[str]:
raw = self.get(section, key, fallback=None)
if not raw: # empty string or none
return []
@ -52,8 +61,7 @@ class Configuration(configparser.RawConfigParser):
def load_includes(self) -> None:
try:
include_dir = self.include
for conf in filter(lambda p: p.endswith('.ini'), sorted(os.listdir(include_dir))):
for conf in filter(lambda p: p.endswith('.ini'), sorted(os.listdir(self.include))):
self.read(os.path.join(self.include, conf))
except (FileNotFoundError, configparser.NoOptionError):
pass

View File

@ -25,7 +25,17 @@ class BuildFailed(Exception):
Exception.__init__(self, f'Package {package} build failed, check logs for details')
class InvalidOptionException(Exception):
class DuplicateRun(Exception):
def __init__(self) -> None:
Exception.__init__(self, 'Another application instance is run')
class InitializeException(Exception):
def __init__(self) -> None:
Exception.__init__(self, 'Could not load service')
class InvalidOption(Exception):
def __init__(self, value: Any) -> None:
Exception.__init__(self, f'Invalid or unknown option value `{value}`')
@ -35,16 +45,11 @@ class InvalidPackageInfo(Exception):
Exception.__init__(self, f'There are errors during reading package information: `{details}`')
class MissingConfiguration(Exception):
def __init__(self, name: str) -> None:
Exception.__init__(self, f'No section `{name}` found')
class ReportFailed(Exception):
def __init__(self, cause: Exception) -> None:
Exception.__init__(self, f'Report failed with reason {cause}')
def __init__(self) -> None:
Exception.__init__(self, 'Report failed')
class SyncFailed(Exception):
def __init__(self, cause: Exception) -> None:
Exception.__init__(self, f'Sync failed with reason {cause}')
def __init__(self) -> None:
Exception.__init__(self, 'Sync failed')

View File

@ -1,7 +1,7 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
@ -32,17 +32,15 @@ class HTML(Report):
def __init__(self, architecture: str, config: Configuration) -> None:
Report.__init__(self, architecture, config)
section = self.config.get_section_name('html', self.architecture)
section = config.get_section_name('html', architecture)
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
if SignSettings.from_option(config.get('sign', 'enabled')) != SignSettings.Disabled:
self.pgp_key = config.get('sign', 'key')
else:
self.pgp_key = None
self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist('sign', 'target')]
self.pgp_key = config.get('sign', 'key', fallback=None)
self.homepage = config.get(section, 'homepage', fallback=None)
self.repository = config.get('repository', 'name')
@ -62,6 +60,8 @@ class HTML(Report):
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=packages,
pgp_key=self.pgp_key,
repository=self.repository)

View File

@ -1,7 +1,7 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
@ -27,9 +27,9 @@ from ahriman.models.report_settings import ReportSettings
class Report:
def __init__(self, architecture: str, config: Configuration) -> None:
self.logger = logging.getLogger('builder')
self.architecture = architecture
self.config = config
self.logger = logging.getLogger('builder')
@staticmethod
def run(architecture: str, config: Configuration, target: str, path: str) -> None:
@ -38,13 +38,13 @@ class Report:
from ahriman.core.report.html import HTML
report: Report = HTML(architecture, config)
else:
from ahriman.core.report.dummy import Dummy
report = Dummy(architecture, config)
report = Report(architecture, config)
try:
report.generate(path)
except Exception as e:
raise ReportFailed(e) from e
except Exception:
report.logger.exception('report generation failed', exc_info=True)
raise ReportFailed()
def generate(self, path: str) -> None:
raise NotImplementedError
pass

View File

@ -21,15 +21,17 @@ import logging
import os
import shutil
from typing import Dict, List, Optional
from typing import Dict, Iterable, List, Optional
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.repo import Repo
from ahriman.core.build_tools.task import Task
from ahriman.core.configuration import Configuration
from ahriman.core.repo.repo_wrapper import RepoWrapper
from ahriman.core.report.report import Report
from ahriman.core.sign.gpg_wrapper import GPGWrapper
from ahriman.core.sign.gpg import GPG
from ahriman.core.upload.uploader import Uploader
from ahriman.core.util import package_like
from ahriman.core.watcher.client import Client
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
@ -41,14 +43,16 @@ class Repository:
self.architecture = architecture
self.config = config
self.aur_url = config.get('aur', 'url')
self.aur_url = config.get('alpm', 'aur_url')
self.name = config.get('repository', 'name')
self.paths = RepositoryPaths(config.get('repository', 'root'), self.architecture)
self.paths = RepositoryPaths(config.get('repository', 'root'), architecture)
self.paths.create_tree()
self.sign = GPGWrapper(config)
self.wrapper = RepoWrapper(self.name, self.paths, self.sign.repository_sign_args)
self.pacman = Pacman(config)
self.sign = GPG(architecture, config)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.web = Client.load(architecture, config)
def _clear_build(self) -> None:
for package in os.listdir(self.paths.sources):
@ -59,8 +63,8 @@ class Repository:
shutil.rmtree(os.path.join(self.paths.manual, package))
def _clear_packages(self) -> None:
for package in os.listdir(self.paths.packages):
os.remove(os.path.join(self.paths.packages, package))
for package in self.packages_built():
os.remove(package)
def packages(self) -> List[Package]:
result: Dict[str, Package] = {}
@ -69,17 +73,22 @@ class Repository:
continue
full_path = os.path.join(self.paths.repository, fn)
try:
local = Package.load(full_path, self.aur_url)
if local.name in result:
continue
result[local.name] = local
local = Package.load(full_path, self.pacman, self.aur_url)
result.setdefault(local.base, local).packages.update(local.packages)
except Exception:
self.logger.exception(f'could not load package from {fn}', exc_info=True)
continue
return list(result.values())
def process_build(self, updates: List[Package]) -> List[str]:
def packages_built(self) -> List[str]:
return [
os.path.join(self.paths.packages, fn)
for fn in os.listdir(self.paths.packages)
]
def process_build(self, updates: Iterable[Package]) -> List[str]:
def build_single(package: Package) -> None:
self.web.set_building(package.base)
task = Task(package, self.architecture, self.config, self.paths)
task.clone()
built = task.build()
@ -91,93 +100,97 @@ class Repository:
try:
build_single(package)
except Exception:
self.logger.exception(f'{package.name} ({self.architecture}) build exception', exc_info=True)
self.web.set_failed(package.base)
self.logger.exception(f'{package.base} ({self.architecture}) build exception', exc_info=True)
continue
self._clear_build()
return [
os.path.join(self.paths.packages, fn)
for fn in os.listdir(self.paths.packages)
]
return self.packages_built()
def process_remove(self, packages: List[str]) -> str:
for fn in os.listdir(self.paths.repository):
if not package_like(fn):
continue
full_path = os.path.join(self.paths.repository, fn)
def process_remove(self, packages: Iterable[str]) -> str:
def remove_single(package: str) -> None:
try:
local = Package.load(full_path, self.aur_url)
if local.name not in packages:
continue
self.wrapper.remove(full_path, local.name)
self.repo.remove(package, package)
except Exception:
self.logger.exception(f'could not load package from {fn}', exc_info=True)
continue
self.logger.exception(f'could not remove {package}', exc_info=True)
return self.wrapper.repo_path
for local in self.packages():
if local.base in packages:
to_remove = local.packages
elif local.packages.intersection(packages):
to_remove = local.packages.intersection(packages)
else:
to_remove = set()
self.web.remove(local.base, to_remove)
for package in to_remove:
remove_single(package)
def process_report(self, targets: Optional[List[str]]) -> None:
return self.repo.repo_path
def process_report(self, targets: Optional[Iterable[str]]) -> None:
if targets is None:
targets = self.config.get_list('report', 'target')
targets = self.config.getlist('report', 'target')
for target in targets:
Report.run(self.architecture, self.config, target, self.paths.repository)
def process_sync(self, targets: Optional[List[str]]) -> None:
def process_sync(self, targets: Optional[Iterable[str]]) -> None:
if targets is None:
targets = self.config.get_list('upload', 'target')
targets = self.config.getlist('upload', 'target')
for target in targets:
Uploader.run(self.architecture, self.config, target, self.paths.repository)
def process_update(self, packages: List[str]) -> str:
def process_update(self, packages: Iterable[str]) -> str:
for package in packages:
files = self.sign.sign_package(package)
for src in files:
dst = os.path.join(self.paths.repository, os.path.basename(src))
shutil.move(src, dst)
package_fn = os.path.join(self.paths.repository, os.path.basename(package))
self.wrapper.add(package_fn)
local = Package.load(package, self.pacman, self.aur_url) # we will use it for status reports
try:
files = self.sign.sign_package(package, local.base)
for src in files:
dst = os.path.join(self.paths.repository, os.path.basename(src))
shutil.move(src, dst)
package_fn = os.path.join(self.paths.repository, os.path.basename(package))
self.repo.add(package_fn)
self.web.set_success(local)
except Exception:
self.logger.exception(f'could not process {package}', exc_info=True)
self.web.set_failed(local.base)
self._clear_packages()
return self.wrapper.repo_path
return self.repo.repo_path
def updates_aur(self, no_vcs: bool, checked: List[str]) -> List[Package]:
def updates_aur(self, no_vcs: bool) -> List[Package]:
result: List[Package] = []
ignore_list = self.config.get_list(
self.config.get_section_name('build', self.architecture), 'ignore_packages')
for fn in os.listdir(self.paths.repository):
if not package_like(fn):
continue
build_section = self.config.get_section_name('build', self.architecture)
ignore_list = self.config.getlist(build_section, 'ignore_packages')
try:
local = Package.load(os.path.join(self.paths.repository, fn), self.aur_url)
remote = Package.load(local.name, self.aur_url)
except Exception:
self.logger.exception(f'could not load package from {fn}', exc_info=True)
continue
if local.name in checked:
continue
if local.name in ignore_list:
for local in self.packages():
if local.base in ignore_list:
continue
if local.is_vcs and no_vcs:
continue
if local.is_outdated(remote):
result.append(remote)
checked.append(local.name)
try:
remote = Package.load(local.base, self.pacman, self.aur_url)
if local.is_outdated(remote):
result.append(remote)
self.web.set_pending(local.base)
except Exception:
self.web.set_failed(local.base)
self.logger.exception(f'could not load remote package {local.base}', exc_info=True)
continue
return result
def updates_manual(self, checked: List[str]) -> List[Package]:
def updates_manual(self) -> List[Package]:
result: List[Package] = []
for fn in os.listdir(self.paths.manual):
local = Package.load(os.path.join(self.paths.manual, fn), self.aur_url)
if local.name in checked:
continue
result.append(local)
checked.append(local.name)
try:
local = Package.load(os.path.join(self.paths.manual, fn), self.pacman, self.aur_url)
result.append(local)
self.web.set_unknown(local)
except Exception:
self.logger.exception(f'could not add package from {fn}', exc_info=True)
self._clear_manual()
return result

View File

@ -28,41 +28,42 @@ from ahriman.core.util import check_output
from ahriman.models.sign_settings import SignSettings
class GPGWrapper:
class GPG:
def __init__(self, config: Configuration) -> None:
def __init__(self, architecture: str, config: Configuration) -> None:
self.logger = logging.getLogger('build_details')
self.key = config.get('sign', 'key', fallback=None)
self.sign = SignSettings.from_option(config.get('sign', 'enabled'))
self.config = config
self.section = config.get_section_name('sign', architecture)
self.target = [SignSettings.from_option(opt) for opt in config.getlist(self.section, 'target')]
self.default_key = config.get(self.section, 'key') if self.target else ''
@property
def repository_sign_args(self) -> List[str]:
if self.sign != SignSettings.SignRepository:
if SignSettings.SignRepository not in self.target:
return []
return ['--sign', '--key', self.key] if self.key else ['--sign']
return ['--sign', '--key', self.default_key]
def process(self, path: str) -> List[str]:
def process(self, path: str, key: str) -> List[str]:
check_output(
*self.sign_cmd(path),
*self.sign_cmd(path, key),
exception=BuildFailed(path),
cwd=os.path.dirname(path),
logger=self.logger)
return [path, f'{path}.sig']
def sign_cmd(self, path: str) -> List[str]:
def sign_cmd(self, path: str, key: str) -> List[str]:
cmd = ['gpg']
if self.key is not None:
cmd.extend(['-u', self.key])
cmd.extend(['-u', key])
cmd.extend(['-b', path])
return cmd
def sign_package(self, path: str) -> List[str]:
if self.sign != SignSettings.SignPackages:
def sign_package(self, path: str, base: str) -> List[str]:
if SignSettings.SignPackages not in self.target:
return [path]
return self.process(path)
key = self.config.get(self.section, f'key_{base}', fallback=self.default_key)
return self.process(path, key)
def sign_repository(self, path: str) -> List[str]:
if self.sign != SignSettings.SignRepository:
if SignSettings.SignRepository not in self.target:
return [path]
return self.process(path)
return self.process(path, self.default_key)

75
src/ahriman/core/tree.py Normal file
View File

@ -0,0 +1,75 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import shutil
import tempfile
from typing import Iterable, List, Set
from ahriman.core.build_tools.task import Task
from ahriman.models.package import Package
class Tree:
def __init__(self) -> None:
self.packages: List[Leaf] = []
def levels(self) -> List[List[Package]]:
result: List[List[Package]] = []
unprocessed = [leaf for leaf in self.packages]
while unprocessed:
result.append([leaf.package for leaf in unprocessed if leaf.is_root(unprocessed)])
unprocessed = [leaf for leaf in unprocessed if not leaf.is_root(unprocessed)]
return result
def load(self, packages: Iterable[Package]) -> None:
for package in packages:
leaf = Leaf(package)
leaf.load_dependencies()
self.packages.append(leaf)
class Leaf:
def __init__(self, package: Package) -> None:
self.package = package
self.dependencies: Set[str] = set()
def is_root(self, packages: Iterable[Leaf]) -> bool:
'''
:param packages:
:return: true if any of packages is dependency of the leaf, false otherwise
'''
for package in packages:
if package.package.packages.intersection(self.dependencies):
return False
return True
def load_dependencies(self) -> None:
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)

View File

@ -1,7 +1,7 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
@ -26,8 +26,8 @@ class Rsync(Uploader):
def __init__(self, architecture: str, config: Configuration) -> None:
Uploader.__init__(self, architecture, config)
section = self.config.get_section_name('rsync', self.architecture)
self.remote = self.config.get(section, 'remote')
section = config.get_section_name('rsync', architecture)
self.remote = config.get(section, 'remote')
def sync(self, path: str) -> None:
check_output('rsync', '--archive', '--verbose', '--compress', '--partial', '--progress', '--delete', path, self.remote,

View File

@ -1,7 +1,7 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
@ -26,8 +26,8 @@ class S3(Uploader):
def __init__(self, architecture: str, config: Configuration) -> None:
Uploader.__init__(self, architecture, config)
section = self.config.get_section_name('s3', self.architecture)
self.bucket = self.config.get(section, 'bucket')
section = config.get_section_name('s3', architecture)
self.bucket = config.get(section, 'bucket')
def sync(self, path: str) -> None:
# TODO rewrite to boto, but it is bullshit

View File

@ -1,7 +1,7 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
@ -27,9 +27,9 @@ from ahriman.models.upload_settings import UploadSettings
class Uploader:
def __init__(self, architecture: str, config: Configuration) -> None:
self.logger = logging.getLogger('builder')
self.architecture = architecture
self.config = config
self.logger = logging.getLogger('builder')
@staticmethod
def run(architecture: str, config: Configuration, target: str, path: str) -> None:
@ -41,13 +41,13 @@ class Uploader:
from ahriman.core.upload.s3 import S3
uploader = S3(architecture, config)
else:
from ahriman.core.upload.dummy import Dummy
uploader = Dummy(architecture, config)
uploader = Uploader(architecture, config)
try:
uploader.sync(path)
except Exception as e:
raise SyncFailed(e) from e
except Exception:
uploader.logger.exception('remote sync failed', exc_info=True)
raise SyncFailed()
def sync(self, path: str) -> None:
raise NotImplementedError
pass

View File

@ -1,7 +1,7 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
@ -17,10 +17,3 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.report.report import Report
class Dummy(Report):
def generate(self, path: str) -> None:
pass

View File

@ -0,0 +1,116 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import logging
from typing import Any, Dict, Set
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
class Client:
def add(self, package: Package, status: BuildStatusEnum) -> None:
pass
def remove(self, base: str, packages: Set[str]) -> None:
pass
def update(self, base: str, status: BuildStatusEnum) -> None:
pass
def set_building(self, base: str) -> None:
return self.update(base, BuildStatusEnum.Building)
def set_failed(self, base: str) -> None:
return self.update(base, BuildStatusEnum.Failed)
def set_pending(self, base: str) -> None:
return self.update(base, BuildStatusEnum.Pending)
def set_success(self, package: Package) -> None:
return self.add(package, BuildStatusEnum.Success)
def set_unknown(self, package: Package) -> None:
return self.add(package, BuildStatusEnum.Unknown)
@staticmethod
def load(architecture: str, config: Configuration) -> Client:
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()
return WebClient(host, port)
class WebClient(Client):
def __init__(self, host: str, port: int) -> None:
self.logger = logging.getLogger('http')
self.host = host
self.port = port
def _url(self, base: str) -> str:
return f'http://{self.host}:{self.port}/api/v1/packages/{base}'
def add(self, package: Package, status: BuildStatusEnum) -> None:
import requests
payload: Dict[str, Any] = {
'status': status.value,
'package': {
'base': package.base,
'packages': [p for p in package.packages],
'version': package.version,
'aur_url': package.aur_url
}
}
try:
response = requests.post(self._url(package.base), json=payload)
response.raise_for_status()
except:
self.logger.exception(f'could not add {package.base}', exc_info=True)
def remove(self, base: str, packages: Set[str]) -> None:
if not packages:
return
import requests
try:
response = requests.delete(self._url(base))
response.raise_for_status()
except:
self.logger.exception(f'could not delete {base}', exc_info=True)
def update(self, base: str, status: BuildStatusEnum) -> None:
import requests
payload: Dict[str, Any] = {'status': status.value}
try:
response = requests.post(self._url(base), json=payload)
response.raise_for_status()
except:
self.logger.exception(f'could not update {base}', exc_info=True)

View File

@ -0,0 +1,57 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Dict, List, Optional, Tuple
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
class Watcher:
def __init__(self, architecture: str, config: Configuration) -> None:
self.architecture = architecture
self.repository = Repository(architecture, config)
self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
@property
def packages(self) -> List[Tuple[Package, BuildStatus]]:
return [pair for pair in self.known.values()]
def load(self) -> None:
for package in self.repository.packages():
# get status of build or assign unknown
current = self.known.get(package.base)
if current is None:
status = BuildStatus()
else:
_, status = current
self.known[package.base] = (package, status)
def remove(self, base: str) -> None:
self.known.pop(base, None)
def update(self, base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
if package is None:
package, _ = self.known[base]
full_status = BuildStatus(status)
self.known[base] = (package, full_status)

View File

@ -0,0 +1,43 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# 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
from enum import Enum
from typing import Optional, Union
class BuildStatusEnum(Enum):
Unknown = 'unknown'
Pending = 'pending'
Building = 'building'
Failed = 'failed'
Success = 'success'
class BuildStatus:
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
timestamp: Optional[datetime.datetime] = None) -> None:
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
self._timestamp = timestamp or datetime.datetime.utcnow()
@property
def timestamp(self) -> str:
return self._timestamp.strftime('%Y-%m-%d %H:%M:%S')

View File

@ -19,36 +19,44 @@
#
from __future__ import annotations
import shutil
import aur
import os
import shutil
import tempfile
from configparser import RawConfigParser
from dataclasses import dataclass
from dataclasses import dataclass, field
from pyalpm import Handle
from srcinfo.parse import parse_srcinfo
from typing import Type
from typing import List, Set, Type
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import check_output
@dataclass
class Package:
name: str
base: str
version: str
url: str
remote: bool
aur_url: str
packages: Set[str] = field(default_factory=set)
@property
def git_url(self) -> str:
return f'{self.aur_url}/{self.base}.git'
@property
def is_vcs(self) -> bool:
return self.name.endswith('-bzr') \
or self.name.endswith('-csv')\
or self.name.endswith('-darcs')\
or self.name.endswith('-git')\
or self.name.endswith('-hg')\
or self.name.endswith('-svn')
return self.base.endswith('-bzr') \
or self.base.endswith('-csv')\
or self.base.endswith('-darcs')\
or self.base.endswith('-git')\
or self.base.endswith('-hg')\
or self.base.endswith('-svn')
@property
def web_url(self) -> str:
return f'{self.aur_url}/packages/{self.base}'
# additional method to handle vcs versions
def actual_version(self) -> str:
@ -58,9 +66,9 @@ class Package:
from ahriman.core.build_tools.task import Task
clone_dir = tempfile.mkdtemp()
try:
Task.fetch(clone_dir, self.url)
Task.fetch(clone_dir, self.git_url)
# update pkgver first
check_output('makepkg', '--nodeps', '--noprepare', '--nobuild',
check_output('makepkg', '--nodeps', '--nobuild',
exception=None, cwd=clone_dir)
# generate new .SRCINFO and put it to parser
src_info_source = check_output('makepkg', '--printsrcinfo',
@ -73,37 +81,49 @@ class Package:
shutil.rmtree(clone_dir, ignore_errors=True)
@classmethod
def from_archive(cls: Type[Package], path: str, aur_url: str) -> Package:
name, version = check_output('expac', '-p', '%e %v', path, exception=None).split()
return cls(name, version, f'{aur_url}/{name}.git', False)
def from_archive(cls: Type[Package], path: str, pacman: Pacman, aur_url: str) -> Package:
package = pacman.handle.load_pkg(path)
return cls(package.base, package.version, aur_url, {package.name})
@classmethod
def from_aur(cls: Type[Package], name: str, aur_url: str)-> Package:
package = aur.info(name)
return cls(package.package_base, package.version, f'{aur_url}/{package.package_base}.git', True)
return cls(package.package_base, package.version, aur_url, {package.name})
@classmethod
def from_build(cls: Type[Package], path: str) -> Package:
git_config = RawConfigParser()
git_config.read(os.path.join(path, '.git', 'config'))
def from_build(cls: Type[Package], path: str, aur_url: str) -> Package:
with open(os.path.join(path, '.SRCINFO')) as fn:
src_info, errors = parse_srcinfo(fn.read())
if errors:
raise InvalidPackageInfo(errors)
packages = set(src_info['packages'].keys())
return cls(src_info['pkgbase'], f'{src_info["pkgver"]}-{src_info["pkgrel"]}',
git_config.get('remote "origin"', 'url'), False)
return cls(src_info['pkgbase'], f'{src_info["pkgver"]}-{src_info["pkgrel"]}', aur_url, packages)
@classmethod
def load(cls: Type[Package], path: str, aur_url: str) -> Package:
@staticmethod
def dependencies(path: str) -> Set[str]:
with open(os.path.join(path, '.SRCINFO')) as fn:
src_info, errors = parse_srcinfo(fn.read())
if errors:
raise InvalidPackageInfo(errors)
makedepends = src_info['makedepends']
# sum over each package
depends: List[str] = src_info.get('depends', [])
for package in src_info['packages'].values():
depends.extend(package.get('depends', []))
# we are not interested in dependencies inside pkgbase
packages = set(src_info['packages'].keys())
return set(depends + makedepends) - packages
@staticmethod
def load(path: str, pacman: Pacman, aur_url: str) -> Package:
try:
if os.path.isdir(path):
package: Package = cls.from_build(path)
package: Package = Package.from_build(path, aur_url)
elif os.path.exists(path):
package = cls.from_archive(path, aur_url)
package = Package.from_archive(path, pacman, aur_url)
else:
package = cls.from_aur(path, aur_url)
package = Package.from_aur(path, aur_url)
return package
except InvalidPackageInfo:
raise
@ -113,4 +133,4 @@ class Package:
def is_outdated(self, remote: Package) -> bool:
remote_version = remote.actual_version() # either normal version or updated VCS
result = check_output('vercmp', self.version, remote_version, exception=None)
return True if int(result) < 0 else False
return True if int(result) < 0 else False

View File

@ -20,16 +20,15 @@
from __future__ import annotations
from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOptionException
from ahriman.core.exceptions import InvalidOption
class ReportSettings(Enum):
HTML = auto()
@classmethod
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
@staticmethod
def from_option(value: str) -> ReportSettings:
if value.lower() in ('html',):
return cls.HTML
raise InvalidOptionException(value)
return ReportSettings.HTML
raise InvalidOption(value)

View File

@ -29,22 +29,37 @@ class RepositoryPaths:
@property
def chroot(self) -> str:
'''
:return: directory for devtools chroot
'''
return os.path.join(self.root, 'chroot')
@property
def manual(self) -> str:
'''
:return: directory for manual updates (i.e. from add command)
'''
return os.path.join(self.root, 'manual')
@property
def packages(self) -> str:
'''
:return: directory for built packages
'''
return os.path.join(self.root, 'packages')
@property
def repository(self) -> str:
'''
:return: repository directory
'''
return os.path.join(self.root, 'repository', self.architecture)
@property
def sources(self) -> str:
'''
:return: directory for downloaded PKGBUILDs for current build
'''
return os.path.join(self.root, 'sources')
def create_tree(self) -> None:

View File

@ -20,22 +20,18 @@
from __future__ import annotations
from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOptionException
from ahriman.core.exceptions import InvalidOption
class SignSettings(Enum):
Disabled = auto()
SignPackages = auto()
SignRepository = auto()
@classmethod
def from_option(cls: Type[SignSettings], value: str) -> SignSettings:
if value.lower() in ('no', 'disabled'):
return cls.Disabled
elif value.lower() in ('package', 'packages', 'sign-package'):
return cls.SignPackages
@staticmethod
def from_option(value: str) -> SignSettings:
if value.lower() in ('package', 'packages', 'sign-package'):
return SignSettings.SignPackages
elif value.lower() in ('repository', 'sign-repository'):
return cls.SignRepository
raise InvalidOptionException(value)
return SignSettings.SignRepository
raise InvalidOption(value)

View File

@ -20,19 +20,18 @@
from __future__ import annotations
from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOptionException
from ahriman.core.exceptions import InvalidOption
class UploadSettings(Enum):
Rsync = auto()
S3 = auto()
@classmethod
def from_option(cls: Type[UploadSettings], value: str) -> UploadSettings:
@staticmethod
def from_option(value: str) -> UploadSettings:
if value.lower() in ('rsync',):
return cls.Rsync
return UploadSettings.Rsync
elif value.lower() in ('s3',):
return cls.S3
raise InvalidOptionException(value)
return UploadSettings.S3
raise InvalidOption(value)

View File

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

View File

@ -1,7 +1,7 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
@ -17,10 +17,3 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.upload.uploader import Uploader
class Dummy(Uploader):
def sync(self, path: str) -> None:
pass

View File

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

View File

@ -0,0 +1,37 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import middleware, Request, Response
from aiohttp.web_exceptions import HTTPClientError
from logging import Logger
from typing import Callable
def exception_handler(logger: Logger) -> Callable:
@middleware
async def handle(request: Request, handler: Callable) -> Response:
try:
return await handler(request)
except HTTPClientError:
raise
except Exception:
logger.exception(f'exception during performing request to {request.path}', exc_info=True)
raise
return handle

34
src/ahriman/web/routes.py Normal file
View File

@ -0,0 +1,34 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import Application
from ahriman.web.views.index import IndexView
from ahriman.web.views.package import PackageView
from ahriman.web.views.packages import PackagesView
def setup_routes(app: Application) -> None:
app.router.add_get('/', IndexView)
app.router.add_get('/index.html', IndexView)
app.router.add_post('/api/v1/packages', PackagesView)
app.router.add_delete('/api/v1/packages/{package}', PackageView)
app.router.add_post('/api/v1/packages/{package}', PackageView)

View File

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

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import View
from ahriman.core.watcher.watcher import Watcher
# special class to make it typed
class BaseView(View):
@property
def service(self) -> Watcher:
return self.request.app['watcher']

View File

@ -0,0 +1,49 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp_jinja2 import template
from typing import Any, Dict
import ahriman.version as version
from ahriman.web.views.base import BaseView
class IndexView(BaseView):
@template("build-status.jinja2")
async def get(self) -> Dict[str, Any]:
# some magic to make it jinja-friendly
packages = [
{
'base': package.base,
'packages': [p for p in sorted(package.packages)],
'status': status.status.value,
'timestamp': status.timestamp,
'version': package.version,
'web_url': package.web_url
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
]
return {
'architecture': self.service.architecture,
'packages': packages,
'repository': self.service.repository.name,
'version': version.__version__,
}

View File

@ -0,0 +1,43 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPOk, Response
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.web.views.base import BaseView
class PackageView(BaseView):
async def delete(self) -> Response:
base = self.request.match_info['package']
self.service.remove(base)
return HTTPOk()
async def post(self) -> Response:
base = self.request.match_info['package']
data = await self.request.json()
package = Package(**data['package']) if 'package' in data else None
status = BuildStatusEnum(data['status'])
self.service.update(base, status, package)
return HTTPOk()

View File

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

75
src/ahriman/web/web.py Normal file
View File

@ -0,0 +1,75 @@
#
# Copyright (c) 2021 Evgenii Alekseev.
#
# 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 aiohttp_jinja2
import jinja2
import logging
from aiohttp import web
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeException
from ahriman.core.watcher.watcher import Watcher
from ahriman.web.middlewares.exception_handler import exception_handler
from ahriman.web.routes import setup_routes
async def on_shutdown(app: web.Application) -> None:
app.logger.warning('server terminated')
async def on_startup(app: web.Application) -> None:
app.logger.info('server started')
try:
app['watcher'].load()
except Exception:
app.logger.exception('could not load packages', exc_info=True)
raise InitializeException()
def run_server(app: web.Application, architecture: str) -> None:
app.logger.info('start server')
section = app['config'].get_section_name('web', architecture)
host = app['config'].get(section, 'host')
port = app['config'].getint(section, 'port')
web.run_app(app, host=host, port=port, handle_signals=False)
def setup_service(architecture: str, config: Configuration) -> web.Application:
app = web.Application(logger=logging.getLogger('http'))
app.on_shutdown.append(on_shutdown)
app.on_startup.append(on_startup)
app.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
app.middlewares.append(exception_handler(app.logger))
app.logger.info('setup routes')
setup_routes(app)
app.logger.info('setup templates')
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(config.get('web', 'templates')))
app.logger.info('setup configuration')
app['config'] = config
app.logger.info('setup watcher')
app['watcher'] = Watcher(architecture, config)
return app