Add web status route (#13)

* add status route

* typed status and get status at the start of application
This commit is contained in:
2021-04-08 01:48:53 +03:00
committed by GitHub
parent a416214e5f
commit 213b2c65a0
16 changed files with 389 additions and 6 deletions

View File

@ -22,9 +22,8 @@ import sys
from pathlib import Path
import ahriman.application.handlers as handlers
import ahriman.version as version
from ahriman import version
from ahriman.application import handlers
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.sign_settings import SignSettings

View File

@ -20,12 +20,14 @@
from __future__ import annotations
import argparse
import logging
import os
from pathlib import Path
from types import TracebackType
from typing import Literal, Optional, Type
from ahriman import version
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
from ahriman.core.status.client import Client
@ -61,12 +63,13 @@ class Lock:
default workflow is the following:
check user UID
remove lock file if force flag is set
check if there is lock file
check web status watcher status
create lock file
report to web if enabled
"""
self.check_user()
self.check_version()
self.create()
self.reporter.update_self(BuildStatusEnum.Building)
return self
@ -85,6 +88,15 @@ class Lock:
self.reporter.update_self(status)
return False
def check_version(self) -> None:
"""
check web server version
"""
status = self.reporter.get_internal()
if status.version is not None and status.version != version.__version__:
logging.getLogger("root").warning(f"status watcher version mismatch, "
f"our {version.__version__}, their {status.version}")
def check_user(self) -> None:
"""
check if current user is actually owner of ahriman root

View File

@ -23,6 +23,7 @@ from typing import List, Optional, Tuple, Type
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus
from ahriman.models.package import Package
@ -62,6 +63,14 @@ class Client:
del base
return []
# pylint: disable=no-self-use
def get_internal(self) -> InternalStatus:
"""
get internal service status
:return: current internal (web) service status
"""
return InternalStatus()
# pylint: disable=no-self-use
def get_self(self) -> BuildStatus:
"""

View File

@ -24,6 +24,7 @@ from typing import List, Optional, Tuple
from ahriman.core.status.client import Client
from ahriman.models.build_status import BuildStatusEnum, BuildStatus
from ahriman.models.internal_status import InternalStatus
from ahriman.models.package import Package
@ -70,6 +71,13 @@ class WebClient(Client):
"""
return f"http://{self.host}:{self.port}/api/v1/packages/{base}"
def _status_url(self) -> str:
"""
url generator
:return: full url for web service for status
"""
return f"http://{self.host}:{self.port}/api/v1/status"
def add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
@ -110,6 +118,23 @@ class WebClient(Client):
self.logger.exception(f"could not get {base}")
return []
def get_internal(self) -> InternalStatus:
"""
get internal service status
:return: current internal (web) service status
"""
try:
response = requests.get(self._status_url())
response.raise_for_status()
status_json = response.json()
return InternalStatus.from_json(status_json)
except requests.exceptions.HTTPError as e:
self.logger.exception(f"could not get web service status: {WebClient._exception_response_text(e)}")
except Exception:
self.logger.exception("could not get web service status")
return InternalStatus()
def get_self(self) -> BuildStatus:
"""
get ahriman status itself

View File

@ -0,0 +1,71 @@
#
# Copyright (c) 2021 ahriman team.
#
# 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
from dataclasses import dataclass, fields
from typing import Any, Dict, List, Tuple, Type
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
@dataclass
class Counters:
"""
package counters
:ivar total: total packages count
:ivar unknown: packages in unknown status count
:ivar pending: packages in pending status count
:ivar building: packages in building status count
:ivar failed: packages in failed status count
:ivar success: packages in success status count
"""
total: int
unknown: int = 0
pending: int = 0
building: int = 0
failed: int = 0
success: int = 0
@classmethod
def from_json(cls: Type[Counters], dump: Dict[str, Any]) -> Counters:
"""
construct counters from json dump
:param dump: json dump body
:return: status counters
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
dump = {key: value for key, value in dump.items() if key in known_fields}
return cls(**dump)
@classmethod
def from_packages(cls: Type[Counters], packages: List[Tuple[Package, BuildStatus]]) -> Counters:
"""
construct counters from packages statuses
:param packages: list of package and their status as per watcher property
:return: status counters
"""
per_status = {"total": len(packages)}
for _, status in packages:
key = status.status.name.lower()
per_status.setdefault(key, 0)
per_status[key] += 1
return cls(**per_status)

View File

@ -0,0 +1,60 @@
#
# Copyright (c) 2021 ahriman team.
#
# 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
from dataclasses import asdict, dataclass, field
from typing import Any, Dict, Optional, Type
from ahriman.models.counters import Counters
@dataclass
class InternalStatus:
"""
internal server status
:ivar architecture: repository architecture
:ivar packages: packages statuses counter object
:ivar repository: repository name
:ivar version: service version
"""
architecture: Optional[str] = None
packages: Counters = field(default=Counters(total=0))
repository: Optional[str] = None
version: Optional[str] = None
@classmethod
def from_json(cls: Type[InternalStatus], dump: Dict[str, Any]) -> InternalStatus:
"""
construct internal status from json dump
:param dump: json dump body
:return: internal status
"""
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
return cls(architecture=dump.get("architecture"),
packages=counters,
repository=dump.get("repository"),
version=dump.get("version"))
def view(self) -> Dict[str, Any]:
"""
generate json status view
:return: json-friendly dictionary
"""
return asdict(self)

View File

@ -23,6 +23,7 @@ from ahriman.web.views.ahriman import AhrimanView
from ahriman.web.views.index import IndexView
from ahriman.web.views.package import PackageView
from ahriman.web.views.packages import PackagesView
from ahriman.web.views.status import StatusView
def setup_routes(application: Application) -> None:
@ -44,6 +45,8 @@ def setup_routes(application: Application) -> None:
GET /api/v1/package/:base get package base status
POST /api/v1/package/:base update package base status
GET /api/v1/status get web service status itself
:param application: web application instance
"""
application.router.add_get("/", IndexView)
@ -58,3 +61,5 @@ def setup_routes(application: Application) -> None:
application.router.add_delete("/api/v1/packages/{package}", PackageView)
application.router.add_get("/api/v1/packages/{package}", PackageView)
application.router.add_post("/api/v1/packages/{package}", PackageView)
application.router.add_get("/api/v1/status", StatusView)

View File

@ -21,8 +21,7 @@ import aiohttp_jinja2
from typing import Any, Dict
import ahriman.version as version
from ahriman import version
from ahriman.core.util import pretty_datetime
from ahriman.web.views.base import BaseView

View File

@ -0,0 +1,45 @@
#
# Copyright (c) 2021 ahriman team.
#
# 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 Response, json_response
from ahriman import version
from ahriman.models.counters import Counters
from ahriman.models.internal_status import InternalStatus
from ahriman.web.views.base import BaseView
class StatusView(BaseView):
"""
web service status web view
"""
async def get(self) -> Response:
"""
get current service status
:return: 200 with service status object
"""
counters = Counters.from_packages(self.service.packages)
status = InternalStatus(
architecture=self.service.architecture,
packages=counters,
repository=self.service.repository.name,
version=version.__version__)
return json_response(status.view())