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" source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
'ahriman.sysusers' 'ahriman.sysusers'
'ahriman.tmpfiles') 'ahriman.tmpfiles')
sha512sums=('d35053c7a52e5cc2dd8a3dca6c9d9f18788296d0059683b16238a54318a74fb4c42544d0727460250e7d0f0ce1009aca96e88d3e52e6bdffab8d45e5e4901b7b' sha512sums=('46f75bb230b7810d0459b58ea3956da45bfbc448cb3119982eacb78b6c24c252a99d29f7785922c0f8e715f16c09b3bdedb50cd0bd0962fff471fe0c6cc2626b'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075' '13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4') '55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini' backup=('etc/ahriman.ini'

View File

@ -11,7 +11,7 @@
<body> <body>
<div class="root"> <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" %} {% include "search-line.jinja2" %}

View File

@ -75,4 +75,26 @@
td.package-success { td.package-success {
background-color: rgba(var(--color-success), 1.0); 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> </style>

View File

@ -26,58 +26,64 @@ from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def add(args: argparse.Namespace) -> None: def add(args: argparse.Namespace, config: Configuration) -> None:
''' '''
add packages callback add packages callback
:param args: command line args :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 world rebuild callback
:param args: command line args :param args: command line args
:param config: configuration instance
''' '''
app = Application.from_args(args) app = Application.from_args(args, config)
packages = app.repository.packages() packages = app.repository.packages()
app.update(packages) app.update(packages)
def remove(args: argparse.Namespace) -> None: def remove(args: argparse.Namespace, config: Configuration) -> None:
''' '''
remove packages callback remove packages callback
:param args: command line args :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 generate report callback
:param args: command line args :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 sync to remote server callback
:param args: command line args :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 update packages callback
:param args: command line args :param args: command line args
:param config: configuration instance
''' '''
# typing workaround # typing workaround
def log_fn(line: str) -> None: def log_fn(line: str) -> None:
return print(line) if args.dry_run else app.logger.info(line) 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) packages = app.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
if args.dry_run: if args.dry_run:
return return
@ -85,13 +91,13 @@ def update(args: argparse.Namespace) -> None:
app.update(packages) app.update(packages)
def web(args: argparse.Namespace) -> None: def web(args: argparse.Namespace, config: Configuration) -> None:
''' '''
web server callback web server callback
:param args: command line args :param args: command line args
:param config: configuration instance
''' '''
from ahriman.web.web import run_server, setup_service from ahriman.web.web import run_server, setup_service
config = Configuration.from_path(args.config)
app = setup_service(args.architecture, config) app = setup_service(args.architecture, config)
run_server(app, args.architecture) run_server(app, args.architecture)
@ -146,5 +152,6 @@ if __name__ == '__main__':
parser.print_help() parser.print_help()
exit(1) exit(1)
with Lock(args.lock, args.architecture, args.force): config = Configuration.from_path(args.config)
args.fn(args) 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) self.repository = Repository(architecture, config)
@classmethod @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 constructor which has to be used to build instance from command line args
:param args: command line args :param args: command line args
:param config: configuration instance
:return: application instance :return: application instance
''' '''
config = Configuration.from_path(args.config)
return cls(args.architecture, config) return cls(args.architecture, config)
def _known_packages(self) -> Set[str]: def _known_packages(self) -> Set[str]:

View File

@ -24,7 +24,10 @@ import os
from types import TracebackType from types import TracebackType
from typing import Literal, Optional, Type from typing import Literal, Optional, Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRun from ahriman.core.exceptions import DuplicateRun
from ahriman.core.watcher.client import Client
from ahriman.models.build_status import BuildStatusEnum
class Lock: class Lock:
@ -32,29 +35,36 @@ class Lock:
wrapper for application lock file wrapper for application lock file
:ivar force: remove lock file on start if any :ivar force: remove lock file on start if any
:ivar path: path to lock file if any :ivar path: path to lock file if any
:ivar reporter: build status reporter instance
''' '''
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 default constructor
:param path: optional path to lock file, if empty no file lock will be used :param path: optional path to lock file, if empty no file lock will be used
:param architecture: repository architecture :param architecture: repository architecture
:param force: remove lock file on start if any :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.path = f'{path}_{architecture}' if path is not None else None
self.force = force self.force = force
self.reporter = Client.load(architecture, config)
def __enter__(self) -> Lock: def __enter__(self) -> Lock:
''' '''
default workflow is the following default workflow is the following:
* remove lock file if force flag is set
* check if there is lock file remove lock file if force flag is set
* create lock file check if there is lock file
create lock file
report to web if enabled
''' '''
if self.force: if self.force:
self.remove() self.remove()
self.check() self.check()
self.create() self.create()
self.reporter.update_self(BuildStatusEnum.Building)
return self return self
def __exit__(self, exc_type: Optional[Type[Exception]], exc_val: Optional[Exception], def __exit__(self, exc_type: Optional[Type[Exception]], exc_val: Optional[Exception],
@ -67,6 +77,8 @@ class Lock:
:return: always False (do not suppress any exception) :return: always False (do not suppress any exception)
''' '''
self.remove() self.remove()
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
self.reporter.update_self(status)
return False return False
def check(self) -> None: def check(self) -> None:

View File

@ -52,6 +52,13 @@ class Client:
''' '''
pass 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: def set_building(self, base: str) -> None:
''' '''
set package status to building set package status to building

View File

@ -43,6 +43,7 @@ class Watcher:
self.repository = Repository(architecture, config) self.repository = Repository(architecture, config)
self.known: Dict[str, Tuple[Package, BuildStatus]] = {} self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
self.status = BuildStatus()
@property @property
def packages(self) -> List[Tuple[Package, BuildStatus]]: def packages(self) -> List[Tuple[Package, BuildStatus]]:
@ -82,3 +83,10 @@ class Watcher:
package, _ = self.known[base] package, _ = self.known[base]
full_status = BuildStatus(status) full_status = BuildStatus(status)
self.known[base] = (package, full_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.host = host
self.port = port 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 url generator
:param base: package base to generate url :param base: package base to generate url
@ -66,7 +73,7 @@ class WebClient(Client):
} }
try: try:
response = requests.post(self._url(package.base), json=payload) response = requests.post(self._package_url(package.base), json=payload)
response.raise_for_status() response.raise_for_status()
except Exception: except Exception:
self.logger.exception(f'could not add {package.base}', exc_info=True) self.logger.exception(f'could not add {package.base}', exc_info=True)
@ -77,7 +84,7 @@ class WebClient(Client):
:param base: basename to remove :param base: basename to remove
''' '''
try: try:
response = requests.delete(self._url(base)) response = requests.delete(self._package_url(base))
response.raise_for_status() response.raise_for_status()
except Exception: except Exception:
self.logger.exception(f'could not delete {base}', exc_info=True) 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} payload: Dict[str, Any] = {'status': status.value}
try: try:
response = requests.post(self._url(base), json=payload) response = requests.post(self._package_url(base), json=payload)
response.raise_for_status() response.raise_for_status()
except Exception: except Exception:
self.logger.exception(f'could not update {base}', exc_info=True) 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 / get build status page
GET /index.html same as above 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/packages force update every package from repository
POST /api/v1/package/:base update package base status 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, packages - sorted list of packages properties: base, packages (sorted list), status,
timestamp, version, web_url. Required timestamp, version, web_url. Required
repository - repository name, string, required repository - repository name, string, required
service - service status properties: status, timestamp. Required
version - ahriman version, string, required version - ahriman version, string, required
''' '''
@ -56,10 +57,15 @@ class IndexView(BaseView):
'web_url': package.web_url 'web_url': package.web_url
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base) } 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 { return {
'architecture': self.service.architecture, 'architecture': self.service.architecture,
'packages': packages, 'packages': packages,
'repository': self.service.repository.name, 'repository': self.service.repository.name,
'service': service,
'version': version.__version__, 'version': version.__version__,
} }