mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-15 06:55:48 +00:00
add config validator subcommand (#80)
* add config validator subcommand * add --exit-code flag * docs & faq update
This commit is contained in:
@ -101,6 +101,7 @@ def _parser() -> argparse.ArgumentParser:
|
||||
_set_repo_check_parser(subparsers)
|
||||
_set_repo_clean_parser(subparsers)
|
||||
_set_repo_config_parser(subparsers)
|
||||
_set_repo_config_validate_parser(subparsers)
|
||||
_set_repo_rebuild_parser(subparsers)
|
||||
_set_repo_remove_unknown_parser(subparsers)
|
||||
_set_repo_report_parser(subparsers)
|
||||
@ -537,6 +538,25 @@ def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def _set_repo_config_validate_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for config validation subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("repo-config-validate", aliases=["config-validate"], help="validate system configuration",
|
||||
description="validate configuration and print found errors",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if configuration is invalid",
|
||||
action="store_true")
|
||||
parser.set_defaults(handler=handlers.Validate, lock=None, report=False, quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for repository rebuild subcommand
|
||||
|
@ -42,5 +42,6 @@ from ahriman.application.handlers.triggers import Triggers
|
||||
from ahriman.application.handlers.unsafe_commands import UnsafeCommands
|
||||
from ahriman.application.handlers.update import Update
|
||||
from ahriman.application.handlers.users import Users
|
||||
from ahriman.application.handlers.validate import Validate
|
||||
from ahriman.application.handlers.versions import Versions
|
||||
from ahriman.application.handlers.web import Web
|
||||
|
150
src/ahriman/application/handlers/validate.py
Normal file
150
src/ahriman/application/handlers/validate.py
Normal file
@ -0,0 +1,150 @@
|
||||
#
|
||||
# Copyright (c) 2021-2023 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/>.
|
||||
#
|
||||
import argparse
|
||||
import copy
|
||||
|
||||
from typing import Any, Callable, Dict, Optional, Type
|
||||
|
||||
from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.configuration.schema import CONFIGURATION_SCHEMA, \
|
||||
GITREMOTE_REMOTE_PULL_SCHEMA, GITREMOTE_REMOTE_PUSH_SCHEMA, \
|
||||
REPORT_CONSOLE_SCHEMA, REPORT_EMAIL_SCHEMA, REPORT_HTML_SCHEMA, REPORT_TELEGRAM_SCHEMA,\
|
||||
UPLOAD_GITHUB_SCHEMA, UPLOAD_RSYNC_SCHEMA, UPLOAD_S3_SCHEMA
|
||||
from ahriman.core.configuration.validator import Validator
|
||||
from ahriman.core.formatters import ValidationPrinter
|
||||
|
||||
|
||||
class Validate(Handler):
|
||||
"""
|
||||
configuration validator handler
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *,
|
||||
report: bool, unsafe: bool) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line args
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
schema = Validate.schema(architecture, configuration)
|
||||
validator = Validator(instance=configuration, schema=schema)
|
||||
|
||||
if validator.validate(configuration.dump()):
|
||||
return # no errors found
|
||||
for node, errors in validator.errors.items():
|
||||
ValidationPrinter(node, errors).print(verbose=True)
|
||||
|
||||
# as we reach this part it means that we always have errors
|
||||
Validate.check_if_empty(args.exit_code, True)
|
||||
|
||||
@staticmethod
|
||||
def schema(architecture: str, configuration: Configuration) -> Dict[str, Any]:
|
||||
"""
|
||||
get schema with triggers
|
||||
|
||||
Args:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: configuration validation schema
|
||||
"""
|
||||
root = copy.deepcopy(CONFIGURATION_SCHEMA)
|
||||
|
||||
# that's actually bad design, but in order to validate built-in triggers we need to know which are set
|
||||
Validate.schema_insert(architecture, configuration, root, "remote-pull", lambda _: GITREMOTE_REMOTE_PULL_SCHEMA)
|
||||
Validate.schema_insert(architecture, configuration, root, "remote-push", lambda _: GITREMOTE_REMOTE_PUSH_SCHEMA)
|
||||
|
||||
report_schemas = {
|
||||
"console": REPORT_CONSOLE_SCHEMA,
|
||||
"email": REPORT_EMAIL_SCHEMA,
|
||||
"html": REPORT_HTML_SCHEMA,
|
||||
"telegram": REPORT_TELEGRAM_SCHEMA,
|
||||
}
|
||||
for schema_name, schema in report_schemas.items():
|
||||
root[schema_name] = Validate.schema_erase_required(copy.deepcopy(schema))
|
||||
Validate.schema_insert(architecture, configuration, root, "report", report_schemas.get)
|
||||
|
||||
upload_schemas = {
|
||||
"github": UPLOAD_GITHUB_SCHEMA,
|
||||
"rsync": UPLOAD_RSYNC_SCHEMA,
|
||||
"s3": UPLOAD_S3_SCHEMA,
|
||||
}
|
||||
for schema_name, schema in upload_schemas.items():
|
||||
root[schema_name] = Validate.schema_erase_required(copy.deepcopy(schema))
|
||||
Validate.schema_insert(architecture, configuration, root, "upload", upload_schemas.get)
|
||||
|
||||
return root
|
||||
|
||||
@staticmethod
|
||||
def schema_erase_required(schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
recursively remove required field from supplied cerberus schema
|
||||
|
||||
Args:
|
||||
schema(Dict[str, Any]): source schema from which required field must be removed
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: schema without required fields
|
||||
"""
|
||||
schema.pop("required", None)
|
||||
for value in filter(lambda v: isinstance(v, dict), schema.values()):
|
||||
Validate.schema_erase_required(value)
|
||||
return schema
|
||||
|
||||
@staticmethod
|
||||
def schema_insert(architecture: str, configuration: Configuration, root: Dict[str, Any], root_section: str,
|
||||
schema_mapping: Callable[[str], Optional[Dict[str, Any]]]) -> Dict[str, Any]:
|
||||
"""
|
||||
insert child schema into the root schema based on mapping rules
|
||||
|
||||
Notes:
|
||||
Actually it is a bad design, because we are reading triggers configuration from parsers which (basically)
|
||||
don't know anything about triggers. But in order to validate built-in triggers we need to know which are set
|
||||
|
||||
Args:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
root(Dict[str, Any]): root schema in which child schema will be inserted
|
||||
root_section(str): section name in root schema
|
||||
schema_mapping(Callable[[str], Optional[Dict[str, Any]]]): extractor for child schema based on trigger type
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: modified root schema. Note, however, that schema will be modified in place
|
||||
"""
|
||||
if not configuration.has_section(root_section):
|
||||
return root
|
||||
|
||||
targets = configuration.getlist(root_section, "target", fallback=[])
|
||||
for target in targets:
|
||||
section, schema_name = configuration.gettype(target, architecture)
|
||||
if (schema := schema_mapping(schema_name)) is not None:
|
||||
root[section] = copy.deepcopy(schema)
|
||||
|
||||
return root
|
20
src/ahriman/core/configuration/__init__.py
Normal file
20
src/ahriman/core/configuration/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
#
|
||||
# Copyright (c) 2021-2023 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 ahriman.core.configuration.configuration import Configuration
|
@ -24,7 +24,7 @@ import shlex
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Type
|
||||
|
||||
from ahriman.core.exceptions import InitializeError
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
@ -63,6 +63,7 @@ class Configuration(configparser.RawConfigParser):
|
||||
|
||||
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
|
||||
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
|
||||
converters: Dict[str, Callable[[str], Any]] # typing guard
|
||||
|
||||
def __init__(self, allow_no_value: bool = False) -> None:
|
||||
"""
|
||||
@ -74,7 +75,7 @@ class Configuration(configparser.RawConfigParser):
|
||||
"""
|
||||
configparser.RawConfigParser.__init__(self, allow_no_value=allow_no_value, converters={
|
||||
"list": shlex.split,
|
||||
"path": self.__convert_path,
|
||||
"path": self._convert_path,
|
||||
})
|
||||
self.architecture: Optional[str] = None
|
||||
self.path: Optional[Path] = None
|
||||
@ -141,7 +142,7 @@ class Configuration(configparser.RawConfigParser):
|
||||
"""
|
||||
return f"{section}:{suffix}"
|
||||
|
||||
def __convert_path(self, value: str) -> Path:
|
||||
def _convert_path(self, value: str) -> Path:
|
||||
"""
|
||||
convert string value to path object
|
||||
|
554
src/ahriman/core/configuration/schema.py
Normal file
554
src/ahriman/core/configuration/schema.py
Normal file
@ -0,0 +1,554 @@
|
||||
#
|
||||
# Copyright (c) 2021-2023 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/>.
|
||||
#
|
||||
# pylint: disable=too-many-lines
|
||||
__all__ = [
|
||||
"CONFIGURATION_SCHEMA",
|
||||
"GITREMOTE_REMOTE_PULL_SCHEMA", "GITREMOTE_REMOTE_PUSH_SCHEMA",
|
||||
"REPORT_CONSOLE_SCHEMA", "REPORT_EMAIL_SCHEMA", "REPORT_HTML_SCHEMA", "REPORT_TELEGRAM_SCHEMA",
|
||||
"UPLOAD_GITHUB_SCHEMA", "UPLOAD_RSYNC_SCHEMA", "UPLOAD_S3_SCHEMA",
|
||||
]
|
||||
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"settings": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"include": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"database": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
},
|
||||
"logging": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"alpm": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"database": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
},
|
||||
"mirror": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"repositories": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"root": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"use_ahriman_cache": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"auth": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"oneof": [
|
||||
{"allowed": ["disabled"]},
|
||||
{"allowed": ["configuration", "mapping"], "dependencies": ["salt"]},
|
||||
{"allowed": ["oauth"], "dependencies": [
|
||||
"client_id", "client_secret", "oauth_provider", "oauth_scopes", "salt"
|
||||
]},
|
||||
],
|
||||
},
|
||||
"allow_read_only": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
"required": True,
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"client_secret": {
|
||||
"type": "string",
|
||||
},
|
||||
"max_age": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
"oauth_provider": {
|
||||
"type": "string",
|
||||
},
|
||||
"oauth_scopes": {
|
||||
"type": "string",
|
||||
},
|
||||
"salt": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"build": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"archbuild_flags": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"build_command": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"ignore_packages": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"makepkg_flags": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"makechrootpkg_flags": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"triggers": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"vcs_allowed_age": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
},
|
||||
},
|
||||
"repository": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"root": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"sign": {
|
||||
"type": "dict",
|
||||
"allow_unknown": True,
|
||||
"keysrules": {
|
||||
"type": "string",
|
||||
"anyof_regex": ["^target$", "^key$", "^key_.*"],
|
||||
},
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"oneof": [
|
||||
{"allowed": []},
|
||||
{"allowed": ["package", "repository"], "dependencies": ["key"]},
|
||||
],
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"web": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
"debug_check_host": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
"debug_allowed_hosts": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
},
|
||||
"index_url": {
|
||||
"type": "string",
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
"max": 65535,
|
||||
},
|
||||
"static_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"templates": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"unix_socket": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
},
|
||||
"unix_socket_unsafe": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"remote-pull": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"remote-push": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"report": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"upload": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
GITREMOTE_REMOTE_PULL_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"pull_url": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"pull_branch": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
GITREMOTE_REMOTE_PUSH_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"commit_author": {
|
||||
"type": "string",
|
||||
},
|
||||
"push_url": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"push_branch": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
REPORT_CONSOLE_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["console"],
|
||||
},
|
||||
"use_utf": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
REPORT_EMAIL_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["email"],
|
||||
},
|
||||
"full_template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"path_exists": True,
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"no_empty_report": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"required": True,
|
||||
},
|
||||
"receivers": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"sender": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"ssl": {
|
||||
"type": "string",
|
||||
"allowed": ["ssl", "starttls", "disabled"],
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
REPORT_HTML_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["html"],
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
REPORT_TELEGRAM_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["telegram"],
|
||||
},
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"chat_id": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"template_type": {
|
||||
"type": "string",
|
||||
"allowed": ["MarkdownV2", "HTML", "Markdown"],
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
UPLOAD_GITHUB_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["github"],
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
UPLOAD_RSYNC_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["rsync"],
|
||||
},
|
||||
"command": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"remote": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
UPLOAD_S3_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["s3"],
|
||||
},
|
||||
"access_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"bucket": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"chunk_size": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"secret_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
}
|
116
src/ahriman/core/configuration/validator.py
Normal file
116
src/ahriman/core/configuration/validator.py
Normal file
@ -0,0 +1,116 @@
|
||||
#
|
||||
# Copyright (c) 2021-2023 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 cerberus import TypeDefinition, Validator as RootValidator # type: ignore
|
||||
from pathlib import Path
|
||||
from typing import Any, List
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Validator(RootValidator): # type: ignore
|
||||
"""
|
||||
class which defines custom validation methods for the service configuration
|
||||
|
||||
Attributes:
|
||||
instance(Configuration): configuration instance
|
||||
"""
|
||||
|
||||
types_mapping = RootValidator.types_mapping.copy()
|
||||
types_mapping["path"] = TypeDefinition("path", (Path,), ())
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
instance(Configuration): configuration instance used for extraction
|
||||
*args(Any): positional arguments to be passed to base validator
|
||||
**kwargs(): keyword arguments to be passed to base validator
|
||||
"""
|
||||
RootValidator.__init__(self, *args, **kwargs)
|
||||
self.instance: Configuration = kwargs["instance"]
|
||||
|
||||
def _normalize_coerce_absolute_path(self, value: str) -> Path:
|
||||
"""
|
||||
extract path from string value
|
||||
|
||||
Args:
|
||||
value(str): converting value
|
||||
|
||||
Returns:
|
||||
Path: value converted to path instance according to configuration rules
|
||||
"""
|
||||
converted: Path = self.instance.converters["path"](value)
|
||||
return converted
|
||||
|
||||
def _normalize_coerce_boolean(self, value: str) -> bool:
|
||||
"""
|
||||
extract boolean from string value
|
||||
|
||||
Args:
|
||||
value(str): converting value
|
||||
|
||||
Returns:
|
||||
bool: value converted to boolean according to configuration rules
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
converted: bool = self.instance._convert_to_boolean(value) # type: ignore
|
||||
return converted
|
||||
|
||||
def _normalize_coerce_integer(self, value: str) -> int:
|
||||
"""
|
||||
extract integer from string value
|
||||
|
||||
Args:
|
||||
value(str): converting value
|
||||
|
||||
Returns:
|
||||
int: value converted to int according to configuration rules
|
||||
"""
|
||||
return int(value)
|
||||
|
||||
def _normalize_coerce_list(self, value: str) -> List[str]:
|
||||
"""
|
||||
extract string list from string value
|
||||
|
||||
Args:
|
||||
value(str): converting value
|
||||
|
||||
Returns:
|
||||
List[str]: value converted to string list instance according to configuration rules
|
||||
"""
|
||||
converted: List[str] = self.instance.converters["list"](value)
|
||||
return converted
|
||||
|
||||
def _validate_path_exists(self, constraint: bool, field: str, value: Path) -> None:
|
||||
"""
|
||||
check if paths exists
|
||||
|
||||
Args:
|
||||
constraint(bool): True in case if path must exist and False otherwise
|
||||
field(str): field name to be checked
|
||||
value(Path): value to be checked
|
||||
|
||||
Examples:
|
||||
The rule's arguments are validated against this schema:
|
||||
{"type": "boolean"}
|
||||
"""
|
||||
if constraint and not value.exists():
|
||||
self._error(field, f"Path {value} must exist")
|
@ -27,6 +27,7 @@ from ahriman.core.formatters.package_printer import PackagePrinter
|
||||
from ahriman.core.formatters.patch_printer import PatchPrinter
|
||||
from ahriman.core.formatters.status_printer import StatusPrinter
|
||||
from ahriman.core.formatters.tree_printer import TreePrinter
|
||||
from ahriman.core.formatters.validation_printer import ValidationPrinter
|
||||
from ahriman.core.formatters.update_printer import UpdatePrinter
|
||||
from ahriman.core.formatters.user_printer import UserPrinter
|
||||
from ahriman.core.formatters.version_printer import VersionPrinter
|
||||
|
@ -41,7 +41,8 @@ class Printer:
|
||||
for prop in self.properties():
|
||||
if not verbose and not prop.is_required:
|
||||
continue
|
||||
log_fn(f"\t{prop.name}{separator}{prop.value}")
|
||||
indent = "\t" * prop.indent
|
||||
log_fn(f"{indent}{prop.name}{separator}{prop.value}")
|
||||
|
||||
def properties(self) -> List[Property]:
|
||||
"""
|
||||
|
77
src/ahriman/core/formatters/validation_printer.py
Normal file
77
src/ahriman/core/formatters/validation_printer.py
Normal file
@ -0,0 +1,77 @@
|
||||
#
|
||||
# Copyright (c) 2021-2023 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 typing import Any, Dict, Generator, List, Union
|
||||
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
class ValidationPrinter(StringPrinter):
|
||||
"""
|
||||
print content of the validation errors
|
||||
|
||||
Attributes:
|
||||
node(str): root level name
|
||||
errors(List[Union[str, Dict[str, Any]]]): validation errors
|
||||
"""
|
||||
|
||||
def __init__(self, node: str, errors: List[Union[str, Dict[str, Any]]]) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
node(str): root level name
|
||||
errors(List[Union[str, Dict[str, Any]]]): validation errors
|
||||
"""
|
||||
StringPrinter.__init__(self, node)
|
||||
self.node = node
|
||||
self.errors = errors
|
||||
|
||||
@staticmethod
|
||||
def get_error_messages(node: str, errors: List[Union[str, Dict[str, Any]]],
|
||||
current_level: int = 1) -> Generator[Property, None, None]:
|
||||
"""
|
||||
extract default error message from cerberus class
|
||||
|
||||
Args:
|
||||
node(str): current node level name
|
||||
errors(List[Union[str, Dict[str, Any]]]): current node validation errors
|
||||
current_level(int, optional): current level number (Default value = 1)
|
||||
|
||||
Yields:
|
||||
Property: error messages from error tree
|
||||
"""
|
||||
for error in errors:
|
||||
if not isinstance(error, str): # child nodes errors
|
||||
for child_node, child_errors in error.items():
|
||||
# increase indentation instead of nodes concatenations
|
||||
# sometimes it is not only nodes, but rules themselves
|
||||
yield from ValidationPrinter.get_error_messages(child_node, child_errors, current_level + 1)
|
||||
else: # current node errors
|
||||
yield Property(node, error, is_required=True, indent=current_level)
|
||||
|
||||
def properties(self) -> List[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
|
||||
Returns:
|
||||
List[Property]: list of content properties
|
||||
"""
|
||||
return list(self.get_error_messages(self.node, self.errors))
|
@ -30,8 +30,10 @@ class Property:
|
||||
name(str): name of the property
|
||||
value(Any): property value
|
||||
is_required(bool): if set to True then this property is required
|
||||
indent(int): property indentation level
|
||||
"""
|
||||
|
||||
name: str
|
||||
value: Any
|
||||
is_required: bool = field(default=False, kw_only=True)
|
||||
indent: int = 1
|
||||
|
Reference in New Issue
Block a user