From a2b9c8719730a4f23960c9546aabd13ef6f13495 Mon Sep 17 00:00:00 2001 From: Dmirty Simonov Date: Tue, 23 Nov 2021 18:31:18 +0300 Subject: [PATCH] some refactoring, 1.4 prepare, task context overload hook --- Makefile | 18 +++++- README.md | 6 +- carnival/__init__.py | 4 +- carnival/cli.py | 6 +- carnival/context.py | 40 ++++++++------ carnival/host.py | 97 ++++++--------------------------- carnival/step.py | 16 ++++-- carnival/task.py | 66 +++++++++++++--------- carnival/templates.py | 6 ++ carnival_tasks_example.py | 37 +++++++++++++ docs/source/hosts.rst | 9 +-- docs/source/steps.rst | 4 +- docs/source/tasks.rst | 14 ++--- mypy.ini | 5 +- poetry.lock | 58 +++++++++++++++++++- pyproject.toml | 3 +- tests/conftest.py | 12 ++-- tests/test_cmd/test_transfer.py | 2 +- tests/test_global_context.py | 2 +- tests/test_internal_tasks.py | 2 +- tests/test_step.py | 12 ++-- tests/test_task.py | 39 ++++++------- tests/test_utils.py | 4 +- 23 files changed, 262 insertions(+), 200 deletions(-) create mode 100644 carnival_tasks_example.py diff --git a/Makefile b/Makefile index dd988bf..93415e1 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,50 @@ -.PHONY: dist clean qa test dev nodev todos install docs test_deps +.PHONY: all +all: test_deps qa docs test -clean: +.PHONY: clean +clean: nodev rm -rf carnival.egg-info dist build __pycache__ .mypy_cache .pytest_cache .coverage +.PHONY: test_deps test_deps: poetry install --no-root +.PHONY: qa qa: poetry run flake8 . poetry run mypy . +.PHONY: test test: qa docs test_deps poetry run python3 -m pytest -x --cov-report term --cov=carnival -vv tests/ +.PHONY: test_fast test_fast: poetry run python3 -m pytest -x --cov-report term --cov=carnival -vv -m "not slow" tests/ +.PHONY: test_local test_local: poetry run python3 -m pytest -x --cov-report term --cov=carnival -vv -m "not remote" tests/ +.PHONY: dev dev: docker-compose -f testdata/docker-compose.yml up --build -d --remove-orphans --force-recreate +.PHONY: nodev nodev: docker-compose -f testdata/docker-compose.yml rm -sf + ssh-keygen -R [127.0.0.1]:22222 + ssh-keygen -R [127.0.0.1]:22223 +.PHONY: todos todos: grep -r TODO carnival +.PHONY: docs docs: poetry run make -C docs html +.PHONY: dist dist: poetry publish --build git tag `poetry version -s` diff --git a/README.md b/README.md index 85c0cbf..a8268e7 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,12 @@ from carnival import Step, Host, Task, cmd class Deploy(Task): def run(self): self.step( - DeployFrontend(), - Host("1.2.3.5", ssh_user="root", can="context", additional="give"), + [DeployFrontend(), ], + [Host("1.2.3.5", ssh_user="root", can="context", additional="give"), ], ) self.step( - DeployFrontend(), + [DeployFrontend(), ], [ Host("root@1.2.3.6", can="give", additional="context"), Host("root@1.2.3.7", can="context", additional="give"), diff --git a/carnival/__init__.py b/carnival/__init__.py index ee5899c..676c7b9 100644 --- a/carnival/__init__.py +++ b/carnival/__init__.py @@ -1,7 +1,7 @@ import sys from carnival.step import Step -from carnival.host import Host, SSHHost, LocalHost +from carnival.host import SSHHost, LocalHost from carnival.task import Task, SimpleTask from carnival import cmd from carnival import internal_tasks @@ -19,7 +19,7 @@ __all__ = [ 'Step', - 'Host', 'SSHHost', 'LocalHost', + 'SSHHost', 'LocalHost', 'Task', 'SimpleTask', 'cmd', 'log', diff --git a/carnival/cli.py b/carnival/cli.py index c5b0246..4041cbd 100644 --- a/carnival/cli.py +++ b/carnival/cli.py @@ -35,7 +35,6 @@ def main() -> int: >>> $ poetry run python -m carnival --help >>> Usage: python -m carnival [OPTIONS] {help|test}... >>> Options: - >>> -d, --dry_run Simulate run >>> --debug Turn on debug mode >>> --help Show this message and exit. """ @@ -48,17 +47,16 @@ def main() -> int: ) @click.command() - @click.option('-d', '--dry_run', is_flag=True, default=False, help="Simulate run") @click.option('--debug', is_flag=True, default=False, help="Turn on debug mode") @click.argument('tasks', required=True, type=click.Choice(list(task_types.keys())), nargs=-1) - def cli(dry_run: bool, debug: bool, tasks: Iterable[str]) -> None: + def cli(debug: bool, tasks: Iterable[str]) -> None: if debug is True: print("Debug mode on.") else: sys.excepthook = except_hook for task in tasks: - task_types[task](dry_run=dry_run).run() + task_types[task]().run() cli(complete_var=complete_var) return 0 diff --git a/carnival/context.py b/carnival/context.py index c2452b3..c56bb8b 100644 --- a/carnival/context.py +++ b/carnival/context.py @@ -1,11 +1,10 @@ import inspect import os -from itertools import chain -from typing import TYPE_CHECKING, Any, Callable, Dict, List +from typing import Any, Callable, Dict, List -if TYPE_CHECKING: - from carnival.host import AnyHost - from carnival.step import Step + +class ContextBuilderError(BaseException): + pass class PassAllArgs(BaseException): @@ -40,14 +39,18 @@ def build_kwargs(fn: Callable[..., Any], context: Dict[str, Any]) -> Dict[str, A return context.copy() kwargs = {} - for context_name, context_val in context.items(): - if context_name in arg_names: - kwargs[context_name] = context_val + for arg_name in arg_names: + # TODO: check if variable has default value + # if arg_name not in context: + # raise ContextBuilderError("Required context var '{arg_name}' is not present in context.") + + if arg_name in context: + kwargs[arg_name] = context[arg_name] return kwargs -def build_context(step: 'Step', host: 'AnyHost') -> Dict[str, Any]: - run_context: Dict[str, Any] = {'host': host} +def build_context(*context_chain: Dict[str, Any]) -> Dict[str, Any]: + run_context: Dict[str, Any] = {} env_prefix = "CARNIVAL_CTX_" # Build context from environment variables @@ -55,14 +58,15 @@ def build_context(step: 'Step', host: 'AnyHost') -> Dict[str, Any]: if env_name.startswith(env_prefix): run_context[env_name[len(env_prefix):]] = env_val - for var_name, var_val in chain(host.context.items(), step.context.items()): - if isinstance(var_val, context_ref): - try: - run_context[var_name] = run_context[var_val.context_var_name] - except KeyError as e: - raise KeyError(f"There is no '{var_val.context_var_name}' variable in context") from e - else: - run_context[var_name] = var_val + for context_item in context_chain: + for var_name, var_val in context_item.items(): + if isinstance(var_val, context_ref): + try: + run_context[var_name] = run_context[var_val.context_var_name] + except KeyError as e: + raise ContextBuilderError(f"There is no '{var_val.context_var_name}' variable in context") from e + else: + run_context[var_name] = var_val return run_context diff --git a/carnival/host.py b/carnival/host.py index df3a00a..70da63d 100644 --- a/carnival/host.py +++ b/carnival/host.py @@ -8,7 +8,10 @@ >>> class SetupFrontend(Task): >>> def run(self, **kwargs): ->>> self.step(Frontend(), SSHHost("1.2.3.4", packages=["htop", ])) +>>> self.step( +>>> [Frontend(), ], +>>> [SSHHost("1.2.3.4", packages=["htop", ]), ], +>>> ) В более сложных, создать списки в файле `inventory.py` @@ -22,28 +25,17 @@ >>> import inventory as i >>> class SetupFrontend(Task): >>> def run(self, **kwargs): ->>> self.step(Frontend(), i.frontends) +>>> self.step([Frontend(), ], i.frontends) """ -from typing import Any, Optional, Union -import warnings +import typing from fabric.connection import Connection as SSHConnection # type: ignore from invoke.context import Context as LocalConnection # type: ignore -from paramiko.client import MissingHostKeyPolicy, AutoAddPolicy # type: ignore +from paramiko.client import MissingHostKeyPolicy, AutoAddPolicy -AnyConnection = Union[SSHConnection, LocalConnection] - -""" -Список адресов которые трактуются как локальное соединение -.. deprecated:: 1.4 - Host is deprecated, use LocalHost or SSHHost explicitly -""" -LOCAL_ADDRS = [ - 'local', - 'localhost', -] +AnyConnection = typing.Union[SSHConnection, LocalConnection] class LocalHost: @@ -51,9 +43,10 @@ class LocalHost: Локальный хост, работает по локальному терминалу """ - def __init__(self, **context: Any) -> None: + def __init__(self, **context: typing.Any) -> None: self.addr = "local" self.context = context + self.context['host'] = self def connect(self) -> LocalConnection: return LocalConnection() @@ -89,12 +82,12 @@ class SSHHost(LocalHost): def __init__( self, addr: str, - ssh_user: Optional[str] = None, ssh_password: Optional[str] = None, ssh_port: int = 22, - ssh_gateway: Optional['SSHHost'] = None, + ssh_user: typing.Optional[str] = None, ssh_password: typing.Optional[str] = None, ssh_port: int = 22, + ssh_gateway: typing.Optional['SSHHost'] = None, ssh_connect_timeout: int = 10, - missing_host_key_policy: MissingHostKeyPolicy = AutoAddPolicy, + missing_host_key_policy: typing.Type[MissingHostKeyPolicy] = AutoAddPolicy, - **context: Any + **context: typing.Any ): """ :param addr: Адрес сервера @@ -108,26 +101,16 @@ def __init__( if "@" in addr: raise ValueError("Please set user in 'ssh_user' arg") + super().__init__(**context) + self.addr = addr self.ssh_port = ssh_port - self.context = context self.ssh_user = ssh_user self.ssh_password = ssh_password self.ssh_connect_timeout = ssh_connect_timeout - self.ssh_gateway: Optional['SSHHost'] = ssh_gateway + self.ssh_gateway: typing.Optional['SSHHost'] = ssh_gateway self.missing_host_key_policy = missing_host_key_policy - def is_connection_local(self) -> bool: - """ - Check if host's connection is local - """ - warnings.warn( - "is_connection_local is deprecated, use LocalHost or SSHHost explicitly", - DeprecationWarning, - stacklevel=2, - ) - return self.host.lower() in LOCAL_ADDRS - def connect(self) -> SSHConnection: gateway = None if self.ssh_gateway: @@ -148,48 +131,4 @@ def connect(self) -> SSHConnection: return conn -AnyHost = Union[LocalHost, SSHHost] - - -class Host(SSHHost): - """ - :param addr: Адрес сервера для SSH или "local" для локального соединения - :param ssh_user: Пользователь SSH - :param ssh_password: Пароль SSH - :param ssh_port: SSH порт - :param ssh_connect_timeout: SSH таймаут соединения - :param ssh_gateway: Gateway - :param context: Контекст хоста - - .. deprecated:: 1.4 - Host is deprecated, use LocalHost or SSHHost explicitly - """ - def __init__( - self, - addr: str, - ssh_user: Optional[str] = None, ssh_password: Optional[str] = None, ssh_port: int = 22, - ssh_gateway: Optional['SSHHost'] = None, ssh_connect_timeout: int = 10, - missing_host_key_policy: MissingHostKeyPolicy = AutoAddPolicy, - **context: Any - ): - warnings.warn( - "Host is deprecated, use LocalHost or SSHHost explicitly", - DeprecationWarning, - stacklevel=2, - ) - - if "@" in addr: - ssh_user, addr = addr.split("@", maxsplit=1) - - super().__init__( - addr, - ssh_user=ssh_user, ssh_password=ssh_password, ssh_port=ssh_port, - ssh_gateway=ssh_gateway, ssh_connect_timeout=ssh_connect_timeout, - missing_host_key_policy=missing_host_key_policy, **context - ) - - def connect(self) -> AnyConnection: - if self.addr in LOCAL_ADDRS: - return LocalConnection() - - return super().connect() +AnyHost = typing.Union[LocalHost, SSHHost] diff --git a/carnival/step.py b/carnival/step.py index a9755db..c39ab98 100644 --- a/carnival/step.py +++ b/carnival/step.py @@ -1,8 +1,7 @@ import abc -from typing import Any, no_type_check +import typing from carnival.context import build_context, build_kwargs -from carnival.host import AnyHost class Step: @@ -28,19 +27,24 @@ class Step: >>> ... """ - def __init__(self, **context: Any): + def __init__(self, **context: typing.Any): """ :param context: Переменные контекста, назначенные при вызове Шага """ self.context = context - def run_with_context(self, host: AnyHost) -> Any: - context = build_context(self, host) + def run_with_context(self, host_ctx: typing.Dict[str, typing.Any]) -> typing.Any: + """ + Выполнить шаг + + :param host_ctx: конекст хоста, (`AnyHost.context`) + """ + context = build_context(host_ctx, self.context) kwargs = build_kwargs(self.run, context) return self.run(**kwargs) # type: ignore @abc.abstractmethod - @no_type_check + @typing.no_type_check def run(self, **kwargs) -> None: """ Метод который нужно определить для выполнения комманд diff --git a/carnival/task.py b/carnival/task.py index 89a4480..b96c9e1 100644 --- a/carnival/task.py +++ b/carnival/task.py @@ -1,7 +1,8 @@ import abc import re from dataclasses import dataclass -from typing import Any, List, Optional, Type, Union +import copy +import typing from carnival import Step, global_context from carnival.host import AnyHost @@ -21,8 +22,17 @@ class TaskResult: Возвращается вызовом метода Task.step """ host: AnyHost + """ + Хост на котором выполнялся шаг + """ step: Step - result: Any + """ + Шаг + """ + result: typing.Any + """ + Результат выполения шага + """ class Task: @@ -41,35 +51,34 @@ class Task: # Имя задачи name: str = "" - module_name: Optional[str] = None + module_name: typing.Optional[str] = None help: str = "" @classmethod def get_name(cls) -> str: return cls.name if cls.name else _underscore(cls.__name__) - def __init__(self, dry_run: bool): - self.dry_run = dry_run - - def call_task(self, task_class: Type['Task']) -> Any: + def call_task(self, task_class: typing.Type['Task']) -> typing.Any: """ Запустить другую задачу Возвращает результат работы задачи """ - return task_class(dry_run=self.dry_run).run() + return task_class().run() + + def extend_host_context(self, host: AnyHost) -> typing.Dict[str, typing.Any]: + """ + Метод для переопределения контекста хоста, вызываемый методом `.step` по умолчанию контекст не переопределяется - def step(self, steps: Union[Step, List[Step]], hosts: Union[AnyHost, List[AnyHost]]) -> List[TaskResult]: + :param host: хост на котором готовится запуск + """ + return copy.deepcopy(host.context) + + def step(self, steps: typing.List[Step], hosts: typing.List[AnyHost]) -> typing.List[TaskResult]: """ Запустить шаг(и) на хост(ах) Возвращает объект TaskResult для получения результатов работы каждого шага на каждом хосте """ - if not isinstance(steps, list) and not isinstance(steps, tuple): - steps = [steps, ] - - if not isinstance(hosts, list) and not isinstance(hosts, tuple): - hosts = [hosts, ] - results = [] for host in hosts: @@ -77,17 +86,16 @@ def step(self, steps: Union[Step, List[Step]], hosts: Union[AnyHost, List[AnyHos for step in steps: step_name = _underscore(step.__class__.__name__) print(f"💃💃💃 Running {self.get_name()}:{step_name} at {host}") - if not self.dry_run: - r = TaskResult( - host=host, - step=step, - result=step.run_with_context(host=host), - ) - results.append(r) + r = TaskResult( + host=host, + step=step, + result=step.run_with_context(self.extend_host_context(host=host)), + ) + results.append(r) return results @abc.abstractmethod - def run(self) -> Any: + def run(self) -> typing.Any: """ Реализация выполнения задачи """ @@ -96,11 +104,17 @@ def run(self) -> Any: class SimpleTask(abc.ABC, Task): """ - Запустить шаги `self.steps` на хостах `self.hosts` + Запустить шаги `steps` на хостах `hosts` """ - hosts: List[AnyHost] - steps: List[Step] + hosts: typing.List[AnyHost] + """ + Список хостов + """ + steps: typing.List[Step] + """ + Список шагов в порядке выполнения + """ def run(self) -> None: self.step( diff --git a/carnival/templates.py b/carnival/templates.py index 3e58d1a..b75d5ec 100644 --- a/carnival/templates.py +++ b/carnival/templates.py @@ -17,6 +17,12 @@ def render(template_path: str, **context: Any) -> str: + """ + Отрендерить jinja2-шаблон в строку + + :param template_path: относительный путь до шаблона, ищется в текущей папке проекта и в папках плагинов + :param context: контекст шаблона + """ template = j2_env.get_template(template_path) return template.render( conn=global_context.conn, diff --git a/carnival_tasks_example.py b/carnival_tasks_example.py new file mode 100644 index 0000000..6844b6e --- /dev/null +++ b/carnival_tasks_example.py @@ -0,0 +1,37 @@ +""" +Carnival tasks example file +Run: +TESTSERVER_ADDR=test_server_addr CARNIVAL_TASKS_MODULE=carnival_tasks_example poetry run carnival help +""" +import typing + +import os +from carnival import cmd +from carnival.task import Task, SimpleTask +from carnival.host import SSHHost +from carnival.step import Step +from carnival import global_context + + +my_server_ip = os.getenv("TESTSERVER_ADDR", "1.2.3.4") +my_server = SSHHost(my_server_ip, ssh_user="root", packages=['htop', "mc"]) + + +class CheckDiskSpace(Task): + help = "Print server root disk usage" + + def run(self) -> None: + with global_context.SetContext(my_server): + cmd.cli.run("df -h /", hide=False) + + +class InstallStep(Step): + def run(self, packages: typing.List[str]) -> None: + cmd.apt.install_multiple(*packages) + + +class InstallPackages(SimpleTask): + help = "Install packages" + + hosts = [my_server] + steps = [InstallStep()] diff --git a/docs/source/hosts.rst b/docs/source/hosts.rst index 414168c..0f056b6 100644 --- a/docs/source/hosts.rst +++ b/docs/source/hosts.rst @@ -4,13 +4,8 @@ .. automodule:: carnival.host -.. autoattribute:: carnival.host.LOCAL_ADDRS - -.. autoclass:: carnival.host.Host - :special-members: __init__ - -.. autoclass:: carnival.host.SSHHost +.. autoclass:: carnival.host.SSHHost() :special-members: __init__ -.. autoclass:: carnival.host.LocalHost +.. autoclass:: carnival.host.LocalHost() :special-members: __init__ diff --git a/docs/source/steps.rst b/docs/source/steps.rst index 76ddc47..155cbf5 100644 --- a/docs/source/steps.rst +++ b/docs/source/steps.rst @@ -3,9 +3,9 @@ ################### -.. autoclass:: carnival.Step +.. autoclass:: carnival.Step() :special-members: __init__ - :members: run + :members: Работа с контекстом diff --git a/docs/source/tasks.rst b/docs/source/tasks.rst index ee0ffe1..c3a93d1 100644 --- a/docs/source/tasks.rst +++ b/docs/source/tasks.rst @@ -2,15 +2,14 @@ Задача (Task) ################### -.. autoclass:: carnival.Task - :members: run +.. autoclass:: carnival.Task() + :members: Простые задачи ================ -.. autoclass:: carnival.SimpleTask - :undoc-members: hosts, steps - :exclude-members: run +.. autoclass:: carnival.SimpleTask() + :members: hosts, steps Встроенные задачи @@ -19,12 +18,11 @@ carnival имеет встроенные задачи для удобства использования .. automodule:: carnival.internal_tasks - :undoc-members: :members: + :exclude-members: run Результат выполнения Task.step ================================= -.. autoclass:: carnival.task.TaskResult - :undoc-members: +.. autoclass:: carnival.task.TaskResult() :members: diff --git a/mypy.ini b/mypy.ini index 66394ed..bba0668 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,4 +3,7 @@ warn_unused_ignores = True strict = True [mypy-tests/*] -ignore_errors = True +warn_unused_ignores = True +strict = False +disallow_untyped_defs = False +disallow_untyped_calls = False diff --git a/poetry.lock b/poetry.lock index d392c26..803bac5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -611,6 +611,45 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "types-cryptography" +version = "3.3.9" +description = "Typing stubs for cryptography" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +types-enum34 = "*" +types-ipaddress = "*" + +[[package]] +name = "types-enum34" +version = "1.1.1" +description = "Typing stubs for enum34" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-ipaddress" +version = "1.0.1" +description = "Typing stubs for ipaddress" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-paramiko" +version = "2.8.1" +description = "Typing stubs for paramiko" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +types-cryptography = "*" + [[package]] name = "typing-extensions" version = "4.0.0" @@ -635,7 +674,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "0bd692935054058ef39d395c7f82379a69f70ea8d85086138ee6b8388467ccc8" +content-hash = "852e5189887c64423410a97cf36fbbce2b9ad0679f07e453810e05949848499f" [metadata.files] alabaster = [ @@ -1071,8 +1110,25 @@ tomli = [ {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, ] +types-cryptography = [ + {file = "types-cryptography-3.3.9.tar.gz", hash = "sha256:39b8ce64fe89a7e31ea49030ee19ad81724c38ca7283e2c1f91b2ecfeea8b102"}, + {file = "types_cryptography-3.3.9-py3-none-any.whl", hash = "sha256:2127dda3016ba9a6e518a76dec46a009a7da112ace79fc6b590fd028fa24db6b"}, +] +types-enum34 = [ + {file = "types-enum34-1.1.1.tar.gz", hash = "sha256:55c44c44f193636ed82f1cb68a9a632e1ea7096724f024c25e015976809df339"}, + {file = "types_enum34-1.1.1-py3-none-any.whl", hash = "sha256:b6d55d7c91867bd2fd6fc90651d91629c98b27cb26df11b1db658ae7a72bb768"}, +] +types-ipaddress = [ + {file = "types-ipaddress-1.0.1.tar.gz", hash = "sha256:dc5540c7fd8d4b3ffe8461bc01f27513c0abe5f2088e491218bd0a98a0b4584e"}, + {file = "types_ipaddress-1.0.1-py3-none-any.whl", hash = "sha256:9d0a642526c4a1f87ee9ae29d91b93f708508b891d038db2e1c9f06219383516"}, +] +types-paramiko = [ + {file = "types-paramiko-2.8.1.tar.gz", hash = "sha256:93b51bd729ac8f9e2bc11b4bdfee190b53cca45268084687737c03c697bd0142"}, + {file = "types_paramiko-2.8.1-py3-none-any.whl", hash = "sha256:7d7b8ca317019ae746ee8886a691a76e5974783f4ec9b6868962ef91535eddc3"}, +] typing-extensions = [ {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, + {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"}, ] urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, diff --git a/pyproject.toml b/pyproject.toml index 534a272..ca645aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "carnival" -version = "1.3.8" +version = "1.4.0" description = "Fabric-based software provisioning tool" authors = ["Dmirty Simonov "] license = "MIT" @@ -22,6 +22,7 @@ pytest-mock = "^3.6.1" flake8 = "^4.0.1" mypy = "^0.910" Sphinx = "^4.3.0" +types-paramiko = "^2.8.1" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/conftest.py b/tests/conftest.py index 1754991..d3c7692 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ from typing import Type import pytest -from carnival import Host, Step +from carnival import Step, SSHHost, LocalHost from paramiko.client import WarningPolicy @@ -19,12 +19,12 @@ def run(self): @pytest.fixture(scope="function") -def noop_step(noop_step_class) -> Step: +def noop_step(noop_step_class: Type[Step]) -> Step: return noop_step_class() @pytest.fixture(scope="function") -def noop_step_context(noop_step_class) -> Step: +def noop_step_context(noop_step_class: Type[Step]) -> Step: return noop_step_class(additional="context") @@ -48,12 +48,12 @@ def __exit__(self, _1, _2, _3): @pytest.fixture(scope="function") def local_host(): - return Host("local") + return LocalHost() @pytest.fixture(scope="function") def ubuntu_ssh_host(): - return Host( + return SSHHost( "127.0.0.1", ssh_user="root", ssh_password="secret", ssh_port=22222, missing_host_key_policy=WarningPolicy @@ -62,7 +62,7 @@ def ubuntu_ssh_host(): @pytest.fixture(scope="function") def centos_ssh_host(): - return Host( + return SSHHost( "127.0.0.1", ssh_user="root", ssh_password="secret", ssh_port=22223, missing_host_key_policy=WarningPolicy diff --git a/tests/test_cmd/test_transfer.py b/tests/test_cmd/test_transfer.py index 8e5db13..16312f6 100644 --- a/tests/test_cmd/test_transfer.py +++ b/tests/test_cmd/test_transfer.py @@ -25,6 +25,6 @@ def test_rsync(suspend_capture, ubuntu_ssh_host, centos_ssh_host): with global_context.SetContext(host): cmd.system.ssh_copy_id() assert cmd.fs.is_dir_exists("/docs") is False - cmd.transfer.rsync("./docs", "/") + cmd.transfer.rsync("./docs", "/", strict_host_keys=False) assert cmd.fs.is_dir_exists("/docs") is True cmd.cli.run("rm -rf /docs") diff --git a/tests/test_global_context.py b/tests/test_global_context.py index e4197e7..29173cb 100644 --- a/tests/test_global_context.py +++ b/tests/test_global_context.py @@ -1,4 +1,4 @@ -from invoke import Context +from invoke import Context # type: ignore from carnival import global_context from carnival.host import LocalHost diff --git a/tests/test_internal_tasks.py b/tests/test_internal_tasks.py index 4e96057..2144e52 100644 --- a/tests/test_internal_tasks.py +++ b/tests/test_internal_tasks.py @@ -2,4 +2,4 @@ def test_help(capsys): - internal_tasks.Help(False).run() + internal_tasks.Help().run() diff --git a/tests/test_step.py b/tests/test_step.py index fbf2b2b..8b88497 100644 --- a/tests/test_step.py +++ b/tests/test_step.py @@ -1,12 +1,12 @@ import pytest -from carnival import Step, Host +from carnival import Step, LocalHost def test_step_abc(mocker): spy = mocker.spy(Step, 'run') with pytest.raises(NotImplementedError): - Step(another="context1").run_with_context(Host("local", add="context")) + Step(another="context1").run_with_context(LocalHost(add="context").context) # type: ignore spy.assert_called_once() @@ -19,23 +19,23 @@ def run(self, another=None): # Waiting step context noop_step = NoopStep(another="context1") spy = mocker.spy(noop_step, 'run') - noop_step.run_with_context(Host("local", add="context")) + noop_step.run_with_context(LocalHost(add="context").context) spy.assert_called_once_with(another='context1') # Waiting no context noop_step = NoopStep() spy = mocker.spy(noop_step, 'run') - noop_step.run_with_context(Host("local", add="context")) + noop_step.run_with_context(LocalHost(add="context").context) spy.assert_called_once_with() # Waiting host context noop_step = NoopStep() spy = mocker.spy(noop_step, 'run') - noop_step.run_with_context(Host("local", another="host_context")) + noop_step.run_with_context(LocalHost(another="host_context").context) spy.assert_called_once_with(another="host_context") # Waiting step and host context, step context overloading step context noop_step = NoopStep(another="context1") spy = mocker.spy(noop_step, 'run') - noop_step.run_with_context(Host("local", another="host_context")) + noop_step.run_with_context(LocalHost(another="host_context").context) spy.assert_called_once_with(another="context1") diff --git a/tests/test_task.py b/tests/test_task.py index 8d72d5f..6d00865 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -1,6 +1,6 @@ import pytest -from carnival import Host +from carnival import LocalHost from carnival.task import _underscore, Task, SimpleTask @@ -9,42 +9,35 @@ def test_underscore(): def test_task_name(): + t: Task + class DryTask(Task): - pass + def run(self) -> None: + pass - t = DryTask(True) + t = DryTask() assert t.get_name() == "dry_task" class DryNameTask(Task): name = "nametask" - t = DryNameTask(True) - assert t.get_name() == "nametask" - - -def test_task_dry_run(noop_step, mocker): - spy = mocker.spy(noop_step, 'run') - - class DryTask(Task): - def run(self): - self.step(noop_step, Host("local")) - t = DryTask(True) + def run(self) -> None: + pass - t.run() - spy.assert_not_called() + t = DryNameTask() + assert t.get_name() == "nametask" def test_task(noop_step, mocker): spy = mocker.spy(noop_step, 'run') with pytest.raises(NotImplementedError): - Task(False).run() + Task().run() # type: ignore class DryTask(Task): def run(self): - self.step(noop_step, Host("local")) - self.step([noop_step, ], [Host("local"), ]) - t = DryTask(False) + self.step([noop_step, ], [LocalHost(), ]) + t = DryTask() t.run() spy.assert_called() @@ -54,12 +47,12 @@ def test_simple_task(noop_step, mocker): spy = mocker.spy(noop_step, 'run') with pytest.raises(NotImplementedError): - Task(False).run() + Task().run() # type: ignore class DryTask(SimpleTask): - hosts = [Host("local"), ] + hosts = [LocalHost(), ] steps = [noop_step, ] - t = DryTask(False) + t = DryTask() t.run() spy.assert_called() diff --git a/tests/test_utils.py b/tests/test_utils.py index adf8223..29c3ca6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,8 +1,8 @@ -from carnival import utils, Host, global_context +from carnival import utils, LocalHost, global_context def test_log(capsys): - with global_context.SetContext(Host("local")): + with global_context.SetContext(LocalHost()): utils.log("Hellotest") captured = capsys.readouterr()