add status command

This commit is contained in:
Evgenii Alekseev 2021-03-20 22:20:47 +03:00
parent 3d74b1485a
commit 15e3d2500c
16 changed files with 305 additions and 145 deletions

View File

@ -23,9 +23,9 @@ import sys
from multiprocessing import Pool from multiprocessing import Pool
import ahriman.application.functions as functions
import ahriman.version as version import ahriman.version as version
from ahriman.application.application import Application
from ahriman.application.lock import Lock from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -47,118 +47,6 @@ def _call(args: argparse.Namespace, architecture: str, config: Configuration) ->
return False return False
def add(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
add packages callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).add(args.package, args.without_dependencies)
def clean(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
clean caches callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).clean(args.no_build, args.no_cache, args.no_chroot,
args.no_manual, args.no_packages)
def dump_config(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
configuration dump callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
del args
config_dump = config.dump(architecture)
for section, values in sorted(config_dump.items()):
print(f'[{section}]')
for key, value in sorted(values.items()):
print(f'{key} = {value}')
print()
def rebuild(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
world rebuild callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
del args
app = Application(architecture, config)
packages = app.repository.packages()
app.update(packages)
def remove(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
remove packages callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).remove(args.package)
def report(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
generate report callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).report(args.target)
def sync(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
sync to remote server callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).sync(args.target)
def update(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
update packages callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
# typing workaround
def log_fn(line: str) -> None:
return print(line) if args.dry_run else application.logger.info(line)
application = Application(architecture, config)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
if args.dry_run:
return
application.update(packages)
def web(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
web server callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
del args
from ahriman.web.web import run_server, setup_service
application = setup_service(architecture, config)
run_server(application, architecture)
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager') parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager')
parser.add_argument( parser.add_argument(
@ -176,14 +64,14 @@ if __name__ == '__main__':
subparsers = parser.add_subparsers(title='command') subparsers = parser.add_subparsers(title='command')
add_parser = subparsers.add_parser('add', description='add package') add_parser = subparsers.add_parser('add', description='add package')
add_parser.add_argument('package', help='package name or archive path', nargs='+') add_parser.add_argument('package', help='package base/name or archive path', nargs='+')
add_parser.add_argument('--without-dependencies', help='do not add dependencies', action='store_true') add_parser.add_argument('--without-dependencies', help='do not add dependencies', action='store_true')
add_parser.set_defaults(fn=add) add_parser.set_defaults(fn=functions.add)
check_parser = subparsers.add_parser('check', description='check for updates. Same as update --dry-run --no-manual') check_parser = subparsers.add_parser('check', description='check for updates. Same as update --dry-run --no-manual')
check_parser.add_argument('package', help='filter check by packages', nargs='*') check_parser.add_argument('package', help='filter check by package base', nargs='*')
check_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true') check_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true')
check_parser.set_defaults(fn=update, no_aur=False, no_manual=True, dry_run=True) check_parser.set_defaults(fn=functions.update, no_aur=False, no_manual=True, dry_run=True)
clean_parser = subparsers.add_parser('clean', description='clear all local caches') clean_parser = subparsers.add_parser('clean', description='clear all local caches')
clean_parser.add_argument('--no-build', help='do not clear directory with package sources', action='store_true') clean_parser.add_argument('--no-build', help='do not clear directory with package sources', action='store_true')
@ -194,37 +82,42 @@ if __name__ == '__main__':
help='do not clear directory with manually added packages', help='do not clear directory with manually added packages',
action='store_true') action='store_true')
clean_parser.add_argument('--no-packages', help='do not clear directory with built packages', action='store_true') clean_parser.add_argument('--no-packages', help='do not clear directory with built packages', action='store_true')
clean_parser.set_defaults(fn=clean) clean_parser.set_defaults(fn=functions.clean)
config_parser = subparsers.add_parser('config', description='dump configuration for specified architecture') config_parser = subparsers.add_parser('config', description='dump configuration for specified architecture')
config_parser.set_defaults(fn=dump_config) config_parser.set_defaults(fn=functions.dump_config, lock=None, no_report=True)
rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository') rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository')
rebuild_parser.set_defaults(fn=rebuild) rebuild_parser.set_defaults(fn=functions.rebuild)
remove_parser = subparsers.add_parser('remove', description='remove package') remove_parser = subparsers.add_parser('remove', description='remove package')
remove_parser.add_argument('package', help='package name', nargs='+') remove_parser.add_argument('package', help='package name or base', nargs='+')
remove_parser.set_defaults(fn=remove) remove_parser.set_defaults(fn=functions.remove)
report_parser = subparsers.add_parser('report', description='generate report') report_parser = subparsers.add_parser('report', description='generate report')
report_parser.add_argument('target', help='target to generate report', nargs='*') report_parser.add_argument('target', help='target to generate report', nargs='*')
report_parser.set_defaults(fn=report) report_parser.set_defaults(fn=functions.report)
status_parser = subparsers.add_parser('status', description='request status of the package')
status_parser.add_argument('--ahriman', help='get service status itself', action='store_true')
status_parser.add_argument('package', help='filter status by package base', nargs='*')
status_parser.set_defaults(fn=functions.status, lock=None, no_report=True)
sync_parser = subparsers.add_parser('sync', description='sync packages to remote server') sync_parser = subparsers.add_parser('sync', description='sync packages to remote server')
sync_parser.add_argument('target', help='target to sync', nargs='*') sync_parser.add_argument('target', help='target to sync', nargs='*')
sync_parser.set_defaults(fn=sync) sync_parser.set_defaults(fn=functions.sync)
update_parser = subparsers.add_parser('update', description='run updates') update_parser = subparsers.add_parser('update', description='run updates')
update_parser.add_argument('package', help='filter check by packages', nargs='*') update_parser.add_argument('package', help='filter check by package base', nargs='*')
update_parser.add_argument( update_parser.add_argument(
'--dry-run', help='just perform check for updates, same as check command', action='store_true') '--dry-run', help='just perform check for updates, same as check command', action='store_true')
update_parser.add_argument('--no-aur', help='do not check for AUR updates. Implies --no-vcs', action='store_true') update_parser.add_argument('--no-aur', help='do not check for AUR updates. Implies --no-vcs', action='store_true')
update_parser.add_argument('--no-manual', help='do not include manual updates', action='store_true') update_parser.add_argument('--no-manual', help='do not include manual updates', action='store_true')
update_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true') update_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true')
update_parser.set_defaults(fn=update) update_parser.set_defaults(fn=functions.update)
web_parser = subparsers.add_parser('web', description='start web server') web_parser = subparsers.add_parser('web', description='start web server')
web_parser.set_defaults(fn=web, lock=None, no_report=True) web_parser.set_defaults(fn=functions.web, lock=None, no_report=True)
cmd_args = parser.parse_args() cmd_args = parser.parse_args()
if 'fn' not in cmd_args: if 'fn' not in cmd_args:

