Open In Colab

๐Ÿš€ SSH Remote Execution Tutorialยถ

This tutorial demonstrates how to use Clustrix for automated SSH-based remote execution without a job scheduler. Perfect for executing functions on remote servers, workstations, or cloud instances.

โœจ New: Automated SSH Key Setupยถ

Clustrix now includes 15-second automated SSH key setup that eliminates manual configuration! No more spending 15-30 minutes on SSH setup.

๐Ÿ“‹ Prerequisitesยถ

  • Access to a remote server (cloud instance, workstation, or HPC login node)

  • Username and password for initial authentication

  • Python installed on the remote server

  • โœจ Thatโ€™s it! No manual SSH key setup required

[ ]:
# Install Clustrix (uncomment if needed)
# !pip install clustrix

import clustrix
from clustrix import cluster, configure, setup_ssh_keys_with_fallback
from clustrix.config import ClusterConfig
import numpy as np

print("โœ… Clustrix imported successfully!")
print("๐Ÿ“ฑ Look for the interactive widget that appeared above or below.")
print("๐Ÿ”‘ You can use the widget's SSH Key Setup section for easy configuration.")

๐Ÿ”‘ Step 1: Automated SSH Key Setupยถ

This is the magic step! Instead of manually setting up SSH keys, Clustrix does it automatically.

[ ]:
# ๐Ÿ”ง Configure your remote server details
# Replace these with your actual server information

config = ClusterConfig(
    cluster_type="ssh",
    cluster_host="your-server.example.com",    # Your server hostname or IP
    username="your-username",                   # Your username on the server
    port=22,                                     # SSH port (usually 22)

    # Remote execution settings
    remote_work_dir="/tmp/clustrix",            # Directory for temporary files
    python_executable="python3",                # Python command on remote server
    cleanup_on_success=True,                     # Clean up after successful execution
    max_parallel_jobs=5,                         # Limit concurrent executions
)

print("โœ… Server configuration created!")
print(f"๐ŸŽฏ Target: {config.cluster_host}")
print(f"๐Ÿ‘ค User: {config.username}")
print(f"๐Ÿ”Œ Port: {config.port}")
print("\n๐Ÿ”‘ Ready for automated SSH key setup...")
[ ]:
# ๐Ÿš€ AUTOMATED SSH KEY SETUP
# This replaces 15-30 minutes of manual work with 15 seconds of automation!

print("๐Ÿ”„ Setting up SSH keys automatically...")
print("๐Ÿ’ก You'll be prompted for your password (this is normal and secure).")
print()

ssh_result = setup_ssh_keys_with_fallback(
    config=config,
    cluster_alias="my_server",       # Creates SSH alias for easy access
    key_type="ed25519",              # Modern, secure key type
    force_refresh=False,             # Set True to generate new keys
)

# ๐Ÿ“Š Display results
print("\n" + "="*60)
print("๐Ÿ”‘ SSH KEY SETUP RESULTS")
print("="*60)

if ssh_result["success"]:
    print("๐ŸŽ‰ SUCCESS! SSH keys configured automatically!")
    print(f"๐Ÿ”‘ Key path: {ssh_result['key_path']}")
    print(f"๐Ÿ“ฆ Key already existed: {ssh_result['key_already_existed']}")
    print(f"๐Ÿš€ Key deployed: {ssh_result['key_deployed']}")
    print(f"๐Ÿ”— Connection tested: {ssh_result['connection_tested']}")

    if "ssh_config_updated" in ssh_result.get("details", {}):
        print("โš™๏ธ SSH config updated with alias")
        print("\n๐ŸŽฏ You can now connect with: ssh my_server")

    print("\nโœจ What just happened:")
    print("   ๐Ÿ” Generated Ed25519 SSH key pair")
    print("   ๐Ÿ“ค Deployed public key to remote server")
    print("   ๐Ÿงน Cleaned up any conflicting old keys")
    print("   โš™๏ธ Updated SSH configuration")
    print("   โœ… Tested connection to verify success")

