Skip to content

Commit

Permalink
roles
Browse files Browse the repository at this point in the history
  • Loading branch information
a1fred committed Dec 2, 2021
1 parent 668f5a9 commit 25fbaf8
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 150 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ contrib
.ideaDataSources
.env
.pytest_cache
.python-version
4 changes: 2 additions & 2 deletions carnival/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
from carnival.step import Step, InlineStep
from carnival.hosts.base import Host, Connection, Result
from carnival.role import Role, SingleHostRole
from carnival.role import Role, SingleRole
from carnival.hosts.local import LocalHost, localhost_connection
from carnival.hosts.ssh import SshHost
from carnival.task import TaskBase, Task
Expand Down Expand Up @@ -39,7 +39,7 @@
__all__ = [
'Step', 'InlineStep',
'SshHost', 'LocalHost', 'localhost_connection', 'Host', 'Connection', 'Result',
'Role', 'SingleHostRole',
'Role', 'SingleRole',
'TaskBase', 'Task',
'cmd',
'log',
Expand Down
18 changes: 16 additions & 2 deletions carnival/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,22 @@ def cli(debug: bool, no_validate: bool, tasks: typing.Iterable[str]) -> None:
if no_validate:
print("Step validation disabled")

for task in tasks:
task_types[task](no_validate=no_validate).run()
# Build chain and validate
task_chain: typing.List[TaskBase] = []
for task_class_str in tasks:
task = task_types[task_class_str](no_validate=no_validate)
if not task.no_validate:
errors = task.validate()
if errors:
print(f"There is validation errors for task {task_class_str}")
for e in errors:
print(f" * {e}")
return
task_chain.append(task)

# Run
for task in task_chain:
task.run()

cli(complete_var=complete_var)
return 0
30 changes: 22 additions & 8 deletions carnival/hosts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
"""

import typing
import ipaddress
import socket
import abc
from dataclasses import dataclass
from carnival.role import RoleBase, role_repository
from invoke.context import Result as InvokeResult # type: ignore


Expand Down Expand Up @@ -86,20 +87,33 @@ class Host:

def __init__(
self,
roles: typing.List[typing.Type[RoleBase]] = [],
context: typing.Optional[typing.Dict[str, typing.Any]] = None,
) -> None:
"""
:param context: Контекст хоста
"""

self.addr = ""
self.context = context or {}

self.roles = roles
role_repository.add(host=self, roles=roles)

self.context['host'] = self
@property
def ip(self) -> str:
# Maybe self.addr is ip?
try:
ip_obj: typing.Union[ipaddress.IPv4Address, ipaddress.IPv6Address] = ipaddress.ip_address(self.addr)
return ip_obj.__str__()
except ValueError:
# Not ip
pass

# Maybe hostname?
try:
return socket.gethostbyname(self.addr)
except socket.gaierror:
# No ;(
pass

# TODO: maybe addr is ~/.ssh/config section?

raise ValueError("cant get host ip")

@abc.abstractmethod
def connect(self) -> Connection:
Expand Down
5 changes: 1 addition & 4 deletions carnival/hosts/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from invoke.context import Context # type: ignore

from carnival.hosts import base
from carnival.role import RoleBase


class LocalConnection(base.Connection):
Expand Down Expand Up @@ -53,13 +52,11 @@ class LocalHost(base.Host):

def __init__(
self,
roles: typing.List[typing.Type[RoleBase]] = [],
context: typing.Optional[typing.Dict[str, typing.Any]] = None,
) -> None:
"""
:param context: Контекст хоста
"""
super().__init__(roles=roles, context=context)
super().__init__()
self.addr = "local"

def connect(self) -> LocalConnection:
Expand Down
6 changes: 1 addition & 5 deletions carnival/hosts/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from fabric.connection import Connection as FabricConnection # type: ignore

from carnival.hosts import base
from carnival.role import RoleBase


class SshConnection(base.Connection):
Expand Down Expand Up @@ -86,9 +85,6 @@ def __init__(
ssh_gateway: typing.Optional['SshHost'] = None,
ssh_connect_timeout: int = 10,
missing_host_key_policy: typing.Type[MissingHostKeyPolicy] = AutoAddPolicy,

roles: typing.List[typing.Type[RoleBase]] = [],
context: typing.Optional[typing.Dict[str, typing.Any]] = None,
):
"""
:param addr: Адрес сервера
Expand All @@ -104,7 +100,7 @@ def __init__(
if "@" in addr:
raise ValueError("Please set user in 'ssh_user' arg")

super().__init__(roles=roles, context=context)
super().__init__()

self.addr = addr
self.ssh_port = ssh_port
Expand Down
2 changes: 1 addition & 1 deletion carnival/internal_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ def run(self) -> None:
from carnival.role import role_repository

for role, hosts in role_repository.items():
print(f"{get_class_full_name(role)}: {', '.join([x.addr for x in hosts])}")
print(f"{get_class_full_name(role)}: {', '.join([x.host.addr for x in hosts])}")
75 changes: 46 additions & 29 deletions carnival/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from carnival import Host


T = typing.TypeVar("T")
RoleBaseT = typing.TypeVar("RoleBaseT", bound="RoleBase")


Expand All @@ -14,6 +15,7 @@ class RoleBase:

def __init__(self, host: "Host") -> None:
self.host = host
role_repository.add(role=self)


class Role(RoleBase):
Expand All @@ -22,62 +24,77 @@ class Role(RoleBase):
"""

@classmethod
def get_hostroles(cls: typing.Type[RoleBaseT]) -> typing.List["RoleBaseT"]:
hostroles: typing.List[RoleBaseT] = []
def resolve(cls: typing.Type[RoleBaseT]) -> typing.List["RoleBaseT"]:
return role_repository.get(cls)

for host in role_repository.get(cls):
hostroles.append(cls(host))

return hostroles
@classmethod
def resolve_host(cls: typing.Type[RoleBaseT], host: "Host") -> RoleBaseT:
for role in role_repository.get(cls):
if role.host == host:
return role
raise ValueError("Role {} with host {} not registrered")


class SingleHostRole(Role):
class SingleRole(Role):
"""
Роль для одного хоста
"""

@classmethod
def get_hostrole(cls: typing.Type[RoleBaseT]) -> RoleBaseT:
def resolve_single(cls: typing.Type[RoleBaseT]) -> RoleBaseT:
from carnival.utils import get_class_full_name

hosts = role_repository.get(cls)
if len(hosts) != 1:
raise ValueError(f"Role {get_class_full_name(cls)} must be singe host, but {hosts} already binded")
hostroles = role_repository.get(cls)
if len(hostroles) != 1:
raise ValueError(f"Role {get_class_full_name(cls)} must be singe host, but {hostroles} already binded")

return cls(host=list(hosts)[0])
return hostroles[0]


class _RoleRepository:
def __init__(self) -> None:
self._rolehosts: typing.Dict[typing.Type[RoleBase], typing.Set["Host"]] = {}
self._rolehosts: typing.List["RoleBase"] = list()

def items(self) -> typing.Iterable[typing.Tuple[typing.Type[RoleBase], typing.Set["Host"]]]:
return self._rolehosts.items()
def items(self) -> typing.Iterable[typing.Tuple[typing.Type[RoleBase], typing.List[RoleBase]]]:
result: typing.Dict[typing.Type[RoleBase], typing.List[RoleBase]] = dict()
for role in self._rolehosts:
result.setdefault(role.__class__, list())
result[role.__class__].append(role)
return list(result.items())

def add(self, host: "Host", roles: typing.List[typing.Type[RoleBase]]) -> None:
def add(self, role: RoleBase) -> None:
from carnival.utils import get_class_full_name

global _rolehosts
for role in roles:
if isinstance(role, SingleHostRole):
hosts = self.get(role)
if hosts:
raise ValueError(
f"Cannot bind {host} to single host role {get_class_full_name(role)}, {hosts} already binded"
)
if isinstance(role, SingleRole):
hostroles = self.get(role.__class__)
if hostroles:
role_class_name = get_class_full_name(role.__class__)
raise ValueError(
f"Cannot bind {role} to single host role {role_class_name}, {hostroles} already binded"
)

self._rolehosts.setdefault(role, set())
self._rolehosts[role].add(host)
self._rolehosts.append(role)

def get(self, role: typing.Type[RoleBase]) -> typing.Set["Host"]:
return self._rolehosts.get(role, set())
def get(self, role_class: typing.Type[RoleBaseT]) -> typing.List[RoleBaseT]:
result: typing.List[RoleBaseT] = []
for role in self._rolehosts:
if isinstance(role, role_class):
result.append(role)
return result


role_repository = _RoleRepository()


# def role_ref(role_class: RoleBaseT, getter: typing.Callable[[RoleBaseT], T]) -> typing.List[T]:
# resuts: typing.List[T] = []
# for role in role_repository.get(role_class):
# resuts.append(getter(role))
# return resuts


__all__ = (
'Role',
'SingleHostRole',
'SingleRole',
'role_repository',
)
2 changes: 1 addition & 1 deletion carnival/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def __init__(self, no_validate: bool) -> None:
super().__init__(no_validate=no_validate)
# Get role from generic
self.role_class: typing.Type[RoleT] = typing.get_args(self.__class__.__orig_bases__[0])[0] # type: ignore
self.hostroles: typing.List[RoleT] = self.role_class.get_hostroles()
self.hostroles: typing.List[RoleT] = self.role_class.resolve()
if not self.hostroles:
print(f"[WARN]: not hosts for {self.role_class}", file=sys.stderr)

Expand Down
Loading

0 comments on commit 25fbaf8

Please sign in to comment.