View File

@ -25,7 +25,7 @@ from typing import Callable, Iterable, List, Optional, Set
from ahriman.core.build_tools.task import Task from ahriman.core.build_tools.task import Task
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.repository.repository import Repository from ahriman.core.repository.repository import Repository
from ahriman.core.tree import Tree from ahriman.core.tree import Tree
from ahriman.models.package import Package from ahriman.models.package import Package

View File

@ -0,0 +1,163 @@
#
# 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/>.
#
import argparse
from typing import Iterable, Tuple
from ahriman.application.application import Application
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
def add(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
add packages callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).add(args.package, args.without_dependencies)
def clean(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
clean caches callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).clean(args.no_build, args.no_cache, args.no_chroot,
args.no_manual, args.no_packages)
def dump_config(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
configuration dump callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
del args
config_dump = config.dump(architecture)
for section, values in sorted(config_dump.items()):
print(f'[{section}]')
for key, value in sorted(values.items()):
print(f'{key} = {value}')
print()
def rebuild(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
world rebuild callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
del args
app = Application(architecture, config)
packages = app.repository.packages()
app.update(packages)
def remove(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
remove packages callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).remove(args.package)
def report(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
generate report callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).report(args.target)
def status(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
package status callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
application = Application(architecture, config)
if args.ahriman:
ahriman = application.repository.reporter.get_self()
print(ahriman.pretty_print())
print()
if args.package:
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
[application.repository.reporter.get(base) for base in args.package],
start=[])
else:
packages = application.repository.reporter.get(None)
for package, package_status in sorted(packages, key=lambda item: item[0].base):
print(package.pretty_print())
print(f'\t{package.version}')
print(f'\t{package_status.pretty_print()}')
def sync(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
sync to remote server callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).sync(args.target)
def update(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
update packages callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
# typing workaround
def log_fn(line: str) -> None:
return print(line) if args.dry_run else application.logger.info(line)
application = Application(architecture, config)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
if args.dry_run:
return
application.update(packages)
def web(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
web server callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
del args
from ahriman.web.web import run_server, setup_service
application = setup_service(architecture, config)
run_server(application, architecture)

View File

@ -22,7 +22,7 @@ import shutil
from typing import List from typing import List
from ahriman.repository.properties import Properties from ahriman.core.repository.properties import Properties
class Cleaner(Properties): class Cleaner(Properties):

View File

@ -24,9 +24,9 @@ from typing import Dict, Iterable, List, Optional
from ahriman.core.build_tools.task import Task from ahriman.core.build_tools.task import Task
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.upload.uploader import Uploader from ahriman.core.upload.uploader import Uploader
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.repository.cleaner import Cleaner
class Executor(Cleaner): class Executor(Cleaner):

View File

@ -21,10 +21,10 @@ import os
from typing import Dict, List from typing import Dict, List
from ahriman.core.repository.executor import Executor
from ahriman.core.repository.update_handler import UpdateHandler
from ahriman.core.util import package_like from ahriman.core.util import package_like
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.repository.executor import Executor
from ahriman.repository.update_handler import UpdateHandler
class Repository(Executor, UpdateHandler): class Repository(Executor, UpdateHandler):

View File

@ -21,8 +21,8 @@ import os
from typing import Iterable, List from typing import Iterable, List
from ahriman.core.repository.cleaner import Cleaner
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.repository.cleaner import Cleaner
class UpdateHandler(Cleaner): class UpdateHandler(Cleaner):

View File

@ -19,8 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import List, Optional, Tuple
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
@ -36,10 +38,28 @@ class Client:
:param status: current package build status :param status: current package build status
''' '''
# pylint: disable=R0201
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
'''
get package status
:param base: package base to get
:return: list of current package description and status if it has been found
'''
del base
return []
# pylint: disable=R0201
def get_self(self) -> BuildStatus:
'''
get ahriman status itself
:return: current ahriman status
'''
return BuildStatus()
def remove(self, base: str) -> None: def remove(self, base: str) -> None:
''' '''
remove packages from watcher remove packages from watcher
:param base: basename to remove :param base: package base to remove
''' '''
def update(self, base: str, status: BuildStatusEnum) -> None: def update(self, base: str, status: BuildStatusEnum) -> None:

View File

@ -24,7 +24,7 @@ import os
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.repository.repository import Repository from ahriman.core.repository.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
@ -73,7 +73,7 @@ class Watcher:
''' '''
def parse_single(properties: Dict[str, Any]) -> None: def parse_single(properties: Dict[str, Any]) -> None:
package = Package.from_json(properties['package']) package = Package.from_json(properties['package'])
status = BuildStatus(**properties['status']) status = BuildStatus.from_json(properties['status'])
if package.base in self.known: if package.base in self.known:
self.known[package.base] = (package, status) self.known[package.base] = (package, status)

View File

@ -18,10 +18,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import logging import logging
from typing import List, Optional, Tuple
import requests import requests
from ahriman.core.watcher.client import Client from ahriman.core.watcher.client import Client
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum, BuildStatus
from ahriman.models.package import Package from ahriman.models.package import Package
@ -50,7 +52,7 @@ class WebClient(Client):
''' '''
return f'http://{self.host}:{self.port}/api/v1/ahriman' return f'http://{self.host}:{self.port}/api/v1/ahriman'
def _package_url(self, base: str) -> str: 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
@ -77,6 +79,44 @@ class WebClient(Client):
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)
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
'''
get package status
:param base: package base to get
:return: list of current package description and status if it has been found
'''
try:
response = requests.get(self._package_url(base or ''))
response.raise_for_status()
status_json = response.json()
return [
(Package.from_json(package['package']), BuildStatus.from_json(package['status']))
for package in status_json
]
except requests.exceptions.HTTPError as e:
self.logger.exception(f'could not get {base}: {e.response.text}', exc_info=True)
except Exception:
self.logger.exception(f'could not get {base}', exc_info=True)
return []
def get_self(self) -> BuildStatus:
'''
get ahriman status itself
:return: current ahriman status
'''
try:
response = requests.get(self._ahriman_url())
response.raise_for_status()
status_json = response.json()
return BuildStatus.from_json(status_json)
except requests.exceptions.HTTPError as e:
self.logger.exception(f'could not get service status: {e.response.text}', exc_info=True)
except Exception:
self.logger.exception('could not get service status', exc_info=True)
return BuildStatus()
def remove(self, base: str) -> None: def remove(self, base: str) -> None:
''' '''
remove packages from watcher remove packages from watcher

View File

@ -17,10 +17,14 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from __future__ import annotations
import datetime import datetime
from enum import Enum from enum import Enum
from typing import Any, Dict, Optional, Union from typing import Any, Dict, Optional, Type, Union
from ahriman.core.util import pretty_datetime
class BuildStatusEnum(Enum): class BuildStatusEnum(Enum):
@ -72,6 +76,22 @@ class BuildStatus:
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp()) self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())
@classmethod
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
'''
construct status properties from json dump
:param dump: json dump body
:return: status properties
'''
return cls(dump.get('status'), dump.get('timestamp'))
def pretty_print(self) -> str:
'''
generate pretty string representation
:return: print-friendly string
'''
return f'{self.status.value} ({pretty_datetime(self.timestamp)})'
def view(self) -> Dict[str, Any]: def view(self) -> Dict[str, Any]:
''' '''
generate json status view generate json status view
@ -81,3 +101,10 @@ class BuildStatus:
'status': self.status.value, 'status': self.status.value,
'timestamp': self.timestamp 'timestamp': self.timestamp
} }
def __repr__(self) -> str:
'''
generate string representation of object
:return: unique string representation
'''
return f'BuildStatus(status={self.status.value}, timestamp={self.timestamp})'

