handle service status

This commit is contained in:
Evgenii Alekseev 2021-03-15 03:37:05 +03:00
parent 3e0b3cdbaa
commit 374b3febc8
12 changed files with 164 additions and 29 deletions

View File

@ -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'

View File

@ -11,7 +11,7 @@
<body>
<div class="root">
<h1>ahriman {{ version|e }} ({{ architecture|e }})</h1>
<h1>ahriman {{ version|e }} ({{ architecture|e }})<sup class="service-{{ service.status|e }}" title="{{ service.timestamp }}">{{ service.status|e }}</sup></h1>
{% include "search-line.jinja2" %}

View File

@ -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);
}
</style>

View File

@ -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)

View File

@ -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]:

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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()

View File

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