wip: make work Services Status
This commit is contained in:
@@ -80,6 +80,10 @@ start-wg:
|
|||||||
@echo "Starting WireGuard service..."
|
@echo "Starting WireGuard service..."
|
||||||
docker-compose up -d wireguard
|
docker-compose up -d wireguard
|
||||||
|
|
||||||
|
start-webui:
|
||||||
|
@echo "Starting WebUi service..."
|
||||||
|
docker-compose up -d webui
|
||||||
|
|
||||||
# Maintenance commands
|
# Maintenance commands
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning up containers and volumes..."
|
@echo "Cleaning up containers and volumes..."
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ python api/app.py
|
|||||||
python api/test_enhanced_api.py
|
python api/test_enhanced_api.py
|
||||||
|
|
||||||
# Start frontend (if available)
|
# Start frontend (if available)
|
||||||
cd webui && npm install && npm run dev
|
cd webui && bun install && npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Production Deployment**
|
### **Production Deployment**
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ python api/app.py
|
|||||||
python api/test_enhanced_api.py
|
python api/test_enhanced_api.py
|
||||||
|
|
||||||
# Start frontend (if available)
|
# Start frontend (if available)
|
||||||
cd webui && npm install && npm run dev
|
cd webui && bun install && npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Service Development**
|
### **Service Development**
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ RUN apt-get update && apt-get install -y \
|
|||||||
wireguard-tools \
|
wireguard-tools \
|
||||||
iptables \
|
iptables \
|
||||||
curl \
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
gnupg \
|
||||||
|
lsb-release \
|
||||||
|
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y docker-ce-cli \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy requirements first for better caching
|
# Copy requirements first for better caching
|
||||||
|
|||||||
+56
-31
@@ -102,9 +102,9 @@ CORS(app)
|
|||||||
app.config['DEVELOPMENT_MODE'] = True # Set to True for development, False for production
|
app.config['DEVELOPMENT_MODE'] = True # Set to True for development, False for production
|
||||||
|
|
||||||
# Initialize enhanced components
|
# Initialize enhanced components
|
||||||
config_manager = ConfigManager()
|
config_manager = ConfigManager(config_file='./config/cell_config.json', data_dir='./data')
|
||||||
service_bus = ServiceBus()
|
service_bus = ServiceBus()
|
||||||
log_manager = LogManager()
|
log_manager = LogManager(log_dir='./data/logs')
|
||||||
|
|
||||||
# Initialize service loggers
|
# Initialize service loggers
|
||||||
service_log_configs = {
|
service_log_configs = {
|
||||||
@@ -150,17 +150,17 @@ def log_request(response):
|
|||||||
def clear_log_context(exc):
|
def clear_log_context(exc):
|
||||||
request_context.set({})
|
request_context.set({})
|
||||||
|
|
||||||
# Initialize managers
|
# Initialize managers with proper directories
|
||||||
network_manager = NetworkManager()
|
network_manager = NetworkManager(data_dir='./data', config_dir='./config')
|
||||||
wireguard_manager = WireGuardManager()
|
wireguard_manager = WireGuardManager(data_dir='./data', config_dir='./config')
|
||||||
peer_registry = PeerRegistry()
|
peer_registry = PeerRegistry(data_dir='./data', config_dir='./config')
|
||||||
email_manager = EmailManager()
|
email_manager = EmailManager(data_dir='./data', config_dir='./config')
|
||||||
calendar_manager = CalendarManager()
|
calendar_manager = CalendarManager(data_dir='./data', config_dir='./config')
|
||||||
file_manager = FileManager()
|
file_manager = FileManager(data_dir='./data', config_dir='./config')
|
||||||
routing_manager = RoutingManager()
|
routing_manager = RoutingManager(data_dir='./data', config_dir='./config')
|
||||||
cell_manager = CellManager()
|
cell_manager = CellManager(data_dir='./data', config_dir='./config')
|
||||||
app.vault_manager = VaultManager()
|
app.vault_manager = VaultManager(data_dir='./data', config_dir='./config')
|
||||||
container_manager = ContainerManager()
|
container_manager = ContainerManager(data_dir='./data', config_dir='./config')
|
||||||
|
|
||||||
# Register services with service bus
|
# Register services with service bus
|
||||||
service_bus.register_service('network', network_manager)
|
service_bus.register_service('network', network_manager)
|
||||||
@@ -686,8 +686,8 @@ def test_network():
|
|||||||
def get_wireguard_keys():
|
def get_wireguard_keys():
|
||||||
"""Get WireGuard keys."""
|
"""Get WireGuard keys."""
|
||||||
try:
|
try:
|
||||||
keys = wireguard_manager.get_keys()
|
# For now, return empty keys - this would need to be implemented
|
||||||
return jsonify(keys)
|
return jsonify({"error": "Not implemented yet"}), 501
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting WireGuard keys: {e}")
|
logger.error(f"Error getting WireGuard keys: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -697,7 +697,9 @@ def generate_peer_keys():
|
|||||||
"""Generate peer keys."""
|
"""Generate peer keys."""
|
||||||
try:
|
try:
|
||||||
data = request.get_json(silent=True)
|
data = request.get_json(silent=True)
|
||||||
result = wireguard_manager.generate_peer_keys(data)
|
if data is None or 'peer_name' not in data:
|
||||||
|
return jsonify({"error": "Missing peer_name"}), 400
|
||||||
|
result = wireguard_manager.generate_peer_keys(data['peer_name'])
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error generating peer keys: {e}")
|
logger.error(f"Error generating peer keys: {e}")
|
||||||
@@ -707,8 +709,8 @@ def generate_peer_keys():
|
|||||||
def get_wireguard_config():
|
def get_wireguard_config():
|
||||||
"""Get WireGuard configuration."""
|
"""Get WireGuard configuration."""
|
||||||
try:
|
try:
|
||||||
config = wireguard_manager.get_config()
|
# For now, return empty config - this would need to be implemented
|
||||||
return jsonify(config)
|
return jsonify({"error": "Not implemented yet"}), 501
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting WireGuard config: {e}")
|
logger.error(f"Error getting WireGuard config: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -717,7 +719,7 @@ def get_wireguard_config():
|
|||||||
def get_wireguard_peers():
|
def get_wireguard_peers():
|
||||||
"""Get WireGuard peers."""
|
"""Get WireGuard peers."""
|
||||||
try:
|
try:
|
||||||
peers = wireguard_manager.get_peers()
|
peers = wireguard_manager.get_wireguard_peers()
|
||||||
return jsonify(peers)
|
return jsonify(peers)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting WireGuard peers: {e}")
|
logger.error(f"Error getting WireGuard peers: {e}")
|
||||||
@@ -728,8 +730,22 @@ def add_wireguard_peer():
|
|||||||
"""Add WireGuard peer."""
|
"""Add WireGuard peer."""
|
||||||
try:
|
try:
|
||||||
data = request.get_json(silent=True)
|
data = request.get_json(silent=True)
|
||||||
result = wireguard_manager.add_peer(data)
|
if data is None:
|
||||||
return jsonify(result)
|
return jsonify({"error": "No data provided"}), 400
|
||||||
|
|
||||||
|
required_fields = ['name', 'public_key', 'allowed_ips']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in data:
|
||||||
|
return jsonify({"error": f"Missing required field: {field}"}), 400
|
||||||
|
|
||||||
|
result = wireguard_manager.add_wireguard_peer(
|
||||||
|
name=data['name'],
|
||||||
|
public_key=data['public_key'],
|
||||||
|
allowed_ips=data['allowed_ips'],
|
||||||
|
endpoint=data.get('endpoint', ''),
|
||||||
|
persistent_keepalive=data.get('persistent_keepalive', 25)
|
||||||
|
)
|
||||||
|
return jsonify({"success": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding WireGuard peer: {e}")
|
logger.error(f"Error adding WireGuard peer: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -739,8 +755,11 @@ def remove_wireguard_peer():
|
|||||||
"""Remove WireGuard peer."""
|
"""Remove WireGuard peer."""
|
||||||
try:
|
try:
|
||||||
data = request.get_json(silent=True)
|
data = request.get_json(silent=True)
|
||||||
result = wireguard_manager.remove_peer(data)
|
if data is None or 'name' not in data:
|
||||||
return jsonify(result)
|
return jsonify({"error": "Missing peer name"}), 400
|
||||||
|
|
||||||
|
result = wireguard_manager.remove_wireguard_peer(data['name'])
|
||||||
|
return jsonify({"success": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error removing WireGuard peer: {e}")
|
logger.error(f"Error removing WireGuard peer: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -772,8 +791,11 @@ def update_peer_ip():
|
|||||||
"""Update peer IP."""
|
"""Update peer IP."""
|
||||||
try:
|
try:
|
||||||
data = request.get_json(silent=True)
|
data = request.get_json(silent=True)
|
||||||
result = wireguard_manager.update_peer_ip(data)
|
if data is None or 'name' not in data or 'ip' not in data:
|
||||||
return jsonify(result)
|
return jsonify({"error": "Missing peer name or IP"}), 400
|
||||||
|
|
||||||
|
# For now, return not implemented - this would need to be implemented
|
||||||
|
return jsonify({"error": "Not implemented yet"}), 501
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error updating peer IP: {e}")
|
logger.error(f"Error updating peer IP: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -782,10 +804,11 @@ def update_peer_ip():
|
|||||||
def get_peer_config():
|
def get_peer_config():
|
||||||
try:
|
try:
|
||||||
data = request.get_json(silent=True)
|
data = request.get_json(silent=True)
|
||||||
if data is None:
|
if data is None or 'name' not in data:
|
||||||
return jsonify({"error": "No data provided"}), 400
|
return jsonify({"error": "Missing peer name"}), 400
|
||||||
result = wireguard_manager.get_peer_config(data)
|
|
||||||
return jsonify(result)
|
# For now, return not implemented - this would need to be implemented
|
||||||
|
return jsonify({"error": "Not implemented yet"}), 501
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting peer config: {e}")
|
logger.error(f"Error getting peer config: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -883,7 +906,8 @@ def update_peer_ip_registry(peer_name):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
|
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
|
||||||
try:
|
try:
|
||||||
wireguard_manager.update_peer_ip(peer_name, new_ip)
|
# For now, skip WireGuard update - method not implemented
|
||||||
|
logger.warning(f"WireGuardManager update_peer_ip not implemented yet")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"WireGuardManager update_peer_ip failed: {e}")
|
logger.warning(f"WireGuardManager update_peer_ip failed: {e}")
|
||||||
return jsonify({"message": f"IP update received for {peer_name}"})
|
return jsonify({"message": f"IP update received for {peer_name}"})
|
||||||
@@ -912,7 +936,8 @@ def ip_update():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
|
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
|
||||||
try:
|
try:
|
||||||
wireguard_manager.update_peer_ip(peer_name, new_ip)
|
# For now, skip WireGuard update - method not implemented
|
||||||
|
logger.warning(f"WireGuardManager update_peer_ip not implemented yet")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"WireGuardManager update_peer_ip failed: {e}")
|
logger.warning(f"WireGuardManager update_peer_ip failed: {e}")
|
||||||
return jsonify({"message": f"IP update received for {peer_name}"})
|
return jsonify({"message": f"IP update received for {peer_name}"})
|
||||||
|
|||||||
+14
-3
@@ -35,10 +35,11 @@ class CalendarManager(BaseServiceManager):
|
|||||||
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
||||||
|
|
||||||
if is_docker:
|
if is_docker:
|
||||||
# Return positive status when running in Docker
|
# Check if calendar container is actually running
|
||||||
|
container_running = self._check_calendar_container_status()
|
||||||
status = {
|
status = {
|
||||||
'running': True,
|
'running': container_running,
|
||||||
'status': 'online',
|
'status': 'online' if container_running else 'offline',
|
||||||
'users_count': 0,
|
'users_count': 0,
|
||||||
'calendars_count': 0,
|
'calendars_count': 0,
|
||||||
'events_count': 0,
|
'events_count': 0,
|
||||||
@@ -97,6 +98,16 @@ class CalendarManager(BaseServiceManager):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _check_calendar_container_status(self) -> bool:
|
||||||
|
"""Check if calendar Docker container is running"""
|
||||||
|
try:
|
||||||
|
import docker
|
||||||
|
client = docker.from_env()
|
||||||
|
containers = client.containers.list(filters={'name': 'cell-radicale'})
|
||||||
|
return len(containers) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def _test_service_connectivity(self) -> Dict[str, Any]:
|
def _test_service_connectivity(self) -> Dict[str, Any]:
|
||||||
"""Test calendar service connectivity"""
|
"""Test calendar service connectivity"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
+383
-382
@@ -1,383 +1,384 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Configuration Manager for Personal Internet Cell
|
Configuration Manager for Personal Internet Cell
|
||||||
Centralized configuration management for all services
|
Centralized configuration management for all services
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
import shutil
|
import shutil
|
||||||
import hashlib
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
"""Centralized configuration management for all services (unified config)"""
|
"""Centralized configuration management for all services (unified config)"""
|
||||||
|
|
||||||
def __init__(self, config_file: str = '/app/config/cell_config.json', data_dir: str = '/app/data'):
|
def __init__(self, config_file: str = '/app/config/cell_config.json', data_dir: str = '/app/data'):
|
||||||
config_file = Path(config_file)
|
config_file = Path(config_file)
|
||||||
if config_file.is_dir():
|
if config_file.is_dir():
|
||||||
config_file = config_file / 'cell_config.json'
|
config_file = config_file / 'cell_config.json'
|
||||||
print(f"[DEBUG] ConfigManager.__init__: config_file = {config_file}")
|
print(f"[DEBUG] ConfigManager.__init__: config_file = {config_file}")
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
self.data_dir = Path(data_dir)
|
self.data_dir = Path(data_dir)
|
||||||
self.backup_dir = self.data_dir / 'config_backups'
|
self.backup_dir = self.data_dir / 'config_backups'
|
||||||
self.secrets_file = self.config_file.parent / 'secrets.yaml'
|
self.secrets_file = self.config_file.parent / 'secrets.yaml'
|
||||||
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.service_schemas = self._load_service_schemas()
|
self.service_schemas = self._load_service_schemas()
|
||||||
self.configs = self._load_all_configs()
|
self.configs = self._load_all_configs()
|
||||||
|
|
||||||
def _load_service_schemas(self) -> Dict[str, Dict]:
|
def _load_service_schemas(self) -> Dict[str, Dict]:
|
||||||
"""Load configuration schemas for all services"""
|
"""Load configuration schemas for all services"""
|
||||||
return {
|
return {
|
||||||
'network': {
|
'network': {
|
||||||
'required': ['dns_port', 'dhcp_range', 'ntp_servers'],
|
'required': ['dns_port', 'dhcp_range', 'ntp_servers'],
|
||||||
'optional': ['dns_zones', 'dhcp_reservations'],
|
'optional': ['dns_zones', 'dhcp_reservations'],
|
||||||
'types': {
|
'types': {
|
||||||
'dns_port': int,
|
'dns_port': int,
|
||||||
'dhcp_range': str,
|
'dhcp_range': str,
|
||||||
'ntp_servers': list
|
'ntp_servers': list
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'wireguard': {
|
'wireguard': {
|
||||||
'required': ['port', 'private_key', 'address'],
|
'required': ['port', 'private_key', 'address'],
|
||||||
'optional': ['peers', 'allowed_ips'],
|
'optional': ['peers', 'allowed_ips'],
|
||||||
'types': {
|
'types': {
|
||||||
'port': int,
|
'port': int,
|
||||||
'private_key': str,
|
'private_key': str,
|
||||||
'address': str
|
'address': str
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'email': {
|
'email': {
|
||||||
'required': ['domain', 'smtp_port', 'imap_port'],
|
'required': ['domain', 'smtp_port', 'imap_port'],
|
||||||
'optional': ['users', 'ssl_cert', 'ssl_key'],
|
'optional': ['users', 'ssl_cert', 'ssl_key'],
|
||||||
'types': {
|
'types': {
|
||||||
'smtp_port': int,
|
'smtp_port': int,
|
||||||
'imap_port': int,
|
'imap_port': int,
|
||||||
'domain': str
|
'domain': str
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'calendar': {
|
'calendar': {
|
||||||
'required': ['port', 'data_dir'],
|
'required': ['port', 'data_dir'],
|
||||||
'optional': ['users', 'calendars'],
|
'optional': ['users', 'calendars'],
|
||||||
'types': {
|
'types': {
|
||||||
'port': int,
|
'port': int,
|
||||||
'data_dir': str
|
'data_dir': str
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'files': {
|
'files': {
|
||||||
'required': ['port', 'data_dir'],
|
'required': ['port', 'data_dir'],
|
||||||
'optional': ['users', 'quota'],
|
'optional': ['users', 'quota'],
|
||||||
'types': {
|
'types': {
|
||||||
'port': int,
|
'port': int,
|
||||||
'data_dir': str,
|
'data_dir': str,
|
||||||
'quota': int
|
'quota': int
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'routing': {
|
'routing': {
|
||||||
'required': ['nat_enabled', 'firewall_enabled'],
|
'required': ['nat_enabled', 'firewall_enabled'],
|
||||||
'optional': ['nat_rules', 'firewall_rules', 'peer_routes'],
|
'optional': ['nat_rules', 'firewall_rules', 'peer_routes'],
|
||||||
'types': {
|
'types': {
|
||||||
'nat_enabled': bool,
|
'nat_enabled': bool,
|
||||||
'firewall_enabled': bool
|
'firewall_enabled': bool
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'vault': {
|
'vault': {
|
||||||
'required': ['ca_configured', 'fernet_configured'],
|
'required': ['ca_configured', 'fernet_configured'],
|
||||||
'optional': ['certificates', 'trusted_keys'],
|
'optional': ['certificates', 'trusted_keys'],
|
||||||
'types': {
|
'types': {
|
||||||
'ca_configured': bool,
|
'ca_configured': bool,
|
||||||
'fernet_configured': bool
|
'fernet_configured': bool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def _load_all_configs(self) -> Dict[str, Dict]:
|
def _load_all_configs(self) -> Dict[str, Dict]:
|
||||||
"""Load all existing service configurations"""
|
"""Load all existing service configurations"""
|
||||||
if self.config_file.exists():
|
if self.config_file.exists():
|
||||||
try:
|
try:
|
||||||
with open(self.config_file, 'r') as f:
|
with open(self.config_file, 'r') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading unified config: {e}")
|
logger.error(f"Error loading unified config: {e}")
|
||||||
return {}
|
return {}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _save_all_configs(self):
|
def _save_all_configs(self):
|
||||||
"""Save all service configurations to the unified config file"""
|
"""Save all service configurations to the unified config file"""
|
||||||
with open(self.config_file, 'w') as f:
|
with open(self.config_file, 'w') as f:
|
||||||
json.dump(self.configs, f, indent=2)
|
json.dump(self.configs, f, indent=2)
|
||||||
|
|
||||||
def get_service_config(self, service: str) -> Dict[str, Any]:
|
def get_service_config(self, service: str) -> Dict[str, Any]:
|
||||||
"""Get configuration for a specific service"""
|
"""Get configuration for a specific service"""
|
||||||
if service not in self.service_schemas:
|
if service not in self.service_schemas:
|
||||||
raise ValueError(f"Unknown service: {service}")
|
raise ValueError(f"Unknown service: {service}")
|
||||||
return self.configs.get(service, {})
|
return self.configs.get(service, {})
|
||||||
|
|
||||||
def update_service_config(self, service: str, config: Dict[str, Any]) -> bool:
|
def update_service_config(self, service: str, config: Dict[str, Any]) -> bool:
|
||||||
"""Update configuration for a specific service"""
|
"""Update configuration for a specific service"""
|
||||||
if service not in self.service_schemas:
|
if service not in self.service_schemas:
|
||||||
raise ValueError(f"Unknown service: {service}")
|
raise ValueError(f"Unknown service: {service}")
|
||||||
try:
|
try:
|
||||||
# Validate configuration
|
# Validate configuration
|
||||||
validation = self.validate_config(service, config)
|
validation = self.validate_config(service, config)
|
||||||
if not validation['valid']:
|
if not validation['valid']:
|
||||||
logger.error(f"Invalid config for {service}: {validation['errors']}")
|
logger.error(f"Invalid config for {service}: {validation['errors']}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Backup current config
|
# Backup current config
|
||||||
self._backup_service_config(service)
|
self._backup_service_config(service)
|
||||||
|
|
||||||
# Update configuration
|
# Update configuration
|
||||||
self.configs[service] = config
|
self.configs[service] = config
|
||||||
self._save_all_configs()
|
self._save_all_configs()
|
||||||
|
|
||||||
logger.info(f"Updated configuration for {service}")
|
logger.info(f"Updated configuration for {service}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error updating config for {service}: {e}")
|
logger.error(f"Error updating config for {service}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def validate_config(self, service: str, config: Dict[str, Any]) -> Dict[str, Any]:
|
def validate_config(self, service: str, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Validate configuration for a service"""
|
"""Validate configuration for a service"""
|
||||||
if service not in self.service_schemas:
|
if service not in self.service_schemas:
|
||||||
return {
|
return {
|
||||||
"valid": False,
|
"valid": False,
|
||||||
"errors": [f"Unknown service: {service}"],
|
"errors": [f"Unknown service: {service}"],
|
||||||
"warnings": []
|
"warnings": []
|
||||||
}
|
}
|
||||||
|
|
||||||
schema = self.service_schemas[service]
|
schema = self.service_schemas[service]
|
||||||
errors = []
|
errors = []
|
||||||
warnings = []
|
warnings = []
|
||||||
|
|
||||||
# Check required fields
|
# Check required fields
|
||||||
for field in schema['required']:
|
for field in schema['required']:
|
||||||
if field not in config:
|
if field not in config:
|
||||||
errors.append(f"Missing required field: {field}")
|
errors.append(f"Missing required field: {field}")
|
||||||
elif field in schema['types']:
|
elif field in schema['types']:
|
||||||
expected_type = schema['types'][field]
|
expected_type = schema['types'][field]
|
||||||
if not isinstance(config[field], expected_type):
|
if not isinstance(config[field], expected_type):
|
||||||
errors.append(f"Field {field} must be of type {expected_type.__name__}")
|
errors.append(f"Field {field} must be of type {expected_type.__name__}")
|
||||||
|
|
||||||
# Check optional fields
|
# Check optional fields
|
||||||
for field in schema['optional']:
|
for field in schema['optional']:
|
||||||
if field in config and field in schema['types']:
|
if field in config and field in schema['types']:
|
||||||
expected_type = schema['types'][field]
|
expected_type = schema['types'][field]
|
||||||
if not isinstance(config[field], expected_type):
|
if not isinstance(config[field], expected_type):
|
||||||
warnings.append(f"Field {field} should be of type {expected_type.__name__}")
|
warnings.append(f"Field {field} should be of type {expected_type.__name__}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"valid": len(errors) == 0,
|
"valid": len(errors) == 0,
|
||||||
"errors": errors,
|
"errors": errors,
|
||||||
"warnings": warnings
|
"warnings": warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
def backup_config(self) -> str:
|
def backup_config(self) -> str:
|
||||||
"""Create a backup of all configurations"""
|
"""Create a backup of all configurations"""
|
||||||
try:
|
try:
|
||||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
backup_id = f"backup_{timestamp}"
|
backup_id = f"backup_{timestamp}"
|
||||||
backup_path = self.backup_dir / backup_id
|
backup_path = self.backup_dir / backup_id
|
||||||
|
|
||||||
# Create backup directory
|
# Create backup directory
|
||||||
backup_path.mkdir(parents=True, exist_ok=True)
|
backup_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Copy all config files
|
# Copy all config files
|
||||||
shutil.copy2(self.config_file, backup_path / 'cell_config.json')
|
shutil.copy2(self.config_file, backup_path / 'cell_config.json')
|
||||||
|
|
||||||
# Copy secrets file if it exists
|
# Copy secrets file if it exists
|
||||||
if self.secrets_file.exists():
|
if self.secrets_file.exists():
|
||||||
shutil.copy2(self.secrets_file, backup_path / 'secrets.yaml')
|
shutil.copy2(self.secrets_file, backup_path / 'secrets.yaml')
|
||||||
|
|
||||||
# Create backup manifest
|
# Create backup manifest
|
||||||
manifest = {
|
manifest = {
|
||||||
"backup_id": backup_id,
|
"backup_id": backup_id,
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
"services": list(self.service_schemas.keys()),
|
"services": list(self.service_schemas.keys()),
|
||||||
"files": [f.name for f in backup_path.iterdir()]
|
"files": [f.name for f in backup_path.iterdir()]
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(backup_path / 'manifest.json', 'w') as f:
|
with open(backup_path / 'manifest.json', 'w') as f:
|
||||||
json.dump(manifest, f, indent=2)
|
json.dump(manifest, f, indent=2)
|
||||||
|
|
||||||
logger.info(f"Created configuration backup: {backup_id}")
|
logger.info(f"Created configuration backup: {backup_id}")
|
||||||
return backup_id
|
return backup_id
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating backup: {e}")
|
logger.error(f"Error creating backup: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def restore_config(self, backup_id: str) -> bool:
|
def restore_config(self, backup_id: str) -> bool:
|
||||||
"""Restore configuration from backup"""
|
"""Restore configuration from backup"""
|
||||||
try:
|
try:
|
||||||
backup_path = self.backup_dir / backup_id
|
backup_path = self.backup_dir / backup_id
|
||||||
if not backup_path.exists():
|
if not backup_path.exists():
|
||||||
raise ValueError(f"Backup {backup_id} not found")
|
raise ValueError(f"Backup {backup_id} not found")
|
||||||
# Read manifest
|
# Read manifest
|
||||||
manifest_file = backup_path / 'manifest.json'
|
manifest_file = backup_path / 'manifest.json'
|
||||||
if not manifest_file.exists():
|
if not manifest_file.exists():
|
||||||
raise ValueError(f"Backup manifest not found")
|
raise ValueError(f"Backup manifest not found")
|
||||||
with open(manifest_file, 'r') as f:
|
with open(manifest_file, 'r') as f:
|
||||||
manifest = json.load(f)
|
manifest = json.load(f)
|
||||||
# Restore config files
|
# Restore config files
|
||||||
config_backup = backup_path / 'cell_config.json'
|
config_backup = backup_path / 'cell_config.json'
|
||||||
if config_backup.exists():
|
if config_backup.exists():
|
||||||
shutil.copy2(config_backup, self.config_file)
|
shutil.copy2(config_backup, self.config_file)
|
||||||
# Restore secrets file if it exists
|
# Restore secrets file if it exists
|
||||||
secrets_backup = backup_path / 'secrets.yaml'
|
secrets_backup = backup_path / 'secrets.yaml'
|
||||||
if secrets_backup.exists():
|
if secrets_backup.exists():
|
||||||
shutil.copy2(secrets_backup, self.secrets_file)
|
shutil.copy2(secrets_backup, self.secrets_file)
|
||||||
# Reload configurations
|
# Reload configurations
|
||||||
self.configs = self._load_all_configs()
|
self.configs = self._load_all_configs()
|
||||||
# Ensure all configs have required fields
|
# Ensure all configs have required fields
|
||||||
for service, schema in self.service_schemas.items():
|
for service, schema in self.service_schemas.items():
|
||||||
config = self.configs.get(service, {})
|
config = self.configs.get(service, {})
|
||||||
for field in schema['required']:
|
for field in schema['required']:
|
||||||
if field not in config:
|
if field not in config:
|
||||||
# Set a default value based on type
|
# Set a default value based on type
|
||||||
t = schema['types'][field]
|
t = schema['types'][field]
|
||||||
if t is int:
|
if t is int:
|
||||||
config[field] = 0
|
config[field] = 0
|
||||||
elif t is str:
|
elif t is str:
|
||||||
config[field] = ''
|
config[field] = ''
|
||||||
elif t is list:
|
elif t is list:
|
||||||
config[field] = []
|
config[field] = []
|
||||||
elif t is bool:
|
elif t is bool:
|
||||||
config[field] = False
|
config[field] = False
|
||||||
self.configs[service] = config
|
self.configs[service] = config
|
||||||
# Write back to file
|
|
||||||
self._save_all_configs()
|
# Write back to file
|
||||||
logger.info(f"Restored configuration from backup: {backup_id}")
|
self._save_all_configs()
|
||||||
return True
|
logger.info(f"Restored configuration from backup: {backup_id}")
|
||||||
except Exception as e:
|
return True
|
||||||
logger.error(f"Error restoring backup {backup_id}: {e}")
|
except Exception as e:
|
||||||
return False
|
logger.error(f"Error restoring backup {backup_id}: {e}")
|
||||||
|
return False
|
||||||
def list_backups(self) -> List[Dict[str, Any]]:
|
|
||||||
"""List all available backups"""
|
def list_backups(self) -> List[Dict[str, Any]]:
|
||||||
backups = []
|
"""List all available backups"""
|
||||||
for backup_dir in self.backup_dir.iterdir():
|
backups = []
|
||||||
if backup_dir.is_dir():
|
for backup_dir in self.backup_dir.iterdir():
|
||||||
manifest_file = backup_dir / 'manifest.json'
|
if backup_dir.is_dir():
|
||||||
if manifest_file.exists():
|
manifest_file = backup_dir / 'manifest.json'
|
||||||
try:
|
if manifest_file.exists():
|
||||||
with open(manifest_file, 'r') as f:
|
try:
|
||||||
manifest = json.load(f)
|
with open(manifest_file, 'r') as f:
|
||||||
backups.append(manifest)
|
manifest = json.load(f)
|
||||||
except Exception as e:
|
backups.append(manifest)
|
||||||
logger.error(f"Error reading backup manifest {backup_dir.name}: {e}")
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading backup manifest {backup_dir.name}: {e}")
|
||||||
return sorted(backups, key=lambda x: x['timestamp'], reverse=True)
|
|
||||||
|
return sorted(backups, key=lambda x: x['timestamp'], reverse=True)
|
||||||
def delete_backup(self, backup_id: str) -> bool:
|
|
||||||
"""Delete a backup"""
|
def delete_backup(self, backup_id: str) -> bool:
|
||||||
try:
|
"""Delete a backup"""
|
||||||
backup_path = self.backup_dir / backup_id
|
try:
|
||||||
if not backup_path.exists():
|
backup_path = self.backup_dir / backup_id
|
||||||
raise ValueError(f"Backup {backup_id} not found")
|
if not backup_path.exists():
|
||||||
|
raise ValueError(f"Backup {backup_id} not found")
|
||||||
shutil.rmtree(backup_path)
|
|
||||||
logger.info(f"Deleted backup: {backup_id}")
|
shutil.rmtree(backup_path)
|
||||||
return True
|
logger.info(f"Deleted backup: {backup_id}")
|
||||||
|
return True
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deleting backup {backup_id}: {e}")
|
except Exception as e:
|
||||||
return False
|
logger.error(f"Error deleting backup {backup_id}: {e}")
|
||||||
|
return False
|
||||||
def get_config_hash(self, service: str) -> str:
|
|
||||||
"""Get hash of service configuration for change detection"""
|
def get_config_hash(self, service: str) -> str:
|
||||||
config = self.get_service_config(service)
|
"""Get hash of service configuration for change detection"""
|
||||||
config_str = json.dumps(config, sort_keys=True)
|
config = self.get_service_config(service)
|
||||||
return hashlib.sha256(config_str.encode()).hexdigest()
|
config_str = json.dumps(config, sort_keys=True)
|
||||||
|
return hashlib.sha256(config_str.encode()).hexdigest()
|
||||||
def has_config_changed(self, service: str, previous_hash: str) -> bool:
|
|
||||||
"""Check if configuration has changed"""
|
def has_config_changed(self, service: str, previous_hash: str) -> bool:
|
||||||
current_hash = self.get_config_hash(service)
|
"""Check if configuration has changed"""
|
||||||
return current_hash != previous_hash
|
current_hash = self.get_config_hash(service)
|
||||||
|
return current_hash != previous_hash
|
||||||
def export_config(self, format: str = 'json') -> str:
|
|
||||||
"""Export all configurations in specified format"""
|
def export_config(self, format: str = 'json') -> str:
|
||||||
try:
|
"""Export all configurations in specified format"""
|
||||||
if format == 'json':
|
try:
|
||||||
return json.dumps(self.configs, indent=2)
|
if format == 'json':
|
||||||
elif format == 'yaml':
|
return json.dumps(self.configs, indent=2)
|
||||||
return yaml.dump(self.configs, default_flow_style=False)
|
elif format == 'yaml':
|
||||||
else:
|
return yaml.dump(self.configs, default_flow_style=False)
|
||||||
raise ValueError(f"Unsupported format: {format}")
|
else:
|
||||||
except Exception as e:
|
raise ValueError(f"Unsupported format: {format}")
|
||||||
logger.error(f"Error exporting config: {e}")
|
except Exception as e:
|
||||||
raise
|
logger.error(f"Error exporting config: {e}")
|
||||||
|
raise
|
||||||
def import_config(self, config_data: str, format: str = 'json') -> bool:
|
|
||||||
"""Import configurations from string"""
|
def import_config(self, config_data: str, format: str = 'json') -> bool:
|
||||||
try:
|
"""Import configurations from string"""
|
||||||
if format == 'json':
|
try:
|
||||||
configs = json.loads(config_data)
|
if format == 'json':
|
||||||
elif format == 'yaml':
|
configs = json.loads(config_data)
|
||||||
configs = yaml.safe_load(config_data)
|
elif format == 'yaml':
|
||||||
else:
|
configs = yaml.safe_load(config_data)
|
||||||
raise ValueError(f"Unsupported format: {format}")
|
else:
|
||||||
# Validate and update each service config
|
raise ValueError(f"Unsupported format: {format}")
|
||||||
for service, config in configs.items():
|
# Validate and update each service config
|
||||||
if service in self.service_schemas:
|
for service, config in configs.items():
|
||||||
self.update_service_config(service, config)
|
if service in self.service_schemas:
|
||||||
# Ensure all configs have required fields
|
self.update_service_config(service, config)
|
||||||
for service, schema in self.service_schemas.items():
|
# Ensure all configs have required fields
|
||||||
config = self.get_service_config(service)
|
for service, schema in self.service_schemas.items():
|
||||||
for field in schema['required']:
|
config = self.get_service_config(service)
|
||||||
if field not in config:
|
for field in schema['required']:
|
||||||
t = schema['types'][field]
|
if field not in config:
|
||||||
if t is int:
|
t = schema['types'][field]
|
||||||
config[field] = 0
|
if t is int:
|
||||||
elif t is str:
|
config[field] = 0
|
||||||
config[field] = ''
|
elif t is str:
|
||||||
elif t is list:
|
config[field] = ''
|
||||||
config[field] = []
|
elif t is list:
|
||||||
elif t is bool:
|
config[field] = []
|
||||||
config[field] = False
|
elif t is bool:
|
||||||
# Write back to file
|
config[field] = False
|
||||||
self._save_all_configs()
|
# Write back to file
|
||||||
logger.info("Imported configurations successfully")
|
self._save_all_configs()
|
||||||
return True
|
logger.info("Imported configurations successfully")
|
||||||
except Exception as e:
|
return True
|
||||||
logger.error(f"Error importing config: {e}")
|
except Exception as e:
|
||||||
return False
|
logger.error(f"Error importing config: {e}")
|
||||||
|
return False
|
||||||
def _backup_service_config(self, service: str):
|
|
||||||
"""Create backup of specific service config before update"""
|
def _backup_service_config(self, service: str):
|
||||||
# No-op for unified config, but keep for compatibility
|
"""Create backup of specific service config before update"""
|
||||||
pass
|
# No-op for unified config, but keep for compatibility
|
||||||
|
pass
|
||||||
def get_all_configs(self) -> Dict[str, Dict]:
|
|
||||||
"""Get all service configurations"""
|
def get_all_configs(self) -> Dict[str, Dict]:
|
||||||
return self.configs.copy()
|
"""Get all service configurations"""
|
||||||
|
return self.configs.copy()
|
||||||
def get_config_summary(self) -> Dict[str, Any]:
|
|
||||||
"""Get summary of all configurations"""
|
def get_config_summary(self) -> Dict[str, Any]:
|
||||||
summary = {
|
"""Get summary of all configurations"""
|
||||||
"total_services": len(self.service_schemas),
|
summary = {
|
||||||
"configured_services": [],
|
"total_services": len(self.service_schemas),
|
||||||
"unconfigured_services": [],
|
"configured_services": [],
|
||||||
"backup_count": len(self.list_backups()),
|
"unconfigured_services": [],
|
||||||
"last_backup": None
|
"backup_count": len(self.list_backups()),
|
||||||
}
|
"last_backup": None
|
||||||
|
}
|
||||||
backups = self.list_backups()
|
|
||||||
if backups:
|
backups = self.list_backups()
|
||||||
summary["last_backup"] = backups[0]["timestamp"]
|
if backups:
|
||||||
|
summary["last_backup"] = backups[0]["timestamp"]
|
||||||
for service in self.service_schemas.keys():
|
|
||||||
config = self.get_service_config(service)
|
for service in self.service_schemas.keys():
|
||||||
if config and not config.get("error"):
|
config = self.get_service_config(service)
|
||||||
summary["configured_services"].append(service)
|
if config and not config.get("error"):
|
||||||
else:
|
summary["configured_services"].append(service)
|
||||||
summary["unconfigured_services"].append(service)
|
else:
|
||||||
|
summary["unconfigured_services"].append(service)
|
||||||
|
|
||||||
return summary
|
return summary
|
||||||
+16
-5
@@ -35,12 +35,13 @@ class EmailManager(BaseServiceManager):
|
|||||||
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
||||||
|
|
||||||
if is_docker:
|
if is_docker:
|
||||||
# Return positive status when running in Docker
|
# Check if email container is actually running
|
||||||
|
container_running = self._check_email_container_status()
|
||||||
status = {
|
status = {
|
||||||
'running': True,
|
'running': container_running,
|
||||||
'status': 'online',
|
'status': 'online' if container_running else 'offline',
|
||||||
'smtp_running': True,
|
'smtp_running': container_running,
|
||||||
'imap_running': True,
|
'imap_running': container_running,
|
||||||
'users_count': 0,
|
'users_count': 0,
|
||||||
'domain': 'cell.local',
|
'domain': 'cell.local',
|
||||||
'timestamp': datetime.utcnow().isoformat()
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
@@ -106,6 +107,16 @@ class EmailManager(BaseServiceManager):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _check_email_container_status(self) -> bool:
|
||||||
|
"""Check if email Docker container is running"""
|
||||||
|
try:
|
||||||
|
import docker
|
||||||
|
client = docker.from_env()
|
||||||
|
containers = client.containers.list(filters={'name': 'cell-mail'})
|
||||||
|
return len(containers) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def _test_smtp_connectivity(self) -> Dict[str, Any]:
|
def _test_smtp_connectivity(self) -> Dict[str, Any]:
|
||||||
"""Test SMTP connectivity"""
|
"""Test SMTP connectivity"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
+15
-4
@@ -478,11 +478,12 @@ umask = 022
|
|||||||
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
||||||
|
|
||||||
if is_docker:
|
if is_docker:
|
||||||
# Return positive status when running in Docker
|
# Check if file container is actually running
|
||||||
|
container_running = self._check_file_container_status()
|
||||||
status = {
|
status = {
|
||||||
'running': True,
|
'running': container_running,
|
||||||
'status': 'online',
|
'status': 'online' if container_running else 'offline',
|
||||||
'webdav_status': {'running': True, 'port': 8080},
|
'webdav_status': {'running': container_running, 'port': 8080},
|
||||||
'users_count': 0,
|
'users_count': 0,
|
||||||
'total_storage_used': {'bytes': 0, 'human_readable': '0 B'},
|
'total_storage_used': {'bytes': 0, 'human_readable': '0 B'},
|
||||||
'timestamp': datetime.utcnow().isoformat()
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
@@ -505,6 +506,16 @@ umask = 022
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.handle_error(e, "get_status")
|
return self.handle_error(e, "get_status")
|
||||||
|
|
||||||
|
def _check_file_container_status(self) -> bool:
|
||||||
|
"""Check if file Docker container is running"""
|
||||||
|
try:
|
||||||
|
import docker
|
||||||
|
client = docker.from_env()
|
||||||
|
containers = client.containers.list(filters={'name': 'cell-webdav'})
|
||||||
|
return len(containers) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def test_connectivity(self) -> Dict[str, Any]:
|
def test_connectivity(self) -> Dict[str, Any]:
|
||||||
"""Test file service connectivity"""
|
"""Test file service connectivity"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
+523
-484
File diff suppressed because it is too large
Load Diff
+82
-18
@@ -408,47 +408,111 @@ class NetworkManager(BaseServiceManager):
|
|||||||
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
||||||
|
|
||||||
if is_docker:
|
if is_docker:
|
||||||
# Return positive status when running in Docker
|
# Check if network containers are actually running
|
||||||
|
dns_running = self._check_dns_container_status()
|
||||||
|
dhcp_running = self._check_dhcp_container_status()
|
||||||
|
ntp_running = self._check_ntp_container_status()
|
||||||
|
all_running = dns_running and dhcp_running and ntp_running
|
||||||
|
|
||||||
status = {
|
status = {
|
||||||
'dns_running': True,
|
'dns_running': dns_running,
|
||||||
'dhcp_running': True,
|
'dhcp_running': dhcp_running,
|
||||||
'ntp_running': True,
|
'ntp_running': ntp_running,
|
||||||
'running': True,
|
'running': all_running,
|
||||||
'status': 'online',
|
'status': 'online' if all_running else 'offline',
|
||||||
|
'network': {
|
||||||
|
'dns_running': dns_running,
|
||||||
|
'dhcp_running': dhcp_running,
|
||||||
|
'ntp_running': ntp_running,
|
||||||
|
'running': all_running,
|
||||||
|
'status': 'online' if all_running else 'offline'
|
||||||
|
},
|
||||||
'timestamp': datetime.utcnow().isoformat()
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# Check actual service status in production
|
# Check actual service status in production
|
||||||
|
dns_running = self._check_dns_status()
|
||||||
|
dhcp_running = self._check_dhcp_status()
|
||||||
|
ntp_running = self._check_ntp_status()
|
||||||
|
|
||||||
status = {
|
status = {
|
||||||
'dns_running': self._check_dns_status(),
|
'dns_running': dns_running,
|
||||||
'dhcp_running': self._check_dhcp_status(),
|
'dhcp_running': dhcp_running,
|
||||||
'ntp_running': self._check_ntp_status(),
|
'ntp_running': ntp_running,
|
||||||
|
'running': dns_running and dhcp_running and ntp_running,
|
||||||
|
'status': 'online' if (dns_running and dhcp_running and ntp_running) else 'offline',
|
||||||
|
'network': {
|
||||||
|
'dns_running': dns_running,
|
||||||
|
'dhcp_running': dhcp_running,
|
||||||
|
'ntp_running': ntp_running,
|
||||||
|
'running': dns_running and dhcp_running and ntp_running,
|
||||||
|
'status': 'online' if (dns_running and dhcp_running and ntp_running) else 'offline'
|
||||||
|
},
|
||||||
'timestamp': datetime.utcnow().isoformat()
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Determine overall status
|
|
||||||
status['running'] = status['dns_running'] and status['dhcp_running'] and status['ntp_running']
|
|
||||||
status['status'] = 'online' if status['running'] else 'offline'
|
|
||||||
|
|
||||||
return status
|
return status
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.handle_error(e, "get_status")
|
return self.handle_error(e, "get_status")
|
||||||
|
|
||||||
|
def _check_dns_container_status(self) -> bool:
|
||||||
|
"""Check if DNS Docker container is running"""
|
||||||
|
try:
|
||||||
|
import docker
|
||||||
|
client = docker.from_env()
|
||||||
|
containers = client.containers.list(filters={'name': 'cell-dns'})
|
||||||
|
return len(containers) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_dhcp_container_status(self) -> bool:
|
||||||
|
"""Check if DHCP Docker container is running"""
|
||||||
|
try:
|
||||||
|
import docker
|
||||||
|
client = docker.from_env()
|
||||||
|
containers = client.containers.list(filters={'name': 'cell-dhcp'})
|
||||||
|
return len(containers) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_ntp_container_status(self) -> bool:
|
||||||
|
"""Check if NTP Docker container is running"""
|
||||||
|
try:
|
||||||
|
import docker
|
||||||
|
client = docker.from_env()
|
||||||
|
containers = client.containers.list(filters={'name': 'cell-ntp'})
|
||||||
|
return len(containers) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def test_connectivity(self) -> Dict[str, Any]:
|
def test_connectivity(self) -> Dict[str, Any]:
|
||||||
"""Test network service connectivity"""
|
"""Test network service connectivity"""
|
||||||
try:
|
try:
|
||||||
|
dns_test = self.test_dns_resolution('google.com')
|
||||||
|
dhcp_test = self.test_dhcp_functionality()
|
||||||
|
ntp_test = self.test_ntp_functionality()
|
||||||
|
|
||||||
results = {
|
results = {
|
||||||
'dns_test': self.test_dns_resolution('google.com'),
|
'dns_test': dns_test,
|
||||||
'dhcp_test': self.test_dhcp_functionality(),
|
'dhcp_test': dhcp_test,
|
||||||
'ntp_test': self.test_ntp_functionality(),
|
'ntp_test': ntp_test,
|
||||||
'timestamp': datetime.utcnow().isoformat()
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Determine overall success
|
# Determine overall success
|
||||||
results['success'] = all(
|
success = all(
|
||||||
result.get('success', False)
|
result.get('success', False)
|
||||||
for result in [results['dns_test'], results['dhcp_test'], results['ntp_test']]
|
for result in [dns_test, dhcp_test, ntp_test]
|
||||||
)
|
)
|
||||||
|
results['success'] = success
|
||||||
|
|
||||||
|
# Add network key for compatibility
|
||||||
|
results['network'] = {
|
||||||
|
'dns_test': dns_test,
|
||||||
|
'dhcp_test': dhcp_test,
|
||||||
|
'ntp_test': ntp_test,
|
||||||
|
'success': success
|
||||||
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+173
-3
@@ -9,6 +9,7 @@ import json
|
|||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional, Tuple, Any
|
from typing import Dict, List, Optional, Tuple, Any
|
||||||
import re
|
import re
|
||||||
@@ -24,12 +25,19 @@ class RoutingManager(BaseServiceManager):
|
|||||||
self.routing_dir = os.path.join(config_dir, 'routing')
|
self.routing_dir = os.path.join(config_dir, 'routing')
|
||||||
self.rules_file = os.path.join(data_dir, 'routing', 'rules.json')
|
self.rules_file = os.path.join(data_dir, 'routing', 'rules.json')
|
||||||
|
|
||||||
|
# Service state tracking
|
||||||
|
self._service_running = False
|
||||||
|
self._state_file = os.path.join(data_dir, 'routing', 'service_state.json')
|
||||||
|
|
||||||
# Ensure directories exist
|
# Ensure directories exist
|
||||||
os.makedirs(self.routing_dir, exist_ok=True)
|
os.makedirs(self.routing_dir, exist_ok=True)
|
||||||
os.makedirs(os.path.dirname(self.rules_file), exist_ok=True)
|
os.makedirs(os.path.dirname(self.rules_file), exist_ok=True)
|
||||||
|
|
||||||
# Initialize routing configuration
|
# Initialize routing configuration
|
||||||
self._ensure_config_exists()
|
self._ensure_config_exists()
|
||||||
|
|
||||||
|
# Load service state
|
||||||
|
self._load_service_state()
|
||||||
|
|
||||||
def _ensure_config_exists(self):
|
def _ensure_config_exists(self):
|
||||||
"""Ensure routing configuration exists"""
|
"""Ensure routing configuration exists"""
|
||||||
@@ -53,6 +61,33 @@ class RoutingManager(BaseServiceManager):
|
|||||||
|
|
||||||
logger.info("Routing rules initialized")
|
logger.info("Routing rules initialized")
|
||||||
|
|
||||||
|
def _load_service_state(self):
|
||||||
|
"""Load service state from file"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(self._state_file):
|
||||||
|
with open(self._state_file, 'r') as f:
|
||||||
|
state = json.load(f)
|
||||||
|
self._service_running = state.get('running', False)
|
||||||
|
else:
|
||||||
|
# Default to running if no state file exists (for backward compatibility)
|
||||||
|
self._service_running = True
|
||||||
|
self._save_service_state()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load service state: {e}")
|
||||||
|
self._service_running = True
|
||||||
|
|
||||||
|
def _save_service_state(self):
|
||||||
|
"""Save service state to file"""
|
||||||
|
try:
|
||||||
|
state = {
|
||||||
|
'running': self._service_running,
|
||||||
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
with open(self._state_file, 'w') as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save service state: {e}")
|
||||||
|
|
||||||
def _validate_cidr(self, cidr):
|
def _validate_cidr(self, cidr):
|
||||||
import ipaddress
|
import ipaddress
|
||||||
try:
|
try:
|
||||||
@@ -485,9 +520,12 @@ class RoutingManager(BaseServiceManager):
|
|||||||
routing_status = self.get_routing_status()
|
routing_status = self.get_routing_status()
|
||||||
rules = self._load_rules()
|
rules = self._load_rules()
|
||||||
|
|
||||||
|
# Check if routing service is actually running by testing basic functionality
|
||||||
|
is_running = self._is_routing_service_running()
|
||||||
|
|
||||||
status = {
|
status = {
|
||||||
'running': routing_status.get('running', False),
|
'running': is_running,
|
||||||
'status': 'online' if routing_status.get('running', False) else 'offline',
|
'status': 'online' if is_running else 'offline',
|
||||||
'routing_status': routing_status,
|
'routing_status': routing_status,
|
||||||
'nat_rules_count': len(rules.get('nat_rules', [])),
|
'nat_rules_count': len(rules.get('nat_rules', [])),
|
||||||
'peer_routes_count': len(rules.get('peer_routes', {})),
|
'peer_routes_count': len(rules.get('peer_routes', {})),
|
||||||
@@ -569,6 +607,13 @@ class RoutingManager(BaseServiceManager):
|
|||||||
'message': f'iptables access failed: {result.stderr}',
|
'message': f'iptables access failed: {result.stderr}',
|
||||||
'error': result.stderr
|
'error': result.stderr
|
||||||
}
|
}
|
||||||
|
except FileNotFoundError:
|
||||||
|
# System tools not available (development environment)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'iptables not available (development mode)',
|
||||||
|
'rules_count': 0
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -596,6 +641,13 @@ class RoutingManager(BaseServiceManager):
|
|||||||
'message': f'Network interfaces access failed: {result.stderr}',
|
'message': f'Network interfaces access failed: {result.stderr}',
|
||||||
'error': result.stderr
|
'error': result.stderr
|
||||||
}
|
}
|
||||||
|
except FileNotFoundError:
|
||||||
|
# System tools not available (development environment)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'Network tools not available (development mode)',
|
||||||
|
'interfaces_count': 0
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -623,6 +675,13 @@ class RoutingManager(BaseServiceManager):
|
|||||||
'message': f'Routing table access failed: {result.stderr}',
|
'message': f'Routing table access failed: {result.stderr}',
|
||||||
'error': result.stderr
|
'error': result.stderr
|
||||||
}
|
}
|
||||||
|
except FileNotFoundError:
|
||||||
|
# System tools not available (development environment)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'Routing tools not available (development mode)',
|
||||||
|
'routes_count': 0
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -815,6 +874,19 @@ class RoutingManager(BaseServiceManager):
|
|||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
# System tools not available (development environment)
|
||||||
|
# Return mock routing table for development
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'route': 'default via 192.168.1.1 dev en0',
|
||||||
|
'parsed': {'destination': 'default', 'via': '192.168.1.1', 'dev': 'en0', 'metric': ''}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'route': '10.0.0.0/24 dev wg0',
|
||||||
|
'parsed': {'destination': '10.0.0.0/24', 'via': '', 'dev': 'wg0', 'metric': ''}
|
||||||
|
}
|
||||||
|
]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get routing table: {e}")
|
logger.error(f"Failed to get routing table: {e}")
|
||||||
return []
|
return []
|
||||||
@@ -843,4 +915,102 @@ class RoutingManager(BaseServiceManager):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to parse route: {e}")
|
logger.error(f"Failed to parse route: {e}")
|
||||||
return {'destination': route_line, 'via': '', 'dev': '', 'metric': ''}
|
return {'destination': route_line, 'via': '', 'dev': '', 'metric': ''}
|
||||||
|
|
||||||
|
def _is_routing_service_running(self) -> bool:
|
||||||
|
"""Check if routing service is actually running"""
|
||||||
|
# Use internal state tracking instead of system tool checks
|
||||||
|
return self._service_running
|
||||||
|
|
||||||
|
def start(self) -> bool:
|
||||||
|
"""Start routing service"""
|
||||||
|
try:
|
||||||
|
# Set internal state to running
|
||||||
|
self._service_running = True
|
||||||
|
self._save_service_state()
|
||||||
|
|
||||||
|
# Try to enable IP forwarding (may fail in Docker without privileges)
|
||||||
|
try:
|
||||||
|
subprocess.run(['sysctl', '-w', 'net.ipv4.ip_forward=1'],
|
||||||
|
check=True, timeout=10)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||||
|
logger.warning(f"Could not enable IP forwarding: {e}")
|
||||||
|
# Continue anyway - service is considered started
|
||||||
|
|
||||||
|
# Load existing rules
|
||||||
|
rules = self._load_rules()
|
||||||
|
|
||||||
|
# Apply all enabled rules (may fail in Docker without privileges)
|
||||||
|
try:
|
||||||
|
for rule in rules.get('nat_rules', []):
|
||||||
|
if rule.get('enabled', True):
|
||||||
|
self._apply_nat_rule(rule)
|
||||||
|
|
||||||
|
for rule in rules.get('firewall_rules', []):
|
||||||
|
if rule.get('enabled', True):
|
||||||
|
self._apply_firewall_rule(rule)
|
||||||
|
|
||||||
|
for route in rules.get('peer_routes', {}).values():
|
||||||
|
if route.get('enabled', True):
|
||||||
|
self._apply_peer_route(route)
|
||||||
|
|
||||||
|
for exit_node in rules.get('exit_nodes', []):
|
||||||
|
if exit_node.get('enabled', True):
|
||||||
|
self._apply_exit_node(exit_node)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not apply routing rules: {e}")
|
||||||
|
# Continue anyway - service is considered started
|
||||||
|
|
||||||
|
logger.info("Routing service started successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start routing service: {e}")
|
||||||
|
self._service_running = False
|
||||||
|
self._save_service_state()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop(self) -> bool:
|
||||||
|
"""Stop routing service"""
|
||||||
|
try:
|
||||||
|
# Set internal state to stopped
|
||||||
|
self._service_running = False
|
||||||
|
self._save_service_state()
|
||||||
|
|
||||||
|
# Try to clear all iptables rules (may fail in Docker without privileges)
|
||||||
|
try:
|
||||||
|
subprocess.run(['iptables', '-t', 'nat', '-F'],
|
||||||
|
check=True, timeout=10)
|
||||||
|
subprocess.run(['iptables', '-F'],
|
||||||
|
check=True, timeout=10)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||||
|
logger.warning(f"Could not clear iptables rules: {e}")
|
||||||
|
# Continue anyway - service is considered stopped
|
||||||
|
|
||||||
|
# Try to disable IP forwarding (may fail in Docker without privileges)
|
||||||
|
try:
|
||||||
|
subprocess.run(['sysctl', '-w', 'net.ipv4.ip_forward=0'],
|
||||||
|
check=True, timeout=10)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||||
|
logger.warning(f"Could not disable IP forwarding: {e}")
|
||||||
|
# Continue anyway - service is considered stopped
|
||||||
|
|
||||||
|
logger.info("Routing service stopped successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to stop routing service: {e}")
|
||||||
|
# Even if system commands fail, we consider the service stopped
|
||||||
|
self._service_running = False
|
||||||
|
self._save_service_state()
|
||||||
|
return True # Return True because the state is now stopped
|
||||||
|
|
||||||
|
def restart(self) -> bool:
|
||||||
|
"""Restart routing service"""
|
||||||
|
try:
|
||||||
|
self.stop()
|
||||||
|
time.sleep(1) # Brief pause
|
||||||
|
return self.start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to restart routing service: {e}")
|
||||||
|
return False
|
||||||
+64
-31
@@ -179,27 +179,40 @@ class ServiceBus:
|
|||||||
def orchestrate_service_start(self, service_name: str) -> bool:
|
def orchestrate_service_start(self, service_name: str) -> bool:
|
||||||
"""Orchestrate starting a service with its dependencies"""
|
"""Orchestrate starting a service with its dependencies"""
|
||||||
try:
|
try:
|
||||||
# Check dependencies
|
# Map service names to Docker container names
|
||||||
dependencies = self.service_dependencies.get(service_name, [])
|
service_to_container = {
|
||||||
for dep in dependencies:
|
'wireguard': 'cell-wireguard',
|
||||||
if dep not in self.service_registry:
|
'email': 'cell-mail',
|
||||||
logger.warning(f"Service {service_name} depends on {dep} which is not registered")
|
'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
|
return False
|
||||||
|
else:
|
||||||
# Run pre-start hooks
|
logger.error("Container manager not available")
|
||||||
if service_name in self.lifecycle_hooks and 'pre_start' in self.lifecycle_hooks[service_name]:
|
return False
|
||||||
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:
|
except Exception as e:
|
||||||
logger.error(f"Error orchestrating start of {service_name}: {e}")
|
logger.error(f"Error orchestrating start of {service_name}: {e}")
|
||||||
@@ -208,20 +221,40 @@ class ServiceBus:
|
|||||||
def orchestrate_service_stop(self, service_name: str) -> bool:
|
def orchestrate_service_stop(self, service_name: str) -> bool:
|
||||||
"""Orchestrate stopping a service"""
|
"""Orchestrate stopping a service"""
|
||||||
try:
|
try:
|
||||||
# Run pre-stop hooks
|
# Map service names to Docker container names
|
||||||
if service_name in self.lifecycle_hooks and 'pre_stop' in self.lifecycle_hooks[service_name]:
|
service_to_container = {
|
||||||
self.lifecycle_hooks[service_name]['pre_stop']()
|
'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
|
||||||
|
}
|
||||||
|
|
||||||
# Stop the service
|
container_name = service_to_container.get(service_name)
|
||||||
if hasattr(self.service_registry[service_name], 'stop'):
|
|
||||||
self.service_registry[service_name].stop()
|
|
||||||
|
|
||||||
# Run post-stop hooks
|
if container_name is None:
|
||||||
if service_name in self.lifecycle_hooks and 'post_stop' in self.lifecycle_hooks[service_name]:
|
# For services without containers (routing, vault, container), just call their stop method
|
||||||
self.lifecycle_hooks[service_name]['post_stop']()
|
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
|
||||||
|
|
||||||
logger.info(f"Orchestrated stop of service: {service_name}")
|
# For services with containers, stop the Docker container
|
||||||
return True
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error orchestrating stop of {service_name}: {e}")
|
logger.error(f"Error orchestrating stop of {service_name}: {e}")
|
||||||
|
|||||||
+693
-673
File diff suppressed because it is too large
Load Diff
@@ -34,13 +34,14 @@ class WireGuardManager(BaseServiceManager):
|
|||||||
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
is_docker = os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER') == 'true'
|
||||||
|
|
||||||
if is_docker:
|
if is_docker:
|
||||||
# Return positive status when running in Docker
|
# Check if WireGuard container is actually running
|
||||||
|
container_running = self._check_wireguard_container_status()
|
||||||
status = {
|
status = {
|
||||||
'running': True,
|
'running': container_running,
|
||||||
'status': 'online',
|
'status': 'online' if container_running else 'offline',
|
||||||
'interface': 'wg0',
|
'interface': 'wg0' if container_running else 'unknown',
|
||||||
'peers_count': 1,
|
'peers_count': len(self._get_configured_peers()) if container_running else 0,
|
||||||
'total_traffic': {'bytes_sent': 1024, 'bytes_received': 2048},
|
'total_traffic': self._get_traffic_stats() if container_running else {'bytes_sent': 0, 'bytes_received': 0},
|
||||||
'timestamp': datetime.utcnow().isoformat()
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
@@ -88,6 +89,16 @@ class WireGuardManager(BaseServiceManager):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _check_wireguard_container_status(self) -> bool:
|
||||||
|
"""Check if WireGuard Docker container is running"""
|
||||||
|
try:
|
||||||
|
import docker
|
||||||
|
client = docker.from_env()
|
||||||
|
containers = client.containers.list(filters={'name': 'cell-wireguard'})
|
||||||
|
return len(containers) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def _check_interface_status(self) -> bool:
|
def _check_interface_status(self) -> bool:
|
||||||
"""Check if WireGuard interface is up"""
|
"""Check if WireGuard interface is up"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ A modern React-based web interface for managing your Personal Internet Cell.
|
|||||||
|
|
||||||
1. Install dependencies:
|
1. Install dependencies:
|
||||||
```bash
|
```bash
|
||||||
npm install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Start the development server:
|
2. Start the development server:
|
||||||
|
|||||||
+311
-283
@@ -1,284 +1,312 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import { useNavigate } from 'react-router-dom';
|
||||||
Server,
|
import {
|
||||||
Users,
|
Server,
|
||||||
Shield,
|
Users,
|
||||||
Mail,
|
Shield,
|
||||||
Calendar,
|
Mail,
|
||||||
FolderOpen,
|
Calendar,
|
||||||
Wifi,
|
FolderOpen,
|
||||||
Activity,
|
Wifi,
|
||||||
CheckCircle,
|
Activity,
|
||||||
XCircle,
|
CheckCircle,
|
||||||
AlertCircle
|
XCircle,
|
||||||
} from 'lucide-react';
|
AlertCircle,
|
||||||
import { cellAPI, servicesAPI } from '../services/api';
|
Play,
|
||||||
|
Square,
|
||||||
function Dashboard({ isOnline }) {
|
RotateCcw
|
||||||
const [cellStatus, setCellStatus] = useState(null);
|
} from 'lucide-react';
|
||||||
const [servicesStatus, setServicesStatus] = useState(null);
|
import { cellAPI, servicesAPI } from '../services/api';
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
function Dashboard({ isOnline }) {
|
||||||
useEffect(() => {
|
const navigate = useNavigate();
|
||||||
const fetchData = async () => {
|
const [cellStatus, setCellStatus] = useState(null);
|
||||||
if (!isOnline) {
|
const [servicesStatus, setServicesStatus] = useState(null);
|
||||||
setIsLoading(false);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
return;
|
const [serviceControls, setServiceControls] = useState({});
|
||||||
}
|
|
||||||
|
useEffect(() => {
|
||||||
try {
|
fetchData();
|
||||||
const [statusResponse, servicesResponse] = await Promise.all([
|
const interval = setInterval(fetchData, 30000); // Refresh every 30 seconds
|
||||||
cellAPI.getStatus(),
|
|
||||||
servicesAPI.getAllStatus()
|
return () => clearInterval(interval);
|
||||||
]);
|
}, [isOnline]);
|
||||||
|
|
||||||
setCellStatus(statusResponse.data);
|
const getStatusIcon = (status) => {
|
||||||
setServicesStatus(servicesResponse.data);
|
if (status === true || status?.status === 'online' || status?.running === true) {
|
||||||
} catch (error) {
|
return <CheckCircle className="h-5 w-5 text-success-500" />;
|
||||||
console.error('Failed to fetch dashboard data:', error);
|
} else if (status === false || status?.status === 'offline' || status?.running === false) {
|
||||||
} finally {
|
return <XCircle className="h-5 w-5 text-danger-500" />;
|
||||||
setIsLoading(false);
|
} else {
|
||||||
}
|
return <AlertCircle className="h-5 w-5 text-warning-500" />;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
fetchData();
|
|
||||||
const interval = setInterval(fetchData, 30000); // Refresh every 30 seconds
|
const getStatusText = (status) => {
|
||||||
|
if (status === true || status?.status === 'online' || status?.running === true) {
|
||||||
return () => clearInterval(interval);
|
return 'Online';
|
||||||
}, [isOnline]);
|
} else if (status === false || status?.status === 'offline' || status?.running === false) {
|
||||||
|
return 'Offline';
|
||||||
const getStatusIcon = (status) => {
|
} else {
|
||||||
if (status === true || status?.status === 'online' || status?.running === true) {
|
return 'Unknown';
|
||||||
return <CheckCircle className="h-5 w-5 text-success-500" />;
|
}
|
||||||
} else if (status === false || status?.status === 'offline' || status?.running === false) {
|
};
|
||||||
return <XCircle className="h-5 w-5 text-danger-500" />;
|
|
||||||
} else {
|
const getStatusColor = (status) => {
|
||||||
return <AlertCircle className="h-5 w-5 text-warning-500" />;
|
if (status === true || status?.status === 'online' || status?.running === true) {
|
||||||
}
|
return 'text-success-600';
|
||||||
};
|
} else if (status === false || status?.status === 'offline' || status?.running === false) {
|
||||||
|
return 'text-danger-600';
|
||||||
const getStatusText = (status) => {
|
} else {
|
||||||
if (status === true || status?.status === 'online' || status?.running === true) {
|
return 'text-warning-600';
|
||||||
return 'Online';
|
}
|
||||||
} else if (status === false || status?.status === 'offline' || status?.running === false) {
|
};
|
||||||
return 'Offline';
|
|
||||||
} else {
|
const handleServiceControl = async (serviceName, action) => {
|
||||||
return 'Unknown';
|
if (!isOnline) return;
|
||||||
}
|
|
||||||
};
|
setServiceControls(prev => ({ ...prev, [serviceName]: { ...prev[serviceName], [action]: 'loading' } }));
|
||||||
|
|
||||||
const getStatusColor = (status) => {
|
try {
|
||||||
if (status === true || status?.status === 'online' || status?.running === true) {
|
let response;
|
||||||
return 'text-success-600';
|
switch (action) {
|
||||||
} else if (status === false || status?.status === 'offline' || status?.running === false) {
|
case 'start':
|
||||||
return 'text-danger-600';
|
response = await servicesAPI.startService(serviceName);
|
||||||
} else {
|
break;
|
||||||
return 'text-warning-600';
|
case 'stop':
|
||||||
}
|
response = await servicesAPI.stopService(serviceName);
|
||||||
};
|
break;
|
||||||
|
case 'restart':
|
||||||
if (isLoading) {
|
response = await servicesAPI.restartService(serviceName);
|
||||||
return (
|
break;
|
||||||
<div className="flex items-center justify-center h-64">
|
default:
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
throw new Error('Invalid action');
|
||||||
</div>
|
}
|
||||||
);
|
|
||||||
}
|
if (response.data.success || response.data.message) {
|
||||||
|
// Refresh status after successful control action
|
||||||
return (
|
setTimeout(() => {
|
||||||
<div>
|
fetchData();
|
||||||
<div className="mb-8">
|
}, 1000);
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
}
|
||||||
<p className="mt-2 text-gray-600">
|
} catch (error) {
|
||||||
Overview of your Personal Internet Cell status and services
|
console.error(`Failed to ${action} ${serviceName}:`, error);
|
||||||
</p>
|
} finally {
|
||||||
</div>
|
setServiceControls(prev => ({ ...prev, [serviceName]: { ...prev[serviceName], [action]: 'idle' } }));
|
||||||
|
}
|
||||||
{/* Cell Status */}
|
};
|
||||||
{cellStatus && (
|
|
||||||
<div className="mb-8">
|
const renderServiceCard = (serviceName, icon, displayName, status) => {
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Cell Status</h2>
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="card">
|
||||||
<div className="card">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Server className="h-8 w-8 text-primary-500" />
|
{icon}
|
||||||
<div className="ml-4">
|
<span className="ml-3 text-sm font-medium text-gray-900">{displayName}</span>
|
||||||
<p className="text-sm font-medium text-gray-500">Cell Name</p>
|
</div>
|
||||||
<p className="text-lg font-semibold text-gray-900">{cellStatus.cell_name}</p>
|
<div className="flex items-center space-x-2">
|
||||||
</div>
|
<div className="flex items-center">
|
||||||
</div>
|
{getStatusIcon(status)}
|
||||||
</div>
|
<span className={`ml-2 text-sm font-medium ${getStatusColor(status)}`}>
|
||||||
|
{getStatusText(status)}
|
||||||
<div className="card">
|
</span>
|
||||||
<div className="flex items-center">
|
</div>
|
||||||
<Users className="h-8 w-8 text-primary-500" />
|
<div className="flex space-x-1">
|
||||||
<div className="ml-4">
|
<button
|
||||||
<p className="text-sm font-medium text-gray-500">Peers</p>
|
onClick={() => handleServiceControl(serviceName, 'start')}
|
||||||
<p className="text-lg font-semibold text-gray-900">{cellStatus.peers_count}</p>
|
disabled={serviceControls[serviceName]?.start === 'loading' || status?.running}
|
||||||
</div>
|
className="p-1 text-green-600 hover:text-green-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
</div>
|
title={`Start ${displayName} Service`}
|
||||||
</div>
|
>
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
<div className="card">
|
</button>
|
||||||
<div className="flex items-center">
|
<button
|
||||||
<Activity className="h-8 w-8 text-primary-500" />
|
onClick={() => handleServiceControl(serviceName, 'stop')}
|
||||||
<div className="ml-4">
|
disabled={serviceControls[serviceName]?.stop === 'loading' || !status?.running}
|
||||||
<p className="text-sm font-medium text-gray-500">Uptime</p>
|
className="p-1 text-red-600 hover:text-red-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
<p className="text-lg font-semibold text-gray-900">
|
title={`Stop ${displayName} Service`}
|
||||||
{Math.floor((cellStatus.uptime || 0) / 3600)}h {Math.floor(((cellStatus.uptime || 0) % 3600) / 60)}m
|
>
|
||||||
</p>
|
<Square className="h-4 w-4" />
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<button
|
||||||
</div>
|
onClick={() => handleServiceControl(serviceName, 'restart')}
|
||||||
|
disabled={serviceControls[serviceName]?.restart === 'loading'}
|
||||||
<div className="card">
|
className="p-1 text-blue-600 hover:text-blue-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
<div className="flex items-center">
|
title={`Restart ${displayName} Service`}
|
||||||
<div className="h-8 w-8 rounded-full bg-primary-100 flex items-center justify-center">
|
>
|
||||||
<div className="h-3 w-3 rounded-full bg-primary-600"></div>
|
<RotateCcw className="h-4 w-4" />
|
||||||
</div>
|
</button>
|
||||||
<div className="ml-4">
|
</div>
|
||||||
<p className="text-sm font-medium text-gray-500">Status</p>
|
</div>
|
||||||
<p className="text-lg font-semibold text-gray-900">Active</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
};
|
||||||
</div>
|
|
||||||
</div>
|
const fetchData = async () => {
|
||||||
)}
|
if (!isOnline) {
|
||||||
|
setIsLoading(false);
|
||||||
{/* Services Status */}
|
return;
|
||||||
{servicesStatus && (
|
}
|
||||||
<div className="mb-8">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Services Status</h2>
|
try {
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
const [statusResponse, servicesResponse] = await Promise.all([
|
||||||
<div className="card">
|
cellAPI.getStatus(),
|
||||||
<div className="flex items-center justify-between">
|
servicesAPI.getAllStatus()
|
||||||
<div className="flex items-center">
|
]);
|
||||||
<Shield className="h-6 w-6 text-primary-500" />
|
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">WireGuard</span>
|
setCellStatus(statusResponse.data);
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
// Transform services data to match expected structure
|
||||||
{getStatusIcon(servicesStatus.wireguard)}
|
const servicesData = servicesResponse.data;
|
||||||
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.wireguard)}`}>
|
const transformedServices = {
|
||||||
{getStatusText(servicesStatus.wireguard)}
|
wireguard: servicesData.wireguard || { running: false, status: 'offline' },
|
||||||
</span>
|
email: servicesData.email || { running: false, status: 'offline' },
|
||||||
</div>
|
calendar: servicesData.calendar || { running: false, status: 'offline' },
|
||||||
</div>
|
files: servicesData.files || { running: false, status: 'offline' },
|
||||||
</div>
|
routing: servicesData.routing || { running: false, status: 'offline' },
|
||||||
|
network: servicesData.network || { running: false, status: 'offline' }
|
||||||
<div className="card">
|
};
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
setServicesStatus(transformedServices);
|
||||||
<Mail className="h-6 w-6 text-primary-500" />
|
} catch (error) {
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">Email</span>
|
console.error('Failed to fetch dashboard data:', error);
|
||||||
</div>
|
} finally {
|
||||||
<div className="flex items-center">
|
setIsLoading(false);
|
||||||
{getStatusIcon(servicesStatus.email)}
|
}
|
||||||
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.email)}`}>
|
};
|
||||||
{getStatusText(servicesStatus.email)}
|
|
||||||
</span>
|
if (isLoading) {
|
||||||
</div>
|
return (
|
||||||
</div>
|
<div className="flex items-center justify-center h-64">
|
||||||
</div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||||
|
</div>
|
||||||
<div className="card">
|
);
|
||||||
<div className="flex items-center justify-between">
|
}
|
||||||
<div className="flex items-center">
|
|
||||||
<Calendar className="h-6 w-6 text-primary-500" />
|
return (
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">Calendar</span>
|
<div>
|
||||||
</div>
|
<div className="mb-8">
|
||||||
<div className="flex items-center">
|
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||||
{getStatusIcon(servicesStatus.calendar)}
|
<p className="mt-2 text-gray-600">
|
||||||
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.calendar)}`}>
|
Overview of your Personal Internet Cell status and services
|
||||||
{getStatusText(servicesStatus.calendar)}
|
</p>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
{/* Cell Status */}
|
||||||
</div>
|
{cellStatus && (
|
||||||
|
<div className="mb-8">
|
||||||
<div className="card">
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Cell Status</h2>
|
||||||
<div className="flex items-center justify-between">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<div className="flex items-center">
|
<div className="card">
|
||||||
<FolderOpen className="h-6 w-6 text-primary-500" />
|
<div className="flex items-center">
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">Files</span>
|
<Server className="h-8 w-8 text-primary-500" />
|
||||||
</div>
|
<div className="ml-4">
|
||||||
<div className="flex items-center">
|
<p className="text-sm font-medium text-gray-500">Cell Name</p>
|
||||||
{getStatusIcon(servicesStatus.files)}
|
<p className="text-lg font-semibold text-gray-900">{cellStatus.cell_name}</p>
|
||||||
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.files)}`}>
|
</div>
|
||||||
{getStatusText(servicesStatus.files)}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<div className="card">
|
||||||
</div>
|
<div className="flex items-center">
|
||||||
|
<Users className="h-8 w-8 text-primary-500" />
|
||||||
<div className="card">
|
<div className="ml-4">
|
||||||
<div className="flex items-center justify-between">
|
<p className="text-sm font-medium text-gray-500">Peers</p>
|
||||||
<div className="flex items-center">
|
<p className="text-lg font-semibold text-gray-900">{cellStatus.peers_count}</p>
|
||||||
<Wifi className="h-6 w-6 text-primary-500" />
|
</div>
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">Routing</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
|
||||||
{getStatusIcon(servicesStatus.routing)}
|
<div className="card">
|
||||||
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.routing)}`}>
|
<div className="flex items-center">
|
||||||
{getStatusText(servicesStatus.routing)}
|
<Activity className="h-8 w-8 text-primary-500" />
|
||||||
</span>
|
<div className="ml-4">
|
||||||
</div>
|
<p className="text-sm font-medium text-gray-500">Uptime</p>
|
||||||
</div>
|
<p className="text-lg font-semibold text-gray-900">
|
||||||
</div>
|
{Math.floor((cellStatus.uptime || 0) / 3600)}h {Math.floor(((cellStatus.uptime || 0) % 3600) / 60)}m
|
||||||
|
</p>
|
||||||
<div className="card">
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
</div>
|
||||||
<div className="flex items-center">
|
</div>
|
||||||
<Server className="h-6 w-6 text-primary-500" />
|
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">Network</span>
|
<div className="card">
|
||||||
</div>
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<div className="h-8 w-8 rounded-full bg-primary-100 flex items-center justify-center">
|
||||||
{getStatusIcon(servicesStatus.network)}
|
<div className="h-3 w-3 rounded-full bg-primary-600"></div>
|
||||||
<span className={`ml-2 text-sm font-medium ${getStatusColor(servicesStatus.network)}`}>
|
</div>
|
||||||
{getStatusText(servicesStatus.network)}
|
<div className="ml-4">
|
||||||
</span>
|
<p className="text-sm font-medium text-gray-500">Status</p>
|
||||||
</div>
|
<p className="text-lg font-semibold text-gray-900">Active</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
{/* Quick Actions */}
|
|
||||||
<div>
|
{/* Services Status */}
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h2>
|
{servicesStatus && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="mb-8">
|
||||||
<button className="card hover:shadow-md transition-shadow cursor-pointer">
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Services Status</h2>
|
||||||
<div className="flex items-center">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<Users className="h-6 w-6 text-primary-500" />
|
{renderServiceCard('wireguard', <Shield className="h-6 w-6 text-primary-500" />, 'WireGuard', servicesStatus.wireguard)}
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">Manage Peers</span>
|
{renderServiceCard('email', <Mail className="h-6 w-6 text-primary-500" />, 'Email', servicesStatus.email)}
|
||||||
</div>
|
{renderServiceCard('calendar', <Calendar className="h-6 w-6 text-primary-500" />, 'Calendar', servicesStatus.calendar)}
|
||||||
</button>
|
{renderServiceCard('files', <FolderOpen className="h-6 w-6 text-primary-500" />, 'Files', servicesStatus.files)}
|
||||||
|
{renderServiceCard('routing', <Wifi className="h-6 w-6 text-primary-500" />, 'Routing', servicesStatus.routing)}
|
||||||
<button className="card hover:shadow-md transition-shadow cursor-pointer">
|
{renderServiceCard('network', <Server className="h-6 w-6 text-primary-500" />, 'Network', servicesStatus.network)}
|
||||||
<div className="flex items-center">
|
</div>
|
||||||
<Shield className="h-6 w-6 text-primary-500" />
|
</div>
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">WireGuard Config</span>
|
)}
|
||||||
</div>
|
|
||||||
</button>
|
{/* Quick Actions */}
|
||||||
|
<div>
|
||||||
<button className="card hover:shadow-md transition-shadow cursor-pointer">
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h2>
|
||||||
<div className="flex items-center">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<Wifi className="h-6 w-6 text-primary-500" />
|
<button
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">Routing Rules</span>
|
onClick={() => navigate('/peers')}
|
||||||
</div>
|
className="card hover:shadow-md transition-shadow cursor-pointer"
|
||||||
</button>
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
<button className="card hover:shadow-md transition-shadow cursor-pointer">
|
<Users className="h-6 w-6 text-primary-500" />
|
||||||
<div className="flex items-center">
|
<span className="ml-3 text-sm font-medium text-gray-900">Manage Peers</span>
|
||||||
<Activity className="h-6 w-6 text-primary-500" />
|
</div>
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900">View Logs</span>
|
</button>
|
||||||
</div>
|
|
||||||
</button>
|
<button
|
||||||
</div>
|
onClick={() => navigate('/wireguard')}
|
||||||
</div>
|
className="card hover:shadow-md transition-shadow cursor-pointer"
|
||||||
</div>
|
>
|
||||||
);
|
<div className="flex items-center">
|
||||||
}
|
<Shield className="h-6 w-6 text-primary-500" />
|
||||||
|
<span className="ml-3 text-sm font-medium text-gray-900">WireGuard Config</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/routing')}
|
||||||
|
className="card hover:shadow-md transition-shadow cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Wifi className="h-6 w-6 text-primary-500" />
|
||||||
|
<span className="ml-3 text-sm font-medium text-gray-900">Routing Rules</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/logs')}
|
||||||
|
className="card hover:shadow-md transition-shadow cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Activity className="h-6 w-6 text-primary-500" />
|
||||||
|
<span className="ml-3 text-sm font-medium text-gray-900">View Logs</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
+207
-204
@@ -1,205 +1,208 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// Create axios instance with base configuration
|
// Create axios instance with base configuration
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
|
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request interceptor for logging
|
// Request interceptor for logging
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
|
console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('API Request Error:', error);
|
console.error('API Request Error:', error);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Response interceptor for error handling
|
// Response interceptor for error handling
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('API Response Error:', error.response?.data || error.message);
|
console.error('API Response Error:', error.response?.data || error.message);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cell Status API
|
// Cell Status API
|
||||||
export const cellAPI = {
|
export const cellAPI = {
|
||||||
getStatus: () => api.get('/api/status'),
|
getStatus: () => api.get('/api/status'),
|
||||||
getConfig: () => api.get('/api/config'),
|
getConfig: () => api.get('/api/config'),
|
||||||
updateConfig: (config) => api.put('/api/config', config),
|
updateConfig: (config) => api.put('/api/config', config),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Network Services API
|
// Network Services API
|
||||||
export const networkAPI = {
|
export const networkAPI = {
|
||||||
getDNSRecords: () => api.get('/api/dns/records'),
|
getDNSRecords: () => api.get('/api/dns/records'),
|
||||||
addDNSRecord: (record) => api.post('/api/dns/records', record),
|
addDNSRecord: (record) => api.post('/api/dns/records', record),
|
||||||
removeDNSRecord: (record) => api.delete('/api/dns/records', { data: record }),
|
removeDNSRecord: (record) => api.delete('/api/dns/records', { data: record }),
|
||||||
getDHCPLeases: () => api.get('/api/dhcp/leases'),
|
getDHCPLeases: () => api.get('/api/dhcp/leases'),
|
||||||
addDHCPReservation: (reservation) => api.post('/api/dhcp/reservations', reservation),
|
addDHCPReservation: (reservation) => api.post('/api/dhcp/reservations', reservation),
|
||||||
removeDHCPReservation: (reservation) => api.delete('/api/dhcp/reservations', { data: reservation }),
|
removeDHCPReservation: (reservation) => api.delete('/api/dhcp/reservations', { data: reservation }),
|
||||||
getNTPStatus: () => api.get('/api/ntp/status'),
|
getNTPStatus: () => api.get('/api/ntp/status'),
|
||||||
testNetwork: (data) => api.post('/api/network/test', data),
|
testNetwork: (data) => api.post('/api/network/test', data),
|
||||||
};
|
};
|
||||||
|
|
||||||
// WireGuard API
|
// WireGuard API
|
||||||
export const wireguardAPI = {
|
export const wireguardAPI = {
|
||||||
getKeys: () => api.get('/api/wireguard/keys'),
|
getKeys: () => api.get('/api/wireguard/keys'),
|
||||||
generatePeerKeys: (data) => api.post('/api/wireguard/keys/peer', data),
|
generatePeerKeys: (data) => api.post('/api/wireguard/keys/peer', data),
|
||||||
getConfig: () => api.get('/api/wireguard/config'),
|
getConfig: () => api.get('/api/wireguard/config'),
|
||||||
getPeers: () => api.get('/api/wireguard/peers'),
|
getPeers: () => api.get('/api/wireguard/peers'),
|
||||||
addPeer: (peer) => api.post('/api/wireguard/peers', peer),
|
addPeer: (peer) => api.post('/api/wireguard/peers', peer),
|
||||||
removePeer: (peer) => api.delete('/api/wireguard/peers', { data: peer }),
|
removePeer: (peer) => api.delete('/api/wireguard/peers', { data: peer }),
|
||||||
getStatus: () => api.get('/api/wireguard/status'),
|
getStatus: () => api.get('/api/wireguard/status'),
|
||||||
testConnectivity: (data) => api.post('/api/wireguard/connectivity', data),
|
testConnectivity: (data) => api.post('/api/wireguard/connectivity', data),
|
||||||
updatePeerIP: (data) => api.put('/api/wireguard/peers/ip', data),
|
updatePeerIP: (data) => api.put('/api/wireguard/peers/ip', data),
|
||||||
getPeerConfig: (data) => api.post('/api/wireguard/peers/config', data),
|
getPeerConfig: (data) => api.post('/api/wireguard/peers/config', data),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Peer Registry API
|
// Peer Registry API
|
||||||
export const peerAPI = {
|
export const peerAPI = {
|
||||||
getPeers: () => api.get('/api/peers'),
|
getPeers: () => api.get('/api/peers'),
|
||||||
addPeer: (peer) => api.post('/api/peers', peer),
|
addPeer: (peer) => api.post('/api/peers', peer),
|
||||||
removePeer: (peerName) => api.delete(`/api/peers/${peerName}`),
|
removePeer: (peerName) => api.delete(`/api/peers/${peerName}`),
|
||||||
registerPeer: (data) => api.post('/api/peers/register', data),
|
registerPeer: (data) => api.post('/api/peers/register', data),
|
||||||
unregisterPeer: (peerName) => api.delete(`/api/peers/${peerName}/unregister`),
|
unregisterPeer: (peerName) => api.delete(`/api/peers/${peerName}/unregister`),
|
||||||
updatePeerIP: (peerName, data) => api.put(`/api/peers/${peerName}/update-ip`, data),
|
updatePeerIP: (peerName, data) => api.put(`/api/peers/${peerName}/update-ip`, data),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Email Services API
|
// Email Services API
|
||||||
export const emailAPI = {
|
export const emailAPI = {
|
||||||
getUsers: () => api.get('/api/email/users'),
|
getUsers: () => api.get('/api/email/users'),
|
||||||
createUser: (user) => api.post('/api/email/users', user),
|
createUser: (user) => api.post('/api/email/users', user),
|
||||||
deleteUser: (username) => api.delete(`/api/email/users/${username}`),
|
deleteUser: (username) => api.delete(`/api/email/users/${username}`),
|
||||||
getStatus: () => api.get('/api/email/status'),
|
getStatus: () => api.get('/api/email/status'),
|
||||||
testConnectivity: () => api.get('/api/email/connectivity'),
|
testConnectivity: () => api.get('/api/email/connectivity'),
|
||||||
sendEmail: (data) => api.post('/api/email/send', data),
|
sendEmail: (data) => api.post('/api/email/send', data),
|
||||||
getMailboxInfo: (username) => api.get(`/api/email/mailbox/${username}`),
|
getMailboxInfo: (username) => api.get(`/api/email/mailbox/${username}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calendar Services API
|
// Calendar Services API
|
||||||
export const calendarAPI = {
|
export const calendarAPI = {
|
||||||
getUsers: () => api.get('/api/calendar/users'),
|
getUsers: () => api.get('/api/calendar/users'),
|
||||||
createUser: (user) => api.post('/api/calendar/users', user),
|
createUser: (user) => api.post('/api/calendar/users', user),
|
||||||
deleteUser: (username) => api.delete(`/api/calendar/users/${username}`),
|
deleteUser: (username) => api.delete(`/api/calendar/users/${username}`),
|
||||||
createCalendar: (data) => api.post('/api/calendar/calendars', data),
|
createCalendar: (data) => api.post('/api/calendar/calendars', data),
|
||||||
addEvent: (data) => api.post('/api/calendar/events', data),
|
addEvent: (data) => api.post('/api/calendar/events', data),
|
||||||
getEvents: (username, calendarName, params) =>
|
getEvents: (username, calendarName, params) =>
|
||||||
api.get(`/api/calendar/events/${username}/${calendarName}`, { params }),
|
api.get(`/api/calendar/events/${username}/${calendarName}`, { params }),
|
||||||
getStatus: () => api.get('/api/calendar/status'),
|
getStatus: () => api.get('/api/calendar/status'),
|
||||||
testConnectivity: () => api.get('/api/calendar/connectivity'),
|
testConnectivity: () => api.get('/api/calendar/connectivity'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// File Services API
|
// File Services API
|
||||||
export const fileAPI = {
|
export const fileAPI = {
|
||||||
getUsers: () => api.get('/api/files/users'),
|
getUsers: () => api.get('/api/files/users'),
|
||||||
createUser: (user) => api.post('/api/files/users', user),
|
createUser: (user) => api.post('/api/files/users', user),
|
||||||
deleteUser: (username) => api.delete(`/api/files/users/${username}`),
|
deleteUser: (username) => api.delete(`/api/files/users/${username}`),
|
||||||
createFolder: (data) => api.post('/api/files/folders', data),
|
createFolder: (data) => api.post('/api/files/folders', data),
|
||||||
deleteFolder: (username, folderPath) => api.delete(`/api/files/folders/${username}/${folderPath}`),
|
deleteFolder: (username, folderPath) => api.delete(`/api/files/folders/${username}/${folderPath}`),
|
||||||
uploadFile: (username, file, path) => {
|
uploadFile: (username, file, path) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('path', path);
|
formData.append('path', path);
|
||||||
return api.post(`/api/files/upload/${username}`, formData, {
|
return api.post(`/api/files/upload/${username}`, formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
downloadFile: (username, filePath) => api.get(`/api/files/download/${username}/${filePath}`),
|
downloadFile: (username, filePath) => api.get(`/api/files/download/${username}/${filePath}`),
|
||||||
deleteFile: (username, filePath) => api.delete(`/api/files/delete/${username}/${filePath}`),
|
deleteFile: (username, filePath) => api.delete(`/api/files/delete/${username}/${filePath}`),
|
||||||
listFiles: (username, folder = '') => api.get(`/api/files/list/${username}`, { params: { folder } }),
|
listFiles: (username, folder = '') => api.get(`/api/files/list/${username}`, { params: { folder } }),
|
||||||
getStatus: () => api.get('/api/files/status'),
|
getStatus: () => api.get('/api/files/status'),
|
||||||
testConnectivity: () => api.get('/api/files/connectivity'),
|
testConnectivity: () => api.get('/api/files/connectivity'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Routing API
|
// Routing API
|
||||||
export const routingAPI = {
|
export const routingAPI = {
|
||||||
getStatus: () => api.get('/api/routing/status'),
|
getStatus: () => api.get('/api/routing/status'),
|
||||||
// NAT
|
// NAT
|
||||||
getNatRules: () => api.get('/api/routing/nat'),
|
getNatRules: () => api.get('/api/routing/nat'),
|
||||||
addNatRule: (rule) => api.post('/api/routing/nat', rule),
|
addNatRule: (rule) => api.post('/api/routing/nat', rule),
|
||||||
deleteNatRule: (ruleId) => api.delete(`/api/routing/nat/${ruleId}`),
|
deleteNatRule: (ruleId) => api.delete(`/api/routing/nat/${ruleId}`),
|
||||||
// Peer Routes
|
// Peer Routes
|
||||||
getPeerRoutes: () => api.get('/api/routing/peers'),
|
getPeerRoutes: () => api.get('/api/routing/peers'),
|
||||||
addPeerRoute: (route) => api.post('/api/routing/peers', route),
|
addPeerRoute: (route) => api.post('/api/routing/peers', route),
|
||||||
deletePeerRoute: (peerName) => api.delete(`/api/routing/peers/${peerName}`),
|
deletePeerRoute: (peerName) => api.delete(`/api/routing/peers/${peerName}`),
|
||||||
// Firewall
|
// Firewall
|
||||||
getFirewallRules: () => api.get('/api/routing/firewall'),
|
getFirewallRules: () => api.get('/api/routing/firewall'),
|
||||||
addFirewallRule: (rule) => api.post('/api/routing/firewall', rule),
|
addFirewallRule: (rule) => api.post('/api/routing/firewall', rule),
|
||||||
deleteFirewallRule: (ruleId) => api.delete(`/api/routing/firewall/${ruleId}`),
|
deleteFirewallRule: (ruleId) => api.delete(`/api/routing/firewall/${ruleId}`),
|
||||||
// Other
|
// Other
|
||||||
addExitNode: (node) => api.post('/api/routing/exit-nodes', node),
|
addExitNode: (node) => api.post('/api/routing/exit-nodes', node),
|
||||||
addBridgeRoute: (route) => api.post('/api/routing/bridge', route),
|
addBridgeRoute: (route) => api.post('/api/routing/bridge', route),
|
||||||
addSplitRoute: (route) => api.post('/api/routing/split', route),
|
addSplitRoute: (route) => api.post('/api/routing/split', route),
|
||||||
testConnectivity: (data) => api.post('/api/routing/connectivity', data),
|
testConnectivity: (data) => api.post('/api/routing/connectivity', data),
|
||||||
getLogs: (lines = 50) => api.get('/api/routing/logs', { params: { lines } }),
|
getLogs: (lines = 50) => api.get('/api/routing/logs', { params: { lines } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Vault & Trust API
|
// Vault & Trust API
|
||||||
export const vaultAPI = {
|
export const vaultAPI = {
|
||||||
getStatus: () => api.get('/api/vault/status'),
|
getStatus: () => api.get('/api/vault/status'),
|
||||||
getCertificates: () => api.get('/api/vault/certificates'),
|
getCertificates: () => api.get('/api/vault/certificates'),
|
||||||
generateCertificate: (data) => api.post('/api/vault/certificates', data),
|
generateCertificate: (data) => api.post('/api/vault/certificates', data),
|
||||||
revokeCertificate: (commonName) => api.delete(`/api/vault/certificates/${commonName}`),
|
revokeCertificate: (commonName) => api.delete(`/api/vault/certificates/${commonName}`),
|
||||||
getCACertificate: () => api.get('/api/vault/ca/certificate'),
|
getCACertificate: () => api.get('/api/vault/ca/certificate'),
|
||||||
getAgePublicKey: () => api.get('/api/vault/age/public-key'),
|
getAgePublicKey: () => api.get('/api/vault/age/public-key'),
|
||||||
getTrustedKeys: () => api.get('/api/vault/trust/keys'),
|
getTrustedKeys: () => api.get('/api/vault/trust/keys'),
|
||||||
addTrustedKey: (data) => api.post('/api/vault/trust/keys', data),
|
addTrustedKey: (data) => api.post('/api/vault/trust/keys', data),
|
||||||
removeTrustedKey: (name) => api.delete(`/api/vault/trust/keys/${name}`),
|
removeTrustedKey: (name) => api.delete(`/api/vault/trust/keys/${name}`),
|
||||||
verifyTrustChain: (data) => api.post('/api/vault/trust/verify', data),
|
verifyTrustChain: (data) => api.post('/api/vault/trust/verify', data),
|
||||||
getTrustChains: () => api.get('/api/vault/trust/chains'),
|
getTrustChains: () => api.get('/api/vault/trust/chains'),
|
||||||
// Secrets management
|
// Secrets management
|
||||||
listSecrets: () => api.get('/api/vault/secrets'),
|
listSecrets: () => api.get('/api/vault/secrets'),
|
||||||
storeSecret: (name, value) => api.post('/api/vault/secrets', { name, value }),
|
storeSecret: (name, value) => api.post('/api/vault/secrets', { name, value }),
|
||||||
getSecret: (name) => api.get(`/api/vault/secrets/${name}`),
|
getSecret: (name) => api.get(`/api/vault/secrets/${name}`),
|
||||||
deleteSecret: (name) => api.delete(`/api/vault/secrets/${name}`),
|
deleteSecret: (name) => api.delete(`/api/vault/secrets/${name}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Services API
|
// Services API
|
||||||
export const servicesAPI = {
|
export const servicesAPI = {
|
||||||
getAllStatus: () => api.get('/api/services/status'),
|
getAllStatus: () => api.get('/api/services/status'),
|
||||||
testAllConnectivity: () => api.get('/api/services/connectivity'),
|
testAllConnectivity: () => api.get('/api/services/connectivity'),
|
||||||
};
|
startService: (serviceName) => api.post(`/api/services/bus/services/${serviceName}/start`),
|
||||||
|
stopService: (serviceName) => api.post(`/api/services/bus/services/${serviceName}/stop`),
|
||||||
// Health check
|
restartService: (serviceName) => api.post(`/api/services/bus/services/${serviceName}/restart`),
|
||||||
export const healthAPI = {
|
};
|
||||||
check: () => api.get('/health'),
|
|
||||||
};
|
// Health check
|
||||||
|
export const healthAPI = {
|
||||||
// Monitoring API
|
check: () => api.get('/health'),
|
||||||
export const monitoringAPI = {
|
};
|
||||||
getBackendLogs: (lines = 100) => api.get('/api/logs', { params: { lines } }),
|
|
||||||
getHealthHistory: () => api.get('/api/health/history'),
|
// Monitoring API
|
||||||
};
|
export const monitoringAPI = {
|
||||||
|
getBackendLogs: (lines = 100) => api.get('/api/logs', { params: { lines } }),
|
||||||
// Container Management API
|
getHealthHistory: () => api.get('/api/health/history'),
|
||||||
export const containerAPI = {
|
};
|
||||||
// Containers
|
|
||||||
listContainers: () => api.get('/api/containers'),
|
// Container Management API
|
||||||
startContainer: (name) => api.post(`/api/containers/${name}/start`),
|
export const containerAPI = {
|
||||||
stopContainer: (name) => api.post(`/api/containers/${name}/stop`),
|
// Containers
|
||||||
restartContainer: (name) => api.post(`/api/containers/${name}/restart`),
|
listContainers: () => api.get('/api/containers'),
|
||||||
getContainerLogs: (name, tail = 100) => api.get(`/api/containers/${name}/logs`, { params: { tail } }),
|
startContainer: (name) => api.post(`/api/containers/${name}/start`),
|
||||||
getContainerStats: (name) => api.get(`/api/containers/${name}/stats`),
|
stopContainer: (name) => api.post(`/api/containers/${name}/stop`),
|
||||||
createContainer: (data) => api.post('/api/containers', data), // data may include 'secrets' array
|
restartContainer: (name) => api.post(`/api/containers/${name}/restart`),
|
||||||
removeContainer: (name, force = false) => api.delete(`/api/containers/${name}`, { params: { force } }),
|
getContainerLogs: (name, tail = 100) => api.get(`/api/containers/${name}/logs`, { params: { tail } }),
|
||||||
// Images
|
getContainerStats: (name) => api.get(`/api/containers/${name}/stats`),
|
||||||
listImages: () => api.get('/api/images'),
|
createContainer: (data) => api.post('/api/containers', data), // data may include 'secrets' array
|
||||||
pullImage: (image) => api.post('/api/images/pull', { image }),
|
removeContainer: (name, force = false) => api.delete(`/api/containers/${name}`, { params: { force } }),
|
||||||
removeImage: (image, force = false) => api.delete(`/api/images/${image}`, { params: { force } }),
|
// Images
|
||||||
// Volumes
|
listImages: () => api.get('/api/images'),
|
||||||
listVolumes: () => api.get('/api/volumes'),
|
pullImage: (image) => api.post('/api/images/pull', { image }),
|
||||||
createVolume: (name) => api.post('/api/volumes', { name }),
|
removeImage: (image, force = false) => api.delete(`/api/images/${image}`, { params: { force } }),
|
||||||
removeVolume: (name, force = false) => api.delete(`/api/volumes/${name}`, { params: { force } }),
|
// Volumes
|
||||||
};
|
listVolumes: () => api.get('/api/volumes'),
|
||||||
|
createVolume: (name) => api.post('/api/volumes', { name }),
|
||||||
|
removeVolume: (name, force = false) => api.delete(`/api/volumes/${name}`, { params: { force } }),
|
||||||
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
Reference in New Issue
Block a user