Stuff
This commit is contained in:
1 parent
9add2c6382
commit
f67b1b1042
18 files changed
+492
-210
No files matched your search
@@ -8,14 +8,31 @@ class Dispatcher:
|
|||||||
self.network = network
|
self.network = network
|
||||||
self.role_registry = role_registry
|
self.role_registry = role_registry
|
||||||
|
|
||||||
def dispatch(self, payload):
|
@staticmethod
|
||||||
self.logger.debug(f"Dispatching payload for command {payload['command']['role']}.{payload["command"]['name']}")
|
def _parse(payload):
|
||||||
|
command = payload.get("command") if isinstance(payload, dict) else None
|
||||||
|
if not isinstance(command, dict):
|
||||||
|
raise ValueError("Payload has no command")
|
||||||
|
|
||||||
c = payload["command"]
|
role_name = command.get("role")
|
||||||
role_name = c["role"]
|
command_name = command.get("name")
|
||||||
command_name = c["name"]
|
if not isinstance(role_name, str) or not role_name or not isinstance(command_name, str) or not command_name:
|
||||||
args = tuple(c.get("args", []))
|
raise ValueError("Command needs a role and a name")
|
||||||
kwargs = dict(c.get("kwargs", {}))
|
|
||||||
|
args = command.get("args")
|
||||||
|
if args is None:
|
||||||
|
args = []
|
||||||
|
kwargs = command.get("kwargs")
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = {}
|
||||||
|
if not isinstance(args, (list, tuple)) or not isinstance(kwargs, dict):
|
||||||
|
raise ValueError("Command args must be a list and kwargs a dict")
|
||||||
|
|
||||||
|
return role_name, command_name, tuple(args), kwargs
|
||||||
|
|
||||||
|
def dispatch(self, payload):
|
||||||
|
role_name, command_name, args, kwargs = self._parse(payload)
|
||||||
|
self.logger.debug(f"Dispatching payload for command {role_name}.{command_name}")
|
||||||
|
|
||||||
role_entry = self.role_registry.get_role_class_by_name(role_name)
|
role_entry = self.role_registry.get_role_class_by_name(role_name)
|
||||||
if role_entry is None:
|
if role_entry is None:
|
||||||
|
|||||||
+33
-12
@@ -22,10 +22,10 @@ class Network:
|
|||||||
)
|
)
|
||||||
self.logger.info(f"Self node created: {self.self_node.id}")
|
self.logger.info(f"Self node created: {self.self_node.id}")
|
||||||
|
|
||||||
self.nodes = self.load_network_from_snapshot()
|
self.nodes = [self.self_node] + [
|
||||||
|
node for node in self.load_network_from_snapshot()
|
||||||
if not self.nodes:
|
if (node.ip, node.port) != (self.self_node.ip, self.self_node.port)
|
||||||
self.nodes.append(self.self_node)
|
]
|
||||||
|
|
||||||
def get_self(self):
|
def get_self(self):
|
||||||
return self.self_node
|
return self.self_node
|
||||||
@@ -70,21 +70,42 @@ class Network:
|
|||||||
snapshot = json.load(f)
|
snapshot = json.load(f)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
self.logger.warning(f"Snapshot file {file_path} not found. Starting with empty network")
|
self.logger.warning(f"Snapshot file {file_path} not found. Starting with empty network")
|
||||||
snapshot = {"nodes": []}
|
return nodes
|
||||||
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
|
self.logger.warning(f"Could not read snapshot {file_path}: {e}")
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
if not isinstance(snapshot, dict):
|
||||||
|
self.logger.warning(f"Snapshot {file_path} is not an object. Ignoring it")
|
||||||
|
return nodes
|
||||||
|
|
||||||
for node_ip, node_data in snapshot.items():
|
for node_ip, node_data in snapshot.items():
|
||||||
result = node_data.get('result')
|
if not isinstance(node_data, dict):
|
||||||
|
self.logger.warning(f"Skipping node {node_ip}: entry is not a dict")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if node_data.get("status") == "unreachable":
|
||||||
|
self.logger.info(f"Skipping node {node_ip}: last seen unreachable")
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = node_data.get("result")
|
||||||
|
|
||||||
if not isinstance(result, dict):
|
if not isinstance(result, dict):
|
||||||
self.logger.warning(f"Skipping node {node_ip}: result is not a dict")
|
self.logger.warning(f"Skipping node {node_ip}: result is not a dict")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
node = Node(
|
roles = result.get("roles", [])
|
||||||
hostname=result["hostname"],
|
try:
|
||||||
ip=result["ip"],
|
node = Node(
|
||||||
port=result["port"],
|
hostname=result["hostname"],
|
||||||
roles=result.get("roles", [])
|
ip=result["ip"],
|
||||||
)
|
port=result["port"],
|
||||||
|
roles=roles if isinstance(roles, list) else []
|
||||||
|
)
|
||||||
|
except KeyError as e:
|
||||||
|
self.logger.warning(f"Skipping node {node_ip}: result has no {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
self.logger.info(f"Loaded node from snapshot: {node.id}")
|
self.logger.info(f"Loaded node from snapshot: {node.id}")
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class HardwareStatistics:
|
|||||||
return json.dumps(self.to_dict(), indent=2)
|
return json.dumps(self.to_dict(), indent=2)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(hw_dict: str):
|
def from_dict(hw_dict: dict):
|
||||||
hw = HardwareStatistics()
|
hw = HardwareStatistics()
|
||||||
hw.cpu = hw_dict.get("cpu")
|
hw.cpu = hw_dict.get("cpu")
|
||||||
hw.mem = hw_dict.get("memory")
|
hw.mem = hw_dict.get("memory")
|
||||||
@@ -59,7 +59,7 @@ class HardwareStatistics:
|
|||||||
return {
|
return {
|
||||||
"cores": psutil.cpu_count(logical=False),
|
"cores": psutil.cpu_count(logical=False),
|
||||||
"threads": psutil.cpu_count(logical=True),
|
"threads": psutil.cpu_count(logical=True),
|
||||||
"usage_percent": psutil.cpu_percent(interval=1)
|
"usage_percent": psutil.cpu_percent(interval=0.2)
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_memory_stats(self):
|
def _get_memory_stats(self):
|
||||||
@@ -72,7 +72,10 @@ class HardwareStatistics:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_gpu_stats(self):
|
def _get_gpu_stats(self):
|
||||||
gpus = GPUtil.getGPUs()
|
try:
|
||||||
|
gpus = GPUtil.getGPUs()
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
if not gpus:
|
if not gpus:
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
@@ -91,7 +94,10 @@ class HardwareStatistics:
|
|||||||
def _get_disk_stats(self):
|
def _get_disk_stats(self):
|
||||||
disk_info = []
|
disk_info = []
|
||||||
for d in psutil.disk_partitions():
|
for d in psutil.disk_partitions():
|
||||||
usage = psutil.disk_usage(d.mountpoint)
|
try:
|
||||||
|
usage = psutil.disk_usage(d.mountpoint)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
disk_info.append({
|
disk_info.append({
|
||||||
"device": d.device,
|
"device": d.device,
|
||||||
"mountpoint": d.mountpoint,
|
"mountpoint": d.mountpoint,
|
||||||
@@ -125,9 +131,9 @@ class HardwareScorer:
|
|||||||
cpu = hw_obj.cpu
|
cpu = hw_obj.cpu
|
||||||
if not cpu:
|
if not cpu:
|
||||||
return 0.0
|
return 0.0
|
||||||
cores = cpu.get("cores", 1)
|
cores = cpu.get("cores") or cpu.get("threads") or 1
|
||||||
threads = cpu.get("threads", 1)
|
threads = cpu.get("threads") or cores
|
||||||
usage = cpu.get("usage_percent", 0) # current CPU utilization in percent
|
usage = cpu.get("usage_percent") or 0 # current CPU utilization in percent
|
||||||
available = (cores + threads) * (1 - usage / 100)
|
available = (cores + threads) * (1 - usage / 100)
|
||||||
return round(available, 2)
|
return round(available, 2)
|
||||||
|
|
||||||
@@ -183,15 +189,18 @@ class HardwareScorer:
|
|||||||
def longevity_score(hw_obj) -> float:
|
def longevity_score(hw_obj) -> float:
|
||||||
"""
|
"""
|
||||||
Compute uptime in hours.
|
Compute uptime in hours.
|
||||||
Converts boot_time from ISO format to datetime, then calculates
|
Accepts boot_time as a datetime or an ISO string, then calculates
|
||||||
the difference from the current time.
|
the difference from the current time.
|
||||||
"""
|
"""
|
||||||
boot_time_iso = hw_obj.boot_time
|
boot_time = hw_obj.boot_time
|
||||||
try:
|
if isinstance(boot_time, str):
|
||||||
boot_time = datetime.datetime.fromisoformat(boot_time_iso)
|
try:
|
||||||
except Exception:
|
boot_time = datetime.datetime.fromisoformat(boot_time)
|
||||||
|
except ValueError:
|
||||||
|
return 0.0
|
||||||
|
if not isinstance(boot_time, datetime.datetime):
|
||||||
return 0.0
|
return 0.0
|
||||||
uptime_hours = (datetime.datetime.now() - boot_time).total_seconds() / 3600
|
uptime_hours = (datetime.datetime.now(tz=boot_time.tzinfo) - boot_time).total_seconds() / 3600
|
||||||
return round(uptime_hours, 2)
|
return round(uptime_hours, 2)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -37,11 +37,10 @@ class LocalNode(Node):
|
|||||||
self.__roles = dict()
|
self.__roles = dict()
|
||||||
|
|
||||||
def get_role_instances(self):
|
def get_role_instances(self):
|
||||||
return self.__roles
|
return dict(self.__roles)
|
||||||
|
|
||||||
def set_role_instances(self, roles):
|
def set_role_instances(self, roles):
|
||||||
for role_name, role_instance in roles.items():
|
self.__roles = dict(roles)
|
||||||
self.__roles[role_name] = role_instance
|
|
||||||
|
|
||||||
def execute_local(self, command: Enum, *args, **kwargs):
|
def execute_local(self, command: Enum, *args, **kwargs):
|
||||||
request = CommandRequest.form(command, *args, **kwargs)
|
request = CommandRequest.form(command, *args, **kwargs)
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from colony.core.network.requests import CommandRequest
|
|
||||||
from colony.core.storage import DEFAULT_STORAGE_FILES
|
from colony.core.storage import DEFAULT_STORAGE_FILES
|
||||||
from colony.core.roles.role import Role
|
from colony.core.roles.role import Role
|
||||||
from colony.core.roles.unit.role import Unit
|
from colony.core.roles.unit.role import Unit
|
||||||
from colony.core.roles.vaultwarden.role import VaultWarden
|
from colony.core.roles.vaultwarden.role import VaultWarden
|
||||||
|
from colony.settings import configuration
|
||||||
from colony.transport.discover import Discover
|
from colony.transport.discover import Discover
|
||||||
from colony.transport.whisperer import Whisperer
|
from colony.utils.network import get_local_subnet
|
||||||
|
|
||||||
|
|
||||||
class Explorer(Role):
|
class Explorer(Role):
|
||||||
CONTINUOUS = True
|
CONTINUOUS = True
|
||||||
LOOP_SECONDS_WAIT = 60
|
LOOP_SECONDS_WAIT = 60
|
||||||
|
|
||||||
|
SCAN_WORKERS = 64
|
||||||
|
SCAN_TIMEOUT = 1
|
||||||
|
|
||||||
class COMMANDS(Enum):
|
class COMMANDS(Enum):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -26,29 +30,77 @@ class Explorer(Role):
|
|||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
self.logger.info("Getting network snapshot")
|
self.logger.info("Getting network snapshot")
|
||||||
snapshot = self.get_snapshot()['result'] or {}
|
snapshot = self.load_snapshot()
|
||||||
|
if snapshot is None:
|
||||||
|
return
|
||||||
self.logger.debug(f"Loaded snapshot")
|
self.logger.debug(f"Loaded snapshot")
|
||||||
|
|
||||||
discovered_peers = self.discover_peers()
|
discovered_peers = self.discover_peers()
|
||||||
self.logger.info(f"Discovered peers: {list(discovered_peers.keys())}")
|
self.logger.info(f"Discovered peers: {list(discovered_peers.keys())}")
|
||||||
|
|
||||||
|
reachable = set()
|
||||||
for ip, port in discovered_peers.items():
|
for ip, port in discovered_peers.items():
|
||||||
self.logger.info(f"Sending SHOW_STATS command to {ip}:{port}")
|
self.logger.info(f"Sending SHOW_STATS command to {ip}:{port}")
|
||||||
|
|
||||||
stats = self.fetch_stats_from_peer(ip, port)
|
stats = self.fetch_stats_from_peer(ip, port)
|
||||||
|
if stats is None:
|
||||||
|
continue
|
||||||
|
|
||||||
self.logger.debug(f"Received stats from {ip}")
|
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'}
|
now = datetime.datetime.now().isoformat()
|
||||||
|
snapshot[ip] = {
|
||||||
|
'status': 'reachable',
|
||||||
|
'result': stats,
|
||||||
|
'last_update': now,
|
||||||
|
'last_seen': now,
|
||||||
|
}
|
||||||
|
reachable.add(ip)
|
||||||
|
|
||||||
missing_ips = set(snapshot.keys()) - set(discovered_peers.keys())
|
for ip in snapshot.keys() - reachable:
|
||||||
for ip in missing_ips:
|
|
||||||
self.logger.warning(f"Peer {ip} was in snapshot but not reachable. Marking as unreachable")
|
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]['last_update'] = datetime.datetime.now().isoformat()
|
||||||
snapshot[ip]['status'] = 'unreachable'
|
snapshot[ip]['status'] = 'unreachable'
|
||||||
|
|
||||||
self.set_snapshot(snapshot)
|
if self._stop_event.is_set():
|
||||||
self.logger.info("Snapshot updated successfully")
|
self.logger.info("Role was removed during the scan, not writing the snapshot")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.save_snapshot(snapshot)
|
||||||
|
|
||||||
|
def load_snapshot(self):
|
||||||
|
try:
|
||||||
|
resp = self.get_snapshot()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Could not read the snapshot: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(resp, dict) or resp.get('status') != 'ok':
|
||||||
|
self.logger.warning("Could not read the snapshot: VaultWarden returned an error")
|
||||||
|
return None
|
||||||
|
|
||||||
|
snapshot = resp.get('result')
|
||||||
|
if snapshot is None:
|
||||||
|
return {}
|
||||||
|
if not isinstance(snapshot, dict):
|
||||||
|
self.logger.warning("Stored snapshot is not an object. Refusing to overwrite it")
|
||||||
|
return None
|
||||||
|
|
||||||
|
for ip in [ip for ip, entry in snapshot.items() if not isinstance(entry, dict)]:
|
||||||
|
self.logger.warning(f"Dropping malformed snapshot entry for {ip}")
|
||||||
|
del snapshot[ip]
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
def save_snapshot(self, snapshot):
|
||||||
|
try:
|
||||||
|
resp = self.set_snapshot(snapshot)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Could not write the snapshot: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(resp, dict) or resp.get('status') != 'ok':
|
||||||
|
self.logger.error("Could not write the snapshot: VaultWarden returned an error")
|
||||||
|
return
|
||||||
|
self.logger.info("Snapshot updated successfully")
|
||||||
|
|
||||||
def get_snapshot(self):
|
def get_snapshot(self):
|
||||||
return self.context.execute(VaultWarden.COMMANDS.OPEN, DEFAULT_STORAGE_FILES.COLONY_SNAPSHOT.value)
|
return self.context.execute(VaultWarden.COMMANDS.OPEN, DEFAULT_STORAGE_FILES.COLONY_SNAPSHOT.value)
|
||||||
@@ -56,17 +108,32 @@ class Explorer(Role):
|
|||||||
def set_snapshot(self, snapshot):
|
def set_snapshot(self, snapshot):
|
||||||
return self.context.execute(VaultWarden.COMMANDS.OVERWRITE, DEFAULT_STORAGE_FILES.COLONY_SNAPSHOT.value, snapshot)
|
return self.context.execute(VaultWarden.COMMANDS.OVERWRITE, DEFAULT_STORAGE_FILES.COLONY_SNAPSHOT.value, snapshot)
|
||||||
|
|
||||||
def discover_peers(self):
|
def _hosts_to_scan(self):
|
||||||
peers = {}
|
configured = configuration.get("explorer.hosts")
|
||||||
|
if isinstance(configured, (list, tuple)) and configured:
|
||||||
|
return [str(host) for host in configured]
|
||||||
|
return [str(host) for host in get_local_subnet().hosts()]
|
||||||
|
|
||||||
# for ip in get_local_subnet().hosts():
|
def _probe(self, ip):
|
||||||
for ip in ['192.168.65.101', '192.168.65.102']:
|
try:
|
||||||
try:
|
return ip, Discover.request(ip, timeout=self.SCAN_TIMEOUT)
|
||||||
port = Discover.request(str(ip))
|
except Exception:
|
||||||
peers[str(ip)] = port
|
return ip, None
|
||||||
except (TimeoutError, ConnectionRefusedError, OSError):
|
|
||||||
continue
|
def discover_peers(self):
|
||||||
return peers
|
with ThreadPoolExecutor(max_workers=self.SCAN_WORKERS) as pool:
|
||||||
|
results = list(pool.map(self._probe, self._hosts_to_scan()))
|
||||||
|
|
||||||
|
return {ip: port for ip, port in results if port is not None}
|
||||||
|
|
||||||
def fetch_stats_from_peer(self, ip, port):
|
def fetch_stats_from_peer(self, ip, port):
|
||||||
return self.context.execute_to_unregistered_node(ip, port, Unit.COMMANDS.SHOW_STATS)
|
try:
|
||||||
|
resp = self.context.execute_to_unregistered_node(ip, port, Unit.COMMANDS.SHOW_STATS)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Could not fetch stats from {ip}:{port}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(resp, dict) or resp.get('status') != 'ok' or not isinstance(resp.get('result'), dict):
|
||||||
|
self.logger.warning(f"Peer {ip}:{port} answered SHOW_STATS with an error")
|
||||||
|
return None
|
||||||
|
return resp['result']
|
||||||
@@ -36,21 +36,30 @@ class Foreman(Role):
|
|||||||
k = (self_node.ip, self_node.port)
|
k = (self_node.ip, self_node.port)
|
||||||
if k not in by_key:
|
if k not in by_key:
|
||||||
by_key[k] = self_node
|
by_key[k] = self_node
|
||||||
return list(by_key.values())
|
return [by_key[key] for key in sorted(by_key)]
|
||||||
|
|
||||||
|
def _call(self, peer, command, *args):
|
||||||
|
try:
|
||||||
|
resp = peer.execute(command, *args)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning("%s failed for %s:%s: %s", command.value, peer.ip, peer.port, e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(resp, dict) or resp.get("status") != "ok":
|
||||||
|
self.logger.warning("%s failed for %s:%s", command.value, peer.ip, peer.port)
|
||||||
|
return None
|
||||||
|
return resp
|
||||||
|
|
||||||
def _fetch_live_roles(self, peers):
|
def _fetch_live_roles(self, peers):
|
||||||
live = {}
|
live = {}
|
||||||
for peer in peers:
|
for peer in peers:
|
||||||
resp = peer.execute(Unit.COMMANDS.SHOW_STATS)
|
resp = self._call(peer, Unit.COMMANDS.SHOW_ROLES)
|
||||||
if resp.get("status") != "ok":
|
if resp is None:
|
||||||
self.logger.warning(
|
|
||||||
"SHOW_STATS failed for %s:%s, assuming no roles",
|
|
||||||
peer.ip,
|
|
||||||
peer.port,
|
|
||||||
)
|
|
||||||
live[peer] = []
|
|
||||||
continue
|
continue
|
||||||
live[peer] = list(resp["result"].get("roles", []))
|
if not isinstance(resp.get("result"), list):
|
||||||
|
self.logger.warning("Bad role list from %s:%s, leaving it out of this round", peer.ip, peer.port)
|
||||||
|
continue
|
||||||
|
live[peer] = list(resp["result"])
|
||||||
return live
|
return live
|
||||||
|
|
||||||
def _assign_critical_holders(self, peers, critical_roles, live_roles):
|
def _assign_critical_holders(self, peers, critical_roles, live_roles):
|
||||||
@@ -68,15 +77,23 @@ class Foreman(Role):
|
|||||||
holders[critical_role] = peer_list[i % n]
|
holders[critical_role] = peer_list[i % n]
|
||||||
return holders
|
return holders
|
||||||
|
|
||||||
def _best_peer_for_optional(self, peers, role_stat):
|
def _fetch_hardware_scores(self, peers):
|
||||||
|
scores = {}
|
||||||
|
for peer in peers:
|
||||||
|
resp = self._call(peer, Unit.COMMANDS.SHOW_HARDWARE)
|
||||||
|
if resp is None:
|
||||||
|
continue
|
||||||
|
hw_obj = HardwareStatistics.from_dict(resp["result"])
|
||||||
|
scores[peer] = HardwareScorer.from_object(hw_obj)
|
||||||
|
return scores
|
||||||
|
|
||||||
|
def _best_peer_for_optional(self, peers, role_stat, hardware_scores):
|
||||||
best_score = float("-inf")
|
best_score = float("-inf")
|
||||||
best_peer = None
|
best_peer = None
|
||||||
for peer in peers:
|
for peer in peers:
|
||||||
hw_resp = peer.execute(Unit.COMMANDS.SHOW_HARDWARE)
|
hs = hardware_scores.get(peer)
|
||||||
if hw_resp.get("status") != "ok":
|
if hs is None:
|
||||||
continue
|
continue
|
||||||
hw_obj = HardwareStatistics.from_dict(hw_resp["result"])
|
|
||||||
hs = HardwareScorer.from_object(hw_obj)
|
|
||||||
score = 0.0
|
score = 0.0
|
||||||
if role_stat.hardware_preferences:
|
if role_stat.hardware_preferences:
|
||||||
for hw_type, weight in role_stat.hardware_preferences.priorities.items():
|
for hw_type, weight in role_stat.hardware_preferences.priorities.items():
|
||||||
@@ -119,6 +136,10 @@ class Foreman(Role):
|
|||||||
return
|
return
|
||||||
|
|
||||||
live_roles = self._fetch_live_roles(peers)
|
live_roles = self._fetch_live_roles(peers)
|
||||||
|
peers = [peer for peer in peers if peer in live_roles]
|
||||||
|
if not peers:
|
||||||
|
return
|
||||||
|
|
||||||
critical_holders = self._assign_critical_holders(
|
critical_holders = self._assign_critical_holders(
|
||||||
peers, critical_roles, live_roles
|
peers, critical_roles, live_roles
|
||||||
)
|
)
|
||||||
@@ -127,14 +148,16 @@ class Foreman(Role):
|
|||||||
for critical_role, holder in critical_holders.items():
|
for critical_role, holder in critical_holders.items():
|
||||||
desired[holder].append(critical_role)
|
desired[holder].append(critical_role)
|
||||||
|
|
||||||
|
hardware_scores = self._fetch_hardware_scores(peers) if optional_roles else {}
|
||||||
for role_name in optional_roles:
|
for role_name in optional_roles:
|
||||||
role_stat = roles[role_name]
|
role_stat = roles[role_name]
|
||||||
best_peer = self._best_peer_for_optional(peers, role_stat)
|
best_peer = self._best_peer_for_optional(peers, role_stat, hardware_scores)
|
||||||
if best_peer is not None:
|
if best_peer is not None:
|
||||||
desired[best_peer].append(role_name)
|
desired[best_peer].append(role_name)
|
||||||
|
|
||||||
for peer in peers:
|
for peer in peers:
|
||||||
peer.execute(
|
self._call(
|
||||||
|
peer,
|
||||||
Unit.COMMANDS.SET_ROLES,
|
Unit.COMMANDS.SET_ROLES,
|
||||||
self._dedupe_preserve_order(desired[peer]),
|
self._dedupe_preserve_order(desired[peer]),
|
||||||
)
|
)
|
||||||
@@ -12,20 +12,25 @@ class RoleManager:
|
|||||||
self.state_manager = state_manager
|
self.state_manager = state_manager
|
||||||
|
|
||||||
self.logger = get_logger(self.__class__.__name__)
|
self.logger = get_logger(self.__class__.__name__)
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
def initialize_roles(self):
|
def initialize_roles(self):
|
||||||
self.logger.info("Initializing roles...")
|
self.logger.info("Initializing roles...")
|
||||||
context = RoleContext(self.network, self)
|
context = RoleContext(self.network, self)
|
||||||
|
|
||||||
roles = {}
|
with self._lock:
|
||||||
for role_name in self.state_manager.get(StateManager.Data.Roles):
|
roles = {}
|
||||||
role_cls = self.role_registry.get_role_class_by_name(role_name)
|
for role_name in self.state_manager.get(StateManager.Data.Roles):
|
||||||
self.logger.debug(f"Creating instance of role {role_name}. ")
|
role_cls = self.role_registry.get_role_class_by_name(role_name)
|
||||||
role_instance = role_cls(context=context)
|
if role_cls is None:
|
||||||
roles[role_name] = role_instance
|
self.logger.warning(f"Skipping unknown role {role_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
self.network.self_node.roles = [role for role in roles.keys()]
|
self.logger.debug(f"Creating instance of role {role_name}. ")
|
||||||
self.network.self_node.set_role_instances(roles)
|
roles[role_name] = role_cls(context=context)
|
||||||
|
|
||||||
|
self.network.self_node.roles = list(roles)
|
||||||
|
self.network.self_node.set_role_instances(roles)
|
||||||
self.logger.info(f"Roles initialized: {list(roles.keys())}. ")
|
self.logger.info(f"Roles initialized: {list(roles.keys())}. ")
|
||||||
|
|
||||||
def get_role_instances(self):
|
def get_role_instances(self):
|
||||||
@@ -34,39 +39,54 @@ class RoleManager:
|
|||||||
def start_all(self):
|
def start_all(self):
|
||||||
for role_name, role in self.get_role_instances().items():
|
for role_name, role in self.get_role_instances().items():
|
||||||
self.logger.info(f"Starting role {role_name}")
|
self.logger.info(f"Starting role {role_name}")
|
||||||
t = threading.Thread(target=role.start, daemon=True, name=role_name)
|
role.start()
|
||||||
t.start()
|
|
||||||
self.logger.debug(f"Thread started for role {role_name}: {t.name}")
|
def _validate_roles(self, new_roles):
|
||||||
|
if not isinstance(new_roles, list):
|
||||||
|
raise ValueError("Roles must be a list of role names")
|
||||||
|
|
||||||
|
for role_name in new_roles:
|
||||||
|
if self.role_registry.get_role_class_by_name(role_name) is None:
|
||||||
|
raise ValueError(f"Unknown role {role_name!r}")
|
||||||
|
|
||||||
|
return list(dict.fromkeys(new_roles))
|
||||||
|
|
||||||
def set_roles(self, new_roles):
|
def set_roles(self, new_roles):
|
||||||
self.logger.info("Updating roles...")
|
self.logger.info("Updating roles...")
|
||||||
|
|
||||||
current_roles = self.get_role_instances()
|
new_roles = self._validate_roles(new_roles)
|
||||||
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:
|
with self._lock:
|
||||||
self.logger.debug(f"Stopping and removing role {role_name}")
|
current_roles = self.get_role_instances()
|
||||||
current_roles[role_name].stop()
|
roles_to_remove = [name for name in current_roles if name not in new_roles]
|
||||||
del current_roles[role_name]
|
roles_to_add = [name for name in new_roles if name not in current_roles]
|
||||||
|
|
||||||
context = RoleContext(self.network, self)
|
if not roles_to_remove and not roles_to_add:
|
||||||
for role_name in roles_to_add:
|
self.logger.info("Roles already up to date")
|
||||||
self.logger.debug(f"Creating and adding new role {role_name}")
|
return
|
||||||
role_cls = self.role_registry.get_role_class_by_name(role_name)
|
|
||||||
current_roles[role_name] = role_cls(context=context)
|
|
||||||
|
|
||||||
self.state_manager.set(StateManager.Data.Roles, list(current_roles.keys()))
|
context = RoleContext(self.network, self)
|
||||||
|
added = {}
|
||||||
|
for role_name in roles_to_add:
|
||||||
|
self.logger.debug(f"Creating new role {role_name}")
|
||||||
|
role_cls = self.role_registry.get_role_class_by_name(role_name)
|
||||||
|
added[role_name] = role_cls(context=context)
|
||||||
|
|
||||||
self.network.self_node.roles = list(current_roles.keys())
|
final_roles = {
|
||||||
self.network.self_node.set_role_instances(current_roles)
|
name: current_roles[name] if name in current_roles else added[name]
|
||||||
|
for name in new_roles
|
||||||
|
}
|
||||||
|
|
||||||
for role_name in roles_to_add:
|
self.state_manager.set(StateManager.Data.Roles, list(final_roles.keys()))
|
||||||
role = current_roles[role_name]
|
self.network.self_node.set_role_instances(final_roles)
|
||||||
self.logger.info(f"Starting role {role_name}")
|
self.network.self_node.roles = list(final_roles.keys())
|
||||||
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())}")
|
for role_name in roles_to_remove:
|
||||||
|
self.logger.debug(f"Stopping removed role {role_name}")
|
||||||
|
current_roles[role_name].stop()
|
||||||
|
|
||||||
|
for role_name, role in added.items():
|
||||||
|
self.logger.info(f"Starting role {role_name}")
|
||||||
|
role.start()
|
||||||
|
|
||||||
|
self.logger.info(f"Roles updated to: {list(final_roles.keys())}")
|
||||||
@@ -31,7 +31,7 @@ class Role:
|
|||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if not self.CONTINUOUS:
|
if not self.CONTINUOUS or self._thread is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._thread = threading.Thread(target=self._run_loop, daemon=True, name=str(self))
|
self._thread = threading.Thread(target=self._run_loop, daemon=True, name=str(self))
|
||||||
@@ -39,7 +39,10 @@ class Role:
|
|||||||
|
|
||||||
def _run_loop(self):
|
def _run_loop(self):
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
self.loop()
|
try:
|
||||||
|
self.loop()
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("Loop failed, retrying after the usual wait")
|
||||||
self._stop_event.wait(self.LOOP_SECONDS_WAIT)
|
self._stop_event.wait(self.LOOP_SECONDS_WAIT)
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ from colony.core.roles.role import Role, Command
|
|||||||
class Unit(Role):
|
class Unit(Role):
|
||||||
class COMMANDS(Enum):
|
class COMMANDS(Enum):
|
||||||
SHOW_STATS = 'Unit.ShowStats'
|
SHOW_STATS = 'Unit.ShowStats'
|
||||||
|
SHOW_ROLES = 'Unit.ShowRoles'
|
||||||
SHOW_HARDWARE = 'Unit.ShowHardware'
|
SHOW_HARDWARE = 'Unit.ShowHardware'
|
||||||
SET_ROLES = 'Unit.SetRoles'
|
SET_ROLES = 'Unit.SetRoles'
|
||||||
|
|
||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
commands = {
|
commands = {
|
||||||
self.COMMANDS.SHOW_STATS.value: Command(self.show_stats),
|
self.COMMANDS.SHOW_STATS.value: Command(self.show_stats),
|
||||||
|
self.COMMANDS.SHOW_ROLES.value: Command(self.show_roles),
|
||||||
self.COMMANDS.SHOW_HARDWARE.value: Command(self.show_hardware),
|
self.COMMANDS.SHOW_HARDWARE.value: Command(self.show_hardware),
|
||||||
self.COMMANDS.SET_ROLES.value: Command(self.set_roles),
|
self.COMMANDS.SET_ROLES.value: Command(self.set_roles),
|
||||||
}
|
}
|
||||||
@@ -21,6 +23,9 @@ class Unit(Role):
|
|||||||
def show_hardware(self):
|
def show_hardware(self):
|
||||||
return HardwareStatistics.collect().to_dict()
|
return HardwareStatistics.collect().to_dict()
|
||||||
|
|
||||||
|
def show_roles(self):
|
||||||
|
return list(self.context.self().roles)
|
||||||
|
|
||||||
def show_stats(self):
|
def show_stats(self):
|
||||||
self_node = self.context.self()
|
self_node = self.context.self()
|
||||||
return self_node.to_dict()
|
return self_node.to_dict()
|
||||||
|
|||||||
@@ -1,41 +1,73 @@
|
|||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
|
import threading
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from colony.core.logger.logging import get_logger
|
||||||
|
from colony.utils.files import atomic_write_json
|
||||||
|
|
||||||
|
|
||||||
class StateManager:
|
class StateManager:
|
||||||
class Data(Enum):
|
class Data(Enum):
|
||||||
Roles = "roles"
|
Roles = "roles"
|
||||||
|
|
||||||
|
DEFAULTS = {
|
||||||
|
Data.Roles: [],
|
||||||
|
}
|
||||||
|
|
||||||
STATE_FILE = Path("state.json")
|
STATE_FILE = Path("state.json")
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.logger = get_logger(self.__class__.__name__)
|
||||||
|
self._lock = threading.RLock()
|
||||||
self._data = {}
|
self._data = {}
|
||||||
self._load()
|
self._load()
|
||||||
|
|
||||||
def _load(self):
|
def _load(self):
|
||||||
if self.STATE_FILE.exists():
|
with self._lock:
|
||||||
with open(self.STATE_FILE, "r", encoding="utf-8") as f:
|
if self.STATE_FILE.exists():
|
||||||
try:
|
try:
|
||||||
self._data = json.load(f)
|
with open(self.STATE_FILE, "r", encoding="utf-8") as f:
|
||||||
except json.decoder.JSONDecodeError:
|
data = json.load(f)
|
||||||
self._data = {}
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("top level is not an object")
|
||||||
|
for key, default in self.DEFAULTS.items():
|
||||||
|
if key.value in data and not isinstance(data[key.value], type(default)):
|
||||||
|
raise ValueError(f"'{key.value}' has the wrong type")
|
||||||
|
self._data = data
|
||||||
|
except (ValueError, OSError) as e:
|
||||||
|
self._recover_from_corrupt_file(e)
|
||||||
|
else:
|
||||||
|
self._data = {}
|
||||||
|
self._apply_defaults()
|
||||||
|
self._save()
|
||||||
|
|
||||||
else:
|
self._apply_defaults()
|
||||||
self._data = {}
|
|
||||||
self._save()
|
|
||||||
|
|
||||||
for key in self.Data:
|
def _recover_from_corrupt_file(self, error):
|
||||||
self._data.setdefault(key.value, {})
|
backup = self.STATE_FILE.with_name(self.STATE_FILE.name + ".corrupt")
|
||||||
|
self.logger.warning(f"Could not use {self.STATE_FILE} ({error}). Keeping a copy as {backup} and resetting to defaults")
|
||||||
|
try:
|
||||||
|
shutil.copy2(self.STATE_FILE, backup)
|
||||||
|
except OSError as copy_error:
|
||||||
|
self.logger.warning(f"Could not back up {self.STATE_FILE}: {copy_error}")
|
||||||
|
self._data = {}
|
||||||
|
|
||||||
|
def _apply_defaults(self):
|
||||||
|
for key, default in self.DEFAULTS.items():
|
||||||
|
self._data.setdefault(key.value, copy.deepcopy(default))
|
||||||
|
|
||||||
def _save(self):
|
def _save(self):
|
||||||
with open(self.STATE_FILE, "w", encoding="utf-8") as f:
|
atomic_write_json(self.STATE_FILE, self._data, indent=4)
|
||||||
json.dump(self._data, f, indent=4)
|
|
||||||
|
|
||||||
def get(self, key: Data) -> Any:
|
def get(self, key: Data) -> Any:
|
||||||
return self._data[key.value]
|
with self._lock:
|
||||||
|
return copy.deepcopy(self._data[key.value])
|
||||||
|
|
||||||
def set(self, key: Data, value: Any):
|
def set(self, key: Data, value: Any):
|
||||||
self._data[key.value] = value
|
with self._lock:
|
||||||
self._save()
|
self._data[key.value] = value
|
||||||
|
self._save()
|
||||||
+29
-9
@@ -1,31 +1,50 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import threading
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from colony.utils.files import atomic_write_json
|
||||||
|
|
||||||
STORAGE_DIR = "storage"
|
STORAGE_DIR = "storage"
|
||||||
|
|
||||||
|
_write_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
class Storage:
|
class Storage:
|
||||||
def __init__(self, storage_dir):
|
def __init__(self, storage_dir):
|
||||||
self.storage_dir = storage_dir
|
self.storage_dir = storage_dir
|
||||||
|
|
||||||
os.makedirs(self.storage_dir, exist_ok=True)
|
os.makedirs(self.storage_dir, exist_ok=True)
|
||||||
|
self._root = os.path.realpath(self.storage_dir)
|
||||||
|
|
||||||
|
def _resolve(self, name, allow_root=False):
|
||||||
|
if not isinstance(name, str) or "\0" in name:
|
||||||
|
raise ValueError("Invalid storage path")
|
||||||
|
|
||||||
|
path = os.path.realpath(os.path.join(self._root, name))
|
||||||
|
if os.path.commonpath([self._root, path]) != self._root:
|
||||||
|
raise ValueError("Storage path escapes the storage directory")
|
||||||
|
if path == self._root and not allow_root:
|
||||||
|
raise ValueError("Storage path must name a file")
|
||||||
|
return path
|
||||||
|
|
||||||
def get_path(self, filename):
|
def get_path(self, filename):
|
||||||
return os.path.join(self.storage_dir, filename)
|
return self._resolve(filename)
|
||||||
|
|
||||||
def ls(self, subdir):
|
def ls(self, subdir):
|
||||||
path = os.path.join(self.storage_dir, subdir)
|
path = self._resolve(subdir, allow_root=True)
|
||||||
|
|
||||||
if not os.path.exists(path):
|
if not os.path.isdir(path):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return os.listdir(path)
|
return [name for name in os.listdir(path) if not name.startswith(".tmp-")]
|
||||||
|
|
||||||
def save(self, filename, data):
|
def save(self, filename, data):
|
||||||
path = self.get_path(filename)
|
path = self.get_path(filename)
|
||||||
|
|
||||||
with open(path, 'w', encoding='utf-8') as f:
|
with _write_lock:
|
||||||
json.dump(data, f, indent=2)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
atomic_write_json(path, data, indent=2)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@@ -44,9 +63,10 @@ class Storage:
|
|||||||
def delete(self, filename):
|
def delete(self, filename):
|
||||||
path = self.get_path(filename)
|
path = self.get_path(filename)
|
||||||
|
|
||||||
if os.path.exists(path):
|
with _write_lock:
|
||||||
os.remove(path)
|
if os.path.exists(path):
|
||||||
return True
|
os.remove(path)
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -47,4 +47,5 @@ class RoleRegistry:
|
|||||||
self.roles = roles
|
self.roles = roles
|
||||||
|
|
||||||
def get_role_class_by_name(self, name):
|
def get_role_class_by_name(self, name):
|
||||||
return self.roles.get(name).cls
|
role_stat = self.roles.get(name) if isinstance(name, str) else None
|
||||||
|
return role_stat.cls if role_stat is not None else None
|
||||||
@@ -1,32 +1,50 @@
|
|||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
|
import time
|
||||||
|
|
||||||
|
from colony.core.logger.logging import get_logger
|
||||||
|
|
||||||
|
|
||||||
class Discover:
|
class Discover:
|
||||||
DISCOVER_PORT = 28149
|
DISCOVER_PORT = 28149
|
||||||
MAGIC = b"OK"
|
MAGIC = b"OK"
|
||||||
|
CLIENT_TIMEOUT = 2
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def listen(real_port, host="0.0.0.0"):
|
def listen(real_port, host="0.0.0.0"):
|
||||||
|
logger = get_logger(Discover.__name__)
|
||||||
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
s.bind((host, Discover.DISCOVER_PORT))
|
s.bind((host, Discover.DISCOVER_PORT))
|
||||||
s.listen()
|
s.listen()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
conn, _ = s.accept()
|
|
||||||
try:
|
try:
|
||||||
data = conn.recv(2)
|
conn, _ = s.accept()
|
||||||
if data == Discover.MAGIC:
|
except OSError as e:
|
||||||
conn.sendall(struct.pack("!H", real_port))
|
logger.error(f"Accept failed: {e}")
|
||||||
finally:
|
time.sleep(1)
|
||||||
conn.close()
|
continue
|
||||||
|
|
||||||
|
with conn:
|
||||||
|
try:
|
||||||
|
conn.settimeout(Discover.CLIENT_TIMEOUT)
|
||||||
|
if conn.recv(len(Discover.MAGIC)) == Discover.MAGIC:
|
||||||
|
conn.sendall(struct.pack("!H", real_port))
|
||||||
|
except OSError as e:
|
||||||
|
logger.debug(f"Discovery request failed: {e}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def request(target_host, timeout=3):
|
def request(target_host, timeout=3):
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
with socket.create_connection((target_host, Discover.DISCOVER_PORT), timeout=timeout) as s:
|
||||||
s.settimeout(timeout)
|
s.sendall(Discover.MAGIC)
|
||||||
s.connect((target_host, Discover.DISCOVER_PORT))
|
|
||||||
s.sendall(Discover.MAGIC)
|
data = b""
|
||||||
data = s.recv(2)
|
while len(data) < 2:
|
||||||
s.close()
|
chunk = s.recv(2 - len(data))
|
||||||
|
if not chunk:
|
||||||
|
raise ConnectionError("Discovery response was cut short")
|
||||||
|
data += chunk
|
||||||
|
|
||||||
return struct.unpack("!H", data)[0]
|
return struct.unpack("!H", data)[0]
|
||||||
@@ -2,63 +2,92 @@ import json
|
|||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
import threading
|
import threading
|
||||||
from asyncio import Queue
|
import time
|
||||||
|
|
||||||
|
from colony.core.logger.logging import get_logger
|
||||||
from colony.settings import configuration
|
from colony.settings import configuration
|
||||||
|
from colony.transport.framing import send_frame, recv_frame
|
||||||
|
|
||||||
|
|
||||||
class Listener:
|
class Listener:
|
||||||
|
CLIENT_TIMEOUT = 10
|
||||||
|
|
||||||
def __init__(self, dispatcher, host=None, port=None, cert_file=None, keyfile=None):
|
def __init__(self, dispatcher, host=None, port=None, cert_file=None, keyfile=None):
|
||||||
|
self.logger = get_logger(self.__class__.__name__)
|
||||||
|
|
||||||
self.dispatcher = dispatcher
|
self.dispatcher = dispatcher
|
||||||
self.host = host or getattr(configuration.listener, "host", "0.0.0.0")
|
self.host = host if host is not None else getattr(configuration.listener, "host", "0.0.0.0")
|
||||||
self.port = port or getattr(configuration.listener, "port", 9000)
|
self.port = port if port is not None else getattr(configuration.listener, "port", 9000)
|
||||||
self.cert_file = cert_file or getattr(configuration.listener, "cert_file", "certs/server.crt")
|
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.keyfile = keyfile or getattr(configuration.listener, "keyfile", "certs/server.key")
|
||||||
self.queue = Queue()
|
|
||||||
self.running = False
|
self.running = False
|
||||||
|
self._sock = None
|
||||||
|
self._ssl_context = None
|
||||||
|
|
||||||
|
def _build_ssl_context(self):
|
||||||
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||||
|
context.load_cert_chain(certfile=self.cert_file, keyfile=self.keyfile)
|
||||||
|
context.load_verify_locations(cafile=self.cert_file)
|
||||||
|
context.verify_mode = ssl.CERT_REQUIRED
|
||||||
|
return context
|
||||||
|
|
||||||
def handle_client(self, conn):
|
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:
|
try:
|
||||||
data = tls_conn.recv(65536)
|
conn.settimeout(self.CLIENT_TIMEOUT)
|
||||||
payload = json.loads(data.decode())
|
tls_conn = self._ssl_context.wrap_socket(conn, server_side=True)
|
||||||
|
except OSError as e:
|
||||||
|
self.logger.warning(f"TLS handshake failed: {e}")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
result = self.dispatcher.dispatch(payload)
|
with tls_conn:
|
||||||
|
try:
|
||||||
|
payload = json.loads(recv_frame(tls_conn).decode())
|
||||||
|
result = self.dispatcher.dispatch(payload)
|
||||||
|
body = json.dumps({"status": "ok", "result": result}).encode()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.exception("Request failed")
|
||||||
|
body = json.dumps({"status": "error", "error": type(e).__name__}).encode()
|
||||||
|
|
||||||
response = json.dumps({
|
try:
|
||||||
"status": "ok",
|
send_frame(tls_conn, body)
|
||||||
"result": result
|
except (OSError, ValueError) as e:
|
||||||
}).encode()
|
self.logger.warning(f"Could not send response: {e}")
|
||||||
|
|
||||||
tls_conn.sendall(response)
|
def bind(self):
|
||||||
except Exception as e:
|
if self._sock is not None:
|
||||||
error = json.dumps({
|
return
|
||||||
"status": "error",
|
|
||||||
"error": str(e)
|
|
||||||
}).encode()
|
|
||||||
tls_conn.sendall(error)
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
tls_conn.close()
|
|
||||||
|
|
||||||
def start(self):
|
self._ssl_context = self._build_ssl_context()
|
||||||
self.running = True
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sock.bind((self.host, self.port))
|
try:
|
||||||
sock.listen(5)
|
sock.bind((self.host, self.port))
|
||||||
|
sock.listen(5)
|
||||||
|
except OSError:
|
||||||
|
sock.close()
|
||||||
|
raise
|
||||||
|
self.port = sock.getsockname()[1]
|
||||||
|
self._sock = sock
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.bind()
|
||||||
|
self.running = True
|
||||||
|
self.logger.info(f"Listening on {self.host}:{self.port}")
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
conn, addr = sock.accept()
|
conn, _ = self._sock.accept()
|
||||||
t = threading.Thread(target=self.handle_client, args=(conn,), name='HandleClient')
|
except OSError as e:
|
||||||
t.daemon = True
|
if not self.running:
|
||||||
t.start()
|
break
|
||||||
except KeyboardInterrupt:
|
self.logger.error(f"Accept failed: {e}")
|
||||||
break
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
sock.close()
|
t = threading.Thread(target=self.handle_client, args=(conn,), name='HandleClient')
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
self._sock.close()
|
||||||
@@ -3,36 +3,37 @@ import socket
|
|||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
from colony.settings import configuration
|
from colony.settings import configuration
|
||||||
|
from colony.transport.framing import send_frame, recv_frame
|
||||||
|
|
||||||
|
|
||||||
class Whisperer:
|
class Whisperer:
|
||||||
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
def __init__(self, host=None, port=None, cert_file=None, keyfile=None):
|
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.host = host or getattr(configuration.whisperer, "host", "127.0.0.1")
|
||||||
self.port = port or getattr(configuration.whisperer, "port", 9000)
|
self.port = port or getattr(configuration.whisperer, "port", 9000)
|
||||||
self.cert_file = cert_file or getattr(configuration.whisperer, "cert_file", None)
|
self.cert_file = cert_file or getattr(configuration.whisperer, "cert_file", "certs/server.crt")
|
||||||
self.keyfile = keyfile or getattr(configuration.whisperer, "keyfile", None)
|
self.keyfile = keyfile or getattr(configuration.whisperer, "keyfile", "certs/server.key")
|
||||||
|
|
||||||
def send(self, payload, timeout=500):
|
def _build_ssl_context(self):
|
||||||
data = json.dumps(payload).encode()
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
|
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||||
context = ssl.create_default_context()
|
|
||||||
context.check_hostname = False
|
context.check_hostname = False
|
||||||
context.verify_mode = ssl.CERT_NONE
|
context.verify_mode = ssl.CERT_REQUIRED
|
||||||
|
context.load_verify_locations(cafile=self.cert_file)
|
||||||
|
context.load_cert_chain(certfile=self.cert_file, keyfile=self.keyfile)
|
||||||
|
return context
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
def send(self, payload, timeout=DEFAULT_TIMEOUT):
|
||||||
sock.settimeout(timeout)
|
data = json.dumps(payload).encode()
|
||||||
tls_sock = context.wrap_socket(sock)
|
context = self._build_ssl_context()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tls_sock.connect((self.host, self.port))
|
with socket.create_connection((self.host, self.port), timeout=timeout) as sock:
|
||||||
tls_sock.sendall(data)
|
with context.wrap_socket(sock) as tls_sock:
|
||||||
|
send_frame(tls_sock, data)
|
||||||
response = tls_sock.recv(65536)
|
response = recv_frame(tls_sock)
|
||||||
if not response:
|
|
||||||
raise RuntimeError("Connection closed without response. ")
|
|
||||||
|
|
||||||
return json.loads(response.decode())
|
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
raise TimeoutError("No response before timeout. ")
|
raise TimeoutError("No response before timeout. ")
|
||||||
finally:
|
|
||||||
tls_sock.close()
|
return json.loads(response.decode())
|
||||||
+12
-7
@@ -3,15 +3,20 @@ import socket
|
|||||||
|
|
||||||
|
|
||||||
def get_local_subnet():
|
def get_local_subnet():
|
||||||
hostname = socket.gethostname()
|
return ipaddress.ip_network(f"{get_local_ip()}/24", strict=False)
|
||||||
local_ip = socket.gethostbyname(hostname)
|
|
||||||
|
|
||||||
subnet = ipaddress.ip_network(f"{local_ip}/24", strict=False)
|
|
||||||
|
|
||||||
return subnet
|
|
||||||
|
|
||||||
def get_local_ip():
|
def get_local_ip():
|
||||||
return socket.gethostbyname(socket.gethostname())
|
try:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
s.connect(("192.0.2.1", 9))
|
||||||
|
return s.getsockname()[0]
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
return socket.gethostbyname(socket.gethostname())
|
||||||
|
except OSError:
|
||||||
|
return "127.0.0.1"
|
||||||
|
|
||||||
def get_hostname():
|
def get_hostname():
|
||||||
return socket.gethostname()
|
return socket.gethostname()
|
||||||
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"whisperer": {
|
"whisperer": {
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 65334
|
"port": 65334,
|
||||||
|
"cert_file": "certs/server.crt",
|
||||||
|
"keyfile": "certs/server.key"
|
||||||
},
|
},
|
||||||
"listener": {
|
"listener": {
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from colony.core.logger.logging import get_logger
|
||||||
from colony.core.network.core import Network
|
from colony.core.network.core import Network
|
||||||
from colony.core.dispatcher import Dispatcher
|
from colony.core.dispatcher import Dispatcher
|
||||||
from colony.core.roles.manager import RoleManager
|
from colony.core.roles.manager import RoleManager
|
||||||
@@ -12,6 +14,7 @@ from colony.settings import configuration
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
logger = get_logger("main")
|
||||||
state_manager = StateManager()
|
state_manager = StateManager()
|
||||||
|
|
||||||
if 'Unit' not in state_manager.get(StateManager.Data.Roles):
|
if 'Unit' not in state_manager.get(StateManager.Data.Roles):
|
||||||
@@ -26,9 +29,17 @@ def main():
|
|||||||
state_manager=state_manager,
|
state_manager=state_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
threads = []
|
role_manager.initialize_roles()
|
||||||
|
|
||||||
listener = Listener(dispatcher=dispatcher)
|
listener = Listener(dispatcher=dispatcher)
|
||||||
|
try:
|
||||||
|
listener.bind()
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"Cannot listen on {listener.host}:{listener.port}: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
|
||||||
t = threading.Thread(
|
t = threading.Thread(
|
||||||
target=listener.start,
|
target=listener.start,
|
||||||
daemon=True,
|
daemon=True,
|
||||||
@@ -46,7 +57,6 @@ def main():
|
|||||||
t.start()
|
t.start()
|
||||||
threads.append(t)
|
threads.append(t)
|
||||||
|
|
||||||
role_manager.initialize_roles()
|
|
||||||
role_manager.start_all()
|
role_manager.start_all()
|
||||||
|
|
||||||
for t in threads:
|
for t in threads:
|
||||||
|
|||||||
Reference in new issue
Block a user