#!/usr/bin/env python3 """ SSH Sync wrapper for Tailscale SSH Sync Agent. Python interface to sshsync CLI operations. """ import subprocess import sys from pathlib import Path from typing import Dict, List, Optional, Tuple import json import logging # Add utils to path sys.path.insert(0, str(Path(__file__).parent)) from utils.helpers import parse_ssh_config, parse_sshsync_config, format_bytes, format_duration from utils.validators import validate_host, validate_group, validate_path_exists, validate_timeout, validate_command logger = logging.getLogger(__name__) def get_host_status(group: Optional[str] = None) -> Dict: """ Get online/offline status of hosts. Args: group: Optional group to filter (None = all hosts) Returns: Dict with status info Example: >>> status = get_host_status() >>> status['online_count'] 8 """ try: # Run sshsync ls --with-status cmd = ["sshsync", "ls", "--with-status"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode != 0: return {'error': result.stderr, 'hosts': []} # Parse output hosts = [] for line in result.stdout.strip().split('\n'): if not line or line.startswith('Host') or line.startswith('---'): continue parts = line.split() if len(parts) >= 2: host_name = parts[0] status = parts[1] if len(parts) > 1 else 'unknown' hosts.append({ 'host': host_name, 'online': status.lower() in ['online', 'reachable', 'āœ“'], 'status': status }) # Filter by group if specified if group: groups_config = parse_sshsync_config() group_hosts = groups_config.get(group, []) hosts = [h for h in hosts if h['host'] in group_hosts] online_count = sum(1 for h in hosts if h['online']) return { 'hosts': hosts, 'total_count': len(hosts), 'online_count': online_count, 'offline_count': len(hosts) - online_count, 'availability_pct': (online_count / len(hosts) * 100) if hosts else 0 } except Exception as e: logger.error(f"Error getting host status: {e}") return {'error': str(e), 'hosts': []} def execute_on_all(command: str, timeout: int = 10, dry_run: bool = False) -> Dict: """ Execute command on all hosts. Args: command: Command to execute timeout: Timeout in seconds dry_run: If True, don't actually execute Returns: Dict with results per host Example: >>> result = execute_on_all("uptime", timeout=15) >>> len(result['results']) 10 """ validate_command(command) validate_timeout(timeout) if dry_run: return { 'dry_run': True, 'command': command, 'message': 'Would execute on all hosts' } try: cmd = ["sshsync", "all", f"--timeout={timeout}", command] result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 30) # Parse results (format varies, simplified here) return { 'success': result.returncode == 0, 'stdout': result.stdout, 'stderr': result.stderr, 'command': command } except subprocess.TimeoutExpired: return {'error': f'Command timed out after {timeout}s'} except Exception as e: return {'error': str(e)} def execute_on_group(group: str, command: str, timeout: int = 10, dry_run: bool = False) -> Dict: """ Execute command on specific group. Args: group: Group name command: Command to execute timeout: Timeout in seconds dry_run: Preview without executing Returns: Dict with execution results Example: >>> result = execute_on_group("web-servers", "df -h /var/www") >>> result['success'] True """ groups_config = parse_sshsync_config() validate_group(group, list(groups_config.keys())) validate_command(command) validate_timeout(timeout) if dry_run: group_hosts = groups_config.get(group, []) return { 'dry_run': True, 'group': group, 'hosts': group_hosts, 'command': command, 'message': f'Would execute on {len(group_hosts)} hosts in group {group}' } try: cmd = ["sshsync", "group", f"--timeout={timeout}", group, command] result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 30) return { 'success': result.returncode == 0, 'group': group, 'stdout': result.stdout, 'stderr': result.stderr, 'command': command } except subprocess.TimeoutExpired: return {'error': f'Command timed out after {timeout}s'} except Exception as e: return {'error': str(e)} def execute_on_host(host: str, command: str, timeout: int = 10) -> Dict: """ Execute command on single host. Args: host: Host name command: Command to execute timeout: Timeout in seconds Returns: Dict with result Example: >>> result = execute_on_host("web-01", "hostname") >>> result['stdout'] "web-01" """ ssh_hosts = parse_ssh_config() validate_host(host, list(ssh_hosts.keys())) validate_command(command) validate_timeout(timeout) try: cmd = ["ssh", "-o", f"ConnectTimeout={timeout}", host, command] result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 5) return { 'success': result.returncode == 0, 'host': host, 'stdout': result.stdout, 'stderr': result.stderr, 'command': command } except subprocess.TimeoutExpired: return {'error': f'Command timed out after {timeout}s'} except Exception as e: return {'error': str(e)} def push_to_hosts(local_path: str, remote_path: str, hosts: Optional[List[str]] = None, group: Optional[str] = None, recurse: bool = False, dry_run: bool = False) -> Dict: """ Push files to hosts. Args: local_path: Local file/directory path remote_path: Remote destination path hosts: Specific hosts (None = all if group also None) group: Group name recurse: Recursive copy dry_run: Preview without executing Returns: Dict with push results Example: >>> result = push_to_hosts("./dist", "/var/www/app", group="production", recurse=True) >>> result['success'] True """ validate_path_exists(local_path) if dry_run: return { 'dry_run': True, 'local_path': local_path, 'remote_path': remote_path, 'hosts': hosts, 'group': group, 'recurse': recurse, 'message': 'Would push files' } try: cmd = ["sshsync", "push"] if hosts: for host in hosts: cmd.extend(["--host", host]) elif group: cmd.extend(["--group", group]) else: cmd.append("--all") if recurse: cmd.append("--recurse") cmd.extend([local_path, remote_path]) result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) return { 'success': result.returncode == 0, 'stdout': result.stdout, 'stderr': result.stderr, 'local_path': local_path, 'remote_path': remote_path } except subprocess.TimeoutExpired: return {'error': 'Push operation timed out'} except Exception as e: return {'error': str(e)} def pull_from_host(host: str, remote_path: str, local_path: str, recurse: bool = False, dry_run: bool = False) -> Dict: """ Pull files from host. Args: host: Host to pull from remote_path: Remote file/directory path local_path: Local destination path recurse: Recursive copy dry_run: Preview without executing Returns: Dict with pull results Example: >>> result = pull_from_host("web-01", "/var/log/nginx", "./logs", recurse=True) >>> result['success'] True """ ssh_hosts = parse_ssh_config() validate_host(host, list(ssh_hosts.keys())) if dry_run: return { 'dry_run': True, 'host': host, 'remote_path': remote_path, 'local_path': local_path, 'recurse': recurse, 'message': f'Would pull from {host}' } try: cmd = ["sshsync", "pull", "--host", host] if recurse: cmd.append("--recurse") cmd.extend([remote_path, local_path]) result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) return { 'success': result.returncode == 0, 'host': host, 'stdout': result.stdout, 'stderr': result.stderr, 'remote_path': remote_path, 'local_path': local_path } except subprocess.TimeoutExpired: return {'error': 'Pull operation timed out'} except Exception as e: return {'error': str(e)} def list_hosts(with_status: bool = True) -> Dict: """ List all configured hosts. Args: with_status: Include online/offline status Returns: Dict with hosts info Example: >>> result = list_hosts(with_status=True) >>> len(result['hosts']) 10 """ if with_status: return get_host_status() else: ssh_hosts = parse_ssh_config() return { 'hosts': [{'host': name} for name in ssh_hosts.keys()], 'count': len(ssh_hosts) } def get_groups() -> Dict[str, List[str]]: """ Get all defined groups and their members. Returns: Dict mapping group names to host lists Example: >>> groups = get_groups() >>> groups['production'] ['prod-web-01', 'prod-db-01'] """ return parse_sshsync_config() def main(): """Test sshsync wrapper functions.""" print("Testing sshsync wrapper...\n") print("1. List hosts:") result = list_hosts(with_status=False) print(f" Found {result.get('count', 0)} hosts") print("\n2. Get groups:") groups = get_groups() print(f" Found {len(groups)} groups") for group, hosts in groups.items(): print(f" - {group}: {len(hosts)} hosts") print("\n3. Test dry-run:") result = execute_on_all("uptime", dry_run=True) print(f" Dry-run: {result.get('message', 'OK')}") print("\nāœ… sshsync wrapper tested") if __name__ == "__main__": main()