Copy CCLI source to backend for Docker builds

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-07-10 12:46:52 +10:00
parent baa48bfcd4
commit 8b32d54e79
26 changed files with 2930 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
"""
Simple SSH Executor for CCLI
Uses subprocess for SSH execution without external dependencies.
"""
import asyncio
import subprocess
import time
import logging
from dataclasses import dataclass
from typing import Optional, Dict, Any
@dataclass
class SSHResult:
"""Result of an SSH command execution"""
stdout: str
stderr: str
returncode: int
duration: float
host: str
command: str
@dataclass
class SSHConfig:
"""SSH connection configuration"""
host: str
username: str = "tony"
connect_timeout: int = 5
command_timeout: int = 30
max_retries: int = 2
ssh_options: Optional[Dict[str, str]] = None
def __post_init__(self):
if self.ssh_options is None:
self.ssh_options = {
"BatchMode": "yes",
"ConnectTimeout": str(self.connect_timeout),
"StrictHostKeyChecking": "no"
}
class SimpleSSHExecutor:
"""Simple SSH command executor using subprocess"""
def __init__(self):
self.logger = logging.getLogger(__name__)
async def execute(self, config: SSHConfig, command: str, **kwargs) -> SSHResult:
"""Execute a command via SSH with retries and error handling"""
for attempt in range(config.max_retries + 1):
try:
return await self._execute_once(config, command, **kwargs)
except Exception as e:
self.logger.warning(f"SSH execution attempt {attempt + 1} failed for {config.host}: {e}")
if attempt < config.max_retries:
await asyncio.sleep(1) # Brief delay before retry
else:
# Final attempt failed
raise Exception(f"SSH execution failed after {config.max_retries + 1} attempts: {e}")
async def _execute_once(self, config: SSHConfig, command: str, **kwargs) -> SSHResult:
"""Execute command once via SSH"""
start_time = time.time()
# Build SSH command
ssh_cmd = self._build_ssh_command(config, command)
try:
# Execute command with timeout
process = await asyncio.create_subprocess_exec(
*ssh_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
**kwargs
)
stdout, stderr = await asyncio.wait_for(
process.communicate(),
timeout=config.command_timeout
)
duration = time.time() - start_time
return SSHResult(
stdout=stdout.decode('utf-8'),
stderr=stderr.decode('utf-8'),
returncode=process.returncode,
duration=duration,
host=config.host,
command=command
)
except asyncio.TimeoutError:
duration = time.time() - start_time
raise Exception(f"SSH command timeout after {config.command_timeout}s on {config.host}")
except Exception as e:
duration = time.time() - start_time
self.logger.error(f"SSH execution error on {config.host}: {e}")
raise
def _build_ssh_command(self, config: SSHConfig, command: str) -> list:
"""Build SSH command array"""
ssh_cmd = ["ssh"]
# Add SSH options
for option, value in config.ssh_options.items():
ssh_cmd.extend(["-o", f"{option}={value}"])
# Add destination
if config.username:
destination = f"{config.username}@{config.host}"
else:
destination = config.host
ssh_cmd.append(destination)
ssh_cmd.append(command)
return ssh_cmd
async def test_connection(self, config: SSHConfig) -> bool:
"""Test if SSH connection is working"""
try:
result = await self.execute(config, "echo 'connection_test'")
return result.returncode == 0 and "connection_test" in result.stdout
except Exception as e:
self.logger.error(f"Connection test failed for {config.host}: {e}")
return False
async def get_connection_stats(self) -> Dict[str, Any]:
"""Get statistics about current connections (simplified for subprocess)"""
return {
"total_connections": 0, # subprocess doesn't maintain persistent connections
"connection_type": "subprocess"
}
async def cleanup(self):
"""Cleanup resources (no-op for subprocess)"""
pass
# Alias for compatibility
SSHExecutor = SimpleSSHExecutor