Skip to content

Commit

Permalink
step validators
Browse files Browse the repository at this point in the history
  • Loading branch information
a1fred committed Dec 3, 2021
1 parent 65a1e1b commit 471a018
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 41 deletions.
2 changes: 1 addition & 1 deletion carnival/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import sys
import dotenv
import os
from carnival.step import Step, InlineStep
from carnival.hosts.base import Host, Connection, Result
from carnival.steps import Step, InlineStep
from carnival.role import Role, SingleRole
from carnival.hosts.local import LocalHost, localhost_connection
from carnival.hosts.ssh import SshHost
Expand Down
6 changes: 0 additions & 6 deletions carnival/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,3 @@ class GlobalConnectionError(CarnivalException):
"""
Global connection switching error
"""


class StepValidationError(CarnivalException):
"""
Ошибка валидации шага
"""
7 changes: 7 additions & 0 deletions carnival/steps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from carnival.steps.step import Step, InlineStep


__all__ = (
"Step",
"InlineStep",
)
46 changes: 46 additions & 0 deletions carnival/steps/_validator_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
Кеширование валидаторов для Steps
Кеширует значение по Type[Step], Host, fact_id:str(задается в валидаторе)
"""

import typing

if typing.TYPE_CHECKING:
from carnival import Host
from carnival.steps.validators import StepValidatorBase


StepValidatorBaseT = typing.TypeVar("StepValidatorBaseT", bound="StepValidatorBase")
__vc: typing.Dict[str, typing.Optional[str]] = {}


def __build_vc_key(step_class: typing.Type[StepValidatorBaseT], host: "Host", fact_id: str) -> str:
from carnival.utils import get_class_full_name
return f"{get_class_full_name(step_class)}::{host.addr}::{fact_id}"


def try_get(
step_class: typing.Type[StepValidatorBaseT],
host: "Host",
fact_id: str,
) -> typing.Tuple[bool, typing.Optional[str]]:
global __vc

return (
__build_vc_key(step_class=step_class, host=host, fact_id=fact_id) in __vc,
__vc.get(
__build_vc_key(step_class=step_class, host=host, fact_id=fact_id),
None,
)
)


def set(step_class: typing.Type[StepValidatorBaseT], host: "Host", fact_id: str, val: typing.Optional[str]) -> None:
global __vc
cachekey = __build_vc_key(step_class=step_class, host=host, fact_id=fact_id)

is_exist, *_ = try_get(step_class=step_class, host=host, fact_id=fact_id)
if is_exist:
raise ValueError(f"Broken cache: '{cachekey}' already exist!")

__vc[cachekey] = val
29 changes: 18 additions & 11 deletions carnival/step.py → carnival/steps/step.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import abc
import typing


if typing.TYPE_CHECKING:
from carnival.steps.validators import StepValidatorBase
from carnival import Connection


Expand All @@ -25,27 +25,34 @@ class Step:
>>> ...
"""

def __init__(self) -> None:
pass

def get_name(self) -> str:
return self.__class__.__name__

def validate(self, c: "Connection") -> None:
def get_validators(self) -> typing.List["StepValidatorBase"]:
"""
Получить список валидаторов для метода `.validate`
"""
return []

def validate(self, c: "Connection") -> typing.List[str]:
"""
Валидатор шага, запускается перед выполнением
Должен выкидывать .StepValidationError в случае ошибки
:param c: Конект к хосту
:return: Список ошибок
"""

:param host: На котором будет выполнен шаг
errors: typing.List[str] = []

:raises StepValidationError: в случае ошибок валидации
for validator in self.get_validators():
err = validator.validate(c=c)
if err is not None:
errors.append(err)

>>> from carnival.exceptions import StepValidationError
>>> ...
>>> def validate(self, c: "Connection") -> None:
>>> raise StepValidationError("Step validation is not implemented")
"""
pass
return errors

@abc.abstractmethod
def run(self, c: "Connection") -> typing.Any:
Expand Down
81 changes: 81 additions & 0 deletions carnival/steps/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import typing
import abc

from carnival import cmd
from carnival.steps import _validator_cache
from carnival.templates import render

from jinja2.exceptions import UndefinedError, TemplateNotFound, TemplateSyntaxError


if typing.TYPE_CHECKING:
from carnival import Connection
from carnival.steps.step import Step


class StepValidatorBase:
def __init__(self, step: "Step"):
self.step = step

@abc.abstractmethod
def validate(self, c: "Connection") -> typing.Optional[str]:
raise NotImplementedError


class InlineValidator(StepValidatorBase):
def __init__(
self,
if_err_true_fn: typing.Callable[["Connection"], bool],
error_message: str,
fact_id_for_caching: typing.Optional[str] = None,
):
self.if_err_true_fn = if_err_true_fn
self.error_message = error_message
self.fact_id_for_caching = fact_id_for_caching

def validate(self, c: "Connection") -> typing.Optional[str]:
if self.fact_id_for_caching is not None:
is_exist, val = _validator_cache.try_get(self.__class__, c.host, self.fact_id_for_caching)
if is_exist:
return val

val = None
if self.if_err_true_fn(c):
val = self.error_message

if self.fact_id_for_caching is not None:
_validator_cache.set(self.__class__, c.host, self.fact_id_for_caching, val=val)
return val


class CommandRequiredValidator(StepValidatorBase):
def __init__(self, command: str) -> None:
self.command = command
self.fact_id = f"path-{command}-required"

def validate(self, c: "Connection") -> typing.Optional[str]:
is_exist, val = _validator_cache.try_get(self.__class__, c.host, self.fact_id)

if is_exist:
return val

val = None
if not cmd.cli.is_cmd_exist(c, self.command):
val = f"'{self.command}' is required"

_validator_cache.set(self.__class__, c.host, self.fact_id, val=val)
return val


class TemplateValidator(StepValidatorBase):
def __init__(self, template_path: str, context: typing.Dict[str, typing.Any]):
self.template_path = template_path
self.context = context

def validate(self, c: "Connection") -> typing.Optional[str]:
# TODO: more catch errors maybe?
try:
render(self.template_path, **self.context)
except (UndefinedError, TemplateNotFound, TemplateSyntaxError) as ex:
return f"{ex.__class__.__name__}: {ex}"
return None
24 changes: 13 additions & 11 deletions carnival/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

from colorama import Fore as F, Style as S, Back as B # type: ignore

from carnival import Step
from carnival.role import Role
from carnival.exceptions import StepValidationError
if typing.TYPE_CHECKING:
from carnival.role import Role
from carnival import Step


def _underscore(word: str) -> str:
Expand Down Expand Up @@ -89,7 +89,7 @@ def run(self) -> None:
raise NotImplementedError


RoleT = typing.TypeVar("RoleT", bound=Role)
RoleT = typing.TypeVar("RoleT", bound="Role")


class Task(abc.ABC, typing.Generic[RoleT], TaskBase):
Expand Down Expand Up @@ -118,7 +118,7 @@ def __init__(self, no_validate: bool) -> None:
print(f"[WARN]: not hosts for {self.role_class}", file=sys.stderr)

@abc.abstractmethod
def get_steps(self) -> typing.List[Step]:
def get_steps(self) -> typing.List["Step"]:
"""
Список шагов в порядке выполнения
"""
Expand All @@ -134,20 +134,22 @@ def validate(self) -> bool:
from carnival.cli import carnival_tasks_module
from carnival.tasks_loader import get_task_full_name
task_name = get_task_full_name(carnival_tasks_module, self.__class__)
print(f"Validating task {S.BRIGHT}{F.BLUE}{task_name}{F.RESET}{S.RESET_ALL}", end="", flush=True)
print(f"Validating task {S.BRIGHT}{F.BLUE}{task_name}{F.RESET}{S.RESET_ALL} ", end="", flush=True)
errors: typing.List[str] = []

for hostrole in self.hostroles:
with hostrole.host.connect() as c:
self.role = hostrole
for step in self.get_steps():
try:
step.validate(c=c)
step_errors = step.validate(c=c)

if not step_errors:
print(f"{F.GREEN}.{F.RESET}", end="", flush=True)
except StepValidationError as ex:
else:
step_name = step.get_name()
errors.append(f"{task_name} -> {step_name} on {hostrole.host}: {F.RED}{ex}{F.RESET}")
print("{Fore.RED}e{Fore.RESET}", end="", flush=True)
for e in step_errors:
errors.append(f"{task_name} -> {step_name} on {hostrole.host}: {F.RED}{e}{F.RESET}")
print(f"{F.RED}e{F.RESET}", end="", flush=True)
del self.role

if errors:
Expand Down
22 changes: 20 additions & 2 deletions carnival/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@
PrefixLoader,
)
from jinja2.runtime import StrictUndefined
from jinja2.exceptions import UndefinedError
from jinja2.exceptions import UndefinedError, TemplateSyntaxError

from carnival.plugins import discover_plugins


def escape_yaml(data: str) -> str:
"""
Jinja2 filter for escape yaml dollar sign
"""
return data.replace("$", "$$")


"""
Initialize loader on current working dir and plugin modules
"""
Expand All @@ -21,10 +29,14 @@
FileSystemLoader(os.getcwd()),
PrefixLoader({x: PackageLoader(x, package_path="") for x in discover_plugins().keys()}),
]),
keep_trailing_newline=True,
undefined=StrictUndefined,
)


j2_env.filters['escape_yaml'] = escape_yaml


def render(template_path: str, **context: Any) -> str:
"""
Отрендерить jinja2-шаблон в строку
Expand All @@ -36,4 +48,10 @@ def render(template_path: str, **context: Any) -> str:
template = j2_env.get_template(template_path)
return template.render(**context)
except UndefinedError as ex:
raise UndefinedError(f"Can't render template {template_path}: {ex}")
raise UndefinedError(f"Can't render template {template_path} - {ex}") from ex
except TemplateSyntaxError as ex:
raise TemplateSyntaxError(
message=f"Can't render template {template_path}:{ex.lineno} - {ex.message}",
lineno=ex.lineno,
filename=ex.filename,
) from ex
8 changes: 5 additions & 3 deletions carnival_tasks_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import os
from carnival import cmd, TaskBase, SshHost, Step, Task, Connection, Role
from carnival.exceptions import StepValidationError


class PackagesRole(Role):
Expand All @@ -34,9 +33,12 @@ def __init__(self, packages: typing.List[str], update: bool = True) -> None:
self.packages = self.packages.strip()
self.update = update

def validate(self, c: Connection) -> None:
def validate(self, c: Connection) -> typing.List[str]:
errors = []
if not self.packages:
raise StepValidationError("packages cant be empty!")
errors.append("packages cant be empty!")

return errors

def run(self, c: Connection) -> None:
if self.update:
Expand Down
7 changes: 0 additions & 7 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +0,0 @@
from carnival import utils, LocalHost


def test_log(capsys):
utils.log("Hellotest", host=LocalHost())
captured = capsys.readouterr()
assert captured.out == "💃💃💃 🖥 local> Hellotest\n"

0 comments on commit 471a018

Please sign in to comment.