View File

@ -57,6 +57,13 @@ class Package:
''' '''
return f'{self.aur_url}/{self.base}.git' return f'{self.aur_url}/{self.base}.git'
@property
def is_single_package(self) -> bool:
'''
:return: true in case if this base has only one package with the same name
'''
return self.base in self.packages and len(self.packages) == 1
@property @property
def is_vcs(self) -> bool: def is_vcs(self) -> bool:
''' '''
@ -224,6 +231,14 @@ class Package:
result: int = vercmp(self.version, remote_version) result: int = vercmp(self.version, remote_version)
return result < 0 return result < 0
def pretty_print(self) -> str:
'''
generate pretty string representation
:return: print-friendly string
'''
details = '' if self.is_single_package else f''' ({' '.join(sorted(self.packages.keys()))})'''
return f'{self.base}{details}'
def view(self) -> Dict[str, Any]: def view(self) -> Dict[str, Any]:
''' '''
generate json package view generate json package view

View File

@ -41,7 +41,7 @@ class IndexView(BaseView):
version - ahriman version, string, required version - ahriman version, string, required
''' '''
@aiohttp_jinja2.template("build-status.jinja2") @aiohttp_jinja2.template('build-status.jinja2')
async def get(self) -> Dict[str, Any]: async def get(self) -> Dict[str, Any]:
''' '''
process get request. No parameters supported here process get request. No parameters supported here

View File

@ -41,10 +41,12 @@ class PackageView(BaseView):
except KeyError: except KeyError:
raise HTTPNotFound() raise HTTPNotFound()
response = { response = [
{
'package': package.view(), 'package': package.view(),
'status': status.view() 'status': status.view()
} }
]
return json_response(response) return json_response(response)
async def delete(self) -> Response: async def delete(self) -> Response: