diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 855f01da..fa7611b2 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -23,7 +23,7 @@ optdepends=('aws-cli: sync to s3' source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz" 'ahriman.sysusers' 'ahriman.tmpfiles') -sha512sums=('d35053c7a52e5cc2dd8a3dca6c9d9f18788296d0059683b16238a54318a74fb4c42544d0727460250e7d0f0ce1009aca96e88d3e52e6bdffab8d45e5e4901b7b' +sha512sums=('46f75bb230b7810d0459b58ea3956da45bfbc448cb3119982eacb78b6c24c252a99d29f7785922c0f8e715f16c09b3bdedb50cd0bd0962fff471fe0c6cc2626b' '13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075' '55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4') backup=('etc/ahriman.ini' diff --git a/package/share/ahriman/build-status.jinja2 b/package/share/ahriman/build-status.jinja2 index e4ac2d0c..62569013 100644 --- a/package/share/ahriman/build-status.jinja2 +++ b/package/share/ahriman/build-status.jinja2 @@ -11,7 +11,7 @@
-

ahriman {{ version|e }} ({{ architecture|e }})

+

ahriman {{ version|e }} ({{ architecture|e }}){{ service.status|e }}

{% include "search-line.jinja2" %} diff --git a/package/share/ahriman/style.jinja2 b/package/share/ahriman/style.jinja2 index e4d5205c..59849438 100644 --- a/package/share/ahriman/style.jinja2 +++ b/package/share/ahriman/style.jinja2 @@ -75,4 +75,26 @@ td.package-success { background-color: rgba(var(--color-success), 1.0); } + + sup.service-unknown { + font-weight: lighter; + background-color: rgba(var(--color-unknown), 1.0); + } + sup.service-building { + font-weight: lighter; + 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; + } + sup.service-failed { + font-weight: lighter; + background-color: rgba(var(--color-failed), 1.0); + } + sup.service-success { + font-weight: lighter; + background-color: rgba(var(--color-success), 1.0); + } \ No newline at end of file diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index cfe71728..711f2bd6 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -26,58 +26,64 @@ from ahriman.application.lock import Lock from ahriman.core.configuration import Configuration -def add(args: argparse.Namespace) -> None: +def add(args: argparse.Namespace, config: Configuration) -> None: ''' add packages callback :param args: command line args + :param config: configuration instance ''' - Application.from_args(args).add(args.package, args.without_dependencies) + Application.from_args(args, config).add(args.package, args.without_dependencies) -def rebuild(args: argparse.Namespace) -> None: +def rebuild(args: argparse.Namespace, config: Configuration) -> None: ''' world rebuild callback :param args: command line args + :param config: configuration instance ''' - app = Application.from_args(args) + app = Application.from_args(args, config) packages = app.repository.packages() app.update(packages) -def remove(args: argparse.Namespace) -> None: +def remove(args: argparse.Namespace, config: Configuration) -> None: ''' remove packages callback :param args: command line args + :param config: configuration instance ''' - Application.from_args(args).remove(args.package) + Application.from_args(args, config).remove(args.package) -def report(args: argparse.Namespace) -> None: +def report(args: argparse.Namespace, config: Configuration) -> None: ''' generate report callback :param args: command line args + :param config: configuration instance ''' - Application.from_args(args).report(args.target) + Application.from_args(args, config).report(args.target) -def sync(args: argparse.Namespace) -> None: +def sync(args: argparse.Namespace, config: Configuration) -> None: ''' sync to remote server callback :param args: command line args + :param config: configuration instance ''' - Application.from_args(args).sync(args.target) + Application.from_args(args, config).sync(args.target) -def update(args: argparse.Namespace) -> None: +def update(args: argparse.Namespace, config: Configuration) -> None: ''' update packages callback :param args: command line args + :param config: configuration instance ''' # typing workaround def log_fn(line: str) -> None: return print(line) if args.dry_run else app.logger.info(line) - app = Application.from_args(args) + app = Application.from_args(args, config) packages = app.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn) if args.dry_run: return @@ -85,13 +91,13 @@ def update(args: argparse.Namespace) -> None: app.update(packages) -def web(args: argparse.Namespace) -> None: +def web(args: argparse.Namespace, config: Configuration) -> None: ''' web server callback :param args: command line args + :param config: configuration instance ''' 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) @@ -146,5 +152,6 @@ if __name__ == '__main__': parser.print_help() exit(1) - with Lock(args.lock, args.architecture, args.force): - args.fn(args) + config = Configuration.from_path(args.config) + with Lock(args.lock, args.architecture, args.force, config): + args.fn(args, config) diff --git a/src/ahriman/application/application.py b/src/ahriman/application/application.py index a9c22ce8..77fb46db 100644 --- a/src/ahriman/application/application.py +++ b/src/ahriman/application/application.py @@ -54,13 +54,13 @@ class Application: self.repository = Repository(architecture, config) @classmethod - def from_args(cls: Type[Application], args: argparse.Namespace) -> Application: + def from_args(cls: Type[Application], args: argparse.Namespace, config: Configuration) -> Application: ''' constructor which has to be used to build instance from command line args :param args: command line args + :param config: configuration instance :return: application instance ''' - config = Configuration.from_path(args.config) return cls(args.architecture, config) def _known_packages(self) -> Set[str]: diff --git a/src/ahriman/application/lock.py b/src/ahriman/application/lock.py index 24e69e6c..1ba580b8 100644 --- a/src/ahriman/application/lock.py +++ b/src/ahriman/application/lock.py @@ -24,7 +24,10 @@ import os from types import TracebackType from typing import Literal, Optional, Type +from ahriman.core.configuration import Configuration from ahriman.core.exceptions import DuplicateRun +from ahriman.core.watcher.client import Client +from ahriman.models.build_status import BuildStatusEnum class Lock: @@ -32,29 +35,36 @@ class Lock: wrapper for application lock file :ivar force: remove lock file on start if any :ivar path: path to lock file if any + :ivar reporter: build status reporter instance ''' - def __init__(self, path: Optional[str], architecture: str, force: bool) -> None: + def __init__(self, path: Optional[str], architecture: str, force: bool, config: Configuration) -> None: ''' default constructor :param path: optional path to lock file, if empty no file lock will be used :param architecture: repository architecture :param force: remove lock file on start if any + :param config: configuration instance ''' self.path = f'{path}_{architecture}' if path is not None else None self.force = force + self.reporter = Client.load(architecture, config) + def __enter__(self) -> Lock: ''' - default workflow is the following - * remove lock file if force flag is set - * check if there is lock file - * create lock file + default workflow is the following: + + remove lock file if force flag is set + check if there is lock file + create lock file + report to web if enabled ''' if self.force: self.remove() self.check() self.create() + self.reporter.update_self(BuildStatusEnum.Building) return self def __exit__(self, exc_type: Optional[Type[Exception]], exc_val: Optional[Exception], @@ -67,6 +77,8 @@ class Lock: :return: always False (do not suppress any exception) ''' self.remove() + status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed + self.reporter.update_self(status) return False def check(self) -> None: diff --git a/src/ahriman/core/watcher/client.py b/src/ahriman/core/watcher/client.py index c868c36e..69834246 100644 --- a/src/ahriman/core/watcher/client.py +++ b/src/ahriman/core/watcher/client.py @@ -52,6 +52,13 @@ class Client: ''' pass + def update_self(self, status: BuildStatusEnum) -> None: + ''' + update ahriman status itself + :param status: current ahriman status + ''' + pass + def set_building(self, base: str) -> None: ''' set package status to building diff --git a/src/ahriman/core/watcher/watcher.py b/src/ahriman/core/watcher/watcher.py index 6248d267..e2686a6a 100644 --- a/src/ahriman/core/watcher/watcher.py +++ b/src/ahriman/core/watcher/watcher.py @@ -43,6 +43,7 @@ class Watcher: self.repository = Repository(architecture, config) self.known: Dict[str, Tuple[Package, BuildStatus]] = {} + self.status = BuildStatus() @property def packages(self) -> List[Tuple[Package, BuildStatus]]: @@ -82,3 +83,10 @@ class Watcher: package, _ = self.known[base] full_status = BuildStatus(status) self.known[base] = (package, full_status) + + def update_self(self, status: BuildStatusEnum) -> None: + ''' + update service status + :param status: new service status + ''' + self.status = BuildStatus(status) diff --git a/src/ahriman/core/watcher/web_client.py b/src/ahriman/core/watcher/web_client.py index 1ebdb329..02b0d284 100644 --- a/src/ahriman/core/watcher/web_client.py +++ b/src/ahriman/core/watcher/web_client.py @@ -46,7 +46,14 @@ class WebClient(Client): self.host = host self.port = port - def _url(self, base: str) -> str: + def _ahriman_url(self) -> str: + ''' + url generator + :return: full url for web service for ahriman service itself + ''' + return f'http://{self.host}:{self.port}/api/v1/ahriman' + + def _package_url(self, base: str) -> str: ''' url generator :param base: package base to generate url @@ -66,7 +73,7 @@ class WebClient(Client): } try: - response = requests.post(self._url(package.base), json=payload) + response = requests.post(self._package_url(package.base), json=payload) response.raise_for_status() except Exception: self.logger.exception(f'could not add {package.base}', exc_info=True) @@ -77,7 +84,7 @@ class WebClient(Client): :param base: basename to remove ''' try: - response = requests.delete(self._url(base)) + response = requests.delete(self._package_url(base)) response.raise_for_status() except Exception: self.logger.exception(f'could not delete {base}', exc_info=True) @@ -91,7 +98,20 @@ class WebClient(Client): payload: Dict[str, Any] = {'status': status.value} try: - response = requests.post(self._url(base), json=payload) + response = requests.post(self._package_url(base), json=payload) response.raise_for_status() except Exception: self.logger.exception(f'could not update {base}', exc_info=True) + + def update_self(self, status: BuildStatusEnum) -> None: + ''' + update ahriman status itself + :param status: current ahriman status + ''' + payload: Dict[str, Any] = {'status': status.value} + + try: + response = requests.post(self._ahriman_url(), json=payload) + response.raise_for_status() + except Exception: + self.logger.exception(f'could not update service status', exc_info=True) diff --git a/src/ahriman/web/routes.py b/src/ahriman/web/routes.py index 480783dc..d5553bac 100644 --- a/src/ahriman/web/routes.py +++ b/src/ahriman/web/routes.py @@ -33,6 +33,8 @@ def setup_routes(application: Application) -> None: GET / get build status page GET /index.html same as above + POST /api/v1/ahriman update service status + POST /api/v1/packages force update every package from repository POST /api/v1/package/:base update package base status diff --git a/src/ahriman/web/views/ahriman.py b/src/ahriman/web/views/ahriman.py new file mode 100644 index 00000000..e61bf146 --- /dev/null +++ b/src/ahriman/web/views/ahriman.py @@ -0,0 +1,51 @@ +# +# 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 . +# +from aiohttp.web import HTTPBadRequest, HTTPOk, Response + +from ahriman.models.build_status import BuildStatusEnum +from ahriman.web.views.base import BaseView + + +class AhrimanView(BaseView): + ''' + service status web view + ''' + + async def post(self) -> Response: + ''' + update service status + + JSON body must be supplied, the following model is used: + { + "status": "unknown", # service status string, must be valid `BuildStatusEnum` + } + + :return: 200 on success + ''' + data = await self.request.json() + + try: + status = BuildStatusEnum(data['status']) + except Exception as e: + raise HTTPBadRequest(text=str(e)) + + self.service.update_self(status) + + return HTTPOk() diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py index 9d2df3e9..36c2f3d1 100644 --- a/src/ahriman/web/views/index.py +++ b/src/ahriman/web/views/index.py @@ -36,6 +36,7 @@ class IndexView(BaseView): packages - sorted list of packages properties: base, packages (sorted list), status, timestamp, version, web_url. Required repository - repository name, string, required + service - service status properties: status, timestamp. Required version - ahriman version, string, required ''' @@ -56,10 +57,15 @@ class IndexView(BaseView): 'web_url': package.web_url } for package, status in sorted(self.service.packages, key=lambda item: item[0].base) ] + service = { + 'status': self.service.status.status.value, + 'timestamp': self.service.status.timestamp + } return { 'architecture': self.service.architecture, 'packages': packages, 'repository': self.service.repository.name, + 'service': service, 'version': version.__version__, }