else:
    print("โŒ SSH key setup failed")
    print(f"๐Ÿ” Error: {ssh_result.get('error', 'Unknown error')}")

    if "details" in ssh_result:
        print("\n๐Ÿ”ง Troubleshooting details:")
        for key, value in ssh_result["details"].items():
            print(f"   {key}: {value}")

    print("\n๐Ÿ’ก Try:")
    print("   - Check hostname and username are correct")
    print("   - Verify network connectivity to the server")
    print("   - Test manual SSH connection first")

print("\n" + "="*60)

โš™๏ธ Step 2: Configure Clustrixยถ

Now that SSH keys are set up, configure Clustrix for remote execution:

[ ]:
# Configure Clustrix with the SSH setup
configure(
    cluster_type="ssh",
    cluster_host=config.cluster_host,
    username=config.username,
    port=config.port,

    # Remote environment
    remote_work_dir=config.remote_work_dir,
    python_executable=config.python_executable,

    # Execution settings
    cleanup_on_success=True,
    max_parallel_jobs=5,

    # Optional: Remote environment activation
    # conda_env_name="myenv",                    # Activate conda environment
    # virtualenv_path="/path/to/venv",           # Activate virtual environment
)

print("โœ… Clustrix configured for SSH remote execution!")
print(f"๐ŸŽฏ Target server: {config.cluster_host}")
print(f"๐Ÿ“ Remote work directory: {config.remote_work_dir}")
print(f"๐Ÿ Python executable: {config.python_executable}")
print("\n๐Ÿš€ Ready to execute functions remotely!")

๐Ÿงฎ Example 1: Basic Remote Computationยถ

Execute a simple mathematical computation remotely:

[ ]:
@cluster
def basic_remote_computation(n=1000000):
    """
    Simple computation executed on remote server.
    """
    import math
    import time
    import platform
    from datetime import datetime

    print(f"๐Ÿ–ฅ๏ธ Executing on: {platform.node()}")
    print(f"๐Ÿ Python version: {platform.python_version()}")
    print(f"โšก Starting computation at {datetime.now()}")
    print(f"๐Ÿ”ข Computing sum of squares for {n:,} numbers")

    start_time = time.time()

    # Compute sum of squares
    total = sum(i*i for i in range(n))

    # Compute some mathematical functions
    sqrt_total = math.sqrt(total)
    log_total = math.log(total)

    end_time = time.time()
    execution_time = end_time - start_time

    result = {
        'n': n,
        'sum_of_squares': total,
        'sqrt_sum': sqrt_total,
        'log_sum': log_total,
        'execution_time_seconds': execution_time,
        'hostname': platform.node(),
        'python_version': platform.python_version(),
        'completion_time': datetime.now().isoformat()
    }

    print(f"โœ… Computation completed in {execution_time:.2f} seconds")
    return result

# Execute on remote server
print("๐Ÿš€ Executing basic computation on remote server...")
result = basic_remote_computation(500000)

print(f"\n๐ŸŽ‰ REMOTE COMPUTATION COMPLETE")
print(f"๐Ÿ–ฅ๏ธ Executed on: {result['hostname']}")
print(f"๐Ÿ Python version: {result['python_version']}")
print(f"๐Ÿ”ข Numbers processed: {result['n']:,}")
print(f"๐Ÿ“Š Sum of squares: {result['sum_of_squares']:,}")
print(f"๐Ÿ“ Square root of sum: {result['sqrt_sum']:,.2f}")
print(f"โฑ๏ธ Execution time: {result['execution_time_seconds']:.2f} seconds")
print(f"๐Ÿ• Completed at: {result['completion_time']}")

๐Ÿ“Š Example 2: Remote Data Processing with NumPyยถ

Process numerical data on the remote server:

