Files
pic/api/service_bus.py
T
Constantin 2277b11563 init
2025-09-12 23:04:52 +03:00

332 lines
13 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:
# Check dependencies
dependencies = self.service_dependencies.get(service_name, [])
for dep in dependencies:
if dep not in self.service_registry:
logger.warning(f"Service {service_name} depends on {dep} which is not registered")
return False
# Run pre-start hooks
if service_name in self.lifecycle_hooks and 'pre_start' in self.lifecycle_hooks[service_name]:
self.lifecycle_hooks[service_name]['pre_start']()
# Start the service
if hasattr(self.service_registry[service_name], 'start'):
self.service_registry[service_name].start()
# Run post-start hooks
if service_name in self.lifecycle_hooks and 'post_start' in self.lifecycle_hooks[service_name]:
self.lifecycle_hooks[service_name]['post_start']()
logger.info(f"Orchestrated start of service: {service_name}")
return True
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:
# Run pre-stop hooks
if service_name in self.lifecycle_hooks and 'pre_stop' in self.lifecycle_hooks[service_name]:
self.lifecycle_hooks[service_name]['pre_stop']()
# Stop the service
if hasattr(self.service_registry[service_name], 'stop'):
self.service_registry[service_name].stop()
# Run post-stop hooks
if service_name in self.lifecycle_hooks and 'post_stop' in self.lifecycle_hooks[service_name]:
self.lifecycle_hooks[service_name]['post_stop']()
logger.info(f"Orchestrated stop of service: {service_name}")
return True
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