365 lines
15 KiB
Python
365 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Service Bus for Personal Internet Cell
|
|
Event-driven service communication and orchestration
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Dict, List, Callable, Any, Optional
|
|
from collections import defaultdict
|
|
import threading
|
|
import queue
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class EventType(Enum):
|
|
"""Event types for service communication"""
|
|
SERVICE_STARTED = "service_started"
|
|
SERVICE_STOPPED = "service_stopped"
|
|
SERVICE_RESTARTED = "service_restarted"
|
|
CONFIG_CHANGED = "config_changed"
|
|
HEALTH_CHECK = "health_check"
|
|
ERROR_OCCURRED = "error_occurred"
|
|
PEER_CONNECTED = "peer_connected"
|
|
PEER_DISCONNECTED = "peer_disconnected"
|
|
SECRET_ROTATED = "secret_rotated"
|
|
CERTIFICATE_EXPIRING = "certificate_expiring"
|
|
BACKUP_CREATED = "backup_created"
|
|
RESTORE_COMPLETED = "restore_completed"
|
|
|
|
@dataclass
|
|
class Event:
|
|
"""Event data structure"""
|
|
event_type: EventType
|
|
source: str
|
|
data: Dict[str, Any]
|
|
timestamp: datetime
|
|
event_id: str
|
|
|
|
class ServiceBus:
|
|
"""Event-driven service communication bus"""
|
|
|
|
def __init__(self):
|
|
self.event_handlers: Dict[EventType, List[Callable]] = defaultdict(list)
|
|
self.service_registry: Dict[str, Any] = {}
|
|
self.event_queue = queue.Queue()
|
|
self.running = False
|
|
self.event_loop_thread = None
|
|
self.event_history: List[Event] = []
|
|
self.max_history = 1000
|
|
|
|
# Service dependency mapping
|
|
self.service_dependencies: Dict[str, List[str]] = {
|
|
'wireguard': ['network'],
|
|
'email': ['network', 'vault'],
|
|
'calendar': ['network', 'vault'],
|
|
'files': ['network', 'vault'],
|
|
'routing': ['network', 'wireguard'],
|
|
'vault': ['network']
|
|
}
|
|
|
|
# Service lifecycle hooks
|
|
self.lifecycle_hooks: Dict[str, Dict[str, Callable]] = defaultdict(dict)
|
|
|
|
def start(self):
|
|
"""Start the service bus"""
|
|
if self.running:
|
|
return
|
|
|
|
self.running = True
|
|
self.event_loop_thread = threading.Thread(target=self._event_loop, daemon=True)
|
|
self.event_loop_thread.start()
|
|
logger.info("Service bus started")
|
|
|
|
def stop(self):
|
|
"""Stop the service bus"""
|
|
self.running = False
|
|
if self.event_loop_thread:
|
|
self.event_loop_thread.join(timeout=5)
|
|
logger.info("Service bus stopped")
|
|
|
|
def register_service(self, name: str, service: Any):
|
|
"""Register a service with the bus"""
|
|
self.service_registry[name] = service
|
|
logger.info(f"Registered service: {name}")
|
|
|
|
# Publish service started event
|
|
self.publish_event(EventType.SERVICE_STARTED, name, {
|
|
"service": name,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
|
|
def unregister_service(self, name: str):
|
|
"""Unregister a service from the bus"""
|
|
if name in self.service_registry:
|
|
del self.service_registry[name]
|
|
logger.info(f"Unregistered service: {name}")
|
|
|
|
# Publish service stopped event
|
|
self.publish_event(EventType.SERVICE_STOPPED, name, {
|
|
"service": name,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
|
|
def publish_event(self, event_type: EventType, source: str, data: Dict[str, Any]):
|
|
"""Publish an event to the bus"""
|
|
import uuid
|
|
|
|
event = Event(
|
|
event_type=event_type,
|
|
source=source,
|
|
data=data,
|
|
timestamp=datetime.utcnow(),
|
|
event_id=str(uuid.uuid4())
|
|
)
|
|
|
|
self.event_queue.put(event)
|
|
logger.debug(f"Published event: {event_type.value} from {source}")
|
|
|
|
def subscribe_to_event(self, event_type: EventType, handler: Callable[[Event], None]):
|
|
"""Subscribe to an event type"""
|
|
self.event_handlers[event_type].append(handler)
|
|
logger.info(f"Subscribed to event: {event_type.value}")
|
|
|
|
def unsubscribe_from_event(self, event_type: EventType, handler: Callable[[Event], None]):
|
|
"""Unsubscribe from an event type"""
|
|
if event_type in self.event_handlers:
|
|
try:
|
|
self.event_handlers[event_type].remove(handler)
|
|
logger.info(f"Unsubscribed from event: {event_type.value}")
|
|
except ValueError:
|
|
logger.warning(f"Handler not found for event: {event_type.value}")
|
|
|
|
def call_service(self, service_name: str, method: str, **kwargs) -> Any:
|
|
"""Call a method on a registered service"""
|
|
if service_name not in self.service_registry:
|
|
raise ValueError(f"Service {service_name} not registered")
|
|
|
|
service = self.service_registry[service_name]
|
|
if not hasattr(service, method):
|
|
raise ValueError(f"Method {method} not found on service {service_name}")
|
|
|
|
try:
|
|
result = getattr(service, method)(**kwargs)
|
|
logger.debug(f"Called {service_name}.{method}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error calling {service_name}.{method}: {e}")
|
|
self.publish_event(EventType.ERROR_OCCURRED, service_name, {
|
|
"error": str(e),
|
|
"method": method,
|
|
"service": service_name
|
|
})
|
|
raise
|
|
|
|
def get_service(self, service_name: str) -> Any:
|
|
"""Get a registered service"""
|
|
return self.service_registry.get(service_name)
|
|
|
|
def list_services(self) -> List[str]:
|
|
"""List all registered services"""
|
|
return list(self.service_registry.keys())
|
|
|
|
def add_lifecycle_hook(self, service_name: str, hook_type: str, hook: Callable):
|
|
"""Add a lifecycle hook for a service"""
|
|
self.lifecycle_hooks[service_name][hook_type] = hook
|
|
logger.info(f"Added {hook_type} hook for {service_name}")
|
|
|
|
def remove_lifecycle_hook(self, service_name: str, hook_type: str):
|
|
"""Remove a lifecycle hook for a service"""
|
|
if service_name in self.lifecycle_hooks and hook_type in self.lifecycle_hooks[service_name]:
|
|
del self.lifecycle_hooks[service_name][hook_type]
|
|
logger.info(f"Removed {hook_type} hook for {service_name}")
|
|
|
|
def orchestrate_service_start(self, service_name: str) -> bool:
|
|
"""Orchestrate starting a service with its dependencies"""
|
|
try:
|
|
# Map service names to Docker container names
|
|
service_to_container = {
|
|
'wireguard': 'cell-wireguard',
|
|
'email': 'cell-mail',
|
|
'calendar': 'cell-radicale',
|
|
'files': 'cell-webdav',
|
|
'network': 'cell-dns', # DNS is the main network service
|
|
'routing': None, # Routing is a system service, not a container
|
|
'vault': None, # Vault is part of API, not a separate container
|
|
'container': None # Container manager doesn't have its own container
|
|
}
|
|
|
|
container_name = service_to_container.get(service_name)
|
|
|
|
if container_name is None:
|
|
# For services without containers (routing, vault, container), just call their start method
|
|
if hasattr(self.service_registry[service_name], 'start'):
|
|
self.service_registry[service_name].start()
|
|
logger.info(f"Started service (no container): {service_name}")
|
|
return True
|
|
|
|
# For services with containers, start the Docker container
|
|
if 'container' in self.service_registry:
|
|
container_manager = self.service_registry['container']
|
|
success = container_manager.start_container(container_name)
|
|
if success:
|
|
logger.info(f"Started container {container_name} for service {service_name}")
|
|
return True
|
|
else:
|
|
logger.error(f"Failed to start container {container_name} for service {service_name}")
|
|
return False
|
|
else:
|
|
logger.error("Container manager not available")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error orchestrating start of {service_name}: {e}")
|
|
return False
|
|
|
|
def orchestrate_service_stop(self, service_name: str) -> bool:
|
|
"""Orchestrate stopping a service"""
|
|
try:
|
|
# Map service names to Docker container names
|
|
service_to_container = {
|
|
'wireguard': 'cell-wireguard',
|
|
'email': 'cell-mail',
|
|
'calendar': 'cell-radicale',
|
|
'files': 'cell-webdav',
|
|
'network': 'cell-dns', # DNS is the main network service
|
|
'routing': None, # Routing is a system service, not a container
|
|
'vault': None, # Vault is part of API, not a separate container
|
|
'container': None # Container manager doesn't have its own container
|
|
}
|
|
|
|
container_name = service_to_container.get(service_name)
|
|
|
|
if container_name is None:
|
|
# For services without containers (routing, vault, container), just call their stop method
|
|
if hasattr(self.service_registry[service_name], 'stop'):
|
|
self.service_registry[service_name].stop()
|
|
logger.info(f"Stopped service (no container): {service_name}")
|
|
return True
|
|
|
|
# For services with containers, stop the Docker container
|
|
if 'container' in self.service_registry:
|
|
container_manager = self.service_registry['container']
|
|
success = container_manager.stop_container(container_name)
|
|
if success:
|
|
logger.info(f"Stopped container {container_name} for service {service_name}")
|
|
return True
|
|
else:
|
|
logger.error(f"Failed to stop container {container_name} for service {service_name}")
|
|
return False
|
|
else:
|
|
logger.error("Container manager not available")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error orchestrating stop of {service_name}: {e}")
|
|
return False
|
|
|
|
def orchestrate_service_restart(self, service_name: str) -> bool:
|
|
"""Orchestrate restarting a service"""
|
|
try:
|
|
if self.orchestrate_service_stop(service_name):
|
|
return self.orchestrate_service_start(service_name)
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"Error orchestrating restart of {service_name}: {e}")
|
|
return False
|
|
|
|
def get_event_history(self, event_type: Optional[EventType] = None,
|
|
source: Optional[str] = None, limit: int = 100) -> List[Event]:
|
|
"""Get event history with optional filtering"""
|
|
events = self.event_history
|
|
|
|
if event_type:
|
|
events = [e for e in events if e.event_type == event_type]
|
|
|
|
if source:
|
|
events = [e for e in events if e.source == source]
|
|
|
|
return events[-limit:]
|
|
|
|
def clear_event_history(self):
|
|
"""Clear event history"""
|
|
self.event_history.clear()
|
|
logger.info("Event history cleared")
|
|
|
|
def _event_loop(self):
|
|
"""Main event processing loop"""
|
|
while self.running:
|
|
try:
|
|
# Get event from queue with timeout
|
|
event = self.event_queue.get(timeout=1)
|
|
|
|
# Add to history
|
|
self.event_history.append(event)
|
|
if len(self.event_history) > self.max_history:
|
|
self.event_history.pop(0)
|
|
|
|
# Process event handlers
|
|
handlers = self.event_handlers.get(event.event_type, [])
|
|
for handler in handlers:
|
|
try:
|
|
handler(event)
|
|
except Exception as e:
|
|
logger.error(f"Error in event handler for {event.event_type.value}: {e}")
|
|
|
|
# Mark task as done
|
|
self.event_queue.task_done()
|
|
|
|
except queue.Empty:
|
|
continue
|
|
except Exception as e:
|
|
logger.error(f"Error in event loop: {e}")
|
|
|
|
def get_service_dependencies(self, service_name: str) -> List[str]:
|
|
"""Get dependencies for a service"""
|
|
return self.service_dependencies.get(service_name, [])
|
|
|
|
def add_service_dependency(self, service_name: str, dependency: str):
|
|
"""Add a dependency for a service"""
|
|
if service_name not in self.service_dependencies:
|
|
self.service_dependencies[service_name] = []
|
|
self.service_dependencies[service_name].append(dependency)
|
|
logger.info(f"Added dependency {dependency} for service {service_name}")
|
|
|
|
def remove_service_dependency(self, service_name: str, dependency: str):
|
|
"""Remove a dependency for a service"""
|
|
if service_name in self.service_dependencies:
|
|
try:
|
|
self.service_dependencies[service_name].remove(dependency)
|
|
logger.info(f"Removed dependency {dependency} for service {service_name}")
|
|
except ValueError:
|
|
logger.warning(f"Dependency {dependency} not found for service {service_name}")
|
|
|
|
def get_service_status_summary(self) -> Dict[str, Any]:
|
|
"""Get summary of all service statuses"""
|
|
summary = {
|
|
"total_services": len(self.service_registry),
|
|
"services": {},
|
|
"event_count": len(self.event_history),
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
for service_name, service in self.service_registry.items():
|
|
try:
|
|
if hasattr(service, 'get_status'):
|
|
status = service.get_status()
|
|
else:
|
|
status = {"status": "unknown"}
|
|
|
|
summary["services"][service_name] = {
|
|
"status": status,
|
|
"dependencies": self.service_dependencies.get(service_name, [])
|
|
}
|
|
except Exception as e:
|
|
summary["services"][service_name] = {
|
|
"status": {"error": str(e)},
|
|
"dependencies": self.service_dependencies.get(service_name, [])
|
|
}
|
|
|
|
return summary |