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__,
}