[ ]:
@cluster
def remote_numpy_computation(matrix_size=1000, num_iterations=5):
    """
    Perform numerical computations using NumPy on remote server.
    """
    import numpy as np
    import time
    import platform
    from datetime import datetime

    print(f"๐Ÿ–ฅ๏ธ Remote execution on: {platform.node()}")
    print(f"๐Ÿ“Š NumPy version: {np.__version__}")
    print(f"๐Ÿ”ข Matrix size: {matrix_size}x{matrix_size}")
    print(f"๐Ÿ”„ Iterations: {num_iterations}")

    results = []
    total_start_time = time.time()

    for iteration in range(num_iterations):
        print(f"\n๐Ÿ”„ Iteration {iteration + 1}/{num_iterations}")

        start_time = time.time()

        # Generate random matrices
        print("   ๐Ÿ“‹ Generating random matrices...")
        A = np.random.randn(matrix_size, matrix_size)
        B = np.random.randn(matrix_size, matrix_size)

        # Matrix multiplication
        print("   โœ–๏ธ Performing matrix multiplication...")
        C = np.dot(A, B)

        # Eigenvalue computation (smaller matrix for speed)
        small_size = min(100, matrix_size)
        print(f"   ๐Ÿงฎ Computing eigenvalues ({small_size}x{small_size})...")
        eigenvalues = np.linalg.eigvals(A[:small_size, :small_size])

        # Statistical analysis
        print("   ๐Ÿ“ˆ Computing statistics...")
        stats = {
            'matrix_mean': float(np.mean(C)),
            'matrix_std': float(np.std(C)),
            'matrix_max': float(np.max(C)),
            'matrix_min': float(np.min(C)),
            'eigenvalue_mean': float(np.mean(eigenvalues.real)),
            'eigenvalue_max': float(np.max(eigenvalues.real)),
            'frobenius_norm': float(np.linalg.norm(C, 'fro')),
        }

        end_time = time.time()
        iteration_time = end_time - start_time

        iteration_result = {
            'iteration': iteration + 1,
            'execution_time': iteration_time,
            'statistics': stats
        }

        results.append(iteration_result)
        print(f"   โฑ๏ธ Iteration completed in {iteration_time:.2f} seconds")

    total_end_time = time.time()
    total_time = total_end_time - total_start_time

    # Aggregate statistics
    execution_times = [r['execution_time'] for r in results]

    final_result = {
        'computation_info': {
            'matrix_size': matrix_size,
            'num_iterations': num_iterations,
            'hostname': platform.node(),
            'numpy_version': np.__version__,
            'completion_time': datetime.now().isoformat()
        },
        'performance': {
            'total_time': total_time,
            'average_iteration_time': np.mean(execution_times),
            'min_iteration_time': np.min(execution_times),
            'max_iteration_time': np.max(execution_times),
            'operations_per_second': (num_iterations * matrix_size * matrix_size) / total_time
        },
        'iteration_results': results
    }

    print(f"\nโœ… All computations completed!")
    print(f"โฑ๏ธ Total execution time: {total_time:.2f} seconds")
    print(f"๐Ÿ“Š Average iteration time: {np.mean(execution_times):.2f} seconds")

    return final_result

# Execute numerical computation on remote server
print("๐Ÿš€ Starting remote NumPy computation...")
numpy_result = remote_numpy_computation(matrix_size=500, num_iterations=3)

print(f"\n๐ŸŽ‰ REMOTE NUMPY COMPUTATION COMPLETE")
info = numpy_result['computation_info']
print(f"๐Ÿ–ฅ๏ธ Executed on: {info['hostname']}")
print(f"๐Ÿ“Š NumPy version: {info['numpy_version']}")
print(f"๐Ÿ”ข Matrix size: {info['matrix_size']}x{info['matrix_size']}")
print(f"๐Ÿ”„ Iterations: {info['num_iterations']}")

perf = numpy_result['performance']
print(f"\n๐Ÿ“ˆ Performance Metrics:")
print(f"   โฑ๏ธ Total time: {perf['total_time']:.2f} seconds")
print(f"   ๐Ÿ“Š Average iteration: {perf['average_iteration_time']:.2f} seconds")
print(f"   โšก Operations/second: {perf['operations_per_second']:,.0f}")
print(f"   ๐Ÿƒ Fastest iteration: {perf['min_iteration_time']:.2f} seconds")
print(f"   ๐ŸŒ Slowest iteration: {perf['max_iteration_time']:.2f} seconds")

