Initial commit
This commit is contained in:
commit
e2421bc8aa
47 files changed
+1519
No files matched your search
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,19 @@
|
||||
from colony.core.network.nodes import LocalNode
|
||||
|
||||
|
||||
class RoleContext:
|
||||
def __init__(self, network, role_manager):
|
||||
self._network = network
|
||||
self._role_manager = role_manager
|
||||
|
||||
def execute(self, command, *args, **kwargs):
|
||||
return self._network.execute(command, *args, **kwargs)
|
||||
|
||||
def peers(self):
|
||||
return self._network.get_all_nodes()
|
||||
|
||||
def self(self) -> LocalNode:
|
||||
return self._network.get_self()
|
||||
|
||||
def set_self_node_roles(self, roles):
|
||||
self._role_manager.set_roles(roles)
|
||||
@@ -0,0 +1,34 @@
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
|
||||
from colony.core.logger.logging import get_logger
|
||||
|
||||
|
||||
class Dispatcher:
|
||||
def __init__(self, network, role_registry):
|
||||
self.logger = get_logger(self.__class__.__name__)
|
||||
|
||||
self.network = network
|
||||
self.role_registry = role_registry
|
||||
|
||||
def dispatch(self, payload):
|
||||
self.logger.debug(f"Dispatching payload for command {payload['command']['role']}.{payload["command"]['name']}")
|
||||
|
||||
c = payload["command"]
|
||||
role_name = c["role"]
|
||||
command_name = c["name"]
|
||||
args = tuple(c.get("args", []))
|
||||
kwargs = dict(c.get("kwargs", {}))
|
||||
|
||||
role_entry = self.role_registry.get_role_class_by_name(role_name)
|
||||
if role_entry is None:
|
||||
raise ValueError(f"Unknown role {role_name}")
|
||||
|
||||
commands_enum = role_entry.COMMANDS
|
||||
command = commands_enum(f"{role_name}.{command_name}")
|
||||
|
||||
node = self.network.get_self()
|
||||
|
||||
self.logger.info(f"Executing {role_name}.{command_name} on local node {node.id}")
|
||||
result = node.execute_local(command, *args, **kwargs)
|
||||
self.logger.debug(f"Passing the results from {role_name}.{command_name}")
|
||||
return result
|
||||
@@ -0,0 +1,5 @@
|
||||
class NetworkException(Exception):
|
||||
pass
|
||||
|
||||
class NoRoleException(Exception):
|
||||
pass
|
||||
Whitespace-only changes.
@@ -0,0 +1,25 @@
|
||||
import logging
|
||||
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
COLORS = {
|
||||
'DEBUG': '\033[37m',
|
||||
'INFO': '\033[36m',
|
||||
'WARNING': '\033[33m',
|
||||
'ERROR': '\033[31m',
|
||||
'CRITICAL': '\033[41m',
|
||||
}
|
||||
RESET = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
|
||||
def format(self, record):
|
||||
color = self.COLORS.get(record.levelname, self.RESET)
|
||||
message = super().format(record)
|
||||
|
||||
if record.levelname in ('WARNING', 'ERROR', 'CRITICAL'):
|
||||
message = f"{self.BOLD}{color}{message}{self.RESET}"
|
||||
else:
|
||||
message = f"{color}{message}{self.RESET}"
|
||||
|
||||
return message
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from colony.core.logger.colors import ColorFormatter
|
||||
|
||||
LOG_DIR = Path("logs")
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
_loggers = {}
|
||||
_logger_lock = threading.Lock()
|
||||
FIXED_NAME_WIDTH = 15
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
with _logger_lock:
|
||||
if name in _loggers:
|
||||
return _loggers[name]
|
||||
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.DEBUG)
|
||||
ch_formatter = ColorFormatter(
|
||||
f'[%(asctime)s] %(levelname)-7s %(name)-{FIXED_NAME_WIDTH}s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
ch.setFormatter(ch_formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
fh = logging.FileHandler(LOG_DIR / f"{name}.log", encoding='utf-8')
|
||||
fh.setLevel(logging.DEBUG)
|
||||
fh_formatter = logging.Formatter(
|
||||
f'[%(asctime)s] [%(threadName)s] %(levelname)-8s %(name)-{FIXED_NAME_WIDTH}s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
fh.setFormatter(fh_formatter)
|
||||
logger.addHandler(fh)
|
||||
|
||||
logger.propagate = False
|
||||
_loggers[name] = logger
|
||||
return logger
|
||||
Whitespace-only changes.
@@ -0,0 +1,95 @@
|
||||
import json
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from colony.core.network.nodes import LocalNode, Node
|
||||
from colony.core.network.requests import CommandRequest
|
||||
from colony.core.logger.logging import get_logger
|
||||
from colony.settings import configuration
|
||||
from colony.utils.network import get_local_ip, get_hostname
|
||||
|
||||
|
||||
class Network:
|
||||
def __init__(self):
|
||||
self.logger = get_logger(self.__class__.__name__)
|
||||
|
||||
self.nodes = []
|
||||
|
||||
self.self_node = LocalNode(
|
||||
hostname=get_hostname(),
|
||||
ip=get_local_ip(),
|
||||
roles=[],
|
||||
port=configuration.listener.port
|
||||
)
|
||||
self.logger.info(f"Self node created: {self.self_node.id}")
|
||||
|
||||
if not self.nodes:
|
||||
self.nodes.append(self.self_node)
|
||||
|
||||
# @staticmethod
|
||||
# def initialize():
|
||||
# network = Network()
|
||||
# network.logger.info("Network.initialize() called")
|
||||
#
|
||||
# network.self_node = LocalNode(
|
||||
# hostname=get_hostname(),
|
||||
# ip=get_local_ip(),
|
||||
# roles=[],
|
||||
# port=configuration.listener.port
|
||||
# )
|
||||
# network.logger.info(f"Self node created: {network.self_node.id}")
|
||||
#
|
||||
# if not network.nodes:
|
||||
# network.nodes.append(network.self_node)
|
||||
#
|
||||
# return network
|
||||
|
||||
def get_self(self):
|
||||
return self.self_node
|
||||
|
||||
def get_all_nodes(self):
|
||||
return self.nodes
|
||||
|
||||
def get_node_of_role(self, role_name: str) -> Optional[Node]:
|
||||
for node in self.nodes:
|
||||
for role in node.roles:
|
||||
if role == role_name:
|
||||
return node
|
||||
|
||||
self.logger.warning(f"No node found with role: {role_name}")
|
||||
return None
|
||||
|
||||
def execute(self, command: Enum, *args, **kwargs):
|
||||
request = CommandRequest.form(command, *args, **kwargs)
|
||||
self.logger.info(f"Executing command {request.id} on role {request.role}")
|
||||
|
||||
node = self.get_node_of_role(request.role)
|
||||
if node is None:
|
||||
raise ValueError(f"No node found with role {request.role}")
|
||||
|
||||
result = node.execute(command, *args, **kwargs)
|
||||
self.logger.debug(f"Command {request.id} executed")
|
||||
return result
|
||||
|
||||
|
||||
def load_network_from_snapshot(self, file_path: str = "storage/colony_snapshot.json"):
|
||||
self.logger.info(f"Loading network snapshot from {file_path}")
|
||||
nodes = []
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
snapshot = json.load(f)
|
||||
except FileNotFoundError:
|
||||
self.logger.warning(f"Snapshot file {file_path} not found. Starting with empty network")
|
||||
snapshot = {"nodes": []}
|
||||
|
||||
for node_data in snapshot.get("nodes", []):
|
||||
node = Node(
|
||||
hostname=node_data["hostname"],
|
||||
ip=node_data["ip"],
|
||||
roles=node_data.get("roles", []),
|
||||
port=node_data["port"]
|
||||
)
|
||||
self.logger.info(f"Loaded node from snapshot: {node.id}")
|
||||
nodes.append(node)
|
||||
|
||||
return nodes
|
||||
@@ -0,0 +1,100 @@
|
||||
import datetime
|
||||
import platform
|
||||
import json
|
||||
|
||||
import GPUtil
|
||||
import psutil
|
||||
|
||||
|
||||
class HardwareStatistics:
|
||||
def __init__(self):
|
||||
self.cpu = None
|
||||
self.mem = None
|
||||
self.gpu = None
|
||||
self.disk = None
|
||||
self.os_info = None
|
||||
self.boot_time = None
|
||||
|
||||
@staticmethod
|
||||
def collect():
|
||||
self = HardwareStatistics()
|
||||
self.cpu = self._get_cpu_stats()
|
||||
self.mem = self._get_memory_stats()
|
||||
self.gpu = self._get_gpu_stats()
|
||||
self.disk = self._get_disk_stats()
|
||||
self.os_info = self._get_os_info()
|
||||
self.boot_time = datetime.datetime.fromtimestamp(
|
||||
psutil.boot_time()
|
||||
).isoformat()
|
||||
return self
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"cpu": self.cpu,
|
||||
"memory": self.mem,
|
||||
"gpu": self.gpu,
|
||||
"disk": self.disk,
|
||||
"os": self.os_info,
|
||||
"boot_time": self.boot_time
|
||||
}
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self.to_dict(), indent=2)
|
||||
|
||||
def _get_cpu_stats(self):
|
||||
return {
|
||||
"cores": psutil.cpu_count(logical=False),
|
||||
"threads": psutil.cpu_count(logical=True),
|
||||
"usage_percent": psutil.cpu_percent(interval=1)
|
||||
}
|
||||
|
||||
def _get_memory_stats(self):
|
||||
mem = psutil.virtual_memory()
|
||||
return {
|
||||
"total": mem.total,
|
||||
"available": mem.available,
|
||||
"used": mem.used,
|
||||
"percent": mem.percent
|
||||
}
|
||||
|
||||
def _get_gpu_stats(self):
|
||||
gpus = GPUtil.getGPUs()
|
||||
if not gpus:
|
||||
return []
|
||||
return [
|
||||
{
|
||||
"id": gpu.id,
|
||||
"name": gpu.name,
|
||||
"load": gpu.load * 100,
|
||||
"memory_total": gpu.memoryTotal,
|
||||
"memory_used": gpu.memoryUsed,
|
||||
"memory_free": gpu.memoryFree,
|
||||
"temperature": gpu.temperature
|
||||
}
|
||||
for gpu in gpus
|
||||
]
|
||||
|
||||
def _get_disk_stats(self):
|
||||
disk_info = []
|
||||
for d in psutil.disk_partitions():
|
||||
usage = psutil.disk_usage(d.mountpoint)
|
||||
disk_info.append({
|
||||
"device": d.device,
|
||||
"mountpoint": d.mountpoint,
|
||||
"fstype": d.fstype,
|
||||
"total": usage.total,
|
||||
"used": usage.used,
|
||||
"free": usage.free,
|
||||
"percent": usage.percent
|
||||
})
|
||||
return disk_info
|
||||
|
||||
def _get_os_info(self):
|
||||
return {
|
||||
"system": platform.system(),
|
||||
"node": platform.node(),
|
||||
"release": platform.release(),
|
||||
"version": platform.version(),
|
||||
"machine": platform.machine(),
|
||||
"processor": platform.processor()
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from colony.core.network.hardware import HardwareStatistics
|
||||
from colony.core.network.requests import CommandRequest
|
||||
from colony.transport.whisperer import Whisperer
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, hostname: str, ip: str, roles: List[str], port: int):
|
||||
self.id = f"{hostname}@{ip}"
|
||||
self.hostname = hostname
|
||||
self.ip = ip
|
||||
self.roles = roles
|
||||
self.port = port
|
||||
|
||||
def execute(self, command: Enum, *args, **kwargs):
|
||||
request = CommandRequest.form(command, *args, **kwargs)
|
||||
|
||||
return Whisperer(self.ip, self.port).send({
|
||||
"command": {
|
||||
"role": request.role,
|
||||
"name": request.name,
|
||||
"args": request.args,
|
||||
"kwargs": request.kwargs
|
||||
}
|
||||
})
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"hostname": self.hostname,
|
||||
"ip": self.ip,
|
||||
"port": self.port,
|
||||
"roles": self.roles,
|
||||
"hardware": HardwareStatistics.collect().to_dict()
|
||||
}
|
||||
|
||||
|
||||
class LocalNode(Node):
|
||||
def __init__(self, hostname: str, ip: str, roles: List[str], port: int):
|
||||
super().__init__(hostname, ip, roles, port)
|
||||
|
||||
self.__roles = dict()
|
||||
|
||||
def get_role_instances(self):
|
||||
return self.__roles
|
||||
|
||||
def set_role_instances(self, roles):
|
||||
for role_name, role_instance in roles.items():
|
||||
self.__roles[role_name] = role_instance
|
||||
|
||||
def execute_local(self, command: Enum, *args, **kwargs):
|
||||
request = CommandRequest.form(command, *args, **kwargs)
|
||||
|
||||
role = self.__roles.get(request.role)
|
||||
if role is None:
|
||||
raise ValueError(f"Role {request.role} not bound to local node")
|
||||
|
||||
return role.execute_local(
|
||||
request.id,
|
||||
*request.args,
|
||||
**request.kwargs
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
import dataclasses
|
||||
from enum import Enum
|
||||
from typing import Any, Tuple, Dict, Optional
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CommandRequest:
|
||||
_id: Enum
|
||||
args: Optional[Tuple] = None
|
||||
kwargs: Optional[Dict[str, Any]] = None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id.value
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
try:
|
||||
return self.id.split('.')[1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def role(self):
|
||||
try:
|
||||
return self.id.split('.')[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def form(cls, id: Enum, *args: Any, **kwargs: Any):
|
||||
return cls(id, args, kwargs)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class StateManager:
|
||||
class Data(Enum):
|
||||
Roles = "roles"
|
||||
|
||||
STATE_FILE = Path("state.json")
|
||||
|
||||
def __init__(self):
|
||||
self._data = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
if self.STATE_FILE.exists():
|
||||
with open(self.STATE_FILE, "r", encoding="utf-8") as f:
|
||||
self._data = json.load(f)
|
||||
else:
|
||||
self._data = {}
|
||||
self._save()
|
||||
|
||||
for key in self.Data:
|
||||
self._data.setdefault(key.value, {})
|
||||
|
||||
def _save(self):
|
||||
with open(self.STATE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self._data, f, indent=4)
|
||||
|
||||
def get(self, key: Data) -> Any:
|
||||
return self._data[key.value]
|
||||
|
||||
def set(self, key: Data, value: Any):
|
||||
self._data[key.value] = value
|
||||
self._save()
|
||||
@@ -0,0 +1,60 @@
|
||||
import os
|
||||
import json
|
||||
from enum import Enum
|
||||
|
||||
STORAGE_DIR = "storage"
|
||||
|
||||
class Storage:
|
||||
def __init__(self, storage_dir):
|
||||
self.storage_dir = storage_dir
|
||||
|
||||
os.makedirs(self.storage_dir, exist_ok=True)
|
||||
|
||||
def get_path(self, filename):
|
||||
return os.path.join(self.storage_dir, filename)
|
||||
|
||||
def ls(self, subdir):
|
||||
path = os.path.join(self.storage_dir, subdir)
|
||||
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
|
||||
return os.listdir(path)
|
||||
|
||||
def save(self, filename, data):
|
||||
path = self.get_path(filename)
|
||||
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
return path
|
||||
|
||||
def open(self, filename):
|
||||
path = self.get_path(filename)
|
||||
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
return json.loads(f.read())
|
||||
|
||||
def exists(self, filename):
|
||||
return os.path.exists(self.get_path(filename))
|
||||
|
||||
def delete(self, filename):
|
||||
path = self.get_path(filename)
|
||||
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class DefaultStorage(Storage):
|
||||
def __init__(self):
|
||||
super().__init__(STORAGE_DIR)
|
||||
|
||||
|
||||
class DEFAULT_STORAGE_FILES(Enum):
|
||||
COLONY_SNAPSHOT = 'colony_snapshot.json'
|
||||
@@ -0,0 +1,50 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RolePriority(Enum):
|
||||
GENERAL = 0 # should be on each and every node
|
||||
CRITICAL = 1 # network can't work properly without it
|
||||
HIGH = 2 # network can manager without it but with some difficulty
|
||||
LOW = 3 # it's just nice to have
|
||||
|
||||
|
||||
class HardwareType(Enum):
|
||||
DISK_SPACE = 1
|
||||
CPU = 2
|
||||
GPU = 3
|
||||
RAM = 4
|
||||
LONGEVITY = 5
|
||||
|
||||
|
||||
class HardwarePriority:
|
||||
hardware: HardwareType
|
||||
priority: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class HardwarePreferences:
|
||||
priorities: dict[HardwareType, int]
|
||||
|
||||
def priority_of(self, hardware: HardwareType) -> int:
|
||||
return self.priorities.get(hardware, 0)
|
||||
|
||||
|
||||
class RoleStats:
|
||||
def __init__(self, cls, priority: RolePriority, hardware_preferences: HardwarePreferences = None):
|
||||
self.name = cls.__name__
|
||||
self.cls = cls
|
||||
self.priority = priority
|
||||
self.hardware_preferences = hardware_preferences
|
||||
|
||||
|
||||
class RoleRegistry:
|
||||
def __init__(self, role_list):
|
||||
roles = {}
|
||||
for role_stat in role_list:
|
||||
roles[role_stat.name] = role_stat
|
||||
|
||||
self.roles = roles
|
||||
|
||||
def get_role_class_by_name(self, name):
|
||||
return self.roles.get(name).cls
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,82 @@
|
||||
import datetime
|
||||
from enum import Enum
|
||||
|
||||
from colony.core.storage import DEFAULT_STORAGE_FILES
|
||||
from colony.roles.role import Role
|
||||
from colony.roles.unit.role import Unit
|
||||
from colony.roles.vaultwarden.role import VaultWarden
|
||||
from colony.transport.discover import Discover
|
||||
from colony.transport.whisperer import Whisperer
|
||||
|
||||
|
||||
class Explorer(Role):
|
||||
CONTINUOUS = True
|
||||
LOOP_SECONDS_WAIT = 60
|
||||
|
||||
class COMMANDS(Enum):
|
||||
pass
|
||||
|
||||
def __init__(self, context):
|
||||
commands = {
|
||||
|
||||
}
|
||||
|
||||
super().__init__(commands, context)
|
||||
|
||||
def loop(self):
|
||||
self.logger.info("Getting network snapshot")
|
||||
snapshot = self.get_snapshot()['result']
|
||||
self.logger.debug(f"Loaded snapshot")
|
||||
|
||||
discovered_peers = self.discover_peers()
|
||||
self.logger.info(f"Discovered peers: {list(discovered_peers.keys())}")
|
||||
|
||||
for ip, port in discovered_peers.items():
|
||||
self.logger.info(f"Sending SHOW_STATS command to {ip}:{port}")
|
||||
|
||||
stats = self.fetch_stats_from_peer(ip, port)
|
||||
|
||||
self.logger.debug(f"Received stats from {ip}")
|
||||
snapshot[ip] = stats | {'last_update': datetime.datetime.now().isoformat(), 'last_seen': datetime.datetime.now().isoformat(), 'status': 'reachable'}
|
||||
|
||||
missing_ips = set(snapshot.keys()) - set(discovered_peers.keys())
|
||||
for ip in missing_ips:
|
||||
self.logger.warning(f"Peer {ip} was in snapshot but not reachable. Marking as unreachable")
|
||||
snapshot[ip]['last_update'] = datetime.datetime.now().isoformat()
|
||||
snapshot[ip]['status'] = 'unreachable'
|
||||
|
||||
self.set_snapshot(snapshot)
|
||||
self.logger.info("Snapshot updated successfully")
|
||||
|
||||
|
||||
def get_snapshot(self):
|
||||
return self.context.execute(VaultWarden.COMMANDS.OPEN, DEFAULT_STORAGE_FILES.COLONY_SNAPSHOT.value)
|
||||
|
||||
def set_snapshot(self, snapshot):
|
||||
return self.context.execute(VaultWarden.COMMANDS.OVERWRITE, DEFAULT_STORAGE_FILES.COLONY_SNAPSHOT.value, snapshot)
|
||||
|
||||
def discover_peers(self):
|
||||
peers = {}
|
||||
|
||||
# for ip in get_local_subnet().hosts():
|
||||
for ip in ['192.168.65.101', '192.168.65.102']:
|
||||
try:
|
||||
port = Discover.request(str(ip))
|
||||
peers[str(ip)] = port
|
||||
except (TimeoutError, ConnectionRefusedError):
|
||||
continue
|
||||
return peers
|
||||
|
||||
def fetch_stats_from_peer(self, ip, port):
|
||||
command = Unit.COMMANDS.SHOW_STATS
|
||||
role_name, command_name = command.value.split('.')
|
||||
request = {
|
||||
"command": {
|
||||
"role": role_name,
|
||||
"name": command_name,
|
||||
"args": [],
|
||||
"kwargs": {}
|
||||
}
|
||||
}
|
||||
|
||||
return Whisperer(ip, port).send(request).get('result', {})
|
||||
Whitespace-only changes.
@@ -0,0 +1,21 @@
|
||||
from enum import Enum
|
||||
from colony.roles.role import Role, Command
|
||||
from colony.roles.unit.role import Unit
|
||||
|
||||
|
||||
class Foreman(Role):
|
||||
CONTINUOUS = True
|
||||
LOOP_SECONDS_WAIT = 60
|
||||
|
||||
class COMMANDS(Enum):
|
||||
pass
|
||||
|
||||
def __init__(self, context):
|
||||
commands = {
|
||||
|
||||
}
|
||||
|
||||
super().__init__(commands, context)
|
||||
|
||||
def loop(self):
|
||||
self.context.self().execute(Unit.COMMANDS.SET_ROLES, ['Unit', 'Foreman', 'Explorer', 'VaultWarden'])
|
||||
@@ -0,0 +1,73 @@
|
||||
import threading
|
||||
|
||||
from dependency_injector.wiring import inject, Container, Provide
|
||||
|
||||
from colony.core.context import RoleContext
|
||||
from colony.core.state_manager import StateManager
|
||||
from colony.core.logger.logging import get_logger
|
||||
|
||||
|
||||
class RoleManager:
|
||||
def __init__(self, network, role_registry):
|
||||
self.network = network
|
||||
self.role_registry = role_registry
|
||||
|
||||
self.logger = get_logger(self.__class__.__name__)
|
||||
|
||||
def initialize_roles(self):
|
||||
self.logger.info("Initializing roles...")
|
||||
context = RoleContext(self.network, self)
|
||||
|
||||
roles = {}
|
||||
for role_name in StateManager().get(StateManager.Data.Roles):
|
||||
role_cls = self.role_registry.get_role_class_by_name(role_name)
|
||||
self.logger.debug(f"Creating instance of role {role_name}. ")
|
||||
role_instance = role_cls(context=context)
|
||||
roles[role_name] = role_instance
|
||||
|
||||
self.network.self_node.roles = [role for role in roles.keys()]
|
||||
self.network.self_node.set_role_instances(roles)
|
||||
self.logger.info(f"Roles initialized: {list(roles.keys())}. ")
|
||||
|
||||
def get_role_instances(self):
|
||||
return self.network.self_node.get_role_instances()
|
||||
|
||||
def start_all(self):
|
||||
for role_name, role in self.get_role_instances().items():
|
||||
self.logger.info(f"Starting role {role_name}")
|
||||
t = threading.Thread(target=role.start, daemon=True, name=role_name)
|
||||
t.start()
|
||||
self.logger.debug(f"Thread started for role {role_name}: {t.name}")
|
||||
|
||||
def set_roles(self, new_roles):
|
||||
self.logger.info("Updating roles...")
|
||||
|
||||
current_roles = self.get_role_instances()
|
||||
roles_to_remove = set(current_roles) - set(new_roles)
|
||||
roles_to_add = set(new_roles) - set(current_roles)
|
||||
|
||||
for role_name in roles_to_remove:
|
||||
self.logger.debug(f"Stopping and removing role {role_name}")
|
||||
current_roles[role_name].stop()
|
||||
del current_roles[role_name]
|
||||
|
||||
context = RoleContext(self.network, self)
|
||||
for role_name in roles_to_add:
|
||||
self.logger.debug(f"Creating and adding new role {role_name}")
|
||||
role_cls = available_roles[role_name]
|
||||
current_roles[role_name] = role_cls(context=context)
|
||||
|
||||
StateManager().set(StateManager.Data.Roles, list(current_roles.keys()))
|
||||
|
||||
self.network.self_node.roles = list(current_roles.keys())
|
||||
self.network.self_node.set_role_instances(current_roles)
|
||||
|
||||
for role_name in roles_to_add:
|
||||
role = current_roles[role_name]
|
||||
self.logger.info(f"Starting role {role_name}")
|
||||
t = threading.Thread(target=role.start, daemon=True, name=role_name)
|
||||
t.start()
|
||||
self.logger.debug(f"Thread started for role {role_name}: {t.name}")
|
||||
|
||||
self.logger.info(f"Roles updated to: {list(current_roles.keys())}")
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import threading
|
||||
|
||||
from colony.core.context import RoleContext
|
||||
from colony.core.logger.logging import get_logger
|
||||
|
||||
|
||||
class Command:
|
||||
def __init__(self, callback):
|
||||
self.callback = callback
|
||||
|
||||
class Role:
|
||||
CONTINUOUS = None
|
||||
LOOP_SECONDS_WAIT = None
|
||||
|
||||
def __init__(self, commands, context: RoleContext):
|
||||
self.commands = commands
|
||||
self.context = context
|
||||
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
self._thread = None
|
||||
|
||||
if self.CONTINUOUS and self.LOOP_SECONDS_WAIT is None:
|
||||
raise NotImplementedError("You made this role to run continuously but have not set seconds to wait. Please set constant variable LOOP_SECONDS_WAIT. ")
|
||||
|
||||
self.logger = get_logger(self.__class__.__name__)
|
||||
|
||||
self.logger.info(f'{self.__class__.__name__} initialized and ready')
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def start(self):
|
||||
if not self.CONTINUOUS:
|
||||
return
|
||||
|
||||
self._thread = threading.Thread(target=self._run_loop, daemon=True, name=str(self))
|
||||
self._thread.start()
|
||||
|
||||
def _run_loop(self):
|
||||
while not self._stop_event.is_set():
|
||||
self.loop()
|
||||
self._stop_event.wait(self.LOOP_SECONDS_WAIT)
|
||||
|
||||
def loop(self):
|
||||
if self.CONTINUOUS:
|
||||
raise NotImplementedError("Continuous roles must implement loop()")
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
||||
def execute_local(self, name, *args, **kwargs):
|
||||
command = self.commands.get(name)
|
||||
|
||||
if not command:
|
||||
raise ValueError(f"No command {name}")
|
||||
|
||||
return command.callback(*args, **kwargs)
|
||||
Whitespace-only changes.
@@ -0,0 +1,29 @@
|
||||
from enum import Enum
|
||||
from colony.core.network.hardware import HardwareStatistics
|
||||
from colony.roles.role import Role, Command
|
||||
|
||||
|
||||
class Unit(Role):
|
||||
class COMMANDS(Enum):
|
||||
SHOW_STATS = 'Unit.ShowStats'
|
||||
SHOW_HARDWARE = 'Unit.ShowHardware'
|
||||
SET_ROLES = 'Unit.SetRoles'
|
||||
|
||||
def __init__(self, context):
|
||||
commands = {
|
||||
self.COMMANDS.SHOW_STATS.value: Command(self.show_stats),
|
||||
self.COMMANDS.SHOW_HARDWARE.value: Command(self.show_hardware),
|
||||
self.COMMANDS.SET_ROLES.value: Command(self.set_roles),
|
||||
}
|
||||
|
||||
super().__init__(commands, context)
|
||||
|
||||
def show_hardware(self):
|
||||
return HardwareStatistics.collect().to_dict()
|
||||
|
||||
def show_stats(self):
|
||||
self_node = self.context.self()
|
||||
return self_node.to_dict()
|
||||
|
||||
def set_roles(self, roles):
|
||||
self.context.set_self_node_roles(roles)
|
||||
Whitespace-only changes.
@@ -0,0 +1,31 @@
|
||||
from enum import Enum
|
||||
|
||||
from colony.core.storage import DefaultStorage
|
||||
from colony.roles.role import Role, Command
|
||||
|
||||
|
||||
class VaultWarden(Role):
|
||||
class COMMANDS(Enum):
|
||||
LS = 'VaultWarden.ls'
|
||||
OPEN = 'VaultWarden.open'
|
||||
OVERWRITE = 'VaultWarden.overwrite'
|
||||
|
||||
def __init__(self, context):
|
||||
commands = {
|
||||
self.COMMANDS.LS.value: Command(self.ls),
|
||||
self.COMMANDS.OPEN.value: Command(self.open),
|
||||
self.COMMANDS.OVERWRITE.value: Command(self.overwrite)
|
||||
}
|
||||
|
||||
self.storage = DefaultStorage()
|
||||
|
||||
super().__init__(commands, context)
|
||||
|
||||
def ls(self, dir):
|
||||
return self.storage.ls(dir)
|
||||
|
||||
def open(self, file):
|
||||
return self.storage.open(file)
|
||||
|
||||
def overwrite(self, filename, content):
|
||||
return self.storage.save(filename, content)
|
||||
@@ -0,0 +1,7 @@
|
||||
from dynaconf import Dynaconf
|
||||
|
||||
configuration = Dynaconf(
|
||||
envvar_prefix="COLONY",
|
||||
settings_files=['config.json'],
|
||||
load_dotenv=False,
|
||||
)
|
||||
Whitespace-only changes.
@@ -0,0 +1,32 @@
|
||||
import socket
|
||||
import struct
|
||||
|
||||
class Discover:
|
||||
DISCOVER_PORT = 28149
|
||||
MAGIC = b"OK"
|
||||
|
||||
@staticmethod
|
||||
def listen(real_port, host="0.0.0.0"):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind((host, Discover.DISCOVER_PORT))
|
||||
s.listen()
|
||||
|
||||
while True:
|
||||
conn, _ = s.accept()
|
||||
try:
|
||||
data = conn.recv(2)
|
||||
if data == Discover.MAGIC:
|
||||
conn.sendall(struct.pack("!H", real_port))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def request(target_host, timeout=3):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(timeout)
|
||||
s.connect((target_host, Discover.DISCOVER_PORT))
|
||||
s.sendall(Discover.MAGIC)
|
||||
data = s.recv(2)
|
||||
s.close()
|
||||
return struct.unpack("!H", data)[0]
|
||||
@@ -0,0 +1,64 @@
|
||||
import json
|
||||
import socket
|
||||
import ssl
|
||||
import threading
|
||||
from asyncio import Queue
|
||||
|
||||
from colony.settings import configuration
|
||||
|
||||
|
||||
class Listener:
|
||||
def __init__(self, dispatcher, host=None, port=None, cert_file=None, keyfile=None):
|
||||
self.dispatcher = dispatcher
|
||||
self.host = host or getattr(configuration.listener, "host", "0.0.0.0")
|
||||
self.port = port or getattr(configuration.listener, "port", 9000)
|
||||
self.cert_file = cert_file or getattr(configuration.listener, "cert_file", "certs/server.crt")
|
||||
self.keyfile = keyfile or getattr(configuration.listener, "keyfile", "certs/server.key")
|
||||
self.queue = Queue()
|
||||
self.running = False
|
||||
|
||||
def handle_client(self, conn):
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(certfile=self.cert_file, keyfile=self.keyfile)
|
||||
|
||||
tls_conn = context.wrap_socket(conn, server_side=True)
|
||||
|
||||
try:
|
||||
data = tls_conn.recv(65536)
|
||||
payload = json.loads(data.decode())
|
||||
|
||||
result = self.dispatcher.dispatch(payload)
|
||||
|
||||
response = json.dumps({
|
||||
"status": "ok",
|
||||
"result": result
|
||||
}).encode()
|
||||
|
||||
tls_conn.sendall(response)
|
||||
except Exception as e:
|
||||
error = json.dumps({
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}).encode()
|
||||
tls_conn.sendall(error)
|
||||
raise
|
||||
finally:
|
||||
tls_conn.close()
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((self.host, self.port))
|
||||
sock.listen(5)
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
conn, addr = sock.accept()
|
||||
t = threading.Thread(target=self.handle_client, args=(conn,), name='HandleClient')
|
||||
t.daemon = True
|
||||
t.start()
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
sock.close()
|
||||
@@ -0,0 +1,38 @@
|
||||
import json
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from colony.settings import configuration
|
||||
|
||||
|
||||
class Whisperer:
|
||||
def __init__(self, host=None, port=None, cert_file=None, keyfile=None):
|
||||
self.host = host or getattr(configuration.whisperer, "host", "127.0.0.1")
|
||||
self.port = port or getattr(configuration.whisperer, "port", 9000)
|
||||
self.cert_file = cert_file or getattr(configuration.whisperer, "cert_file", None)
|
||||
self.keyfile = keyfile or getattr(configuration.whisperer, "keyfile", None)
|
||||
|
||||
def send(self, payload, timeout=500):
|
||||
data = json.dumps(payload).encode()
|
||||
|
||||
context = ssl.create_default_context()
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
tls_sock = context.wrap_socket(sock)
|
||||
|
||||
try:
|
||||
tls_sock.connect((self.host, self.port))
|
||||
tls_sock.sendall(data)
|
||||
|
||||
response = tls_sock.recv(65536)
|
||||
if not response:
|
||||
raise RuntimeError("Connection closed without response. ")
|
||||
|
||||
return json.loads(response.decode())
|
||||
except socket.timeout:
|
||||
raise TimeoutError("No response before timeout. ")
|
||||
finally:
|
||||
tls_sock.close()
|
||||
Whitespace-only changes.
@@ -0,0 +1,17 @@
|
||||
import ipaddress
|
||||
import socket
|
||||
|
||||
|
||||
def get_local_subnet():
|
||||
hostname = socket.gethostname()
|
||||
local_ip = socket.gethostbyname(hostname)
|
||||
|
||||
subnet = ipaddress.ip_network(f"{local_ip}/24", strict=False)
|
||||
|
||||
return subnet
|
||||
|
||||
def get_local_ip():
|
||||
return socket.gethostbyname(socket.gethostname())
|
||||
|
||||
def get_hostname():
|
||||
return socket.gethostname()
|
||||
Reference in new issue
Block a user