478 lines
17 KiB
Python
478 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Enhanced CLI Tool for Personal Internet Cell
|
|
Advanced command-line interface with interactive mode and service management
|
|
"""
|
|
|
|
import argparse
|
|
import requests
|
|
import json
|
|
import sys
|
|
import os
|
|
import cmd
|
|
from datetime import datetime
|
|
from typing import Dict, List, Optional, Any
|
|
import yaml
|
|
from pathlib import Path
|
|
|
|
# Optional readline import for better CLI experience
|
|
try:
|
|
import readline
|
|
except ImportError:
|
|
# readline not available on Windows, that's okay
|
|
pass
|
|
|
|
API_BASE = "http://localhost:3000/api"
|
|
|
|
class APIClient:
|
|
"""API client for making requests to the cell API"""
|
|
|
|
def __init__(self, base_url: str = API_BASE):
|
|
self.base_url = base_url
|
|
self.session = requests.Session()
|
|
self.session.headers.update({'Content-Type': 'application/json'})
|
|
|
|
def request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Optional[Dict]:
|
|
"""Make API request"""
|
|
url = f"{self.base_url}{endpoint}"
|
|
try:
|
|
if method == "GET":
|
|
response = self.session.get(url)
|
|
elif method == "POST":
|
|
response = self.session.post(url, json=data)
|
|
elif method == "PUT":
|
|
response = self.session.put(url, json=data)
|
|
elif method == "DELETE":
|
|
response = self.session.delete(url)
|
|
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"❌ API Error: {e}")
|
|
return None
|
|
|
|
class ConfigManager:
|
|
"""Configuration management for CLI"""
|
|
|
|
def __init__(self, config_dir: str = "~/.picell"):
|
|
self.config_dir = Path(config_dir).expanduser()
|
|
self.config_file = self.config_dir / "cli_config.yaml"
|
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
self.config = self._load_config()
|
|
|
|
def _load_config(self) -> Dict[str, Any]:
|
|
"""Load configuration from file"""
|
|
if self.config_file.exists():
|
|
try:
|
|
with open(self.config_file, 'r') as f:
|
|
return yaml.safe_load(f) or {}
|
|
except Exception as e:
|
|
print(f"Warning: Could not load config: {e}")
|
|
return {}
|
|
|
|
def _save_config(self):
|
|
"""Save configuration to file"""
|
|
try:
|
|
with open(self.config_file, 'w') as f:
|
|
yaml.dump(self.config, f, default_flow_style=False)
|
|
except Exception as e:
|
|
print(f"Warning: Could not save config: {e}")
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""Get configuration value"""
|
|
return self.config.get(key, default)
|
|
|
|
def set(self, key: str, value: Any):
|
|
"""Set configuration value"""
|
|
self.config[key] = value
|
|
self._save_config()
|
|
|
|
def export_config(self, format: str = 'json') -> str:
|
|
"""Export configuration"""
|
|
if format == 'json':
|
|
return json.dumps(self.config, indent=2)
|
|
elif format == 'yaml':
|
|
return yaml.dump(self.config, default_flow_style=False)
|
|
else:
|
|
raise ValueError(f"Unsupported format: {format}")
|
|
|
|
def import_config(self, config_data: str, format: str = 'json'):
|
|
"""Import configuration"""
|
|
try:
|
|
if format == 'json':
|
|
new_config = json.loads(config_data)
|
|
elif format == 'yaml':
|
|
new_config = yaml.safe_load(config_data)
|
|
else:
|
|
raise ValueError(f"Unsupported format: {format}")
|
|
|
|
self.config.update(new_config)
|
|
self._save_config()
|
|
print("✅ Configuration imported successfully")
|
|
except Exception as e:
|
|
print(f"❌ Error importing configuration: {e}")
|
|
|
|
class EnhancedCLI(cmd.Cmd):
|
|
"""Interactive CLI shell"""
|
|
|
|
intro = """
|
|
🚀 Personal Internet Cell - Enhanced CLI
|
|
Type 'help' for available commands or 'help <command>' for detailed help.
|
|
Type 'exit' or 'quit' to exit.
|
|
"""
|
|
prompt = "picell> "
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.api_client = APIClient()
|
|
self.config_manager = ConfigManager()
|
|
self.current_service = None
|
|
|
|
def do_status(self, arg):
|
|
"""Show cell status"""
|
|
status = self.api_client.request("GET", "/status")
|
|
if status:
|
|
self._display_status(status)
|
|
else:
|
|
print("❌ Failed to get status")
|
|
|
|
def do_services(self, arg):
|
|
"""Show all services status"""
|
|
services = self.api_client.request("GET", "/services/status")
|
|
if services:
|
|
self._display_services(services)
|
|
else:
|
|
print("❌ Failed to get services status")
|
|
|
|
def do_peers(self, arg):
|
|
"""List configured peers"""
|
|
peers = self.api_client.request("GET", "/peers")
|
|
if peers is not None:
|
|
if not peers:
|
|
print("📭 No peers configured.")
|
|
return
|
|
self._display_peers(peers)
|
|
else:
|
|
print("❌ Failed to fetch peers")
|
|
|
|
def do_add_peer(self, arg):
|
|
"""Add a new peer: add_peer <name> <ip> <public_key>"""
|
|
args = arg.split()
|
|
if len(args) != 3:
|
|
print("❌ Usage: add_peer <name> <ip> <public_key>")
|
|
return
|
|
|
|
name, ip, public_key = args
|
|
data = {"name": name, "ip": ip, "public_key": public_key}
|
|
result = self.api_client.request("POST", "/peers", data)
|
|
if result:
|
|
print(f"✅ {result.get('message', 'Peer added successfully')}")
|
|
else:
|
|
print("❌ Failed to add peer")
|
|
|
|
def do_remove_peer(self, arg):
|
|
"""Remove a peer: remove_peer <name>"""
|
|
if not arg:
|
|
print("❌ Usage: remove_peer <name>")
|
|
return
|
|
|
|
result = self.api_client.request("DELETE", f"/peers/{arg}")
|
|
if result:
|
|
print(f"✅ {result.get('message', 'Peer removed successfully')}")
|
|
else:
|
|
print("❌ Failed to remove peer")
|
|
|
|
def do_config(self, arg):
|
|
"""Show cell configuration"""
|
|
config = self.api_client.request("GET", "/config")
|
|
if config:
|
|
self._display_config(config)
|
|
else:
|
|
print("❌ Failed to get configuration")
|
|
|
|
def do_update_config(self, arg):
|
|
"""Update configuration: update_config <key> <value>"""
|
|
args = arg.split(' ', 1)
|
|
if len(args) != 2:
|
|
print("❌ Usage: update_config <key> <value>")
|
|
return
|
|
|
|
key, value = args
|
|
data = {key: value}
|
|
result = self.api_client.request("PUT", "/config", data)
|
|
if result:
|
|
print(f"✅ {result.get('message', 'Configuration updated')}")
|
|
else:
|
|
print("❌ Failed to update configuration")
|
|
|
|
def do_logs(self, arg):
|
|
"""Show service logs: logs [service] [lines]"""
|
|
args = arg.split()
|
|
service = args[0] if args else "api"
|
|
lines = int(args[1]) if len(args) > 1 else 50
|
|
|
|
logs = self.api_client.request("GET", f"/logs?lines={lines}")
|
|
if logs and "log" in logs:
|
|
print(f"📋 Logs for {service} (last {lines} lines):")
|
|
print("-" * 50)
|
|
print(logs["log"])
|
|
else:
|
|
print("❌ Failed to get logs")
|
|
|
|
def do_health(self, arg):
|
|
"""Show health check results"""
|
|
health = self.api_client.request("GET", "/health/history")
|
|
if health:
|
|
self._display_health(health)
|
|
else:
|
|
print("❌ Failed to get health data")
|
|
|
|
def do_backup(self, arg):
|
|
"""Create configuration backup"""
|
|
backup = self.api_client.request("POST", "/config/backup")
|
|
if backup:
|
|
print(f"✅ Backup created: {backup.get('backup_id', 'unknown')}")
|
|
else:
|
|
print("❌ Failed to create backup")
|
|
|
|
def do_restore(self, arg):
|
|
"""Restore configuration from backup: restore <backup_id>"""
|
|
if not arg:
|
|
print("❌ Usage: restore <backup_id>")
|
|
return
|
|
|
|
result = self.api_client.request("POST", f"/config/restore/{arg}")
|
|
if result:
|
|
print(f"✅ Configuration restored from backup: {arg}")
|
|
else:
|
|
print("❌ Failed to restore configuration")
|
|
|
|
def do_backups(self, arg):
|
|
"""List available backups"""
|
|
backups = self.api_client.request("GET", "/config/backups")
|
|
if backups:
|
|
self._display_backups(backups)
|
|
else:
|
|
print("❌ Failed to get backups")
|
|
|
|
def do_service(self, arg):
|
|
"""Switch to service context: service <service_name>"""
|
|
if not arg:
|
|
print("❌ Usage: service <service_name>")
|
|
return
|
|
|
|
self.current_service = arg
|
|
print(f"🔧 Switched to service context: {arg}")
|
|
self.prompt = f"picell:{arg}> "
|
|
|
|
def do_exit(self, arg):
|
|
"""Exit the CLI"""
|
|
print("👋 Goodbye!")
|
|
return True
|
|
|
|
def do_quit(self, arg):
|
|
"""Exit the CLI"""
|
|
return self.do_exit(arg)
|
|
|
|
def do_EOF(self, arg):
|
|
"""Exit on EOF"""
|
|
return self.do_exit(arg)
|
|
|
|
def _display_status(self, status: Dict[str, Any]):
|
|
"""Display cell status"""
|
|
print("📊 Personal Internet Cell Status")
|
|
print("=" * 40)
|
|
print(f"Cell Name: {status.get('cell_name', 'Unknown')}")
|
|
print(f"Domain: {status.get('domain', 'Unknown')}")
|
|
print(f"Peers: {status.get('peers_count', 0)}")
|
|
print(f"Uptime: {status.get('uptime', 0)} seconds")
|
|
|
|
print("\n🔧 Services:")
|
|
services = status.get('services', {})
|
|
for service, service_status in services.items():
|
|
if isinstance(service_status, dict):
|
|
running = service_status.get('running', False)
|
|
status_text = service_status.get('status', 'unknown')
|
|
else:
|
|
running = bool(service_status)
|
|
status_text = 'online' if running else 'offline'
|
|
|
|
status_icon = "🟢" if running else "🔴"
|
|
print(f" {status_icon} {service}: {status_text}")
|
|
|
|
def _display_services(self, services: Dict[str, Any]):
|
|
"""Display services status"""
|
|
print("🔧 Services Status")
|
|
print("=" * 40)
|
|
for service, status in services.items():
|
|
if service == 'timestamp':
|
|
continue
|
|
|
|
if isinstance(status, dict):
|
|
running = status.get('running', False)
|
|
status_text = status.get('status', 'unknown')
|
|
else:
|
|
running = bool(status)
|
|
status_text = 'online' if running else 'offline'
|
|
|
|
status_icon = "🟢" if running else "🔴"
|
|
print(f"{status_icon} {service}: {status_text}")
|
|
|
|
def _display_peers(self, peers: List[Dict[str, Any]]):
|
|
"""Display peers"""
|
|
print("👥 Configured Peers:")
|
|
print("=" * 40)
|
|
for peer in peers:
|
|
print(f"Name: {peer.get('name', 'Unknown')}")
|
|
print(f"IP: {peer.get('ip', 'Unknown')}")
|
|
print(f"Public Key: {peer.get('public_key', 'Unknown')[:20]}...")
|
|
print(f"Added: {peer.get('added_at', 'Unknown')}")
|
|
print("-" * 20)
|
|
|
|
def _display_config(self, config: Dict[str, Any]):
|
|
"""Display configuration"""
|
|
print("⚙️ Cell Configuration:")
|
|
print("=" * 40)
|
|
for key, value in config.items():
|
|
print(f"{key}: {value}")
|
|
|
|
def _display_health(self, health: List[Dict[str, Any]]):
|
|
"""Display health data"""
|
|
print("❤️ Health Check History")
|
|
print("=" * 40)
|
|
for entry in health[-5:]: # Show last 5 entries
|
|
timestamp = entry.get('timestamp', 'Unknown')
|
|
alerts = entry.get('alerts', [])
|
|
print(f"📅 {timestamp}")
|
|
if alerts:
|
|
for alert in alerts:
|
|
print(f" ⚠️ {alert}")
|
|
print("-" * 20)
|
|
|
|
def _display_backups(self, backups: List[Dict[str, Any]]):
|
|
"""Display backups"""
|
|
print("💾 Available Backups:")
|
|
print("=" * 40)
|
|
for backup in backups:
|
|
print(f"ID: {backup.get('backup_id', 'Unknown')}")
|
|
print(f"Timestamp: {backup.get('timestamp', 'Unknown')}")
|
|
print(f"Services: {', '.join(backup.get('services', []))}")
|
|
print("-" * 20)
|
|
|
|
def batch_operations(commands: List[str]):
|
|
"""Execute batch operations"""
|
|
cli = EnhancedCLI()
|
|
for command in commands:
|
|
print(f"🔄 Executing: {command}")
|
|
cli.onecmd(command)
|
|
print()
|
|
|
|
def export_config(format: str = 'json') -> str:
|
|
"""Export configuration"""
|
|
config_manager = ConfigManager()
|
|
return config_manager.export_config(format)
|
|
|
|
def import_config(config_file: str, format: str = 'json') -> bool:
|
|
"""Import configuration"""
|
|
try:
|
|
with open(config_file, 'r') as f:
|
|
config_data = f.read()
|
|
|
|
config_manager = ConfigManager()
|
|
config_manager.import_config(config_data, format)
|
|
return True
|
|
except Exception as e:
|
|
print(f"❌ Error importing configuration: {e}")
|
|
return False
|
|
|
|
def service_wizard(service: str):
|
|
"""Interactive service configuration wizard"""
|
|
print(f"🔧 {service.title()} Service Configuration Wizard")
|
|
print("=" * 50)
|
|
|
|
config = {}
|
|
|
|
if service == 'network':
|
|
config['dns_port'] = input("DNS Port (default: 53): ") or 53
|
|
config['dhcp_range'] = input("DHCP Range (default: 10.0.0.100-10.0.0.200): ") or "10.0.0.100-10.0.0.200"
|
|
config['ntp_servers'] = input("NTP Servers (comma-separated): ").split(',') if input("NTP Servers (comma-separated): ") else []
|
|
|
|
elif service == 'wireguard':
|
|
config['port'] = int(input("WireGuard Port (default: 51820): ") or 51820)
|
|
config['address'] = input("WireGuard Address (default: 10.0.0.1/24): ") or "10.0.0.1/24"
|
|
print("Private key will be generated automatically")
|
|
|
|
elif service == 'email':
|
|
config['domain'] = input("Email Domain: ")
|
|
config['smtp_port'] = int(input("SMTP Port (default: 587): ") or 587)
|
|
config['imap_port'] = int(input("IMAP Port (default: 993): ") or 993)
|
|
|
|
else:
|
|
print(f"❌ Wizard not available for service: {service}")
|
|
return
|
|
|
|
# Save configuration
|
|
api_client = APIClient()
|
|
result = api_client.request("PUT", f"/config/{service}", config)
|
|
if result:
|
|
print(f"✅ {service.title()} configuration saved")
|
|
else:
|
|
print(f"❌ Failed to save {service} configuration")
|
|
|
|
def main():
|
|
"""Main CLI entry point"""
|
|
parser = argparse.ArgumentParser(description="Personal Internet Cell Enhanced CLI")
|
|
parser.add_argument('--interactive', '-i', action='store_true',
|
|
help='Start interactive mode')
|
|
parser.add_argument('--batch', '-b', nargs='+',
|
|
help='Execute batch commands')
|
|
parser.add_argument('--export-config', choices=['json', 'yaml'],
|
|
help='Export configuration')
|
|
parser.add_argument('--import-config', metavar='FILE',
|
|
help='Import configuration from file')
|
|
parser.add_argument('--wizard', metavar='SERVICE',
|
|
help='Run configuration wizard for service')
|
|
parser.add_argument('--status', action='store_true',
|
|
help='Show cell status')
|
|
parser.add_argument('--services', action='store_true',
|
|
help='Show all services status')
|
|
parser.add_argument('--peers', action='store_true',
|
|
help='List peers')
|
|
parser.add_argument('--logs', metavar='SERVICE',
|
|
help='Show service logs')
|
|
parser.add_argument('--health', action='store_true',
|
|
help='Show health data')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.interactive:
|
|
EnhancedCLI().cmdloop()
|
|
elif args.batch:
|
|
batch_operations(args.batch)
|
|
elif args.export_config:
|
|
print(export_config(args.export_config))
|
|
elif args.import_config:
|
|
format = 'json' if args.import_config.endswith('.json') else 'yaml'
|
|
import_config(args.import_config, format)
|
|
elif args.wizard:
|
|
service_wizard(args.wizard)
|
|
elif args.status:
|
|
cli = EnhancedCLI()
|
|
cli.do_status("")
|
|
elif args.services:
|
|
cli = EnhancedCLI()
|
|
cli.do_services("")
|
|
elif args.peers:
|
|
cli = EnhancedCLI()
|
|
cli.do_peers("")
|
|
elif args.logs:
|
|
cli = EnhancedCLI()
|
|
cli.do_logs(args.logs)
|
|
elif args.health:
|
|
cli = EnhancedCLI()
|
|
cli.do_health("")
|
|
else:
|
|
parser.print_help()
|
|
|
|
if __name__ == '__main__':
|
|
main() |