# Show statistics from the last iteration
if numpy_result['iteration_results']:
    last_stats = numpy_result['iteration_results'][-1]['statistics']
    print(f"\n๐Ÿ“Š Final Matrix Statistics:")
    print(f"   ๐Ÿ“ˆ Mean: {last_stats['matrix_mean']:.4f}")
    print(f"   ๐Ÿ“Š Std Dev: {last_stats['matrix_std']:.4f}")
    print(f"   ๐Ÿ”บ Max: {last_stats['matrix_max']:.4f}")
    print(f"   ๐Ÿ”ป Min: {last_stats['matrix_min']:.4f}")
    print(f"   ๐Ÿงฎ Eigenvalue Mean: {last_stats['eigenvalue_mean']:.4f}")
    print(f"   ๐Ÿ“ Frobenius Norm: {last_stats['frobenius_norm']:.2f}")

๐Ÿ—‚๏ธ Example 3: Remote File System Analysisยถ

Analyze the file system structure on the remote server:

[ ]:
@cluster
def remote_system_analysis():
    """
    Analyze system information and file system on remote server.
    """
    import os
    import platform
    import shutil
    import subprocess
    import psutil  # Common on many systems
    from datetime import datetime

    print(f"๐Ÿ–ฅ๏ธ Analyzing system: {platform.node()}")

    # Basic system information
    system_info = {
        'hostname': platform.node(),
        'system': platform.system(),
        'release': platform.release(),
        'version': platform.version(),
        'machine': platform.machine(),
        'processor': platform.processor(),
        'python_version': platform.python_version(),
        'architecture': platform.architecture(),
    }

    print(f"๐Ÿ’ป System: {system_info['system']} {system_info['release']}")
    print(f"๐Ÿ—๏ธ Architecture: {system_info['machine']}")
    print(f"๐Ÿ Python: {system_info['python_version']}")

    # Memory and CPU information
    try:
        memory = psutil.virtual_memory()
        cpu_info = {
            'cpu_count': psutil.cpu_count(),
            'cpu_percent': psutil.cpu_percent(interval=1),
            'memory_total_gb': memory.total / (1024**3),
            'memory_available_gb': memory.available / (1024**3),
            'memory_percent': memory.percent,
        }
        print(f"โšก CPUs: {cpu_info['cpu_count']}")
        print(f"๐Ÿง  Memory: {cpu_info['memory_total_gb']:.1f} GB total, {cpu_info['memory_available_gb']:.1f} GB available")
    except ImportError:
        print("๐Ÿ“Š psutil not available, skipping detailed system metrics")
        cpu_info = {'error': 'psutil not available'}

    # Disk usage analysis
    disk_info = {}
    important_paths = ['/', '/home', '/tmp', '/var', '/usr']

    print("\n๐Ÿ’พ Disk Usage Analysis:")
    for path in important_paths:
        if os.path.exists(path):
            try:
                usage = shutil.disk_usage(path)
                disk_info[path] = {
                    'total_gb': usage.total / (1024**3),
                    'used_gb': usage.used / (1024**3),
                    'free_gb': usage.free / (1024**3),
                    'used_percent': (usage.used / usage.total) * 100
                }
                print(f"   ๐Ÿ“ {path}: {disk_info[path]['used_gb']:.1f}GB used / {disk_info[path]['total_gb']:.1f}GB total ({disk_info[path]['used_percent']:.1f}%)")
            except (OSError, PermissionError):
                disk_info[path] = {'error': 'Permission denied or path inaccessible'}

    # Environment analysis
    env_info = {
        'user': os.environ.get('USER', 'unknown'),
        'home': os.environ.get('HOME', 'unknown'),
        'shell': os.environ.get('SHELL', 'unknown'),
        'path_entries': len(os.environ.get('PATH', '').split(':')),
        'working_directory': os.getcwd(),
    }

    print(f"\n๐Ÿ‘ค Environment Info:")
    print(f"   User: {env_info['user']}")
    print(f"   Home: {env_info['home']}")
    print(f"   Shell: {env_info['shell']}")
    print(f"   Working Dir: {env_info['working_directory']}")

    # Available Python packages
    print("\n๐Ÿ Checking Python Environment:")
    common_packages = [
        'numpy', 'pandas', 'scipy', 'matplotlib', 'sklearn', 'requests',
        'psutil', 'jupyter', 'ipython', 'pytest', 'click', 'flask'
    ]

    package_status = {}
    for package in common_packages:
        try:
            __import__(package)
            # Try to get version
            try:
                mod = __import__(package)
                version = getattr(mod, '__version__', 'unknown')
                package_status[package] = {'available': True, 'version': version}
            except:
                package_status[package] = {'available': True, 'version': 'unknown'}
        except ImportError:
            package_status[package] = {'available': False}

    available_packages = [pkg for pkg, info in package_status.items() if info['available']]
    print(f"   โœ… Available packages ({len(available_packages)}/{len(common_packages)}): {', '.join(available_packages[:8])}")

    # Network connectivity test
    network_info = {}
    try:
        import socket
        hostname = socket.gethostname()
        ip_address = socket.gethostbyname(hostname)
        network_info = {
            'hostname': hostname,
            'ip_address': ip_address,
            'connectivity': 'basic_ok'
        }
        print(f"\n๐ŸŒ Network: {hostname} ({ip_address})")
    except Exception as e:
        network_info = {'error': str(e)}
        print(f"\n๐ŸŒ Network: Error getting network info")

    # Final analysis result
    analysis_result = {
        'analysis_metadata': {
            'timestamp': datetime.now().isoformat(),
            'analysis_type': 'remote_system_analysis'
        },
        'system_information': system_info,
        'performance_info': cpu_info,
        'disk_usage': disk_info,
        'environment': env_info,
        'python_packages': package_status,
        'network_info': network_info
    }

    print(f"\nโœ… System analysis completed!")
    return analysis_result

# Analyze remote system
print("๐Ÿš€ Starting remote system analysis...")
system_result = remote_system_analysis()

print(f"\n๐ŸŽ‰ REMOTE SYSTEM ANALYSIS COMPLETE")
sys_info = system_result['system_information']
print(f"๐Ÿ–ฅ๏ธ System: {sys_info['hostname']} ({sys_info['system']} {sys_info['release']})")
print(f"๐Ÿ—๏ธ Architecture: {sys_info['machine']}")
print(f"๐Ÿ Python: {sys_info['python_version']}")

if 'error' not in system_result['performance_info']:
    perf = system_result['performance_info']
    print(f"\n๐Ÿ“Š Performance:")
    print(f"   โšก CPUs: {perf['cpu_count']}")
    print(f"   ๐Ÿง  Memory: {perf['memory_total_gb']:.1f} GB ({perf['memory_percent']:.1f}% used)")
    print(f"   ๐Ÿ”ฅ CPU Usage: {perf['cpu_percent']:.1f}%")

env = system_result['environment']
print(f"\n๐Ÿ‘ค Environment:")
print(f"   User: {env['user']}")
print(f"   Home: {env['home']}")
print(f"   Working Dir: {env['working_directory']}")

packages = system_result['python_packages']
available = [pkg for pkg, info in packages.items() if info['available']]
print(f"\n๐Ÿ Python Environment:")
print(f"   ๐Ÿ“ฆ Available packages: {len(available)}/{len(packages)}")
print(f"   โœ… Key packages: {', '.join(available[:6])}")

disk = system_result['disk_usage']
print(f"\n๐Ÿ’พ Storage:")
for path, info in disk.items():
    if 'error' not in info:
        print(f"   ๐Ÿ“ {path}: {info['free_gb']:.1f} GB free")

๐Ÿงช Example 4: Remote Environment Testingยถ

Test specific capabilities and benchmark performance:

[ ]:
@cluster
def benchmark_remote_performance():
    """
    Benchmark computational performance on remote server.
    """
    import time
    import math
    import platform
    from datetime import datetime

    print(f"๐Ÿ Starting performance benchmarks on {platform.node()}")
    benchmarks = {}

    # CPU benchmark: Prime number calculation
    print("\n๐Ÿ”ข CPU Benchmark: Prime number calculation")
    start_time = time.time()

    def is_prime(n):
        if n < 2:
            return False
        for i in range(2, int(math.sqrt(n)) + 1):
            if n % i == 0:
                return False
        return True

    primes = [n for n in range(2, 10000) if is_prime(n)]
    cpu_time = time.time() - start_time

    benchmarks['cpu_benchmark'] = {
        'test': 'prime_calculation',
        'range': '2-10000',
        'primes_found': len(primes),
        'execution_time': cpu_time,
        'primes_per_second': len(primes) / cpu_time
    }

    print(f"   โœ… Found {len(primes)} primes in {cpu_time:.3f} seconds")
    print(f"   ๐Ÿ“Š Rate: {len(primes) / cpu_time:.1f} primes/second")

    # Memory benchmark: List operations
    print("\n๐Ÿง  Memory Benchmark: Large list operations")
    start_time = time.time()

    # Create large list
    large_list = list(range(1000000))

    # Perform operations
    reversed_list = large_list[::-1]
    sorted_sample = sorted(large_list[::1000])
    list_sum = sum(large_list[::100])

    memory_time = time.time() - start_time

    benchmarks['memory_benchmark'] = {
        'test': 'list_operations',
        'list_size': len(large_list),
        'operations': ['reverse', 'sort_sample', 'sum_subset'],
        'execution_time': memory_time,
        'sum_result': list_sum
    }

    print(f"   โœ… Processed {len(large_list):,} elements in {memory_time:.3f} seconds")
    print(f"   ๐Ÿ“Š Rate: {len(large_list) / memory_time:,.0f} elements/second")

    # I/O benchmark: File operations
    print("\n๐Ÿ“ I/O Benchmark: File read/write operations")
    import tempfile
    import os

    start_time = time.time()

    # Write test
    test_data = "\n".join([f"Line {i}: {i*i}" for i in range(10000)])

    with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
        temp_file = f.name
        f.write(test_data)

    # Read test
    with open(temp_file, 'r') as f:
        read_data = f.read()

    # Verify and cleanup
    lines_read = len(read_data.split('\n'))
    file_size = os.path.getsize(temp_file)
    os.unlink(temp_file)

    io_time = time.time() - start_time

    benchmarks['io_benchmark'] = {
        'test': 'file_read_write',
        'lines_written': 10000,
        'lines_read': lines_read,
        'file_size_bytes': file_size,
        'execution_time': io_time,
        'throughput_mb_per_sec': (file_size / (1024*1024)) / io_time
    }

    print(f"   โœ… Wrote/read {file_size:,} bytes in {io_time:.3f} seconds")
    print(f"   ๐Ÿ“Š Throughput: {(file_size / (1024*1024)) / io_time:.2f} MB/second")

    # Mathematical benchmark: Floating point operations
    print("\n๐Ÿงฎ Math Benchmark: Floating point operations")
    start_time = time.time()

    total = 0.0
    for i in range(100000):
        total += math.sin(i) * math.cos(i) + math.sqrt(i + 1)

    math_time = time.time() - start_time

    benchmarks['math_benchmark'] = {
        'test': 'trigonometric_operations',
        'operations_count': 100000 * 3,  # sin, cos, sqrt per iteration
        'result': total,
        'execution_time': math_time,
        'operations_per_second': (100000 * 3) / math_time
    }

    print(f"   โœ… Performed {100000 * 3:,} operations in {math_time:.3f} seconds")
    print(f"   ๐Ÿ“Š Rate: {(100000 * 3) / math_time:,.0f} operations/second")

    # Summary
    total_benchmark_time = sum([b['execution_time'] for b in benchmarks.values()])

    result = {
        'benchmark_metadata': {
            'hostname': platform.node(),
            'system': platform.system(),
            'machine': platform.machine(),
            'python_version': platform.python_version(),
            'timestamp': datetime.now().isoformat(),
            'total_benchmark_time': total_benchmark_time
        },
        'benchmarks': benchmarks
    }

    print(f"\n๐Ÿ All benchmarks completed!")
    print(f"โฑ๏ธ Total benchmark time: {total_benchmark_time:.3f} seconds")

    return result

# Run performance benchmarks
print("๐Ÿš€ Starting remote performance benchmarks...")
benchmark_result = benchmark_remote_performance()

print(f"\n๐ŸŽ‰ REMOTE BENCHMARKS COMPLETE")
meta = benchmark_result['benchmark_metadata']
print(f"๐Ÿ–ฅ๏ธ System: {meta['hostname']} ({meta['system']} {meta['machine']})")
print(f"๐Ÿ Python: {meta['python_version']}")
print(f"โฑ๏ธ Total time: {meta['total_benchmark_time']:.3f} seconds")

benchmarks = benchmark_result['benchmarks']

print(f"\n๐Ÿ“Š Benchmark Results:")
cpu = benchmarks['cpu_benchmark']
print(f"   ๐Ÿ”ข CPU: {cpu['primes_per_second']:.1f} primes/sec")

memory = benchmarks['memory_benchmark']
print(f"   ๐Ÿง  Memory: {len(memory['operations'])} ops on {memory['list_size']:,} elements in {memory['execution_time']:.3f}s")

io = benchmarks['io_benchmark']
print(f"   ๐Ÿ“ I/O: {io['throughput_mb_per_sec']:.2f} MB/sec throughput")

math_bench = benchmarks['math_benchmark']
print(f"   ๐Ÿงฎ Math: {math_bench['operations_per_second']:,.0f} ops/sec")

print(f"\n๐Ÿ† Remote server performance profile complete!")

๐Ÿ”ง SSH Connection Testing and Troubleshootingยถ

Test your SSH connection and get troubleshooting information:

[ ]:
def test_ssh_connection():
    """
    Test SSH connection and provide troubleshooting information.
    """
    from clustrix import get_config
    from clustrix.executor import ClusterExecutor

    try:
        print("๐Ÿ” Testing SSH connection...")
        config = get_config()

        if config.cluster_type != 'ssh':
            print("โŒ Current configuration is not for SSH.")
            print("๐Ÿ’ก Please run the SSH configuration cell above first.")
            return False

        print(f"๐ŸŽฏ Target: {config.cluster_host}:{getattr(config, 'port', 22)}")
        print(f"๐Ÿ‘ค User: {config.username}")
        print(f"๐Ÿ”‘ Key: {getattr(config, 'key_file', 'auto-detected')}")

        # Test basic connection
        executor = ClusterExecutor(config)
        executor.connect()
        print("โœ… SSH connection successful!")

        # Test basic commands
        print("\n๐Ÿงช Testing basic commands...")
        commands = [
            ("hostname", "๐Ÿ–ฅ๏ธ Remote hostname"),
            ("whoami", "๐Ÿ‘ค Remote user"),
            ("pwd", "๐Ÿ“ Working directory"),
            ("python3 --version", "๐Ÿ Python version"),
            ("uname -a", "๐Ÿ’ป System info")
        ]

        for cmd, description in commands:
            try:
                stdout, stderr = executor._execute_command(cmd)
                output = (stdout or stderr or "no output").strip()
                print(f"   โœ… {description}: {output}")
            except Exception as e:
                print(f"   โŒ {description}: {str(e)}")

        # Test work directory
        work_dir = getattr(config, 'remote_work_dir', '/tmp/clustrix')
        print(f"\n๐Ÿ“ Testing work directory: {work_dir}")
        try:
            stdout, stderr = executor._execute_command(f"mkdir -p {work_dir} && echo 'Directory OK'")
            if "Directory OK" in stdout:
                print(f"   โœ… Work directory accessible and writable")
            else:
                print(f"   โš ๏ธ Work directory test inconclusive")
        except Exception as e:
            print(f"   โŒ Work directory error: {e}")

        executor.disconnect()
        print("\n๐ŸŽ‰ SSH connection test completed successfully!")
        print("โœ… Your SSH configuration is working correctly.")
        return True

    except Exception as e:
        print(f"\nโŒ SSH connection test failed: {e}")
        print("\n๐Ÿ”ง Troubleshooting suggestions:")
        print("   1. Check hostname and port are correct")
        print("   2. Verify username is correct")
        print("   3. Test manual SSH: ssh user@hostname")
        print("   4. Check firewall and network connectivity")
        print("   5. Try force refresh: setup_ssh_keys_with_fallback(..., force_refresh=True)")
        return False

# Run connection test
print("๐Ÿ” SSH CONNECTION TEST")
print("=" * 30)
test_success = test_ssh_connection()

if test_success:
    print("\n๐Ÿš€ Ready for remote execution!")
else:
    print("\n๐Ÿ”ง Please fix SSH issues before proceeding.")

๐Ÿ“š Summary and Best Practicesยถ

๐ŸŽ‰ What Youโ€™ve Learnedยถ

  1. ๐Ÿ”‘ Automated SSH Setup: 15-second setup vs 15-30 minute manual process

  2. โš™๏ธ Remote Configuration: Easy Clustrix setup for SSH execution

  3. ๐Ÿงฎ Remote Computing: Mathematical computations on remote servers

  4. ๐Ÿ“Š Data Processing: NumPy operations and analysis remotely

  5. ๐Ÿ—‚๏ธ System Analysis: File system and environment inspection

  6. ๐Ÿ Performance Testing: Benchmarking remote server capabilities

  7. ๐Ÿ”ง Troubleshooting: Connection testing and problem resolution

๐Ÿ”’ Security Best Practicesยถ

  • โœ… Use SSH keys: Automated setup creates secure Ed25519 keys

  • โœ… Unique keys: Different keys for different servers

  • โœ… Regular rotation: Use force_refresh=True periodically

  • โœ… Secure storage: Keys stored with proper permissions (600/644)

  • โœ… Clean up: Enable cleanup_on_success=True

  • โœ… Monitor access: Check SSH logs on your servers

๐Ÿ’ก Performance Tipsยถ

  • Parallel execution: Set max_parallel_jobs appropriately

  • Work directory: Use fast storage (e.g., /tmp or SSD)

  • Environment setup: Use conda/virtualenv for package management

  • Data transfer: Minimize large data transfers between local/remote

  • Connection reuse: Clustrix automatically reuses SSH connections

๐ŸŽฏ When to Use SSH vs Other Cluster Typesยถ

Choose SSH when:

  • Working with single servers or workstations

  • Need immediate execution (no queuing)

  • Prototyping and development

  • Cloud instances (AWS, GCP, Azure)

  • Personal computing resources

Choose SLURM/PBS/SGE when:

  • Large HPC clusters with job schedulers

  • Need resource management and fair sharing

  • Production workloads with resource constraints

  • Long-running computations requiring scheduling

Choose Kubernetes when:

  • Containerized execution environments

  • Auto-scaling and fault tolerance needed

  • Cloud-native applications

  • Microservices architecture

๐Ÿš€ Next Stepsยถ

  1. Try other tutorials:

  2. Explore advanced features:

    • Multiple cluster configurations

    • Custom environment setup

    • Filesystem utilities

    • Cloud provider integrations

  3. Read documentation:

๐ŸŽŠ Congratulations!ยถ

Youโ€™ve successfully learned how to use Clustrixโ€™s automated SSH setup and remote execution capabilities. You can now:

  • โšก Set up SSH access in 15 seconds instead of 15-30 minutes

  • ๐Ÿš€ Execute Python functions on any SSH-accessible server

  • ๐Ÿ“Š Perform complex computations remotely

  • ๐Ÿ”ง Troubleshoot and optimize your setup

  • ๐Ÿ”’ Maintain security best practices

Happy remote computing! ๐ŸŽ‰